Skip to content

cli: keep a cold run's console when OpenVMM exits before the peer check - #1523

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
feature/nvx-refactorfrom
agents/slim-one-shot-flake
Nov 12, 2026
Merged

Pedro Henrique Penna (ppenna) merged 1 commit into
feature/nvx-refactorfrom
agents/slim-one-shot-flake

Conversation

@ppenna

Copy link
Copy Markdown
Contributor

Part of #1076 (slice one-shot-flake: the flaky one_shot_runs_exit_with_the_workload_or_openvmm_status).

What and why

Symptom. On Linux, a cold nvx-rs run could 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_status failed that way at one_shot.rs:372 (left: [], the fake's openvmm: booted the microVM\n missing):

Root cause. The run connects to the boot console on which OpenVMM listens (--virtio-console listen=…/console.sock), and connect_endpoint 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 ("the original socket peer exited"), and connect_before_exit yields 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 until nvx-rs 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; before Linux 6.16, SO_PEERPIDFD itself already fails with EINVAL for a reaped peer. Every other connection, the control sessions and the restore-ready endpoint, keeps refusing exited peers, unchanged. Windows is unchanged: its connect_endpoint checks 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::kill and it share pidfd_send_signal; From<OwnedFd> for PidFd wraps the SO_PEERPIDFD descriptor.
  • host/platform/linux/endpoint.rs and mod.rs: connect_child_endpoint (cli feature); it and connect_endpoint share connect_peer; check_original_peer takes accept_unreaped, which only connect_child_endpoint sets.
  • host/cli/run.rs: connect_running uses it on Linux.

#1501 neither causes nor worsens the flake: its run.rs change 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 in platform/linux/tests.rs.

Out of scope, not fixed here and tracked separately:

  • one_shot::forwarding_failure_stops_the_verified_vm_during_connect_and_handshake once 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.
  • Windows, suspected and not verified with a real OpenVMM: a named-pipe server that ends without FlushFileBuffers discards what its client hasn't read (in a local probe, ReadFile failed with ERROR_BROKEN_PIPE and no data once the server process had ended), and OpenVMM's Windows pipe poll_flush does 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.

Host Kernel Base, idle Base, load Fix, idle Fix, load
nvx-kvm-test (KVM, VM) 6.6.159 0/100 42/100 0/100 0/100
nvx-mshv-test (MSHV, VM) 6.18.34 0/100 22/100 0/100 0/100

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 connect and 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_PEERPIDFD returns a pidfd for an unreaped peer, through which signal 0 succeeds, and fails with EINVAL once the peer is reaped; on 6.18, it returns a pidfd in both cases, and signal 0 fails with ESRCH once the peer is reaped.

Checks:

  • New test 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 on connect_child_endpoint(..).unwrap() with "the original socket peer exited".
  • cargo fmt --all --check: passed on Windows and Linux.
  • cargo clippy --locked … -- -D warnings for the six feature sets of check-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-fast on Linux 6.18: 2697 passed, 0 failed, 78 ignored. On 6.6: the same 28 guest:: 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 cli on 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-features and cargo +1.89 check --locked --all-targets --all-features on Linux: passed.
  • python scripts/check_test_layout.py: passed. doc/acceptance-case-matrix.md names none of the tests that this changes.
  • A code-review agent reviewed the diff and found no issue.

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-refactor lists a_transferred_listener_cannot_authenticate_its_exited_creator. Its Python launcher moved into run_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.

@ppenna Pedro Henrique Penna (ppenna) added tests-modified Changes existing tests, test data, or test wiring feat-nvx-refactor labels Nov 12, 2026
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

This pull request changes existing tests

It 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

Change What Before After
modified test fn a_transferred_listener_cannot_authenticate_its_exited_creator in host::platform::linux::tests nvx-rs/src/host/platform/linux/tests.rs:114 nvx-rs/src/host/platform/linux/tests.rs:114

Relocated unchanged: 0; added: 2. Compared the merge base 580adf029fd2 with d36c57a0e541.

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 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat-nvx-refactor tests-modified Changes existing tests, test data, or test wiring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants