Repository navigation
cli: keep a cold run's console when OpenVMM exits before the peer check - #1523
Merged
Pedro Henrique Penna (ppenna) merged 1 commit intoNov 12, 2026
Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
Pedro Henrique Penna (ppenna) merged 1 commit into
Conversation
Contributor
This pull request changes existing testsIt modifies or removes 1 existing test asset(s), listed below. Review each with care: a change to a test can hide a regression that the test caught. See doc/contribute.md#changes-to-existing-tests. Test code
Relocated unchanged: 0; added: 2. Compared the merge base |
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues block approval of the Linux cold-run console race fix.
No open issues 🎉
What changed in this PR
Fixes a Linux cold-run race that could silently discard console output when OpenVMM exits before peer verification. This addresses the one-shot test flake tracked under #1076 without changing Windows behavior.
Changes:
- Allows the cold-run console to accept an exited but unreaped child, while still rejecting reaped peers.
- Adds a pidfd signal-zero check to determine whether a process still holds its PID; other endpoint connections retain their existing checks.
- Adds regression coverage for unreaped-child acceptance and rejection after reaping, sharing the existing transferred-listener test setup.
| File | Description |
|---|---|
nvx-rs/src/host/sys/process.rs |
Adds PID-retention checking and shares pidfd signal handling. |
nvx-rs/src/host/platform/linux/tests.rs |
Tests child-peer acceptance before reaping and rejection afterward. |
nvx-rs/src/host/platform/linux/mod.rs |
Exposes the child-specific connector within the CLI build. |
nvx-rs/src/host/platform/linux/endpoint.rs |
Adds child-specific connection handling while preserving stricter checks elsewhere. |
nvx-rs/src/host/cli/run.rs |
Uses the child-specific connector for Linux cold-run consoles. |
Review effort: Balanced
A cold `nvx-rs run` on Linux could exit with OpenVMM's status, 0, and print none of OpenVMM's console output, with no error. The CLI test `one_shot_runs_exit_with_the_workload_or_openvmm_status` failed that way in CI run 43156460151 (job 130977435086, on #1501), and once in a local run of SL6.2b's on nvx-mshv-test. The run connects to the boot console on which OpenVMM listens, then checks the socket's original peer: its `SO_PEERCRED` PID must be OpenVMM's, and its `SO_PEERPIDFD` pidfd must not report an exit. The fake OpenVMM accepts the connection, writes its line and exits at once. When it exits between the run's `connect` and that check, the check refuses the peer as exited, and `connect_before_exit` yields to OpenVMM's exit status, dropping the connection with the output already queued in it. A real OpenVMM, too, writes its output and exits without waiting for the run to read it, so a run that stalls at that point on a loaded host loses its output silently. The console now connects with `connect_child_endpoint`, which checks the peer as `connect_endpoint` does, but accepts an original peer that has exited and has not been reaped, and still refuses a reaped one. That is safe because the run's own unreaped child keeps its PID, which no other process can reuse until the run reaps it, so a peer that holds the child's PID is the child. `PidFd::holds_its_id` tells an unreaped process from a reaped one with signal 0 through the pidfd, which fails with `ESRCH` only once the process is reaped. Every other connection, the control sessions and the restore-ready endpoint, still refuses an exited peer. Windows is unchanged: its `connect_endpoint` checks only the pipe's server process ID. Tests: `a_child_endpoint_accepts_its_exited_child_until_the_child_is_reaped` connects to a listener whose creator, the test's child, has exited and is not reaped: `connect_endpoint` refuses it, `connect_child_endpoint` accepts it and reads through it, and both refuse it once the test has reaped the creator. It shares the Python launcher of `a_transferred_listener_cannot_authenticate_its_exited_creator`, which now runs through the same helper and still reaps the creator first. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/slim-one-shot-flake
branch
from
November 12, 2026 06:10
5dd9341 to
d36c57a
Compare
Pedro Henrique Penna (ppenna)
marked this pull request as ready for review
November 12, 2026 06:10
Pedro Henrique Penna (ppenna)
merged commit Nov 12, 2026
385192f
into
feature/nvx-refactor
81 of 84 checks passed
This was referenced Nov 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1076 (slice
one-shot-flake: the flakyone_shot_runs_exit_with_the_workload_or_openvmm_status).What and why
Symptom. On Linux, a cold
nvx-rs runcould exit with OpenVMM's status, 0, without OpenVMM's console output and without an error.tests/cli/one_shot.rs::one_shot_runs_exit_with_the_workload_or_openvmm_statusfailed that way atone_shot.rs:372(left: [], the fake'sopenvmm: booted the microVM\nmissing):nvx-rs / ubuntu-latest(130977435086), on sandbox: record the network's options and build its policy at each start (SL3.10c) #1501: the casesrun: Ubuntuandrun: network outcome report.run: Ubuntu, once, in a full--all-featuresrun.Root cause. The run connects to the boot console on which OpenVMM listens (
--virtio-console listen=…/console.sock), andconnect_endpointthen checks the socket's original peer: itsSO_PEERCREDPID must be OpenVMM's, and itsSO_PEERPIDFDpidfd must not report an exit. The fake OpenVMM accepts the connection, writes its line and exits at once. When it exits between the run'sconnectand that check, the check refuses the peer ("the original socket peer exited"), andconnect_before_exityields to OpenVMM's exit status: the run goes on without a console and drops the connection, with the output already queued in it. An instrumented build printed that reason for every failing case. A real OpenVMM also writes its console output and exits without waiting for the run to read it, so a run that stalls at that point on a loaded host loses OpenVMM's output silently too. With real guests that is unlikely, since the boot console holds the guest's output until the run connects, but it can happen.Fix. On Linux, the cold run's console connects with a new
connect_child_endpoint, for that console only. It accepts a peer that has exited but hasn't been reaped, and still refuses a reaped one. That is safe because the run's own unreaped child keeps its PID, which no other process can reuse untilnvx-rsreaps it, so a peer that holds the child's PID is the child.PidFd::holds_its_idtells an unreaped process from a reaped one with signal 0 through the pidfd, which fails withESRCHonly once the process is reaped; before Linux 6.16,SO_PEERPIDFDitself already fails withEINVALfor a reaped peer. Every other connection, the control sessions and the restore-ready endpoint, keeps refusing exited peers, unchanged. Windows is unchanged: itsconnect_endpointchecks only the pipe's server process ID. The change adds no sleep and changes no assertion.host/sys/process.rs:PidFd::holds_its_id;PidFd::killand it sharepidfd_send_signal;From<OwnedFd> for PidFdwraps theSO_PEERPIDFDdescriptor.host/platform/linux/endpoint.rsandmod.rs:connect_child_endpoint(clifeature); it andconnect_endpointshareconnect_peer;check_original_peertakesaccept_unreaped, which onlyconnect_child_endpointsets.host/cli/run.rs:connect_runninguses it on Linux.#1501 neither causes nor worsens the flake: its
run.rschange only builds the network configuration differently before the launch, and SL3.10c's agent measured 37 of 150 failures at its base and 32 of 150 at its head under load. This branch is rebased on it (533bbb8).Measured size: 42 core lines in 4 counted files (
cli/run.rs,platform/linux/endpoint.rs,platform/linux/mod.rs,sys/process.rs); the test is inplatform/linux/tests.rs.Out of scope, not fixed here and tracked separately:
one_shot::forwarding_failure_stops_the_verified_vm_during_connect_and_handshakeonce exceeded its 3-second completion bound in its handshake phase on a loaded Linux host (seen by SL6.2a's agent). Another mechanism, which this change doesn't cover.FlushFileBuffersdiscards what its client hasn't read (in a local probe,ReadFilefailed withERROR_BROKEN_PIPEand no data once the server process had ended), and OpenVMM's Windows pipepoll_flushdoes nothing, so console output that OpenVMM writes just before it exits may be lost on WHP. The Windows fake flushes its console before it exits, so the tests can't show it.Validation
Load matrix of
one_shot_runs_exit_with_the_workload_or_openvmm_status, built with--all-features, its test binary run directly, 100 runs per cell; load is 16 busy loops on the hosts' 8 vCPUs. Base is 533bbb8, fix is 5dd9341.Earlier, at 07c508c on nvx-kvm-test: 4 of 30 and 2 of 30 idle runs failed, and 60 of 100 under the same load. With a 100 ms sleep inserted, uncommitted, between the run's
connectand the peer check, 5 of 5 runs failed at the base (20 failing cases, all "the original socket peer exited"), and none of 10 with the fix.Kernel behavior that the fix relies on, probed on both hosts with a listener whose creator had exited: on 6.6,
SO_PEERPIDFDreturns a pidfd for an unreaped peer, through which signal 0 succeeds, and fails withEINVALonce the peer is reaped; on 6.18, it returns a pidfd in both cases, and signal 0 fails withESRCHonce the peer is reaped.Checks:
host::platform::linux::tests::a_child_endpoint_accepts_its_exited_child_until_the_child_is_reaped: passes on 6.6 and 6.18. With the acceptance turned off, it fails onconnect_child_endpoint(..).unwrap()with "the original socket peer exited".cargo fmt --all --check: passed on Windows and Linux.cargo clippy --locked … -- -D warningsfor the six feature sets ofcheck-nvx-rs: passed on Windows and on both Linux hosts (glibc);--target x86_64-unknown-linux-musl --all-targets --all-features: passed on both Linux hosts.cargo test --locked --all-features --no-fail-faston Linux 6.18: 2697 passed, 0 failed, 78 ignored. On 6.6: the same 28guest::tests fail at the base and with the fix, a known failure on that kernel, and the rest pass.cargo test --locked: 1292 passed, 0 failed, on both.cargo test --locked --lib --no-default-features: 283 passed on both.cargo test --locked --all-features --test clion Windows: 110 passed, 2 ignored; the flaky test 50 times: no failure. Not under load on Windows: its code path is unchanged, and the workstation is shared.RUSTDOCFLAGS='-D warnings' cargo doc --locked --no-deps --all-featuresandcargo +1.89 check --locked --all-targets --all-featureson Linux: passed.python scripts/check_test_layout.py: passed.doc/acceptance-case-matrix.mdnames none of the tests that this changes.Left to CI: the rest of the matrix, including the macOS check, the public API snapshots (no API change), the guest binaries and the microVM jobs.
Tests modified (
tests-modified):check_test_changes.py --base origin/feature/nvx-refactorlistsa_transferred_listener_cannot_authenticate_its_exited_creator. Its Python launcher moved intorun_with_transferred_listener, which the new test shares, with an argument that leaves the listener's creator unreaped for the new test; the existing test still reaps it first and checks the same things.