Skip to content

nvx-rs: build a clone's activation record and launch its restore - #949

Open
Enrique Saurez (esaurez) wants to merge 4 commits into
esaurez/844-pr1-clone-plan-geometry-20261024from
esaurez/844-pr2-activation-restore-launch-20261024
Open

Enrique Saurez (esaurez) wants to merge 4 commits into
esaurez/844-pr1-clone-plan-geometry-20261024from
esaurez/844-pr2-activation-restore-launch-20261024

Conversation

@esaurez

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

Copy link
Copy Markdown
Contributor

Stacked on #888; this PR retargets when its parent merges.
Chain: #936 → #937 → #939 → #940 → #941 → #888 → this PR → #950 → #951, on dev; #879, #881, #885, #886, #877, #932, #884, #887, #933, and #880 have merged.

Part of #844 (PR 2 of 5: the activation record and the restore launch).

What and why

#888's ClonePlan checks a clone's inputs against the generation of the template that it leased. This PR builds the clone's activation record from that plan, keeps it in the clone's sandbox, and launches the clone as OpenVMM's restore of the generation's snapshot, as the launch that the clone's lease reserves. The restore sequence, which reads the VMM's restore-ready event, sends the record in the adapter's Activate, starts the sandbox, and commits the launch, comes in PR 3.

The activation record

ClonePlan::activation() is the record that the clone's guest applies in place of the configuration that its template captured. The plan builds it from the request's workload view, overlay mode, and journal policy, and from the inputs that it checks:

  • a restore section with a fresh transaction ID of 16 random bytes and the lease's TemplateReference;
  • the image, an EROFS layer, with its binding's identity and the UUID that the adapter's plan expects of it;
  • a tmpfs upper, or the scratch image's binding identity with the journal policy;
  • no export;
  • the network of the template's NIC: the policy identity that its key records, the DNS server that the template's guest configured its resolver with, unpublished listeners, and the guest's proxy URL;
  • the template's processors and the memory target.

A binding's identity is the SHA-256 digest of a domain and of the binding's serde form, never of a file's content. TemplateKey::dns_server reads the DNS server from the virtnet_dns= token of the key's effective command line, which OpenVMM adds when the NIC's policy lets the guest reach the gateway's DNS server: once at most, for a template with a NIC, naming one IP address in its canonical spelling. Any other form is a malformed key, which planning a clone refuses. The plan also refuses a GPT image, which a clone's guest does not assemble yet, a network on another guest network than the template's NIC, whose address OpenVMM restores from the snapshot while the launch passes none, and settings that no record holds, before it opens or creates anything. Nor may a scratch image be a file of the generation's snapshot, by any path, hard links included: every clone of the generation restores the snapshot in place, while its guest writes its scratch image.

The record in the sandbox

The sandbox's store owns two more names: clone-activation.bin, the record, and restore.sock, the socket at which the launch awaits the VMM's restore-ready event on Linux, as long a name as control.sock. A release first ends the lock's reservation of the clone's launch, so that one that fails part of the way leaves no launch of a plan whose files it removed, then removes them with clone-scratch.raw, before the clone binding, whose durable removal makes theirs durable too. A removal of the sandbox does the same: it removes them through the handle of the sandbox's directory with which it checks the sandbox's lock and removes the binding, so that no change of a link in the path redirects their removal to another sandbox, and before the sync that makes the binding's absence durable. On a Windows file system without POSIX deletion, a file of the clone whose deletion is pending, because a reader holds it, lingers as a pending binding does until the reader closes it, rather than being refused as an entry that is not the store's, and a retried release takes it for removed.

The restore launch

ClonePlan::launch(self, &mut Acquired, openvmm, &RuntimeIdentity) -> Restoring runs the launch that the clone's lease reserves, under the lock that acquired the lease, once the sandbox's clone binding records the lease. It:

  1. writes the record, in its canonical encoding, to clone-activation.bin, from which a caller resends it unchanged after a crash;
  2. listens for the VMM's restore-ready event, at restore.sock on Linux, and at a named pipe of a random name on Windows;
  3. runs supervisor::launch with OpenVMM's restore of the clone's devices: --restore-snapshot with the generation's snapshot in place, which nothing copies, links, or changes, so that clones of one generation restore it at once; --restore-memory with the memory target; --restore-ready-path; the template's backend, CPU profile, and processors; a boot console that discards its output, as cold boots have; and the control console, but no kernel, initramfs, command line, or guest address, which the snapshot fixes.

