Repository navigation
nvx-rs: export one directory to a template's clone, and map it before its start - #955
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
Host/guest filesystem integration requires human confirmation of #956’s store-path canonicalization, the OpenVMM fix for #904, and privileged multi-backend VM validation on the final restacked head.
No open issues 🎉
What changed in this PR
Adds single-directory exports to template clones in nvx-rs, implementing the export and mapping portion of #846. The restore sequence now applies mappings after Ready and before Start, using the existing failure-recording and teardown machinery.
Changes:
- Validates clone export plans, records their identities, and renders directory-mount arguments for restore.
- Supports empty mapping sources for export roots while preserving the managed agent’s rejection of them.
- Separates mapping from Start in the runtime adapter and adds protocol, planning, lifecycle, and guest-mapper tests.
The PR description tracks VM scenarios and store-path normalization in #956, plus the required OpenVMM fix in #904.
| File | Description |
|---|---|
tests/runtime-adapter/tests/host_session.rs |
Tests separate mapping and Start operations, failures, and root sources. |
tests/runtime-adapter/src/host.rs |
Extracts map_table and updates clone control framing. |
tests/runtime-adapter/README.md |
Documents root sources and clone mapping order. |
nvx-rs/tests/vectors/README.md |
Explains the version-1 empty-source vector exception. |
nvx-rs/tests/vectors/nvxc-v1.txt |
Reclassifies the empty-source vector as valid. |
nvx-rs/src/protocol/tests.rs |
Tests empty-source encoding, decoding, and target validation. |
nvx-rs/src/protocol/maps.rs |
Accepts empty sources as export roots. |
nvx-rs/src/host/template/mod.rs |
Documents clone exports and mapping-before-Start sequencing. |
nvx-rs/src/host/template/clone/tests/restore.rs |
Tests mapping order, failure attribution, and teardown. |
nvx-rs/src/host/template/clone/tests/launch.rs |
Tests export arguments and prelaunch verification failures. |
nvx-rs/src/host/template/clone/tests.rs |
Tests export planning, identities, policies, and refusals. |
nvx-rs/src/host/template/clone/restore.rs |
Sends mappings before Start and classifies mapping failures. |
nvx-rs/src/host/template/clone/record.rs |
Adds export identity and mapping count to activation records. |
nvx-rs/src/host/template/clone/reattach.rs |
Clarifies that reattach resends neither mappings nor Start. |
nvx-rs/src/host/template/clone/launch.rs |
Verifies exports and includes their restore arguments and tables. |
nvx-rs/src/host/template/clone/failure.rs |
Documents export evidence and mapping-stage progress. |
nvx-rs/src/host/template/clone/export.rs |
Implements single-directory export validation and rendering. |
nvx-rs/src/host/template/clone.rs |
Adds optional exports to clone requests and plans. |
nvx-rs/src/host/filesystem.rs |
Preserves inspection errors separately from changed-object failures. |
nvx-rs/src/guest/mappings/tests.rs |
Tests root-source mapping with the fake kernel. |
nvx-rs/src/guest/mappings/privileged.rs |
Tests root-source mount isolation and restrictions on Linux. |
nvx-rs/src/guest/mappings.rs |
Opens empty sources as . beneath the export root. |
nvx-rs/src/guest/managed_agent/mappings.rs |
Explicitly rejects empty sources before applying mappings. |
nvx-rs/README.md |
Updates clone lifecycle and guest-mapper documentation. |
doc/design/4.4-host-guest-control-interface.md |
Documents root-source semantics and vector compatibility. |
Review effort: Balanced
This branch exists only to run CI. It is never a pull request's head, and this commit never goes onto a pull request's branch. It is #958's head, 4a614d9, which holds #955, #956, and #958, with the fixes that dev carries for the failures that their review bases, pinned to OpenVMM 79a501b6, show in CI, and that the pull requests do not change: - #885's test-only 3fbaf8a, cherry-picked before this commit. On GitHub's hosted Windows runners an open with backup semantics bypasses the test's access list. - This commit, which moves the openvmm gitlink 79a501b6 -> e68871cc, the virtio-fs fix of #904 that dev pins since #918, so that a clone's export serves requests after a gated restore. The platform tier's allowlist there admits maxcpus=, so the copy of it in nvx-rs/tests/vectors/openvmm, which the CLI jobs compare with the submodule, takes that line too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
This branch exists only to run CI. It is never a pull request's head, and this commit never goes onto a pull request's branch. It is #958's head, 4a614d9, which holds #955, #956, and #958, with the fixes that dev carries for the failures that their review bases, pinned to OpenVMM 79a501b6, show in CI, and that the pull requests do not change: - #885's test-only 3fbaf8a, cherry-picked before this commit. On GitHub's hosted Windows runners an open with backup semantics bypasses the test's access list. - This commit, which moves the openvmm gitlink 79a501b6 -> e68871cc, the virtio-fs fix of #904 that dev pins since #918, so that a clone's export serves requests after a gated restore. The platform tier's allowlist there admits maxcpus=, so the copy of it in nvx-rs/tests/vectors/openvmm, which the CLI jobs compare with the submodule, takes that line too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
This branch exists only to run CI. It is never a pull request's head, and this commit never goes onto a pull request's branch. It is #958's head, 4a614d9, which holds #955, #956, and #958, with the fixes that dev carries for the failures that their review bases, pinned to OpenVMM 79a501b6, show in CI, and that the pull requests do not change: - #885's test-only 3fbaf8a, cherry-picked before this commit. On GitHub's hosted Windows runners an open with backup semantics bypasses the test's access list. - This commit, which moves the openvmm gitlink 79a501b6 -> e68871cc, the virtio-fs fix of #904 that dev pins since #918, so that a clone's export serves requests after a gated restore. The platform tier's allowlist there admits maxcpus=, so the copy of it in nvx-rs/tests/vectors/openvmm, which the CLI jobs compare with the submodule, takes that line too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
This branch exists only to run CI. It is never a pull request's head, and this commit never goes onto a pull request's branch. It is #958's head, 4a614d9, which holds #955, #956, and #958, with the fixes that dev carries for the failures that their review bases, pinned to OpenVMM 79a501b6, show in CI, and that the pull requests do not change: - #885's test-only 3fbaf8a, cherry-picked before this commit. On GitHub's hosted Windows runners an open with backup semantics bypasses the test's access list. - This commit, which moves the openvmm gitlink 79a501b6 -> e68871cc, the virtio-fs fix of #904 that dev pins since #918, so that a clone's export serves requests after a gated restore. The platform tier's allowlist there admits maxcpus=, so the copy of it in nvx-rs/tests/vectors/openvmm, which the CLI jobs compare with the submodule, takes that line too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
96b4fbd to
08d111c
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Host-directory isolation and restore-time mount sequencing cross platform-specific security boundaries, requiring human confirmation of exact-head CI and KVM, MSHV, and WHP VM validation before approval.
No open issues 🎉
Review effort: Balanced
2674fd2 to
efe51b4
Compare
08d111c to
e7959b3
Compare
There was a problem hiding this comment.
🟡 Changes recommended
On Linux, replacing a pinned path with a self-referential symlink incorrectly produces HostFailure instead of InvalidMappings; symlink-loop errors need changed-object classification with the cause preserved and a regression test.
1 open issue
Review effort: Balanced
A clone may now share host paths with its root. CloneRequest takes zero or one ExportPlan that filesystem::plan made for the container root. A template's filesystem slot is dormant, and OpenVMM binds one directory to it at a restore, so the plan must export exactly one child, a directory, which is then the export's root: - ClonePlan::new refuses, before anything is opened, a plan that check() rejects, such as mappings without an export or an export without mappings, a plan of several directories or files, and a file. It also refuses a directory inside the template store, or one that holds it, comparing the paths that the plan resolved with the store's, without reading anything. Each refusal is a CloneFailure at the plan's stage, whose evidence is the request's mappings, InvalidMappings, and which the caller holds. - The activation record names the export, a directory, by the identity of its plan, the SHA-256 digest of a domain and of the plan's serde form, and announces its mappings, the plan's binds, from 1 to 4,096. A clone without an export has neither, and its slot stays dormant. - ClonePlan::launch checks the plan with ExportPlan::verify before it writes anything, as a cold start does, and binds the directory with --mount, its hidden, re-exposed, and writable paths, and its owner. A refusal of verify, such as an object that changed or went missing since the plan, is the request's mappings, InvalidMappings, and any other failure of it the host's own; both come before the progress log, so nothing records them. To tell them apart, verify now keeps the cause of a failure to inspect an object, with ErrorKind::Io, unless the failure shows that the object's path no longer reaches it, which it still takes for a changed object, at a cold start as at a restore. - ClonePlan::table returns the binds as the mapping table, whose sources name paths in the directory without the child's index. - Activated::start sends the table through a new CloneControl::map once the clone is ready, in order, and records and sends the start only once the guest applied every mapping, both by the Ready-to-Start deadline. Neither is sent again. A partial or failed table ends the launch in the one place where a failed start ends it: the failure is recorded as pending, the launch ends through Starting::abandon, and the record is written again with the teardown. It fails at the start's stage with the clone's progress still at Ready, which tells it from a failure of the start, whose progress is Starting. - A table that the guest refused, once the framing tells it through the new provided method CloneControl::refused, is the request's mappings, InvalidMappings, whenever the refusal came. Any other failure of the table, every failure by default, is what the host saw of it: a passed deadline, Timeout; a lost session, Reset; or a broken protocol, Protocol. None is the template's, so none quarantines, and an activation status with which the guest ended its VM still prevails, since no workload ran before the start: the export's status, 56, makes the failure InvalidMappings. A reattach never sends a mapping table: the launch of a live clone committed once the clone applied it and started, and a reattach ends an interrupted launch. Activated::fail no longer gives the mapping of a host directory as an example of a step of the caller's own, since the restore sequence maps a clone's export itself. The test adapter's CloneOrder now takes only the guest agent's build: host::map_table, the table half of host::start_sandbox, implements map(), and start() sends Start alone. It keeps the default refused(): the guest's MapFailed does not tell whether the request's mappings or a failure of the guest's own failed the table, so the restore sequence takes it for a broken protocol, unless the deadline passed or the session was lost. The adapter's host program builds it in clone_order(), the one place that makes it, for a clone's start and a reattach to it. The adapter's requests name no export yet, CloneRequest.export: None, so its clones still map nothing, and the VM tests' Early framing, which ends before the clone is ready, gains a map() that it never reaches. Tests, with a fake VMM and a fake control: the record, table, and arguments of a clone with and without an export; refusals of two directories, a file, mappings without an export, an export without mappings, a reserved target, and an export that overlaps the store, with their evidence; a mapped object and an exported directory that changed or went missing since the plan, refused before the launch, and an object that cannot be inspected, the host's failure; the table after Ready and before the start; a partial, refused, failed, lost, or silent table, and a refusal past the deadline, after which the launch ends, with no start and no retry, and the sandbox records the failure at the start's stage, at Ready, with its evidence; and a table that failed as the guest ended its VM with the export's status, which prevails. ExportPlan::verify's own tests tell a path that no longer reaches its object from a failure to inspect it. The adapter's fake broker checks that the order stops the guest at a part that it fails, and takes that failure for no refusal of the request's. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
A clone whose export is one directory gets that directory alone through
the template's dormant slot, so its mapping table maps the directory
itself with an empty source: the export's root has no child index, as
an aggregate's children do. The MAPS protocol and the guest now accept
an empty source as the export's root:
- nvx::protocol::maps accepts it in the encoder and both decoders.
- nvx::guest::mappings::Mapping::new takes it for the source root
itself, which the mapper opens as "." beneath the root with the same
RESOLVE_BENEATH, RESOLVE_NO_SYMLINKS, RESOLVE_NO_MAGICLINKS, and
RESOLVE_NO_XDEV flags, so the mapping is a copy of the export's own
mount, without its submounts.
Everything else stays refused: an absolute source ("/" included), an
empty, "." or ".." component inside a non-empty source, a NUL byte, a
path of more than 4,095 bytes, and every target that was refused
before. NVX's managed agent, whose export is always an aggregate whose
root lists its children, keeps refusing an empty source, with one check
before it maps anything, so NVX's own sandboxes accept no more than
before. The host and the guest deploy together, so no capability
discovery announces the change.
Version 1's conformance vector of an empty source moves from the
rejection vectors (INVALID_MAPS|EMPTY_SOURCE) to the mapping vectors
(MAPS|ROOT_SOURCE), with the same bytes. That is an exception to the
rule that a vector changes only with a protocol version, which the
vectors' README and design section 4.4.4 now record: only the clone of
a template whose export is a directory sends an empty source, to the
template's own guest, which deploys with the host, so the C agent of
earlier releases, which refuses it, never receives one.
The tests cover an empty source's round trips through the encoder and
both decoders, its vector, the targets that it still needs, the mapping
of the source root beneath it, with a fake kernel and as root with the
real one, the refusals of a symbolic link and of a mount crossing
beneath it, the managed agent's refusal, and a clone's table, with its
root bind, through the MAPS payloads of the fake transport and of the
test adapter's order.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
`runtime-adapter-host start` takes the shares of a launch, `--share HOST:TARGET[:ro]`, `--deny`, `--reserved`, and `--share-root`, with the same rules: `--deny`, `--reserved`, and `--share-root` need a `--share`. `clone::Request` carries them as `sharing::HostFsSharing`, and `Request::export` plans their export as a launch plans it, with NVX's `filesystem::plan` for the container root, so the start refuses shares that it cannot export, with status 1, before it reads the template store, needs NVX's artifacts, or creates the sandbox's store. `Request::clone_request` takes the planned export into NVX's `CloneRequest`, whose `ClonePlan` takes only an export of one directory, which the clone's restore binds to the template's dormant filesystem slot, and refuses others, as a failure of the request, before any VMM starts. The restore sequence sends the export's mapping table through the adapter's clone order once the clone is ready, and `mapped` now prints the number of mappings that the clone's activation record announces, which the guest applied before its sandbox started. Tests without a hypervisor cover the new usage errors, the refusals before the template store, and the export that NVX's request carries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
The VM tests start clones that share host directories with their roots, through `runtime-adapter-host start` and its shares, which NVX exports as one directory, bound to the template's dormant filesystem slot, and whose mapping table the restore sequence sends once the clone is ready. In vm_clone: - A clone over a scratch image shares one host directory: the directory itself, read-only, a read-write directory inside it, at its target there, a read-only file, and a read-write directory, with a file hidden. It starts with four mappings, and its activation record names the export, a directory, by the identity of its plan, with four mappings. On Linux, its VMM has one `--mount`, of the directory, read-write. The guest mounted the export once, read-write, nosuid and nodev, and the mappings below the root, in the table's order, each with its access, showing what the host holds but for the hidden file. An execution finds the same, and the writable mappings alone take its writes, which the host's directory holds, before and after the stop. - Four clones of one generation at once: one with a read-write export of its own and a generated machine ID, two over the same read-only export, and one without an export. Each record names its export by the identity of its plan, the same for the clones of one export, and the clone without one has none, no mapping, and on Linux no `--mount`, which leaves the slot dormant. Each export is mounted read-write, even where its mapping is read-only; the mount table lists the bind of the generated machine ID beside the mapping; and the helper of the `entries` probe, which writes the root's machine ID, finds that file read-only. - A start refuses two directories, or a file, before any VMM starts, with status 8 and the record of a failure of the request, at the plan, by the evidence of invalid mappings. - A clone whose second mapping puts a directory onto a file of the image ends at its Ready, before its sandbox starts, with status 8, the guest's report that the table failed after its first mapping, and the record of a failure of the start, by the evidence of the protocol: nothing quarantined, the lease released, and the host's directory unchanged. The harness reads a clone's activation record from its sandbox, and on Linux the arguments of its VMM. Part of #846 Part of #845 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
ClonePlan::new refuses an export that overlaps the template store, whose leased payloads nothing may change. It compared the paths that the export's plan resolved with the store's directory as the store names it, so another spelling of the store's path hid an overlap: a link; on Windows, another letter case, a short name, or the verbatim prefix; and on Unix, a `..` component, which an absolute path keeps. The plan now resolves the store's directory with filesystem::canonicalize, as filesystem::plan resolves the export's paths, before it compares them. The host still reaches the store, and OpenVMM the snapshots in it, by the path that the store names, so the plan also resolves the directories in which the host looks up the names of that path, links followed and `..` included, and refuses an export that lets the guest write in one of them: the guest could otherwise replace a link of the path, and turn the host's later operations on the store elsewhere, or make a directory of the path unsearchable, and keep the host from the store. It does both only for a clone with an export, so a clone without one reads nothing more. A store whose path cannot be resolved fails the plan as the host's failure. The plan also refuses an export whose directory is, or holds, the directory of the clone's sandbox, in which the host writes the clone's records by their paths: the guest could otherwise change them, or replace the directory with a link and turn the host's writes elsewhere, into a leased snapshot, say. The lease names the sandbox by its path with its links resolved, so the directories through which that path leads are the ones above it, which only such an export could let the guest change. A directory inside the sandbox's, among the caller's files, may still be exported. These checks compare paths, so an alias of the store that an administrator configures, such as a bind mount of it, is outside them, as the directories above the store are, and they cannot see what changes after them: OpenVMM resolves an export's path again at each launch and restore. The documentation of the export, in ClonePlan's and in the README, now says that whoever names an export must not name such an alias, nor a directory that holds one, nor one inside a directory that a writable export of another sandbox or clone holds: the directories above an export's directory must be ones that no guest may change, as those above the clone's block devices must. The test of the overlap names its store in another spelling of its directory: through `..` on Unix, and with the verbatim prefix on Windows. Another names it through two links, one to the other's directory: it refuses a writable export of the directory of either link, but not a read-only one, nor one whose writes the plan limits to a directory beside the link. A third names it through a directory and its `..`, which Unix looks up in that directory: Unix refuses a writable export of the directory, and Windows, which resolves `..` from the path's text, accepts it. A fourth refuses an export of the sandbox's directory, and of the directory that holds it, but not of a directory inside the sandbox's. Part of #846 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 34e6ffe3-3ebf-42db-bfca-e0ecfade6873
d48bf38 to
551ebda
Compare

