Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions aci_edge_sandboxes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ publishes guest ports on host loopback. The repeatable `--env KEY=VALUE`, layere
guest's environment, and `--cwd PATH` apply to the command. The ignored
`tests/nvxhost_guest.rs` exercises an actual guest when the corresponding `NVXHOST_TEST_*` paths
and approved library digest are set: under WHP on Windows, or under the hypervisor that
`NVXHOST_TEST_HYPERVISOR` names, such as `mshv` on Linux.
`NVXHOST_TEST_HYPERVISOR` names, such as `mshv` on Linux. `NVXHOST_TEST_CPU_PROFILE=host` boots
its guests on a host CPU profile.

For example, from `aci_edge_sandboxes` on a Windows WHP host, set the following
paths to compatible, separately built artifacts and a caller-prepared GPT disk
Expand Down Expand Up @@ -225,8 +226,12 @@ matching private sources; this example neither fetches nor builds them. Use the
pinned OpenVMM, which includes the scratchless RAM-overlay topology, and the NVX
kernel that its time ABI requires, such as a release's `guest/vmlinux`. OpenVMM
selects a [CPU profile](../doc/usage.md#cpu-profiles) at every cold boot, so start
fails with `backend_error` on a host whose CPU none of its built-in profiles serves;
the OpenVMM log that the error names gives the reason. On Linux, supply a matching
fails with `backend_error` on a host that none of its built-in profiles serves, or
whose hypervisor does not support its profile; the OpenVMM log that the error names
gives the reason. On such a development host,
`NvxHostConfig::with_host_cpu_profile(true)` (the example's `--cpu-profile host`)
boots the guests on a host profile, which OpenVMM derives from the host's
hypervisor at every cold boot and does not pin. On Linux, supply a matching
`libnvxhost.so` and OpenVMM build and select `--hypervisor mshv`.

### Native host paths and network
Expand Down
27 changes: 26 additions & 1 deletion aci_edge_sandboxes/examples/nvxhost_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn run() -> Result<(), String> {
let mut forwards = Vec::new();
let mut environment = Vec::new();
let mut cwd = None;
let mut host_cpu_profile = false;
let mut command = None;
let mut args = std::env::args().skip(1);
while let Some(option) = args.next() {
Expand Down Expand Up @@ -72,6 +73,7 @@ fn run() -> Result<(), String> {
"--host-loopback-forward" => forwards.push(parse_forward(&option, &value()?)?),
"--env" => environment.push(value()?),
"--cwd" => cwd = Some(value()?),
"--cpu-profile" => host_cpu_profile = parse_cpu_profile(&option, &value()?)?,
"--" => {
command = Some(args.by_ref().collect::<Vec<_>>().join(" "));
break;
Expand Down Expand Up @@ -144,7 +146,9 @@ fn run() -> Result<(), String> {
let config = OpenVmmConfig::new(openvmm, kernel, initrd, hypervisor, PathBuf::from(root));
let backend = Arc::new(
NvxHostBackend::new(
NvxHostConfig::new(config, image, library, digest).with_image_digest(image_digest),
NvxHostConfig::new(config, image, library, digest)
.with_image_digest(image_digest)
.with_host_cpu_profile(host_cpu_profile),
)
.map_err(describe)?,
);
Expand Down Expand Up @@ -229,6 +233,15 @@ fn parse_access(option: &str, text: &str) -> Result<Access, String> {
}
}

/// Parses `auto`, OpenVMM's built-in profile of the host's CPU, or `host`, a host profile.
fn parse_cpu_profile(option: &str, text: &str) -> Result<bool, String> {
match text {
"auto" => Ok(false),
"host" => Ok(true),
_ => Err(format!("{option} must be auto or host")),
}
}

/// Parses `CIDR` or `CIDR:tcp|udp:PORT`, such as `192.0.2.0/24` or `192.0.2.1:tcp:443`.
fn parse_rule(option: &str, text: &str) -> Result<NetworkRule, String> {
let malformed = || format!("{option} must be CIDR or CIDR:tcp|udp:PORT");
Expand Down Expand Up @@ -313,6 +326,18 @@ mod tests {
assert!(parse_access("--egress", "Deny").is_err());
}

#[test]
fn cpu_profiles_are_auto_or_host() {
assert!(!parse_cpu_profile("--cpu-profile", "auto").unwrap());
assert!(parse_cpu_profile("--cpu-profile", "host").unwrap());
for other in ["", "Host", "amd.milan.v1"] {
assert!(
parse_cpu_profile("--cpu-profile", other).is_err(),
"{other}"
);
}
}

#[test]
fn forwards_name_a_protocol_and_two_ports() {
let parse = |text: &str| parse_forward("--host-loopback-forward", text);
Expand Down
59 changes: 53 additions & 6 deletions aci_edge_sandboxes/src/nvxhost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const FLAG_REMOTE_CLOSED: u8 = 1;
const FLAG_NO_DATA: u8 = 4;
const LAUNCH_GUEST_DEBUG: u32 = 1;
const LAUNCH_HAS_MEMORY: u32 = 4;
const LAUNCH_HOST_CPU_PROFILE: u32 = 16;
const PLAN_VALIDATE_ONLY: u32 = 1;

#[repr(C)]
Expand Down Expand Up @@ -347,12 +348,7 @@ impl HostLibrary {
.map_err(|_| Error::policy_validation("guest memory exceeds the nvxhost ABI range"))?;
let request = NvxLaunchRequest {
struct_size: size_of::<NvxLaunchRequest>() as u32,
flags: LAUNCH_HAS_MEMORY
| if inputs.guest_debug {
LAUNCH_GUEST_DEBUG
} else {
0
},
flags: inputs.flags(),
memory_mb,
kernel_path: NvxStr::present(kernel),
initrd_path: NvxStr::present(initrd),
Expand Down Expand Up @@ -476,10 +472,26 @@ pub(crate) struct LaunchInputs<'a> {
pub(crate) hypervisor: &'a str,
pub(crate) memory_mb: u32,
pub(crate) guest_debug: bool,
/// Boots on the CPU profile that OpenVMM derives from this host.
pub(crate) host_cpu_profile: bool,
/// JSON of the sandbox plan from [`HostLibrary::plan_sandbox`], if the sandbox has one.
pub(crate) plan: Option<&'a str>,
}

impl LaunchInputs<'_> {
/// Launch flags of the nvxhost ABI for these inputs.
fn flags(&self) -> u32 {
let mut flags = LAUNCH_HAS_MEMORY;
if self.guest_debug {
flags |= LAUNCH_GUEST_DEBUG;
}
if self.host_cpu_profile {
flags |= LAUNCH_HOST_CPU_PROFILE;
}
flags
}
}

#[derive(Debug)]
struct NativeFailure {
kind: u32,
Expand Down Expand Up @@ -967,6 +979,28 @@ mod tests {
assert!(error.message().contains("cannot locate nvxhost"));
}

#[test]
fn launch_flags_follow_the_inputs() {
let mut inputs = LaunchInputs {
kernel: Path::new("vmlinux"),
initrd: Path::new("initramfs"),
image: Path::new("image.gpt"),
control: "control",
boot: "boot",
hypervisor: "whp",
memory_mb: 256,
guest_debug: false,
host_cpu_profile: false,
plan: None,
};
assert_eq!(inputs.flags(), LAUNCH_HAS_MEMORY);
inputs.guest_debug = true;
assert_eq!(inputs.flags(), LAUNCH_HAS_MEMORY | LAUNCH_GUEST_DEBUG);
inputs.guest_debug = false;
inputs.host_cpu_profile = true;
assert_eq!(inputs.flags(), LAUNCH_HAS_MEMORY | LAUNCH_HOST_CPU_PROFILE);
}

#[test]
#[ignore = "set NVXHOST_TEST_LIBRARY to a separately built nvxhost DLL or shared library"]
fn privately_built_library_exposes_the_ramfs_launch_abi() {
Expand All @@ -991,6 +1025,7 @@ mod tests {
hypervisor: "whp",
memory_mb: 256,
guest_debug: false,
host_cpu_profile: false,
plan,
};
let args = host.launch_arguments(&inputs(None)).unwrap();
Expand All @@ -1007,6 +1042,18 @@ mod tests {
1
);
assert!(!args.contains(&"--mount") && !args.contains(&"--net"));
assert!(!args.contains(&"--cpu-profile"));
let hosted = host
.launch_arguments(&LaunchInputs {
host_cpu_profile: true,
..inputs(None)
})
.unwrap();
assert!(
hosted
.windows(2)
.any(|pair| pair[0] == "--cpu-profile" && pair[1] == "host")
);

// Validation consults nothing on the host; unenforceable policies are policy errors.
assert_eq!(host.plan_sandbox("{}", "10.0.0.2/24", true).unwrap(), None);
Expand Down
17 changes: 17 additions & 0 deletions aci_edge_sandboxes/src/openvmm/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ pub struct NvxHostConfig {
pub content_verification: bool,
/// Requests verbose guest kernel diagnostics on the boot console.
pub guest_debug: bool,
/// Boots guests on the CPU profile that OpenVMM derives from this host instead of the
/// built-in profile of the host's CPU generation.
pub host_cpu_profile: bool,
}

impl NvxHostConfig {
Expand All @@ -100,6 +103,7 @@ impl NvxHostConfig {
runtime_sha256: None,
content_verification: false,
guest_debug: false,
host_cpu_profile: false,
}
}

Expand Down Expand Up @@ -130,6 +134,18 @@ impl NvxHostConfig {
self.guest_debug = enabled;
self
}

/// Boots guests on a host CPU profile (OpenVMM's `--cpu-profile host`), for development
/// hosts that no built-in CPU profile serves or whose hypervisor does not support it.
///
/// OpenVMM derives the profile from the hypervisor on this host at every cold boot, under
/// the built-in profiles' policy, and still fails a host that lacks a feature that the time
/// ABI requires. Unlike a built-in profile, a host profile is not pinned.
#[must_use]
pub fn with_host_cpu_profile(mut self, enabled: bool) -> Self {
self.host_cpu_profile = enabled;
self
}
}

/// SHA-256 digests of the OpenVMM executable, guest kernel, and guest initramfs.
Expand Down Expand Up @@ -912,6 +928,7 @@ impl Backend for NvxHostBackend {
hypervisor: self.config.openvmm.hypervisor.as_str(),
memory_mb: record.memory_mib,
guest_debug: self.config.guest_debug,
host_cpu_profile: self.config.host_cpu_profile,
plan: plan.as_deref(),
})?;
let report = self.base.store.outcome_path(sandbox_id);
Expand Down
17 changes: 16 additions & 1 deletion aci_edge_sandboxes/tests/nvxhost_guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//! The tests need `NVXHOST_TEST_OPENVMM`, `NVXHOST_TEST_KERNEL`, `NVXHOST_TEST_INITRD`,
//! `NVXHOST_TEST_IMAGE`, `NVXHOST_TEST_LIBRARY`, and the approved `NVXHOST_TEST_SHA256`.
//! `NVXHOST_TEST_HYPERVISOR` selects `whp`, `mshv`, or `kvm`, and defaults to `whp` on Windows.
//! `NVXHOST_TEST_CPU_PROFILE=host` boots the guests on a host CPU profile, for hosts that no
//! built-in profile serves; the default, `auto`, uses the built-in profile.
//! The host-path, network, proxy, port-forwarding, and exec-environment tests also need `python3`
//! in the image. Run the tests one at a time so that the check for leftover OpenVMM processes is
//! exact, and optimized, since creating a backend hashes the runtime files and registering an
Expand Down Expand Up @@ -62,6 +64,17 @@ fn hypervisor() -> Hypervisor {
}
}

/// Returns whether `NVXHOST_TEST_CPU_PROFILE` selects a host CPU profile (`host`) rather than the
/// built-in profile of the host's CPU (`auto`, the default).
fn host_cpu_profile() -> bool {
match std::env::var("NVXHOST_TEST_CPU_PROFILE") {
Ok(name) if name == "host" => true,
Ok(name) if name == "auto" => false,
Ok(name) => panic!("NVXHOST_TEST_CPU_PROFILE must be auto or host, not {name:?}"),
Err(_) => false,
}
}

/// Returns the test configuration for `image`, with sandbox state under `state`.
fn native_config(state: &Path, image: &Path) -> NvxHostConfig {
NvxHostConfig::new(
Expand All @@ -76,6 +89,7 @@ fn native_config(state: &Path, image: &Path) -> NvxHostConfig {
required("NVXHOST_TEST_LIBRARY"),
approved_digest(),
)
.with_host_cpu_profile(host_cpu_profile())
}

fn backend_with(
Expand Down Expand Up @@ -382,7 +396,8 @@ fn guest_lifecycle_stops_gracefully_and_cleans_up() {
required("NVXHOST_TEST_LIBRARY"),
approved_digest(),
)
.with_guest_debug(true),
.with_guest_debug(true)
.with_host_cpu_profile(host_cpu_profile()),
)
.unwrap(),
);
Expand Down