fix(sandbox): detect silent data loss in write_atomic on fakeowner/network mounts

On fakeowner-type mounts (macOS sandbox overlays, some NFS/SMB setups),
os.write() may return the requested byte count but silently discard the
data. The existing write_atomic helper wrote the temp file, ran os.fsync(),
then did os.replace() — so the destination file was atomically replaced
with an empty (or truncated) file with a new inode.

Fix: after writing all chunks, compare os.fstat(temp_fd).st_size against
the total bytes written. A mismatch (size < bytes_written) now raises
OSError(EIO) before os.replace() is called, so the original file is
never touched and the caller receives a clear error instead of silent
data loss.

Also guards the individual os.write() calls: if the kernel returns fewer
bytes than requested (short write) the same EIO is raised immediately.

Fixes #44657
This commit is contained in:
Cursor Agent 2026-03-13 05:47:59 +00:00
parent a0f09a4589
commit 940ff4235c
No known key found for this signature in database

View File

@ -87,12 +87,21 @@ export const SANDBOX_PINNED_MUTATION_PYTHON = [
" temp_name = None",
" try:",
" temp_name, temp_fd = create_temp_file(parent_fd, basename)",
" total_written = 0",
" while True:",
" chunk = stdin_buffer.read(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)', basename)",
" total_written += written",
" os.fsync(temp_fd)",
" # Verify the kernel flushed the correct number of bytes.",
" # On fakeowner/network mounts, os.write can return success but discard data.",
" 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)', basename)",
" os.close(temp_fd)",
" temp_fd = None",
" os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)",