From d3f6885dfa68bb665be838d64468a5245ef2d4f7 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Mon, 9 Nov 2026 09:30:25 -0800 Subject: [PATCH 1/5] runtime-adapter: take nokaslr for a token outside the platform allowlist The runtime adapter's template tests took `quiet` for a kernel parameter that the platform tier's allowlist refuses. The next pin of OpenVMM admits `quiet` on a platform snapshot's command line, with the other kernel tuning parameters of #995, and so does the guest agent, which checks a template's command line with the excerpt of OpenVMM's allowlist. the_platform_tier_s_allowlist_is_openvmm_s, the_platform_tier_s_allowlist_takes_tokens_as_openvmm_splits_them, and template_mode_refuses_a_token_outside_the_allowlist take `nokaslr` instead, which the allowlist refuses before and after that pin, and check the same refusals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/runtime-adapter/src/template/tests.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/runtime-adapter/src/template/tests.rs b/tests/runtime-adapter/src/template/tests.rs index 58e331281..a24ffcb6a 100644 --- a/tests/runtime-adapter/src/template/tests.rs +++ b/tests/runtime-adapter/src/template/tests.rs @@ -105,7 +105,7 @@ fn the_platform_tier_s_allowlist_is_openvmm_s() { "driver_async_probe=virtio_mmio", "init=/bin/sh", "rdinit=/bin/sh", - "quiet", + "nokaslr", "--", "--=1", ] { @@ -128,16 +128,16 @@ fn the_platform_tier_s_allowlist_takes_tokens_as_openvmm_splits_them() { // but the kernel splits each in two at the vertical tab or at the no-break space's 0xa0, // and its second parameter is not on the allowlist, so a template's VM refuses it. for token in [ - "virtnet_dns=10.0.2.3\u{0b}quiet", - "virtnet_dns=10.0.2.3\u{a0}quiet", + "virtnet_dns=10.0.2.3\u{0b}nokaslr", + "virtnet_dns=10.0.2.3\u{a0}nokaslr", ] { assert!(is_platform_token(token.as_bytes()), "{token:?}"); let names: Vec<&[u8]> = cmdline::parameters(token.as_bytes()) .map(|parameter| parameter.name) .collect(); - assert_eq!(names, [&b"virtnet_dns"[..], b"quiet"], "{token:?}"); + assert_eq!(names, [&b"virtnet_dns"[..], b"nokaslr"], "{token:?}"); } - assert!(!is_platform_token(b"quiet")); + assert!(!is_platform_token(b"nokaslr")); } #[test] @@ -242,7 +242,7 @@ fn template_mode_refuses_a_gateway_that_openvmm_would_not_render() { #[test] fn template_mode_refuses_a_token_outside_the_allowlist() { for token in [ - "quiet", + "nokaslr", "init=/bin/sh", "console=ttyS0", "nvx_config=0xd0010000,65537", @@ -295,11 +295,11 @@ fn template_mode_refuses_a_token_outside_the_allowlist() { // no-break space's 0xa0, its first byte, 0xc2, stays in the platform token, which is then // not text, and so not on the allowlist, which takes tokens as OpenVMM's text. for (separator, refused) in [ - ("\u{0b}", &b"quiet"[..]), + ("\u{0b}", &b"nokaslr"[..]), ("\u{a0}", b"virtnet_dns=10.0.2.3\xc2"), ] { let command_line = format!( - "{} virtnet_dns=10.0.2.3{separator}quiet\n", + "{} virtnet_dns=10.0.2.3{separator}nokaslr\n", TEMPLATE.trim_end() ); assert_eq!( @@ -309,10 +309,10 @@ fn template_mode_refuses_a_token_outside_the_allowlist() { ); } let mut latin1 = format!("{} virtnet_dns=10.0.2.3", TEMPLATE.trim_end()).into_bytes(); - latin1.extend_from_slice(b"\xa0quiet\n"); + latin1.extend_from_slice(b"\xa0nokaslr\n"); assert_eq!( boot(&latin1), - Err(CommandLineError::Token(b"quiet".to_vec())) + Err(CommandLineError::Token(b"nokaslr".to_vec())) ); assert_eq!( CommandLineError::Token(b"\xff=\x01".to_vec()).to_string(), From 5e8384841a2afda58e5b6fdd13d6a05bd532991c Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Mon, 9 Nov 2026 11:46:14 -0800 Subject: [PATCH 2/5] openvmm: pin kernel tuning on platform command lines Pin microsoft/openvmm#5001's head, b4028c1aa775d8aafba1a170857acc032c7c86a1, "openvmm_helpers: admit kernel tuning on platform command lines", one commit on feature/microvm/dev f41a1ecfc. Its platform snapshots admit a closed set of guest kernel tuning parameters on their kernel command lines, at publication and at every restore, each parameter at most once: `mitigations=off`, `mitigations=auto`, and `mitigations=auto,nosmt`; `no_timer_check`; and `quiet`, and `loglevel=0` to `loglevel=7` (#995). It refuses every other token, as before. The excerpt of OpenVMM's platform allowlist that nvx-rs's template tests and the runtime adapter compile, nvx-rs/tests/vectors/openvmm/platform_command_line_token_is_invariant.rs, takes the function's new lines unchanged, with the constant of the tuning parameters before it, and the vectors' README records the commit. The guest agent of a runtime adapter's template, which checks its kernel command line with the excerpt, so admits the tuning parameters too. nvx-rs's openvmms_platform_allowlist_admits_its_tokens_and_refuses_tenant_data took `quiet` for a token outside the allowlist, which the new excerpt admits. It now admits the tuning parameters, and refuses other values and spellings of them, such as `mitigations=on`, `loglevel=8`, and `no-timer-check`, and other parameters, such as `nokaslr`. `cargo test --test openvmm_vectors -- --ignored` and `--test openvmm_cpu_profiles -- --ignored` pass at this pin. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/src/host/template/plan/tests.rs | 17 ++++++++++++++- nvx-rs/tests/vectors/README.md | 5 +++-- ...latform_command_line_token_is_invariant.rs | 21 +++++++++++++++++++ openvmm | 2 +- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/nvx-rs/src/host/template/plan/tests.rs b/nvx-rs/src/host/template/plan/tests.rs index 8a9db9d56..e1e94695f 100644 --- a/nvx-rs/src/host/template/plan/tests.rs +++ b/nvx-rs/src/host/template/plan/tests.rs @@ -539,6 +539,14 @@ fn openvmms_platform_allowlist_admits_its_tokens_and_refuses_tenant_data() { "virtnet_dns=10.0.0.1", "virtnet_ip6=fd00::2/64", "virtnet_gw6=fd00::1", + // The kernel tuning parameters, which are fixed at boot and the same for every clone. + "mitigations=off", + "mitigations=auto", + "mitigations=auto,nosmt", + "no_timer_check", + "quiet", + "loglevel=0", + "loglevel=7", ] { assert!(openvmm::admits(token), "{token}"); } @@ -569,7 +577,14 @@ fn openvmms_platform_allowlist_admits_its_tokens_and_refuses_tenant_data() { "nvx_sandbox=0", "nvx_snapshot_tier=workload-start", "console=ttyS0", - "quiet", + // Other values of the kernel tuning parameters, and other spellings of them. + "mitigations=on", + "mitigations=", + "loglevel=8", + "loglevel=07", + "quiet=1", + "no-timer-check", + "nokaslr", "", ] { assert!(!openvmm::admits(token), "{token}"); diff --git a/nvx-rs/tests/vectors/README.md b/nvx-rs/tests/vectors/README.md index 98d1ca8c2..f24c66442 100644 --- a/nvx-rs/tests/vectors/README.md +++ b/nvx-rs/tests/vectors/README.md @@ -81,7 +81,7 @@ the key that the vector's bytes decode to. `openvmm/` holds `control_session_protocol_vectors.txt` and `control_session_protocol_cases.txt` of OpenVMM's `vm/devices/virtio/virtio_console/test_data/`, unchanged, from OpenVMM commit -[`e68871cc6f14fceb6d38a899504ae8105f89e310`](https://github.com/microsoft/openvmm/tree/e68871cc6f14fceb6d38a899504ae8105f89e310/vm/devices/virtio/virtio_console/test_data). +[`b4028c1aa775d8aafba1a170857acc032c7c86a1`](https://github.com/microsoft/openvmm/tree/b4028c1aa775d8aafba1a170857acc032c7c86a1/vm/devices/virtio/virtio_console/test_data). They keep OpenVMM's own format, which its `control_session_protocol` tests read: one record of each type, as `NAME|BYTES`, and 37 cases of the boundaries and refusals of the records' payloads and headers. The tests of @@ -91,7 +91,8 @@ and that each case is accepted or refused as it says. `openvmm/` also holds two excerpts of OpenVMM's sources at that commit, whole lines, unchanged: `platform_command_line_token_is_invariant.rs`, the function of `openvmm/openvmm_helpers/src/snapshot/microvm.rs` that admits the kernel -command-line tokens of a capture at OpenVMM's platform tier, and +command-line tokens of a capture at OpenVMM's platform tier, after the +constant of the kernel tuning parameters that it admits among them, and `microvm_control_tty_command_line.rs`, the constant of `openvmm/openvmm_defs/src/microvm.rs` that names the control tty's token, which the function reads. The tests of `nvx::host::template` compile them, and check diff --git a/nvx-rs/tests/vectors/openvmm/platform_command_line_token_is_invariant.rs b/nvx-rs/tests/vectors/openvmm/platform_command_line_token_is_invariant.rs index 4486d4276..292fd7284 100644 --- a/nvx-rs/tests/vectors/openvmm/platform_command_line_token_is_invariant.rs +++ b/nvx-rs/tests/vectors/openvmm/platform_command_line_token_is_invariant.rs @@ -1,3 +1,23 @@ +/// The guest kernel tuning parameters that a platform snapshot's command line +/// may carry, each parameter at most once. Like the other tokens that the +/// command line may carry, each is fixed when the guest kernel boots, is the +/// same for every clone, and carries no tenant data. +const PLATFORM_TUNING_TOKENS: [&str; 13] = [ + "mitigations=off", + "mitigations=auto", + "mitigations=auto,nosmt", + "no_timer_check", + "quiet", + "loglevel=0", + "loglevel=1", + "loglevel=2", + "loglevel=3", + "loglevel=4", + "loglevel=5", + "loglevel=6", + "loglevel=7", +]; + fn platform_command_line_token_is_invariant(token: &str) -> bool { matches!( token, @@ -12,6 +32,7 @@ fn platform_command_line_token_is_invariant(token: &str) -> bool { ) || token.starts_with("nr_cpus=") || token.starts_with("maxcpus=") || token == openvmm_defs::microvm::MICROVM_CONTROL_TTY_COMMAND_LINE + || PLATFORM_TUNING_TOKENS.contains(&token) || [ "virtio_mmio.device=", "virtnet_ip=", diff --git a/openvmm b/openvmm index f41a1ecfc..b4028c1aa 160000 --- a/openvmm +++ b/openvmm @@ -1 +1 @@ -Subproject commit f41a1ecfc13379e501cf78e2a96585426f16c87e +Subproject commit b4028c1aa775d8aafba1a170857acc032c7c86a1 From 7a937ffe378f7fda9b513d4d9d503069e50dab17 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Mon, 9 Nov 2026 08:42:15 -0800 Subject: [PATCH 3/5] nvx-rs: boot templates with their class's kernel tuning parameters A template's kernel booted with NVX's own command line alone, `nvx_sandbox=1` and, for a template whose processors are not all online at boot, `maxcpus=`. An adapter whose cold sandboxes boot with kernel tuning parameters, such as `mitigations=off`, could not give them to its templates, so its clones ran another kernel configuration than its cold sandboxes: Linux applies `mitigations=` once, as it boots, and a clone has no other way to get it (#995). KERNEL_TUNING_PARAMETERS names the parameters that OpenVMM now admits on a platform capture's command line: `mitigations=off`, `mitigations=auto`, and `mitigations=auto,nosmt`; `no_timer_check`; and `quiet`, and `loglevel=0` to `loglevel=7`. CaptureClass gains kernel_tuning, the parameters with which the template's kernel boots, in their order, which CaptureClass::check refuses unless each is one of them and each parameter appears at most once, so that a class that no capture takes is refused before any VM boots. TemplatePlan::with_kernel_tuning renders them after the plan's own tokens, and a Store's captures boot with their class's. TemplateKey::new also checks that the tuning parameters of the manifest's command line are the class's, in their order, as it checks the rest of the machine that the class requested. The key holds the command line already, so its encoding is unchanged, and templates of classes that differ in their tuning alone have keys of their own. NVX's fake OpenVMM accepts the parameters after a capture's own tokens, as OpenVMM does, each at most once. The class's canonical encoding holds the parameters last, as a list of texts, under version 2. A class's identity names the slot of its captures and its generations in a store's index, so a creation finds no generation that a class of version 1 captured, and captures one again; those generations still serve the clones that select them by their keys, which are unchanged. tests/vectors/capture-class-v2.txt holds the class vectors of version 2, derived from the bytes of version 1's vectors and the definition, and one with tuning parameters. template-key-v1.txt keeps the keys' vectors alone, since nothing encodes a class of version 1 any more. Tests: the plan renders the parameters after its own tokens, in their order, and replaces them; the class check and the plan refuse a parameter outside the set, another value or spelling of one, and a repeated parameter; classes that differ in their tuning alone, order included, have identities of their own, and encode the parameters last; TemplateKey::new refuses a capture whose command line holds other tuning than its class's; a store's capture passes its class's parameters in OpenVMM's --cmdline, and refuses a snapshot whose command line lacks them; nvx-rs's set is OpenVMM's, as the excerpt of its allowlist defines it; the class vectors encode; and the fake OpenVMM accepts a capture's tuning parameters, with or without a limit of the processors online at boot, refuses others and repeated ones, and holds nvx-rs's set. With the check, the encoding, the plan, the key's check, and the capture's tuning neutralized, the seven tests of that logic fail. Existing tests change where the new check or encoding reaches them: the key test's change of its command line adds `quiet` to its class too, the class vectors move to capture-class-v2.txt, and the helpers that build classes name no tuning. The runtime adapter's class names none either. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/README.md | 32 +- nvx-rs/public-api/linux.txt | 3 + nvx-rs/public-api/windows.txt | 3 + nvx-rs/src/host/template/key.rs | 145 +++++++++- nvx-rs/src/host/template/key/tests.rs | 273 +++++++++++++++--- nvx-rs/src/host/template/mod.rs | 10 +- nvx-rs/src/host/template/plan.rs | 78 +++-- nvx-rs/src/host/template/plan/tests.rs | 98 +++++++ nvx-rs/src/host/template/store/capture.rs | 10 +- .../src/host/template/store/capture/tests.rs | 64 ++++ nvx-rs/src/host/template/tests.rs | 1 + nvx-rs/src/testing/fake_openvmm.rs | 56 +++- nvx-rs/src/testing/fake_openvmm/tests.rs | 40 +++ nvx-rs/tests/template_capture.rs | 1 + nvx-rs/tests/vectors/README.md | 40 ++- nvx-rs/tests/vectors/capture-class-v2.txt | 24 ++ nvx-rs/tests/vectors/template-key-v1.txt | 36 +-- tests/runtime-adapter/src/capture.rs | 1 + 18 files changed, 788 insertions(+), 127 deletions(-) create mode 100644 nvx-rs/tests/vectors/capture-class-v2.txt diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 4cce113fd..e181a90ce 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -679,9 +679,11 @@ policy, whatever the network's own, which each clone's restore replaces with the clone's; `TemplateResources`, with the processors, 1, 2, 4, or 8, and those online at boot, as many or fewer, and RAM and a `--memory-capacity` in blocks of `MEMORY_BLOCK_SIZE`; the kernel command line `nvx_sandbox=1`, with `maxcpus=` -and the processors online at boot when they are fewer, so that the capture's -whole command line holds tokens of OpenVMM's platform allowlist alone, and no -tenant data, which the tests check with a copy of the allowlist's source; and +and the processors online at boot when they are fewer, then the kernel tuning +parameters of the template's class, which `TemplatePlan::with_kernel_tuning` +sets, in their order, so that the capture's whole command line holds tokens of +OpenVMM's platform allowlist alone, and no tenant data, which the tests check +with a copy of the allowlist's source; and the capture's options, `--snapshot-tier platform`, a `--snapshot-destination`, `SNAPSHOT_NAME` in the transaction directory, and `--snapshot-generation-id` with the capture's bound-block `StorageGeneration`, which OpenVMM does not @@ -701,11 +703,18 @@ those that make a manifest a template's. runtime, the guest's build, the backend, the requested CPU profile, the processors, those online at boot, and RAM, the geometries of the disks, the network, of which it takes what the template captures alone, its NIC at the -guest's address under the complete deny-all policy, and the adapter's other -`Invariants`. +guest's address under the complete deny-all policy, the adapter's other +`Invariants`, and the kernel tuning parameters with which the template's kernel +boots, as the adapter's cold sandboxes do, so that its clones run their kernel +configuration: those of `KERNEL_TUNING_PARAMETERS`, `mitigations=off`, +`mitigations=auto`, or `mitigations=auto,nosmt`, `no_timer_check`, `quiet`, +and `loglevel=0` to `loglevel=7`, each parameter at most once, which OpenVMM +admits on a platform capture's command line, and which `CaptureClass::check` +checks before any VM boots. `TemplateKey::new` checks that the capture made the machine that its class -requested, and derives the template's compatibility key, from metadata alone: +requested, the class's kernel tuning parameters on its kernel command line +among it, and derives the template's compatibility key, from metadata alone: the runtime and contract versions through which its clones are restored, the backend, CPU profile and effective CPUID, topology, rates, resources, kernel command line, disks, and devices of its machine, with the NIC's addresses and @@ -718,12 +727,17 @@ A key and a class each have one canonical, versioned encoding, whose domain-separated SHA-256 digest is its identity, a `KeyIdentity`, which converts to and from the `nvx::activation::Identity` by which a clone's activation record names its template's key, or a `ClassIdentity`; the -[conformance vectors](tests/vectors/README.md) of version 1, -`tests/vectors/template-key-v1.txt`, pin both encodings and identities. The +[conformance vectors](tests/vectors/README.md) of version 1 of a key's, +`tests/vectors/template-key-v1.txt`, and of version 2 of a class's, +`tests/vectors/capture-class-v2.txt`, pin both encodings and identities. The key's encoding, version 1, leaves the network policy out, since templates capture their NIC under the complete deny-all policy, and has not been released, so a template store that an earlier build created is not supported: delete it -and create a new one. There is no migration. +and create a new one. There is no migration. A class's encoding, version 2, +adds the kernel tuning parameters to version 1, so a creation finds no +generation that a class of version 1 captured, and captures one again, while +those generations still serve the clones that select them by their keys, which +version 2 leaves as they were. `Store` keeps the generations of templates that a host published, apart from every sandbox's store, at a location that the adapter chooses: a private diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index af619d838..8895dcb86 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -2681,6 +2681,7 @@ field nvx::host::template::CaptureClass::guest_build field nvx::host::template::CaptureClass::hypervisor method nvx::host::template::CaptureClass::identity field nvx::host::template::CaptureClass::invariants +field nvx::host::template::CaptureClass::kernel_tuning field nvx::host::template::CaptureClass::memory field nvx::host::template::CaptureClass::memory_capacity field nvx::host::template::CaptureClass::network @@ -2862,6 +2863,7 @@ method nvx::host::template::Invariants::iter method nvx::host::template::Invariants::len method nvx::host::template::Invariants::new method nvx::host::template::Invariants::with +const nvx::host::template::KERNEL_TUNING_PARAMETERS const nvx::host::template::KEY_NAME struct nvx::host::template::KeyIdentity method nvx::host::template::KeyIdentity::as_bytes @@ -3046,6 +3048,7 @@ method nvx::host::template::TemplatePlan::resources method nvx::host::template::TemplatePlan::scratch_length method nvx::host::template::TemplatePlan::scratch_path method nvx::host::template::TemplatePlan::transaction +method nvx::host::template::TemplatePlan::with_kernel_tuning struct nvx::host::template::TemplateResources field nvx::host::template::TemplateResources::boot_processors field nvx::host::template::TemplateResources::memory diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 42d7cc2e3..7b2d8a0e8 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1617,6 +1617,7 @@ field nvx::host::template::CaptureClass::guest_build field nvx::host::template::CaptureClass::hypervisor method nvx::host::template::CaptureClass::identity field nvx::host::template::CaptureClass::invariants +field nvx::host::template::CaptureClass::kernel_tuning field nvx::host::template::CaptureClass::memory field nvx::host::template::CaptureClass::memory_capacity field nvx::host::template::CaptureClass::network @@ -1798,6 +1799,7 @@ method nvx::host::template::Invariants::iter method nvx::host::template::Invariants::len method nvx::host::template::Invariants::new method nvx::host::template::Invariants::with +const nvx::host::template::KERNEL_TUNING_PARAMETERS const nvx::host::template::KEY_NAME struct nvx::host::template::KeyIdentity method nvx::host::template::KeyIdentity::as_bytes @@ -1982,6 +1984,7 @@ method nvx::host::template::TemplatePlan::resources method nvx::host::template::TemplatePlan::scratch_length method nvx::host::template::TemplatePlan::scratch_path method nvx::host::template::TemplatePlan::transaction +method nvx::host::template::TemplatePlan::with_kernel_tuning struct nvx::host::template::TemplateResources field nvx::host::template::TemplateResources::boot_processors field nvx::host::template::TemplateResources::memory diff --git a/nvx-rs/src/host/template/key.rs b/nvx-rs/src/host/template/key.rs index ec3e4f6f3..4f08e9d0c 100644 --- a/nvx-rs/src/host/template/key.rs +++ b/nvx-rs/src/host/template/key.rs @@ -362,6 +362,77 @@ fn cpu_profile_problem(profile: &str) -> Option { }) } +/// The kernel tuning parameters with which a template's kernel may boot: guest kernel parameters +/// that an adapter's cold boots may pass too, and that OpenVMM admits on the command line of a +/// capture of its platform tier, and again at every restore, since, like the other tokens of that +/// command line, each is fixed when the kernel boots, is the same for every clone, and carries no +/// tenant data: +/// +/// - `mitigations=off`, `mitigations=auto`, and `mitigations=auto,nosmt`, which choose the +/// kernel's mitigations of CPU vulnerabilities, which it applies once, as it boots, so that a +/// clone has no other way to get them; +/// - `no_timer_check`, which skips the kernel's check of the timer interrupt as it boots; +/// - `quiet`, and `loglevel=0` to `loglevel=7`, the console's log level. +/// +/// A template boots with each parameter at most once, so with one of the values of +/// `mitigations=` and of `loglevel=`. The kernel takes `-` and `_` alike in a parameter's name, +/// but OpenVMM admits these spellings alone, and none of the clock parameters that the time ABI +/// refuses. +pub const KERNEL_TUNING_PARAMETERS: [&str; 13] = [ + "mitigations=off", + "mitigations=auto", + "mitigations=auto,nosmt", + "no_timer_check", + "quiet", + "loglevel=0", + "loglevel=1", + "loglevel=2", + "loglevel=3", + "loglevel=4", + "loglevel=5", + "loglevel=6", + "loglevel=7", +]; + +/// Returns the name of the kernel parameter `token`: what precedes its first `=`, or all of it. +fn parameter_name(token: &str) -> &str { + token.split_once('=').map_or(token, |(name, _)| name) +} + +/// Returns whether `token` names a kernel tuning parameter, with any value: whether its name is +/// that of one of [`KERNEL_TUNING_PARAMETERS`]. +fn is_tuning_parameter(token: &str) -> bool { + let name = parameter_name(token); + KERNEL_TUNING_PARAMETERS + .iter() + .any(|parameter| parameter_name(parameter) == name) +} + +/// Explains why `parameters` are not the kernel tuning parameters of a template, if they are not: +/// each must be one of [`KERNEL_TUNING_PARAMETERS`], and each parameter must appear at most once. +pub(super) fn kernel_tuning_problem(parameters: &[String]) -> Option { + for (index, parameter) in parameters.iter().enumerate() { + if !KERNEL_TUNING_PARAMETERS.contains(¶meter.as_str()) { + return Some(format!( + "kernel tuning parameter {parameter:?} is not one with which a template may boot: \ + mitigations=off, mitigations=auto, mitigations=auto,nosmt, no_timer_check, \ + quiet, or loglevel=0 to loglevel=7" + )); + } + let name = parameter_name(parameter); + if parameters[..index] + .iter() + .any(|earlier| parameter_name(earlier) == name) + { + return Some(format!( + "kernel tuning parameter {name} appears more than once, but a template boots \ + with each at most once" + )); + } + } + None +} + /// Returns the guest's network on `nic`, its IPv4 address and prefix length, in the canonical form /// of a [`SandboxNetwork`]. fn guest_network_of(nic: &Nic) -> String { @@ -416,15 +487,34 @@ pub struct CaptureClass { pub network: Option, /// The invariant settings beyond the machine. pub invariants: Invariants, + /// The kernel tuning parameters with which the template's kernel boots, in their order, after + /// the template's own tokens, as [`TemplatePlan::kernel_command_line`] renders them: each one + /// of [`KERNEL_TUNING_PARAMETERS`], and each parameter at most once. An adapter passes those + /// of its cold boots, so that its clones run the kernel configuration of its cold sandboxes. + /// The template's key holds them in its kernel command line, so the templates of classes that + /// differ in them alone are templates of their own. + /// + /// [`TemplatePlan::kernel_command_line`]: super::TemplatePlan::kernel_command_line + pub kernel_tuning: Vec, } impl CaptureClass { /// Version of the canonical encoding of a class, its first two bytes. - pub const VERSION: u16 = 1; + /// + /// Version 2 adds the kernel tuning parameters to version 1. A class's identity names the + /// slot of its captures and the class of each generation in a store's index, so + /// [`Store::create_template`] finds no generation that a class of version 1 captured, and + /// captures one again. Those generations still serve the clones that select them by their + /// keys, which version 2 leaves as they were. + /// + /// [`Store::create_template`]: super::Store::create_template + pub const VERSION: u16 = 2; /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) unless every field /// has the form that its documentation gives, the runtime identity passes - /// [`RuntimeIdentity::check`], and the network lowers to OpenVMM's arguments. + /// [`RuntimeIdentity::check`], the network lowers to OpenVMM's arguments, and each kernel + /// tuning parameter is one of [`KERNEL_TUNING_PARAMETERS`], each parameter at most once, so + /// that a class that no capture takes is refused before any VM boots. pub fn check(&self) -> Result<()> { self.runtime.check()?; if let Some(problem) = cpu_profile_problem(&self.cpu_profile) { @@ -455,6 +545,9 @@ impl CaptureClass { } } self.network_arguments()?; + if let Some(problem) = kernel_tuning_problem(&self.kernel_tuning) { + return Err(Error::unsupported(problem)); + } Ok(()) } @@ -470,7 +563,7 @@ impl CaptureClass { /// Returns the canonical encoding of the class, which its [`identity`](Self::identity) /// digests. /// - /// Version 1 holds, in order: [`VERSION`](Self::VERSION) as a `u16`; the runtime identity's + /// Version 2 holds, in order: [`VERSION`](Self::VERSION) as a `u16`; the runtime identity's /// OpenVMM, kernel, and initramfs identities as texts, its boot contract version as a `u64`, /// its control contract revision as a text, and its control session version as a `u64`; the /// guest build as a text; the snapshot format, device-state schema, time ABI, restore packet, @@ -479,8 +572,10 @@ impl CaptureClass { /// capacity as `u64`s; the `distro` and scratch geometries, each a `u64` length and two /// `u32` block sizes; the network, as the list of the texts of OpenVMM's arguments of the /// template's NIC, which [`SandboxNetwork::template_arguments`] lowers it to, empty without a - /// NIC; and the invariant settings, as a list of names and values, each a text, in the order - /// of their names. The [module](super) describes the encoding of each kind of value. + /// NIC; the invariant settings, as a list of names and values, each a text, in the order of + /// their names; and the kernel tuning parameters, as a list of texts, in their order. Version + /// 1 held the same but the kernel tuning parameters. The [module](super) describes the + /// encoding of each kind of value. /// /// Fails as [`check`](Self::check) does. pub fn to_bytes(&self) -> Result> { @@ -507,6 +602,9 @@ impl CaptureClass { writer.text(argument) }); self.invariants.write(&mut writer); + writer.list(&self.kernel_tuning, |writer, parameter| { + writer.text(parameter) + }); Ok(writer.finish()) } @@ -533,7 +631,8 @@ impl CaptureClass { /// restoring host's TSC rate. The time ABI version is the time contract's. /// - **Resources:** the processor capacity and the processors online at boot; the RAM at boot, /// its capacity, the size of the blocks in which a restore expands it, and the ranges of both. -/// - **Boot:** the exact effective kernel command line, and its digest. +/// - **Boot:** the exact effective kernel command line, which holds the kernel tuning parameters +/// of the template's class, and its digest. /// - **Disks:** the length and block sizes of the `distro`, which is the capacity of every clone's /// image, and of the scratch. /// - **Devices and isolation:** each device's ID, kind, state unit, transport, address ranges, @@ -592,11 +691,13 @@ impl TemplateKey { /// /// The manifest's machine must be the class's: the backend, the processors and those online /// at boot, the RAM and its capacity, the `distro` and scratch geometries, the presence of a - /// NIC, and the NIC's address and prefix, and a CPU profile that the class's request selects. - /// A manifest whose machine has no processors online at boot activates all of them at boot, - /// which a class whose processors are all online at boot requests. The key takes the class's - /// runtime, guest build, and invariant settings, and this crate's versions of the other - /// contracts. + /// NIC, and the NIC's address and prefix, a CPU profile that the class's request selects, and + /// the kernel tuning parameters of its kernel command line, the tokens that name one of + /// [`KERNEL_TUNING_PARAMETERS`], whatever their values, which must be the class's, in their + /// order. A manifest whose machine has no processors online at boot activates all of them at + /// boot, which a class whose processors are all online at boot requests. The key takes the + /// class's runtime, guest build, and invariant settings, and this crate's versions of the + /// other contracts. /// /// The key also takes the identity of the network policy that OpenVMM enforced on the NIC, /// which this check leaves to OpenVMM: OpenVMM derives it from the arguments of the @@ -688,6 +789,28 @@ impl TemplateKey { describe(requested_network), ); } + let found_tuning: Vec<&str> = machine + .command_line + .split_ascii_whitespace() + .filter(|token| is_tuning_parameter(token)) + .collect(); + let requested_tuning: Vec<&str> = class.kernel_tuning.iter().map(String::as_str).collect(); + if found_tuning != requested_tuning { + let describe = |parameters: &[&str]| { + if parameters.is_empty() { + "none".to_owned() + } else { + parameters.join(" ") + } + }; + return mismatch( + format!( + "its kernel tuning parameters are {}", + describe(&found_tuning) + ), + describe(&requested_tuning), + ); + } Ok(Self { runtime: class.runtime.clone(), guest_build: class.guest_build.clone(), diff --git a/nvx-rs/src/host/template/key/tests.rs b/nvx-rs/src/host/template/key/tests.rs index 2894fa09b..862fdfeb2 100644 --- a/nvx-rs/src/host/template/key/tests.rs +++ b/nvx-rs/src/host/template/key/tests.rs @@ -119,8 +119,9 @@ fn each_part_of_the_key_changes_it() { spec.contract().expansion_ranges = vec![(512 * MIB, 512 * MIB), (4096 * MIB, 1024 * MIB)]; }), - // Boot. - ("command line", |_, spec| { + // Boot: the command line, whose kernel tuning parameters are the class's. + ("command line", |class, spec| { + class.kernel_tuning.push("quiet".to_owned()); spec.contract().command_line.push_str(" quiet"); }), // Disks. @@ -907,8 +908,193 @@ fn identities_have_a_text_form() { ); } -/// The conformance vectors of version 1 of the canonical encodings of keys and classes. -const VECTORS: &str = include_str!("../../../../tests/vectors/template-key-v1.txt"); +/// Returns the manifest of the template of the tests whose kernel booted with the kernel tuning +/// parameters `tuning`, which NVX gives it after its own tokens. +fn tuned_spec(tuning: &str) -> ManifestSpec { + let mut spec = ManifestSpec::template(); + let contract = spec.contract(); + contract.command_line = + contract + .command_line + .replacen("nvx_sandbox=1", &format!("nvx_sandbox=1 {tuning}"), 1); + spec +} + +/// Returns the class of the template of the tests with the kernel tuning parameters `tuning`. +fn tuned_class(tuning: &[&str]) -> CaptureClass { + CaptureClass { + kernel_tuning: tuning + .iter() + .map(|parameter| (*parameter).to_owned()) + .collect(), + ..support::class() + } +} + +#[test] +fn classes_refuse_kernel_tuning_parameters_that_no_capture_takes() { + for (tuning, named) in [ + // Other values, and other parameters. + (&["mitigations=on"][..], "mitigations=on"), + (&["mitigations="][..], "mitigations="), + ( + &["mitigations=auto,nosmt,off"][..], + "mitigations=auto,nosmt,off", + ), + (&["loglevel=8"][..], "loglevel=8"), + (&["loglevel=07"][..], "loglevel=07"), + (&["quiet=1"][..], "quiet=1"), + (&["no-timer-check"][..], "no-timer-check"), + (&["nokaslr"][..], "nokaslr"), + (&["tsc_early_khz=2100000"][..], "tsc_early_khz=2100000"), + (&["nvx_hostname=tenant"][..], "nvx_hostname=tenant"), + (&["quiet loglevel=0"][..], "quiet loglevel=0"), + (&[""][..], "\"\""), + // A parameter more than once, with one value or with two. + (&["quiet", "quiet"][..], "quiet"), + (&["mitigations=off", "mitigations=auto"][..], "mitigations"), + (&["loglevel=0", "quiet", "loglevel=7"][..], "loglevel"), + ] { + let class = tuned_class(tuning); + for error in [ + class.check().unwrap_err(), + class.to_bytes().unwrap_err(), + class.identity().unwrap_err(), + TemplateKey::new(&class, &manifest(&ManifestSpec::template())).unwrap_err(), + ] { + assert_eq!(error.code(), ErrorCode::Unsupported, "{tuning:?}: {error}"); + assert!( + error.message().contains("kernel tuning parameter") + && error.message().contains(named), + "{tuning:?}: {:?}", + error.message() + ); + } + } + // Each parameter of the set alone, and one of each. + for parameter in KERNEL_TUNING_PARAMETERS { + tuned_class(&[parameter]).check().unwrap(); + } + tuned_class(&[ + "mitigations=auto,nosmt", + "no_timer_check", + "quiet", + "loglevel=0", + ]) + .check() + .unwrap(); +} + +#[test] +fn classes_that_differ_in_their_kernel_tuning_alone_are_classes_of_their_own() { + let mut cases: Vec> = vec![Vec::new()]; + cases.extend( + KERNEL_TUNING_PARAMETERS + .iter() + .map(|parameter| vec![*parameter]), + ); + cases.extend([ + vec!["quiet", "loglevel=0"], + // The kernel applies these two in their order, so the order is the class's too. + vec!["loglevel=0", "quiet"], + vec!["mitigations=off", "no_timer_check", "quiet", "loglevel=0"], + ]); + let mut identities = HashSet::new(); + for tuning in &cases { + let class = tuned_class(tuning); + assert!( + identities.insert(class.identity().unwrap()), + "{tuning:?} leaves the class's identity as another's" + ); + // Version 2 of the encoding holds the parameters last, as a list of texts. + let bytes = class.to_bytes().unwrap(); + assert_eq!(&bytes[..2], &[2, 0]); + let mut list = u32::try_from(tuning.len()).unwrap().to_le_bytes().to_vec(); + for parameter in tuning { + list.extend(u32::try_from(parameter.len()).unwrap().to_le_bytes()); + list.extend(parameter.as_bytes()); + } + assert!(bytes.ends_with(&list), "{tuning:?}"); + let untuned = tuned_class(&[]).to_bytes().unwrap(); + assert_eq!( + bytes[..bytes.len() - list.len()], + untuned[..untuned.len() - 4], + "{tuning:?}" + ); + } + assert_eq!(CaptureClass::VERSION, 2); +} + +#[test] +fn a_key_holds_its_class_s_kernel_tuning_in_its_command_line() { + let untuned = key(&support::class(), &ManifestSpec::template()); + let tuning = ["mitigations=off", "no_timer_check", "quiet", "loglevel=0"]; + let tuned = key(&tuned_class(&tuning), &tuned_spec(&tuning.join(" "))); + assert!( + tuned + .command_line() + .contains("nvx_sandbox=1 mitigations=off no_timer_check quiet loglevel=0 "), + "{}", + tuned.command_line() + ); + assert_ne!(tuned.identity(), untuned.identity()); + // The command line of a template of other kernel tuning makes a key of its own. + let other = key( + &tuned_class(&["mitigations=auto"]), + &tuned_spec("mitigations=auto"), + ); + assert_ne!(other.identity(), tuned.identity()); + assert_ne!(other.identity(), untuned.identity()); + // A capture whose kernel booted with other tuning than its class's did not make the template + // that its class requested. + for (class, spec, found, requested) in [ + ( + tuned_class(&tuning), + ManifestSpec::template(), + "none", + "mitigations=off no_timer_check quiet loglevel=0", + ), + ( + support::class(), + tuned_spec("mitigations=off"), + "mitigations=off", + "none", + ), + ( + tuned_class(&["quiet", "loglevel=0"]), + tuned_spec("loglevel=0 quiet"), + "loglevel=0 quiet", + "quiet loglevel=0", + ), + ( + tuned_class(&["mitigations=off"]), + tuned_spec("mitigations=auto"), + "mitigations=auto", + "mitigations=off", + ), + // A value outside the set, which OpenVMM would refuse, names the parameter all the same. + ( + tuned_class(&["mitigations=off"]), + tuned_spec("mitigations=on"), + "mitigations=on", + "mitigations=off", + ), + ] { + assert_mismatched( + &class, + &spec, + &format!( + "its kernel tuning parameters are {found}, but the class requests {requested}" + ), + ); + } +} + +/// The conformance vectors of version 1 of the canonical encoding of keys. +const KEY_VECTORS: &str = include_str!("../../../../tests/vectors/template-key-v1.txt"); + +/// The conformance vectors of version 2 of the canonical encoding of classes. +const CLASS_VECTORS: &str = include_str!("../../../../tests/vectors/capture-class-v2.txt"); /// The versions of the contracts that the vectors hold. const VECTOR_CONTRACTS: Contracts = Contracts { @@ -919,10 +1105,9 @@ const VECTOR_CONTRACTS: Contracts = Contracts { activation: 1, }; -/// Returns each vector of `kind`: its name, its bytes, and its values. -fn vectors(kind: &str) -> Vec<(&'static str, Vec, Vec<&'static str>)> { - VECTORS - .lines() +/// Returns each vector of `kind` in the vectors `file`: its name, its bytes, and its values. +fn vectors(file: &'static str, kind: &str) -> Vec<(&'static str, Vec, Vec<&'static str>)> { + file.lines() .filter(|line| !line.is_empty() && !line.starts_with('#')) .filter_map(|line| { let mut fields = line.split('|'); @@ -942,18 +1127,25 @@ fn vectors(kind: &str) -> Vec<(&'static str, Vec, Vec<&'static str>)> { /// Returns the class of the capture that the vectors call `name`, with the runtime that they /// hold. fn vector_class(name: &str) -> CaptureClass { - let mut class = if name == "NETWORK" { - let class = network_class(); - CaptureClass { - invariants: Invariants::new() - .with("isolation.seccomp", "strict") - .unwrap() - .with("limits.pids", "512") - .unwrap(), - ..class + let mut class = match name { + "NETWORK" => { + let class = network_class(); + CaptureClass { + invariants: Invariants::new() + .with("isolation.seccomp", "strict") + .unwrap() + .with("limits.pids", "512") + .unwrap(), + ..class + } } - } else { - support::class() + "TUNING" => tuned_class(&[ + "mitigations=auto,nosmt", + "no_timer_check", + "quiet", + "loglevel=0", + ]), + _ => support::class(), }; class.runtime = RuntimeIdentity { openvmm: artifact('a'), @@ -1020,7 +1212,7 @@ fn vector_spec(name: &str) -> ManifestSpec { #[test] fn the_conformance_vectors_decode_and_encode() { - let keys = vectors("KEY"); + let keys = vectors(KEY_VECTORS, "KEY"); let names: Vec<&str> = keys.iter().map(|(name, ..)| *name).collect(); assert_eq!(names, ["TEMPLATE", "NETWORK", "HOST_PROFILE", "MINIMAL"]); for (name, bytes, values) in keys { @@ -1039,19 +1231,7 @@ fn the_conformance_vectors_decode_and_encode() { assert_eq!(derived, key, "{name}"); } } - let classes = vectors("CLASS"); - let names: Vec<&str> = classes.iter().map(|(name, ..)| *name).collect(); - assert_eq!(names, ["TEMPLATE", "NETWORK"]); - for (name, bytes, values) in classes { - let class = vector_class(name); - assert_eq!(class.encode(&VECTOR_CONTRACTS).unwrap(), bytes, "{name}"); - assert_eq!( - values, - [ClassIdentity(digest(CLASS_DOMAIN, &bytes)).to_string()], - "{name}" - ); - } - let versions = vectors("INVALID_KEY_VERSION"); + let versions = vectors(KEY_VECTORS, "INVALID_KEY_VERSION"); assert_eq!(versions.len(), 3); for (name, bytes, _) in versions { let error = TemplateKey::from_bytes(&bytes).expect_err(name); @@ -1061,10 +1241,35 @@ fn the_conformance_vectors_decode_and_encode() { "{name}: {error}" ); } - let invalid = vectors("INVALID_KEY"); + let invalid = vectors(KEY_VECTORS, "INVALID_KEY"); assert_eq!(invalid.len(), 23); for (name, bytes, _) in invalid { let error = TemplateKey::from_bytes(&bytes).expect_err(name); assert_eq!(error.code(), ErrorCode::Io, "{name}: {error}"); } } + +#[test] +fn the_conformance_vectors_of_classes_encode() { + let classes = vectors(CLASS_VECTORS, "CLASS"); + let names: Vec<&str> = classes.iter().map(|(name, ..)| *name).collect(); + assert_eq!(names, ["TEMPLATE", "NETWORK", "TUNING"]); + for (name, bytes, values) in classes { + let class = vector_class(name); + assert_eq!(class.encode(&VECTOR_CONTRACTS).unwrap(), bytes, "{name}"); + assert_eq!( + values, + [ClassIdentity(digest(CLASS_DOMAIN, &bytes)).to_string()], + "{name}" + ); + } + // The file holds classes alone, and the keys' file no class. + let lines = |file: &str| { + file.lines() + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .map(|line| line.split('|').next().unwrap().to_owned()) + .collect::>() + }; + assert_eq!(lines(CLASS_VECTORS), HashSet::from(["CLASS".to_owned()])); + assert!(!lines(KEY_VECTORS).contains("CLASS")); +} diff --git a/nvx-rs/src/host/template/mod.rs b/nvx-rs/src/host/template/mod.rs index 8bd161d34..5476a0c2d 100644 --- a/nvx-rs/src/host/template/mod.rs +++ b/nvx-rs/src/host/template/mod.rs @@ -11,7 +11,8 @@ //! //! - [`CaptureClass`] holds the invariant inputs of a capture, known before it: the runtime, the //! guest's build, the backend, the CPU profile, the resources, the geometries of the disks, the -//! network, and the other invariant settings. +//! network, the other invariant settings, and the kernel tuning parameters, of +//! [`KERNEL_TUNING_PARAMETERS`], with which the template's kernel boots. //! - [`TemplateManifest::read`] reads the manifest that OpenVMM wrote for a capture, with its time //! contract, and refuses one that is not a template's. //! - [`TemplateKey::new`] derives the key of a template from its class and its manifest, after it @@ -81,7 +82,8 @@ //! - A list is its count, a `u32`, then its items in order. //! //! [`TemplateKey::from_bytes`] refuses any other encoding, and bytes left after it. The vectors in -//! `tests/vectors/template-key-v1.txt` fix version 1 of a key's encoding and of a class's. +//! `tests/vectors/template-key-v1.txt` fix version 1 of a key's encoding, and those in +//! `tests/vectors/capture-class-v2.txt` version 2 of a class's. mod canonical; mod clone; @@ -101,7 +103,9 @@ pub use self::clone::{ RestoreDeadlines, Restoring, Stage, Started, Teardown, reattach_clone, status_evidence, }; pub use self::eligibility::{Downtime, DowntimeSource, Eligibility, HostTime, MAX_DOWNTIME_NS}; -pub use self::key::{CaptureClass, ClassIdentity, Invariants, KeyIdentity, TemplateKey}; +pub use self::key::{ + CaptureClass, ClassIdentity, Invariants, KERNEL_TUNING_PARAMETERS, KeyIdentity, TemplateKey, +}; #[cfg(feature = "cli")] pub(crate) use self::manifest::read_attachments; pub use self::manifest::{ diff --git a/nvx-rs/src/host/template/plan.rs b/nvx-rs/src/host/template/plan.rs index b14f51c4a..6d96a8c8b 100644 --- a/nvx-rs/src/host/template/plan.rs +++ b/nvx-rs/src/host/template/plan.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; +use super::key::kernel_tuning_problem; use super::manifest::PROCESSOR_COUNTS; use super::store::{SNAPSHOT_NAME, StorageGeneration}; use crate::error::{Error, Result}; @@ -12,6 +13,8 @@ use crate::host::sandbox::SandboxNetwork; use crate::host::sandbox::distro::DistroBinding; use crate::host::sandbox::plan::bound_image; use crate::host::sandbox::{SandboxPlan, SandboxUpper, reject_container_path, reject_disk_path}; +#[cfg(doc)] +use crate::host::template::{CaptureClass, KERNEL_TUNING_PARAMETERS}; /// Bytes in a mebibyte, the unit in which the plan writes OpenVMM's `--memory` and /// `--memory-capacity`. @@ -101,14 +104,17 @@ pub struct TemplateResources { /// template that boots with fewer processors online than its capacity, `maxcpus=` with the /// processors online at boot, which limits those that the guest brings online at boot, and which /// OpenVMM records as the template's, so that each restore may bring a prefix of the others -/// online. OpenVMM writes the other tokens itself, each on the allowlist: the early console, -/// console, reboot, and panic settings, `nr_cpus=` with the processor capacity, -/// `nvx_snapshot_tier=platform`, the virtio devices and the control tty that the guest discovers, -/// and the NIC's addresses. The arguments name no workload identity, lifecycle, share, or -/// published listener, for which OpenVMM would write other tokens, and under the NIC's deny-all -/// policy, OpenVMM names no DNS server either. The command line therefore holds no image -/// identity, hostname, share, proxy, DNS server, entrypoint, or other tenant data, and every -/// clone of the template has the same. +/// online. The kernel tuning parameters of the template's class follow them, in their order, +/// which [`with_kernel_tuning`](Self::with_kernel_tuning) sets: those of an adapter's cold boots +/// that the allowlist holds too, [`KERNEL_TUNING_PARAMETERS`], such as `mitigations=off`, which +/// the kernel applies once, as it boots, so that a clone has no other way to get them. OpenVMM +/// writes the other tokens itself, each on the allowlist: the early console, console, reboot, and +/// panic settings, `nr_cpus=` with the processor capacity, `nvx_snapshot_tier=platform`, the +/// virtio devices and the control tty that the guest discovers, and the NIC's addresses. The +/// arguments name no workload identity, lifecycle, share, or published listener, for which +/// OpenVMM would write other tokens, and under the NIC's deny-all policy, OpenVMM names no DNS +/// server either. The command line therefore holds no image identity, hostname, share, proxy, DNS +/// server, entrypoint, or other tenant data, and every clone of the template has the same. /// /// # The capture /// @@ -150,7 +156,8 @@ pub struct TemplateResources { /// memory_capacity: 16 * MEMORY_BLOCK_SIZE, /// }; /// let template = -/// TemplatePlan::new(adapter, binding, 64 << 20, resources, transaction, generation)?; +/// TemplatePlan::new(adapter, binding, 64 << 20, resources, transaction, generation)? +/// .with_kernel_tuning(&["mitigations=off".to_owned()])?; /// let mut arguments = template.openvmm_arguments()?; /// arguments.extend(["--cmdline".to_owned(), template.kernel_command_line().to_owned()]); /// // ... the caller's own arguments: the hypervisor, the kernel and the initramfs, the CPU @@ -273,16 +280,7 @@ impl TemplatePlan { reject_disk_path(&scratch, &scratch.display().to_string())?; let destination = transaction.join(SNAPSHOT_NAME); let plan = SandboxPlan::adapter(distro.clone(), SandboxUpper::scratch_at(scratch)); - // A template whose processors are all online at boot needs no limit, and records none, - // as OpenVMM records none without the parameter. - let command_line = if resources.boot_processors < resources.processors { - format!( - "{COMMAND_LINE} {BOOT_PROCESSORS}={}", - resources.boot_processors - ) - } else { - COMMAND_LINE.to_owned() - }; + let command_line = own_command_line(&resources); Ok(Self { plan, nic, @@ -296,6 +294,28 @@ impl TemplatePlan { }) } + /// Returns the plan with the kernel tuning parameters `parameters`, which replace any earlier + /// ones: those with which the template's kernel boots, in their order, after the plan's own + /// tokens, as [`CaptureClass::kernel_tuning`] holds them, and as + /// [The kernel command line](Self#the-kernel-command-line) describes. A plan that + /// [`new`](Self::new) returns has none. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) as + /// [`CaptureClass::check`] refuses the parameters: for one that is not among + /// [`KERNEL_TUNING_PARAMETERS`], and for a parameter that appears more than once. + pub fn with_kernel_tuning(mut self, parameters: &[String]) -> Result { + if let Some(problem) = kernel_tuning_problem(parameters) { + return Err(Error::unsupported(problem)); + } + let mut command_line = own_command_line(&self.resources); + for parameter in parameters { + command_line.push(' '); + command_line.push_str(parameter); + } + self.command_line = command_line; + Ok(self) + } + /// Returns the capture's devices but its NIC: an adapter's plan of the placeholder over the /// dormant scratch device, without a network, whose NIC [`nic_arguments`](Self::nic_arguments) /// attaches. @@ -356,8 +376,9 @@ impl TemplatePlan { /// Returns the kernel command line of the capture's VM, which the caller passes as OpenVMM's /// `--cmdline`, alone: `nvx_sandbox=1`, then, for a template that boots with fewer processors - /// online than its capacity, `maxcpus=` with the processors online at boot, as - /// [The kernel command line](Self#the-kernel-command-line) describes. + /// online than its capacity, `maxcpus=` with the processors online at boot, then the kernel + /// tuning parameters, if [`with_kernel_tuning`](Self::with_kernel_tuning) set any, in their + /// order, as [The kernel command line](Self#the-kernel-command-line) describes. /// /// It takes the place of the command line of [`plan`](Self::plan), which /// [`SandboxPlan::kernel_command_line`] and a [`PreparedLaunch`] render: that one names the @@ -426,6 +447,21 @@ fn is_block_length(length: u64) -> bool { length != 0 && length.is_multiple_of(LOGICAL_BLOCK_SIZE) } +/// Returns the template's own kernel command line for `resources`: `nvx_sandbox=1`, then +/// `maxcpus=` with the processors online at boot when they are fewer than the processors. +fn own_command_line(resources: &TemplateResources) -> String { + // A template whose processors are all online at boot needs no limit, and records none, as + // OpenVMM records none without the parameter. + if resources.boot_processors < resources.processors { + format!( + "{COMMAND_LINE} {BOOT_PROCESSORS}={}", + resources.boot_processors + ) + } else { + COMMAND_LINE.to_owned() + } +} + /// Refuses `resources` that no template's VM has: processors other than 1, 2, 4, or 8, processors /// online at boot other than 1, 2, 4, or 8, or more than the processors, and RAM or a RAM capacity /// that is not a nonzero multiple of [`MEMORY_BLOCK_SIZE`], or a capacity below the RAM. diff --git a/nvx-rs/src/host/template/plan/tests.rs b/nvx-rs/src/host/template/plan/tests.rs index e1e94695f..85664cf97 100644 --- a/nvx-rs/src/host/template/plan/tests.rs +++ b/nvx-rs/src/host/template/plan/tests.rs @@ -14,6 +14,7 @@ use crate::host::network::{ use crate::host::sandbox::distro::{BINDING_VERSION, DISTRO_POLICY_REVISION}; use crate::host::sandbox::{SandboxDistro, SandboxNetwork, SandboxSettings}; use crate::host::seal::SEAL_VERSION; +use crate::host::template::KERNEL_TUNING_PARAMETERS; use crate::protocol::cmdline; use crate::protocol::root::RootImage; use crate::uuid::Uuid; @@ -37,6 +38,9 @@ mod openvmm { pub(super) fn admits(token: &str) -> bool { platform_command_line_token_is_invariant(token) } + + /// OpenVMM's kernel tuning parameters, which its platform allowlist admits. + pub(super) const TUNING: &[&str] = &PLATFORM_TUNING_TOKENS; } /// Bytes in a GiB. @@ -488,6 +492,100 @@ fn templates_limit_their_processors_online_at_boot_on_their_command_line() { assert_eq!(template.resources(), inputs.resources); } +#[test] +fn templates_boot_with_their_kernel_tuning_parameters_after_their_own_tokens() { + let tuning = strings(&["mitigations=off", "no_timer_check", "quiet", "loglevel=0"]); + // After the limit of the processors online at boot, the last of the template's own tokens. + let limited = Inputs { + resources: TemplateResources { + boot_processors: 1, + ..resources(4) + }, + ..Inputs::default() + } + .lower() + .unwrap(); + let tuned = limited.clone().with_kernel_tuning(&tuning).unwrap(); + assert_eq!( + tuned.kernel_command_line(), + "nvx_sandbox=1 maxcpus=1 mitigations=off no_timer_check quiet loglevel=0" + ); + // Nothing else of the plan changes. + assert_eq!( + tuned.openvmm_arguments().unwrap(), + limited.openvmm_arguments().unwrap() + ); + assert_eq!( + (tuned.resources(), tuned.plan(), tuned.nic_arguments()), + (limited.resources(), limited.plan(), limited.nic_arguments()) + ); + // In their order, which the kernel keeps: the last of `quiet` and `loglevel=` sets the + // console's log level. + let untuned = Inputs::default().lower().unwrap(); + let ordered = strings(&["loglevel=7", "quiet", "mitigations=auto,nosmt"]); + assert_eq!( + untuned + .clone() + .with_kernel_tuning(&ordered) + .unwrap() + .kernel_command_line(), + "nvx_sandbox=1 loglevel=7 quiet mitigations=auto,nosmt" + ); + // Each parameter alone, as OpenVMM's platform allowlist admits it. + for parameter in KERNEL_TUNING_PARAMETERS { + let template = untuned + .clone() + .with_kernel_tuning(&strings(&[parameter])) + .unwrap(); + assert_eq!( + template.kernel_command_line(), + format!("nvx_sandbox=1 {parameter}") + ); + assert!(openvmm::admits(parameter), "{parameter}"); + } + // Later parameters replace earlier ones, and none leaves the template's own tokens. + assert_eq!( + tuned + .clone() + .with_kernel_tuning(&strings(&["quiet"])) + .unwrap() + .kernel_command_line(), + "nvx_sandbox=1 maxcpus=1 quiet" + ); + assert_eq!(tuned.with_kernel_tuning(&[]).unwrap(), limited); +} + +#[test] +fn the_kernel_tuning_parameters_are_those_of_openvmms_platform_allowlist() { + assert_eq!(KERNEL_TUNING_PARAMETERS.as_slice(), openvmm::TUNING); +} + +#[test] +fn templates_refuse_kernel_tuning_parameters_that_no_capture_takes() { + for (parameters, named) in [ + (&["mitigations=on"][..], "mitigations=on"), + (&["loglevel=8"][..], "loglevel=8"), + (&["no-timer-check"][..], "no-timer-check"), + (&["nokaslr"][..], "nokaslr"), + (&["nvx_hostname=tenant"][..], "nvx_hostname=tenant"), + (&["quiet loglevel=0"][..], "quiet loglevel=0"), + (&["quiet", "quiet"][..], "quiet"), + (&["mitigations=off", "mitigations=auto"][..], "mitigations"), + ] { + let error = Inputs::default() + .lower() + .unwrap() + .with_kernel_tuning(&strings(parameters)) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{parameters:?}"); + assert!( + error.message().contains("kernel tuning parameter") && error.message().contains(named), + "{parameters:?}: {}", + error.message() + ); + } +} + #[test] fn the_upper_of_an_adapters_plan_is_a_clones_and_no_part_of_its_template() { let over_scratch = template(adapter(None)); diff --git a/nvx-rs/src/host/template/store/capture.rs b/nvx-rs/src/host/template/store/capture.rs index 284f3990e..9052d3f78 100644 --- a/nvx-rs/src/host/template/store/capture.rs +++ b/nvx-rs/src/host/template/store/capture.rs @@ -92,8 +92,9 @@ const CAPTURES_WHAT: &str = "the captures of the template store"; /// [`Store::create_template`] and [`Store::refresh_template`] capture. /// /// The adapter's plan is lowered as [`TemplatePlan::new`] lowers it, with the processors, those -/// online at boot among them, and RAM of the class, and a scratch of the class's scratch length; -/// its upper is no part of the template. The class and the plan must describe one VM: a +/// online at boot among them, and RAM of the class, and a scratch of the class's scratch length, +/// and with the class's kernel tuning parameters, as [`TemplatePlan::with_kernel_tuning`] sets +/// them; its upper is no part of the template. The class and the plan must describe one VM: a /// placeholder whose binding records the class's `distro` length, and a network of the plan whose /// NIC, as [`SandboxNetwork::template_arguments`] lowers it for a template, is the class's: none, /// or one at the same guest's address, under the complete deny-all policy, whatever the policies @@ -104,7 +105,7 @@ const CAPTURES_WHAT: &str = "the captures of the template store"; pub struct Capture { /// The class of the capture, which names its slot, and from which the capture takes its /// backend, its CPU profile, its processors and those online at boot, its RAM, its scratch - /// length, and the runtime identity of its launch. + /// length, its kernel tuning parameters, and the runtime identity of its launch. pub class: CaptureClass, /// The adapter's plan, which [`SandboxPlan::adapter`] makes, of the placeholder `distro` and /// the template's network. @@ -516,7 +517,8 @@ impl Store { inputs.resources, &transaction, StorageGeneration::generate()?, - )?; + )? + .with_kernel_tuning(&capture.class.kernel_tuning)?; let envelope = match self.check_capture(capture, &inputs, &template, &locked, deadline, hooks) { Ok(envelope) => envelope, diff --git a/nvx-rs/src/host/template/store/capture/tests.rs b/nvx-rs/src/host/template/store/capture/tests.rs index 0bc55448e..1e6fd5511 100644 --- a/nvx-rs/src/host/template/store/capture/tests.rs +++ b/nvx-rs/src/host/template/store/capture/tests.rs @@ -514,6 +514,70 @@ fn captures_limit_the_processors_that_their_class_keeps_offline_at_boot() { assert_eq!((key.processors(), key.boot_processors()), (4, 1)); } +#[test] +fn captures_boot_with_the_kernel_tuning_parameters_of_their_class() { + let mut bench = Bench::new(); + bench.capture.class.kernel_tuning = vec!["mitigations=off".to_owned(), "quiet".to_owned()]; + // OpenVMM records the command line with which the kernel booted. + let mut spec = ManifestSpec::template(); + let contract = spec.contract(); + contract.command_line = + contract + .command_line + .replacen("nvx_sandbox=1", "nvx_sandbox=1 mitigations=off quiet", 1); + let emulation = Emulation { + manifest: Some(spec), + ..Emulation::capture() + }; + let arguments = RefCell::new(Vec::new()); + let run = |locked: &mut LockedStore, vm: &Vm<'_>| { + let command = vm.command("control", Path::new("outcome.json"), locked.dir())?; + arguments.replace(command.arguments); + emulation.run(locked, vm) + }; + let created = bench.serve_with(Some(&|| Ok(after(1))), &run, &uninterrupted, false); + let Created::Captured(generation) = created.unwrap() else { + panic!("the first creation of a class captures it"); + }; + let arguments: Vec = arguments + .take() + .into_iter() + .map(|argument| argument.into_string().unwrap()) + .collect(); + let at = arguments + .iter() + .position(|given| given == "--cmdline") + .unwrap(); + assert_eq!(arguments[at + 1], "nvx_sandbox=1 mitigations=off quiet"); + let key = bench.store.read_key(&generation).unwrap(); + assert!( + key.command_line() + .contains(" nvx_sandbox=1 mitigations=off quiet "), + "{}", + key.command_line() + ); + + // A capture whose kernel booted without them made no template of its class, and publishes + // nothing. + let refused = bench.refresh(&Emulation::capture()); + let error = refused.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Io, "{error}"); + assert!( + error.message().contains( + "its kernel tuning parameters are none, but the class requests mitigations=off quiet" + ), + "{error}" + ); + assert_eq!(bench.indexed(), [1]); + + // A parameter that no capture takes is refused before any VM boots. + let runs = bench.runs.get(); + bench.capture.class.kernel_tuning = vec!["mitigations=on".to_owned()]; + let error = bench.refresh(&Emulation::capture()).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert_eq!(bench.runs.get(), runs); +} + #[test] fn refreshes_leave_older_generations_and_their_leases_untouched() { let bench = Bench::new(); diff --git a/nvx-rs/src/host/template/tests.rs b/nvx-rs/src/host/template/tests.rs index d16127ade..1badb2835 100644 --- a/nvx-rs/src/host/template/tests.rs +++ b/nvx-rs/src/host/template/tests.rs @@ -974,6 +974,7 @@ pub(super) fn class() -> CaptureClass { }, network: None, invariants: Invariants::new(), + kernel_tuning: Vec::new(), } } diff --git a/nvx-rs/src/testing/fake_openvmm.rs b/nvx-rs/src/testing/fake_openvmm.rs index e9d029782..ba1a713f7 100644 --- a/nvx-rs/src/testing/fake_openvmm.rs +++ b/nvx-rs/src/testing/fake_openvmm.rs @@ -114,7 +114,8 @@ //! share, `--snapshot-destination`, `--snapshot-tier platform`, a nonzero //! `--snapshot-generation-id` of 32 hexadecimal digits, the options of a cold boot of a run with //! a `--memory-capacity` that is a multiple of 128 MiB, `nvx_sandbox=1` on the kernel command -//! line, alone or followed by a canonical `maxcpus=` of 1, 2, 4, or 8 of its `--processors`, +//! line, alone or followed by a canonical `maxcpus=` of 1, 2, 4, or 8 of its `--processors`, then +//! by kernel tuning parameters that OpenVMM admits on a capture's command line, each at most once, //! `--virtio-console none`, the control console with its capability on standard input, and an //! outcome report. On Unix, the //! control console's socket must be in the directory of the snapshot's destination, as above. The @@ -208,7 +209,27 @@ const CAPTURE_SCENARIO_NAME: &str = "fake-openvmm-capture.json"; /// The refusal of a capture whose kernel command line is not a template's. const CAPTURE_COMMAND_LINE: &str = "a capture's kernel command line is nvx_sandbox=1, alone or \ - with a canonical maxcpus= of 1, 2, 4, or 8 of its processors"; + with a canonical maxcpus= of 1, 2, 4, or 8 of its processors, \ + then kernel tuning parameters that OpenVMM admits, each at \ + most once"; + +/// The kernel tuning parameters that OpenVMM admits on the command line of a capture of its +/// platform tier, each parameter at most once. +const KERNEL_TUNING_PARAMETERS: [&str; 13] = [ + "mitigations=off", + "mitigations=auto", + "mitigations=auto,nosmt", + "no_timer_check", + "quiet", + "loglevel=0", + "loglevel=1", + "loglevel=2", + "loglevel=3", + "loglevel=4", + "loglevel=5", + "loglevel=6", + "loglevel=7", +]; /// Prefix of the name of the file that backs the RAM of a capture's VM, beside the snapshot's /// destination, which the process ID of the VMM follows. @@ -1379,14 +1400,13 @@ fn parse(program: &str, arguments: &[String]) -> Result { if capture { // A template's VM boots the invariant configuration alone, without a tenant, with at // most the limit of its processors online at boot, which OpenVMM records in the - // canonical form alone. + // canonical form alone, and then the kernel tuning parameters that OpenVMM admits. let processors = values .get("--processors") .and_then(|given| given.first()) .and_then(|given| given.parse::().ok()); - let invariant = match tokens.as_slice() { - ["nvx_sandbox=1"] => true, - ["nvx_sandbox=1", limit] => limit + let is_limit = |limit: &str| { + limit .strip_prefix("maxcpus=") .and_then(|count| { count @@ -1396,7 +1416,13 @@ fn parse(program: &str, arguments: &[String]) -> Result { }) .is_some_and(|count| { [1, 2, 4, 8].contains(&count) && processors.is_some_and(|all| count <= all) - }), + }) + }; + let invariant = match tokens.as_slice() { + ["nvx_sandbox=1", limit, tuning @ ..] if limit.starts_with("maxcpus=") => { + is_limit(limit) && is_kernel_tuning(tuning) + } + ["nvx_sandbox=1", tuning @ ..] => is_kernel_tuning(tuning), _ => false, }; if !invariant { @@ -1909,6 +1935,22 @@ fn ram_overlay_requested(tokens: &[&str]) -> Result { } } +/// Returns whether `tokens` are kernel tuning parameters that OpenVMM admits on the command line of +/// a capture: each one of [`KERNEL_TUNING_PARAMETERS`], and each parameter, by its name, at most +/// once. +fn is_kernel_tuning(tokens: &[&str]) -> bool { + /// Returns the name of the kernel parameter `token`. + fn name(token: &str) -> &str { + token.split_once('=').map_or(token, |(name, _)| name) + } + tokens.iter().enumerate().all(|(index, token)| { + KERNEL_TUNING_PARAMETERS.contains(token) + && !tokens[..index] + .iter() + .any(|earlier| name(earlier) == name(token)) + }) +} + /// Returns the number of host mappings that the `nvx_maps=` tokens of a kernel command line, /// whose values are `counts`, announce: none without a token, and otherwise from 1 through /// [`MAX_HOST_MAPPINGS`], given once. diff --git a/nvx-rs/src/testing/fake_openvmm/tests.rs b/nvx-rs/src/testing/fake_openvmm/tests.rs index dba0e7637..6d095f2bd 100644 --- a/nvx-rs/src/testing/fake_openvmm/tests.rs +++ b/nvx-rs/src/testing/fake_openvmm/tests.rs @@ -636,6 +636,46 @@ fn captures_may_limit_the_processors_online_at_boot() { } } +#[test] +fn captures_may_boot_with_the_kernel_tuning_parameters_that_openvmm_admits() { + // The capture of `CAPTURE` has 2 processors. + for command_line in [ + "nvx_sandbox=1 mitigations=off", + "nvx_sandbox=1 maxcpus=1 mitigations=auto,nosmt no_timer_check quiet loglevel=0", + "nvx_sandbox=1 loglevel=7 quiet mitigations=auto", + ] { + let (_temporary, options) = parse_capture(&capture_with("--cmdline", command_line)); + assert_eq!(options.err(), None, "{command_line}"); + } + // Other values and parameters, a parameter more than once, and a limit that OpenVMM would not + // record. + for command_line in [ + "nvx_sandbox=1 mitigations=on", + "nvx_sandbox=1 loglevel=8", + "nvx_sandbox=1 no-timer-check", + "nvx_sandbox=1 nokaslr", + "nvx_sandbox=1 quiet quiet", + "nvx_sandbox=1 mitigations=off mitigations=auto", + "nvx_sandbox=1 maxcpus=01 quiet", + ] { + let (_temporary, options) = parse_capture(&capture_with("--cmdline", command_line)); + assert_eq!( + options.err(), + Some(CAPTURE_COMMAND_LINE.to_owned()), + "{command_line}" + ); + } +} + +#[cfg(feature = "host")] +#[test] +fn the_kernel_tuning_parameters_are_those_of_nvx_s_capture_classes() { + assert_eq!( + KERNEL_TUNING_PARAMETERS, + crate::host::template::KERNEL_TUNING_PARAMETERS + ); +} + #[cfg(unix)] #[test] fn captures_listen_in_the_directory_of_their_snapshot_destination() { diff --git a/nvx-rs/tests/template_capture.rs b/nvx-rs/tests/template_capture.rs index 71ce7b209..ab8804f47 100644 --- a/nvx-rs/tests/template_capture.rs +++ b/nvx-rs/tests/template_capture.rs @@ -103,6 +103,7 @@ fn class() -> CaptureClass { }, network: None, invariants: Invariants::new(), + kernel_tuning: Vec::new(), } } diff --git a/nvx-rs/tests/vectors/README.md b/nvx-rs/tests/vectors/README.md index f24c66442..0b121eab1 100644 --- a/nvx-rs/tests/vectors/README.md +++ b/nvx-rs/tests/vectors/README.md @@ -12,7 +12,8 @@ yet breaks those peers. Only bytes that no codec produced catch such a change. | [`nvxc-v1.txt`](nvxc-v1.txt) | Version 1 of the control protocol: `NVXC` frames, their `EXEC`, `MAPS`, and features payloads, and `NVXS` records of NVX's host and guest | | [`nvxl-v1.txt`](nvxl-v1.txt) | Version 1 of the `NVXL` listener announcement | | [`activation-v1.txt`](activation-v1.txt) | Version 1 of the activation record of a layered root, and what Ready reports about an activation | -| [`template-key-v1.txt`](template-key-v1.txt) | Version 1 of the canonical encodings of a template's compatibility key and of a capture class, with their identities | +| [`template-key-v1.txt`](template-key-v1.txt) | Version 1 of the canonical encoding of a template's compatibility key, with its identity | +| [`capture-class-v2.txt`](capture-class-v2.txt) | Version 2 of the canonical encoding of a capture class, with its identity | | [`openvmm/`](openvmm) | OpenVMM's vectors and cases of the `NVXS` session, and the excerpts of its sources that define the kernel command-line tokens of its platform tier, unchanged | [4.4 Host-Guest Control Interface](../../../doc/design/4.4-host-guest-control-interface.md) @@ -23,9 +24,10 @@ describe the protocols, and the documentation of `nvx::activation`, in ## Line format -`nvxc-v1.txt`, `nvxl-v1.txt`, `activation-v1.txt`, and `template-key-v1.txt` -share one line format. A line that starts with `#` is a comment, and an empty -line is ignored. Every other line is one vector, whose fields `|` separates: +`nvxc-v1.txt`, `nvxl-v1.txt`, `activation-v1.txt`, `template-key-v1.txt`, and +`capture-class-v2.txt` share one line format. A line that starts with `#` is a +comment, and an empty line is ignored. Every other line is one vector, whose +fields `|` separates: ```text KIND|NAME|BYTES|VALUE|... @@ -50,10 +52,12 @@ must encode the values to exactly its bytes. A kind whose name starts with `INVALID_` has no values: every decoder must refuse the bytes of its vectors. A key or a class holds too many values for one line, so the value of each -vector of `template-key-v1.txt` is its identity, and the file's header names -the captures whose keys and classes the vectors encode. The tests of -`nvx::host::template` derive each key from its capture, and check that it is -the key that the vector's bytes decode to. +vector of `template-key-v1.txt` and of `capture-class-v2.txt` is its identity, +and each file's header names the captures whose keys or classes the vectors +encode. The tests of `nvx::host::template` derive each key from its capture, +and check that it is the key that the vector's bytes decode to, and encode +each class to the vector's bytes. Nothing decodes a class, which only its +identity names. ## Rules @@ -67,12 +71,17 @@ the key that the vector's bytes decode to. hosts, whose exports are aggregates, send none, and a host whose export is one directory sends one only to a guest that deploys with it, so the C agent of earlier releases, which refuses it, never receives one. And - `CLASS|NETWORK` of - `template-key-v1.txt` came to encode its network in place as the template - captures it, under the complete deny-all policy: the encodings of a key and - of a class have not been released, so a template store that an earlier build - created is not supported, and is deleted and created again, since nothing - migrates it. + `CLASS|NETWORK` of version 1 of a class's encoding came to encode its + network in place as the template captures it, under the complete deny-all + policy: the encodings of a key and of a class have not been released, so a + template store that an earlier build created is not supported, and is + deleted and created again, since nothing migrates it. +- Version 2 of a class's encoding, which adds the kernel tuning parameters of + [microsoft/nvx#995](https://github.com/microsoft/nvx/issues/995), replaced + version 1, whose vectors `template-key-v1.txt` held until then. Its vectors + in `capture-class-v2.txt` encode the classes of version 1's, and one with + kernel tuning parameters. No decoder refuses version 1, since nothing decodes + a class. - The bytes come from the protocol's definition, never from the code that they check, and so do the values of each vector. @@ -96,7 +105,8 @@ constant of the kernel tuning parameters that it admits among them, and `microvm_control_tty_command_line.rs`, the constant of `openvmm/openvmm_defs/src/microvm.rs` that names the control tty's token, which the function reads. The tests of `nvx::host::template` compile them, and check -each token of a template's kernel command line with OpenVMM's own function. The +each token of a template's kernel command line with OpenVMM's own function, and +the kernel tuning parameters of a template with OpenVMM's own constant. The runtime adapter in `tests/runtime-adapter/` compiles them too, as the allowlist with which the guest agent of a template's VM checks its kernel command line, and its tests pin the set of tokens that they admit. diff --git a/nvx-rs/tests/vectors/capture-class-v2.txt b/nvx-rs/tests/vectors/capture-class-v2.txt new file mode 100644 index 000000000..d1a5b95f8 --- /dev/null +++ b/nvx-rs/tests/vectors/capture-class-v2.txt @@ -0,0 +1,24 @@ +# Conformance vectors of version 2 of the canonical encoding of a capture class, +# nvx::host::template::CaptureClass, which the module nvx::host::template describes. README.md +# describes the line format and the rules that the vectors follow. +# +# CLASS|NAME|BYTES|IDENTITY +# The canonical encoding of a class, and its identity: the SHA-256 digest of "nvx capture class", +# a NUL byte, and the encoding. An encoder must encode the class to the bytes. +# +# Version 2 holds what version 1 held after its version, then the kernel tuning parameters, a list +# of texts. The classes are those of the captures that template-key-v1.txt names, with the +# OpenVMM, kernel, and initramfs identities sha256:a..., sha256:b..., and sha256:c..., the guest +# build guest-build-1, boot contract 3, control contract nvx-microvm-v2-control-v2, control +# session 1, snapshot format 6, device-state schema 1, time ABI 1, restore packet 4, and +# activation contract 1. +# The class of the capture of TEMPLATE: the CPU profile auto, and 2 processors, all online at +# boot, without kernel tuning parameters. +CLASS|TEMPLATE|0200470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f0200000002000000000000200000000000000080000000000000004000000000000200000010000000000004000000000002000000020000000000000000000000000000|e875bc416862b2713b15c52a28ae474d78f1a78638748ef233eae9e34a4c8ecc +# The class of the capture of NETWORK, whose NIC the capture attaches under the complete +# deny-all policy, whatever the policy of the class's network, here one that allows egress, +# without kernel tuning parameters. +CLASS|NETWORK|0200470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f02000000020000000000002000000000000000800000000000000040000000000002000000100000000000040000000000020000000200000a000000050000002d2d6e65740b00000031302e302e302e322f3234110000002d2d6e6574776f726b2d70726f66696c6508000000706f727461626c65100000002d2d6e6574776f726b2d6567726573730400000064656e79110000002d2d6e6574776f726b2d696e67726573730400000064656e790f0000002d2d686f73742d6c6f6f706261636b0400000064656e79020000001100000069736f6c6174696f6e2e736563636f6d70060000007374726963740b0000006c696d6974732e706964730300000035313200000000|cd1a67bbeeb1322901e2c617a06856d8c5c9bea41e3c4f35c624580cb794c3da +# The class of TEMPLATE with the kernel tuning parameters mitigations=auto,nosmt, +# no_timer_check, quiet, and loglevel=0, in that order. +CLASS|TUNING|0200470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f0200000002000000000000200000000000000080000000000000004000000000000200000010000000000004000000000002000000020000000000000000000004000000160000006d697469676174696f6e733d6175746f2c6e6f736d740e0000006e6f5f74696d65725f636865636b0500000071756965740a0000006c6f676c6576656c3d30|35871b8eca507e82f2ee9434e5dfd828f7853adbc0b7f3a4b7a229afba55d2ba diff --git a/nvx-rs/tests/vectors/template-key-v1.txt b/nvx-rs/tests/vectors/template-key-v1.txt index 1694806a6..a8271b935 100644 --- a/nvx-rs/tests/vectors/template-key-v1.txt +++ b/nvx-rs/tests/vectors/template-key-v1.txt @@ -1,29 +1,26 @@ -# Conformance vectors of version 1 of the canonical encodings of a template's compatibility key, -# nvx::host::template::TemplateKey, and of a capture class, nvx::host::template::CaptureClass, -# which the module nvx::host::template describes. README.md describes the line format and the -# rules that the vectors follow. +# Conformance vectors of version 1 of the canonical encoding of a template's compatibility key, +# nvx::host::template::TemplateKey, which the module nvx::host::template describes. README.md +# describes the line format and the rules that the vectors follow. The vectors of version 2 of +# the canonical encoding of a capture class, which replaced version 1, are in +# capture-class-v2.txt. # # KEY|NAME|BYTES|IDENTITY # The canonical encoding of a key, and its identity: the SHA-256 digest of "nvx template key", a # NUL byte, and the encoding. A decoder must decode the bytes, and an encoder must encode the key # that they hold to the same bytes. -# CLASS|NAME|BYTES|IDENTITY -# The canonical encoding of a class, and its identity: the SHA-256 digest of "nvx capture class", -# a NUL byte, and the encoding. # INVALID_KEY_VERSION|NAME|BYTES # A key of another version of the encoding, which every decoder refuses as unsupported. # INVALID_KEY|NAME|BYTES # Bytes that every decoder of a key must refuse. # -# The keys and classes are those of the captures that tests/fixtures/template/README.md -# describes, as microVM ABI 2 captured them, when every virtio device but the NIC also offered -# access-platform (feature bit 33), each console also offered its size (feature bit 0), and the -# virtio devices interrupted on ISA IRQs (the NIC on 10, the filesystem on 6, the consoles on 7 -# and 3, and the distro and scratch blocks on 4 and 11), with the OpenVMM, kernel, and initramfs -# identities sha256:a..., sha256:b..., and -# sha256:c..., the guest build guest-build-1, boot contract 3, control contract -# nvx-microvm-v2-control-v2, control session 1, snapshot format 6, device-state schema 1, time -# ABI 1, restore packet 4, and activation contract 1. +# The keys are those of the captures that tests/fixtures/template/README.md describes, as microVM +# ABI 2 captured them, when every virtio device but the NIC also offered access-platform (feature +# bit 33), each console also offered its size (feature bit 0), and the virtio devices interrupted +# on ISA IRQs (the NIC on 10, the filesystem on 6, the consoles on 7 and 3, and the distro and +# scratch blocks on 4 and 11), with the OpenVMM, kernel, and initramfs identities sha256:a..., +# sha256:b..., and sha256:c..., the guest build guest-build-1, boot contract 3, control contract +# nvx-microvm-v2-control-v2, control session 1, snapshot format 6, device-state schema 1, time ABI +# 1, restore packet 4, and activation contract 1. # The key of the template of a KVM microVM of 2 processors, all online at boot, and 512 MiB # of RAM, of a capacity of 2 GiB, with a distro of 1 GiB, a scratch of 64 MiB, the dormant # filesystem slot, a console, a control console, and no NIC, on the CPU profile @@ -39,13 +36,6 @@ KEY|HOST_PROFILE|0100470000007368613235363a6161616161616161616161616161616161616 # A machine of one processor and one device, and no NIC, which the invalid vectors change. KEY|MINIMAL|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000|d79a8fefc401da8568586cdc595a93a7e6c988aefbc62b03038901940211c1f1 -# The class of the capture of TEMPLATE: the CPU profile auto, and 2 processors, all online at -# boot. -CLASS|TEMPLATE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f02000000020000000000002000000000000000800000000000000040000000000002000000100000000000040000000000020000000200000000000000000000|b53b0169db5d983b31f24bec900cd0b962d26f12480d90803f21ae1d5c3ff334 -# The class of the capture of NETWORK, whose NIC the capture attaches under the complete -# deny-all policy, whatever the policy of the class's network, here one that allows egress. -CLASS|NETWORK|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f02000000020000000000002000000000000000800000000000000040000000000002000000100000000000040000000000020000000200000a000000050000002d2d6e65740b00000031302e302e302e322f3234110000002d2d6e6574776f726b2d70726f66696c6508000000706f727461626c65100000002d2d6e6574776f726b2d6567726573730400000064656e79110000002d2d6e6574776f726b2d696e67726573730400000064656e790f0000002d2d686f73742d6c6f6f706261636b0400000064656e79020000001100000069736f6c6174696f6e2e736563636f6d70060000007374726963740b0000006c696d6974732e7069647303000000353132|2f32edae7e9eb532625af600232c68aa78b92e02c8116cb49f857f13ff5fcc7c - # MINIMAL in other versions of the encoding. INVALID_KEY_VERSION|VERSION_0|0000470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 INVALID_KEY_VERSION|VERSION_2|0200470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 diff --git a/tests/runtime-adapter/src/capture.rs b/tests/runtime-adapter/src/capture.rs index 37cdf8bf6..8267758db 100644 --- a/tests/runtime-adapter/src/capture.rs +++ b/tests/runtime-adapter/src/capture.rs @@ -173,6 +173,7 @@ impl Request { scratch: geometry(scratch, self.scratch_length)?, network: self.network.clone(), invariants, + kernel_tuning: Vec::new(), }; Ok(Capture { class, From a5908fa951e0671d88552a5cdb746d7ceaa2754b Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Mon, 9 Nov 2026 08:47:41 -0800 Subject: [PATCH 4/5] runtime-adapter: capture templates with kernel tuning parameters The runtime adapter, which builds on nvx's public API as an independent adapter does, could not ask for the kernel tuning parameters that NVX's capture classes now take (#995). create and refresh take --kernel-tuning PARAMETER, once for each parameter, in order, which capture::Request carries into the class's kernel_tuning, so that the template's kernel, and every clone of it, runs with them. NVX's class check refuses a parameter that is not one of its KERNEL_TUNING_PARAMETERS, and a parameter given twice, before the host program creates the store. The guest agent's check of a template's command line, which compiles the excerpt of OpenVMM's platform allowlist, admits the parameters since the pin of OpenVMM that admits them, and is_platform_token's documentation and the README's template mode now list them. Tests: template_capture's a_capture_takes_its_kernel_tuning_from_the_request checks that the class takes the request's parameters in their order, which name a class of their own, order included, while nothing else of the capture changes, and that NVX's class refuses other parameters; vm_template's kernel_tuning_that_no_capture_takes_is_refused_before_the_store checks that `nokaslr`, `mitigations=on`, and `mitigations=` with two values fail with status 1 before the store exists; and vm_clone's a_template_s_clones_run_with_its_kernel_tuning_parameters captures a template with `mitigations=off`, whose key's command line holds it, and starts a clone whose /proc/cmdline is that command line. The template module's a_template_s_command_line_may_hold_kernel_tuning_parameters checks that a template's VM boots with the parameters on its command line, and the_platform_tier_s_allowlist_is_openvmm_s and template_mode_refuses_a_token_outside_the_allowlist now admit them and refuse other values and spellings of them, such as `mitigations=on`, `loglevel=8`, and `no-timer-check`. scenarios.txt and the README's tables name the new tests, and template_capture's request helper names no tuning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/runtime-adapter/README.md | 46 +++++++++---- tests/runtime-adapter/scenarios.txt | 3 + .../src/bin/runtime-adapter-host.rs | 18 ++++- tests/runtime-adapter/src/capture.rs | 16 ++++- tests/runtime-adapter/src/template.rs | 11 +-- tests/runtime-adapter/src/template/tests.rs | 52 +++++++++++++- .../runtime-adapter/tests/template_capture.rs | 67 ++++++++++++++++++- tests/runtime-adapter/tests/vm_clone.rs | 39 +++++++++++ tests/runtime-adapter/tests/vm_template.rs | 27 +++++++- 9 files changed, 254 insertions(+), 25 deletions(-) diff --git a/tests/runtime-adapter/README.md b/tests/runtime-adapter/README.md index 1eb8cec9a..3c7647123 100644 --- a/tests/runtime-adapter/README.md +++ b/tests/runtime-adapter/README.md @@ -740,6 +740,10 @@ byte: - `earlycon=xe9`, `console=hvc0`, `console=hvc1`, `reboot=t`, `panic=-1`, `nvx_sandbox=1`, `nvx_config=0xd0010000,65536`, `nvx_snapshot_tier=platform`, and `nvx_control_tty=hvc2`; +- the kernel tuning parameters `mitigations=off`, `mitigations=auto`, + `mitigations=auto,nosmt`, `no_timer_check`, `quiet`, and `loglevel=0` to + `loglevel=7`, with which a template boots as its adapter's cold sandboxes + do, and which OpenVMM also refuses to repeat; - `nr_cpus=`, `maxcpus=`, `virtio_mmio.device=`, `virtnet_ip=`, `virtnet_mask=`, `virtnet_gw=`, `virtnet_dns=`, `virtnet_ip6=`, and `virtnet_gw6=`, with any value that the host chooses. @@ -1463,8 +1467,10 @@ command's options choose of the template, and `Request::capture` returns NVX's fewest that the template's clones take, the RAM and its capacity, the geometries of the placeholder and of the dormant scratch, as OpenVMM presents a sandbox block device, which `capture::geometry` gives, the network, as - the capture attaches its NIC, which no network's policy changes, and, for a - template whose clones delay their repair, for tests, the delay, in + the capture attaches its NIC, which no network's policy changes, the kernel + tuning parameters that the command's `--kernel-tuning` options name, in their + order, with which the template's kernel boots, such as `mitigations=off`, and, + for a template whose clones delay their repair, for tests, the delay, in milliseconds, as its one invariant setting, `runtime-adapter.repair-delay-ms`, so that the class is one of its own; - NVX's artifacts, with the initramfs that puts the guest agent in place of @@ -1488,11 +1494,19 @@ so NVX refuses a store whose path is too long for it. The capture boots the template's VM with the kernel command line `nvx_sandbox=1`, and for a template whose processors are not all online at -boot, `maxcpus=` with those that are, to which OpenVMM adds the platform tier's +boot, `maxcpus=` with those that are, then the template's kernel tuning +parameters, to which OpenVMM adds the platform tier's tokens, `nvx_snapshot_tier=platform` among them, so the worker of the guest agent captures the VM in [template mode](#template-mode) before it assembles any root, and the capture ends the VM. OpenVMM records the processors online at -boot, from which each restore brings a clone's processor target online. The +boot, from which each restore brings a clone's processor target online. NVX's +class refuses a kernel tuning parameter that is not one of +`nvx::host::template::KERNEL_TUNING_PARAMETERS`, `mitigations=off`, +`mitigations=auto`, `mitigations=auto,nosmt`, `no_timer_check`, `quiet`, and +`loglevel=0` to `loglevel=7`, and a parameter that two `--kernel-tuning` +options name, before the command creates the store. Every clone of the +template restores the template's command line, and with it the kernel +configuration that the parameters set as the template's kernel booted. The store checks what the VM left, and publishes the snapshot as its next generation, with the record of its capture and its compatibility key, whose kernel command line holds the platform tier's tokens @@ -1934,7 +1948,7 @@ Each command of `runtime-adapter-host` but `stage`, `list`, `inspect`, and mapping, the launch sends no more of the table, nor `Start`, stops the guest, which tears down the root, waits for the VM to end, abandons the launch, and checks that the VM ended with a success and a complete teardown. -- `create STORE --guest-agent PATH --distro FORMAT:PATH --distro-identity UUID [--hypervisor NAME] [--cpu-profile PROFILE] [--processors N] [--boot-processors B] [--memory MIB] [--memory-capacity MIB] [--scratch-length BYTES] [--network FILE]` +- `create STORE --guest-agent PATH --distro FORMAT:PATH --distro-identity UUID [--hypervisor NAME] [--cpu-profile PROFILE] [--processors N] [--boot-processors B] [--memory MIB] [--memory-capacity MIB] [--scratch-length BYTES] [--network FILE] [--kernel-tuning PARAMETER]...` serves a [template](#templates) of the guest agent PATH from the template store STORE, which it creates if needed: a generation of the template's class that the store holds, and that is eligible at the host's time, or else @@ -1947,12 +1961,14 @@ Each command of `runtime-adapter-host` but `stage`, `list`, `inspect`, and RAM, 256 by default, a RAM capacity of MIB mebibytes, its RAM by default, and a dormant scratch of BYTES bytes, 64 MiB by default. It has one NIC under the policy of the [network](#networks) FILE, unless the - policy lets nothing through, and none without a network. The command reads - the network before anything else, as a launch does, and checks the class - before it creates the store, then prints the generation once the store has - committed it, and [evicts](#eviction) the generations of the class that it - supersedes. -- `refresh STORE --guest-agent PATH --distro FORMAT:PATH --distro-identity UUID [--hypervisor NAME] [--cpu-profile PROFILE] [--processors N] [--boot-processors B] [--memory MIB] [--memory-capacity MIB] [--scratch-length BYTES] [--network FILE]` + policy lets nothing through, and none without a network. Its kernel boots + with the kernel tuning parameter PARAMETER of each `--kernel-tuning`, in + their order, such as `mitigations=off`, and so does each of its clones. The + command reads the network before anything else, as a launch does, and checks + the class before it creates the store, then prints the generation once the + store has committed it, and [evicts](#eviction) the generations of the class + that it supersedes. +- `refresh STORE --guest-agent PATH --distro FORMAT:PATH --distro-identity UUID [--hypervisor NAME] [--cpu-profile PROFILE] [--processors N] [--boot-processors B] [--memory MIB] [--memory-capacity MIB] [--scratch-length BYTES] [--network FILE] [--kernel-tuning PARAMETER]...` takes the options of `create`, and captures a new generation of the template's class in STORE, whether or not the store holds an eligible one, then prints it once the store has committed it, and evicts the generations of @@ -2221,7 +2237,8 @@ is not UTF-8 text, and admits by its prefix one that the kernel splits in two at a vertical tab or at 0xa0, and template mode: a command line in which the kernel finds no platform tier boots cold, whatever else it holds, and one with it boots a template's VM if it holds the tier's tokens -alone, between any of the kernel's whitespace, and is refused otherwise, for a +alone, between any of the kernel's whitespace, the kernel tuning parameters +among them, and is refused otherwise, for a parameter outside the allowlist at its start, in its middle, or at its end, or within a platform token's value, after a vertical tab or the byte 0xa0, which the kernel takes for whitespace and a split at ASCII whitespace does not, for a @@ -2962,6 +2979,7 @@ artifacts, and creates a template of it in a template store of its own: | `a_network_that_lets_nothing_through_has_no_nic` | A network that denies egress, ingress, and host-loopback access, without a proxy | A captured template without a NIC | | `the_networks_of_every_policy_at_one_guest_s_address_share_one_template` | Host-loopback access both ways, with a forward of port 18080 of host loopback to the guest's port 8080; then, at the same guest's address, each network that lets something through: egress allowed, with outbound host-loopback access or without, one TCP rule to the gateway at port 8080, with outbound host-loopback access, a proxy at port 3128 of host loopback, ingress allowed, host-loopback access both ways, and that forward | A captured template with a NIC, under the complete deny-all policy, whose key's command line names the NIC's addresses, but neither a DNS server nor a publication of the guest's listeners; then each creation `existing`, with the same generation, key, and class, without a capture | | `a_class_that_no_capture_takes_is_refused_before_the_store` | 3 processors, then 2 processors with 4 online at boot | NVX's refusal of each class, with status 1, before the store exists | +| `kernel_tuning_that_no_capture_takes_is_refused_before_the_store` | The kernel tuning parameter `nokaslr`, then `mitigations=on`, then `mitigations=off` with `mitigations=auto` | NVX's refusal of each class, with status 1, which names the parameter, before the store exists | | `a_refresh_captures_and_retires_the_generations_that_it_supersedes` | A creation, then two refreshes, the second while a clone leases the second generation, then a creation once the clone released it | `list` and `inspect` of the first generation; each refresh captures a new generation, although the store holds an eligible one, and its eviction retires the first generation, and leaves the leased second one, tombstoned, with its payload; the creation serves the third without a capture, and its eviction deletes the second | | `a_removal_reports_busy_while_leases_keep_the_generation` | A creation, two clones' leases of its generation, a removal, a second creation, then a removal after each release | `list` and `inspect` with the leases; the removal tombstones the generation, which keeps its payload and takes no new lease, and reports busy, with status 7 and its leases; the second creation captures a new generation, and leaves the first to its leases; a removal reports busy while a lease remains, and deletes the generation once none does; a second removal finds it retired, one of a generation that the store never published fails, and a generation that no lease keeps goes at once | | `a_refresh_refuses_a_class_that_no_capture_takes_before_the_store` | A refresh of 3 processors | NVX's refusal of the class, with status 1, before the store exists | @@ -3018,6 +3036,7 @@ directory that the starts authorize: | `one_generation_serves_clones_of_each_upper_and_memory_target` | Two at once, over a scratch image at 256 MiB, and in RAM at 512 MiB with untrusted lowers, then one over a scratch image at 1 GiB | Each started, by the host's monotonic clock, with its own lease, which the store lists with the clone's sandbox, its memory target online, its upper, overlay mode, and image's contents, and the events of its activation in order; a stop releases each lease; the generation's snapshot unchanged, and its payload with nothing but the capture's files once the clones stopped | | `one_generation_serves_clones_of_each_processor_count_from_its_boot_online_count_to_its_capacity` | Of a template of 4 processors, 1 of them online at boot: in turn, one without a processor target, one of 2, and one of 4; then targets of 3 and 8 | Each started, with the CPUs of its target online, CPU 0 alone by default, of the 4 possible ones, and the CPUs beyond the first that the time ABI's record of the restore names as those that the restore brought online, none by default, and the events of its activation in order; then for each of the others, status 8, with NVX's reason, and the record of a failure of the request, at the plan, without a VM, by the evidence of invalid targets, which quarantines nothing; the lease released, and the generation's snapshot unchanged | | `a_generation_refuses_clones_of_fewer_processors_than_its_template_booted_with` | Of a template of 4 processors, 2 of them online at boot: a target of 1, then in turn one without a processor target, and one of 4 | Status 8, with NVX's reason, and the record of a failure of the request, at the plan, without a VM, by the evidence of invalid targets; then each started, with CPUs 0 and 1 online by default, which the restore brought none of online, and all 4 online for the target of 4, of which the restore brought CPUs 2 and 3 online | +| `a_template_s_clones_run_with_its_kernel_tuning_parameters` | Of a template whose kernel boots with the kernel tuning parameter `mitigations=off`: one | The template's key, whose kernel command line holds `mitigations=off` once; then the clone started, whose `/proc/cmdline`, which an execution reads, holds it once, and is the key's command line, token for token; a stop releases the lease | | `one_generation_serves_clones_of_either_format_over_either_upper` | Of a template whose placeholder is a GPT disk of two layers, longer than the EROFS image, four at once, each image shorter than the placeholder: the EROFS image over a scratch image and in RAM, a GPT disk of one layer, of the EROFS image's length, in RAM, and one of three layers, as short as its layout allows, over a scratch image, with a bound that its first partition meets exactly; then the disk of one layer, with a bound one byte smaller than its first partition | Each started, with its upper and the overlay's options; the EROFS image's root, without a first partition; each GPT disk's layers, ext4 file systems mounted read-only once each, from the partitions that its own table names, stacked in order, and the bytes of its own first partition; the image's read-only disk at the placeholder's length, and the scratch device at the template's scratch length, each of which the guest reads to its end; for a clone in RAM, no file of its sandbox as long as the template's scratch, and on Linux `--restore-absent-scratch` and no `scratch` block device among its VMM's arguments, which name one over a scratch image; then status 8, and the record of a failure of the request, at the activation, once the VM exited, by the evidence of an unusable root, with the guest's status 54; nothing quarantined, and the lease released, with the records | | `a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts` | An EROFS image and a GPT disk 4 KiB longer, and an EROFS image 100 bytes longer, than the placeholder, a scratch image of twice the template's scratch, memory targets of 2 GiB and of 320 MiB, unstaged images, and a generation that the store does not hold, then the generation once its key is gone | For those that NVX's plan refuses, status 8, with NVX's reason, and the record of a failure of the request, at the plan, without a VM, by the evidence of a length that the request supplied, of the image's geometry, which OpenVMM cannot attach, or of invalid targets, which quarantines nothing; for unstaged images, status 1, without a record; for the generation, status 10, with the record of a selection that names no template, which the store never published, without a lease; any lease released, and no VMM's log or record in the sandbox; for the generation without its key, status 8, and the record of a failure of the template, at the plan, by the evidence of a malformed generation, which quarantines the generation, so that the store refuses a later start's selection, with status 10, as `ineligible`, by the evidence of its tombstone, without a lease | | `a_clone_whose_activation_fails_ends_before_it_is_ready` | A scratch image of zeros, a journal that the record forbids, and, through NVX's restore sequence, a ping, a start, or the start of an execution before the `Activate` | Status 8, no `Ready`, and the record of a failure, at the activation, once the VM exited: of the request, by the evidence of an unusable root, with the guest's status 54, or, before the `Activate`, of an unknown cause, by the protocol's, with 51, which the sandbox records until the release; nothing quarantined, and the lease released, with the records | @@ -3124,6 +3143,7 @@ layered roots, captures of templates, and the starts and runs of their clones. | Target | Scenario | Proves | | --- | --- | --- | | `template_capture` | `a_capture_takes_its_class_and_inputs_from_the_request` | A capture's request: the plan of the placeholder over RAM, with the request's network, the placeholder's binding, the policy revision, the artifacts, and the class, which the network names, and the delay of a template's clones' repair, as its invariant setting | +| `template_capture` | `a_capture_takes_its_kernel_tuning_from_the_request` | A capture's class takes the request's kernel tuning parameters, in their order, which, order included, name a class of its own, and nothing else of the capture changes; NVX's class refuses a parameter that is not NVX's, and one given twice | | `template_capture` | `the_host_program_refuses_a_repair_delay_that_it_cannot_read` | `create` and `refresh` refuse a value of `RUNTIME_ADAPTER_REPAIR_DELAY` that sets no delay with status 2 before they create anything | | `template_capture` | `the_host_program_refuses_a_template_before_it_creates_anything` | `create` refuses a network that the plan refuses, or that it cannot read, with status 1 before it needs NVX's artifacts or creates the store | | `template_operations` | `a_refresh_refuses_a_template_before_it_creates_anything` | `refresh` refuses a network that the plan refuses with status 1 before it needs NVX's artifacts or creates the store, as `create` does | @@ -3244,6 +3264,7 @@ layered roots, captures of templates, and the starts and runs of their clones. | `vm_template` | `a_removal_reports_busy_while_leases_keep_the_generation` | `remove` tombstones a leased generation, keeps its payload, and reports busy, with status 7 and its leases, until no lease remains, then deletes it | | `vm_template` | `a_template_whose_processors_are_all_online_at_boot_limits_none` | `create` captures a template whose processors are all online at boot without `maxcpus=`, and whose key records none online at boot | | `vm_template` | `a_template_without_a_network_has_no_nic` | `create` captures a template without a NIC, whose key holds what it asked for, its processor online at boot among it, and serves it again without a capture | +| `vm_template` | `kernel_tuning_that_no_capture_takes_is_refused_before_the_store` | `create` of kernel tuning parameters that no capture takes, one that is not NVX's, another value of one, or one given twice, fails with status 1, naming the parameter, before the store exists | | `vm_template` | `the_networks_of_every_policy_at_one_guest_s_address_share_one_template` | `create` of a network of any policy captures one template, whose NIC the capture attaches under the complete deny-all policy, and the creations of the networks of every other policy at the same guest's address serve it without a capture | | `vm_clone` | `a_clone_binds_the_resolver_of_its_policy_s_dns_server_read_only` | A clone of a network with egress allowed binds a resolver file that names the gateway's DNS server read-only at `/etc/resolv.conf`, and a TCP connection to the server's DNS port opens, which the gateway's DNS proxy accepts; one whose resolver cannot be bound ends with status 57 | | `vm_clone` | `a_clone_maps_an_aggregate_of_directories_and_files_beneath_its_root_before_its_sandbox_starts` | A clone maps an aggregate of host directories and files beneath its root before its sandbox starts, each with its access, a hidden file, and a hidden directory that shows a file again, and writes reach the host's directories and file through the writable ones alone | @@ -3270,6 +3291,7 @@ layered roots, captures of templates, and the starts and runs of their clones. | `vm_clone` | `a_selection_that_names_no_template_leases_nothing` | A start of a template store that does not exist, or of a generation that `remove` retired, ends with status 10 and the refusal's record, without a lease or a VM | | `vm_clone` | `a_start_refuses_an_export_whose_mapped_objects_changed_before_any_vmm_starts` | An export whose mapped file, or pinned directory, another object replaced once NVX's plan checked the clone is refused with status 8 at the clone's launch, before any VMM starts | | `vm_clone` | `a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts` | Images, scratch images, and memory targets that the template cannot serve are refused with status 8 before any VMM starts, unstaged images with status 1, and a generation that the store does not hold with status 10 | +| `vm_clone` | `a_template_s_clones_run_with_its_kernel_tuning_parameters` | A template whose kernel boots with `mitigations=off` holds it in its key's command line, and its clone runs with it: the clone's `/proc/cmdline` is the template's command line | | `vm_clone` | `a_template_without_a_nic_serves_clones_without_a_network` | A template without a NIC serves clones without a network, or with one that lets nothing through, and refuses those of a network with a NIC with status 8 | | `vm_clone` | `a_tombstone_leaves_the_running_clone_of_its_generation_to_its_lease` | A tombstone refuses new clones of its generation with status 10, while the running clone serves on, and releases its lease once it stops | | `vm_clone` | `clones_at_once_keep_their_own_uppers_and_machine_ids` | Clones at once keep their own uppers, and their own machine IDs, generated, distinct, and bound read-only, or the image's own | diff --git a/tests/runtime-adapter/scenarios.txt b/tests/runtime-adapter/scenarios.txt index 6b98922ac..4b0743e55 100644 --- a/tests/runtime-adapter/scenarios.txt +++ b/tests/runtime-adapter/scenarios.txt @@ -7,6 +7,7 @@ # run and pass. The README's "Stage scenarios" says what each scenario proves. template_capture a_capture_takes_its_class_and_inputs_from_the_request +template_capture a_capture_takes_its_kernel_tuning_from_the_request template_capture the_host_program_refuses_a_repair_delay_that_it_cannot_read template_capture the_host_program_refuses_a_template_before_it_creates_anything @@ -136,6 +137,7 @@ vm_template a_refresh_refuses_a_class_that_no_capture_takes_before_the_store vm_template a_removal_reports_busy_while_leases_keep_the_generation vm_template a_template_whose_processors_are_all_online_at_boot_limits_none vm_template a_template_without_a_network_has_no_nic +vm_template kernel_tuning_that_no_capture_takes_is_refused_before_the_store vm_template the_networks_of_every_policy_at_one_guest_s_address_share_one_template vm_clone a_clone_binds_the_resolver_of_its_policy_s_dns_server_read_only @@ -163,6 +165,7 @@ vm_clone a_run_whose_clone_s_vm_exits_during_its_execution_ends_with_the_clone_s vm_clone a_selection_that_names_no_template_leases_nothing vm_clone a_start_refuses_an_export_whose_mapped_objects_changed_before_any_vmm_starts vm_clone a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts +vm_clone a_template_s_clones_run_with_its_kernel_tuning_parameters vm_clone a_template_without_a_nic_serves_clones_without_a_network vm_clone a_tombstone_leaves_the_running_clone_of_its_generation_to_its_lease vm_clone clones_at_once_keep_their_own_uppers_and_machine_ids diff --git a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs index bdb464ccf..a2b121337 100644 --- a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs +++ b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs @@ -73,7 +73,8 @@ //! end, abandons it, and checks that the VM ended with success and a complete teardown. //! - `runtime-adapter-host create STORE --guest-agent PATH --distro FORMAT:PATH --distro-identity //! UUID [--hypervisor NAME] [--cpu-profile PROFILE] [--processors N] [--boot-processors B] -//! [--memory MIB] [--memory-capacity MIB] [--scratch-length BYTES] [--network FILE]` serves a +//! [--memory MIB] [--memory-capacity MIB] [--scratch-length BYTES] [--network FILE] +//! [--kernel-tuning PARAMETER]...` serves a //! template of the guest agent PATH from the template store STORE, an //! `nvx::host::template::Store`, which it creates if needed: a generation of the template's //! class that the store holds, and that is eligible at the host's time, or else a new one, @@ -90,7 +91,11 @@ //! its network, under the complete deny-all policy, whatever the network's own, which each //! clone's restore replaces with the clone's, unless the network's policy lets nothing through, //! and none without a network: the networks of every policy at one guest's address share one -//! class, and so one template. The creation +//! class, and so one template. Its kernel boots with the kernel tuning parameters that each +//! `--kernel-tuning` names, in their order, such as `mitigations=off`, so that its clones run +//! with them: NVX's class takes them, and refuses one that is not one of +//! `nvx::host::template::KERNEL_TUNING_PARAMETERS`, and a parameter that two of them name, such +//! as `mitigations=` with two values. The creation //! reads the network before anything else, then stages the placeholder as `stage` stages an //! image, verifying it explicitly, which hashes it only when a manifest is beside it, and checks //! the class before it creates the store. A capture boots the template's VM through NVX's @@ -612,7 +617,7 @@ const USAGE: &str = concat!( " [--processors N]\n", " [--boot-processors N] [--memory MIB] [--memory-capacity MIB]", " [--scratch-length BYTES]\n", - " [--network FILE]\n", + " [--network FILE] [--kernel-tuning PARAMETER]...\n", " runtime-adapter-host list STORE\n", " runtime-adapter-host (inspect | remove) STORE GENERATION\n", " runtime-adapter-host (reattach | stop) STORE [--guest-build BUILD]", @@ -904,6 +909,8 @@ struct Template { scratch_length: u64, /// The file of the template's network, if it has one. network: Option, + /// The kernel tuning parameters of the template, in their order. + kernel_tuning: Vec, /// The delay of each clone's repair, for tests, if `RUNTIME_ADAPTER_REPAIR_DELAY` sets one. repair_delay: Option, } @@ -1032,6 +1039,8 @@ struct Options { memory_capacity: Option, /// `--scratch-length`. scratch_length: Option, + /// `--kernel-tuning`, in order. + kernel_tuning: Vec, /// `--boot-console`: whether the clone's launch attaches a boot console that listens. boot_console: Option, } @@ -1310,6 +1319,7 @@ fn parse(arguments: Vec) -> Result { memory_capacity: options.memory_capacity.unwrap_or(memory), scratch_length: options.scratch_length.unwrap_or(DEFAULT_SCRATCH_LENGTH), network: options.network, + kernel_tuning: options.kernel_tuning, repair_delay: repair_delay_from_env()?, }), serving: if name == "refresh" { @@ -1528,6 +1538,7 @@ fn parse_option( format!("--scratch-length takes a positive number of bytes, not {value:?}") })?); } + ("create", "--kernel-tuning") => options.kernel_tuning.push(text(value)?), (_, "--guest-build") if name != "stage" && options.guest_build.is_none() => { options.guest_build = Some(text(value)?); } @@ -2279,6 +2290,7 @@ fn create( memory_capacity: template.memory_capacity, scratch_length: template.scratch_length, network, + kernel_tuning: template.kernel_tuning.clone(), repair_delay: template.repair_delay, }; // The placeholder is staged as `stage` stages an image: its explicit verification may hash it, diff --git a/tests/runtime-adapter/src/capture.rs b/tests/runtime-adapter/src/capture.rs index 8267758db..3f043ae95 100644 --- a/tests/runtime-adapter/src/capture.rs +++ b/tests/runtime-adapter/src/capture.rs @@ -5,7 +5,8 @@ //! //! A [`Request`] holds what the host program's options choose of a template: its placeholder, the //! backend, the CPU profile, the processors and those online at boot, the RAM and its capacity, -//! the length of the dormant scratch, and the network, if any. [`Request::capture`] returns NVX's +//! the length of the dormant scratch, the network, if any, and the kernel tuning parameters, if +//! any. [`Request::capture`] returns NVX's //! `Capture` of it: the adapter's plan of the placeholder, which the host program plans as it //! plans an image, with the network, the placeholder's binding, which the host program stages as //! it stages an image, NVX's artifacts with the initramfs of the adapter's VMs, and the class of @@ -23,6 +24,9 @@ //! - the network, of which the class takes the NIC alone, at the guest's address, under the //! complete deny-all policy, whatever the network's own, which each clone's restore replaces //! with the clone's; +//! - the kernel tuning parameters with which the template's kernel boots, such as +//! `mitigations=off`, so that the template's clones run the kernel configuration of the cold +//! sandboxes that boot with them: Linux applies some, such as `mitigations=`, once, as it boots; //! - for tests, the delay of each clone's repair, if [`REPAIR_DELAY_VARIABLE`] sets one, as the //! invariant setting [`REPAIR_DELAY_SETTING`], and no other invariant setting, so that a //! template whose clones delay their repair is of a class of its own. @@ -32,7 +36,8 @@ //! if any; see [`crate::initramfs`]. NVX gives it the kernel command //! line `nvx_sandbox=1`, with which NVX's `/init` hands it to the guest agent's PID 1 of a layered //! root, and for a template whose processors are not all online at boot, `maxcpus=` with those -//! that are, and OpenVMM adds the platform tier's tokens, `nvx_snapshot_tier=platform` among them: +//! that are, then the template's kernel tuning parameters, and OpenVMM adds the platform tier's +//! tokens, `nvx_snapshot_tier=platform` among them: //! the worker then captures the VM before it assembles any root, and the capture ends the VM. use std::env; @@ -116,6 +121,11 @@ pub struct Request { /// The network at whose guest's address the template has its fixed NIC, under the complete /// deny-all policy, or `None` for a template without a NIC. pub network: Option, + /// The kernel tuning parameters with which the template's kernel boots, in their order, as + /// `CaptureClass::kernel_tuning` holds them: each one of + /// `nvx::host::template::KERNEL_TUNING_PARAMETERS`, such as `mitigations=off`, and each + /// parameter at most once, which `CaptureClass::check` checks. + pub kernel_tuning: Vec, /// How long each clone of the template delays its repair, for tests, or `None` for no delay. pub repair_delay: Option, } @@ -173,7 +183,7 @@ impl Request { scratch: geometry(scratch, self.scratch_length)?, network: self.network.clone(), invariants, - kernel_tuning: Vec::new(), + kernel_tuning: self.kernel_tuning.clone(), }; Ok(Capture { class, diff --git a/tests/runtime-adapter/src/template.rs b/tests/runtime-adapter/src/template.rs index 3263886e1..086658a47 100644 --- a/tests/runtime-adapter/src/template.rs +++ b/tests/runtime-adapter/src/template.rs @@ -49,10 +49,13 @@ use nvx::protocol::cmdline::{self, Parameter}; /// platform snapshot admits it in the VM's kernel command line. /// /// The allowlist holds `earlycon=xe9`, `console=hvc0`, `console=hvc1`, `reboot=t`, `panic=-1`, -/// `nvx_sandbox=1`, `nvx_config=0xd0010000,65536`, [`PLATFORM_TIER`], and the control tty's token, -/// `nvx_control_tty=hvc2`, whole, and the tokens that start with `nr_cpus=`, `maxcpus=`, -/// `virtio_mmio.device=`, `virtnet_ip=`, `virtnet_mask=`, `virtnet_gw=`, `virtnet_dns=`, -/// `virtnet_ip6=`, or `virtnet_gw6=`, whose values OpenVMM writes or checks. +/// `nvx_sandbox=1`, `nvx_config=0xd0010000,65536`, [`PLATFORM_TIER`], the control tty's token, +/// `nvx_control_tty=hvc2`, and the kernel tuning parameters `mitigations=off`, `mitigations=auto`, +/// `mitigations=auto,nosmt`, `no_timer_check`, `quiet`, and `loglevel=0` to `loglevel=7`, whole, +/// and the tokens that start with `nr_cpus=`, `maxcpus=`, `virtio_mmio.device=`, `virtnet_ip=`, +/// `virtnet_mask=`, `virtnet_gw=`, `virtnet_dns=`, `virtnet_ip6=`, or `virtnet_gw6=`, whose values +/// OpenVMM writes or checks. OpenVMM also refuses a command line that repeats a kernel tuning +/// parameter, which this check of single tokens leaves to it. /// /// It is OpenVMM's own check, `platform_command_line_token_is_invariant` of /// `openvmm_helpers::snapshot::microvm`, compiled from the excerpts of OpenVMM's sources that diff --git a/tests/runtime-adapter/src/template/tests.rs b/tests/runtime-adapter/src/template/tests.rs index a24ffcb6a..fe3eacbdb 100644 --- a/tests/runtime-adapter/src/template/tests.rs +++ b/tests/runtime-adapter/src/template/tests.rs @@ -42,9 +42,27 @@ const WHOLE: [&str; 9] = [ "nvx_control_tty=hvc2", ]; +/// The kernel tuning parameters that the platform tier's allowlist holds whole too, which a +/// template boots with as its adapter's cold sandboxes do. +const TUNING: [&str; 13] = [ + "mitigations=off", + "mitigations=auto", + "mitigations=auto,nosmt", + "no_timer_check", + "quiet", + "loglevel=0", + "loglevel=1", + "loglevel=2", + "loglevel=3", + "loglevel=4", + "loglevel=5", + "loglevel=6", + "loglevel=7", +]; + #[test] fn the_platform_tier_s_allowlist_is_openvmm_s() { - for token in WHOLE { + for token in WHOLE.into_iter().chain(TUNING) { assert!(is_platform_token(token.as_bytes()), "{token}"); } for token in [ @@ -105,6 +123,16 @@ fn the_platform_tier_s_allowlist_is_openvmm_s() { "driver_async_probe=virtio_mmio", "init=/bin/sh", "rdinit=/bin/sh", + // Other values of the kernel tuning parameters, and other spellings of them. + "mitigations=on", + "mitigations=", + "mitigations", + "loglevel=8", + "loglevel=07", + "loglevel", + "quiet=1", + "no_timer_check=1", + "no-timer-check", "nokaslr", "--", "--=1", @@ -208,6 +236,26 @@ fn a_template_s_command_line_holds_the_platform_tier_s_tokens_alone() { assert_eq!(boot(&latin1), Ok(Boot::Template { gateway: None })); } +#[test] +fn a_template_s_command_line_may_hold_kernel_tuning_parameters() { + // NVX gives a template the kernel tuning parameters of its class after its own tokens. + let tuned = TEMPLATE.replacen( + "nvx_sandbox=1", + "nvx_sandbox=1 mitigations=off no_timer_check quiet loglevel=0", + 1, + ); + assert_eq!(boot(tuned.as_bytes()), Ok(Boot::Template { gateway: None })); + for parameter in TUNING { + for command_line in with(parameter) { + assert_eq!( + boot(command_line.as_bytes()), + Ok(Boot::Template { gateway: None }), + "{command_line:?}" + ); + } + } +} + #[test] fn template_mode_refuses_a_gateway_that_openvmm_would_not_render() { for (gateway, problem) in [ @@ -243,6 +291,8 @@ fn template_mode_refuses_a_gateway_that_openvmm_would_not_render() { fn template_mode_refuses_a_token_outside_the_allowlist() { for token in [ "nokaslr", + "mitigations=on", + "loglevel=8", "init=/bin/sh", "console=ttyS0", "nvx_config=0xd0010000,65537", diff --git a/tests/runtime-adapter/tests/template_capture.rs b/tests/runtime-adapter/tests/template_capture.rs index 961a78eae..527229520 100644 --- a/tests/runtime-adapter/tests/template_capture.rs +++ b/tests/runtime-adapter/tests/template_capture.rs @@ -8,13 +8,14 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; -use nvx::Hypervisor; use nvx::host::artifacts::Artifacts; use nvx::host::sandbox::SandboxNetwork; use nvx::host::sandbox::distro::DISTRO_POLICY_REVISION; use nvx::host::store::{ArtifactIdentity, RuntimeIdentity}; +use nvx::host::template::CaptureClass; use nvx::protocol::root::{ImageFormat, RootImage}; use nvx::uuid::Uuid; +use nvx::{ErrorKind, Hypervisor}; use runtime_adapter::capture::{ DEFAULT_BOOT_PROCESSORS, DEFAULT_CPU_PROFILE, DEFAULT_PROCESSORS, DEFAULT_SCRATCH_LENGTH, LOGICAL_BLOCK_SIZE, REPAIR_DELAY_SETTING, REPAIR_DELAY_VARIABLE, Request, geometry, @@ -88,6 +89,7 @@ fn template(distro: &Path, network: Option) -> Request { memory_capacity: 256 * MIB, scratch_length: DEFAULT_SCRATCH_LENGTH, network, + kernel_tuning: Vec::new(), repair_delay: None, } } @@ -246,6 +248,69 @@ fn a_capture_takes_its_class_and_inputs_from_the_request() { assert!(presented.physical_block_size.is_power_of_two()); } +#[test] +fn a_capture_takes_its_kernel_tuning_from_the_request() { + let directory = directory("template-capture-tuning"); + let distro = directory.join("placeholder.erofs"); + fs::write(&distro, vec![0; 2 * MIB as usize]).unwrap(); + let runtime = runtime(&directory); + let initramfs = directory.join("template.cpio.gz"); + let staged = Staged::stage(&distro, ImageFormat::Erofs, identity(), None).unwrap(); + let capture = |kernel_tuning: &[&str]| { + let request = Request { + kernel_tuning: kernel_tuning + .iter() + .map(|parameter| (*parameter).to_owned()) + .collect(), + ..template(&distro, None) + }; + request + .capture( + staged.distro.binding.clone(), + &runtime, + "build", + &initramfs, + &directory, + ) + .unwrap() + }; + + // The class takes the request's kernel tuning parameters, in their order, and nothing else + // changes, but the class, which is one of its own. + let untuned = capture(&[]); + assert!(untuned.class.kernel_tuning.is_empty()); + let tuned = capture(&["mitigations=off", "quiet", "loglevel=0"]); + assert_eq!( + tuned.class.kernel_tuning, + ["mitigations=off", "quiet", "loglevel=0"] + ); + assert_eq!(tuned.adapter, untuned.adapter); + assert_eq!( + CaptureClass { + kernel_tuning: Vec::new(), + ..tuned.class.clone() + }, + untuned.class + ); + let identity = tuned.class.identity().unwrap(); + assert_ne!(identity, untuned.class.identity().unwrap()); + let reordered = capture(&["mitigations=off", "loglevel=0", "quiet"]); + assert_ne!(reordered.class.identity().unwrap(), identity); + + // NVX's class refuses a parameter that no capture takes, and a parameter given twice, which + // the host program checks before it creates the store. + for refused in [ + &["nokaslr"][..], + &["mitigations=on"][..], + &["no-timer-check"][..], + &["quiet", "quiet"][..], + &["mitigations=off", "mitigations=auto"][..], + ] { + let error = capture(refused).class.identity().unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{refused:?}: {error}"); + } +} + #[test] fn the_host_program_refuses_a_template_before_it_creates_anything() { let directory = directory("template-capture-command"); diff --git a/tests/runtime-adapter/tests/vm_clone.rs b/tests/runtime-adapter/tests/vm_clone.rs index 59a3bc234..0e586bbc5 100644 --- a/tests/runtime-adapter/tests/vm_clone.rs +++ b/tests/runtime-adapter/tests/vm_clone.rs @@ -25,6 +25,9 @@ //! NVX's plan refuses a count that a microVM cannot have, and one beyond the capacity, before //! any VMM starts. The generation of a template that boots with 2 of its 4 processors online //! refuses a clone of 1, and serves those of 2, its default, and 4. +//! - A template whose kernel boots with the kernel tuning parameter `mitigations=off`, which Linux +//! applies once, as it boots, holds it in its key's kernel command line, and its clone runs with +//! it: the clone's `/proc/cmdline` is the template's command line, which the restore keeps. //! - One generation serves clones of either format over either upper, at once: the platform's //! EROFS image, and GPT disks of other layouts than the template's placeholder, a GPT disk too, //! and than each other's. The guest of a GPT clone rereads its disk's partition table before it @@ -2891,6 +2894,42 @@ fn a_generation_refuses_clones_of_fewer_processors_than_its_template_booted_with assert!(template.leases().is_empty()); } +#[test] +#[ignore = "boots VMs; see the module documentation for what they need"] +fn a_template_s_clones_run_with_its_kernel_tuning_parameters() { + // Linux applies `mitigations=` once, as it boots, so a clone gets it from its template alone. + let tuning = ["--kernel-tuning", "mitigations=off"]; + let template = Template::captured("cl-tune", None, None, &tuning, &[]); + let key = template + .store() + .read_key(&template.generation()) + .expect("the store holds the template's key"); + let count = |command_line: &str| { + command_line + .split_ascii_whitespace() + .filter(|token| *token == "mitigations=off") + .count() + }; + assert_eq!(count(key.command_line()), 1, "{}", key.command_line()); + + // The clone's kernel command line, the template's, which the restore keeps, holds it. + let mut cloned = template.start("cl-t", StartOf::default(), 0); + cloned.assert_started(); + let lines = cloned.script("cat /proc/cmdline"); + let [command_line] = lines.as_slice() else { + panic!("{lines:?}"); + }; + assert_eq!(count(command_line), 1, "{command_line}"); + assert_eq!( + command_line.split_ascii_whitespace().collect::>(), + key.command_line() + .split_ascii_whitespace() + .collect::>() + ); + cloned.stop(); + assert!(template.leases().is_empty()); +} + #[test] #[ignore = "boots VMs; see the module documentation for what they need"] fn one_generation_serves_clones_of_either_format_over_either_upper() { diff --git a/tests/runtime-adapter/tests/vm_template.rs b/tests/runtime-adapter/tests/vm_template.rs index 726a22d96..44d99e540 100644 --- a/tests/runtime-adapter/tests/vm_template.rs +++ b/tests/runtime-adapter/tests/vm_template.rs @@ -17,7 +17,7 @@ //! template's class then holds no transaction, and records no launch, and a second creation serves //! the generation without a capture, as does the creation of a network of any other policy at the //! same guest's address, whose class is the same. The host program refuses a class that no capture -//! takes before it creates the store. +//! takes before it creates the store, such as one of kernel tuning parameters that are not NVX's. //! //! Other scenarios run the host program's operations on the generations of a store: `refresh`, //! which captures a generation of a class whose store holds an eligible one already, and retires @@ -619,6 +619,31 @@ fn a_class_that_no_capture_takes_is_refused_before_the_store() { } } +#[test] +#[ignore = "needs NVX's artifacts; see the module documentation"] +fn kernel_tuning_that_no_capture_takes_is_refused_before_the_store() { + // A parameter that is not one of NVX's kernel tuning parameters, and one given twice. + for (tuning, refused) in [ + (&["nokaslr"][..], "\"nokaslr\""), + (&["mitigations=on"][..], "\"mitigations=on\""), + (&["mitigations=off", "mitigations=auto"][..], "mitigations"), + ] { + let mut templates = Templates::new("tpl-tune"); + let mut options = templates.options(&Template::new(Network::None)); + for parameter in tuning { + options.extend([OsString::from("--kernel-tuning"), OsString::from(parameter)]); + } + let options: Vec<&OsStr> = options.iter().map(OsString::as_os_str).collect(); + templates.sandbox.create(&options, FAILED); + let stderr = templates.sandbox.stderr(); + assert!( + stderr.contains("kernel tuning parameter") && stderr.contains(refused), + "{tuning:?}: {stderr}" + ); + assert!(!templates.sandbox.store.exists(), "{tuning:?}"); + } +} + /// Returns the store generation `number`. fn generation(number: u64) -> StoreGeneration { StoreGeneration::new(NonZeroU64::new(number).unwrap()) From d48786283feb36ac38cc1327968d60d8ff926a5e Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Mon, 9 Nov 2026 12:07:44 -0800 Subject: [PATCH 5/5] doc: map the kernel tuning on platform command lines in the OpenVMM roadmap Maps NVX's new pin, b4028c1aa, the head of microsoft/openvmm#5001, at position 235, after position 234, f41a1ecfc, the head of feature/microvm/dev, which microsoft/openvmm#4997 merged. The pin is 104 commits past the mapped tip, and positions 132-235 stay unassigned. Position 235 makes the platform tier's check of a snapshot's command line, which PR19's position 093 adds and PR20's position 097, PR23's position 116, and positions 147 and 168 change, admit a closed set of kernel tuning parameters in its allowlist, and refuse a command line that repeats one of them, after position 097's check of the boot-online processor count. It adds tests beside position 093's, and extends the --snapshot-tier entry that position 093 adds to the CLI reference of PR15's position 064. Replaying it on upstream main conflicts in both of its files, since upstream main lacks the microVM snapshot module of PR17's position 074, the tiers of position 093, and the microVM CLI of position 064, so it follows PR15, PR17, and PR19 upstream. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/openvmm-upstream-roadmap.md | 37 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/doc/openvmm-upstream-roadmap.md b/doc/openvmm-upstream-roadmap.md index 15db8d78d..6a13b56c3 100644 --- a/doc/openvmm-upstream-roadmap.md +++ b/doc/openvmm-upstream-roadmap.md @@ -8,10 +8,11 @@ reorganization kept it there; see [Upstream status](#upstream-status). - Mapped fork tip: `7677ad8d56a59864475012b63b6f01a22575c30d`, position 131. The graph, PR summary, and commit mapping below cover positions 001-131. -- NVX's pinned OpenVMM revision: `f41a1ecfc13379e501cf78e2a96585426f16c87e`, position 234, the - head of microsoft/openvmm#4997, whose commit follows position 233, `d501e5066`, the head of - `feature/microvm/dev`, which microsoft/openvmm#4996 merged after position 232, `6cb6ba87c`, - which microsoft/openvmm#4995 merged after position 231, `98e5b3ca5`, which +- NVX's pinned OpenVMM revision: `b4028c1aa775d8aafba1a170857acc032c7c86a1`, position 235, the + head of microsoft/openvmm#5001, whose commit follows position 234, `f41a1ecfc`, the head of + `feature/microvm/dev`, which microsoft/openvmm#4997 merged after position 233, `d501e5066`, + which microsoft/openvmm#4996 merged after position 232, `6cb6ba87c`, which + microsoft/openvmm#4995 merged after position 231, `98e5b3ca5`, which microsoft/openvmm#4994 merged with position 230 after position 229, `ed8fc0713`, which microsoft/openvmm#4993 merged after position 228, `d6addf8c1`, which microsoft/openvmm#4992 merged after position 227, `154548a0e`, which @@ -27,8 +28,8 @@ position 221 into positions 035 and 036, and combined v5.1 positions 222 and 223, the two commits of microsoft/openvmm#4984, into position 220. Positions 001-147 keep their positions, v5.1 positions 149-220 are positions 148-219, and the tree of position 220 is the tree of - `3f2ab4c2f`; see [The v6.0 reorganization](#the-v60-reorganization). The pin is 103 commits past - the mapped tip on the same first-parent chain. Positions 132-234 are not yet assigned to PRs; see + `3f2ab4c2f`; see [The v6.0 reorganization](#the-v60-reorganization). The pin is 104 commits past + the mapped tip on the same first-parent chain. Positions 132-235 are not yet assigned to PRs; see [Commits after the mapped tip](#commits-after-the-mapped-tip). - Upstream main: `ba105dd7f00499acc1862456c179c0930cb2c8f7` (2026-11-06), the fork base itself; see [Upstream main past the fork base](#upstream-main-past-the-fork-base). @@ -990,7 +991,8 @@ ba105dd7f -> unknown paired-scratch restore mode refused [232] -> clone's activation record carried in the restore packet [233] -> no-replace snapshot publication on every target [234] - -> f41a1ecfc (NVX pin) + -> kernel tuning admitted on platform command lines [235] + -> b4028c1aa (NVX pin) ``` For upstream review, PR01-PR06 and PR14 can be rebased independently onto current upstream main, @@ -1006,13 +1008,13 @@ reorganization folded the PR20 and PR12 fixups into the commits that they fix; s ## Commits after the mapped tip -The NVX pin extends the chain past position 131 without merges. None of the 103 later commits is a +The NVX pin extends the chain past position 131 without merges. None of the 104 later commits is a fixup of a mapped PR, and none is yet classified or assigned to PRs. Each of positions 132-186 combines the v2.4 commits that the table lists, as v3.0 positions 145-160 and 162-200 did, which developed one concept; "(part)" marks a v2.4 fixup that split across commits. Positions 187-191 followed the v3.0 reorganization, positions 192-205 are the ABI restart of the v4.0 reorganization, positions 206-211 followed it, positions 212-218 followed the v5.0 -reorganization, positions 219 and 220 followed the v5.1 rebase, and positions 221-234 followed the +reorganization, positions 219 and 220 followed the v5.1 rebase, and positions 221-235 followed the v6.0 reorganization, so they combine none, apart from position 220, which combines v5.1 positions 222 and 223. Each commit has the position that it had in v5.1 through position 147, and one less from v5.1 position 149 on; see @@ -1124,6 +1126,7 @@ at `e1ca353eb`, whose first 210 commits are those of `feature/microvm/release/v4 | 232 | `6cb6ba87c` | - | - | - | - | openvmm_helpers: refuse an unknown paired-scratch restore mode (microsoft/openvmm#4995) | | 233 | `d501e5066` | - | - | - | - | microvm: carry a clone's activation record in the restore packet (microsoft/openvmm#4996) | | 234 | `f41a1ecfc` | - | - | - | - | openvmm_helpers: don't publish a snapshot over an empty directory (microsoft/openvmm#4997) | +| 235 | `b4028c1aa` | - | - | - | - | openvmm_helpers: admit kernel tuning on platform command lines (microsoft/openvmm#5001) | The remarks that say whether commits apply to upstream main record what the v3.0 reorganization found on its base, `b2676222b`, at the positions that the commits have now. @@ -1242,6 +1245,12 @@ elsewhere: every target now renames through `pal`, which the commit gives a no-r Unix and on Windows, the latter on the extended-length encoding that position 219 moves into `pal`'s file system module. It also adds a test beside position 032's test of a destination created after staging, and extends the snapshot guide's sentence that PR17's position 076 adds. +Position 235 changes the platform tier's check of a snapshot's command line, which PR19's position +093 adds and PR20's position 097, PR23's position 116, and positions 147 and 168 change: the +allowlist of the command line's tokens also admits a closed set of kernel tuning parameters, and +the check refuses a command line that repeats one of them, after position 097's check of the +boot-online processor count. It also extends the `--snapshot-tier` entry that position 093 adds to +the CLI reference. Assigning the later commits therefore means revisiting those PRs, not only appending new ones. Position 177, by contrast, changes only `disk_blob`, which upstream main and the fork base @@ -1471,6 +1480,16 @@ extended-length encoding of position 219 and that sentence; only the Unix rename manifest apply there. So it follows PR11 and PR17 upstream, and its Windows rename follows position 219's `pal` change. +Position 235 changes only the platform tier's check of the microVM snapshot module of +`openvmm_helpers`, which PR17's position 074 adds, PR19's position 093 gives the platform tier, and +PR20's position 097, PR23's position 116, and positions 147 and 168 change, and adds tests to that +module, which use the paired-scratch manifest of PR19's position 092, the platform tier of position +093, and the worker's platform command line of PR20's position 097. It also extends the +`--snapshot-tier` entry that position 093 adds to the CLI reference of PR15's position 064. +Replaying it on upstream main conflicts in both of its files: upstream main lacks the microVM +snapshot module of position 074, the tiers of position 093, and the microVM CLI of position 064, so +it follows PR15, PR17, and PR19 upstream. + ## PR summary Each count is of v6.0 commits, which include the fixups that the v3.0, v4.0, v5.0, and v6.0