Skip to content

nvx-rs: give user IDs and file modes distinct types in Linux checks - #1011

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
nvx-issue-fix/643-typed-uid-mode
Oct 29, 2026
Merged

Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
nvx-issue-fix/643-typed-uid-mode

Conversation

@ppenna

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

Copy link
Copy Markdown
Contributor

Fixes #643.

The Linux privacy checks of the host's platform module, store_dir_problem, owner_only_problem, and foreign_writer in nvx-rs/src/host/platform/linux.rs, took a file's owner, its mode, and the current user as three u32s. A caller that passed a mode as a user ID, or a user ID as a mode, still compiled, and the check then compared the wrong values: at dev, store_dir_problem(0o40700, 1000, 1000), which passes a mode as the owner and a user ID as the mode, compiles. The checks now take distinct types, so such a call fails to compile, and nothing else changes.

Based on dev at e4ca7acaa. dev has since moved to 8f3a840f6: #974, an OpenVMM pin; #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, 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.

Change

Commit Files Change
0e79fce4a nvx-rs nvx-rs/src/host/platform/linux.rs, nvx-rs/src/host/store/files.rs, nvx-rs/src/host/store/tests.rs, nvx-rs/src/host/template/store/tests.rs The checks take nix::unistd::Uid for the owner and the current user, and nix::sys::stat::Mode for the mode; the directory and file helpers and the mode constants of the module take Mode too
  • Types. nix, already a dependency of the host feature, provides both, so the crate adds no newtype.
  • Boundary. The new private owner_and_mode converts where std::os::unix::fs::MetadataExt returns raw integers, with Uid::from_raw and Mode::from_bits_truncate. The latter keeps exactly the permission bits, 0o7777, that the checks masked with PERMISSION_BITS, and drops the bits of the file's type, so the mask and PERMISSION_BITS go away. The current user is Uid::effective(), the geteuid() that sys::effective_ids() returned.
  • Other places in platform/linux.rs. create_dir_of_mode, restore_private_file_mode, whose only caller, the store's open_lock, converts its PRIVATE_FILE_MODE, and the constants PRIVATE_DIR_MODE, STORE_DIR_MODE, and GROUP_AND_OTHER_BITS take Mode; Mode::bits() hands the raw mode to the standard library's DirBuilderExt::mode and PermissionsExt::from_mode.
  • No behavior change. Uid displays as its decimal integer, and Mode formats in octal as its bits, so the messages are the same. The comparisons test the same bits: foreign_writer's 0o022 is S_IWGRP | S_IWOTH, and its comparison with 0 is Uid::is_root.

The issue's acceptance criteria:

  • A mode passed as a user ID, or the reverse, fails to compile. Scratch calls, not committed, that pass integers, a Mode where a Uid belongs, and a Uid where a Mode belongs each fail with E0308.
  • The tests pass unchanged in what they assert. The unit tests of store_dir_problem and owner_only_problem keep their cases and assertions, converting the raw st_mode values that they pass, with the bits of a directory's type, as the boundary does. The store's privacy tests, which reach the checks through real directories and files, and the restore-ready socket's test, which now compares the permission bits as a Mode, assert the same.
  • No behavior changes, as above.

Validation

BASE is dev at e4ca7acaa87f2b0f3b909b2e390c1824f2de1fae; the head is 0e79fce4ac0ad3708b763937710695af24a968dd. The commit is signed.

On Windows, in nvx-rs with NVX_BUNDLE=skip:

  • Reproduction: at BASE, a scratch store_dir_problem(0o40700, 1000, 1000) passes cargo check --lib --target x86_64-unknown-linux-gnu; at the head, it and the two other scratch calls fail with E0308.
  • cargo fmt --all --check.
  • Clippy with -D warnings in check-nvx-rs's three configurations (--all-targets --all-features, --lib, and --lib --no-default-features) for x86_64-unknown-linux-gnu, the only target that compiles platform/linux.rs, and natively with --all-targets --all-features.
  • rustdoc with -D warnings and --all-features for x86_64-unknown-linux-gnu.
  • The minimum supported Rust version, cargo +1.89 check --all-targets --all-features --target x86_64-unknown-linux-gnu, and the macOS check, cargo check --all-targets --all-features --target aarch64-apple-darwin.
  • The runtime adapter: cargo fmt --all --check; Clippy with -D warnings for its protocol-only and host-only configurations, natively and for x86_64-unknown-linux-gnu, and for its guest-only configuration for x86_64-unknown-linux-musl; and cargo +1.89 check --all-targets of each configuration for its Linux target.

