From 940ff4235c1a75b9d1d8136e8ae413f5afb22fe6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 05:47:59 +0000 Subject: [PATCH 1/3] fix(sandbox): detect silent data loss in write_atomic on fakeowner/network mounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/agents/sandbox/fs-bridge-mutation-helper.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/agents/sandbox/fs-bridge-mutation-helper.ts b/src/agents/sandbox/fs-bridge-mutation-helper.ts index 3c6edb2c2cb..52942e45a84 100644 --- a/src/agents/sandbox/fs-bridge-mutation-helper.ts +++ b/src/agents/sandbox/fs-bridge-mutation-helper.ts @@ -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)", From f991f93370398c3c888478b237b9538f31811e97 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 06:38:30 +0000 Subject: [PATCH 2/3] fix(sandbox): apply same write guard to move_entry cross-device copy path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/agents/sandbox/fs-bridge-mutation-helper.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/agents/sandbox/fs-bridge-mutation-helper.ts b/src/agents/sandbox/fs-bridge-mutation-helper.ts index 52942e45a84..39372dfd586 100644 --- a/src/agents/sandbox/fs-bridge-mutation-helper.ts +++ b/src/agents/sandbox/fs-bridge-mutation-helper.ts @@ -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)", From 49a5b3ff692f784d4655afa35cb71ecd9faa93a4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 19 Mar 2026 02:11:27 +0000 Subject: [PATCH 3/3] fix(sandbox): rollback EXDEV dir move on size-check failure to preserve source tree --- src/agents/sandbox/fs-bridge-mutation-helper.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/agents/sandbox/fs-bridge-mutation-helper.ts b/src/agents/sandbox/fs-bridge-mutation-helper.ts index 39372dfd586..19beb8bad26 100644 --- a/src/agents/sandbox/fs-bridge-mutation-helper.ts +++ b/src/agents/sandbox/fs-bridge-mutation-helper.ts @@ -146,6 +146,14 @@ export const SANDBOX_PINNED_MUTATION_PYTHON = [ " try:", " for child in os.listdir(src_dir_fd):", " move_entry(src_dir_fd, child, temp_dir_fd, child)", + " except Exception:", + " # Rollback: move children from temp_dir back to source to preserve original tree.", + " # Without this, a size-check failure on a later child leaves earlier children stranded", + " # in the hidden temp dir and the source partially deleted.", + " for child in os.listdir(temp_dir_fd):", + " move_entry(temp_dir_fd, child, src_dir_fd, child)", + " remove_tree(dst_parent_fd, temp_dir_name)", + " raise", " finally:", " os.close(src_dir_fd)", " os.close(temp_dir_fd)",