fix(sandbox): apply same write guard to move_entry cross-device copy path

The write_atomic fix in the previous commit left an identical unguarded
write loop in move_entry's cross-device fallback (EXDEV path). When a
rename spans different mount roots, move_entry copies the file chunk-by-
chunk into a temp file before replacing the destination. On fakeowner or
network mounts the same silent data loss can occur: os.write() reports
success but discards data, os.replace() then commits an empty temp file
as the destination while the source is unlinked.

Apply the same two-layer guard:
- Per-chunk short-write check (written != len(chunk)) → OSError(EIO)
- Post-fsync fstat size comparison (st_size != total_written) → OSError(EIO)

The os.replace() and os.unlink(src) are only reached after both checks
pass, preserving the source file on any write failure.
This commit is contained in:
Cursor Agent 2026-03-13 06:38:30 +00:00
parent 940ff4235c
commit f991f93370
No known key found for this signature in database

View File

@ -170,16 +170,24 @@ export const SANDBOX_PINNED_MUTATION_PYTHON = [
" temp_name = None",
" try:",
" temp_name, temp_fd = create_temp_file(dst_parent_fd, dst_basename)",
" total_written = 0",
" while True:",
" chunk = os.read(src_fd, 65536)",
" if not chunk:",
" break",
" os.write(temp_fd, chunk)",
" written = os.write(temp_fd, chunk)",
" if written != len(chunk):",
" raise OSError(errno.EIO, 'short write to sandbox temp file: wrote ' + str(written) + ' of ' + str(len(chunk)) + ' bytes (fakeowner or network fs may be dropping writes)', dst_basename)",
" total_written += written",
" try:",
" os.fchmod(temp_fd, stat.S_IMODE(src_stat.st_mode))",
" except AttributeError:",
" pass",
" os.fsync(temp_fd)",
" # Verify bytes were actually persisted — fakeowner/network mounts can silently drop writes.",
" stat_result = os.fstat(temp_fd)",
" if stat_result.st_size != total_written:",
" raise OSError(errno.EIO, 'sandbox temp file size mismatch after write: expected ' + str(total_written) + ' bytes but got ' + str(stat_result.st_size) + ' (fakeowner or network fs may be silently dropping writes)', dst_basename)",
" os.close(temp_fd)",
" temp_fd = None",
" os.replace(temp_name, dst_basename, src_dir_fd=dst_parent_fd, dst_dir_fd=dst_parent_fd)",