Skip to content

nvx-rs: return a restore's seed in memory and zero it on drop - #880

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
esaurez/842-pr1-owned-seed-20261024
Oct 28, 2026
Merged

Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
esaurez/842-pr1-owned-seed-20261024

Conversation

@esaurez

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

Copy link
Copy Markdown
Contributor

Chain: this PR → #936 → #937 → #939 → #940 → #941, on dev; #879, #881, #885, #886, #877, #932, #884, #887, and #933 have merged.

Part of #842 (PR 1 of 3: the owned seed).

Rebased onto #933. With nvx::guest::clone now below it, this commit also gives the test-only Restore::to a seed: None, and the clone tests compare a Restore by its Debug form, since a Restore is no longer PartialEq.

What and why

A capture wrote the restore packet's fresh entropy to a file, which the caller then read back, and Seed held its 64 bytes inline, so a move could leave a copy of them, and nothing erased them. This lets a library caller take a restore's entropy in memory, as an owned Seed that zeroes its bytes when it is dropped, while the command tools keep the file mode.

  • CaptureRequest::in_memory(scratch, generation_id) selects the in-memory mode: TimeAbi::capture writes no file, and the Restore holds the entropy as an owned Seed, which Restore::take_seed() hands over (None in the file mode, or once taken). CaptureRequest::new(scratch, generation_id, entropy_path) is unchanged and keeps the file mode.
  • Seed keeps its bytes in one heap allocation, overwrites them with ptr::write_volatile and a compiler_fence(SeqCst) when it is dropped, is neither Clone nor Copy, and formats none of its bytes: its Debug form is now Seed { .. } (it printed the generation ID; Seed::generation_id() still returns it). No dependency is added: the volatile writes are a safe wrapper in the guest's sys module, the only one that may use unsafe.
  • The command tools keep the file mode: nvx-time capture 0|1 GENERATION_ID ENTROPY_FILE [--finish] still builds CaptureRequest::new, with the same flags, mode 0600, order of operations, and diagnostics; nvx-snapshot and nvx-reseed are untouched.

How the bytes stay single-owner

  • The restore packet's entropy is decoded straight from the port buffer into the seed's allocation (RestorePacket::entropy is now a Seed), and the port buffer is zeroed right after decoding.
  • In the in-memory mode, that allocation moves into the Restore, the CaptureOutcome, and out through take_seed(): each move moves its address, not the bytes. In the file mode, the bytes are written to the file, and the seed is then dropped, which zeroes them.
  • Seed::read reads a seed file straight into the allocation; a read that fails zeroes what it read.
  • reseed copies the seed into the payload that RNDADDENTROPY takes, which zeroes its copy when the call returns.
  • Seed::from_bytes zeroes its own copy of its argument; the caller owns any copy that it keeps.

API changes

  • New: CaptureRequest::in_memory, Restore::take_seed, and Drop for Seed.
  • Restore and CaptureOutcome derive only Debug (they were also Clone, PartialEq, and Eq), since a restore may own the seed.
  • Seed::from_bytes is no longer a const fn, since it allocates.

Validation

This head is the previous one, e9a0339, rebased without changes onto #933's rebased head, e021475, which brings dev's changes since, OpenVMM at d2338ea2 and nvx-rs download among them; its per-commit checks and the checks below passed on it, and the tests under all features grew only by the 55 that nvx-rs download adds to host::cli.

On this head:

Linux x86_64:

  • nvx-rs: cargo fmt --check; Clippy with -D warnings, --all-targets --all-features, --lib, and --lib --no-default-features, and with each feature alone; cargo test --all-features (1534 passed), cargo test (802 passed), and the --lib --no-default-features build and test (258 passed); tests/host_modules.rs (22 passed); the bundled-artifacts test; rustdoc with -D warnings; the MSRV (1.89) check; the aarch64-apple-darwin check; the guest's tests and MSRV check with only guest.
  • Guest binaries: nvx.py check-guest-binaries (musl Clippy and tests, 848 passed), build-guest-binaries, test-guest-binaries (33 passed), check-static-elf.
  • tests/runtime-adapter: fmt, Clippy, build, tests, and MSRV in its protocol-only, host-only, and musl guest configurations.
  • scripts/check_markdown_links.py.
  • As root: the test privileged_captures_hand_over_the_entropy_in_their_mode, which runs the real capture() over the fake privileged operations, in a forked child alone in a mount namespace of its own with a tmpfs at /run, in both modes (two mutations, an in-memory mode that drops the seed and one that also writes it to a file, each made it fail), and the tests of guest::clone and guest::reseed (31 passed).

Windows (x86_64-pc-windows-msvc): nvx.py verify; nvx-rs fmt, Clippy (all features, --lib, no default features, and each feature alone), tests (all features 955 passed, default 817, no default features 258), tests/host_modules.rs, rustdoc, MSRV; tests/runtime-adapter in its protocol-only and host configurations; the Markdown links.

CI: this PR's own run, ci.yml run 40620699319, since #933's merge retargeted this PR to dev, passed: 34 jobs succeeded, among them nvx-rs and the CLI on Linux and Windows, the guest artifacts, OpenVMM's vmm-tests, the NVX microVM tests and the platform jobs on KVM, MSHV, and WHP, and the performance regression gate; 7 were skipped by design: OpenVMM's unit tests, since this PR doesn't change OpenVMM, the debug-kernel microVM tests on MSHV and WHP, which pull requests run on KVM only, and the 4 jobs that only dev runs.

At the top of the chain, #937, which holds this commit:

  • The runtime adapter's VM suites, on its previous head, 3d8aed5, with OpenVMM at e68871cc: 58 of 58 passed on WHP and on MSHV (vm_layered 43, vm_template 9, vm_restart 3, vm_steps 3). vm_template captures templates through nvx::guest::clone, whose capture nvx-rs: repair a clone's restore in the guest #936 switches to this PR's in-memory mode.
  • CI: ci.yml run 40617461513, dispatched on the chain's top, nvx-rs: give a clone's failures documented statuses #937, at 5683164, since the PRs above this one don't target dev and get no PR CI, passed: 36 jobs succeeded, among them nvx-rs and the CLI on Linux and Windows, the guest artifacts, OpenVMM's unit tests and vmm-tests, the NVX microVM tests and the platform jobs on KVM, MSHV, and WHP; the 6 jobs that only pull requests or dev run were skipped.

Tests added:

  • A Seed zeroes its bytes when it is dropped (observed after the volatile writes, before its allocation is freed, so no test reads freed memory), moves without copying them, and formats none of them; the RNDADDENTROPY payload zeroes its copy; zeroize itself.
  • The in-memory mode returns the seed and writes no file; the file mode writes the 64 bytes with mode 0600 and removes the file when the VM continues; nvx-time capture still builds the file mode.

In this PR, the capture of nvx::guest::clone still uses the file mode; #936 switches it to the in-memory mode.

Copilot AI balanced review requested due to automatic review settings October 24, 2026 20:06
@esaurez
Enrique Saurez (esaurez) marked this pull request as ready for review October 24, 2026 20:09

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

Security-sensitive entropy ownership and manual memory erasure warrant final human review, with hypervisor validation still pending.

No open issues 🎉

What changed in this PR

Implements the owned-seed foundation for #842, allowing guest library callers to receive restore entropy in memory while preserving the command tools’ file-based behavior.

Changes:

  • Adds in-memory capture requests and one-time seed transfer through Restore::take_seed().
  • Gives Seed heap-backed ownership and zero-on-drop cleanup; clears packet buffers and reseed payloads.
  • Adds ownership, cleanup, and capture-mode tests, and updates API documentation.
File Description
nvx-rs/​src/​guest/​time/​mod.rs Documents in-memory capture and reseeding.
nvx-rs/​src/​guest/​time/​decoders.rs Decodes packet entropy into an owned seed.
nvx-rs/​src/​guest/​time/​commands.rs Extracts capture argument parsing and tests file-mode compatibility.
nvx-rs/​src/​guest/​time/​api.rs Adds entropy delivery modes, seed transfer, buffer cleanup, and tests.
nvx-rs/​src/​guest/​sys/​secret.rs Introduces volatile memory erasure and its unit test.
nvx-rs/​src/​guest/​sys/​mod.rs Exposes the internal erasure helper.
nvx-rs/​src/​guest/​sys/​ioctl.rs Clears the reseed payload on drop and tests cleanup.
nvx-rs/​src/​guest/​reseed.rs Adds heap-backed seed ownership, redacted formatting, and cleanup tests.
nvx-rs/​README.md Describes capture modes and seed ownership guarantees.

Review effort: Balanced

@esaurez
Enrique Saurez (esaurez) changed the base branch from dev to esaurez/841-pr3-captures-in-vms-20261024 October 27, 2026 21:31
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/842-pr1-owned-seed-20261024 branch from 3c41d28 to e9a0339 Compare October 27, 2026 21:31
@esaurez
Enrique Saurez (esaurez) requested a balanced review from Copilot October 27, 2026 21: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.

🔵 Needs a closer look

Security-sensitive entropy erasure and privileged restore integration warrant human review alongside the cross-backend CI results.

No open issues 🎉

Review effort: Balanced

A capture wrote the restore packet's fresh entropy to a file, which the
caller then read back, and Seed held its 64 bytes inline, so a move
could leave a copy of them, and nothing erased them.

CaptureRequest::in_memory selects a mode in which TimeAbi::capture
writes no file: the Restore holds the entropy as an owned Seed, which
Restore::take_seed hands over. CaptureRequest::new keeps the file mode
as it was, and nvx-time capture keeps using it.

A Seed now keeps its bytes in one heap allocation, which a move leaves
in place, overwrites them with volatile writes and a compiler fence when
it is dropped, can't be cloned, and formats none of them. The capture
decodes the packet's entropy straight into that allocation and zeroes
the port buffer that held it, a seed file is read straight into it, and
the RNDADDENTROPY payload zeroes its copy once the ioctl returns. The
volatile writes live in the guest's sys module, the only one that may
use unsafe code, so no dependency is added.

Restore and CaptureOutcome are no longer Clone or comparable, since a
restore may own a seed, and Seed::from_bytes is no longer a const fn.

Part of #842

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fbebd4aa-2ad7-427d-80ea-49358a645e9a
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.

3 participants