Skip to content

nvx-rs: lower adapter plans into the plans of template captures - #884

Open
Enrique Saurez (esaurez) wants to merge 16 commits into
esaurez/839-pr4-supervisor-seam-20261024from
esaurez/841-pr1-template-lowering-20261024
Open

Enrique Saurez (esaurez) wants to merge 16 commits into
esaurez/839-pr4-supervisor-seam-20261024from
esaurez/841-pr1-template-lowering-20261024

Conversation

@esaurez

@esaurez Enrique Saurez (esaurez) commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #886; this PR retargets when its parent merges.

Chain: #871 → #879 → #881 → #885 → #886 → this PR.

Part of #841 (PR 1 of 3: template lowering).

What and why

A template is a capture of a microVM at OpenVMM's platform tier, after the VM consumed only its invariant configuration, for clones that each bind their own image, upper, and export when they are restored. Before a store can capture one, the host needs the exact VM and options of that capture, and they must meet OpenVMM's rules for the platform tier: a kernel command line of allowlisted tokens only, no tenant data, and nothing that a snapshot cannot hold.

nvx::host::template::TemplatePlan::new lowers an adapter's SandboxPlan (SandboxPlan::adapter) into that plan, without I/O:

  • The placeholder distro, bound through its DistroBinding: the plan's image must be the format and identity that the binding names, and the length that the binding records is the capacity of every clone's image (image_capacity()). Its path must be absolute, since OpenVMM would resolve a relative one against the directory in which it runs; it may hold no comma or semicolon, which OpenVMM's block device options would split; and its name may not end in .vhd, .vhdx, .vmgs, or .iso, which OpenVMM would not attach as a raw image, as a sandbox's validation and PreparedLaunch check them.
  • A dormant scratch device in place of the adapter's upper, which is a clone's: SCRATCH_NAME (scratch.raw) in the capture's transaction directory, a sparse file whose length (scratch_length()) and block sizes the device takes, and which the template does not keep.
  • The dormant filesystem slot: the plan exports nothing, and OpenVMM exposes the slot at every cold boot.
  • No NIC, or one fixed NIC under the template's policy; a policy that lets nothing through attaches none. A policy that allows ingress, or host-loopback access both ways (which forwards need), is refused: OpenVMM publishes the guest's listeners through them as live host attachments, and refuses them for snapshots.
  • TemplateResources: 1, 2, 4, or 8 processors, all online at boot (B = C), and RAM with --memory-capacity, both in blocks of 128 MiB (MEMORY_BLOCK_SIZE). The capacity is always rendered: without it, a capture records no memory expansion, and TemplateManifest::read refuses it.
  • The kernel command line nvx_sandbox=1 (kernel_command_line()), which the caller passes as --cmdline alone, in place of the adapter plan's, which names the root's image identity. OpenVMM writes the other tokens itself, each on its platform allowlist: the console, reboot, and panic settings, nr_cpus=, nvx_snapshot_tier=platform, the virtio devices and the control tty, and the NIC's addresses.
  • The capture's options: --snapshot-tier platform, --snapshot-destination at SNAPSHOT_NAME (snapshot, the name by which a generation of the template store names its snapshot) in the transaction directory, and --snapshot-block-identity generation with the capture's StorageGeneration as --snapshot-generation-id, so that the capture hashes no block device. The template keeps no storage generation: OpenVMM unbinds its distro, and its scratch is fresh.

openvmm_arguments() renders the devices by path, as SandboxPlan::openvmm_arguments does, then capture_arguments(), which a caller that renders the devices' arguments itself can take alone.

The bound-block storage generation is #881's StorageGeneration, rendered with its Display; this PR adds no type of its own for it, and derives it from none of the other namespaces of a template's generations.

Outside nvx-rs/src/host/template/plan.rs and its tests, the change declares and re-exports the module, makes sandbox::reject_disk_path, sandbox::reject_container_path, and SandboxDistro::pure_path crate-visible, extends the OpenVMM vector test, and updates the docs.

