Repository navigation
nvx-rs: fix exec's kill by ID and the guest tests on older Linux kernels - #1351
Merged
Merged
Conversation
On kernels before Linux 6.16, Execution::new and Execution::adopt killed, by its ID, the process group of a program whose status another collector had taken, while a descendant kept that group: the kill by ID that an execution must never send, since the ID may name another group by then. Both open the program's pidfd by its ID through sys::open_pidfd, and end what the options name by ID unless the open fails with ESRCH, which tells that the status is gone. pidfd_open answers ESRCH for an ID that nothing holds, but before Linux 6.16 it answers EINVAL for an ID that no thread-group leader has while something still holds it: the process group or the session that a reaped process led, or a thread that leads no process. torvalds/linux@8cf4b738f6d8, first in Linux 6.16, made those answers ESRCH and ENOENT. Child::with_pidfd, which documents ESRCH for an ID that names no process, returned EINVAL there too. sys::open_pidfd now reports EINVAL and ENOENT as ESRCH: for a positive ID and no flags, they mean nothing else. A test hook in it, which the allowlist records, lets a test have pidfd_open answer the test's thread as a kernel before Linux 6.16 does: EINVAL for ENOENT, and for ESRCH while a process group has the ID. The two exec tests of a program whose status another collector took now run with the kernel's own answers and with those of a kernel before Linux 6.16, and check that the descendant still runs before they check the error, so that a kill by ID fails them as such. A new test checks that open_pidfd fails with ESRCH, either way, for a reaped leader whose group a member keeps and for a thread that leads no process. At 8394746, on nvx-kvm-test, a KVM VM with Linux 6.6.159.1-1.azl3, both exec tests failed with EINVAL in three runs of three, and on nvx-mshv-test, an MSHV VM with Linux 6.18.34.mshv3-1.azl3, they passed. For a reaped leader whose group lives, one whose group is gone, and a thread that leads no process, os.pidfd_open answered EINVAL, ESRCH, and EINVAL on the first, and ESRCH, ESRCH, and ENOENT on the second. With this change, the library tests pass on nvx-mshv-test, and on nvx-kvm-test all but the runtime binds' test, which the next commit fixes. Without the new mapping, the new test fails on both hosts, and so do both exec tests, because the kill by ID ends their descendant: with the kernel's own answers on nvx-kvm-test, and with the simulated ones on nvx-mshv-test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
runtime_binds_need_a_mount_root_unless_there_are_none expected InvalidInput for a root that is not a mount's root, but only the unique mount IDs of statx, which Linux 6.8 added, tell a mount's root: on an earlier kernel, Entry::bind_runtime's check of the root fails first, with EOPNOTSUPP, as sys::file_status documents. bind_runtime needs Linux 6.8 or later, as it documents, but the test checked for neither, so it failed in each run on nvx-kvm-test, a KVM VM with Linux 6.6.159.1-1.azl3, at 8394746. The test now checks for unique mount IDs, as the activation tests do, and expects InvalidInput where the kernel has them and EOPNOTSUPP where it does not. Its other checks hold on every kernel, and stay. With this change, the library tests pass on nvx-kvm-test and on nvx-mshv-test, an MSHV VM with Linux 6.18.34.mshv3-1.azl3, and so does NVX_BUNDLE=skip cargo test --locked --all-features. On nvx-kvm-test, scripts/nvx_rs_coverage.py, which exited 1 at 8394746 since the issue's three tests failed again when it reran them, now passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
This pull request changes existing testsIt modifies or removes 4 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
Test policy
Relocated unchanged: 0; added: 5. Compared the merge base |
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues remain for the guest process-handling compatibility changes.
No open issues 🎉
What changed in this PR
Fixes #1341 by making guest process handling consistent across Linux kernels, preventing exec from killing a process group by ID after another collector has reaped its leader.
Changes:
- Normalize
pidfd_openerrors for IDs that name no process toESRCH, and document that behavior. - Add simulated pre-6.16 kernel responses and regression coverage for exec start and adoption.
- Adjust the runtime-bind test to expect
EOPNOTSUPPwhen unique mount IDs are unavailable.
| File | Description |
|---|---|
scripts/test-layout-allowlist.json |
Allows the kernel-response simulation hook in production code. |
nvx-rs/src/guest/sys/tests.rs |
Exports the simulation control for exec tests. |
nvx-rs/src/guest/sys/process/tests.rs |
Adds older-kernel simulation and pidfd error regression coverage. |
nvx-rs/src/guest/sys/process.rs |
Maps EINVAL and ENOENT to ESRCH when opening a pidfd. |
nvx-rs/src/guest/process.rs |
Documents consistent Child::with_pidfd error behavior. |
nvx-rs/src/guest/exec/tests.rs |
Checks descendant survival with native and simulated kernel responses. |
nvx-rs/src/guest/entry/tests.rs |
Makes runtime-bind error expectations depend on unique mount ID support. |
Review effort: Balanced
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.
Fixes #1341
What and why
On kernels before Linux 6.16,
nvx::guest::execkilled, by its ID, the process group of a program whose status another collector had taken while a descendant kept the group: the kill by ID that an execution must never send, since the ID may name another group by then. Two library tests of thenvxcrate caught it in every run on a Linux 6.6 host, and a third failed there because it checked what only Linux 6.8 can tell without checking for Linux 6.8. NVX's own guests run Linux 6.18, so NVX isn't affected, but an adapter that runsnvx::guest::execon an older guest kernel is.Root cause
Execution::newandExecution::adoptopen the program's pidfd by its ID throughsys::open_pidfd, and if that fails with anything butESRCH, they end the program, and withOptions::process_groupits process group, by their IDs.pidfd_openanswersESRCHfor an ID that nothing holds, but before Linux 6.16 it answersEINVALfor an ID that no thread-group leader has while something still holds it: the process group or the session that a reaped process led, or a thread that leads no process. torvalds/linux@8cf4b738f6d8, first in Linux 6.16, made those answersESRCHandENOENT. Python'sos.pidfd_openanswers, on the test VMs:nvx-kvm-testEINVALESRCHEINVALnvx-mshv-testESRCHESRCHENOENTChild::with_pidfd, which documentsESRCHfor an ID that names no process, returnedEINVALthere too.Changes
sys::open_pidfdreportsEINVALandENOENTasESRCH: for a positive ID and no flags, they mean nothing else. Its documentation andChild::with_pidfd's say so. One test hook in it, between the call and the mapping, lets a test havepidfd_openanswer the test's thread as a kernel before Linux 6.16 does, withEINVALforENOENT, and forESRCHwhile a process group has the ID, asBEFORE_LINUX_6_9simulates a kernel before Linux 6.9, so that hosts whose kernels answer as CI's do test the older answers too. The newan_id_that_no_process_has_opens_no_pidfd_on_any_kernelchecks thatopen_pidfdfails withESRCH, with the kernel's own answers and with the simulated ones, for a reaped leader whose group a member keeps and for a thread that leads no process.statx, which Linux 6.8 added, tell a mount's root, so on an earlier kernelEntry::bind_runtime's check of the root fails first, withEOPNOTSUPP, assys::file_statusdocuments.runtime_binds_need_a_mount_root_unless_there_are_nonenow checks for unique mount IDs as the activation tests do, and expectsInvalidInputwith them andEOPNOTSUPPwithout them.Acceptance criteria of #1341
NVX_BUNDLE=skip cargo test --locked --all-featurespasses on a Linux 6.6 host and on one with Linux 6.16 or later: met onnvx-kvm-testandnvx-mshv-test.Execution::newandExecution::adoptend nothing by ID for a program whose status another collector took, with thepidfd_openanswers of a kernel before Linux 6.16: met by the two exec tests' simulated runs, which catch the kill by ID without the mapping on the 6.18 host, as the kernel's own answers do on the 6.6 host.nvx-kvm-test: met.scripts/nvx_rs_coverage.pyexits 0 there, in 116 s, with no reruns.Found, not fixed here
nvx::host::sys::PidFd::opendocumentsEINVALfor the ID of a thread that leads no process, andopen_processinnvx-rs/src/host/platform/linux/process.rstakesESRCHandEINVALfor an exited process, but from Linux 6.16 such an ID isENOENT, whichopen_processreturns as an error instead. That is host code, outside nvx-rs: three guest tests fail on Linux 6.6 hosts; before Linux 6.16, exec ends a reaped program's process group by ID #1341, so it is left for a separate change.Validation
Locally, on Windows, at the head: rustfmt (
cargo fmt --all --check),check_test_layout.py, which fails without the new allowlist entry,check_test_changes.py --base origin/dev,test_test_layout.pyandtest_test_changes.py(80 tests), andnvx.py verify. Windows builds never compilenvx-rs/src/guest, so the Linux hosts ran the Rust gates.The hosts ran
c741ba71c, the head before its commit messages gained these results; the head,06c59acf2, and its first commit have the same trees asc741ba71cand its first commit,14eabcdf.nvx-kvm-test, Linux 6.6.159.1-1.azl3kvmvirtual-machine8394746fd(dev)nvx-kvm-testkvmvirtual-machine8394746fdNVX_BUNDLE=skip cargo test --locked --all-features --no-fail-fastnvx-kvm-testkvmvirtual-machine8394746fdpython3 scripts/nvx_rs_coverage.pynvx-mshv-test, Linux 6.18.34.mshv3-1.azl3mshvvirtual-machine8394746fdnvx-kvm-testkvmvirtual-machineopen_pidfdansweredEINVALfor both IDs, and both exec tests killed their descendant with the kernel's own answersnvx-mshv-testmshvvirtual-machine[ESRCH, ENOENT]from the kernel and[EINVAL, EINVAL]simulated, and both exec tests killed their descendant with the simulated answerskvm,mshvvirtual-machinekvm,mshvvirtual-machineNVX_BUNDLE=skip cargo test --locked --all-featureskvm,mshvvirtual-machinekvm,mshvvirtual-machinenvx-kvm-testkvmvirtual-machinenvx-kvm-testkvmvirtual-machinepython3 scripts/nvx_rs_coverage.py, with cargo-llvm-cov 0.9.1 and Rust 1.97.1'sllvm-toolsin temporary Rust homesLeft to CI: check-nvx-rs on Windows, validate-nvx on Linux and Windows, check-quality, and the microVM scenarios on KVM, MSHV, and WHP. NVX's guests run Linux 6.18, where the change alters only the answer for a thread's ID, which no scenario opens a pidfd for, so the hosts reran no microVM scenario.
Changes to existing tests
check_test_changes.py --base origin/devlists 4, hence thetests-modifiedlabel:a_start_after_another_collector_took_the_status_ends_nothing_by_idandadopting_a_program_whose_status_another_collector_took_ends_nothing_by_idnow run with the kernel's ownpidfd_openanswers and with those of a kernel before Linux 6.16, and check that the descendant still runs before they check the error. They check what they did,ESRCHand a descendant that nothing ended; the simulated run makes them check the older answers on every kernel, and the new order makes a kill by ID fail them as such rather than at the error, which nvx-rs: three guest tests fail on Linux 6.6 hosts; before Linux 6.16, exec ends a reaped program's process group by ID #1341 notes. Without the mapping, both fail on both hosts.runtime_binds_need_a_mount_root_unless_there_are_nonekeeps itsInvalidInputcheck where the kernel has unique mount IDs, and expectsfile_status'sEOPNOTSUPPwhere it doesn't. Its other checks don't change.scripts/test-layout-allowlist.jsonallows one test hook innvx-rs/src/guest/sys/process.rs. To test the mapping, the simulation has to sit between thepidfd_opencall and the mapping, insideopen_pidfd, so it cannot move to the tests module.check_test_layout.pyfails without the entry, counting the hook.