Skip to content

A read-only map is mounted read-only in the guest and follows the host's files - #1045

Merged
ejc3 merged 4 commits into
mainfrom
read-only-map-follows-host
Oct 2, 2026
Merged

ejc3 merged 4 commits into
mainfrom
read-only-map-follows-host

Conversation

@ejc3

@ejc3 ejc3 commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

A --map HOST:GUEST:ro volume is now mounted read-only in the guest and without the FUSE writeback cache, so the guest follows the host's files. This closes the read-only half of #1041 and the first item of #1042.

Stacked on: main (f7fe35c). Four commits: the change, the repairs from its review, and two for the check before boot: 4d39a47 folded .. in a guest path, and 6c5f1bb replaces that with refusing such a path in a run that has a read-only map, because where .. leads depends on symlinks in the guest.

The problem

Measured on main at f7fe35c (the issues hold the probes):

  • With the writeback cache the guest's kernel keeps the size and mtime it first cached for a regular file and drops the server's. A file the host rewrote longer in place was read cut off at the old length: 8 of 92 bytes in a running VM, 8 of 134 in a clone restored from a snapshot taken before the rewrite. One rewritten shorter kept its old length, padded with zero bytes. With --portable-volumes a file replaced by rename kept its old size and mtime in the clone.
  • A :ro map was mounted rw in the guest. fcvm exec --vm -- sh -c 'echo x > /mapro/f' exited 0 and created the file on the host. Only the container's bind mount was read-only.
  • No test caught it: test_remap_fs_host_file_changes and test_remap_fs_snapshot_file_replace each replace a file with one of the same length, and no test wrote to a :ro map from the guest OS.

The change

  • fuse_pipe::MountSettings::for_volume(read_only, no_writeback_cache) is the one decision. A read-only volume gets MS_RDONLY and never asks for FUSE_WRITEBACK_CACHE. A read-write volume is mounted as before. The Unix socket path and the vsock path share one session setup.
  • fc-agent passes each volume's read_only to the mount, and mounts an outer volume before a volume inside it. Before, every mount started at once and an outer mount that landed second covered the inner one.
  • A mount point inside a read-only map has to exist in the map's host directory. fcvm checks that before every boot: in podman run and podman prepare before anything is set up and before the snapshot cache is consulted, and before each relaunch after a guest reboot. The image store of a localhost/ image in overlay mode (/mnt/image-store) counts as a mount point. The error names both arguments and the missing directory.
  • FCVM_NO_WRITEBACK_CACHE no longer changes the snapshot key or the kernel command line of a run with no read-write map, where it now changes nothing.
  • fc-agent's fatal error line carries its whole cause chain, so fcvm's log at INFO says which path failed.
  • README.md and DESIGN.md say what :ro means in the guest, which runs are checked, what the host may change under a read-write map, and that the host must not replace a directory another mount sits on.

Contract and downstream impact

A consumer that maps host directories :ro and restores clones hours later (the www VM maps eleven) reads the host's current files in a running VM and in a clone. A snapshot made by an older fc-agent keeps its old mounts until it is made again; the snapshot cache key already separates them through the initrd path, so no key ingredient is added. Read-write maps are unchanged.

Evidence

Red on f7fe35c plus the new VM tests:

make test-all FILTER="-p fcvm --test test_read_only_map"
Summary [ 102.022s] 3 tests run: 0 passed, 3 failed, 0 skipped
grows.txt: host size=92 read 92 bytes; guest size=8 read 8 bytes
shrinks.txt: host size=20; guest size=200 read 200 bytes
the guest's mount table has /mnt/ro as rw,relatime
creating a file from the guest OS did not fail with EROFS: "status=0"

Red for the repairs, each test with its item taken back:

Summary [   5.033s] 12 tests run: 7 passed, 5 failed, 1387 skipped
a_group_of_volumes_is_ready_before_the_next_group_starts
  left: ["start /a", "start /a/b", "ready /a", "ready /a/b"]