OpenVMM requires a restore's control socket in the snapshot's parent on Linux, so the lease reserves the clone's control endpoint there: <launch>.sock in the generation's payload, named after the launch that the lease reserves, which the journal and the runtime record name as any launch's endpoint. The launch refuses, before it writes anything, a socket that is not beside the plan's snapshot, a payload whose mode is not exactly 0700, a socket path that, with or without the payload's links resolved, would not fit a Unix socket's address, and a control socket path that holds , or =, which end the path in OpenVMM's option listen=<path>. On Windows, the VMM listens at a fresh named pipe, as every launch does. For an upper in RAM, the launch also checks, before it writes anything, that the scratch device's file that the plan created is still a regular file of the template's scratch geometry: a later plan under the same acquisition replaces it, and removes it if that plan fails.

The VMM inherits a scratch image's claim: its descriptor on Linux, and on Windows the image's guard, once the launch has closed the claim's handle, which OpenVMM's open of the image, sharing no writing, would refuse.

The VMM removes its socket as it exits. The end of a launch and the clearing of a runtime record remove one that an abrupt exit leaves, and a release removes it and syncs the payload before it ends the lease, so a payload holds the sockets of its clones only while its generation has leases; a retirement removes the payload with whatever a crash left in it. The sandbox's store recognizes a clone's socket by its path in the payload of the generation that the binding's lease names, generations/<generation>/<launch>.sock. Since the sandbox's store builds on nothing of the template module, it makes that check in its own lease module, beside the lease, where the name of the generations' directory, GENERATIONS_NAME, now lives too; the template module re-exports it at its public path, as it re-exports the lease.

Not in this PR

Validation

The gates at this head, caa6a0b, are pending until they finish.

Per commit, on Linux x86_64 and on Windows: fmt and Clippy with -D warnings for nvx-rs in CI's three configurations (--all-targets --all-features, --lib, and --lib --no-default-features), tests/host_modules.rs, and the tests of the template, store, sandbox, supervisor, and platform modules; on Linux, also the aarch64-apple-darwin check.

Tests added, with a fake VMM in a process of its own, which listens beside the snapshot on Linux and holds the snapshot open sharing only reads on Windows, as OpenVMM does: the rendered arguments, the record, the claim, two clones of one generation launched at once, the socket's removal at teardown, at release, and after a crash, the refusals before any spawn, including a control socket path with , or =, and crashes of the launch's caller before it begins, once it began, and once its VMM started. Also: the records of clones of every upper, network, and workload; the DNS server of a key's command line and its refusals; the end of the lock's reservation before a release that fails part of the way removes anything; the removal of the clone's files at release and at the sandbox's removal, durably and through the sandbox's directory, also when a link in its path names another sandbox meanwhile, and, on Windows, while their deletion is pending, also by a retried release; the refusal of a plan of another guest address or prefix than the template's NIC; the refusal of a scratch image that is a hard link to the snapshot's memory; and the refusal of the launch of a stale plan in RAM whose file a failed plan of the clone removed.

No VM: a real restore comes with PR 5's VM tests.

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

Cleanup can delete another running sandbox’s clone files, and clone requests can silently restore the wrong network address.

3 open issues
What changed in this PR

This PR extends NVX’s template-clone planning with activation records and supervised OpenVMM restore launches, implementing part 2 of #844. The Activate, Ready, and Start sequence remains for later PRs.

Changes:

  • Builds activation records from clone settings, binding metadata, and template resources.
  • Launches restores from leased snapshots with platform-specific readiness endpoints and scratch-claim inheritance.
  • Adds clone-file and socket cleanup, with tests for records, durability, and lifecycle behavior.
File Description
nvx-rs/​src/​host/​template/​store/​leases/​tests.rs Tests durable cleanup during lease release.
nvx-rs/​src/​host/​template/​store/​leases.rs Reserves clone sockets and removes launch artifacts on release.
nvx-rs/​src/​host/​template/​store/​capture.rs Shares snapshot and socket-limit constants.
nvx-rs/​src/​host/​template/​store.rs Documents clone sockets within generation payloads.
nvx-rs/​src/​host/​template/​mod.rs Exports and documents Restoring.
nvx-rs/​src/​host/​template/​key/​tests.rs Tests DNS extraction and malformed-token refusals.
nvx-rs/​src/​host/​template/​key.rs Exposes template DNS and network-policy metadata internally.
nvx-rs/​src/​host/​template/​clone/​tests.rs Tests activation records and unsupported settings.
nvx-rs/​src/​host/​template/​clone/​record.rs Builds activation records and binding identities.
nvx-rs/​src/​host/​template/​clone/​launch.rs Implements restore launches and the Restoring handle.
nvx-rs/​src/​host/​template/​clone.rs Adds activation settings and launch-device planning.
nvx-rs/​src/​host/​supervisor/​mod.rs Passes runtime records into clone-socket cleanup.
nvx-rs/​src/​host/​store/​tests.rs Tests selective cleanup of clone launch files.
nvx-rs/​src/​host/​store/​mod.rs Adds owned clone files, endpoint reservations, and cleanup.
nvx-rs/​src/​host/​store/​lifecycle/​tests.rs Extends removal tests for clone files and redirected paths.
nvx-rs/​src/​host/​store/​lifecycle.rs Removes clone files through the verified directory handle.
nvx-rs/​src/​host/​store/​lease.rs Centralizes generation-directory and clone-socket naming.
nvx-rs/​src/​host/​platform/​windows.rs Adds private named-pipe readiness listeners.
nvx-rs/​src/​host/​platform/​other.rs Adds unsupported-platform readiness stubs.
nvx-rs/​src/​host/​platform/​linux.rs Adds Unix-socket readiness listeners.
nvx-rs/​README.md Documents activation records, restore launches, and cleanup.

Review effort: Balanced

Comment thread nvx-rs/src/host/store/lifecycle.rs
Comment thread nvx-rs/src/host/template/clone/launch.rs
Comment thread nvx-rs/src/host/template/clone.rs Outdated
A clone's plan builds the activation record that the clone's guest
applies in place of the configuration that its template captured, from
the request's workload view, overlay mode and journal policy, and from
the inputs that the plan checks: a restore section with a fresh
transaction ID of 16 random bytes and the lease's template reference;
the EROFS image with its binding's identity and the UUID that the
adapter's plan expects; a tmpfs, or the scratch image's binding identity
with the journal policy; no export; the network of the template's NIC,
with the policy identity that its key records, the DNS server that the
template's guest configured its resolver with, unpublished listeners
and the guest's proxy URL; and the template's processors and the memory
target. A binding's identity is the SHA-256 digest of a domain and of
the binding's serde form, which reads no file content.

TemplateKey::dns_server reads the DNS server from the virtnet_dns= token
of the key's effective command line, which OpenVMM adds when the NIC's
policy lets the guest reach the gateway's DNS server: once at most, for
a template with a NIC, naming one IP address in its canonical spelling.
Any other form is a malformed key, which planning a clone refuses as an
I/O error.

The plan refuses a GPT image, which a clone's guest does not assemble
yet, and settings that no record holds, before it opens or creates
anything.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a424884c-018a-45f5-a7fe-c57548a873f0

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

A failed lease release can leave a launch reservation active after clone files are deleted, allowing a VMM to start with a missing scratch disk.

3 open issues
3 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/src/host/template/store/leases.rs Outdated
Comment thread nvx-rs/src/host/store/mod.rs Outdated
Comment thread nvx-rs/src/host/template/clone/launch.rs
The sandbox store owns two more names for a clone's launch: its
activation record, clone-activation.bin, and the socket at which the
launch awaits the VMM's restore-ready event on Linux, restore.sock, as
long a name as control.sock.