Decisions to review

  • nvx_config=0xd0010000,65536 is on OpenVMM's allowlist, but nothing in NVX defines or reads it, so a template does not pass it. Passing it later changes one constant.
  • The transaction directory must be valid Unicode and absolute, and its path may hold no comma or semicolon, which OpenVMM's block device options would split. The placeholder's path must be absolute too, for the same reason.
  • For the capture itself (PR 2): with --snapshot-destination, OpenVMM opens each block device itself, without following a link at its path (O_NOFOLLOW; FILE_FLAG_OPEN_REPARSE_POINT), and on Windows without sharing write access. A PreparedLaunch's /proc/self/fd/N paths, and on Windows its inherited scratch claim, therefore don't serve a capture at this OpenVMM revision, and the docs here name no such composition. PR 2 therefore hands OpenVMM the paths of the files that it checked, and holds no writable handle on them when OpenVMM starts.

Tests

  • Each token against OpenVMM's allowlist: the template's command line, and the tokens that OpenVMM derives from the rendered arguments (nr_cpus=, nvx_snapshot_tier=, virtnet_ip=, virtnet_gw=), are checked with OpenVMM's own allowlist function, compiled from verbatim excerpts of its source in nvx-rs/tests/vectors/openvmm/. The ignored test of tests/openvmm_vectors.rs, which the NVX CLI jobs run, checks that the excerpts are whole lines of the submodule's files.
  • No tenant data: for both image formats, both uppers, six networks, and every processor count, neither the command line nor the arguments hold the image's identity or the clone's upper, the command line holds no proxy, and the arguments hold only the options of the devices, the resources, and the capture, with ingress and host loopback denied.
  • The rendered arguments, exactly: without a NIC, and with NICs under policies that allow egress, egress with host-loopback access from the guest, and a proxy alone.
  • Each refusal and their order, and the normalization of the transaction directory.

Validation

No VM is needed for this change: everything below ran without one.

  • Windows: in nvx-rs, with NVX_BUNDLE=skip: cargo fmt --all --check; cargo clippy --locked --all-targets --all-features -- -D warnings; cargo clippy --locked --lib --no-default-features -- -D warnings; cargo clippy --locked --all-targets --no-default-features --features <feature> -- -D warnings for host, cli, testing, serde, and bundled; cargo test --locked --all-features; cargo test --locked; cargo build --locked --lib --no-default-features and cargo test --locked --lib --no-default-features, also with --features host; RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps --all-features; cargo +1.89 check --locked --all-targets --all-features; and, from the root, the ignored cargo test --locked --manifest-path nvx-rs/Cargo.toml --no-default-features --test openvmm_vectors -- --ignored. Then the commands of check-aci-edge-sandboxes for aci_edge_sandboxes, those of check-nvx-rs for tests/runtime-adapter (protocol-only and host-only), and python scripts/check_markdown_links.py.
  • Linux: the same, plus --features guest, cargo check --target aarch64-apple-darwin for nvx-rs and aci_edge_sandboxes, python3 scripts/nvx.py check-guest-binaries, and the runtime adapter's static musl guest configuration.
  • The ignored excerpt test fails when an excerpt's line differs from the submodule's (checked by editing a copy, then restoring it).
  • Left to CI: the full matrix, once this PR targets dev; a stacked PR runs none.

