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-110 — patched_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-128 — do_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-1003 — stat_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-216 — do_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-605 — resolve_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-44 — init::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
On a bind-mounted volume, files owned on the host by the user who ran
msbsurface 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 usualchownworkaround inside the guest doesn't stick for files the host creates afterwards.Related: #707, #667 (both touch the same
vm.rsbind-mount config path); #456 (where--userwas added); #390 (passthrough fs origin).Description
user.virtiofs.overridexattr on every stat reply (patched_stat). The xattr supplies the guest-visible uid/gid/mode.patched_statto the raw host stat — so the guest sees the host uid/gid directly.do_setattrwrites uid/gid changes into the xattr only — it never calls realfchown(FS server lacksCAP_CHOWN). So in-guestchownis xattr-only too.chown -R $guest_uid /mountfrom 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/passwdbyagentdinside the guest; the host has no idea what numeric uid e.g.--user appuserwill 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
Actual Behavior
Same thing happens for the
chownworkaround: in-guestchown -R 1000:1000 /worksticks 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 andpatched_statfalls through to the raw host stat.Expected Behavior
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
--useridentity.Workaround
In-guest
chown -R $guest_uid /workonly fixes already-existing files. New host writes still leak through as host uid.Relevant Code
crates/filesystem/lib/backends/shared/stat_override.rs:63-110—patched_statoverlays 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-128—do_setattr's uid/gid/mode branch writes only to the xattr; comments at lines 10-12 confirmfchown/fchmodare deliberately not used (noCAP_CHOWN).crates/filesystem/lib/backends/passthroughfs/inode.rs:962-1003—stat_inodeis the chokepoint; every guest-visible stat funnels through it intopatched_stat. A change here coversdo_getattr,do_setattr's return,do_access, and the lookup path.crates/filesystem/lib/backends/passthroughfs/metadata.rs:174-216—do_accessreads fromstat_inode, so any uid/gid surfaced bypatched_statalready participates in the in-guest permission check.crates/agentd/lib/session.rs:554-605—resolve_user_specresolves--userviagetpwnam_ragainst the guest's/etc/passwd; runs inside the guest at exec time.crates/agentd/lib/init.rs:20-44—init::initruns as PID 1 inside the guest;apply_dir_mountsat 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 readsMSB_USERfrom env on startup, runsinit::initsynchronously 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; thecore.readypayload is sent over it.crates/runtime/lib/vm.rs:658-675— bind-mountPassthroughFsbackends are constructed here. Only these (not rootfs/runtime mounts) would need a mapping handle.Acceptance Criteria
--useridentity, without requiring any in-guestchown.--user 1000:1000) can read/write/delete host-created files in a bind mount when host mode permits.PassthroughFsinstances (which legitimately need raw passthrough of identity from inside an OCI image).