Skip to content
Merged
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
5 changes: 4 additions & 1 deletion Guide/src/reference/openvmm/management/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,10 @@ describes the source definitions.
derive a development profile from this host's backend fingerprint for a
host that no pinned profile serves. `auto` never selects a host profile. A
host without a profile, or a profile of another generation, fails before the
partition is created. A restore uses the snapshot's profile and rejects any
partition is created. On an Intel or AMD host, `auto`'s errors name
`--cpu-profile host` when no pinned profile serves the host and when the
backend does not support the pinned profile (`E_PROFILE_UNSUPPORTED`). A
restore uses the snapshot's profile and rejects any
other; `host` also names a host profile that the snapshot recorded. See
[Time and CPU compatibility](../../../user_guide/openvmm/snapshots.md#time-and-cpu-compatibility).
* `--x-time-abi-verify`: Build the microVM partition and run the time ABI
Expand Down
29 changes: 19 additions & 10 deletions Guide/src/user_guide/openvmm/snapshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,23 +254,32 @@ identity MSRs that return the declared TSC rate (`0x40000022`) and LAPIC rate
command line must not set `tsc_early_khz=` or `lapic_timer_hz=`
(`E_CMDLINE_CLOCK_TOKEN`). The TSC is invariant, and the LAPIC timer runs in
one-shot mode at a fixed rate: 1 GHz on KVM and 200 MHz on MSHV and WHP.
TSC-deadline mode, `IA32_TSC_ADJUST`, and kvmclock are not exposed.
TSC-deadline mode, `IA32_TSC_ADJUST`, and kvmclock are not exposed. Under an
AMD CPU profile, AMD's configuration MSRs read fixed values on every backend:
HWCR (`0xc0010015`) only `TscFreqSel`, so Linux takes the TSC to count at the
P0 frequency, and DE_CFG (`0xc0011029`) only `LFENCE` serialization. Writing
the value read back changes nothing; any other write raises #GP.

The guest CPU is a pinned CPU profile: one per CPU generation, the same on
every backend. OpenVMM pins `intel.skylake-sp.v1` (Xeon Scalable, first
generation: family 6, model 85, steppings 0 to 4), `intel.icelake-sp.v1`
(Xeon Scalable, third generation: 6/106), `intel.emeraldrapids.v1` (Xeon
Scalable, fifth generation: 6/207), and `intel.alderlake.v1` (Core, twelfth
generation: 6/151 and 6/154). `--cpu-profile <ID>` selects one; the default,
Scalable, fifth generation: 6/207), `intel.alderlake.v1` (Core, twelfth
generation: 6/151 and 6/154), and `amd.milan.v1` (AMD EPYC, third generation:
25/1). `--cpu-profile <ID>` selects one; the default,
`auto`, selects the profile of the host's generation, and a host without one
fails (`E_PROFILE_HOST_UNKNOWN`). On an Intel development host without one,
`--cpu-profile host` boots on a host profile instead: OpenVMM fingerprints the
backend on this host, as `--cpu-fingerprint` does, and applies the pinned
profiles' derivation policy to it. A host profile, `intel.host.v1`, serves only
the host's CPU model and stepping, is not pinned, and can change with the
host's microcode, firmware, or hypervisor. Every boot checks that the backend
fails (`E_PROFILE_HOST_UNKNOWN`). On an Intel or AMD development host without
one, `--cpu-profile host` boots on a host profile instead: OpenVMM
fingerprints the backend on this host, as `--cpu-fingerprint` does, and
applies the pinned profiles' derivation policy to it. A host profile,
`intel.host.v1` or `amd.host.v1`, serves only the host's CPU model and
stepping, is not pinned, and can change with the host's microcode, firmware,
or hypervisor. Every boot checks that the backend
supports the profile (`E_PROFILE_UNSUPPORTED`) and that VP 0 observes its
effective CPUID (`E_CPU_SURFACE`). A snapshot records the profile and the
effective CPUID (`E_CPU_SURFACE`). On an Intel or AMD host, `auto`'s errors
name `--cpu-profile host` both where no pinned profile serves the host and
where the backend does not support the one that does, because a host profile
derives from what the backend supports. A snapshot records the profile and the
effective CPUID. Restore requires the same profile, pinned in this OpenVMM with
the same digest, or, for a host profile, the snapshot's own copy of it, which
must match the recorded digest; a host of the profile's generation; and an
Expand Down
21 changes: 19 additions & 2 deletions openvmm/openvmm_core/src/worker/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,28 @@ impl Worker for VmWorker {
.shared_memory
.map(|fd| restore_params.shared_memory_backing(fd));

// A cold boot whose built-in CPU profile this host cannot boot names
// the host profile where one could boot instead.
#[cfg(guest_arch = "x86_64")]
let hint = {
let requested = manifest
.microvm
.time_abi
.as_ref()
.map(|time_abi| time_abi.cpu_profile.clone());
let restoring = parameters.saved_state.is_some();
move |err| time_abi::hint_cold_boot(err, requested.as_deref(), restoring)
};
#[cfg(not(guest_arch = "x86_64"))]
let hint = |err: anyhow::Error| err;

let vm = block_on(InitializedVm::new(
VmTaskDriverSource::new(ThreadDriverBackend::new(device_driver)),
hypervisor.0,
manifest,
shared_memory,
))?;
))
.map_err(&hint)?;
let saved_state = parameters
.saved_state
.map(|m| m.parse())
Expand All @@ -379,7 +395,8 @@ impl Worker for VmWorker {
microvm_params.check_cold_boot(&vm, saved_state.is_some())?;

let mut vm =
block_with_io(|_| vm.load(saved_state, parameters.notify, restore_params.state))?;
block_with_io(|_| vm.load(saved_state, parameters.notify, restore_params.state))
.map_err(&hint)?;
vm.snapshot_boundary = microvm_params.snapshot_boundary;

LOADED_VM.store(&vm);
Expand Down
139 changes: 128 additions & 11 deletions openvmm/openvmm_core/src/worker/dispatch/time_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,70 @@ fn select_on_host(
&& err.code == ProfileErrorCode::ProfileHostUnknown
&& cpu_profile::supports_host_profiles(host)
{
err.message.push_str(
"; --cpu-profile host boots on a profile derived from this host, for development",
);
err.message.push_str("; ");
err.message.push_str(HOST_PROFILE_HINT);
}
err
})
}

