Skip to content

nvx-rs: serve shorter images, and restore clones in RAM without a scratch file - #925

Open
Enrique Saurez (esaurez) wants to merge 4 commits into
esaurez/848-base-snapshot-20261028from
esaurez/848-shorter-images-absent-scratch-20261026
Open

Enrique Saurez (esaurez) wants to merge 4 commits into
esaurez/848-base-snapshot-20261028from
esaurez/848-shorter-images-absent-scratch-20261026

Conversation

@esaurez

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

Copy link
Copy Markdown
Contributor

Part of #848.

Review base: this PR temporarily targets esaurez/848-base-snapshot-20261028, a frozen copy of the work it builds on, so that its diff shows only its own commits while the pull requests below it are in review. Before it merges, it is rebased onto #963, #847's second pull request, and retargeted.

The review base also holds a commit that pins OpenVMM a12d9167, dev's pin since #901, whose zero fill (microsoft/openvmm#4749) this change needs. That pin is part of the review base, not of this change, and the final rebase drops it.

The last commit, 3ff9b99, is #926's e2bc9ff, unchanged. It brings the base's copy of OpenVMM's platform allowlist, nvx-rs/tests/vectors/openvmm/platform_command_line_token_is_invariant.rs, up to a12d9167, which also admits maxcpus=, and records a12d9167 in the vectors' README, so that the OpenVMM vectors test passes at that pin. It is not part of this change either, and the final rebase drops it too, since #884, below, refreshes the same copy.

What and why

Until now, a clone's image had to be exactly as long as the template's placeholder, and each clone in RAM got a sparse scratch file of the template's scratch length, which it never used. OpenVMM now presents the distro capacity that a platform snapshot recorded over a shorter image, reading zeros past its end, and lets a restore declare a fresh scratch absent (microsoft/openvmm#4749). So one template now serves images of any length up to its capacity, in either format, and clones in RAM restore with no scratch file at all.

Images shorter than a template's capacity

ClonePlan::new admits an image no longer than the template's placeholder (TemplateKey::distro), with its logical and physical block sizes, and refuses, before any VMM starts:

  • an image of other block sizes, by the evidence supplied-geometry, whatever its length;
  • a longer image, by the evidence supplied-length: "the image PATH is N bytes long, longer than the template's placeholder, whose M bytes are the capacity of every clone's image".

A file whose length is not a nonzero multiple of 512 bytes stays refused, by supplied-geometry. On Linux a file's physical block size is its filesystem's I/O block size (st_blksize), so the image must lie on a filesystem of the placeholder's; on Windows it is always 4096 bytes. A scratch image keeps the exact rule: the template's scratch geometry, its length included. Nothing reads, pads, or resizes the image.

Clones in RAM without a scratch file

Every restore attaches the template's scratch block device, which the template's guest negotiated. For a clone in RAM, the restore now passes --restore-absent-scratch and no scratch block device, so OpenVMM attaches the scratch at the recorded geometry with no file: reads return zeros, writes fail, and the clone's guest, whose upper is a tmpfs, never uses it. A clone over a scratch image still passes the image as the scratch block device. So the host creates nothing for a clone in RAM:

  • CloneUpper::Tmpfs is now a unit variant, and ClonePlan::scratch_path() returns Option<&Path>, None in RAM.
  • SandboxStore::clone_scratch_path() is gone, and nothing creates the clone's scratch file, clone-scratch.raw, any more. The store still counts its name, nvx::host::store::CLONE_SCRATCH_NAME, among its files, FILE_NAMES, so that a release and a removal remove one that an earlier version left for a clone in RAM, which a removal would otherwise report as foreign.
  • A template whose scratch presented physical blocks of another size than the sandbox's filesystem now serves clones in RAM, since no file of that filesystem backs their scratch.

A template's capture keeps its own dormant scratch file, since a cold boot cannot declare it absent.

The guest