Enrique Saurez and others added 5 commits October 24, 2026 10:38
A clone that the host restores from a template keeps the template's
captured configuration and command line, so nothing tells its guest the
image, upper, export, network settings, and workload view of its own
launch, and nothing identifies the settings that a guest applied
(#838). The host, the guest, and every adapter's framing need one
record of them, which they all check by the same rules.

nvx::activation defines it without features, beside nvx::protocol. An
ActivationRecord holds the version of its schema, the restore section
of a clone, which names the restore's transaction ID and the template
that it restores, by its store generation and the identity of its key,
and the Configuration that the activation applies: the image, with the
identity of its binding and the UUID or disk GUID that the guest
expects; the upper, a tmpfs or the scratch with its binding's identity
and journal policy; the overlay mode; one export, a directory or an
aggregate, with its plan's identity and its 1 to 4,096 mappings; the
network, with its policy's identity, a DNS address, the listeners'
publication, and a proxy endpoint; the workload view, its hostname,
machine-ID mode, and optional workload identity; and the CPU count and
memory size that the guest must have. The record names objects by
32-byte identities that the host computes, and never holds their
content; the other inputs of the guest's root assembly are invariants
of the template. ReadyActivation is what Ready reports: the
configuration identity, and a clone's transaction ID.

The limits are public constants: 1,024 bytes for a record, 4,096
mappings, 64 bytes for a hostname, and 255 for a proxy endpoint.
ActivationRecord::new admits a configuration only once
Configuration::check finds that it keeps them, pairs an export with its
mappings, and holds a nonzero UID and GID, a hostname without a NUL, as
the guest's entry requires, a proxy endpoint of visible ASCII without
an @, and so without credentials, and nonzero sizes. The canonical
encoding and the configuration identity follow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f7bd07b-b09f-48d5-bc49-ad06f47ef3f9
The kernel reads an ID of u32::MAX as "unchanged", so the guest's entry
refuses it, and the managed agent ignores such a workload ID on its
command line. Configuration::check refused only 0, so a record could
hold a workload identity that no guest applies. It now refuses
u32::MAX too, for the UID and the GID alike, and the tests take
u32::MAX - 1 as the largest ID that a record holds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f7bd07b-b09f-48d5-bc49-ad06f47ef3f9
The template store publishes each template under a nonzero 64-bit ID,
which it assigns in the order of publication and never reuses, and a
clone's record names the template that it restores by that ID and the
identity of the template's key (#838, #839). StoreGeneration held 16
opaque bytes, so the store would have had to fit its ID into bytes of
another shape, by a convention of its own.

StoreGeneration now wraps a NonZeroU64: StoreGeneration::new takes one,
StoreGeneration::get returns it, two generations of one store compare
in the order of their publication, and a generation displays as its
decimal number. The identity of the template's key stays 32 bytes, and
the transaction ID 16.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f7bd07b-b09f-48d5-bc49-ad06f47ef3f9
A template's compatibility key must hold the version of every contract
through which its clones are restored. A runtime identity records the boot
and control versions, but none of those of OpenVMM's snapshot format, the
schema of the saved device state, the time ABI, and the restore packet.

Add them to nvx::contract with the values of the pinned OpenVMM, and derive
the guest's restore-packet decoder version from its constant. The ignored
test of tests/openvmm_vectors.rs, which the NVX CLI jobs run with the
submodule checked out, now also checks each version against the constant
in OpenVMM's source that it must match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
A template is a capture of a microVM from which independent clones are
restored. Which clones it can serve, and on which hosts, depends on small
metadata: the runtime that captured it, the contracts through which its
clones are restored, and the machine that they restore. Stores, adapters,
and restores need one key that holds exactly that metadata, which they
compare, and whose canonical bytes and identity never vary between hosts or
builds.

Add nvx::host::template:

- TemplateManifest::read reads the manifest that OpenVMM wrote for a
  capture, at most 1 MiB of a regular file that is not a link or other
  reparse point, through a strict reader of the protobuf wire format that
  refuses unknown, mistyped, repeated, and truncated fields. It refuses a
  capture that is not a template's: a platform-tier capture for clones,
  with a read-only distro whose identity is unbound, a fresh scratch, and
  the dormant filesystem slot.
- CaptureClass holds the invariant inputs of a capture, known before it,
  and TemplateKey::new checks that the capture made the machine that its
  class requested before it derives the key from metadata alone. It never
  reads or digests an artifact.
- A key and a class each have one canonical, versioned encoding, and their
  identities are domain-separated SHA-256 digests of it. TemplateKey's
  from_bytes refuses any other encoding. Besides the versions of the
  snapshot contracts in nvx::contract, a key holds the version of the
  activation contract, nvx::activation::VERSION, and a key's identity
  converts without loss to and from the nvx::activation::Identity by which
  a clone's activation record names its template.

The fixtures are manifests that OpenVMM's own writer encoded, so the tests
check the reader against OpenVMM's encoding and not only against the test
encoder. The vectors of tests/vectors/template-key-v1.txt, which an
independent encoder wrote, fix version 1 of the encoding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The scoped, I/O-free lowering and supporting tests have no identified blocking issues.

No open issues 🎉

What changed in this PR

Adds pure adapter-plan lowering for platform template captures in nvx-rs, the first stage of #841’s capture-and-publication work. This PR prepares capture configuration without launching a VM or publishing a template.

Changes:

  • Adds TemplatePlan and TemplateResources, validating placeholder bindings, resources, network policies, and transaction paths.
  • Renders dormant devices, tenant-free kernel options, and generation-based snapshot arguments.
  • Adds lowering and allowlist tests, verifies copied OpenVMM source excerpts, and documents the API.
File Description
nvx-rs/​tests/​vectors/​README.md Documents OpenVMM source excerpts and verification.
nvx-rs/​tests/​vectors/​openvmm/​platform_command_line_token_is_invariant.rs Copies the platform token allowlist for tests.
nvx-rs/​tests/​vectors/​openvmm/​microvm_control_tty_command_line.rs Copies the control TTY token constant.
nvx-rs/​tests/​openvmm_vectors.rs Checks source excerpts against the submodule.
nvx-rs/​src/​host/​template/​plan/​tests.rs Tests lowering, argument rendering, allowlisting, and refusals.
nvx-rs/​src/​host/​template/​plan.rs Implements capture plans and resource validation.
nvx-rs/​src/​host/​template/​mod.rs Declares and re-exports the plan API.
nvx-rs/​src/​host/​sandbox/​mod.rs Makes disk-path validation available within the crate.
nvx-rs/​src/​host/​mod.rs Documents template-plan lowering.
nvx-rs/​README.md Describes the new API and capture configuration.
doc/​ci.md Documents allowlist excerpt checks in CI.
.github/​workflows/​ci.yml Updates comments describing existing comparison checks.

Review effort: Balanced

Enrique Saurez and others added 4 commits October 24, 2026 15:19
The row of nvx::host::template ended mid-sentence: the conformance vectors
of version 1 pin the key's encoding and identity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
Templates need a store of their own, apart from every sandbox's store,
whose generations clones lease and restore, at a location that the adapter
chooses.

Add nvx::host::template::Store: a private directory with an identity of
its own, the immutable payload of each generation, its key's canonical
encoding beside the snapshot that OpenVMM published, and a versioned index
that lists each generation with the identities of its key and its capture
class and its own metadata, such as its time contract, which eligibility
and restores read without the payload. Creation is crash-safe, makes one
store between concurrent creations, and refuses a directory of anything
else. Records are JSON in one format, replaced with the sandbox store's
atomic writes, and records of another format fail rather than being
migrated.

The store's directories must be private to the current user, through the
sandbox store's checks with a wording of their own on Linux, and links,
and on Windows other reparse points, are refused among its directories
and files. Four namespaces stay apart, each with a type of its own: the
store generation, nvx::activation::StoreGeneration, which the index
assigns in the order of publication and never twice, and by which a
clone's activation record names its template; the bound-block storage
generation, which belongs to the storage owner; the time lineage counter
of the time contract; and the VM generation ID, which the store never
records.

Publication, which captures a template and indexes it, builds on the
store, and the manifest reader now shares the store's bounded reads of
regular files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
A generation of a template must stay in its store while a clone may
still restore it, and no clone may restore one outside the restore
window of its capture's time contract, which OpenVMM refuses only once
the restore has begun.

Add the eligibility of a capture at a host's time, apart from its
compatibility: TimeContract::eligibility measures the downtime as
OpenVMM's restore does, by the host's monotonic clock within the boot of
the capture and by UTC otherwise, and refuses a negative downtime, and
one that would advance the clone's VM time by more than 30 days.
HostTime::now samples the identities and clocks of the host as OpenVMM
does.

Add the leases of a store's generations. Store::acquire leases an
eligible generation that no tombstone quarantines to a provisioned
sandbox that records no VM, launch, or lease, in one transaction under
the store's lock and then the sandbox's: it writes the lease's record in
the store, and the same record as the clone binding, clone.json, beside
the sandbox's launch journal, and returns the sandbox's lock, under
which the caller launches the clone. A lease never expires:
Store::release ends it once the sandbox records no VM and no launch, and
so once the clone's VM has exited, as the sandbox's store verifies
through its recorded process. Store::select picks the latest eligible
generation of a key. Store::tombstone quarantines a generation, which
takes no new lease while its leases keep working, and Store::retire
deletes it once its leases have drained: its payload, then its entry in
the index, then its tombstone, and it removes the records of leases
that no clone binding names any more. Each of these transactions makes
its steps durable in order, syncing directories where the platform can,
so that no clone binding outlasts, through a crash of the host, the
record that keeps its generation in the store, even when the store and
the sandbox are on different filesystems.

The sandbox store counts the clone binding among its files, so that a
removal of a sandbox removes it, and three error codes name the new
refusals: template-ineligible, template-tombstoned, and sandbox-leased.
The documentation of foreign-files now covers the template store too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
Address the review of the leases of the template store:

- Tombstones are read and checked wherever the store lists them, so that a
  tombstone of another store or of another format, or a link, fails a
  listing and a selection, as it fails an acquisition, rather than
  quarantining a generation. A retirement removes a tombstone only once it
  has checked it.
- A retirement makes the absence of a clone binding durable in the
  directory that holds the sandbox's entry, besides the sandbox's own
  directory: a sandbox that was removed and provisioned again at its path
  is a new directory, whose entries never held the binding.
- The first use of a store's directory of leases or of tombstones in a
  process also syncs the store's parent, so that no binding names a record
  of a store whose own entry a crash of the host could lose.
- On Windows, a record that a transaction in another process removes as
  this one opens or removes it is gone, rather than an error: a file system
  without POSIX deletion refuses every open of a removed file while other
  handles, such as a scanner's, hold it. A retirement that raced releases
  failed so, rarely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/839-pr2-template-store-20261024 branch from 401015b to 8718d50 Compare October 24, 2026 22:20
@esaurez
Enrique Saurez (esaurez) changed the base branch from esaurez/839-pr2-template-store-20261024 to esaurez/839-pr3-leases-20261024 October 24, 2026 22:30
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/841-pr1-template-lowering-20261024 branch from 39ac01d to 79d580b Compare October 24, 2026 22:30
@esaurez
Enrique Saurez (esaurez) requested a balanced review from Copilot October 24, 2026 22:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The self-contained lowering and argument rendering have focused regression coverage, with no unresolved blocking findings.

No open issues 🎉

Review effort: Balanced

On Windows, a refused read or removal of a template store's record
counted as a removal by another transaction if an open without access
rights was refused too. Win32 opens ask for SYNCHRONIZE and
FILE_READ_ATTRIBUTES even then, so a file whose access control list
grants neither was refused both ways, and taken for a removed one: a
retirement skipped the record of a live lease, or the binding that
names it, and deleted the generation, and a tombstone that refused
access quarantined nothing.

platform::is_deleted now opens the file with NtOpenFile, relative to
its directory, whose status tells STATUS_DELETE_PENDING from
STATUS_ACCESS_DENIED, which Win32 reports alike, and a refused file
counts as removed only if its deletion is pending, or it is gone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Placeholder paths bypass lexical validation, allowing capture plans to render block arguments that OpenVMM cannot attach as intended.

1 open issue

Review effort: Balanced

Comment thread nvx-rs/src/host/template/plan.rs
Enrique Saurez and others added 6 commits October 24, 2026 16:10
A lease named its sandbox by the absolute path through which it was
leased. That path could run through a link, or, on Unix, through a
directory that a `..` component passes through. Once that link or
directory went, the path named nothing, though the sandbox and its
clone binding remained, so a retirement took the lease for unbound,
and could delete its generation while the clone ran. And a link that
changed while the sandbox was leased could have redirected the
binding to another sandbox.

acquire now resolves the links of the sandbox's path before it takes
the sandbox's lock, and the lease, the binding, and the lock that it
returns all name the sandbox by that path. The tests that build paths
through links now move a sandbox's directory and link its old path
after the lease.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
A release took the sandbox's lock through any path. If that path ran
through a link, and the link changed after the release checked that
no VM runs, the release could remove another sandbox's binding, whose
lock it does not hold, so a retirement could then delete the
generation that the other sandbox's running clone uses.

A release now refuses, before it removes anything, a lock that does
not name the sandbox by its lease's path. That path has its links
resolved, and acquire takes the lock through it too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
A removal of a sandbox removed its clone binding with its other files
without syncing the directory. A retirement that later found the
binding gone synced only the directories that still existed. If the
sandbox's parent had been removed and created again, that sync missed
the directory that held the old entry, so a crash of the host could
bring the binding back after the retirement had deleted the generation
it names.

A removal now removes the binding first and syncs the sandbox's
directory before anything else goes. The configuration record still
goes last, so a failed sync leaves a provisioned sandbox that can be
removed again. The sync also covers a release that a crash interrupted
before its own sync. This costs one more directory sync per removal on
Unix; on Windows syncing a directory is a no-op.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
…rder

A clone's lease now reserves the launch of its clone. The acquisition draws
the launch's identity, records it in the lease's record and in the clone
binding before any VMM of the clone exists, and returns it as
Lease::launch. The supervisor takes the launch's identity from its caller:
supervisor::launch and LockedStore::begin_launch take a LaunchId, which
callers that launch no clone draw with LaunchId::generate, now public.

A lease allows one launch of its clone, under the lock that acquired it:
begin_launch refuses, with sandbox-leased, any other launch of a sandbox
that holds a lease, a launch under another lock, and a second launch, until
a release ends the lease. After a crash before the launch, a release ends
the lease at once, and another acquisition checks the generation's
eligibility again.

A stop or a termination clears the runtime record only if it is still the
record of its own VM, and not another VMM's under a launch identity that
its caller used again.

The supervisor's documentation describes the launch of a clone, its
crashes, and one lock order: the template store's lock, then the sandbox's
lifecycle lock, which guards its clone binding and launch journal, then the
scratch image's claim. No lock lasts for a VM's lifetime.

Tests crash a clone's orchestrator, in a process of its own, at each step
of an acquisition and of a release, and around the spawn of the clone's
VMM, and check what each evidence of a VM's exit means for its lease: an
earlier boot, a reused process ID, a process that cannot be examined, and a
forced teardown that fails.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
Four lease tests began a launch, dropped it, and asserted that the
next recovery ends it. On Linux, a process that a concurrent test
forks holds a copy of the closed log until it executes its program.
If that outlasts recovery's own wait, the recovery is indeterminate
and the test fails although nothing is wrong.

The tests now retry the recovery until it ends the launch, through
test_support::released, as the sandbox store's tests do.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bc335726-838c-4538-a3c4-d6791aba5101
A template is a capture of a microVM that consumed only its invariant
configuration, for clones that each bind their own image, upper, and
export when they are restored. Before a store captures one, the host
needs the exact VM and options of that capture: OpenVMM refuses a
platform-tier capture whose kernel command line holds a token outside its
platform allowlist, and a template must hold no tenant data.

Add nvx::host::template::TemplatePlan, which lowers an adapter's
SandboxPlan without I/O into:

- the placeholder distro, bound through its DistroBinding, whose length
  is the capacity of every clone's image, at an absolute path that
  OpenVMM's block device options hold whole, and that it attaches as a
  raw image, over a dormant scratch device, a sparse file in the capture's
  transaction directory;
- the dormant filesystem slot, and no NIC or one fixed NIC under the
  template's policy, which may allow neither ingress nor host-loopback
  access both ways, since OpenVMM publishes the guest's listeners through
  them as live host attachments, which no snapshot holds;
- TemplateResources: every processor online at boot, and RAM with a
  --memory-capacity, in blocks of 128 MiB;
- the kernel command line nvx_sandbox=1, the one token that the host
  adds to those that OpenVMM writes itself;
- the capture's options: --snapshot-tier platform, a
  --snapshot-destination in the transaction directory, under the name of
  a generation's snapshot in the template store, and
  --snapshot-block-identity generation with the capture's
  StorageGeneration, the bound-block storage generation, so that the
  capture hashes no block device.

The tests check each token of a template's command line with OpenVMM's
own allowlist, compiled from verbatim excerpts of its source in
tests/vectors/openvmm/, which the ignored test of
tests/openvmm_vectors.rs compares with the OpenVMM submodule.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 32eea669-a52c-4a53-87fa-521c498537e5
@esaurez
Enrique Saurez (esaurez) changed the base branch from esaurez/839-pr3-leases-20261024 to esaurez/839-pr4-supervisor-seam-20261024 October 25, 2026 00:59
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/841-pr1-template-lowering-20261024 branch from 7f55573 to 290a45b Compare October 25, 2026 00:59
@esaurez
Enrique Saurez (esaurez) requested a balanced review from Copilot October 25, 2026 01:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The self-contained lowering and validation have focused test coverage, with no unresolved review findings; tests were not rerun.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

This branch has not been deployed

No deployments
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.

2 participants