Repository navigation
tests: run the runtime adapter's executions in VMs through its host program - #870
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unbounded output capture risks exhausting host memory, and the descendant-cleanup scenario needs deterministic synchronization.
2 open issues
What changed in this PR
Adds the host-command and VM-test portion of #805, connecting the runtime adapter’s execution engine to programs running in assembled guest roots.
Changes:
- Adds reusable host-side execution handling and a single-batch session driver.
- Introduces
execand--workload-identity, with argument validation and output reporting. - Expands protocol, driver, and VM tests and documents the new behavior.
| File | Description |
|---|---|
tests/runtime-adapter/tests/vm_layered.rs |
Adds VM execution and workload-identity scenarios. |
tests/runtime-adapter/tests/layered_launch.rs |
Tests rejection of invalid execution and identity options. |
tests/runtime-adapter/tests/host_session.rs |
Tests execution driving, output, protocol failures, and deadlines. |
tests/runtime-adapter/tests/guest_session.rs |
Reuses the shared host-side execution implementation. |
tests/runtime-adapter/src/worker.rs |
Uses the shared concurrent-execution limit. |
tests/runtime-adapter/src/host/executions.rs |
Adds the single-batch execution driver. |
tests/runtime-adapter/src/host.rs |
Exports the driver and rejects host-only execution messages. |
tests/runtime-adapter/src/exec/tests.rs |
Tests cancellation delivery with zero grace and settlement time. |
tests/runtime-adapter/src/exec.rs |
Adds HostExecution and the shared execution limit. |
tests/runtime-adapter/src/bin/runtime-adapter-host.rs |
Implements execution commands, identity options, and result formatting. |
tests/runtime-adapter/README.md |
Documents commands, driver constraints, and test coverage. |
Review effort: Balanced
79c86f4 to
dddb286
Compare
916f3d0 to
e487ea4
Compare
dddb286 to
d472108
Compare
e487ea4 to
654b1da
Compare
d472108 to
e4e5256
Compare
e4e5256 to
ba8a5f6
Compare
| /// The chunks of an execution's input that the guest may hold for `exec`. | ||
| const EXEC_INPUT_FRAMES: u32 = 4; | ||
| /// The chunks of an execution's output, of both streams, that `exec` may hold. | ||
| const EXEC_OUTPUT_FRAMES: u32 = 8; |
There was a problem hiding this comment.
how are this consts defined? why those specific numbers?
There was a problem hiding this comment.
They're values chosen for tests that make a valid configuration, not derived from a formula. The 16 KiB chunk, the 1 s grace and the 5 s settlement time follow the example in nvx::exec's documentation, whose 64 KiB window the input's 4 chunks, and each stream's half of the 8 output chunks, match; the 60 s timeout matches the launch's START_TIMEOUT, ample for the tests' programs. What they must respect are the limits that the guest and the host enforce: chunks of at most exec::MAX_CHUNK bytes; at most worker::MAX_EXECUTION_MEMORY, 1 MiB, held in the guest for each execution (these hold 96 KiB: a chunk of each stream and the input's window); and output windows of at most host::MAX_OUTPUT_FRAMES, 32, frames. fc0bc9f adds a comment by the constants that says so.
ca51780 to
2f3935d
Compare
2f3935d to
6fca426
Compare
…rogram The guest's worker runs executions in the assembled root, under their identities, but the host program could not start one, so no test ran an execution in a VM, and nothing entered a real root. The host program's new exec command reattaches to a VM and runs several executions at once over one session. A single-threaded driver, host::Executions, serves the host's side of each, an exec::HostExecution: the engine's peer, with a sink of the program's output and a source of its input. The driver refuses a start that the guest would not decode, and a second start of an ID, before it sends anything, since the guest ends its session on either. Execution N's key is (B + N, E): E is the attachment's epoch and B the broker instance's ID, so no command repeats another's key, even after a broker restored from a snapshot starts its epochs over. The launch gains --workload-identity, the identity as which every execution of the sandbox runs. The client now also refuses the messages about executions that only the host sends. The VM scenarios run executions in the root over one session: output beyond a MiB, executions that meet only while all of them run, a descendant in a session of its own, the identities that starts name, root with one allowed capability, an exact environment, a working directory, the configured hostname, and the refusals of stage 0. Others run executions as the sandbox's workload identity, and fail the identity stage for one without an account. The in-memory tests of the host cover the driver's output, outcomes, refusals, and timers, and the in-process tests of the guest now drive the library's HostExecution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
…n at once A peer without grace or settlement time gives up on an execution in the step that ends it, for its timeout or its caller's cancellation. HostExecution::serve took the peer's events before its messages, and the last event, once taken, discarded the cancellation that waited to be sent, so the guest's program ran on after the host's side finished. serve now takes the messages first, as the example of nvx::exec does. The driver's sends wait for the transport, as the client's do, until the run's deadline, and no timer fires meanwhile. Its documentation now says so, and that a deadline that passes in a receive leaves the client usable, while one that passes in a send ends it, which the tests now check, the latter with a guest that stops reading. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
The session's broker passes the guest's credit for the host's records on only behind the guest's own records, which wait while the host does not read, and takes no more of the host's records than that credit allows. So a single-threaded host that sends without reading can wait for credit that only its reading would free, even with a guest that reads. The exec command sent every start before it read anything, and a batch of hundreds, which the guest refuses beyond the executions that it runs at once, could fill the way back with refusals and hold the next start until its deadline. The driver now runs at most exec::MAX_EXECUTIONS executions that have not finished, as many as the guest runs at once. Their answers to their starts wait for the run, since no output flows before the run's credit does, and exec refuses more executions as a usage error. The driver also sends no program's input, which one thread could not send while it reads the programs' output: it refuses a start that says that the host sends input, and Executions::start no longer takes any. The worker's limit of executions now comes from that shared constant. The test of a send that waits for a guest that stops reading counts the run's deadline from the start's send, so that the deadline passes in the send that it checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
Small sends did not yet bound what the driver sends before the guest's credit for the host's records can reach the broker. An output window of many frames of few bytes lets the guest queue thousands of output messages ahead of its credit, while the driver sends a credit for each one that it reads. A start after a run may also wait for credit behind the output of executions that the run gave up on, or left running when its deadline passed. The driver now takes only output windows that count frames, at most host::MAX_OUTPUT_FRAMES of them, so that the credits for the guest's messages ahead of its own credit stay well within the guest's receive window. It also runs one batch: it starts every execution before it runs them and none after, so a later batch takes an attachment of its own, as each command of the host program does. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
The host's side of an execution took all of a stream's output into memory, so a program that writes without end grew the host program's memory without bound until its deadline. HostExecution now keeps at most exec::MAX_CAPTURE, 16 MiB, of each stream. Once more of a stream arrives, it cancels the execution, and its peer discards the rest when the grace ends, so the outcome marks the stream truncated, with the cause cancelled. The VM scenario whose program leaves a descendant in a session of its own printed `started` as soon as it forked setsid, so the program could end before the descendant called setsid(2) or held its output. The program now waits until the descendant has printed its process ID and moved its output to the program's, and checks in /proc that it leads its session, before it prints `started`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
…s take The executions take nvx::guest::entry's default hostname, which no configuration changes, rather than a hostname that the kernel command line configures. The VM scenario that checks it now describes it so. Its check, nvx-sandbox, is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
The host program gives the executions of `exec` timers and limits whose values are chosen for tests, as a valid configuration within the limits that the guest and the host allow. A comment now says so, and names those limits: `exec::MAX_CHUNK`, the most bytes of a chunk; `worker::MAX_EXECUTION_MEMORY`, the most bytes that an execution holds in the guest; and `host::MAX_OUTPUT_FRAMES`, the most frames of an output window. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
fc0bc9f to
b4d666a
Compare


Stacked on #868 (PR 2 of #805), which is stacked on #862 (PR 1 of #805), on #878 (PR 8 of #822), whose other PRs have merged into
dev: #863 → #867 → #869 → #865 → #864 → #872 → #875. This PR retargets todevonce #878, #862 and #868 merge.Part of #805: PR 3 of 3, the host's
execcommand, and executions in VMs.What changed
The guest's worker runs executions in the assembled root, under their identities (#868), but the host program could not start one, so no test ran an execution in a VM, and nothing entered a real root.
exec::HostExecutionis the host's side of one execution, in every configuration: the engine'sPeer, with a sink that takes the program's output as it arrives, unless its caller stalls it, which withholds the guest's credit, and a source of the program's input.deliverhands it the guest's messages,servereturns the peer's, anddeadlineis the peer's next timer.servetakes the peer's messages before its events, so it still returns the cancellation of an execution that the peer gives up on in the step that ends it, as a peer without grace or settlement time does. The sink keeps at mostexec::MAX_CAPTURE, 16 MiB, of each stream: once more of a stream arrives, it cancels the execution, and the peer discards the rest when the grace ends, so the outcome marks the stream truncated, with the causecancelled. The guest's in-process tests (tests/guest_session.rs) now drive it in place of their own copy.host::Executions(withhost::ExecutionsError) is the host program's single-threaded driver of executions over one attachment of the client:startsends a start once it decodes as the guest decodes it. These starts fail before anything is sent and leave the client usable:host::MAX_OUTPUT_FRAMES(32) frames;exec::MAX_EXECUTIONS(8) executions;runserves every execution, sends their messages, and receives until each finished. Each receive waits at most until the earliest timer of the executions, so timeouts fire whatever the guest sends. These end the run:exec::MAX_EXECUTIONSis the guest's limit of executions at once, which the worker'sworker::MAX_EXECUTIONSnow takes its value from.Client::receivenow also refuses the messages about executions that only the host sends:ExecStart,ExecInput,ExecInputEnd, andExecCancel.runtime-adapter-host exec STORE [--guest-build B] EXECUTION [';' EXECUTION]..., withEXECUTION = [--identity UID:GID [--groups GID,...] [--capabilities N,...] [--numeric]] [--cwd DIR] [--env NAME=VALUE]... -- PROGRAM [ARG]..., reattaches and runs up to 8 executions at once over one session, in the sandbox thatlaunchstarted: under runtime adapter: run cold sandboxes end to end on KVM, MSHV and WHP #822's order,launchexits with 0 only once the guest started the sandbox, after the mapping table. It printsexec.N.launch,status,cause,truncated,settled,stdout, andstderronce all finished.--identity, a start names no identity, so the sandbox's workload identity applies.\xHH.(B + N, E): E is the attachment's epoch, and B the first 8 bytes of the broker instance's ID, as a little-endian number. The guest ends its session on a second start of a key, even in a later epoch, and keeps its history of generations across resets. Epochs only advance within a broker instance, while a broker restored from a snapshot has a new instance ID whose epochs start over, so B keeps different commands' keys apart.launch ... --workload-identity UID:GID, for a layered root only, beside runtime adapter: run cold sandboxes end to end on KVM, MSHV and WHP #822's--share,--deny,--reservedand--share-root, each ID from 1 to 4294967294, names the sandbox's workload identity in the root configuration. A workload identity whose account the image lacks fails the root'sidentitystage, and the launch exits with 4, as for any stage that fails.execcommand, its keys and outputs,--workload-identity, the driver and its limits,HostExecution, and the new tests.Tests
src/exec/tests.rs: with zero grace and settlement, the host's side still sends the cancellation of an execution that it gives up on at once, for a timeout and for its caller's cancellation. It keeps exactly 16 MiB of a stream that a program writes without end, then cancels the execution, which ends with that stream truncated and the causecancelled.tests/host_session.rs, in memory, on Linux and Windows. A fake guest runs NVX's execution engine's runner for each start, as the worker does, for:The client also refuses the host-only messages about executions.
tests/layered_launch.rs: theexecand--workload-identitycommand lines that the host program refuses, with status 2, nine executions included.tests/vm_layered.rs, three new VM scenarios after runtime adapter: run cold sandboxes end to end on KVM, MSHV and WHP #822's 32, which CI runs on KVM, MSHV, and WHP:executions_run_in_the_root, without a workload identity. Over one session: four executions asnobodythat meet only while all of them run, beside one as root that writes 1.29 MB to its standard output and some to its standard error, byte-exact. Over a later session, with new keys:/procshows before the program prints, and holds the program's output, which the program's end, as PID 1 of its namespace, ends, so that no output is lost;nobody's IDs and groups, with empty permitted, effective, and bounding sets;CAP_CHOWNalone in each of those sets;nvx-sandbox, as PID 1;EINVAL) and of one withCAP_SYS_ADMIN(EPERM), fenced and without output.executions_run_as_the_workload_identity: executions without an identity run asnobody, without capabilities, with exactly the start's environment, and the guest refuses a start that names an identity (EINVAL).accountless_workload_identity_fails_its_stage: the identity stage fails once the guest assembled the root, and the VM ends with 16, with the scratch unmounted.Validation
This branch is rebased onto #868's current head (ca51780), which, with #862 (a8cc6e2), is on #878's current head (47f8bcd), the maintainer's re-commit of #878 on
devafter #875, #907, #908 and #911 merged; the OpenVMM gitlink stays 2b681564. The rebase had no conflicts, andgit range-diffshows the six commits' patches unchanged (4d7bb87 → bd2b901, d864b22 → 962245a, afa35d5 → 2e7814d, bc8f6f4 → 738924f, 25b905d → d37dc79, e4e5256 → ba8a5f6). Since the previous rebase, onto #875's rework, the first and fourth commits use its names and layout:sharing, asharing::HostFsSharingwith accessors, where this PR hadshares: the launch's options keepworkload_identitybesidesharing, and the options that need--distrocheck--workload-identitybeside--share, throughsharing.shares();src/host.rsdeclares theexecutionsmodule beside tests: run the host's order of a layered root in the adapter's host program #875'smap_replyandorder_error, and re-exportsExecutions,ExecutionsErrorand, from the fourth commit,MAX_OUTPUT_FRAMESbeside tests: run the host's order of a layered root in the adapter's host program #875'sMapReplyandOrderError.The command lines of
exec, its limits and its printed keys are unchanged. At ba8a5f6:check-nvx-rsin its three configurations: rustfmt, Clippy with warnings denied, the build, the tests, andcargo +1.89 check(the MSRV);scripts/check_markdown_links.py;guest_sessiontests;guest_sessionas root and 10 unprivileged, and 3 runs of the library's tests, all passing, with no cgroup left.host_session, all passing.v0.1.0-dev.c2ace076c60aartifacts (OpenVMM 2b681564):--cpu-profile hostbecause no built-in profile serves its CPU, and under a short temporary directory, which keeps the VMs' control sockets' paths withinSUN_LEN: all of them pass too.stage, thenlaunchwith an upper in RAM, which printedsandbox=started, thenexecof three executions over one session: one asnobody, which printed its IDs, the default hostnamenvx-sandboxand a line on its standard error; one as root withCAP_CHOWNalone in its permitted and effective sets, in/tmp, which printed a byte that is not UTF-8 as\xff; and one without an identity, which the guest refused at stage 0 withEINVAL(exec.3.launch=failed 0 22), fenced. The first two settled reusable. Thenstop, graceful, with a complete teardown.execare values chosen for tests that make a valid configuration, within the limits that the guest and the host allow, which it names. It changes no code; rustfmt, and Clippy with warnings denied in the three configurations, pass at it.setsid(2).cause=cancelledandtruncated=stdout, and one that wrote exactly 16 MiB exited 0, untruncated.