Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: emulate Hyper-V enlightenment stack #849

Merged
merged 20 commits into from
Feb 11, 2025
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
lib: hyper-v migration support
  • Loading branch information
gjcolombo committed Feb 5, 2025
commit 41d90342fbd6d15adebf5b1166c519385a5f192b
4 changes: 2 additions & 2 deletions lib/propolis/src/enlightenment/hyperv/hypercall.rs
Original file line number Diff line number Diff line change
@@ -6,14 +6,14 @@

use crate::common::{GuestAddr, PAGE_SHIFT, PAGE_SIZE};

/// Represents a value written to the [`MSR_HYPERCALL`] register.
/// Represents a value written to the [`HV_X64_MSR_HYPERCALL`] register.
///
/// Writing to this register enables the hypercall page. The hypervisor overlays
/// this page with an instruction sequence that the guest should execute in
/// order to issue a call to the hypervisor. See
/// [`HYPERCALL_INSTRUCTION_SEQUENCE`].
///
/// [`MSR_HYPERCALL`]: super::bits::MSR_HYPERCALL
/// [`HV_X64_MSR_HYPERCALL`]: super::bits::HV_X64_MSR_HYPERCALL
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct MsrHypercallValue(pub(super) u64);

88 changes: 79 additions & 9 deletions lib/propolis/src/enlightenment/hyperv/mod.rs
Original file line number Diff line number Diff line change
@@ -25,7 +25,10 @@ use crate::{
accessors::MemAccessor,
common::{GuestRegion, Lifecycle, VcpuId, PAGE_SIZE},
enlightenment::AddCpuidError,
migrate::Migrator,
migrate::{
MigrateCtx, MigrateSingle, MigrateStateError, Migrator, PayloadOffer,
PayloadOutput,
},
msr::{MsrId, RdmsrOutcome, WrmsrOutcome},
vmm::SubMapping,
};
@@ -59,11 +62,13 @@ impl HyperV {
fn handle_wrmsr_guest_os_id(&self, value: u64) -> WrmsrOutcome {
let mut inner = self.inner.lock().unwrap();

// TLFS section 3.13 specifies that if the guest OS ID register is
// cleared, then the hypercall page immediately "becomes disabled." The
// exact semantics of "becoming disabled" are unclear, but in context it
// seems most reasonable to read this as "the Enabled bit is cleared and
// the hypercall overlay is removed."
// TLFS section 3.13 says that the hypercall page "becomes disabled" if
// the guest OS ID register is cleared after the hypercall register is
// set. It also specifies that attempts to set the Enabled bit in that
// register will be ignored if the guest OS ID is zeroed, so handle this
// case by clearing the hypercall MSR's Enabled bit but otherwise
// leaving the hypercall page untouched (as would happen if the guest
// manually cleared this bit).
if value == 0 {
info!(&self.log, "guest cleared HV_X64_MSR_GUEST_OS_ID");
inner.msr_hypercall_value.clear_enabled();
@@ -79,7 +84,7 @@ impl HyperV {
let mut inner = self.inner.lock().unwrap();
let old = inner.msr_hypercall_value;

// TLFS section 3.13 says that this MSR is "immutable" once the locked
// TLFS section 3.13 says that this MSR is immutable once the locked
// bit is set.
if old.locked() {
return WrmsrOutcome::Handled;
@@ -214,8 +219,73 @@ impl Lifecycle for HyperV {
*inner = Inner::default();
}

// TODO: Migration support.
fn migrate(&'_ self) -> Migrator<'_> {
Migrator::NonMigratable
Migrator::Single(self)
}
}

impl MigrateSingle for HyperV {
fn export(
&self,
_ctx: &MigrateCtx,
) -> Result<PayloadOutput, MigrateStateError> {
let inner = self.inner.lock().unwrap();
Ok(migrate::HyperVEnlightenmentV1 {
msr_guest_os_id: inner.msr_guest_os_id_value,
msr_hypercall: inner.msr_hypercall_value.0,
}
.into())
}

fn import(
&self,
mut offer: PayloadOffer,
ctx: &MigrateCtx,
) -> Result<(), MigrateStateError> {
let data: migrate::HyperVEnlightenmentV1 = offer.parse()?;

let hypercall_msr = MsrHypercallValue(data.msr_hypercall);
if hypercall_msr.enabled() {
if data.msr_guest_os_id == 0 {
return Err(MigrateStateError::ImportFailed(
"hypercall MSR enabled but guest OS ID MSR is 0"
.to_string(),
));
}

let Some(mapping) = ctx
.mem
.writable_region(&GuestRegion(hypercall_msr.gpa(), PAGE_SIZE))
else {
return Err(MigrateStateError::ImportFailed(
"couldn't map GPA in hypercall MSR".to_string(),
));
};

write_overlay_page(&mapping, &hypercall_page_contents());
}

let mut inner = self.inner.lock().unwrap();
inner.msr_guest_os_id_value = data.msr_guest_os_id;
inner.msr_hypercall_value = hypercall_msr;
Ok(())
}
}

mod migrate {
use serde::{Deserialize, Serialize};

use crate::migrate::{Schema, SchemaId};

#[derive(Debug, Serialize, Deserialize)]
pub struct HyperVEnlightenmentV1 {
pub(super) msr_guest_os_id: u64,
pub(super) msr_hypercall: u64,
}

impl Schema<'_> for HyperVEnlightenmentV1 {
fn id() -> SchemaId {
(super::TYPE_NAME, 1)
}
}
}
62 changes: 46 additions & 16 deletions phd-tests/tests/src/hyperv.rs
Original file line number Diff line number Diff line change
@@ -2,25 +2,14 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use phd_framework::{artifacts, lifecycle::Action, TestVm};
use phd_testcase::*;
use tracing::warn;

#[phd_testcase]
async fn hyperv_smoke_test(ctx: &Framework) {
let mut cfg = ctx.vm_config_builder("hyperv_smoke_test");
cfg.guest_hv_interface(
propolis_client::types::GuestHypervisorInterface::HyperV {
features: vec![],
},
);
let mut vm = ctx.spawn_vm(&cfg, None).await?;
vm.launch().await?;
vm.wait_to_boot().await?;

// Make a best-effort attempt to detect that Hyper-V is actually present in
// the guest. It's valuable to run this test to completion regardless since
// it exercises Propolis shutdown while the Hyper-V enlightenment stack is
// active.
/// Attempts to see if the guest has detected Hyper-V support. This is
/// best-effort, since not all PHD guest images contain in-box tools that
/// display the current hypervisor vendor.
async fn guest_detect_hyperv(vm: &TestVm) -> anyhow::Result<()> {
if vm.guest_os_kind().is_linux() {
// Many Linux distros come with systemd installed out of the box. On
// these distros, it's easiest to use `systemd-detect-virt` to determine
@@ -44,4 +33,45 @@ async fn hyperv_smoke_test(ctx: &Framework) {
// these don't identify the detected hypervisor type.)
warn!("running on Windows, can't verify it detected Hyper-V support");
}

Ok(())
}

#[phd_testcase]
async fn hyperv_smoke_test(ctx: &Framework) {
let mut cfg = ctx.vm_config_builder("hyperv_smoke_test");
cfg.guest_hv_interface(
propolis_client::types::GuestHypervisorInterface::HyperV {
features: vec![],
},
);
let mut vm = ctx.spawn_vm(&cfg, None).await?;
vm.launch().await?;
vm.wait_to_boot().await?;

guest_detect_hyperv(&vm).await?;
}

#[phd_testcase]
async fn hyperv_migration_smoke_test(ctx: &Framework) {
let mut cfg = ctx.vm_config_builder("hyperv_migration_smoke_test");
cfg.guest_hv_interface(
propolis_client::types::GuestHypervisorInterface::HyperV {
features: vec![],
},
);
let mut vm = ctx.spawn_vm(&cfg, None).await?;
vm.launch().await?;
vm.wait_to_boot().await?;

ctx.lifecycle_test(
vm,
&[Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT)],
|target: &TestVm| {
Box::pin(async {
guest_detect_hyperv(target).await.unwrap();
})
},
)
.await?;
}