the_fatal_error_line_carries_the_whole_cause_chain
  left: "[fc-agent] Error: mounting FUSE volumes"
a_read_only_map_around_the_image_store_is_refused_when_the_run_mounts_one
no_writeback_cache_does_not_fragment_keys_of_runs_with_only_read_only_maps
every_boot_has_its_maps_checked_first

Green at the head:

make build   # exit 0
make lint    # exit 0
make test-unit
Summary [  71.318s] 1399 tests run: 1399 passed (1 slow, 2 flaky), 0 skipped
make test-all, one file at a time
test_read_only_map          5 tests run: 5 passed
test_portable_volumes       8 tests run: 8 passed
test_volume_coherency       2 tests run: 2 passed
test_fuse_snapshot_matrix   8 tests run: 8 passed
test_sanity test_rootless_map_nonroot_reader          1 passed
test_snapshot_clone test_async_pf_inside_nmi_...      1 passed

The two flaky tests are in src/uffd/server.rs, which this change does not touch. Each failed its first try and passed its second on a host whose load average was above 200. The unit run predates a clippy allow on one function.

Not in this PR

Not run locally: make test-root, the bridged tests, the arm64 nested tests.

Summary by CodeRabbit

  • New Features
    • :ro volume maps are mounted read-only in the guest, and host file updates—including replacements—become visible while VMs are running and after snapshot restoration.
    • Snapshot-restored VMs retain volume mounts and their settings. Nested writable maps remain writable within read-only maps.
    • Startup checks now catch missing host directories required by mount points inside read-only maps.
  • Bug Fixes
    • Read-only maps no longer use writeback caching; writes from the guest fail with a read-only filesystem error. Read-write maps retain their existing caching behavior.
  • Documentation
    • Clarified map permissions, caching, host file updates, and snapshot behavior.

ejc3 added 2 commits October 1, 2026 11:02
A --map HOST:GUEST:ro volume was mounted in the guest like a read-write
one: writable, and with FUSE_WRITEBACK_CACHE unless the VM-wide
FCVM_NO_WRITEBACK_CACHE switch was set. VolumeMount.read_only reached
fc-agent in the boot plan and was used only for podman's -v ...:ro.

