Skip to content

Never pad a file that shrank under a mount with zeros - #186

Merged
lydakis merged 7 commits into
mainfrom
claude/project-thread-dorvpr
Oct 28, 2026
Merged

lydakis merged 7 commits into
mainfrom
claude/project-thread-dorvpr

Conversation

@lydakis

@lydakis lydakis commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Requested by George · project thread

What changed

A mount of a folder lists each file once and then reads its bytes live from the folder. If a file got shorter in the folder after that, a read came back short of the listed size. A copy onto the file does this, for example, because it truncates the file first. The NFS READ reply then said eof = false and carried the old cached size, so macOS's NFS client filled the rest with zeros. In George's evaluation, a cp from a Sonny task's mount onto the mount's own folder zeroed seven real files this way.

  • NFS read (nfs.rs): when a read comes back short of the cached size, the mount asks the source about that one file again. If the file changed, that read fails with NFS3ERR_STALE.
    • Why it fails rather than ending early: the macOS client won't take a smaller size from a READ reply. In xnu, READ post-op attributes are loaded with dontshrink. CI showed that eof = true plus fresh attributes still gave zeros.
    • The file also gets a new inode. Every name of the file, hard links included, is taken off the old inode, and its folder's modified time moves on. The client then looks the name up again, gets the new inode with the new size, and reads the file as it is now. Reads through the old handle keep failing.
    • nfsproc3_read in the vendored nfsserve now takes the attributes after the read.
    • A read that comes back short of a size that hasn't changed is simply the file's end.
  • FUSE read (fs.rs): the same recheck runs on a short read, so a later stat agrees with what the kernel saw. Before, stat kept the old size for the attribute TTL. FUSE keeps the inode, since the kernel takes the new size from attributes.
  • Core: Workspace::recheck(path) replaces the cached entry and cached bytes of one base file with what the source says now.
    • This covers files the workspace only chmodded and hard links made here, which hold their own copy of the source's size and time.
    • The listing and the path-entry cache are updated together under that listing's lock, so concurrent rechecks don't undo each other.
    • The rest of the directory stays as first listed, and files the workspace wrote are left alone.
    • Source::entry_now(key) is new. Its default returns None, which changes nothing. DirSource implements it with one lstat, after checking that the file's folder still resolves inside the root.
  • Errors: errors from DirSource now name the path (source error: PATH: Permission denied). The evaluation's daemon log had 140 of these with no path.

What this doesn't fix:

  • Apart from this end-of-file case, a live mount still shows a file as it was when first listed.
  • A copy from a mount onto its own folder still truncates the file first. On macOS it now fails with "Stale NFS file handle" and reads correctly within about a second, instead of writing zeros.

Frozen copies on the source's disk (next PRs) fix both, along with briefs that name mount paths only.

API changes

  • sandfs-core: new trait method Source::entry_now, with a default so existing sources keep compiling, and new Workspace::recheck.
  • sandfs: no changes to the public API. The Python API is unchanged.

Tests

  • cargo test -p sandfs-core --test workspace recheck:
    • a shrunk file is taken as it is now, including a chmodded file (still executable) and a hard link;
    • other files stay as listed;
    • a written file is left alone;
    • forks share the answer;
    • a folder swapped for a link that points outside is refused. This last check fails without the confinement fix.
  • cargo test -p sandfs --lib shrank: an NFS read of a 24-byte file shrunk to 3 bytes fails, keeps failing through the old id, the folder's time moves, and a fresh lookup gives size 3 and reads "new". With a hard link, both names look up to the same new inode.
  • SANDFS_REQUIRE_FUSE=1 cargo test -p sandfs-cli --test mount shrank: an end-to-end mount test.
    • It never accepts zeros.
    • On macOS it allows failed reads before the correct one.
    • On Linux it failed before this change, because stat stayed at 24, and passes after.
  • cargo test --workspace passes here, except the two sandfs shell tests that need bubblewrap, which isn't installed in this container.

🤖 Generated with Claude Code

https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK

…eros

A mount of a folder lists each file once and reads its bytes live. A file
that shrank in the folder since (a copy onto it truncates it first) read
short of its listed size, and macOS's NFS client padded the rest with
zeros, so copying from a mount onto its own folder wrote zeros into it.

A short read is now the file's end, and that one file's entry is asked
again (Source::entry_now, Workspace::recheck), so the NFS reply, whose
attributes are now taken after the read, and later stats carry the size
the folder has now. Errors from a folder name the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK
@lydakis lydakis self-assigned this Oct 27, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T00:36:44.494909Z 9e2f38b New commits
🔒 Security Review ✅ Completed 2026-10-28T00:38:08.111717Z 9e2f38b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8922bc7e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/sandfs-core/src/workspace.rs Resolved
Comment thread crates/sandfs-core/src/source.rs Resolved
…macOS

The macOS NFS client ignores a smaller size in READ post-op attributes, so
it zero-filled the rest of the read. The read now fails once with a stale
handle error carrying the new size, and the next read sees the file as it
is. Rechecked entries are merged into the current listing under its lock,
and DirSource::entry_now checks the file's folder stays inside the root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK
@lydakis lydakis changed the title Read a file that shrank under a mount to its end, never padded with zeros Never pad a file that shrank under a mount with zeros Oct 27, 2026
The macOS client retried the failed read and still zero-filled it. The
recheck now drops the file's inode and bumps its folder's time, as an
update does for changed files, so reads through the old handle keep
failing and a new lookup gets the size now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 446ff5c679

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/sandfs-core/src/workspace.rs Resolved
A chmod puts the file's source record, with its size and time, in the
overlay, which a recheck of the listing alone left stale. Workspace::recheck
now takes &mut self and refreshes that record too, keeping the mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38760a2135

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/sandfs-core/src/workspace.rs Resolved
A hard-linked base file keeps its source record in the workspace's links,
which the recheck now refreshes too, through either name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9ef999cfb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/sandfs/src/fs.rs Resolved
With a hard link, the recheck moved the inode to the other name, so the
old handle kept working and kept the client's old size. Every name now
leaves the inode at once, each folder gets a new time, and the inode goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2bfc95ca3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/sandfs-core/src/workspace.rs Resolved
Two rechecks of one file racing could leave the listing with one answer
and the path cache with the other; the path cache is now written while the
listing's lock is held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGETmgGUb8hcSEpppQK
@lydakis
lydakis merged commit dde4ba0 into main Oct 28, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants