Repository navigation
nvx-rs: lease eligible templates to clones, and retire them - #885
Merged
Pedro Henrique Penna (ppenna) merged 8 commits intoOct 27, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved crash-durability gaps and tombstone validation bypasses undermine the store’s documented guarantees.
4 open issues
What changed in this PR
Adds eligibility checks and persistent leases to the Rust host library’s template store, preparing it for clone restoration. This is the third stage of #839; runtime integration remains for later PRs.
Changes:
- Adds restore-window eligibility checks and host identity/time sampling on Linux and Windows.
- Adds clone bindings, lease acquisition and release, tombstones, retirement, and ordered durability steps.
- Adds concurrency and failure-recovery tests, public API documentation, and error codes.
| File | Description |
|---|---|
nvx-rs/src/lib.rs |
Documents template leases in the crate overview. |
nvx-rs/src/host/template/store/tests.rs |
Shares fixtures with lease tests. |
nvx-rs/src/host/template/store/leases/tests.rs |
Tests lease lifecycle, durability, validation, and concurrency. |
nvx-rs/src/host/template/store/leases.rs |
Implements leases, clone bindings, tombstones, and retirement. |
nvx-rs/src/host/template/store.rs |
Exports lease APIs and documents store guarantees. |
nvx-rs/src/host/template/mod.rs |
Exports eligibility and lease types. |
nvx-rs/src/host/template/eligibility/tests.rs |
Tests clock selection and restore-window boundaries. |
nvx-rs/src/host/template/eligibility.rs |
Implements host-time sampling and eligibility checks. |
nvx-rs/src/host/store/mod.rs |
Registers clone bindings as sandbox-owned files. |
nvx-rs/src/host/store/files.rs |
Adds directory synchronization. |
nvx-rs/src/host/platform/windows.rs |
Reads machine identity and interrupt time. |
nvx-rs/src/host/platform/other.rs |
Adds unsupported-platform stubs. |
nvx-rs/src/host/platform/mod.rs |
Adds host identity parsing and tests. |
nvx-rs/src/host/platform/linux.rs |
Reads machine identity and boot-time clock. |
nvx-rs/src/host/mod.rs |
Documents template leasing. |
nvx-rs/src/error.rs |
Adds eligibility, quarantine, and lease error codes. |
nvx-rs/README.md |
Documents new APIs and errors. |
nvx-rs/Cargo.toml |
Enables Windows interrupt-time API support. |
doc/usage.md |
Lists the new error codes. |
Review effort: Balanced
Enrique Saurez (esaurez)
force-pushed
the
esaurez/839-pr2-template-store-20261024
branch
from
October 24, 2026 22:20
401015b to
8718d50
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/839-pr3-leases-20261024
branch
from
October 24, 2026 22:20
bb8fc3d to
7c6f75b
Compare
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-filesystem crash consistency, concurrent retirement, and native Windows deletion semantics warrant final human review despite no new confirmed defects.
No open issues 🎉
2 resolved since last review
Review effort: Balanced
This was referenced Oct 25, 2026
Enrique Saurez (esaurez)
force-pushed
the
esaurez/839-pr2-template-store-20261024
branch
from
October 27, 2026 02:56
8718d50 to
95a7e95
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/839-pr3-leases-20261024
branch
from
October 27, 2026 02:56
780ab5f to
4a8b534
Compare
Pedro Henrique Penna (ppenna)
deleted the
esaurez/839-pr3-leases-20261024
branch
October 27, 2026 19:06
This was referenced Oct 27, 2026
This was referenced Oct 28, 2026
Enrique Saurez (esaurez)
pushed a commit
that referenced
this pull request
Oct 28, 2026
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
Enrique Saurez (esaurez)
pushed a commit
that referenced
this pull request
Oct 28, 2026
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
Enrique Saurez (esaurez)
pushed a commit
that referenced
this pull request
Oct 28, 2026
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
Enrique Saurez (esaurez)
pushed a commit
that referenced
this pull request
Oct 28, 2026
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 was referenced Oct 29, 2026
Merged
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.


Chain: this PR → #886 → #884 → #887 → #888, on
dev; #866, #882, #883, #876, #871, #873, #874, #879, and #881 have merged.A clone restores a generation of a template from the store's payload, so the generation must stay in its store as long as a clone may still restore it, or run from it. And a clone must not restore a capture outside the restore window of its time contract, which OpenVMM refuses only once the restore has begun. This adds both to
nvx::host::template: the eligibility of a capture at a host's time, apart from its compatibility, and the leases of a store's generations, with their clone bindings, tombstones, and retirement. It changes no behavior: nothing leases a generation yet. Restore, #844, builds on it.Part of #839: the third of its four pull requests. The first, #879, adds the compatibility key, and the second, #881, the store; the fourth adds the supervisor seam and the lock order.
What changed
Eligibility (
template/eligibility.rs).TimeContract::eligibility(&HostTime), andGeneration::eligibility, decide whether a clone may restore a capture at a host's time, as OpenVMM's restore decides it (vmm_core/virt/src/time_abi/downtime.rs,select_downtime, andopenvmm_defs/src/time_abi.rs,vm_time_downtime_ns):Eligibility::Early), as on a host whose UTC is behind the capturing host's;MAX_DOWNTIME_NS, 30 days (Eligibility::Expired).HostTimeholds the host's identity, its boot's, its monotonic clock and that clock's reading, and its UTC.HostTime::nowsamples them as OpenVMM samples them at a capture (vmm_core/virt/src/time_abi/host.rs): the machine ID or machine GUID, the boot ID, and UTC between two readings ofCLOCK_BOOTTIMEor the interrupt time, whose midpoint it keeps. Its fields are public, so that callers can check eligibility at other times, such as with a margin before a window closes.Leases (
template/store/leases.rs).Store::acquireleases a generation to the clone of one sandbox, in one transaction under the store's lock and then the sandbox's lifecycle lock. It checks that the store indexes the generation, that no tombstone quarantines it, that it is eligible at the host's time, and that the sandbox is provisioned, records no VM and no launch, and holds no lease. It then writes the lease's record in the store,leases/<L>.json, and the same record as the sandbox's clone binding,clone.json, beside its launch journal, before any VMM of the clone exists. It resolves the links of the sandbox's path before it locks the sandbox, and the lease, the binding, and the returned lock all name the sandbox by that path. So the removal or change of a link, or of a directory that a..component passed through, can neither hide a live binding from a retirement nor redirect it. It returns the lease with the sandbox's lock, under which the caller launches the clone, and releases the store's lock.A lease never expires.
Store::releaseends it only once the sandbox records no VM and no launch, so once the clone's VM has exited, as the sandbox's store verifies through the VM's recordedProcessRef, also after a reattach. It removes the binding, and then the store's record. The caller takes the sandbox's lock through the lease's path,Lease::sandbox, asacquiredoes: a release refuses any other lock before it removes anything, since a link in the lock's path could change and redirect the removal to another sandbox.Store::selectselects the latest generation of a key that is neither quarantined nor ineligible, andStore::leaseslists a generation's leases.Lease::templatereturns theTemplateReferencethat a clone's activation record holds.Tombstones and retirement.
Store::tombstonequarantines a generation,tombstones/<G>.json: it takes no new lease, and selection skips it, while its leases keep working.Store::retiretombstones a generation, and deletes it once its leases have drained: its payload, then its entry in the index, then its tombstone, so that an interrupted retirement leaves a quarantined generation, which retiring it again deletes. It also removes the records of leases that no clone binding names any more, which an interrupted release or a removal of a sandbox leaves, and the temporary files of interrupted record replacements. The store's records and directories have the checks of #881's: private to the current user, no links or reparse points, and records inSTORE_FORMAT1, refused in any other. Every reader of a tombstone checks it, so a tombstone of another store or of another format fails a listing, a selection, an acquisition, or a retirement, rather than quarantining a generation or being removed. An entry that is not the store's fails withForeignFiles.Durability. Each transaction makes its steps durable in order, so that the order holds through a crash of the host, even when a sandbox and the store are on different filesystems:
acquiremakes the lease's record durable before it writes the binding, and the binding durable before it returns. If it cannot make the binding's removal durable after a failure, it keeps the record, and its error says that the sandbox may still hold the lease;releasemakes the binding's removal durable before it removes the record;LockedStore::remove, removes its binding first, and makes the binding's absence durable before anything else goes. Its configuration record still goes last, so a removal that fails to sync leaves a provisioned sandbox, which can be removed again;retiremakes the tombstone durable before it deletes anything. It makes a binding's absence durable before it removes the record of that binding's lease, since a release that crashed or failed its sync may not have: in the sandbox's directory, and in the directory that holds the sandbox's entry, since a sandbox that was removed and provisioned again at its path is a new directory. Each deletion is durable before the next, and a retirement that finds its generation already unindexed makes the index durable before it removes the tombstone.A new
sync_dir, beside the stores' atomic writes (store/files.rs), syncs a directory on Unix. A process also syncs the store's directory, and then the store's parent, the first time that it uses each ofleases/andtombstones/, which the store never removes, in case the process that created them, or the store, did not finish that sync, so that no binding names a record of a store whose own entry a crash could lose. Windows has no portable way to flush a directory, so there a rename or a removal is as durable as the filesystem makes it, as OpenVMM's snapshot publication already assumes (openvmm_helpers/src/snapshot/fs.rs,sync_directory).Concurrent removals. A release and a retirement, in other processes, may remove one lease's record at once, and the readers that take no lock may meet a record as another transaction removes it. On Windows, a file system without POSIX deletion keeps a removed file under its name while other handles, such as a scanner's, hold it, and refuses every open of it until then. Win32 reports that refusal as a denied access, as it reports a file whose access control list refuses the open. So the store takes a refused file for gone only if
platform::is_deletedconfirms it: that function opens the file again with the native API, relative to its directory, and only the statusSTATUS_DELETE_PENDING, or no file at all, counts as gone. Any other refusal fails the read or the removal, so a record that refuses access never counts as missing.A removal of a sandbox removes its binding once, and takes a binding whose deletion is pending for removed. It deletes the binding with POSIX semantics where the filesystem supports them, so that its name goes at once. Elsewhere, a binding that a reader holds lingers, as a removed lock file does, until the reader closes it: the removal leaves it, and its
Lingering::waitwaits for it, rather than reporting it as a foreign entry.The sandbox store (
store/mod.rs,store/lifecycle.rs) counts the clone binding among its files,CLONE_BINDING_NAMEinFILE_NAMES, so that a removal of a sandbox removes it, first, once, and durably, as above. The removal opens the sandbox's directory once. Through that handle, it checks that the directory holds the lock file whose lock it holds, and refuses otherwise, then that the sandbox records no VM and no launch, and then removes the binding and syncs the directory. So a link in the sandbox's path that changes meanwhile cannot redirect the binding's removal to another sandbox, whose VM may run. The rest of the removal goes through the path, as before, under the store's trust model: only the user or an administrator may change the directories above a sandbox's. The sandbox store also gainsSandboxStore::binding_path.Platform (
platform/).host_idandmonotonic_ns, on Linux and Windows, forHostTime::now.is_deleted, which on Windows tells a file whose deletion is pending from one that refuses access, throughNtOpenFile;windows-sysgains itsWdk_FoundationandWdk_Storage_FileSystemfeatures for it. AndDirectory, a directory held open for a removal's operations on its entries:fstatat,unlinkat, andfsyncon Unix, and, on Windows, native opens relative to the directory, whichis_deletednow shares, and a deletion through a handle.New public API to review
nvx::host::template:MAX_DOWNTIME_NS;HostTime;DowntimeSource;Downtime;Eligibility;TimeContract::eligibility;Generation::eligibility;LeaseId;Lease;Acquired;Retirement;LEASES_NAME;TOMBSTONES_NAME; andStore::select,acquire,release,leases,tombstone,tombstones, andretire.nvx::host::store:CLONE_BINDING_NAME, whichFILE_NAMESnow includes, andSandboxStore::binding_path.nvx::ErrorCode, the README, anddoc/usage.md:template-ineligible(kindUnavailable): the generation's restore window excludes the host's time;template-tombstoned(kindUnavailable): a tombstone quarantines the generation;sandbox-leased(kindOther): the sandbox holds a lease already.The documentation of
foreign-files, which #881 began to use for the template store, now covers it too.Assumptions to review
LockedStore::remove. For a sandbox removed otherwise, a retirement makes its binding's absence durable only while the directories that held the sandbox remain.Lease::sandboxreturns it; the lock thatacquirereturns is one.HostTime::now, and passes it toacquireandselect; OpenVMM checks the window again as it restores.acquireleaves the sandbox's lease, whichreleaseends, since the sandbox then records no launch; the fourth pull request ties a lease to the launch that it reserves.select,leases,tombstones, andLease::ofread without the store's lock, so what they return may change before the caller acts on it;acquirechecks again under the lock.Validation
At
3fbaf8a, the head, rebased on #881's5e751ca, on Windows and on Linux x86_64:cargo fmt --check;cargo clippywith--all-targets --all-features,--lib, and--lib --no-default-features,-D warnings;cargo testwith all, default, and no features, among themtests/host_modules.rs, the layering ofnvx::host;cargo docwith-D warnings, also of private items on Windows;cargo +1.89 check;tests/openvmm_vectors.rswith--ignored, against the OpenVMM submodule at its pin,e68871c;nvx, against a synthetic bundle;scripts/test_nvx_tools.py's check thatdoc/usage.mddocuments every error code, and the check of the Markdown links;cargo fmt --check,cargo clippywith--all-targets --all-features,--lib, and--lib --no-default-features,-D warnings, andtests/host_modules.rs, and, on Linux, the macOS check.The eligibility tests check clock fixtures within one boot and across a reboot, on another host, and by another clock, the bounds of the window, the cut of the VM's time, and the host's own time. The lease tests cover acquisitions, releases, tombstones, and retirements, their refusals, interrupted releases and retirements, records of other formats and stores, foreign entries, and links. They also check the order in which each transaction makes its steps durable, and the failed syncs, and that a removal of a sandbox makes its binding's absence durable before anything else goes, and keeps the sandbox provisioned if that sync fails. They check that a lease names its sandbox by its resolved path, also through a
..component or a link that goes later, that a link that changes during an acquisition does not redirect its binding, and that a release refuses a lock taken through a link, also once the link names another sandbox. The sandbox store's tests check that a removal through a path whose link changes between its checks and the binding's removal removes only the binding of the sandbox whose lock it holds, and that a removal through a path that has come to name another sandbox's directory refuses, and removes nothing. On Windows, they check that a removal takes a binding whose deletion is pending for removed, waits while a reader holds it, and finishes once the reader closes it. Each of these regressions fails when its fix is reverted. With several processes, they cover acquisitions that race to lease one sandbox, a tombstone that races acquisitions, and a retirement that races acquisitions and releases. On Windows, they also leave records pending deletion, as a file system without POSIX deletion does while a scanner holds them, and check that readers and a release take them for gone. They also check that records whose access control lists refuse every open are not taken for gone: a retirement then fails, and keeps the generation, and a tombstone still quarantines its generation. The race of a retirement and releases also ran 320 times on Windows, in eight parallel copies, with no failure; before the fix of concurrent removals, about one run in a hundred failed.Every check passed on both hosts. One earlier run on Linux failed
guest::cgroup::tests::settlement_collects_statuses_between_its_checks_until_the_deadline, a timing test of guest code that this change does not touch, which expects two collections within 50 ms. It passed 30 reruns on that host, and the whole suite passed again.A final review by a separate model found issues in three rounds, which
bb8fc3daddressed:Copilot's review then found four issues, and the Windows tests a race, which
7c6f75baddresses:tombstones(), and soselect(), took every entry named as a tombstone for one. The retirement of an unindexed generation also removed its tombstone without reading it. Both now check the tombstone, asacquiredoes.The review of that fix found that it also took a record whose access control list refuses every open for gone, so a retirement could delete a leased generation.
1190e11tells the two apart by the native status of the open.Copilot's second review found that a lease recorded the path through which its sandbox was leased. On Unix, that path could pass through a directory by a
..component, or, on either platform, through a link. Once that directory or link went, the path named nothing, and a retirement took the live lease for unbound.b23cb64resolves the links of the sandbox's path before it locks the sandbox. The lease, the binding, and the returned lock all name the sandbox by the resolved path, so a link that changes during or after the acquisition cannot redirect the binding either, as the final review of the fix required.Copilot's third review found that a release took the sandbox's lock through any path. If a link in that path changed after the release checked that no VM runs, the release could remove another sandbox's binding, whose lock it did not hold.
43f5272refuses, before it removes anything, a lock that does not name the sandbox by its lease's path.Copilot's fourth review found that a removal of a sandbox removed its binding without making that durable. A retirement then synced only the directories that remained, so if the sandbox's parent had been removed and created again, a crash of the host could bring the binding back after the retirement had deleted its generation.
a7e68admakes a removal remove the binding first, and sync the sandbox's directory before anything else goes. Syncing every ancestor in the retirement instead would need read access to each, which hosts with unreadable ancestors do not grant.Copilot's fifth review found two more issues in the removal of a sandbox, which
780ab5faddresses:780ab5falso makes the lease tests retry the recoveries of closed launches, as the fourth pull request's tests do: on Linux, a process that a concurrent test forks holds a copy of the closed log until it executes its program. A final review of780ab5fby a separate model found no issues.The restack on #874, through #879 and #881, resolved two conflicts, and changed no code. In the README's crate tree, it keeps
dev's text and adds this pull request's leases. In the Windows tests of the sandbox store,devadded a test of provisions that wait for a removed directory where this pull request adds its test of a binding whose deletion is pending: both tests stay, and sharedev's helper, which now also deletes directories. The race of a retirement and releases ran 320 times again at4a8b534, in eight parallel copies, with no failure. A final review of the restack by a separate model found no issues.A second restack, on #874's
fa45e48, which drops the configuration identity of activation records, through #879 and #881, replayed this pull request's commits without conflicts, and changed no code. The race of a retirement and releases ran 320 times again atc2aa75a, in eight parallel copies, with no failure. On Linux,aci_edge_sandboxes' testcrashed_vms_fall_back_to_provisioned, whose fake OpenVMM exits one second after it starts, failed once atc2aa75awhile the host was loaded, because the start had not completed by then. The step passed on its rerun, and the test in 40 further runs, atc2aa75aand at #881's head; this pull request adds nothing to a start's path. A final review of it by a separate model found no issues.e69106blets the Windows test of a pending deletion run where an open can bypass an access control list. The test gave a file a list that allows no one anything, and asserted, before its checks of removals, that an open of the file without access rights but with backup semantics fails. On GitHub's hosted Windows runners that open succeeds, most likely because the runner's elevated process holds the backup privilege, which lets such an open bypass the list, so the test failed there every time. It now checks that open's error only where the open fails, and keeps every assertion about which files were removed. It changes only that test: no other test assumes that such an open fails. The race of a retirement and releases ran 320 times again ate69106b, in eight parallel copies, with no failure. A final review of the change by a separate model found no issues.The rebase onto
dev, after #879 merged, through #881, resolved a conflict only in the crate tree ofnvx-rs/README.md, keepingdev's text and adding this pull request's; it changed no code. The race of a retirement and releases ran 320 times again at3fbaf8a, in eight parallel copies, with no failure, and a final review of the rebase by a separate model found no issues.NVX CI has not run on this pull request yet; a run dispatched on #886's head,
7c97c9c, which holds this pull request's commits, is in progress.