Skip to content

Bind mounts leak host uid/gid into guest, blocking non-root workflows #741

@ariofrio

Description

@ariofrio

On a bind-mounted volume, files owned on the host by the user who ran msb surface inside the guest with the raw host uid (e.g. 501), not a guest-meaningful identity. Non-root guest processes can't read/write/delete those files, and the usual chown workaround inside the guest doesn't stick for files the host creates afterwards.

Related: #707, #667 (both touch the same vm.rs bind-mount config path); #456 (where --user was added); #390 (passthrough fs origin).

Description

  • The passthrough FS overlays a 20-byte user.virtiofs.override xattr on every stat reply (patched_stat). The xattr supplies the guest-visible uid/gid/mode.
  • Files that have no override xattr fall through patched_stat to the raw host stat — so the guest sees the host uid/gid directly.
  • New host-created files never get an xattr, so they always show up in the guest with the msb-process uid.
  • do_setattr writes uid/gid changes into the xattr only — it never calls real fchown (FS server lacks CAP_CHOWN). So in-guest chown is xattr-only too.
  • That means the common workaround — chown -R $guest_uid /mount from inside the guest — sticks only for the files that already exist at chown time. Anything the host writes after that surfaces as the host uid again.
  • --user (the guest user identity) is resolved against the guest's own /etc/passwd by agentd inside the guest; the host has no idea what numeric uid e.g. --user appuser will be until the guest tells it. So there's currently no plumbing for the FS server to know what guest identity to map onto.

Reproduction

# host (macOS, uid 501)
mkdir ~/bind-demo && echo hello > ~/bind-demo/host.txt
stat -f '%Sp uid=%u(%Su) gid=%g(%Sg) %N' ~/bind-demo/host.txt

msb run ubuntu --volume ~/bind-demo:/work --user 1000:1000 -- sh -c \
  'id; ls -la /work; (echo append >> /work/host.txt) 2>&1; echo exit=$?'

Actual Behavior

-rw-r--r-- uid=501(ariofrio) gid=20(staff) /Users/ariofrio/bind-demo/host.txt
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),...
drwxr-xr-x+ 3 501 dialout 96 /work       # raw host uid leaks; gid 20 maps to ubuntu's "dialout"
-rw-r--r--+ 1 501 dialout  6 /work/host.txt
sh: 1: cannot create /work/host.txt: Permission denied
exit=2

Same thing happens for the chown workaround: in-guest chown -R 1000:1000 /work sticks for existing files (xattr is set), but any file the host creates after the chown shows up in the next guest session as uid 501 again — because the new file has no xattr and patched_stat falls through to the raw host stat.

Expected Behavior

uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),...
drwxr-xr-x+ 3 ubuntu ubuntu  96 /work    # appears as the sandbox user
-rw-r--r--+ 1 ubuntu ubuntu   6 /work/host.txt
exit=0                                    # write succeeds

Non-root guest workflows over a bind mount should work without requiring the user to manually align identities. Host-side file edits should remain accessible from the guest as the sandbox's --user identity.

Workaround

# either run the guest as root (defeats the purpose of --user)
msb run ubuntu --volume ~/bind-demo:/work -- ...

# or align the host uid to the guest uid before launching
# (only practical if you control the host setup)

In-guest chown -R $guest_uid /work only fixes already-existing files. New host writes still leak through as host uid.

Relevant Code

  • crates/filesystem/lib/backends/shared/stat_override.rs:63-110patched_stat overlays the xattr; for files without an xattr it returns the raw host stat (this is where the leak surfaces).
  • crates/filesystem/lib/backends/passthroughfs/metadata.rs:94-128do_setattr's uid/gid/mode branch writes only to the xattr; comments at lines 10-12 confirm fchown/fchmod are deliberately not used (no CAP_CHOWN).
  • crates/filesystem/lib/backends/passthroughfs/inode.rs:962-1003stat_inode is the chokepoint; every guest-visible stat funnels through it into patched_stat. A change here covers do_getattr, do_setattr's return, do_access, and the lookup path.
  • crates/filesystem/lib/backends/passthroughfs/metadata.rs:174-216do_access reads from stat_inode, so any uid/gid surfaced by patched_stat already participates in the in-guest permission check.
  • crates/agentd/lib/session.rs:554-605resolve_user_spec resolves --user via getpwnam_r against the guest's /etc/passwd; runs inside the guest at exec time.
  • crates/agentd/lib/init.rs:20-44init::init runs as PID 1 inside the guest; apply_dir_mounts at line 27 is where bind mounts are actually attached to the guest namespace (so the bind mount isn't visible to any guest process until agentd has run).
  • crates/agentd/bin/main.rs:29-83 — agentd reads MSB_USER from env on startup, runs init::init synchronously in Phase 1, then opens the virtio-serial control channel in Phase 2.
  • crates/agentd/lib/agent.rs:60-107 — virtio-serial port is opened in Phase 2; the core.ready payload is sent over it.
  • crates/runtime/lib/vm.rs:658-675 — bind-mount PassthroughFs backends are constructed here. Only these (not rootfs/runtime mounts) would need a mapping handle.

Acceptance Criteria

  • Host files owned by the msb-process uid appear inside the guest as the sandbox's resolved --user identity, without requiring any in-guest chown.
  • Host files owned by other uids appear inside the guest as the overflow uid (65534).
  • Files with an existing override xattr still surface with the xattr's uid/gid (mapping is fallback-only, takes lower precedence).
  • A non-root guest (--user 1000:1000) can read/write/delete host-created files in a bind mount when host mode permits.
  • No regression for rootfs / runtime / non-bind-mount PassthroughFs instances (which legitimately need raw passthrough of identity from inside an OCI image).
  • Test coverage: bind-mount write from non-root guest succeeds against a host-owned target.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions