Skip to content

nvx-rs: bind a resolver file into entries, and return removable runtime binds - #876

Merged
Pedro Henrique Penna (ppenna) merged 4 commits into
devfrom
esaurez/860-pr1-resolver-runtime-binds-20261024
Oct 27, 2026
Merged

Pedro Henrique Penna (ppenna) merged 4 commits into
devfrom
esaurez/860-pr1-resolver-runtime-binds-20261024

Conversation

@esaurez

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

Copy link
Copy Markdown
Contributor

Rebased on dev at d70c0ba3, after #883 merged.

Stack: #876 (this PR) → #871 → #873 → #874, on dev. #863, #864, #865, #866, #867, #868, #869, #870, #872, #875, #878, #882, and #883 are merged.

Part of #860: PR 1 of 2, the resolver bind, and removable runtime binds, in nvx::guest. PR 2, DNS in the test adapter, builds on it.

Why

A layered root's executions see the image's own /etc/resolv.conf, which an image may ship empty, so they resolve no names even where their network policy allows DNS. And Entry::bind_runtime leaves its binds in the caller's mount namespace with no way to remove them, so a caller that tears the root down in that namespace fails: a root's teardown detaches no busy mount.

What

  • The DNS server (nvx::protocol::cmdline): dns_server reads OpenVMM's virtnet_dns= token, DNS_TOKEN, strictly, as OpenVMM renders and reads it: the command line split at ASCII whitespace, as OpenVMM splits it, rather than as the kernel does; at most one token; one IPv4 or IPv6 address in the form that OpenVMM writes. write_resolver writes the resolver file that names it, nameserver ADDRESS, with the content that NVX's init writes into the initramfs's /etc/resolv.conf.
  • The reserved target (nvx::protocol::share): /etc/resolv.conf joins RESERVED_MOUNT_TARGETS, which the host's planner and the guest's mapper both refuse, so a share at it or below it, allowed until now, is refused. A neighbor such as /etc/resolvconf stays free.
  • The resolver bind (nvx::guest::entry): Entry::resolver(path) names a resolver file, and Entry::bind_runtime binds it read-only, nosuid, and nodev over the root's /etc/resolv.conf, after the machine ID and the time status, through the same walk and attach. As at /etc/machine-id, a symbolic link at /etc/resolv.conf itself is replaced with an empty file first. Without a resolver file, the image's own stays. A failure is entry::Error::Resolver.
  • Removable binds: bind_runtime returns RuntimeBinds, the binds that it made. RuntimeBinds::remove unmounts them in reverse order, attempting each and reporting every one that stays as a mappings::TeardownError, so that a caller removes them before the root's teardown, which then succeeds. If a bind fails, bind_runtime removes the binds that it made before it, and returns an entry::Error::Rollback, which holds the bind's error and names every bind that stays mounted, if that removal fails.

Compatibility

  • Entry::bind_runtime returns Result<RuntimeBinds, Error> instead of Result<(), Error>, and the non-exhaustive entry::Error has two new variants, Resolver and Rollback. RuntimeBinds isn't #[must_use], and dropping it leaves the binds mounted, as before, so callers whose mount namespace ends before the teardown, the managed agent and the test adapter's entries helper, are unchanged in behavior.
  • The shell init agent keeps its own list of reserved targets: the host refuses a share at /etc/resolv.conf before launch, and nvx-rs: run NVX's layered sandbox root from a Rust PID 1 on the guest library #817 replaces that agent.

Validation

At this head, f3e0317c, rebased onto dev's d70c0ba3 without conflicts: git range-diff shows each of the four commits unchanged, and the six commits of #866, #882, and #883 that this branch carried are dropped, since dev holds them.

  • Windows (x86_64): the Windows steps of check-nvx-rs (format, the three Clippy configurations, the tests with all, default, and no default features, the documentation, and MSRV 1.89), and the runtime adapter's protocol-only and host configurations: pass. check-aci-edge-sandboxes no longer exists, since aci_edge_sandboxes: retire the crate with its CI, commands, and docs #917 retired it.
  • Linux target (x86_64-unknown-linux-gnu, checked from Windows): Clippy on the library's three configurations and on the runtime adapter's guest and host configurations: pass.
  • CI: passes at this head (run 40453367434).