/// What a failed cold boot with `--cpu-profile auto` suggests where host
/// profiles serve the host CPU and a host profile could boot.
const HOST_PROFILE_HINT: &str =
"--cpu-profile host boots on a profile derived from this host, for development";

/// Names `--cpu-profile host` in `err`, the failure of a VM worker that cold
/// booted with `--cpu-profile requested` on the host CPU `host`, where a host
/// profile could boot instead: `auto` selected a built-in profile that this
/// host's backend does not support (`E_PROFILE_UNSUPPORTED`), and host
/// profiles serve the CPU's vendor. A host profile derives from what the
/// backend supports. As for `E_PROFILE_HOST_UNKNOWN`
/// ([`select_cpu_profile`]), an explicit profile ID and `host` get no hint.
///
/// The hint is a context of `err`, which keeps the backend's error chain:
/// the code, where the backend found the shortfall, and what it lacks.
fn hint_host_profile(
err: anyhow::Error,
requested: &str,
host: &HostCpuSignature,
) -> anyhow::Error {
if requested != cpu_profile::AUTO
|| !cpu_profile::supports_host_profiles(host)
|| leading_code(&err) != Some(TimeAbiCode::ProfileUnsupported)
{
return err;
}
err.context(format!(
"this host cannot boot the built-in CPU profile that --cpu-profile auto selected; \
{HOST_PROFILE_HINT}"
))
}

/// Returns `err`, the failure of a VM worker, with the hint of
/// [`hint_host_profile`] if the worker cold booted a time ABI microVM with
/// `--cpu-profile requested` on this host. A restore (`restoring`), which
/// takes the snapshot's profile, gets none.
pub(super) fn hint_cold_boot(
err: anyhow::Error,
requested: Option<&str>,
restoring: bool,
) -> anyhow::Error {
match (requested, restoring, host_cpu()) {
(Some(requested), false, Ok(host)) => hint_host_profile(err, requested, &host),
_ => err,
}
}