Chain: #951 → #954 → #957 → #959 → #960 → this PR → #958, on
dev; #879, #881, #885, #886, #877, #932, #884, #887, #933, #880, #936, #937, #939, #940, #941, #888, #949, and #950 have merged.Stacked on #960. Part of #846 (PR 1 of 2: one directory export, and MAPS), and of #845, whose export scenarios it holds. Since the final restack, it also carries the commits and the VM scenarios of #956, which folded into it and closed.
Commits
nvx-rs: export one directory to a clone and map it before its start: the export, its record, and the mapping table after Ready, below. It also holds what was tests: start clones with exports in VMs #956's first commit, which lets the test adapter's restore sequence send a clone's mapping table:clone_order()buildsCloneOrder { build }, a clone's request names no export yet, and the harness'sEarlyframing gains amap()that it never reaches. runtime-adapter: start clones of templates #954 builds NVX'sCloneRequest, the adapter'sCloneOrderwith a table, and aCloneControlwithoutmap(), so without those changes the test adapter's host configuration does not compile at this commit.nvx-rs: let an empty mapping source name the export's root.runtime-adapter: let a clone's start share host paths with its root: see Starting a clone with shares.runtime-adapter: start clones with exports, in VMs: the VM scenarios below.nvx-rs: resolve the template store's path in a clone's export check: the checks of the store's path and of the sandbox's directory, below.A directory export maps its root with an empty source
A clone's export is one host directory, which OpenVMM binds to the template's dormant filesystem slot at the restore. The guest's mount of the export is that directory itself, not an aggregate whose root lists numbered children, so the directory's own bind has no child index to name. The clone's mapping table therefore maps the directory itself with an empty source, and the paths inside it with sources relative to it, such as
workrather than0/work.The
MAPSprotocol and the guest now accept an empty source as "the export's root":nvx::protocol::mapsaccepts it in the encoder and in both decoders.nvx::guest::mappings::Mapping::newtakes it for the source root itself. The mapper opens it as.beneath the root, with the sameopenat2resolution as every other source (RESOLVE_BENEATH,RESOLVE_NO_SYMLINKS,RESOLVE_NO_MAGICLINKS, andRESOLVE_NO_XDEV), so the mapping is a copy of the export's own mount, without the file systems mounted below it.What stays refused:
/included; an empty,., or..component inside a non-empty source; a NUL byte; and a path of more than 4,095 bytes./itself.Why: the template's slot binds a single directory, and an empty source is the only spelling of its root that keeps every other source rule intact. The host and the guest deploy together, and a template's key names its guest's build, so no feature bit or protocol version announces the change. Version 1's conformance vector of an empty source therefore moves, with the same bytes, from
INVALID_MAPS|EMPTY_SOURCEtoMAPS|ROOT_SOURCE. The vectors' README and design section 4.4.4 record it as the one exception to "a vector changes only with a protocol version": only the clone of a template whose export is a directory sends an empty source, to the template's own guest, so the C agent of earlier releases, which refuses it, never receives one.Exporting one directory to a clone
CloneRequesttakes zero or oneExportPlanthatfilesystem::planmade for the container root. The plan must export exactly one child, a directory, which becomes the export's root.ClonePlan::newrefuses, before any VMM starts: a plan thatcheck()rejects, such as mappings without an export or an export without mappings; several children; a file; a reserved target; a directory inside the template store, or one that holds it; one that lets the guest write in a directory through which the store's path leads, links followed and..included, where the guest could turn the host's later operations on the store elsewhere, or keep the host from the store; and one that is, or holds, the directory of the clone's sandbox, in which the host writes the clone's records. The store check compares the paths that the plan resolved with the store's directory, which it resolves withfilesystem::canonicalize, so that no other spelling of its path hides an overlap. It resolves the store's path only for a clone with an export, so a clone without one reads nothing more. These checks compare paths, so whoever names an export must not name an alias of the store that an administrator configures, such as a bind mount, which no check of paths reveals, nor a directory that holds one, nor one inside a directory that a writable export of another sandbox or clone holds, since OpenVMM resolves an export's path again at each launch and restore.ClonePlan::launchchecks the plan withExportPlan::verifybefore each restore, as a cold start does, and binds the directory with--mount, with its hidden, re-exposed, and writable paths and its owner.ClonePlan::tablereturns the binds as the mapping table: each source is a path in the directory, without a child index, or empty for the directory itself.Mapping before the start
CloneControlgainsmap(session, table, deadline).Activated::startsends the table once the clone is Ready, in order, and records and sends Start only once the guest applied every mapping, both by the Ready-to-Start deadline. Neither is retried, and no Start follows a failed table.Starting::abandon, and the record is written again with its teardown. It is recorded at the start's stage with the clone's progress still atReady, while a failed Start has progressStarting, so the record tells the two apart without a new stage or progress step, and its format is unchanged.Activated::failno longer gives the mapping of a host directory as an example of a caller's own step, since the sequence maps a clone's export itself.How a clone's export fails
Every failure is a
CloneFailurewhose evidence follows #844's attribution:check()rejects, several children, a file, or an export that overlaps the template store, lets the guest write in a directory through which the store's path leads, or is or holds the sandbox's directory, refused byClonePlan::newInvalidMappingsHostFailure, orResourceExhaustionwhen the error proves itExportPlan::verify, before the launchInvalidMappingsExportPlan::verifyto inspect an object that shows nothing of it, such as an I/O errorHostFailure, orResourceExhaustionwhen the error proves itCloneControl::refusedInvalidMappingsTimeout,Reset, orProtocolInvalidMappings, with status 56Refusals before the launch come before the progress log, so the caller holds them and the sandbox records nothing. To tell a changed object from the host's own failure,
ExportPlan::verifynow keeps the cause of a failure to inspect an object, withErrorKind::Io, unless the failure shows that the object's path no longer reaches it (not found, not a directory, or permission denied, as for the supplied images), which it still reports as a changed object. A cold start that verifies an export reports such an I/O error the same way;filesystem::planis unchanged.CloneControl::refusedreturns false by default: a framing whose answers cannot tell a refusal of the table from a failure of the guest's own leaves it unknown rather than blaming the request. A guest status still counts after a failed table, since the start was not sent and no workload ran. No failure of the export is the template's, so none quarantines a generation.The test adapter's
CloneOrdertakes only the guest agent's build.host::map_table, the table half ofhost::start_sandbox, implementsmap()and stops at the first part that the guest fails;start()sends Start alone; cold boots are unchanged. It keeps the defaultrefused(), because the guest'sMapFaileddoes not tell whether the request's mappings or a failure of the guest's own failed the table, so the sequence takes it for a broken protocol.Starting a clone with shares
runtime-adapter-host starttakes a launch's shares,--share HOST:TARGET[:ro],--deny,--reserved, and--share-root, with the same rules:--deny,--reserved, and--share-rootneed a--share.clone::Requestcarries them, as a launch'ssharing::HostFsSharing, andRequest::exportplans their export as a launch plans it, withfilesystem::planfor the container root, so a start refuses shares that it cannot export, with status 1, before it reads the template store, needs NVX's artifacts, or creates the sandbox's store.Request::clone_requesttakes the planned export into NVX'sCloneRequest, andmapped=prints the number of mappings that the clone's activation record announces.Tests
ExportPlan::verify's own tests tell a path that no longer reaches its object from a failure to inspect it...on Unix and with the verbatim prefix on Windows; another names it through two links, symbolic links on Unix and junctions on Windows, and refuses a writable export of the directory of either link, but not a read-only one, nor one whose writes the plan limits to a directory beside the link; a third names it through a directory and its.., whose writable export Unix refuses and Windows accepts; and a fourth refuses an export of the sandbox's directory, and of the directory that holds it, but not of a directory inside it.Ready, with its evidence and a verified teardown; and a table that failed as the guest ended its VM with status 56, which prevails. The fake control carries each table through theMAPSpayloads, so the root bind's empty source crosses the real encoder and decoder.MAPS|ROOT_SOURCE, and still needs a valid target;/stays refused.ELOOP) and mount crossings (EXDEV) still refused.clone_start: the start's new usage errors (--deny,--reserved, and--share-rootwithout a share, and malformed shares); shares that it cannot export, refused with status 1 before the template store; and the export that NVX's request carries: none without shares, and with them one directory, the root of a read-only share that holds a read-write one, mapped from an empty source and from its path.vm_clone), on KVM, MSHV, and WHP:--mount, of the directory, read-write. The guest mounted the export once, read-write,nosuid, andnodev, and the mappings below the root in the table's order, each with its access, showing what the host holds but for the hidden file. An execution finds the same, and the writable mappings alone take its writes, which the host's directory holds before and after the stop.--mount, so its slot stays dormant. Each sees only its own directory, and the records of one export name it by the same identity. An export is mounted read-write even where its mapping is read-only, the mount table lists the bind of the generated machine ID beside the mapping, and the helper of theentriesprobe, which writes the root's machine ID, finds that file read-only.Ready, before its sandbox starts, with status 8, the guest's report that the table failed after its first mapping, and the record of a failure of the start, by the evidence of the protocol: nothing quarantined, the lease released, and the host's directory unchanged.What a VM cannot reach stays with the tests above: a mapped object that changed since the plan, a refused, lost, or silent table, and the export's status 56, which no VM provokes without fault injection.
Validation
At this PR's head 551ebda, the commits of the earlier head 08d111c, rebased onto #960's head efe51b4, include one fix to the first commit, from a review thread.
ExportPlan::verifynow reports a path whose links loop, such as a link that names itself in place of a pinned object, as a changed object, with the host's error as its cause, rather than as a failure of the host. Without that fix, this PR's tree, 02237d22, is the merge of 08d111c intodevat fbeb563, which holds #951, #954 and #957. CI at #958's head e19ceed, which holds this PR unchanged, in run 40901423035: 36 jobs passed, among them the microVM tests and the platform checks on KVM, MSHV and WHP, and OpenVMM's unit and VM tests; the six that run only for a pull request or ondevwere skipped. CI at #958's head before the fix, 2a777a3, in run 40896659416, passed the same way, so it covers everything but the fix. A pre-check of 02237d22 passed on Linux and on Windows: the library's documentation with-D warnings; the test adapter's documentation in its protocol-only and host configurations, and in its musl guest configuration on Linux; validate-nvx; and the Markdown links. For the fix, on Windows: fmt; Clippy with-D warningsof all targets with all features, of the library with the default features and without them, and of all targets with all features forx86_64-unknown-linux-musl; the tests ofhost::filesystemandtests/host_modules.rs; the documentation with-D warnings; and the MSRV check. On Linux x86_64: Clippy of all targets with all features; the tests ofhost::filesystem, among them one that puts a link that names itself in place of a pinned path; and the aarch64-apple-darwin check. The rest of this section describes the validation at 08d111c, on #960's earlier head 2674fd2.At the final restack, onto #960's head with OpenVMM 832d5bb4, each commit on its own:
nvx-rs's fmt; Clippy with-D warningsof all targets with all features, of the library with the default features and without them, and of all targets with all features forx86_64-unknown-linux-musl;tests/host_modules.rs; and the library tests ofhost::template,host::filesystem,host::network, andprotocol. The test adapter's fmt; Clippy of its protocol-only, host, andx86_64-unknown-linux-muslguest configurations; and its protocol-only, host library,host_session, andclone_starttests. The check of the README's module table inscripts/test_nvx_tools.py.guest::mappingsandguest::managed_agenttests, as the user and as root with the root-only tests; and the test adapter's guest library andguest_sessiontests forx86_64-unknown-linux-musl.-D warnings, MSRV 1.89 ofnvx-rsand of the test adapter's three configurations, and the Markdown link check.At #958's head, which holds this pull request unchanged:
vm_clonepassed 25 of 25, this pull request's export scenarios included, andvm_template9,vm_layered43,vm_restart3, andvm_steps3.nvx-rschecks (fmt; Clippy of all targets with all features, of the library with the default features, and of the library without them; tests with all features, the defaults, and without default features; docs; MSRV 1.89), Clippy of its guest code forx86_64-unknown-linux-musl,tests/host_modules.rs, the test adapter's protocol-only and host checks and Clippy of its guest, the check of the README's module table,scripts/nvx.py verify, and the Markdown link check.nvx-rschecks, its macOS check, and Clippy of its guest code forx86_64-unknown-linux-musl; the lint and the tests of the guest binaries (scripts/nvx.py check-guest-binaries); the test adapter's protocol-only, host, and guest configurations, each with its Clippy, build, tests, and MSRV; and, as root, the tests that need it (198 of 198, the 13 that only root runs included).nvx-rs downloadinstalled, and the initramfs and the test adapter's guest agent built from the tree:vm_clone25 of 25,vm_template9,vm_layered43,vm_restart3, andvm_steps3.These runs hold for #958's head, e9e7fc6, by tree identity: the CI workflow ran at cfdc779, and the Windows, Linux, and VM runs at 954d098, before #958's fix-ups were squashed into its second commit, which changed no file. cfdc779 has the tree of #958's head, 72dea16c, and 954d098's tree differs from it in a comment alone.