No change, as #848 expects. The guest rereads a GPT disk's partition table before it assembles the root (#845), and nvx::gpt requires only that the primary header's alternate LBA lie inside the device (read_header in nvx-rs/src/gpt/mod.rs), which holds for a disk shorter than the device; it never reads the backup header. A clone in RAM looks for no scratch device.

Tests

nvx-rs unit tests:

  • Images of either format of 512 bytes, of half and of all but 512 bytes of the placeholder's length, and of its length, are served; longer ones of either format are refused, by supplied-length, and ragged lengths by supplied-geometry. A template whose placeholder presented other physical blocks serves no image of this filesystem, whatever its length.
  • On Linux hosts that offer huge pages, files on hugetlbfs, whose physical blocks are 2 MiB: an image as long as the placeholder or shorter, and a scratch image, refused by supplied-geometry.
  • A clone in RAM is planned with no file in the sandbox's directory, and its VMM's arguments hold --restore-absent-scratch and no scratch block device; a clone over a scratch image has no such flag.
  • A template whose scratch presented other physical blocks serves clones in RAM, and refuses scratch images of this filesystem.

Runtime adapter VM scenarios, in vm_clone, on every backend:

  • one_generation_serves_clones_of_either_format_over_either_upper: the GPT template's placeholder is now 16 MiB longer than the platform's EROFS image, so its four clones at once, the EROFS image over a scratch image and in RAM, a GPT disk of the EROFS image's length in RAM, and one as short as its layout allows over a scratch image, are all shorter than the capacity. Each finds its image's disk at the capacity and its scratch device at the template's scratch length, and reads both to their ends. No file in the sandbox of a clone in RAM is as long as the template's scratch, and on Linux its VMM's arguments hold --restore-absent-scratch and no scratch block device.
  • a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts: an EROFS image and a GPT disk 4 KiB longer than the placeholder are refused, by supplied-length, in place of the image 4 KiB shorter that the exact rule refused.

An image on a filesystem of another block size has no VM scenario: no CI host offers the suite a second filesystem of another I/O block size without a privileged mount, and Windows presents 4096-byte physical blocks for every file. The nvx-rs tests above cover that refusal.