A release removes them with the scratch device's file of a clone in
RAM, before the clone binding, whose durable removal makes theirs
durable too. A removal of the sandbox now does the same: it removes the
clone's files with the binding, before the sync that makes the
binding's absence durable, rather than with the store's other files
after it. Both take a clone's file whose deletion is pending, as a
Windows file system without POSIX deletion leaves one that a reader
holds, for removed, so a release that failed part of the way can be
retried.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a424884c-018a-45f5-a7fe-c57548a873f0

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

Scratch validation can expose shared snapshot artifacts as writable devices, while failed replanning can leave older plans pointing to a missing scratch device.

2 open issues
3 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/src/host/template/clone/launch.rs
Comment thread nvx-rs/src/host/template/clone/launch.rs
Enrique Saurez and others added 2 commits October 27, 2026 23:01
ClonePlan::launch runs the launch that a clone's lease reserves, under the
lock that acquired the lease, once the sandbox's clone binding records the
lease. The VMM restores the generation's snapshot in place, which nothing
copies, links, or changes, so clones of one generation restore it at once.
OpenVMM requires a restore's control socket in the snapshot's parent on
Linux, so the lease reserves the clone's control endpoint there:
<launch>.sock in the generation's payload, named after the launch, which
the journal and the runtime record name as any launch's endpoint. On
Windows the VMM listens at a fresh named pipe, as every launch does. The
launch refuses, before it writes anything, a socket that is not beside the
plan's snapshot, a payload whose mode is not exactly 0700, a socket path
that, with or without the payload's links resolved, would not fit a Unix
socket's address, and a control socket path that holds ',' or '=', which
end the path in OpenVMM's option listen=<path>.

The VMM restores the snapshot in place while the clone's guest writes
its scratch image, so a plan refuses a scratch image that is a file of
the generation's snapshot, by any path, through a hard link say. The
launch also refuses, before it writes anything, a plan in RAM whose
scratch device's file is gone, as when a later plan of the clone
replaced it and failed.

It writes the clone's activation record to clone-activation.bin, listens
for the VMM's restore-ready event, and runs supervisor::launch with
OpenVMM's restore of the clone's devices: --restore-snapshot with the
generation's snapshot, --restore-memory with the memory target,
--restore-ready-path, the template's backend, CPU profile, and processors,
a boot console that discards its output, as cold boots have, and the
control console, without a kernel, command line, or guest address.

The VMM inherits a scratch image's claim: its descriptor on Linux, and on
Windows the image's guard, once the launch has closed the claim's handle,
which OpenVMM's open of the image, sharing no writing, would refuse.

The VMM removes its socket as it exits. The end of a launch and the
clearing of a runtime record remove one that an abrupt exit leaves, and a
release removes it and syncs the payload before it ends the lease, so a
payload holds clone sockets only while its generation has leases; a
retirement removes the payload with whatever a crash left in it. A
release ends the lock's reservation of the launch before it removes
anything, so one that fails part of the way leaves no launch of a plan
whose files it removed.

Tests run the launch with a fake VMM in a process of its own, which
listens beside the snapshot on Linux and holds the snapshot open sharing
only reads on Windows, as OpenVMM does: the rendered arguments, the
record, the claim, two clones of one generation launched at once, the
socket's removal at teardown, at release, and after a crash, the refusals
before any spawn, and crashes of the launch's caller before it begins,
once it began, and once its VMM started.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a424884c-018a-45f5-a7fe-c57548a873f0
…ate's

OpenVMM restores a clone's NIC, with the guest's address, from the
template's snapshot, and a clone's launch drops the --net of the
adapter's plan, so a plan that gave the guest another address or prefix
than the template's NIC planned a clone that silently ran on the
template's. ClonePlan::new now refuses such a plan with
ErrorKind::Unsupported: it compares the plan's network with the guest
network of the template key's NIC, TemplateKey::guest_network, in the
canonical form that both write. Which policy a template serves stays the
network templates' concern, one template per policy, whose key holds
its identity.

Part of #844.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a424884c-018a-45f5-a7fe-c57548a873f0

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

Scratch bindings can alias another generation’s snapshot, allowing clone writes to corrupt data used by other clones.

1 open issue
2 resolved since last review

Review effort: Balanced

path, binding, &key, &owned, deadline, access,
)?),
(Some(path), Some(binding)) => {
let shared = SnapshotFiles::of(&snapshot)?;

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