Skip to content

nvx-rs: wrap the file operations of the sandbox store in a type - #1012

Merged
Pedro Henrique Penna (ppenna) merged 2 commits into
devfrom
nvx-issue-fix/645-store-file
Oct 29, 2026
Merged

Pedro Henrique Penna (ppenna) merged 2 commits into
devfrom
nvx-issue-fix/645-store-file

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #645.

The sandbox store reached its private files through free functions of nvx-rs/src/host/store/files.rs, to which every caller passed a file's path and its description again, and every caller had to remember to check the privacy of what it opened. This adds StoreFile, a type private to the store module that holds one file of the store, its path with what messages call it, and gives it the operations that the store uses. SandboxStore now opens, creates, replaces, and reads its lock, launch journal, runtime record, capability, and log only through it.

Based on dev at b8b82a606. dev has since moved to 8f3a840f6: #1013, documentation; #960 and #977, which change nvx-rs's clone activation and sandbox options; #1015, which moves the init's command-line readers onto protocol::cmdline; and baseline and diagram updates. None touches these files or calls the store functions that this changes, and the branch merges cleanly: at 8f3a840f6, the merge passes cargo fmt --all --check, Clippy with -D warnings and --all-targets --all-features natively on Windows and for x86_64-unknown-linux-musl, cargo test --locked --all-features on Windows, and the Markdown link check.

Problem