On a Linux host, natively; no VM was started, since the change has no hypervisor behavior:

Profile Backend Host type Test Result
nvx-kvm-test kvm virtual-machine nvx-rs: cargo test --locked --all-features --no-fail-fast 2007 passed, 3 failed, 33 ignored; the 3 failures are pre-existing, below
nvx-kvm-test kvm virtual-machine nvx-rs: cargo test --locked --no-fail-fast 1009 passed, 0 failed
nvx-kvm-test kvm virtual-machine nvx-rs: cargo build --lib --no-default-features and cargo test --lib --no-default-features built; 266 passed
nvx-kvm-test kvm virtual-machine nvx-rs: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features clean
nvx-kvm-test kvm virtual-machine The affected tests by name: host::store::tests::{the_store_is_private_to_the_current_user, stores_belong_to_the_current_user_with_mode_700}, host::template::store::tests::{the_store_is_private_to_the_current_user, private_directories_belong_to_the_current_user_and_grant_others_nothing}, host::platform::linux::tests::a_restore_ready_socket_lets_its_owner_connect_whatever_the_umask, and tests/store.rs, with a_restrictive_umask_leaves_the_store_usable 136 and 14 passed, 0 failed
nvx-kvm-test kvm virtual-machine Runtime adapter, protocol-only and host-only configurations: build, tests, and rustdoc with -D warnings pass

Pre-existing failures on that host, in guest code that this change doesn't touch: guest::entry::tests::runtime_binds_need_a_mount_root_unless_there_are_none gets EOPNOTSUPP from a mount, 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 get EINVAL where they expect ESRCH. They fail the same way on a rerun, and at BASE on the same host (Azure Linux 3, kernel 6.6).

Left to CI, which ran them: run 40902235482, attempt 1, passed on head 0e79fce4a, with Required status check and the other 38 checks passing and 7 skipped:

  • Clippy and rustfmt natively on Linux, since the host's toolchain has neither component; both ran on Windows, Clippy for the Linux target, as above.
  • The artifact-bundling test, the guest binaries' checks, and the runtime adapter's guest-only build, tests, and rustdoc, none of which compiles the host's platform module, and cargo test on Windows, which doesn't compile it either.
  • The hypervisor jobs that CI schedules for an nvx-rs change, although the checks run on the host before any VM starts: the NVX microVM suites on Linux/KVM, Linux/MSHV, and Windows/WHP, the KVM debug-kernel suite, the OpenVMM unit and VMM tests, and the platform checks, all passed.

Follow-ups

  • Swapping the owner and the current user still type-checks, since both are user IDs; the issue's acceptance criteria cover only a mode and a user ID mistaken for each other.
  • check_private(owner: u32, mode: u32, user: u32) in nvx-rs/src/guest/root/share.rs, a guest-side check, and the u32 modes of nvx-rs/src/host/cli/sandbox/lifecycle/directory.rs have the same shape, outside the Linux platform module that this issue covers.

