Repository navigation
nvx-rs: let an execution drive a program that its caller started - #831
Merged
Merged
Conversation
An Execution drove only a program that spawn started: it waited for the launch report that Spawned holds, on pipes that Pipes::new created. A caller that starts its programs by means of its own, as one that joins namespaces that already exist, or that installs a seccomp filter, which neither spawn nor entry does, could not use it, and kept a loop of its own for the program's output, input, timeout, and ending. Execution::adopt now drives such a program. The caller hands it the Child that names the program, and its own ends of the program's pipes. Child::from_pidfd names a child by a pidfd that the caller holds, such as the one that clone3 returns with CLONE_PIDFD; a child that only its ID names gets a pidfd opened by that ID, as Execution::new opens one, and a failure to open it ends what the options name, as for new. Pipes::from_ends takes the caller's ends once it has checked that each is a pipe's end that goes the way of its stream, and makes each nonblocking and close-on-exec, as Pipes::new makes the execution's own. A launcher may start a child with an exit signal other than SIGCHLD, which the child keeps until it runs a program. A Child's waits now name its child whatever signal the child reports its end with, as __WALL makes them. A pidfd that its opener made nonblocking makes Child::wait fail with WouldBlock while the child runs, as waitid through it does, but an execution, which never blocks on a status, adopts its child as it does any other. The caller saw how the program's launch went, so the execution waits on no launch report: its next event reports the launch at once, with no failure, which a binding hands its runner as it does a spawned program's. From then on the execution drives the program as one that spawn started. A child whose status the caller's collector handed over before the adoption has ended: the execution closes the program's input, and kills its cgroup if its output is still open, as an observation does. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4e92c43-6bfc-4784-9c86-3ddd2a4aa409
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
The expanded process-identity, status-collection, and termination semantics warrant human review despite no concrete defects being identified.
No open issues 🎉
What changed in this PR
This PR lets NVX’s guest execution driver manage programs started by external launchers, reusing its existing input/output, timeout, termination, and binding behavior.
Changes:
- Adds
Execution::adopt,Child::from_pidfd, and validated pipe-end adoption. - Extends child waits to handle exit signals other than
SIGCHLD. - Adds adoption and binding tests and documents the new APIs.
| File | Description |
|---|---|
nvx-rs/src/guest/sys/process.rs |
Extends wait semantics and adds a test child helper. |
nvx-rs/src/guest/sys/mod.rs |
Exports the new test helper. |
nvx-rs/src/guest/sys/fd.rs |
Validates pipe ends and sets descriptor flags. |
nvx-rs/src/guest/process.rs |
Accepts caller-owned pidfds and tests child waits. |
nvx-rs/src/guest/exec/tests.rs |
Tests adoption, streams, cancellation, and cleanup. |
nvx-rs/src/guest/exec/binding/tests.rs |
Tests adopted executions through a binding. |
nvx-rs/src/guest/exec/binding.rs |
Documents support for adopted executions. |
nvx-rs/src/guest/exec.rs |
Implements adoption and shared execution initialization. |
nvx-rs/README.md |
Documents adoption in the guest API overview. |
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.
An
Executiondrives only a program thatspawnstarted: it waits for the launch report thatSpawnedholds, on the pipes thatPipes::newcreated. A caller that starts its programs by means of its own cannot use it, and keeps a loop of its own for each program's output, input, timeout, and ending. Such a caller launches with steps that neitherspawnnorentrytakes, such as joining namespaces that already exist or installing a seccomp filter, and it already knows how its launch went, and holds the program's pidfd and its own ends of the program's pipes.Adoption.
Execution::adoptdrives such a program. It takes theChildthat names the program and the caller's ends of the program's pipes, and from then on drives the program as one thatspawnstarted: the same flow control, input, timeout, kills, cgroup, status collection or handover, and binding.Child::from_pidfdnames a child by a pidfd that the caller holds, such as the one thatclone3returns withCLONE_PIDFD. Nothing is opened by the child's ID then, so the caller's collector may even have taken the child's status first, and hands it over withobserve. A child that only its ID names gets a pidfd thatadoptopens by that ID, asExecution::newopens one, so nothing may collect its status beforeadoptreturns; if opening it fails,adoptends what the options name, asnewdoes, by ID unlessESRCHtells that the status is gone, and closes the pipes before it kills the cgroup.Pipes::from_endstakes the caller's ends, and refuses withEINVALany end that is not a pipe's end going the way of its stream: a writer for the input, readers for the outputs. It makes each nonblocking and close-on-exec, asPipes::newmakes the execution's own; the program's ends of the same pipes are other open file descriptions, and keep blocking. The pipes must carry bytes, not packets, since no check on a reader can tell that its writer writes packets, asO_DIRECTmakes it.observedoes, and reads what remains of the output.Waits. A launcher may start a child with an exit signal other than
SIGCHLD, which the child keeps until it runs a program. AChild's waits now name its child whatever signal the child reports its end with, as__WALLmakes them: before, a child that ended with another exit signal before it ran a program wasECHILDto them, and an execution that adopted it would have taken it for one whose status another collector took. A pidfd that its opener made nonblocking, asPIDFD_NONBLOCKdoes, makesChild::waitfail withWouldBlockwhile the child runs, aswaitidthrough it does, which the documentation now says; an execution never blocks on a status, so it adopts such a child as it does any other.Execution::newkeeps its behavior: it andadoptshare one constructor, and the launch report becomes one of the two launches that an execution reports. A pseudoterminal stays out of scope, since an execution reads and writes pipes alone.Tests. An adopted program's launch comes first, at once, with no descriptor to wait on, and then its output and status; it reads the input that the execution writes to its end; one that only its ID names gets its pidfd by that ID and is cancelled with its process group; one whose status was observed before the adoption has its input closed, its cgroup killed, and its output read; adopting one whose status another collector took fails with
ESRCHand kills nothing by ID, so a descendant in its process group lives on; a failed adoption without a spare descriptor ends the program and its cgroup;from_endsmakes the caller's ends nonblocking and close-on-exec, leaves the program's ends blocking, and refuses an end that is no pipe's and one that goes the other way; a bound adopted program starts, streams, and settles as a spawned one does;Child::from_pidfdwaits through the caller's pidfd, findsECHILDonce another collector took the status, takes it fromobserve, and refuses impossible IDs; an adopted program that a nonblocking pidfd names runs as any other; and a child that reports its end with no signal is waited for, through its pidfd and by its ID. The tests start their programs with the standard library'sCommand, a launcher other thanspawn, and the one that makes descriptors inheritable for a moment runs in a process of its own.Validation
At this branch's head, d00841e, on dev at ffb03cd:
clone3and the newer mount calls, as a normal user: innvx-rs, withNVX_BUNDLE=skip,cargo fmt --all --check,cargo clippy --locked --all-targets --all-features -- -D warnings,cargo clippy --locked --lib --no-default-features -- -D warnings,cargo test --locked --all-features,cargo test --locked, the build and test without default features, the bundled-artifacts test,cargo docwithRUSTDOCFLAGS=-D warnings,cargo +1.89 check, and the macOS check;nvx.py check-guest-binaries,build-guest-binaries, andtest-guest-binaries;tests/runtime-adapterin its default,host, and muslguestconfigurations, each with itscargo +1.89 check; theaci_edge_sandboxeschecks withtest-agent-bundling; and the Markdown links.from_endstest fails; without closing an observed child's input, or with a pidfd opened by the ID of an observed child, the observed-status test fails; without the pidfd thatadoptopens by ID, the cancellation,ESRCH, and failed-adoption tests fail; with a kill by ID afterESRCH, theESRCHtest fails; without ending the program on a failed adoption, the failed-adoption test fails; with an adopted launch that reports a failure, six exec tests and the binding test fail; withfrom_pidfddropping its pidfd, its test fails; and without__WALL, the wait for a child with no exit signal finds no child.both_streams_take_turns_while_each_has_outputfails at dev too, in 41 of 100 runs of the exec and process tests, and in 49 of 100 with this change: once both pipes are drained, it expects their ends at once, while a child that another test's thread forked may still hold the program's ends until it runs its own program. None of the last 40 CI runs failed it.Left to CI: KVM, MSHV, and WHP, whose VM suites run the managed agent and the runtime adapter, whose executions
Execution::newstill starts; the Windows runs of thenvx-rsandaci_edge_sandboxeschecks; and the quality gates of the Python and shell code, which this change does not touch.