Guide (#591's numbering)

This PR leaves doc/design unchanged; #591 takes:

  • 2.1.3: a platform capture's distro presents the placeholder's length, the capacity of every image that a restore supplies, apart from the image's own length; and every block of a snapshot-capable machine is backed by a file, except a fresh scratch that a restore declares absent.
  • 3.4.4: the same exception to "Each block must then be a nonempty, 512-byte-aligned raw regular file", and, on Linux, a substitute layer's physical block size is its filesystem's, so it must lie on a filesystem of the recorded physical block size.
  • 3.4.5 (the fresh policy) and 5.5.2: a restore may declare a fresh scratch absent, for a guest that never uses it, such as one whose overlay upper is in RAM: the device presents the recorded geometry, reads return zeros, and writes fail.
  • 5.4.7 (Table 5.4, sandbox block): besides "a new scratch file of the same geometry", none, for a fresh scratch declared absent.

Validation

At 673f222. 09b594a then changed documentation alone, after which fmt, RUSTDOCFLAGS=-D warnings cargo doc, and the nvx::host::template tests passed again on Windows.

  • Windows: the steps of check-nvx-rs that run there (fmt; clippy with all features, without features, and with each of host, cli, testing, serde, and bundled; tests with all and default features and without features; docs; MSRV 1.89), those of check-aci-edge-sandboxes, the runtime adapter's protocol-only and host-only configurations, the Markdown link check, and nvx.py verify: all pass.
  • Linux (x86_64): the same, plus clippy with the guest feature, the macOS target check, nvx.py check-guest-binaries, and the runtime adapter's guest-only configuration: all pass.
  • On both, at 09b594a, the OpenVMM vectors test (--test openvmm_vectors -- --ignored) failed, not because of this change: at the review base's OpenVMM pin, platform_command_line_token_is_invariant admits one more token, maxcpus=, so the base's copy of it no longer matched. At 3ff9b99, which refreshes the copy, the test passes on Windows, as do fmt, the Markdown link check, and the tests of nvx::host::template::plan, which compile the copy.
  • The runtime adapter's suite of 153 scenarios, nvx.py test-runtime-adapter, on WHP and on Linux with MSHV:
  • Reviews: Copilot found no issues, and a final local review, in two rounds, found one documentation gap, which 09b594a fixes. Copilot again found no issues at 3ff9b99. A last local review then found that a release left the scratch file that an earlier version had created for a clone in RAM, and that a removal reported it as foreign; f663a1f keeps its name among the store's files, so that both remove it. At f663a1f, Copilot found no issues, and that local review, rerun over the whole diff, found none.
  • CI, a first manual full run on 09b594a, https://github.com/microsoft/nvx/actions/runs/40453845153. Every OpenVMM job passed, its VM tests on KVM, MSHV, and WHP among them, as did the quality checks, aci_edge_sandboxes on Linux and Windows, nvx-rs on Linux, and the guest artifacts. Three jobs failed, none because of this PR:
    • the NVX CLI on Linux and on Windows, at the comparison of OpenVMM's protocol vectors, which failed as described above, and which 3ff9b99 fixes. Every NVX microVM, platform, and performance job needs those two, so all of them were skipped, and this run has no NVX VM coverage;
    • nvx-rs on Windows, in a test of the base, host::template::store::leases::tests::windows::only_a_file_that_is_gone_or_whose_deletion_is_pending_was_removed: on the hosted runner, platform::file_identity opens a file whose access control list allows no one anything, which the test expects it to refuse (leases/tests.rs:2282). The other 1004 tests passed there. This PR changed that file only to stop naming the clone's scratch file, and now changes only a comment there.
  • CI, a second manual full run on 3ff9b99, https://github.com/microsoft/nvx/actions/runs/40519346177: 33 jobs passed, 5 failed, and 4 were skipped.
    • Passed: every OpenVMM job, its VM tests on KVM, MSHV, and WHP among them; the NVX CLI on Linux and on Windows, with the comparison of OpenVMM's protocol vectors; the NVX microVM tests' shards and debug-kernel jobs, and the platform jobs, on KVM, MSHV, and WHP; the quality checks, aci_edge_sandboxes on Linux and Windows, nvx-rs on Linux, and the guest artifacts.
    • Failed, none because of this PR: the three jobs of guests and sandboxes, on KVM, MSHV, and WHP, each at the runtime adapter's scenarios, in the 5 that map an export after a restore, which time out applying their mapping tables as OpenVMM: virtio-fs queues never serve requests after a restore behind the post-restore input gate #904 describes, while every other scenario, this PR's among them, passes; nvx-rs on Windows, in the base's test, as in the first run, while the other 1004 tests pass; and the required status check, which reports those.
    • Skipped: the performance gate and the rehearsal of guest artifact reuse, which run for pull requests alone, and the persistence of the performance baseline and the development release, which run for dev alone.
  • CI, a third manual full run, https://github.com/microsoft/nvx/actions/runs/40738052407, on esaurez/848-ci-20261028: 3ff9b99 with two commits of dev that the review base lacks, 88cc894, which pins OpenVMM e68871cc, the virtio-fs fix of OpenVMM: virtio-fs queues never serve requests after a restore behind the post-restore input gate #904, and 3fbaf8a, which fixes the base's leases test above. 35 jobs passed, 3 failed, and the same 4 as in the second run were skipped.
    • Passed: every job that passed in the second run, and now also nvx-rs on Windows, and guests and sandboxes on WHP, with all 153 of the runtime adapter's scenarios.
    • Failed, none because of this PR: guests and sandboxes on KVM and on MSHV, each in 2 of the 153 scenarios, a_clone_maps_its_export_beneath_its_root_before_its_sandbox_starts and a_run_s_execution_finds_its_shares_beneath_the_root, in which the sandboxed workload's writes through a read-write export are refused, with Permission denied, on the hosted Linux runners, which run the suite unprivileged. Both are scenarios of the base, whose clones use a scratch image and an image as long as the placeholder, and both pass on WHP and, as root, on Linux with MSHV. The required status check reports those.
  • At f663a1f, which keeps removing the scratch file that an earlier version left:
    • fmt, clippy with all features and without features, the nvx-rs library tests, and RUSTDOCFLAGS=-D warnings cargo doc pass on Windows, and the same, without the docs, pass on Linux (x86_64). On Linux, one test of the base's guest code, guest::sys::rooted::tests::paths_resolve_only_beneath_their_directory_and_through_no_link, failed once, with EAGAIN for a/../.. where it expects EXDEV, which openat2 with RESOLVE_BENEATH returns when a rename races with the lookup, as other tests' renames may; it passed in a full rerun and in 20 runs on its own. This PR changes no guest code.
    • The runtime adapter's 153 scenarios, with OpenVMM e68871cc and the guest artifacts of the third run, all pass on WHP and on Linux with MSHV. On WHP, each target's tests ran one at a time: run concurrently at 3ff9b99, a_clone_whose_activation_fails_ends_before_it_is_ready twice found the log of its VMM still held after the VMM had exited, which a handle that a concurrent spawn inherits would explain, in the base's code.

…atch file

A template's restore presents the capacity that its placeholder fixed for
the `distro` block device, and OpenVMM reads zeros past the end of a
shorter image, so a clone's image need no longer be exactly the
placeholder's length. `ClonePlan` admits an image of either format no
longer than the placeholder, with its logical and physical block sizes,
and refuses a longer one, or one of other block sizes, as before any VMM
starts. On Linux a file's physical block size is the I/O block size of
its filesystem, so the image must lie on a filesystem of the
placeholder's. A scratch image still presents the template's scratch
exactly.

The restore of a clone in RAM declares the template's scratch absent,
with `--restore-absent-scratch`, so that OpenVMM attaches it with no
file: its reads return zeros and its writes fail, and the clone's guest
never uses it. The host no longer creates the sparse `clone-scratch.raw`
in the sandbox's directory: `CloneUpper::Tmpfs` holds nothing,
`ClonePlan::scratch_path` returns `None` for a clone in RAM, and the
sandbox store loses `CLONE_SCRATCH_NAME` and `clone_scratch_path`.

The guest needs no change: it rereads a GPT disk's partition table
before it assembles the root, `nvx::gpt` requires only that a disk's
backup header lie inside the device, and a clone in RAM looks for no
scratch device.

The runtime adapter's GPT template now has a placeholder 16 MiB longer
than the platform's image, so the clones of either format that it serves
are all shorter than the capacity: they find their image's disk at the
capacity and their scratch device at the template's scratch length, and
read each to its end; nothing in the sandbox of a clone in RAM backs its
scratch device, and on Linux its VMM declares the scratch absent. A start
refuses an EROFS image and a GPT disk longer than the placeholder, in
place of the shorter image that the exact length refused.

Part of #848.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 053388a9-90ee-4632-8cd4-711de08d267e

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.

🔵 Needs a closer look

The stacked restore changes cross device and guest boundaries, with the final rebased head and shorter-image/absent-scratch VM scenarios still requiring validation on KVM, MSHV, and WHP.

No open issues 🎉

What changed in this PR

This PR implements #848 in the NVX host library, using OpenVMM’s zero-fill support so one template can serve shorter images and restore RAM-backed clones without creating scratch files.

Changes:

  • Accept images up to the template’s capacity while preserving block-size checks and exact scratch-image geometry requirements.
  • Replace RAM clones’ sparse scratch files with --restore-absent-scratch, updating the public API and store cleanup.
  • Expand unit and VM scenarios for shorter images, oversized-image refusals, and file-free scratch devices; update documentation.
File Description
tests/​runtime-adapter/​tests/​vm_clone.rs Tests shorter images, presented capacities, and absent scratch backing.
tests/​runtime-adapter/​src/​clone.rs Documents the relaxed image-length contract.
tests/​runtime-adapter/​src/​bin/​runtime-adapter-host.rs Updates command documentation for shorter images and RAM uppers.
tests/​runtime-adapter/​README.md Documents restore behavior and revised VM scenarios.
nvx-rs/​src/​host/​template/​store/​leases/​tests.rs Removes obsolete scratch-file cleanup expectations.
nvx-rs/​src/​host/​template/​store/​leases.rs Updates lease-release documentation.
nvx-rs/​src/​host/​template/​store.rs Documents restores with no scratch backing file.
nvx-rs/​src/​host/​template/​clone/​tests/​launch.rs Checks absent-scratch arguments and scratch-image launches.
nvx-rs/​src/​host/​template/​clone/​tests.rs Tests capacity admission, geometry refusals, and file-free RAM plans.
nvx-rs/​src/​host/​template/​clone/​launch.rs Adds the absent-scratch restore flag for RAM uppers.
nvx-rs/​src/​host/​template/​clone.rs Relaxes image-length validation and removes RAM scratch-file creation.
nvx-rs/​src/​host/​store/​tests.rs Updates clone-file cleanup tests.
nvx-rs/​src/​host/​store/​mod.rs Removes the clone scratch filename, path API, and cleanup entry.
nvx-rs/​src/​host/​store/​lifecycle/​tests.rs Updates removal tests for the reduced store file set.
nvx-rs/​src/​host/​sandbox/​distro.rs Documents image capacity and block-size requirements.
nvx-rs/​README.md Updates the host-library overview for both restore changes.

Review effort: Balanced

The template module's overview still said that a clone's plan refuses
an image of another length than the placeholder, `SuppliedLength` that
a supplied file has another length than the template's device, and
`IncompatibleHost` that the sandbox's filesystem may present another
geometry than the template's scratch, which only the removed sparse
scratch file could. Describe the capacity rule instead: an image longer
than the placeholder, or of other block sizes, and a scratch image of
another length than the template's scratch. The README's launch also
names `--restore-absent-scratch` for a clone in RAM.

Part of #848.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 053388a9-90ee-4632-8cd4-711de08d267e

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.

🔵 Needs a closer look

The draft depends on an unmerged base, with shorter-image restores and absent-scratch behavior still awaiting KVM, MSHV, and WHP validation and review of the final rebased diff.

No open issues 🎉

Review effort: Balanced

OpenVMM's platform tier admits a canonical maxcpus= token on the kernel
command line of a capture whose processors are not all online at boot,
since a12d9167. The openvmm_vectors excerpt of the allowlist predates it
and failed against that revision. It now admits maxcpus= as the source
does, and the vectors' README names the revision that it mirrors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4582f45d-8921-477b-8006-e9c7c709af88
(cherry picked from commit e2bc9ff)
@esaurez
Enrique Saurez (esaurez) changed the base branch from esaurez/847-pr2-start-run-suite-20261024 to esaurez/848-base-snapshot-20261028 October 28, 2026 17:31
@esaurez
Enrique Saurez (esaurez) marked this pull request as ready for review October 28, 2026 17:32
@esaurez
Enrique Saurez (esaurez) requested a balanced review from Copilot October 28, 2026 17:32

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

No active review issues were identified for the template-clone changes.

No open issues 🎉

Review effort: Balanced

…t a scratch file

Keep removing the scratch file that earlier versions created for a clone in RAM

A sandbox may still hold clone-scratch.raw from a clone that an earlier
version launched. Nothing creates the file any more, but the store keeps
its name among its files, and a release or a removal removes it, so that
such a sandbox can still be released and removed rather than left with a
file that the store calls foreign.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 053388a9-90ee-4632-8cd4-711de08d267e

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

No active review issues remain for the template-cloning changes.

No open issues 🎉

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