/// Returns the time ABI code that OpenVMM's exit message gives `err`: the
/// first bracketed code in its chain.
fn leading_code(err: &anyhow::Error) -> Option<TimeAbiCode> {
let chain = format!("{err:#}");
chain.match_indices("[E_").find_map(|(start, _)| {
let code = &chain[start + 1..];
TimeAbiCode::from_name(&code[..code.find(']')?])
})
}

/// Derives the host profile of this host and the `hypervisor` backend: the
/// backend's CPU fingerprint, as `--cpu-fingerprint` takes it, under the
/// profiles' derivation policy.
Expand Down Expand Up @@ -718,10 +774,12 @@ mod tests {
/// The signature of the test profile's CPU, an Ice Lake-SP Xeon.
const TEST_SIGNATURE: u32 = 0x0006_06a6;

/// The end of the `E_PROFILE_HOST_UNKNOWN` message of `auto` on a host
/// that host profiles serve.
const HOST_PROFILE_HINT: &str =
"; --cpu-profile host boots on a profile derived from this host, for development";
/// Returns whether `message` ends with the `--cpu-profile host` hint, as
/// `auto`'s `E_PROFILE_HOST_UNKNOWN` message does on a host that host
/// profiles serve.
fn ends_with_hint(message: &str) -> bool {
message.ends_with(&format!("; {HOST_PROFILE_HINT}"))
}

fn test_profile() -> &'static CpuProfile {
cpu_profile::pinned(TEST_PROFILE).unwrap()
Expand Down Expand Up @@ -1056,7 +1114,7 @@ mod tests {
"{message}"
);
assert_eq!(
message.ends_with(HOST_PROFILE_HINT),
ends_with_hint(&message),
cpu_profile::supports_host_profiles(&host),
"{message}"
);
Expand All @@ -1073,15 +1131,16 @@ mod tests {
}

/// `auto` names `--cpu-profile host` only where a host profile can boot:
/// on Intel CPUs that no pinned profile serves.
/// on Intel and AMD CPUs that no pinned profile serves.
#[test]
fn auto_names_host_profiles_only_where_they_serve() {
let tiger_lake = HostCpuSignature::new(*b"GenuineIntel", 0x0008_06c1);
let zen3 = HostCpuSignature::new(*b"AuthenticAMD", 0x00a2_0f12);
for (host, hint) in [(tiger_lake, true), (zen3, false)] {
let hygon = HostCpuSignature::new(*b"HygonGenuine", 0x0090_0f01);
for (host, hint) in [(tiger_lake, true), (zen3, true), (hygon, false)] {
let error = select_on_host(cpu_profile::AUTO, &host, None).unwrap_err();
assert_eq!(error.code, ProfileErrorCode::ProfileHostUnknown, "{error}");
assert_eq!(error.message.ends_with(HOST_PROFILE_HINT), hint, "{error}");
assert_eq!(ends_with_hint(&error.message), hint, "{error}");
assert_eq!(
error.message.contains("--cpu-profile host"),
hint,
Expand All @@ -1094,6 +1153,64 @@ mod tests {
assert!(!error.message.contains("--cpu-profile host"), "{error}");
}

/// A cold boot with `auto` names `--cpu-profile host` where the backend
/// does not support the built-in profile that `auto` selected, on Intel
/// and AMD CPUs only, as it does where no built-in profile serves the
/// host. The hint keeps the backend's error chain.
#[test]
fn auto_names_host_profiles_where_the_backend_lacks_the_profile() {
let unsupported = || {
anyhow::Error::new(TimeAbiError::new(
TimeAbiCode::ProfileUnsupported,
"the kvm backend does not support CPU profile amd.milan.v1",
))
.context("failed to create the prototype partition")
};
let icelake = HostCpuSignature::new(*b"GenuineIntel", TEST_SIGNATURE);
let milan = HostCpuSignature::new(*b"AuthenticAMD", 0x00a0_0f11);
let hygon = HostCpuSignature::new(*b"HygonGenuine", 0x0090_0f01);
for host in [&icelake, &milan] {
let err = hint_host_profile(unsupported(), cpu_profile::AUTO, host);
let message = format!("{err:#}");
assert!(message.starts_with("this host cannot boot the built-in CPU profile that --cpu-profile auto selected; --cpu-profile host boots"), "{message}");
assert!(
message.ends_with(&format!("{:#}", unsupported())),
"{message}"
);
assert_eq!(
leading_code(&err),
Some(TimeAbiCode::ProfileUnsupported),
"{message}"
);
}
// Another vendor's CPU, an explicit profile, `host`, and other
// failures get no hint.
for (requested, host, err) in [
(cpu_profile::AUTO, &hygon, unsupported()),
(TEST_PROFILE, &icelake, unsupported()),
(cpu_profile::HOST, &milan, unsupported()),
(
cpu_profile::AUTO,
&milan,
anyhow::Error::new(TimeAbiError::new(TimeAbiCode::CpuSurface, "VP 0 differs"))
.context("failed to create the prototype partition"),
),
(
cpu_profile::AUTO,
&milan,
anyhow::anyhow!("no time ABI code"),
),
] {
let message = format!("{:#}", hint_host_profile(err, requested, host));
assert!(!message.contains(HOST_PROFILE_HINT), "{message}");
}
// Neither does a restore, nor a VM without the time ABI.
for (requested, restoring) in [(Some(cpu_profile::AUTO), true), (None, false)] {
let message = format!("{:#}", hint_cold_boot(unsupported(), requested, restoring));
assert_eq!(message, format!("{:#}", unsupported()));
}
}

#[test]
fn a_host_profile_needs_a_backend_fingerprint() {
let error = select_cpu_profile(cpu_profile::HOST, "nosuch", None).unwrap_err();
Expand Down
32 changes: 29 additions & 3 deletions openvmm/openvmm_helpers/src/snapshot/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,18 +299,44 @@ pub(super) mod tests {
const TEST_SIGNATURE: u32 = 0x0006_06a6;

/// Returns the effective CPUID of a partition with `vp_count` VPs and
/// `profile`: the topology fields OpenVMM sets for one socket, an xAPIC,
/// and no identity leaves.
/// `profile`: the topology fields OpenVMM sets for one socket, of the
/// profile's vendor, an xAPIC, and no identity leaves.
fn test_effective_cpuid(
profile: &cpu_profile::CpuProfile,
vp_count: u32,
) -> cpu_profile::EffectiveCpuid {
use cpu_profile::CpuVendor;
use cpu_profile::CpuidResult;
let mut vm = vec![
CpuidResult::new(1, [0, vp_count << 16, 0, 0]).masked([0, 0xffff_0000, 0, 0]),
CpuidResult::new(4, [(vp_count - 1) << 26, 0, 0, 0]).masked([0xffff_c000, 0, 0, 0]),
cpu_profile::x2apic_cpuid(false),
];
match profile.cpu_vendor() {
CpuVendor::Intel => vm.push(
CpuidResult::new(4, [(vp_count - 1) << 26, 0, 0, 0]).masked([0xffff_c000, 0, 0, 0]),
),
CpuVendor::Amd => {
let apic_id_size = vp_count.next_power_of_two().trailing_zeros();
vm.push(
CpuidResult::new(0x8000_0008, [0, 0, (vp_count - 1) | apic_id_size << 12, 0])
.masked([0, 0, 0xf0ff, 0]),
);
// The L3 cache is the socket's; the others are each VP's.
for subleaf in 0.. {
let eax = profile.lookup(0x8000_001d, subleaf)[0];
if eax & 0x1f == 0 {
break;
}
let sharing = if (eax >> 5) & 7 == 3 { vp_count - 1 } else { 0 };
vm.push(
CpuidResult::new(0x8000_001d, [sharing << 14, 0, 0, 0])
.indexed(subleaf)
.masked([0x03ff_c000, 0, 0, 0]),
);
}
vm.push(CpuidResult::new(0x8000_001e, [0; 4]).masked([!0, 0xffff, 0x7ff, 0]));
}
}
let max_basic = profile.lookup(0, 0)[0];
for leaf in cpu_profile::VM_OWNED_LEAVES
.into_iter()
Expand Down
Loading
Loading