store_dir_problem, owner_only_problem, and foreign_writer in the Linux
platform module took a file's owner, its mode, and the current user as
three u32s. A caller that passed a mode as a user ID, or a user ID as
a mode, still compiled, and the privacy check then compared the wrong
values (#643): at dev, store_dir_problem(0o40700, 1000, 1000), which
passes a mode as the owner and a user ID as the mode, compiles.

The checks now take nix::unistd::Uid for the owner and the current
user, and nix::sys::stat::Mode for the mode. nix, a dependency of the
host feature, already provides both, so the crate needs no newtype of
its own. The new owner_and_mode converts at the boundary where
std::os::unix::fs::MetadataExt returns raw integers, with
Uid::from_raw and Mode::from_bits_truncate. The latter keeps exactly
the permission bits that the checks masked with PERMISSION_BITS,
0o7777, and drops the bits of the file's type, so the mask, and
PERMISSION_BITS, go away. The current user is Uid::effective(), the
geteuid() that sys::effective_ids() returned.

The other places in platform/linux.rs that passed modes as bare
integers take Mode too: create_dir_of_mode, restore_private_file_mode,
whose only caller, the store's open_lock, converts its
PRIVATE_FILE_MODE, and the constants PRIVATE_DIR_MODE, STORE_DIR_MODE,
and GROUP_AND_OTHER_BITS. Mode::bits() hands the raw mode to the
standard library's DirBuilderExt::mode and PermissionsExt::from_mode.
The module-wide import of Mode replaces the local ones of
Directory::open_file and Directory::open_dir_with.

Nothing else changes. Uid displays as its decimal integer and Mode
formats in octal as its bits, so the messages are the same. The
comparisons test the same bits: foreign_writer's 0o022 is now
S_IWGRP | S_IWOTH, and its comparison with 0 is Uid::is_root.

Passing an integer, a Mode where a Uid belongs, or a Uid where a Mode
belongs now fails to compile with E0308. The unit tests of
store_dir_problem and owner_only_problem keep their cases and
assertions, converting the raw st_mode values that they pass, with
the bits of a directory's type, as the boundary does. The store's
privacy tests, which reach the checks through real directories and
files, and the restore-ready socket's test, which now compares the
permission bits as a Mode, pass unchanged in what they assert.

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

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 are reported for the user-ID and file-mode type-safety changes.

No open issues 🎉

What changed in this PR

This PR addresses #643 by using distinct Uid and Mode types in NVX’s Linux host privacy checks, preventing accidental mixing of user IDs and file modes while preserving existing behavior and diagnostics.

Changes:

  • Converts raw metadata values at the boundary and uses typed ownership checks, permission checks, and mode constants.
  • Updates lock-file permission restoration to pass a typed mode.
  • Adapts existing privacy and socket-permission tests without changing their assertions.
File Description
nvx-rs/​src/​host/​template/​store/​tests.rs Adapts directory privacy tests to typed inputs.
nvx-rs/​src/​host/​store/​tests.rs Adapts store ownership and mode tests to typed inputs.
nvx-rs/​src/​host/​store/​files.rs Converts the lock-file mode for permission restoration.
nvx-rs/​src/​host/​platform/​linux.rs Introduces typed ownership and permission checks and metadata conversion.

Review effort: Balanced

@rahul05ranjan Rahul (rahul05ranjan) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core claim here — that nix::sys::stat::Mode gives compile-time separation of user IDs from modes while preserving the old masking semantics — checks out against the dependency you're pinned to:

  • I pulled the nix 0.31.3 source (from your Cargo.lock): Mode is defined as exactly the 0o7777 bits (S_IRWXU…S_IXOTH plus S_ISUID/S_ISGID/S_ISVTX), with the file-type bits (S_IFMT etc.) living in the separate SFlag type. So Mode::from_bits_truncate(metadata.mode()) is precisely the old mode & PERMISSION_BITS — the removal of the internal mask in store_dir_problem/owner_only_problem/foreign_writer is safe because truncation now happens once at construction in owner_and_mode (linux.rs:459), and callers can't smuggle type bits in since Mode can't represent them.
  • sys::effective_ids() isn't orphaned by the switch to Uid::effective() — it's still used by host/launch.rs and the template clone tests, so no dead-code fallout.
  • The behavioral assertions are unchanged where it matters: 0o42700 still reports "has mode 2700" (setgid survives truncation and display via bitflags' Octal impl), !uid.is_root() matches the old uid != 0, and mode.intersects(GROUP_AND_OTHER_BITS) matches mode & 0o077 != 0.
  • The test rewrites in store/tests.rs and template/store/tests.rs route through the same from_bits_truncate construction the production code uses, so they keep testing the truncation seam rather than bypassing it.

One small thing, take it or leave it: in store/files.rs:100 you convert at the call site with Mode::from_bits_truncate(PRIVATE_FILE_MODE). Since PRIVATE_FILE_MODE stays a raw u32 (0o600) for the other uses (DirBuilder, Permissions, and the & PRIVATE_FILE_MODE != PRIVATE_FILE_MODE check at line 131), that's reasonable, but making it a Mode const and calling .bits() at the u32 sites would put the whole file on the typed side of the line too.

@ppenna

Copy link
Copy Markdown
Contributor Author

Rahul (@rahul05ranjan) Thanks for checking the truncation against nix 0.31.3. I'm keeping PRIVATE_FILE_MODE a u32 in store/files.rs. Three of its four uses are std's Unix extension traits, OpenOptionsExt::mode, Permissions::from_mode, and PermissionsExt::mode, which take and return u32 on every Unix. Mode::bits() returns mode_t, which libc 0.2.189 defines as u32 on Linux but u16 on Apple targets, and CI checks the crate for aarch64-apple-darwin. A Mode constant would therefore need a conversion at each of those three sites, an identity conversion on Linux, instead of the single conversion at the Linux-only restore_private_file_mode call.

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: give user IDs and file modes distinct types in the Linux platform primitives

3 participants