Skip to content

fix: dynamic root ModTime for NFS cache invalidation#67

Merged
jamestexas merged 5 commits intomainfrom
fix/nfs-root-dynamic-mtime
Mar 5, 2026
Merged

fix: dynamic root ModTime for NFS cache invalidation#67
jamestexas merged 5 commits intomainfrom
fix/nfs-root-dynamic-mtime

Conversation

@jamestexas
Copy link
Copy Markdown
Contributor

Summary

  • GraphFS.Lstat("/") returned a fixed mountTime, causing macOS NFS clients to cache the initial directory listing even with noac
  • After HotSwapGraph.Swap(), client saw same mtime β†’ served stale cached (empty) listing
  • Now queries graph root node for ModTime β€” CompositeGraph returns time.Now(), forcing NFS client cache invalidation

Context

x-ray mounts an NFS filesystem backed by a HotSwapGraph that starts empty and gets swapped to a CompositeGraph when a browser tab connects. The macOS NFS client cached the initial empty listing and never refreshed.

Test plan

  • go test ./internal/nfsmount/ passes
  • Manual: XRAY_NFS_MOUNT=true agentd β†’ ls /tmp/xray-mache/ shows browser/, iterm/, etc. after tab connects

πŸ€– Generated with Claude Code

GraphFS.Lstat("/") returned a fixed mountTime, causing macOS NFS clients
to cache the initial (possibly empty) directory listing even with noac.
After HotSwapGraph.Swap, the client saw the same mtime and served the
stale cached listing.

Now Lstat("/") queries the graph's root node for ModTime. CompositeGraph
returns time.Now(), so NFS clients see a changed mtime and re-read the
directory. MemoryStore returns zero ModTime, falling back to mountTime.
Reproduces the x-ray NFS scenario:
- HotSwapGraph starts with empty MemoryStore
- Swap to CompositeGraph with mounted sub-graphs
- Verify ReadDir("/") returns composite mount names after Swap
- Verify root Lstat returns dynamic mtime from CompositeGraph
- Verify sub-graph content is readable through the full chain
staticFileInfo.Sys() returned *syscall.Stat_t{Ino: 0} for every file,
causing ALL NFS entries to share Fileid=0. macOS NFS client uses Fileid
as inode numbers β€” duplicate values break directory listings entirely.

Now Sys() returns nil, making go-nfs fall back to FNV hash of the full
path for unique Fileid values. Tradeoff: uid/gid default to 0 (root
ownership), but directory listings actually work.
All staticFileInfo construction now goes through newFileInfo(fullPath, ...)
which auto-computes ino from FNV hash. This:
- Fixes Fileid=0 bug that broke macOS NFS directory listings
- Preserves uid/gid in Sys() (reverts nil-Sys approach)
- Makes it impossible to forget ino on new virtual file sites

Adds TestUniqueFileids to catch regressions.
A mounted sub-graph that delegates back to the CompositeGraph (e.g.
x-ray's focus.Router) caused infinite recursion β†’ stack overflow.

Add atomic depth counter (maxCallerDepth=2) to short-circuit the cycle.
Test confirms: without guard β†’ stack overflow, with guard β†’ returns cleanly.
@jamestexas jamestexas merged commit 7be09bd into main Mar 5, 2026
13 checks passed
@jamestexas jamestexas deleted the fix/nfs-root-dynamic-mtime branch March 5, 2026 23:32
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.

1 participant