At the previous head, 50f7fe14, on #883's c453a9f2:

  • Windows (x86_64): the Windows steps of check-nvx-rs (format, both Clippy configurations, the tests with all, default, and no default features, the documentation, and MSRV 1.89), the runtime adapter's protocol-only and host configurations, and check-aci-edge-sandboxes without its agent bundling step: pass.
  • Linux (x86_64, kernel 6.18): every step of check-nvx-rs and check-aci-edge-sandboxes, including the macOS check, the guest binaries, the runtime adapter's guest configuration, and the agent bundling: pass. Clippy and the library's tests also pass at each commit. One known flaky test that this PR doesn't touch, guest::sys::signal::tests::a_thread_that_blocks_sigpipe_keeps_only_a_signal_that_was_pending, failed once at the second commit and passed when rerun.
  • Linux, as root: guest::entry's privileged tests, which check the binds' flags, the links' replacement, and binds removed before a teardown that then succeeds, and the mapper's privileged tests: pass. With their expected resolver contents altered, both entry tests failed, so they run rather than skip. CI runs them without privileges, where they return early.
  • CI: while this PR's base was tests: enforce the runtime adapter's networks in VMs #883's branch, dev's CI didn't run on it; once the PR targeted dev, CI passed at that head.
  • Not run here: VM tests. PR 2 adds them, on KVM, MSHV, and WHP.

Copilot AI balanced review requested due to automatic review settings October 24, 2026 18:40

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.

🟡 Changes recommended

Failed-bind rollback silently discards unmount errors, leaving unreported mounts that can block root teardown.

1 open issue
What changed in this PR

Implements the guest-library foundation for layered-root DNS in #860: optional resolver binds and explicit runtime-bind removal. Adapter integration and VM validation remain for PR 2.

Changes:

  • Adds strict DNS-token parsing and resolver-file writing.
  • Adds resolver binds and removable runtime-bind tracking.
  • Reserves /etc/resolv.conf across host and guest validation, with tests and documentation.
File Description
nvx-rs/​src/​protocol/​share.rs Reserves the resolver target and tests path boundaries.
nvx-rs/​src/​protocol/​mod.rs Documents DNS protocol helpers.
nvx-rs/​src/​protocol/​cmdline/​tests.rs Tests DNS parsing and resolver output.
nvx-rs/​src/​protocol/​cmdline.rs Adds DNS parsing and resolver writing.
nvx-rs/​src/​host/​sandbox/​tests.rs Tests resolver-target rejection.
nvx-rs/​src/​host/​sandbox/​specification.rs Extends reserved-target property tests.
nvx-rs/​src/​host/​sandbox/​plan/​tests.rs Tests rejection in export plans.
nvx-rs/​src/​host/​sandbox/​mount/​tests.rs Tests reserved targets and allowed neighbors.
nvx-rs/​src/​host/​filesystem.rs Extends filesystem export validation tests.
nvx-rs/​src/​guest/​root/​share.rs Tests guest share reservation boundaries.
nvx-rs/​src/​guest/​root.rs Documents the reserved resolver target.
nvx-rs/​src/​guest/​mappings/​tests.rs Tests reverse unbinding and failure reporting.
nvx-rs/​src/​guest/​mappings/​privileged.rs Adds privileged resolver-target rejection coverage.
nvx-rs/​src/​guest/​mappings.rs Tracks runtime binds and adds reverse unbinding.
nvx-rs/​src/​guest/​managed_agent/​seam.rs Adapts bind results and resolver errors.
nvx-rs/​src/​guest/​managed_agent/​dispatch.rs Tests rejection of resolver mappings.
nvx-rs/​src/​guest/​entry/​tests.rs Tests resolver binds, rollback, flags, and teardown.
nvx-rs/​src/​guest/​entry.rs Adds resolver configuration and removable runtime binds.
nvx-rs/​README.md Documents the new library APIs.
doc/​usage.md Updates CLI reserved-target documentation.
doc/​run.md Explains resolver-target reservation.

