You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #621: A3.2, descriptor-rooted mappings, also used by the mapping table and the runtime binds.
Refs #619, #622, #535.
What and why
Host mappings were bind-mounted by path name. G4's Mapping created a missing target by path, bind-mounted the source, and only then remounted it nosuid, nodev, and read-only, so a symbolic link, a magic link, or a rename that raced the walk could redirect a bind, and a read-only bind was briefly writable. This rebuilds nvx::guest::mappings as a mapper that works beneath descriptors, and moves both of its callers onto it. No pathname path is left.
The mapper, nvx::guest::mappings:
A TargetRoot is the descriptor of the root of a mount, such as a layered root's overlay or a guest's /, with the unique mount IDs of the root's own mounts. Each of those mounts must show its whole file system, which statmount tells: on a bind of a directory below its file system's root, a rename through another mount of that file system could move a directory that the walk opened out of the root. A SourceRoot is the descriptor of the sources' root, such as the guest's mount of the host's export.
Walk: each component of a target is opened with openat2 beneath the one before it, from the root's descriptor, with RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS. A source is opened in one call, with RESOLVE_NO_XDEV too.
Creation: a missing component is created only where statx shows its parent on one of the root's own mounts, by STATX_MNT_ID_UNIQUE: never inside another mapping, nor on another mount, a bind of the root's own file system included, which has the same st_dev. It gets exactly mode 0755, or 0644 for a file target, whatever the caller's umask or a default ACL of its parent, as a layered root's live shares did, so that a workload that is not root can enter it. A file is created with O_CREAT | O_EXCL and gets its mode through the descriptor that the creation returns, which refers to that file alone. A directory is created with no permissions, which neither the umask nor an ACL can narrow; the mapper then opens it as an existing component and, through that descriptor, gives it 0755 only if it still has no permissions and the mapper's user as its owner, as the one that it created has. No other user can give a directory both, nor move one that has both out of another directory, which takes the permission to write it: of what a rename could put in its place, only such a directory from the same parent would get the mode. One that existed already keeps its mode.
Checks: kinds, overlaps between entries, and reserved targets and their ancestors: the /run/nvx/time tree always, plus the root profile's Reserved set. Reserved::container_root() takes NVX's container-root set from nvx::protocol::share, and an adapter passes its own. A target that is the root of a mount already is refused, unless the root owns that mount, such as the tmpfs at a guest's /tmp, which the mapping then covers until the teardown.
Attach:open_tree(OPEN_TREE_CLONE) copies the source's mount, detached; mount_setattr sets nosuid, nodev, and the read-only flag on the copy; only then does move_mount attach it onto the target's descriptor. The mapper then finds the target again from the root, and detaches a mapping that a rename moved meanwhile. If that detach fails, the mapper keeps the mount's descriptor, and its teardown detaches it first, or reports it.
Order and failure: zero entries do no work, entries apply in order, and the first failure latches the mapper. It tracks its mappings, and its teardown unmounts them in reverse order through each parent's descriptor, and reports each failure.
Runtime binds:bind applies one bind, such as the machine ID or the time status, through the same walk and attach, for the execution entry that follows (A3.4).
Syscalls:guest/sys/rooted.rs adds safe wrappers for openat2, statx with the owner, the mode, and the unique mount ID, statmount for a mount's root, mkdirat, an exclusive creation of a file with its exact mode, open_tree, mount_setattr, and move_mount, and three calls that name a descriptor through the calling thread's /proc/thread-self/fd, so that a thread with a descriptor table of its own reaches what its descriptors refer to: a change of an O_PATH descriptor's mode, which fchmod does not take, and the unmounts beneath a parent's descriptor and of a detached tree. The constants that libc lacks come from the kernel's UAPI headers.
Callers:
A layered Root mounts a live share at staging in its runtime directory, then moves it into the root through its mapper; it binds an aggregate's children through the mapper; and Root::map applies host mappings beneath the overlay. A failure after a request's checks latches the root, and the teardown unmounts the mapper's mappings first. The root's own pathname checks and creation are gone.
NVX's managed agent applies its direct-mode MAPS table through a mapper beneath /, which creates targets only on / and on the tmpfs at /tmp, and latches at the first failure, as before. A mapping of the host's /tmp itself, which the host's planner accepts, covers that tmpfs, as the pathname mapper's bind did.
Tests:
The root's fake kernel, from nvx-rs: add guest operations for a layered root #674, simulates mount IDs, detached trees, and the new calls. Unit and fault-injection tests cover target roots of whole file systems, the walk's refusals, creation only on the root's own mounts, with exact modes under a umask of 077 and below a restrictive default ACL, for the mapper and a layered root's live shares, and under the umask for the managed agent's table, a failure to give a created directory its mode, a mapping that covers the root of one of them, and the managed agent's mapping of /tmp itself, a directory that a rename puts in place of a created one before it gets its mode, overlaps and reserved targets, the attach order with the flags before move_mount, latching, zero entries, a moved mapping that the attach fails to detach, and the teardown's order and failures.
Privileged tests (#[ignore], run as root) cover, on a real kernel, a symbolic link and a magic link at every component, a rename race during the walk, kinds, a target nested in another mapping, reserved targets and ancestors, a mapping that covers the root of a mount that the root owns until the teardown, and is refused on one that it does not, a workload that is not root reading through the directories that a mapper created under a umask of 077 and below a restrictive default ACL, a read-only mapping that is never visible writable, the teardown, also from a thread with a descriptor table of its own, and a bind of a subdirectory refused as a root or as one of its mounts.
The runtime adapter's worker gains probes that map beneath its assembled layered root, through Root::map and through mappers with reserved sets of its own. Two VM scenarios check them: mappings_apply_beneath_the_root and a_refused_mapping_latches_the_root.
One PR rather than two: the task allows splitting the mapper from the managed agent's switch if they grow large. Mapping is rebuilt in place, so both of its callers move with it, or the pathname code would have stayed for a PR. The managed agent's part is about 150 lines of code and 250 of tests. The two commits keep the library apart from the adapter's scenarios.
Direct mode's reserved targets: the managed agent's direct mode reserves the runtime's tree, as every mapper does, and nothing more, as before; the host's planner refuses its own reserved targets before it sends MAPS. The sandbox mode's MAPS, beneath the container root with its reserved set, come with A4.
Validation
On Linux (kernel 6.18, as a non-root user), the checks of check-nvx-rs and check-aci-edge-sandboxes:
in nvx-rs: cargo fmt --all --check; cargo clippy --locked --all-targets --all-features -- -D warnings; cargo clippy --locked --lib --no-default-features -- -D warnings; cargo test --locked --all-features; cargo test --locked; cargo doc --locked --no-deps --all-features with RUSTDOCFLAGS=-D warnings; cargo +1.89 check --locked --all-targets --all-features; the macOS check; the artifact bundling check;
python3 scripts/nvx.py check-guest-binaries, build-guest-binaries, and test-guest-binaries;
tests/runtime-adapter in its protocol-only, host-only, and guest-only configurations. The guest-only configuration's MSRV check ran for the x86_64-unknown-linux-gnu target; CI runs it for musl;
aci_edge_sandboxes: fmt, Clippy, tests, docs, MSRV, the macOS check, and python3 scripts/nvx.py test-agent-bundling.
On Windows: the nvx-rs checks above but the macOS check, the runtime adapter's protocol-only and host-only configurations, and aci_edge_sandboxes' Clippy, tests, MSRV check, and test-agent-bundling. python scripts/check_markdown_links.py passes.
The privileged tests, cargo test --all-features -- --ignored guest::mappings::privileged, pass inside real VMs on NVX's guest kernel, 6.18.38, on WHP and MSHV: a static musl build of the test binary ran as PID 1, with two processors. They also pass as root on a Linux host, with kernel 6.18, and passed in a KVM VM on an earlier base, before the last three commits. Each rename race had all three outcomes: attached, refused at a link, and moved and detached.
A sandbox that maps the host's /tmp read-only, run once by hand on MSHV in a private mount namespace whose /tmp was a fresh tmpfs, read the host's file at /tmp and showed the mapping above the guest's tmpfs, as with this PR's base. Without the commit that lets a mapping cover the root of a mount that the root owns, its start failed with EBUSY.
Real VMs, with an Alpine initramfs built from this branch:
Backend
Where
test-aci-edge-sandboxes
test-microvm scenarios
Runtime adapter vm_layered
WHP
Windows 11
18 of 18
pass
6 of 6
MSHV
a Linux VM, as root, --cpu-profile host
13 of 18 (see below)
not run
6 of 6
KVM
WSL, --cpu-profile host, on an earlier base, before the last three commits
18 of 18
pass
6 of 6
test-microvm ran filesystem-shares, filesystem-policy, filesystem-owner, filesystem-comma-paths, denied-filesystem-paths, filesystem-snapshot, managed-lifecycle, lifecycle, workload-identity, and structured-outcome, with --processors 1 2.
On MSHV, /dev/mshv is root-only there, so the tests ran as root, and five read-write mapping tests fail: host_paths_are_mapped_into_the_guest, host_paths_with_commas_keep_their_access, mapped_files_hide_their_siblings_and_names_may_hold_spaces, single_files_are_granted_without_their_directories, and unrelated_directories_keep_their_own_access. They fail identically, at the same assertions, with the initramfs built from this PR's base: the workload cannot write the root-owned host directories.
Sizes of the stripped guest binaries and the Alpine initramfs, before and after, both built from source:
Artifact
Before
After
nvx-managed-agent
487,968
496,160 (+8,192)
other guest binaries
unchanged
unchanged
initramfs.cpio.gz (Alpine)
8,216,979
8,222,812 (+5,833)
Left to CI: KVM on this base, the full microVM matrix, MSHV runs as a non-root user, and the benchmarks.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The security-sensitive syscall, containment, and mount-race cleanup changes warrant human review alongside fresh privileged-kernel and cross-backend CI results.
No open issues 🎉
What changed in this PR
Advances #621’s descriptor-rooted mapping work by replacing pathname-based attachment in the guest library’s callers. Layered roots and the managed agent use the shared mapper to enforce containment and apply mount restrictions before attachment.
Changes:
Adds descriptor-relative syscall wrappers and integrates mapping ownership, failure latching, and teardown into layered roots and direct-mode MAPS.
Expands fake-kernel, privileged-kernel, and runtime-adapter coverage for containment, mount flags, and cleanup.
Documents the mapping APIs, guarantees, and VM scenarios.
File
Description
tests/runtime-adapter/tests/vm_layered.rs
Adds mapping and failure-latching VM scenarios.
tests/runtime-adapter/src/worker/probes.rs
Shares mount-table helpers with mapping probes.
tests/runtime-adapter/src/worker/mapping.rs
Adds mapping enforcement and cleanup probes.
tests/runtime-adapter/src/worker.rs
Routes mapping probes to the mutable root.
tests/runtime-adapter/README.md
Documents mapping probes and scenarios.
tests/runtime-adapter/Cargo.toml
Enables mount operations for guest probes.
nvx-rs/src/lib.rs
Updates the guest-library overview.
nvx-rs/src/guest/sys/rooted.rs
Adds descriptor-relative filesystem and mount wrappers.
nvx-rs/src/guest/sys/mod.rs
Exposes the new internal syscall wrappers.
nvx-rs/src/guest/sys/fd.rs
Clarifies raw-descriptor usage in documentation.
nvx-rs/src/guest/root/tests.rs
Tests mapper integration, latching, and teardown.
nvx-rs/src/guest/root/share.rs
Routes live shares and host mappings through the mapper.
nvx-rs/src/guest/root/kernel.rs
Extends the fake kernel with descriptor and mount operations.
nvx-rs/src/guest/root.rs
Adds mapper ownership and ordered cleanup.
nvx-rs/src/guest/mod.rs
Updates guest API documentation.
nvx-rs/src/guest/mappings/privileged.rs
Adds real-kernel containment and mount-lifecycle tests.
nvx-rs/src/guest/mappings/kernel.rs
Defines the mapper’s replaceable kernel interface.
nvx-rs/src/guest/managed_agent/seam.rs
Supplies real and fake mapping kernels.
nvx-rs/src/guest/managed_agent/mod.rs
Documents the expanded system seam.
nvx-rs/src/guest/managed_agent/mappings.rs
Applies ordered MAPS through a persistent mapper.
nvx-rs/src/guest/managed_agent/loopback.rs
Updates integration assertions for descriptor-based attachment.
nvx-rs/src/guest/managed_agent/dispatch.rs
Updates mapping dispatch tests and fixtures.
nvx-rs/README.md
Documents descriptor-rooted APIs and lifecycle behavior.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The security-sensitive containment and rollback rewrite warrants human review, with an unresolved live-share permissions regression also needing attention.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Preserve traversable permissions for newly created live-share parents
nvx-rs/src/guest/root/share.rs:415
This switch removes the fixed permissions previously applied to newly created live-share parents. With a caller umask of 077, mounting at /srv/new/share now creates the root-owned /srv/new with mode 0700, rather than 0755. The mount covers only share, so a non-root workload cannot traverse new, even though mount_share or mount_aggregate succeeds. Preserve traversable permissions for newly created live-share parents using a race-safe creation mechanism, without changing existing or replacement directories. Add a restrictive-umask regression test that checks access by the workload identity.
Copilot's previously missed issue on f7e1157, Preserve traversable permissions for newly created live-share parents (nvx-rs/src/guest/root/share.rs:415), is fixed in 3711ac7, together with the default-ACL case of the review of 4351bdb, which that commit replaces.
The mapper, which creates what a live share's target lacks, now gives each missing directory exactly 0755, and a file target exactly 0644, whatever the caller's umask or a default ACL of the parent, as a layered root did before this PR, without changing anything that existed already or that a rename put in place of what it created. The thread on rooted.rs describes how.
Regression tests under a umask of 077 and below a default ACL of u::rwx,g::---,o::---:
guest::root: a live share at /srv/new/share and a file child at /opt/app/config leave /srv, /srv/new, /opt, and /opt/app with 0755, and the file with 0644.
guest::mappings: the mapper's own targets get the same modes, and a directory that existed with 0700 keeps it; the managed agent's table does under the umask too.
Privileged, a_workload_that_is_not_root_enters_what_a_mapper_created_whatever_the_umask_or_a_default_acl: a thread whose file system identity is nobody reads through the shares and files that a mapper created under the umask and below the ACL.
The description's validation reran on this head, and its sizes are updated: the managed agent is 8,192 bytes larger than on dev, as before the fix.
Rebuild nvx::guest::mappings as a mapper that works beneath descriptors
instead of path strings, so that no symbolic link, magic link or rename
can redirect a bind, and every bind has its flags before it is visible.
- The mapper walks each target component with openat2 and
RESOLVE_BENEATH, RESOLVE_NO_SYMLINKS and RESOLVE_NO_MAGICLINKS from
the root's descriptor, and opens each source the same way without
crossing a mount. The root, and each mount added as its own, must
show its whole file system, as statmount tells, so that no rename
through another mount of a bound directory's file system can move
what the walk opened out of the root.
- It creates a missing directory or file only on the root's own mounts,
checked by unique mount ID, never inside another mapping or mount,
with mode 0755 or 0644 before the umask, and then opens it as an
existing component, so it changes nothing that a rename puts there.
- It checks kinds, overlaps between entries, and reserved targets with
their ancestors: the runtime's /run/nvx/time tree always, plus the
root profile's set, which the container root takes from
protocol::share.
- It attaches each mapping as a detached copy (open_tree with
OPEN_TREE_CLONE) whose flags mount_setattr sets before move_mount
puts it on the target's descriptor, then checks that the mapping is
at its target and detaches it if a rename moved it. If that detach
fails, the mapper keeps the mount's descriptor and detaches it first
at the teardown, or reports it.
- Entries apply in order, zero entries do no work, and the first
failure latches the mapper. It tracks its mappings and unmounts them
in reverse at the teardown, through each parent's descriptor, and
reports any that stay.
- bind applies a single mapping, such as a runtime bind, through the
same code.
guest/sys gains the wrappers: openat2, statx with the unique mount ID,
statmount for a mount's root, mkdirat, open_tree, mount_setattr,
move_mount, and the unmounts beneath a parent descriptor and of a
detached tree.
The root's live shares and aggregate children now attach through the
mapper beneath the overlay: a share mounts at a staging point in the
runtime directory and moves from there. Root::map applies host mappings
beneath the overlay, the root latches at any failure after a request's
validation, and its teardown unmounts the mapper's mappings first. The
managed agent's direct mode applies its MAPS table through a mapper
beneath /, which creates targets only on / and /tmp. The pathname
mapper and the root's own pathname checks are gone.
The fake kernel of the root's seam simulates mount IDs, detached trees,
binds of subdirectories and the new calls for the unit and
fault-injection tests. Privileged tests check the walk, a rename race,
kinds, nesting, reserved targets, read-only binds and roots on binds of
subdirectories on a real kernel.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c8311d40-fd44-4294-b42e-3c6cd185c00b
The runtime adapter's worker maps beneath the layered root that it
assembled, as an adapter maps the host paths of its sandboxes, through
the probes of a new worker::mapping module, and two layered scenarios
check them on a real hypervisor:
- mappings_apply_beneath_the_root maps through Root::map: zero mappings
that do no work, then a table in order, each mapping with its access,
nosuid and nodev, on targets that the mapper creates on the overlay.
Through mappers of the adapter's own, it checks the refusal of a link
and of a magic link at each component, of the wrong kinds, of a
target that would be created inside another mapping, and of the
runtime's, the container root's and the adapter's own reserved
targets, and a read-only mapping that is never writable while another
thread tries to open it for writing. The stop then unmounts the
root's mappings before the overlay.
- a_refused_mapping_latches_the_root maps through a symbolic link that
it plants in the root, which the root refuses, after which it refuses
every mapping.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c8311d40-fd44-4294-b42e-3c6cd185c00b
The mapper refused every target that was the root of a mount. In the
managed agent's direct mode, that refused a mapping of the host's /tmp,
whose guest target is the tmpfs that the guest's init mounts at /tmp and
that the table's mapper owns: the host's planner accepts that mapping,
and the pathname mapper bound it.
A target that is the root of one of the root's own mounts is now one
that the mapping covers until the teardown. The root of any other mount,
an earlier mapping's included, is still refused.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c8311d40-fd44-4294-b42e-3c6cd185c00b
unmount_beneath and detach_mount named their descriptors through
/proc/self/fd, which shows the descriptor table of the process's first
thread. A thread with a table of its own, as after unshare(CLONE_FILES),
would then name another file than its descriptor's, or none, and its
teardown would fail or unmount something else. They now use
/proc/thread-self/fd, as the fake kernel and the tests' read of the
umask do too.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c8311d40-fd44-4294-b42e-3c6cd185c00b
A layered root created the missing parents and targets of its live
shares with mode 0755, or 0644 for a file, whatever the umask or a
default ACL of their parents, which a change of mode by path overrode.
The mapper that creates them now left their modes to the caller's umask
and to any default ACL instead: under a umask of 077, or below a
directory whose default ACL is u::rwx,g::---,o::---, a share at
/srv/new/share left /srv/new with mode 0700, which a workload that is
not root cannot enter.
The mapper now creates each missing file exclusively and sets its mode
through the descriptor that the creation returns, which refers to that
file alone. It creates each missing directory with no permissions,
which neither the umask nor an ACL can narrow, and, once the walk opens
it, gives it its mode through that descriptor, but only if it still has
no permissions and the mapper's user as its owner, as the directory
that it created has. A directory that a rename put in its place keeps
its mode, as does everything that existed already. A directory keeps
the set-group-ID bit that it inherits from its parent, as with mkdir.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c8311d40-fd44-4294-b42e-3c6cd185c00b
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive descriptor containment, rename-race handling, and mount teardown warrant final human review alongside the reported privileged-kernel validation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #621: A3.2, descriptor-rooted mappings, also used by the mapping table and the runtime binds.
Refs #619, #622, #535.
What and why
Host mappings were bind-mounted by path name. G4's
Mappingcreated a missing target by path, bind-mounted the source, and only then remounted itnosuid,nodev, and read-only, so a symbolic link, a magic link, or a rename that raced the walk could redirect a bind, and a read-only bind was briefly writable. This rebuildsnvx::guest::mappingsas a mapper that works beneath descriptors, and moves both of its callers onto it. No pathname path is left.The mapper,
nvx::guest::mappings:TargetRootis the descriptor of the root of a mount, such as a layered root's overlay or a guest's/, with the unique mount IDs of the root's own mounts. Each of those mounts must show its whole file system, whichstatmounttells: on a bind of a directory below its file system's root, a rename through another mount of that file system could move a directory that the walk opened out of the root. ASourceRootis the descriptor of the sources' root, such as the guest's mount of the host's export.openat2beneath the one before it, from the root's descriptor, withRESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS. A source is opened in one call, withRESOLVE_NO_XDEVtoo.statxshows its parent on one of the root's own mounts, bySTATX_MNT_ID_UNIQUE: never inside another mapping, nor on another mount, a bind of the root's own file system included, which has the samest_dev. It gets exactly mode 0755, or 0644 for a file target, whatever the caller's umask or a default ACL of its parent, as a layered root's live shares did, so that a workload that is not root can enter it. A file is created withO_CREAT | O_EXCLand gets its mode through the descriptor that the creation returns, which refers to that file alone. A directory is created with no permissions, which neither the umask nor an ACL can narrow; the mapper then opens it as an existing component and, through that descriptor, gives it 0755 only if it still has no permissions and the mapper's user as its owner, as the one that it created has. No other user can give a directory both, nor move one that has both out of another directory, which takes the permission to write it: of what a rename could put in its place, only such a directory from the same parent would get the mode. One that existed already keeps its mode./run/nvx/timetree always, plus the root profile'sReservedset.Reserved::container_root()takes NVX's container-root set fromnvx::protocol::share, and an adapter passes its own. A target that is the root of a mount already is refused, unless the root owns that mount, such as the tmpfs at a guest's/tmp, which the mapping then covers until the teardown.open_tree(OPEN_TREE_CLONE)copies the source's mount, detached;mount_setattrsetsnosuid,nodev, and the read-only flag on the copy; only then doesmove_mountattach it onto the target's descriptor. The mapper then finds the target again from the root, and detaches a mapping that a rename moved meanwhile. If that detach fails, the mapper keeps the mount's descriptor, and its teardown detaches it first, or reports it.bindapplies one bind, such as the machine ID or the time status, through the same walk and attach, for the execution entry that follows (A3.4).Syscalls:
guest/sys/rooted.rsadds safe wrappers foropenat2,statxwith the owner, the mode, and the unique mount ID,statmountfor a mount's root,mkdirat, an exclusive creation of a file with its exact mode,open_tree,mount_setattr, andmove_mount, and three calls that name a descriptor through the calling thread's/proc/thread-self/fd, so that a thread with a descriptor table of its own reaches what its descriptors refer to: a change of anO_PATHdescriptor's mode, whichfchmoddoes not take, and the unmounts beneath a parent's descriptor and of a detached tree. The constants thatlibclacks come from the kernel's UAPI headers.Callers:
Rootmounts a live share atstagingin its runtime directory, then moves it into the root through its mapper; it binds an aggregate's children through the mapper; andRoot::mapapplies host mappings beneath the overlay. A failure after a request's checks latches the root, and the teardown unmounts the mapper's mappings first. The root's own pathname checks and creation are gone./, which creates targets only on/and on the tmpfs at/tmp, and latches at the first failure, as before. A mapping of the host's/tmpitself, which the host's planner accepts, covers that tmpfs, as the pathname mapper's bind did.Tests:
/tmpitself, a directory that a rename puts in place of a created one before it gets its mode, overlaps and reserved targets, the attach order with the flags beforemove_mount, latching, zero entries, a moved mapping that the attach fails to detach, and the teardown's order and failures.#[ignore], run as root) cover, on a real kernel, a symbolic link and a magic link at every component, a rename race during the walk, kinds, a target nested in another mapping, reserved targets and ancestors, a mapping that covers the root of a mount that the root owns until the teardown, and is refused on one that it does not, a workload that is not root reading through the directories that a mapper created under a umask of 077 and below a restrictive default ACL, a read-only mapping that is never visible writable, the teardown, also from a thread with a descriptor table of its own, and a bind of a subdirectory refused as a root or as one of its mounts.Root::mapand through mappers with reserved sets of its own. Two VM scenarios check them:mappings_apply_beneath_the_rootanda_refused_mapping_latches_the_root.One PR rather than two: the task allows splitting the mapper from the managed agent's switch if they grow large.
Mappingis rebuilt in place, so both of its callers move with it, or the pathname code would have stayed for a PR. The managed agent's part is about 150 lines of code and 250 of tests. The two commits keep the library apart from the adapter's scenarios.Direct mode's reserved targets: the managed agent's direct mode reserves the runtime's tree, as every mapper does, and nothing more, as before; the host's planner refuses its own reserved targets before it sends MAPS. The sandbox mode's MAPS, beneath the container root with its reserved set, come with A4.
Validation
On Linux (kernel 6.18, as a non-root user), the checks of
check-nvx-rsandcheck-aci-edge-sandboxes:nvx-rs:cargo fmt --all --check;cargo clippy --locked --all-targets --all-features -- -D warnings;cargo clippy --locked --lib --no-default-features -- -D warnings;cargo test --locked --all-features;cargo test --locked;cargo doc --locked --no-deps --all-featureswithRUSTDOCFLAGS=-D warnings;cargo +1.89 check --locked --all-targets --all-features; the macOS check; the artifact bundling check;python3 scripts/nvx.py check-guest-binaries,build-guest-binaries, andtest-guest-binaries;tests/runtime-adapterin its protocol-only, host-only, and guest-only configurations. The guest-only configuration's MSRV check ran for thex86_64-unknown-linux-gnutarget; CI runs it for musl;aci_edge_sandboxes: fmt, Clippy, tests, docs, MSRV, the macOS check, andpython3 scripts/nvx.py test-agent-bundling.On Windows: the
nvx-rschecks above but the macOS check, the runtime adapter's protocol-only and host-only configurations, andaci_edge_sandboxes' Clippy, tests, MSRV check, andtest-agent-bundling.python scripts/check_markdown_links.pypasses.The privileged tests,
cargo test --all-features -- --ignored guest::mappings::privileged, pass inside real VMs on NVX's guest kernel, 6.18.38, on WHP and MSHV: a static musl build of the test binary ran as PID 1, with two processors. They also pass as root on a Linux host, with kernel 6.18, and passed in a KVM VM on an earlier base, before the last three commits. Each rename race had all three outcomes: attached, refused at a link, and moved and detached.A sandbox that maps the host's
/tmpread-only, run once by hand on MSHV in a private mount namespace whose/tmpwas a fresh tmpfs, read the host's file at/tmpand showed the mapping above the guest's tmpfs, as with this PR's base. Without the commit that lets a mapping cover the root of a mount that the root owns, its start failed withEBUSY.Real VMs, with an Alpine initramfs built from this branch:
test-aci-edge-sandboxestest-microvmscenariosvm_layered--cpu-profile host--cpu-profile host, on an earlier base, before the last three commitstest-microvmranfilesystem-shares,filesystem-policy,filesystem-owner,filesystem-comma-paths,denied-filesystem-paths,filesystem-snapshot,managed-lifecycle,lifecycle,workload-identity, andstructured-outcome, with--processors 1 2./dev/mshvis root-only there, so the tests ran as root, and five read-write mapping tests fail:host_paths_are_mapped_into_the_guest,host_paths_with_commas_keep_their_access,mapped_files_hide_their_siblings_and_names_may_hold_spaces,single_files_are_granted_without_their_directories, andunrelated_directories_keep_their_own_access. They fail identically, at the same assertions, with the initramfs built from this PR's base: the workload cannot write the root-owned host directories.Sizes of the stripped guest binaries and the Alpine initramfs, before and after, both built from source:
nvx-managed-agentinitramfs.cpio.gz(Alpine)Left to CI: KVM on this base, the full microVM matrix, MSHV runs as a non-root user, and the benchmarks.