A guest loses a mount when it lists the parent of an overlay directory that shadows a real directory. For example, /tmp is an overlay directory added with krun_fs_overlay_add_dir (or krun_fs_add_overlay_dir in 1.x), the shared root also has a real tmp, and the guest mounts a tmpfs on /tmp. After one ls /, the tmpfs is gone and writes to /tmp reach the read-only root instead. We hit this with overlay directories for dev, proc, sys, tmp and workspace on a rootfs that also had all five. One ls / detached the guest's /workspace virtiofs, its /tmp tmpfs and its /proc, and getcwd in the workspace returned ENOENT.
Root cause
AugmentFs::lookup (src/devices/src/virtio/fs/augment_fs.rs:160) returns the virtual inode for an overlay name. readdir and readdirplus (augment_fs.rs:518, :534) pass straight through to the inner filesystem. The passthrough readdirplus (linux/passthrough.rs:1104; macOS at macos/passthrough.rs:1575) calls its own lookup for each name (linux/passthrough.rs:1124), so READDIRPLUS returns the real directory's node id for a name the guest cached with the virtual one.
On the guest side, fuse_direntplus_link() in fs/fuse/readdir.c treats a node id mismatch as a stale dentry:
inode = d_inode(dentry);
if (inode && get_node_id(inode) != o->nodeid)
inode = NULL;
if (!inode || fuse_stale_inode(inode, o->generation, &o->attr)) {
...
d_invalidate(dentry);d_invalidate() detaches every mount on that dentry. Once readdirplus is negotiated, the kernel always uses it for the first chunk of a listing (ctx->pos == 0), so any listing of the parent triggers this.
Fix
For a name that a virtual entry shadows, AugmentFs now reports what lookup returns:
readdirreplaces the dirent's inode and type with the virtual entry's.readdirplusdoes the same and returns the virtualEntry. It also callsinner.forget(inode, 1)for the inner lookup it replaces, which keeps the passthrough's lookup count balanced.
Offsets come from the inner filesystem unchanged, because entries are replaced one for one. Listing does not consume a one-shot name. One-shot entries have a zero entry timeout, so the guest looks the name up again before using it, and that lookup consumes it as before. Names that exist only in the overlay are still not listed; that was existing behaviour and doesn't cause this bug.
Tests
- New unit tests in
augment_fs.rsuse a fake inner filesystem. They check thatreaddirandreaddirplusreport the inode, type andEntrythatlookupreturns for a shadowed name, that other names pass through unchanged, and that the inner lookup gets forgotten. All three fail without the fix. - New integration test
augmentfs-shadowed-dir. It boots a passthrough root that has a realshadowed/directory, plus an overlay directory with the same name. The guest mounts a tmpfs on/shadowed, writes a file there, lists/and reads the file back. On unpatched main the read fails with ENOENT because the tmpfs has been detached. With the fix it passes. - Also run on Linux x86_64:
cargo fmt -- --check(root andtests/), the Linux clippy matrix fromcode-quality.yml,cargo test, and theaugmentfs,virtiofs-misc,virtiofs-root-roandroot-disk-remountintegration tests.
Reproduce
make test TEST=augmentfs-shadowed-dirWithout the first commit, this fails with listing / detached the tmpfs mounted on /shadowed.
Backport
The bug is also present on stable-1.19.x; augment_fs.rs there matches main. The fix cherry-picks with a context-only conflict in virtual_entry.rs, because 1.19's VirtualEntry has no lifetime parameter. I applied it to stable-1.19.x and ran a VMM that adds overlay directories for dev, proc, sys, tmp and workspace on a rootfs that has real dev and tmp. With unpatched 1.19.7, ls / detached /dev and /tmp. With the patched build, all mounts stayed. I'd suggest the cherry-pick-1.19.x label.