#950

virtio/fs: list shadowed names with the identity lookup returns

OpenMouhand-Kaddowants to merge intofrom

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:

  • readdir replaces the dirent's inode and type with the virtual entry's.
  • readdirplus does the same and returns the virtual Entry. It also calls inner.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.rs use a fake inner filesystem. They check that readdir and readdirplus report the inode, type and Entry that lookup returns 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 real shadowed/ 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 and tests/), the Linux clippy matrix from code-quality.yml, cargo test, and the augmentfs, virtiofs-misc, virtiofs-root-ro and root-disk-remount integration tests.

Reproduce

make test TEST=augmentfs-shadowed-dir

Without 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.