With the writeback cache the guest kernel keeps the size and mtime it
first cached for a regular file and drops the server's. A file the host
rewrote longer in place was read cut off at the old length (8 of 92
bytes in a running VM, 8 of 134 in a clone restored from a snapshot
taken before the rewrite). One rewritten shorter kept its old length,
padded with zero bytes. With --portable-volumes a file replaced by
rename kept its old size and mtime in the clone (#1041). A write from
the guest OS (fcvm exec --vm) succeeded and changed the host directory;
only the container's bind mount was read-only (#1042).

What changes:

- fuse_pipe::MountSettings::for_volume(read_only, no_writeback_cache)
  is the one decision. A read-only volume is mounted with MS_RDONLY and
  never asks for FUSE_WRITEBACK_CACHE, whatever the VM-wide switch says.
  A read-write volume is mounted as before. The fields are private, so
  no other combination can be built.
- fuse-pipe's mount API takes the settings as an argument:
  MountConfig::new, mount_vsock, mount_vsock_with_readers,
  mount_vsock_with_options, mount_vsock_with_reconnect and the
  FuseClient constructors. The Unix socket path and the vsock path share
  one session setup (mount_fuse_session), so a local mount is made by
  the code the guest runs. FuseClient::init no longer reads
  FCVM_NO_WRITEBACK_CACHE itself.
- fc-agent passes each volume's read_only to the mount and mounts
  volumes in groups, an outer volume before a volume inside it. Every
  mount used to be started at once in plan order, and whether an outer
  mount covered an inner one was a race between their threads.
- fcvm podman run refuses, before boot, a mount point (another --map, or
  a --disk, --disk-dir or --nfs) that lies inside a read-only map and is
  not already a directory of that map's host directory. fc-agent cannot
  create it in a read-only mount. The error names both arguments. There
  is no fallback to a read-write mount.
- README.md and DESIGN.md say what :ro means in the guest, what the host
  may change under a read-write map, and what a snapshot keeps. DESIGN.md
  said snapshots are disabled with --map volumes, which is no longer
  true; that paragraph is replaced. The TODO in src/volume/mod.rs is
  replaced with what is true and a pointer to #1042.

Plain volumes and --portable-volumes go through the same guest mount.
Only the host's server differs.

Snapshots. The mounts live in the guest's memory, so a snapshot made by
an older fc-agent keeps its old mounts until it is made again. The
snapshot cache key already separates old from new: it hashes
FirecrackerConfig.boot_source.initrd_path, and the initrd is named after
the hash of the fc-agent binary (fc-agent-bfcd8a448162.initrd before,
fc-agent-f214cb9799e7.initrd after, on the host that ran the tests). No
key ingredient is added.

Why no test caught it. test_remap_fs_host_file_changes and
test_remap_fs_snapshot_file_replace each replace a file with one of the
same length (version-1 and version-2, original-v1 and replaced-v2), so
a size the guest never refreshed still fit the new content. No test
wrote to a :ro map from the guest OS or read the guest's mount table.

Red, on f7fe35c plus tests/test_read_only_map.rs:

  make test-all FILTER="-p fcvm --test test_read_only_map"
  Summary [ 102.022s] 3 tests run: 0 passed, 3 failed, 0 skipped

  test_read_only_map_follows_host_in_vm_and_clone
    running VM, 10s after the host changed the files:
    grows.txt: host size=92 read 92 bytes; guest size=8 read 8 bytes
    shrinks.txt: host size=20; guest size=200 read 200 bytes
      starting "s1:.................\0\0\0\0"
    clone restored from a snapshot taken before the host changed them
    again:
    grows.txt: host size=134; guest size=8 read 8 bytes
    shrinks.txt: host size=5; guest size=200 read 200 bytes
    replaced.txt: host size=301; guest size=57, with the mtime of the
      file it replaced
  test_read_only_map_refuses_guest_writes
    the guest's mount table has /mnt/ro as rw,relatime (superblock
      rw,user_id=0,group_id=0,default_permissions,allow_other), not ro
    creating a file from the guest OS did not fail with EROFS: "status=0"
    the host directory holds ["created-by-guest", "existing.txt"]
    existing.txt on the host now reads "overwritten by the guest\n"
  test_map_inside_read_only_map_is_mounted_over_it
    try 1: the outer map is writable from the guest OS: "status=0"
    try 2: /mnt/outer/inner in the guest OS does not list the inner
      map's one file: "under-the-inner-map.txt\nstatus=0"

