Skip to content

feat(sandbox): support file-level volume mounts#504

Merged
appcypher merged 8 commits intosuperradcompany:mainfrom
dijdzv:fix/file-volume-mount
Apr 7, 2026
Merged

feat(sandbox): support file-level volume mounts#504
appcypher merged 8 commits intosuperradcompany:mainfrom
dijdzv:fix/file-volume-mount

Conversation

@dijdzv
Copy link
Copy Markdown
Contributor

@dijdzv dijdzv commented Apr 4, 2026

Summary

Closes #247

If this is already being worked on, apologies — happy for this to serve as one possible approach rather than a definitive solution.

virtio-fs only supports directory-level sharing. When a user mounts a
single file (--volume /host/file.txt:/guest/file.txt), PassthroughFs
fails with ENOTDIR. This PR adds transparent file mount support by
staging each file in an isolated directory (hard-linked to the original)
and using a guest-side bind mount to place it at the intended path.

Approach

Same pattern as apple/containerization#487:
staging dir + hardlink + virtiofs share + bind mount.

  • Host (spawn.rs): Stage file bind mounts in isolated directories.
    sandbox_cli_args() generates --mount args and MSB_FILE_MOUNTS
    env var based on the staging results.
  • Guest (agentd/init.rs): Parse MSB_FILE_MOUNTS, mount virtiofs
    at staging point, bind-mount file to guest path.
  • Protocol (protocol/lib.rs): Add ENV_FILE_MOUNTS / FILE_MOUNTS_DIR.
    Rename ENV_MOUNTSENV_DIR_MOUNTS ("MSB_DIR_MOUNTS") for clarity.

The user API is unchanged — .volume(guest, |m| m.bind(host)) works
transparently for both files and directories.

Limitations

Test plan

  • parse_file_mount_entry unit tests
  • sandbox_cli_args file mount generation tests
  • Existing tests updated for new function signatures and pass

Integration (manual):

# Read
echo "hello" > /tmp/test.txt
msb run --volume /tmp/test.txt:/root/test.txt alpine -- cat /root/test.txt
# Expected: hello

# Write-back
echo "original" > /tmp/test.txt
msb run --volume /tmp/test.txt:/root/test.txt alpine -- sh -c 'echo "modified" > /root/test.txt'
cat /tmp/test.txt
# Expected: modified

@dijdzv
Copy link
Copy Markdown
Contributor Author

dijdzv commented Apr 5, 2026

Security testing from guest side

I tested the following scenarios to verify the guest cannot perform unintended operations through file mounts.
I'm not confident in the coverage.

# Test 1: Only mounted file visible in staging dir (no adjacent files)
echo "secret" > /tmp/mount-test.txt
echo "adjacent" > /tmp/adjacent-file.txt
msb run --volume /tmp/mount-test.txt:/root/test.txt alpine -- ls /.msb/file-mounts/fm_root_test.txt/
# Expected: init.krun and mount-test.txt only. adjacent-file.txt must NOT appear.

# Test 2: Cannot escape staging dir via ../
msb run --volume /tmp/mount-test.txt:/root/test.txt alpine -- cat /.msb/file-mounts/fm_root_test.txt/../../etc/hostname
# Expected: shows guest hostname, NOT host files (stays within guest filesystem)

# Test 3: Guest chmod does not affect host permissions
chmod 600 /tmp/mount-test.txt
msb run --volume /tmp/mount-test.txt:/root/test.txt alpine -- chmod 777 /root/test.txt
ls -la /tmp/mount-test.txt
# Expected: still -rw------- (600), not 777

All passed on my environment. Readonly bypass (guest mount -o remount,rw) could not be tested since CLI :ro flag is not yet supported.

@appcypher
Copy link
Copy Markdown
Member

appcypher commented Apr 7, 2026

Thanks again for the contribution and sorry for the delay in getting back to you.

I was going to work on this myself and had slightly different plans but your implementation mostly looks good to me. There are 3 issues that need addressing.

[1] File mount staging can leak previously mounted files

The staging directory at $SANDBOX_DIR/file-mounts/fm_<tag>/ is deterministically named based on the guest path.

But when linking a new file that mounts to the same guest path, the code attempts to remove the file, making the wrong assumption that the file will always be named the same for the same guest path.

Run 1: mount `/host/config-v1.toml` → `/etc/app.conf`
    - staging dir `fm_etc_app.conf/config-v1.toml`

Run 2: mount `/host/config-v2.toml` → `/etc/app.conf`
    - removes `fm_etc_app.conf/config-v2.toml` // <- issue is here
    - links `config-v2.toml`
    - `config-v1.toml` is still sitting there!

On the guest side, agentd mounts the entire staging directory as a virtiofs share at /.msb/file-mounts/<tag>/, so both config-v1.toml and config-v2.toml are browsable
there.

[2] Tag collision from path mangling

guest_mount_tag derives the virtiofs tag by replacing / with _ and stripping leading underscores. For example, /etc/app and /etc_app both yield etc_app. A collision means two different file mounts would share the same staging directory, overwriting or exposing each other's files.

[3] Staging directory remains visible in guest

After mount_file() bind-mounts the specific file to the guest path, the virtiofs share at /.msb/file-mounts/<tag>/ is never unmounted. The staging directory remains browsable, providing an alternate access path to the mounted file.

Fix

The fixes would be to generate a new tempdir for each file mount and to use a random tag instead of one derived from the guest path. The tempdir ensures that each file mount gets a clean staging directory, eliminating leakage. The random tag eliminates collisions. Finally, unmounting the virtiofs share after the bind mount would remove the staging directory from the guest namespace entirely, preventing any alternate access paths.

I'm writing a patch to fix these issues now.

Replace persistent staging directories with ephemeral TempDir to
prevent stale file leakage across sandbox restarts. Generate random
virtiofs tags (fm_{hex}) instead of deriving from guest paths to
eliminate tag collisions. Unmount staging virtiofs share inside the
guest after bind mount succeeds to remove alternate access paths.
@appcypher appcypher merged commit 59d0c47 into superradcompany:main Apr 7, 2026
10 checks passed
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.

Volume mounting a file results in guest seeing i/o errors

2 participants