store/files.rs offered create_new, open_lock, write_private, read_private, and remove_if_present as free functions (introduced by #628, whose review asked for a type that wraps them). Each caller passed a path and a description again, and the privacy checks lived with the callers:

  • read_private refused a record or the capability that is not a regular file, or that others may open.
  • take_lock opened its lock with open_lock and then checked it with platform::private_file_problem itself.
  • read_capability built the message of a missing capability from a description of its own.

A new record, or a new caller, could skip a check or name a file differently.

Change

Commit Files Change
b0fbf2f7a nvx-rs store/tests.rs Pins, on every platform, the messages with which the store refuses a missing capability, one of the wrong length or all zero, a record that does not parse, and a directory in the place of a record or of the capability; and, on Windows, the whole messages, rather than their ends, that refuse a journal, a capability, or a lock that others may open. The tests pass with the store as it was.
b59f94f27 nvx-rs store/files.rs, store/mod.rs, store/record.rs, store/tests.rs Adds StoreFile with create_new, open_lock, replace, read, and remove_if_present. SandboxStore builds one for each of its lock, journal, runtime record, capability, and log; record::read and record::write take one.

How the change meets the issue:

  • One type per file. StoreFile holds the path and the description (sandbox lock, sandbox launch journal, sandbox runtime record, sandbox control capability, log; the records' come from Record::DESCRIPTION), and the messages of its operations name the file through them.
  • The operations that the store uses. replace is the atomic replacement through a flushed temporary file (write_private, unchanged); read is read_private, which is gone, so the store reads a file only as a StoreFile, which refuses one that is not a regular file or not private; create_new creates it exclusively; remove_if_present removes it.
  • The lock's check travels with it. StoreFile::open_lock opens the lock and refuses it if others may open it, the check that take_lock made itself, and maps failures with the caller's mapper, so that cannot lock <path> still names them all.
  • Private to the store module. StoreFile is pub(super), and so are SandboxStore's builders. private_options, which only files.rs uses, is now private to it.
  • Requirements. The on-disk layout, the record format, the error kinds, and the messages are unchanged: the first commit's tests pass unchanged after the second.

What stays as it was, deliberately:

  • The template store (template::Store, its captures and leases, and the clone records) keeps using the free functions create_new, open_lock, write_private, remove_if_present, and sync_dir, which stay pub(crate): the issue concerns the sandbox store's files. See the follow-ups.
  • The store's lifecycle still removes its lock file, and reaches the entries of its directory, by name: it removes the lock file only while the lock's handle still names it, and reports a RemovalError, and it removes the other entries through a handle of the directory. A StoreFile models neither, and changing them would change error types.
  • SandboxStore::log_path and capability_path, which callers use, still return the paths.

Validation

BASE: dev at b8b82a606e2e501be0ad9ecf630d9c99e31c086a. Both commits are signed.

Local, on Windows, the applicable commands of check-nvx-rs:

  • Each commit, with git rebase -x: cargo fmt --all --check, and cargo clippy --locked --all-targets --all-features -- -D warnings both natively and with --target x86_64-unknown-linux-gnu. All clean.
  • b0fbf2f7a at BASE: the new test and the_store_is_private_to_the_current_user pass (22 host::store::tests passed).
  • The head: Clippy --lib and --lib --no-default-features, natively and for Linux; cargo test --locked --all-features (1,090 library tests and every integration test passed), cargo test --locked (948), cargo build and cargo test --lib --no-default-features (266); cargo doc --no-deps --all-features with -D warnings; the minimum Rust version, 1.89, with cargo check --all-targets --all-features; the macOS check for aarch64-apple-darwin; and the runtime adapter's fmt, protocol-only and host-only Clippy (also for Linux), build, tests, documentation, and minimum Rust version. All clean.

On a Linux host, from a bundle of these commits:

Host profile Backend Host type Test Result
nvx-kvm-test none needed (Azure Linux 3, kernel 6.6) virtual-machine Each commit: cargo test --all-features --lib host::store and --test store Passed: 59 and 17 at each commit
nvx-kvm-test none needed virtual-machine Head: cargo test --locked --all-features --no-fail-fast 2,008 passed; 3 failed, all pre-existing (below)
nvx-kvm-test none needed virtual-machine Head: cargo test --locked (1,010), --lib --no-default-features (266), cargo doc with -D warnings, the runtime adapter's host-only build, tests (107), and documentation Passed

The 3 failures are guest tests that this change does not touch, and they fail identically at BASE on this host, so they are pre-existing there: guest::entry::tests::runtime_binds_need_a_mount_root_unless_there_are_none (Operation not supported, at src/guest/entry/tests.rs:542), and guest::exec::tests::a_start_after_another_collector_took_the_status_ends_nothing_by_id and guest::exec::tests::adopting_a_program_whose_status_another_collector_took_ends_nothing_by_id (EINVAL, not ESRCH, at src/guest/exec/tests.rs:766 and :1333).

The host has no rustfmt, Clippy, or Rust 1.89, so formatting, Clippy, and the minimum Rust version ran on Windows, Clippy also for the Linux target.

Left to CI, which ran them: the Linux runs of Clippy and of the minimum Rust version, the bundled-artifacts test, and the guest binaries and the runtime adapter's guest-only configuration, which do not compile host::store. No hypervisor was used locally, since the change touches no microVM behavior; CI's microVM jobs, which changes to nvx-rs select, cover the sandbox commands that use the store on every backend.

CI at head b59f94f27: run 40914352621, attempt 2, succeeded, Required status check included: 31 checks passed and 12 were skipped. Its attempt 1 failed only in NVX microVM tests / Linux / MSHV / Shard 1 of 2, at its Validate runner step, before any test ran: check H4 of nvx.py doctor on the runner azure-mshv-1 found its backend's TSC rate 110.467 ppm from the rate measured against chronyd's frequency estimate, over the limit of 100 ppm. That qualifies the runner's clock, which this change cannot affect, so the failed jobs were rerun once, and passed.

Follow-ups

  • The template store repeats what this removes from the sandbox store: template/store.rs opens its lock with open_lock and checks private_file_problem itself, template/store/capture.rs and template/clone/records.rs check the privacy of what they open themselves, and store::lease::read_private reads records with the same checks as StoreFile::read plus a size limit. A type that also takes a size limit could serve both stores.

The sandbox store reads its runtime record, launch journal, and
capability, and takes its lock, through free functions of
store/files.rs, to which each caller passes the file's path and its
description again (#645). The tests check that a record or the
capability fails to read when it is not a regular file, is malformed,
or is not private, but mostly through fragments of the messages, such
as "is malformed" or "allows other users", and no test checks the
messages of a missing or an invalid capability. A change that gathers
those paths and descriptions in one type could then change what a
message calls a file, or which file it names, without a test noticing.

The new test pins, on every platform, the messages with which the
store refuses a missing capability, one of the wrong length or all
zero, a runtime record or a journal that does not parse, and a
directory in the place of either record or of the capability, which
Unix opens and refuses as no regular file, and Windows refuses to
open. The Windows test of the store's privacy now pins the whole
message with which the store refuses a journal, a capability, and a
lock whose access control list allows other users, rather than its
end.

The tests pass with the store as it is.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
store/files.rs offered the sandbox store's file operations as free
functions: create_new, open_lock, write_private, read_private, and
remove_if_present. Every caller passed a file's path again, with the
description that messages give the file, and had to remember to check
the privacy of what it opened: read_private refused a record or the
capability that others may open, but take_lock checked its lock file
with platform::private_file_problem itself, and read_capability built
the message of a missing capability from a description of its own. A
new record or caller could skip a check, or name a file differently
(#645).

A StoreFile now holds one file of the store, its path with what
messages call it, and gives it the operations that the store uses:
create_new, open_lock, replace, read, and remove_if_present.
SandboxStore builds one for each file that it creates and reads, its
lock, launch journal, runtime record, capability, and log, and opens,
creates, replaces, and reads those files only through it:

- StoreFile::read is read_private, which is gone, so the store reads
  a file only as a StoreFile, which refuses it if it is not a regular
  file or not private.
- StoreFile::open_lock opens the lock and refuses it if others may
  open it, the check that take_lock made, and maps the failures with
  the caller's mapper, so that "cannot lock" still names them all.
- record::read and record::write take the StoreFile of the record,
  whose description is the record's Record::DESCRIPTION, and name it
  in their messages.
- The messages of the capability and of the log, and the refusal of a
  lock, name the file by its description: "sandbox control
  capability", "log", and "sandbox lock", as they did.

private_options, which only files.rs uses, is private to it now. The
free functions that the template store uses, create_new, open_lock,
write_private, remove_if_present, and sync_dir, stay pub(crate). The
store's lifecycle still removes its lock file, and reaches the entries
of its directory, by name: it removes the lock file only while the
lock's handle still names it, and reports a RemovalError, and it
removes the other entries through a handle of the directory, neither
of which a StoreFile models.

The on-disk layout, the record format, the error kinds, and the
messages are unchanged. The previous commit's test, which pins the
messages with which the store refuses its records and its capability
on every platform, and on Windows those that refuse a journal, a
capability, or a lock that others may open, passes unchanged, as do
the FIFO and link refusals on Unix, the rest of the store's unit
tests, which now write records through their StoreFiles, and
tests/store.rs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 29, 2026 16:43

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 StoreFile refactor has no active review issues.

No open issues 🎉

What changed in this PR

Addresses #645 by introducing a store-private StoreFile type that keeps each sandbox file’s path, description, and privacy checks together, while preserving existing formats, error messages, and atomic replacement behavior.

Changes:

  • Wraps sandbox file creation, reading, locking, replacement, and removal in StoreFile.
  • Routes sandbox lifecycle and record operations through the new type, leaving template-store helpers unchanged.
  • Adds regression tests for read errors and strengthens Windows privacy-error assertions.
File Description
nvx-rs/​src/​host/​store/​tests.rs Pins error messages and adapts record-writing tests to StoreFile.
nvx-rs/​src/​host/​store/​record.rs Updates record reading and writing to accept StoreFile.
nvx-rs/​src/​host/​store/​mod.rs Adds file builders and routes sandbox operations through them.
nvx-rs/​src/​host/​store/​files.rs Introduces StoreFile with centralized read and lock privacy checks.

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit efbb8f0 into dev Oct 29, 2026
81 of 83 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the nvx-issue-fix/645-store-file branch October 29, 2026 19:39
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.

nvx-rs: wrap the file operations of the sandbox store in a type

2 participants