Red, with the new API in place and its three decisions returning what
f7fe35c does (for_volume ignoring read_only, one mount group, no check
of mount points):

  make test-unit FILTER="-E 'test(a_read_only_volume_is_mounted_read_only_and_without_the_writeback_cache) | test(only_a_read_only_volume_adds_the_ro_mount_option) | binary(test_read_only_mount) | test(a_volume_inside_another_is_mounted_after_it) | test(a_mount_point_missing_from_a_read_only_map_fails_and_names_both_maps) | test(only_a_read_only_innermost_map_needs_the_mount_point_on_the_host)'"
  Summary [  25.225s] 8 tests run: 1 passed, 7 failed, 1384 skipped

  The pass is the fixture's own read-write mount test. The failures:
  a_read_only_volume_is_mounted_read_only_and_without_the_writeback_cache
    volume read_only=true, VM-wide no_writeback_cache=false:
    left: (false, true)  right: (true, false)
  only_a_read_only_volume_adds_the_ro_mount_option
    left: [FSName("fuse-pipe"), Suid, Dev, DefaultPermissions]
    right: [FSName("fuse-pipe"), Suid, Dev, DefaultPermissions, RO]
  a_read_only_mount_follows_a_file_rewritten_longer_in_place
    left: (8, SystemTime { tv_sec: 1000000000, tv_nsec: 0 }, "xxxxxxxx")
    right: (92, <the rewrite's mtime>, <92 bytes>)
  a_read_only_mount_refuses_writes_with_erofs
    the mount table has it as rw,nosuid,nodev,relatime, not ro
    creating a file did not fail with EROFS: Ok(())
  a_volume_inside_another_is_mounted_after_it
    the groups ["/a", "/a/b"] are mounted in
    left: [[0, 1]]  right: [[0], [1]]
  a_mount_point_missing_from_a_read_only_map_fails_and_names_both_maps
    a mount point the read-only outer map does not have was accepted
  only_a_read_only_innermost_map_needs_the_mount_point_on_the_host
    --disk-dir x:/a/disk with 1 map(s): Ok(())

Green:

  the same make test-unit FILTER:
  Summary [   6.110s] 8 tests run: 8 passed, 1384 skipped
  make test-all FILTER="-p fcvm --test test_read_only_map"
  Summary [  44.030s] 3 tests run: 3 passed, 0 skipped
  make build: exit 0
  make test-unit
  Summary [  72.551s] 1392 tests run: 1392 passed (1 slow, 2 flaky), 0 skipped

  The two flaky tests are in src/uffd/server.rs, which this change does
  not touch: a_fault_parked_during_replay_resolves_while_the_balloon_
  keeps_inflating and faults_served_while_the_balloon_keeps_inflating_
  resolve_far_inside_the_bound. Each measured a fault blocked for 375 ms
  and 611 ms on its first try and passed its second.

Out of scope:

- The host VolumeServer still accepts writes on a read-only volume, so a
  guest that remounts the volume read-write can write to the host
  directory (#1042, second item).
- A read-write volume keeps the writeback cache, and with it the guest's
  own size and mtime for a file it has cached (#1041). The host must not
  change such a file unless the VM was booted with
  FCVM_NO_WRITEBACK_CACHE=1.

Not run:

- make lint past clippy. cargo fmt --check and cargo clippy
  --all-targets -- -D warnings passed. cargo audit could not fetch the
  advisory database from github.com, so it and cargo deny check did not
  run.
- make test-root, the bridged tests and the arm64 nested tests.
  tests/test_kvm.rs and nested.sh map the fcvm config directory :ro
  into the outer VM and run fcvm there from the guest OS. fcvm writes
  that directory only in setup --generate-config, which they do not run.
… dormant key ingredient

Repairs to 67d53ad (mount a read-only map read-only and without the
FUSE writeback cache) from two reviews.

The host check covers the image store. For a localhost/ image in overlay
image mode fc-agent creates and mounts /mnt/image-store after the FUSE
volumes are mounted (fc-agent/src/container.rs, mount_overlay_image).
The host check knew only the mount points of the arguments, so
--map DIR:/mnt:ro with such an image booted and failed in the guest on
mkdir with EROFS, where f7fe35c booted. The check now gets that mount
point when the boot attaches an image disk and the image mode is
overlay, and the error says fcvm mounts the image store there. The path
is IMAGE_STORE_MOUNT_POINT in src/commands/podman/types.rs. A unit test
reads the fc-agent source and fails when fc-agent mounts the store
elsewhere, or when agent.rs, container.rs or mounts.rs gains another
create_dir_all whose failure fails the boot. Those are, today, the mount
points of the plan's volumes, disks and NFS shares, and the image store.
Every other directory fc-agent creates discards its error.

The check runs on every boot. It ran once, after the kernel, rootfs and
initrd were ensured and after the snapshot-cache branch had returned, so
a cache hit was never checked and neither relaunch after a guest reboot
was. types::checked_volume_mappings parses the maps and checks them, and
is the only way the three callers of configure_and_boot_vm get their
maps:
- fcvm podman run and podman prepare call it before any asset, the
  image, the snapshot cache or the VM is touched. Everything the check
  needs is in the arguments, so nothing of it waits for a later step.
  fcvm snapshot run of a disk-only snapshot goes through the same code.
- The relaunch of a podman run VM calls it before the new VMM is
  spawned. A refusal ends the run with the named error.
- The relaunch of a restored clone calls it in build_clone_reboot_plan,
  which runs when the guest reboots. A refusal is logged and the reboot
  is treated as the VM's exit, as for any plan that cannot be built.
fcvm snapshot run of a memory snapshot is not checked: its mounts are in
the guest's memory. The check does not follow a symlink on the way to a
mount point and cannot see a directory removed after it ran. There
fc-agent fails to create the mount point and the container exits 1.
DESIGN.md says which runs are checked and what happens where the check
does not reach.

fc-agent's fatal error is one line. main printed the error with {:?}:
only the outermost context was on the [fc-agent] line, and the causes
went to DEBUG on the host. The line now carries the chain in anyhow's
{:#} form, with a multi-line cause joined.

The order of nested mounts has a unit test. mount_fuse_volumes passes
the start and the readiness wait into mount_in_levels, and a test
records the events. Before, only a VM test whose red depended on which
mount thread won covered that a group is ready before the next starts.

FCVM_NO_WRITEBACK_CACHE no longer changes a run with no read-write map.
A read-only volume is never mounted with the writeback cache, but the
switch still changed the snapshot key and put no_writeback_cache=1 on
the kernel command line of a run whose maps are all :ro.
GuestBootInputs::for_launch now takes the parsed maps and drops the
switch unless one of them is read-write. build_firecracker_config and
build_launch_config pass the maps they are given.

Smaller items:
- FuseClient::with_destroyed_flag lost its only caller in 67d53ad and
  is removed.
- The --map help says what :ro means.
- DESIGN.md states that the host must not replace or remove a directory
  another mount sits on: the guest drops the inner mount within the
  entry timeout. It also says disks and NFS shares are mounted after
  every map and cover a map below them.
- tests/test_read_only_map.rs runs the fcvm binary for both refusals and
  asserts a non-zero exit, the named error, untouched host directories
  and no VM in fcvm's state. A unit test parses real command lines with
  clap and asserts the list of mount points.

Red, each test on the tree with that item's change taken back (the check
knowing only the arguments' mount points, the clone's relaunch plan and
the first boot parsing the maps without checking, every volume started
before any wait, {:?} on the error line, for_launch dropping the switch
only for a run with no volume):

  make test-unit FILTER="-E 'test(a_group_of_volumes_is_ready_before_the_next_group_starts) | ...'"
  Summary [   5.033s] 12 tests run: 7 passed, 5 failed, 1387 skipped
  a_group_of_volumes_is_ready_before_the_next_group_starts
    left: ["start /a", "start /a/b", "ready /a", "ready /a/b"]
    right: ["start /a", "ready /a", "start /a/b", "ready /a/b"]
  the_fatal_error_line_carries_the_whole_cause_chain
    left: "[fc-agent] Error: mounting FUSE volumes"
  a_read_only_map_around_the_image_store_is_refused_when_the_run_mounts_one
    a run that mounts the image store inside a read-only map was accepted
  no_writeback_cache_does_not_fragment_keys_of_runs_with_only_read_only_maps
    the switch changed the key of a run whose maps are all read-only
    left: "b2d1de61b5e3"  right: "ca6444070a5c"
  every_boot_has_its_maps_checked_first
    build_clone_reboot_plan does not check the maps

  make test-all FILTER="-p fcvm --test test_read_only_map -E 'test(/refused_before_boot/)'"
  test_read_only_map_around_image_store_is_refused_before_boot, with the
  image store left out of the check:
    stderr does not name --map <dir>:/mnt:ro
    stderr does not name /mnt/image-store
  test_mount_point_missing_from_read_only_map_is_refused_before_boot,
  with the first boot's check removed: the VM booted and the container
  exited 1
    stderr does not name --map <inner>:/mnt/outer/inner
    stderr does not name --map <outer>:/mnt/outer:ro

Green at this commit:

  the same make test-unit FILTER:
  Summary [   0.024s] 12 tests run: 12 passed, 1387 skipped
  make build: exit 0
  make lint: exit 0 (fmt, clippy, advisories ok, bans ok, licenses ok,
    sources ok)
  make test-unit, run before a clippy allow was added to
  build_launch_config, which now takes eight arguments:
  Summary [  71.318s] 1399 tests run: 1399 passed (1 slow, 2 flaky), 0 skipped
    The two flaky tests are the two in src/uffd/server.rs that the first
    commit names. Each failed its first try and passed its second.
  make test-all, one file at a time:
  -p fcvm --test test_read_only_map
  Summary [  35.247s] 5 tests run: 5 passed, 0 skipped
  -p fcvm --test test_portable_volumes
  Summary [ 100.872s] 8 tests run: 8 passed, 0 skipped
  -p fcvm --test test_volume_coherency
  Summary [  41.715s] 2 tests run: 2 passed, 0 skipped
  -p fcvm --test test_fuse_snapshot_matrix
  Summary [ 118.654s] 8 tests run: 8 passed, 0 skipped
  -p fcvm --test test_sanity -E 'test(test_rootless_map_nonroot_reader)'
  Summary [  22.289s] 1 test run: 1 passed, 9 skipped
  -p fcvm --test test_snapshot_clone -E 'test(test_async_pf_inside_nmi_does_not_panic_a_restored_guest)'
  Summary [  65.421s] 1 test run: 1 passed, 22 skipped

Not in this commit:

- Keeping an inner mount when the host replaces the directory it sits
  on (#1043). DESIGN.md states the constraint.
- A map inside a --disk, --disk-dir or --nfs guest path is mounted first
  and then covered, as on main (#1044).

Not run: make test-root, the bridged tests and the arm64 nested tests.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T03:05:56.345007Z 6c5f1bb Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: dbabadc8-8d4e-465b-92db-6ed6c2bc0ff6

📥 Commits

Reviewing files that changed from the base of the PR and between f7fe35c and 539d633.

📒 Files selected for processing (21)
  • DESIGN.md
  • README.md
  • fc-agent/src/fuse/mod.rs
  • fc-agent/src/main.rs
  • fc-agent/src/mounts.rs
  • fuse-pipe/src/client/fuse.rs
  • fuse-pipe/src/client/mod.rs
  • fuse-pipe/src/client/mount.rs
  • fuse-pipe/src/lib.rs
  • fuse-pipe/tests/common/mod.rs
  • fuse-pipe/tests/pjdfstest_common.rs
  • fuse-pipe/tests/test_read_only_mount.rs
  • src/cli/args.rs
  • src/commands/podman/mod.rs
  • src/commands/podman/snapshot.rs
  • src/commands/podman/types.rs
  • src/commands/podman/vm_config.rs
  • src/commands/snapshot.rs
  • src/firecracker/config.rs
  • src/volume/mod.rs
  • tests/test_read_only_map.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds per-volume read-only FUSE settings, validates volume mount points before VM launch, and orders nested guest mounts. Read-only maps reject guest writes and reflect host file changes. Snapshot and launch configuration now use the parsed volume mappings.

Changes

Read-only volume maps

Layer / File(s) Summary
Per-volume FUSE mount settings
fuse-pipe/src/client/*, fuse-pipe/src/lib.rs, fuse-pipe/tests/*, fc-agent/src/fuse/mod.rs
Mount configuration now uses MountSettings to select read-only options and writeback caching. Unix and vsock session setup passes the settings to FuseClient. Tests cover mount options, write rejection, and host file updates.
Volume validation and launch propagation
src/commands/podman/types.rs, src/commands/podman/mod.rs, src/commands/podman/snapshot.rs, src/commands/podman/vm_config.rs, src/commands/snapshot.rs, src/firecracker/config.rs, tests/test_read_only_map.rs
fcvm checks mount points within read-only maps before launch and reboot relaunch. It passes checked mappings into VM configuration. Launch settings and snapshot-key tests account for whether mappings are read-only or read-write.
Guest mount ordering and read-only behavior
fc-agent/src/mounts.rs, fc-agent/src/fuse/mod.rs, src/volume/mod.rs, tests/test_read_only_map.rs, src/cli/args.rs, README.md, DESIGN.md
fc-agent starts nested mounts by containment level and waits for each level to become ready. Tests and documentation describe host updates, guest write rejection, nested writable maps, and read-only mount-point requirements.

Startup diagnostics

Layer / File(s) Summary
Single-line fatal error output
fc-agent/src/main.rs
Fatal startup output now presents the non-empty lines of the full error cause chain on one prefixed line. Tests cover chained and multiline errors.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant fc-agent
  participant start_fuse_mount
  participant mount_vsock_reconnectable
  participant FUSE_session
  participant mountinfo
  fc-agent->>start_fuse_mount: start volumes in the current mount level
  start_fuse_mount->>mount_vsock_reconnectable: pass mount point and read_only setting
  mount_vsock_reconnectable->>FUSE_session: create session with MountSettings
  fc-agent->>mountinfo: check mount entry and directory readiness
  mountinfo-->>fc-agent: report readiness before the next level
Loading

Merge Risk: ⚪ Minimal · up to 539d6

Read-only maps are now mounted read-only in the guest, without writeback caching, so guests see host file changes. Read-write maps keep their existing behavior. fcvm now refuses to start when a needed mount point is missing inside a read-only map. No concrete merge-blocking issue was found.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 539d6

The change strengthens normal read-only behavior without showing increased host access. Protection still depends on the guest honoring its mount settings, and older saved machines retain their previous writable settings until recreated.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — Each volume server canonicalizes its selected host directory and supplies it as the backend root. Write operations pass guest-provided UID, GID, and PID into the host filesystem backend. Effective access therefore depends on backend authorization and host process privileges, which were not established for every deployment mode.

Security Findings and Attack Paths

  • inferred — A guest with authority to remount a volume read-write can still submit mutations to mapped host files, subject to backend and host permissions. The server path and exposure predate this PR, so this is an existing trust-model limitation, not an active introduced architecture concern.

Trust Boundaries and Controls

  • observed — For an unmodified read-only mount, kernel enforcement rejects writes before they reach FUSE. Disabling writeback caching also leaves host-reported file size and modification time authoritative. Neither control makes host content immutable or prevents a privileged guest from changing mount policy.

Resilience and Maintainability Implications

  • observed — Preflight is not an atomic reservation of host mount-point directories: it cannot prevent later host changes or resolve paths in the guest namespace. Startup failures enter the fatal shutdown path. Documentation also preserves the operational constraint that replacing an active nested mount-point directory can invalidate the inner mount.

Hardening Proposals

  • proposed — If hostile guest kernels are within the intended threat model, enforce per-volume read-only authorization at the host server for every mutating filesystem operation, independently of guest mount settings.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 108 functions across 19 files. (2 skipped: …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: read-only map volumes are mounted read-only in the guest and reflect host file changes.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 539d633e4b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/commands/podman/types.rs
check_mount_points_inside_read_only_maps compared guest paths as they
were typed and stopped its walk at the first component that is not a
plain name. With a read-only map at /data whose host directory has no
cache, --map IN:/data/../data/cache passed the check, and
/other/../data/cache was not seen as inside the map at all. The guest's
kernel resolves both to /data/cache, so fc-agent booted and failed to
create the mount point with EROFS, which is what the check exists to
refuse before boot.

The check now folds . and .. in the mount point and in each map's guest
path before it compares them. It still looks at no filesystem for that
and still leaves a symlink on the way to the guest.

Found by the Codex review of this pull request.

Red, the test on the tree before the fix:

  make test-unit FILTER="-E 'test(a_mount_point_written_with_parent_components_is_checked_where_it_resolves)'"
  FAIL commands::podman::types::tests::a_mount_point_written_with_parent_components_is_checked_where_it_resolves
    /data/../data/cache was accepted with no cache directory in the map

Green:

  the same FILTER:
  Summary [   0.014s] 1 test run: 1 passed, 1399 skipped
  make test-unit FILTER="-E 'test(/commands::podman::types::tests/) | test(/commands::podman::tests::/)'"
  Summary [   0.149s] 51 tests run: 51 passed, 1349 skipped
  make lint: exit 0

fc-agent orders nested mounts by the paths as typed (mount_levels), so
two maps of which one is written with .. are still started together.
That is the race main has for every nested pair, and this change does
not touch it.

@ejc3 ejc3 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RED-VERIFIED: commands::podman::types::tests::a_mount_point_written_with_parent_components_is_checked_where_it_resolves

This answers the Codex review of 539d633, whose one finding is the inline thread on src/commands/podman/types.rs: a mount point written with .. passed the check. The test fails on 539d633 (/data/../data/cache was accepted with no cache directory in the map) and passes on 4d39a47, which folds . and .. before comparing guest paths.

@ejc3

ejc3 commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d39a470db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/commands/podman/types.rs Outdated
The check before boot folded `.` and `..` in guest paths without the
guest's filesystem. Where `..` leads depends on the symlinks before it:
with /var/run a link to /run, /var/run/../actual/inner is /actual/inner in
the guest, and the fold read it as /var/actual/inner, outside a read-only
map at /actual. The run then passed the check and fc-agent failed in the
guest with EROFS.

A run with a read-only map now refuses a mount point or a map guest path
that has a `..` component, and names the argument. `lexically_normal` is
gone: comparing paths by component already ignores `.` and repeated
slashes. A run with no read-only map is unchanged.

Tested:
  make test-unit FILTER="-E 'test(a_guest_path_with_a_parent_component_is_refused_beside_a_read_only_map)'"
    on 4d39a47 with the test alone: 0 passed, 1 failed,
      "/var/run/../actual/inner was accepted beside a read-only map"
    with the change: 1 passed
  make test-unit FILTER="-E 'test(/commands::podman::types::tests/) | test(/commands::podman::tests::/)'"
    51 passed
  make lint: exit 0

@ejc3 ejc3 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RED-VERIFIED: commands::podman::types::tests::a_guest_path_with_a_parent_component_is_refused_beside_a_read_only_map

This answers the Codex review of 4d39a47, whose one finding is the inline thread on src/commands/podman/types.rs: folding .. is wrong when a symlink precedes it in the guest. The test fails on 4d39a47 (/var/run/../actual/inner was accepted beside a read-only map) and passes on 6c5f1bb, which refuses a guest path with .. in a run that has a read-only map.

@ejc3

ejc3 commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c5f1bbe31

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/commands/podman/types.rs
Comment thread fuse-pipe/src/client/mount.rs

@ejc3 ejc3 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DISAGREE: this answers the Codex review of 6c5f1bb, whose two findings are the inline threads, and each thread has the detail.

The check-to-mount window ends in a refused boot whose error names the path, and no lock binds another process that removes a directory of the user's own tree. The unchanged-metadata rewrite reads the same through a read-write map mounted as on main, a new open reads the new bytes, and #1049 tracks the documentation and the test.

@ejc3 ejc3 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT-A-DEFECT: this answers CodeRabbit's review summary, which reports no actionable comments and no merge-blocking issue. It names no defect.

@ejc3
ejc3 merged commit f90572a into main Oct 2, 2026
14 checks passed
@ejc3
ejc3 deleted the read-only-map-follows-host branch October 2, 2026 04:04
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.

1 participant