Review effort: Balanced

Comment thread nvx-rs/src/guest/entry.rs Outdated
Copilot AI balanced review requested due to automatic review settings October 24, 2026 18:56

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 scoped library changes preserve existing callers’ behavior, cover cleanup and rollback failures, and have no identified blocking issues.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/860-pr1-resolver-runtime-binds-20261024 branch from 1f17457 to 50f7fe1 Compare October 27, 2026 01:00
@esaurez
Enrique Saurez (esaurez) changed the base branch from dev to esaurez/822-pr11-network-enforcement-vms-20261023 October 27, 2026 01:00
Copilot AI balanced review requested due to automatic review settings October 27, 2026 01:00

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

Privileged mount removal, rollback handling, and public lifecycle API changes warrant final human review despite the supplied test coverage.

No open issues 🎉

Review effort: Balanced

Enrique Saurez and others added 3 commits October 26, 2026 21:58
OpenVMM names the guest's DNS server, one of the gateway's addresses,
with a virtnet_dns= token when the network policy lets the guest reach
the gateway's DNS service, and NVX's init writes the initramfs's
/etc/resolv.conf from it. Nothing reads it for a layered root, whose
executions see the image's own file (#860).

cmdline::dns_server reads the token as OpenVMM renders and reads it,
strictly: the command line split at ASCII whitespace, as OpenVMM splits
it, rather than as the kernel does, at most one token, and one IPv4 or
IPv6 address in the form that OpenVMM writes. cmdline::write_resolver
writes the resolver file that names it, with the content that the init
writes, so that a root's executions and the initramfs agree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 28b0282d-1872-4f0c-bc51-7d2a72ebd2db
A layered root's executions are to see the guest's resolver file over
the root's /etc/resolv.conf, as they see a generated machine ID over
its /etc/machine-id (#860). A live share or a host mapping there would
cover that file, or be covered by it, and only /etc itself and
/etc/machine-id are reserved today.

Add /etc/resolv.conf to RESERVED_MOUNT_TARGETS, which the host's
planner and the guest's mapper both refuse, so that a share at it, or
below it, is refused before launch, and in the guest. A neighbor such
as /etc/resolvconf stays free. Test it on the host, in the guest's
checks, and with the kernel's mounts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 28b0282d-1872-4f0c-bc51-7d2a72ebd2db
A layered root's executions see the image's own /etc/resolv.conf, which
an image may ship empty, so they resolve no names even where their
network policy allows DNS. And Entry::bind_runtime leaves its binds in
the caller's mount namespace with no way to remove them, so a caller
that tears the root down in that namespace fails, since a teardown
detaches no busy mount (#860).

Entry::resolver names a resolver file, such as the one that the guest's
init wrote from the command line's DNS server, and bind_runtime binds
it over the root's /etc/resolv.conf, after the machine ID and the time
status, read-only, nosuid, and nodev, through the same walk and attach.
As at /etc/machine-id, a symbolic link at /etc/resolv.conf itself,
which no bind can cover, is replaced with an empty file first. Without
a resolver file, the image's own stays.

bind_runtime now returns the RuntimeBinds that it made, which
RuntimeBinds::remove unmounts in reverse order, attempting each and
reporting every one that stays, so that a caller removes them before
the root's teardown, which then succeeds. If a bind fails, the call
removes the binds that it made before it. A caller whose mount
namespace ends before the teardown, as the managed agent's does, drops
them as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 28b0282d-1872-4f0c-bc51-7d2a72ebd2db
When a runtime bind fails, Entry::bind_runtime removes the binds that
it made before it, but it discarded the failures of that removal. A
bind that stayed mounted, such as a busy one, then went unreported,
and the caller, which never received the binds, learned of it only
when the root's teardown failed.

Report it instead: if the removal fails, the call returns an
Error::Rollback that holds the bind's error and the TeardownError of
the binds that stay, and whose source is the bind's error. The managed
agent maps it to the errno of the bind that failed, as before. The
call still attempts every removal, whatever failed before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 28b0282d-1872-4f0c-bc51-7d2a72ebd2db
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