Skip to content

Commit c849bab

Browse files
authored
lib: add better management of Hyper-V overlay pages (#851)
Add to the Hyper-V module some supporting types to support overlay pages. A Hyper-V overlay page is a sort of auxiliary page of guest memory that "covers up" or temporarily "replaces" a specific physical page in the guest's physical memory, such that accesses to the overlaid GPA end up accessing the overlay instead. See the doc comments throughout overlay.rs for many more details. This commit does not implement first-class migration support for the overlay manager; instead, migration targets re-create their overlays during Hyper-V manager import based on the MSR values they've imported. This runs the risks that (a) the source and target overlay managers will disagree about the order in which overlays are to be applied, and (b) the target will overwrite data that was otherwise written to the overlay page on the source. These risks are mitigated here by (a) having only one kind of overlay and (b) the fact that guests should generally not expect to be able to write to an overlay page. First-class overlay migration support will address both these issues.
1 parent 725eb8e commit c849bab

File tree

6 files changed

+995
-58
lines changed

6 files changed

+995
-58
lines changed

lib/propolis/src/enlightenment/hyperv/hypercall.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
//! Support for hypercalls and their related MSRs.
66
7-
use crate::common::{GuestAddr, PAGE_MASK, PAGE_SIZE};
7+
use crate::{
8+
common::{GuestAddr, PAGE_MASK, PAGE_SHIFT, PAGE_SIZE},
9+
vmm::Pfn,
10+
};
811

912
const LOCKED_BIT: u64 = 1;
1013
const LOCKED_MASK: u64 = 1 << LOCKED_BIT;
@@ -38,6 +41,12 @@ impl std::fmt::Debug for MsrHypercallValue {
3841
}
3942

4043
impl MsrHypercallValue {
44+
/// Yields the guest page number (the PFN) at which the guest would like the
45+
/// hypercall page to be placed.
46+
pub fn gpfn(&self) -> Pfn {
47+
Pfn::new(self.0 >> PAGE_SHIFT).unwrap()
48+
}
49+
4150
/// Returns the guest physical address at which the guest would like the
4251
/// hypercall page to be placed.
4352
pub fn gpa(&self) -> GuestAddr {

lib/propolis/src/enlightenment/hyperv/mod.rs

Lines changed: 111 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
//! that intends to implement a Hyper-V-compatible interface:
1515
//! https://github.com/MicrosoftDocs/Virtualization-Documentation/blob/main/tlfs/Requirements%20for%20Implementing%20the%20Microsoft%20Hypervisor%20Interface.pdf
1616
17-
use std::sync::Mutex;
17+
use std::sync::{Arc, Mutex};
1818

1919
use cpuid_utils::{CpuidIdent, CpuidSet, CpuidValues};
20+
use overlay::{
21+
OverlayContents, OverlayError, OverlayKind, OverlayManager, OverlayPage,
22+
};
2023

2124
use crate::{
2225
accessors::MemAccessor,
23-
common::{GuestRegion, Lifecycle, VcpuId, PAGE_SIZE},
26+
common::{Lifecycle, VcpuId},
2427
enlightenment::{
2528
hyperv::{
2629
bits::*,
@@ -33,11 +36,11 @@ use crate::{
3336
PayloadOutput,
3437
},
3538
msr::{MsrId, RdmsrOutcome, WrmsrOutcome},
36-
vmm::SubMapping,
3739
};
3840

3941
mod bits;
4042
mod hypercall;
43+
mod overlay;
4144

4245
#[usdt::provider(provider = "propolis")]
4346
mod probes {
@@ -49,13 +52,18 @@ mod probes {
4952

5053
const TYPE_NAME: &str = "guest-hyperv-interface";
5154

52-
#[derive(Debug, Default)]
55+
#[derive(Default)]
5356
struct Inner {
57+
/// This enlightenment's overlay manager.
58+
overlay_manager: Arc<OverlayManager>,
59+
5460
/// The last value stored in the [`bits::HV_X64_MSR_GUEST_OS_ID`] MSR.
5561
msr_guest_os_id_value: u64,
5662

5763
/// The last value stored in the [`bits::HV_X64_MSR_HYPERCALL`] MSR.
5864
msr_hypercall_value: MsrHypercallValue,
65+
66+
hypercall_overlay: Option<OverlayPage>,
5967
}
6068

6169
pub struct HyperV {
@@ -87,6 +95,7 @@ impl HyperV {
8795
// manually cleared this bit).
8896
if value == 0 {
8997
inner.msr_hypercall_value.clear_enabled();
98+
inner.hypercall_overlay.take();
9099
}
91100

92101
inner.msr_guest_os_id_value = value;
@@ -122,36 +131,42 @@ impl HyperV {
122131
// the guest.
123132
if !new.enabled() {
124133
inner.msr_hypercall_value = new;
134+
inner.hypercall_overlay.take();
125135
return WrmsrOutcome::Handled;
126136
}
127137

128-
let memctx = self
129-
.acc_mem
130-
.access()
131-
.expect("guest memory is always accessible during wrmsr");
132-
133-
let region = GuestRegion(new.gpa(), PAGE_SIZE);
134-
135-
// Mapping will fail if the requested GPA is out of the guest's physical
136-
// address range. The TLFS specifies that this should raise #GP.
137-
let Some(mapping) = memctx.writable_region(&region) else {
138-
probes::hyperv_wrmsr_hypercall_bad_gpa!(|| new.gpa().0);
139-
return WrmsrOutcome::GpException;
138+
// Ensure the overlay is in the correct position.
139+
let res = if let Some(overlay) = inner.hypercall_overlay.as_mut() {
140+
overlay.move_to(new.gpfn())
141+
} else {
142+
inner
143+
.overlay_manager
144+
.add_overlay(
145+
new.gpfn(),
146+
OverlayKind::Hypercall,
147+
OverlayContents(Box::new(hypercall_page_contents())),
148+
)
149+
.map(|overlay| {
150+
inner.hypercall_overlay = Some(overlay);
151+
})
140152
};
141153

142-
// Write the hypercall instruction sequence to the requested GPA.
143-
//
144-
// TODO: TLFS section 5.2.1 specifies that when an overlay is removed,
145-
// "the underlying GPA page is 'uncovered', and an existing mapping
146-
// becomes accessible to the guest." Empirically, at least some other
147-
// Hv#1 implementations don't appear to follow this rule, and most
148-
// common guest OSes don't rely on being able to disable or remove the
149-
// hypercall page. Nevertheless, Propolis should eventually follow this
150-
// rule.
151-
write_overlay_page(&mapping, &hypercall_page_contents());
152-
153-
inner.msr_hypercall_value = new;
154-
WrmsrOutcome::Handled
154+
match res {
155+
Ok(()) => {
156+
inner.msr_hypercall_value = new;
157+
WrmsrOutcome::Handled
158+
}
159+
Err(OverlayError::AddressInaccessible(_)) => {
160+
WrmsrOutcome::GpException
161+
}
162+
// There should only ever be one hypercall overlay at a time, and
163+
// guest memory should be accessible in the context of a VM exit, so
164+
// (barring some other invariant being violated) adding an overlay
165+
// should always succeed if the target PFN is valid.
166+
Err(e) => {
167+
panic!("unexpected error establishing hypercall overlay: {e}")
168+
}
169+
}
155170
}
156171
}
157172

@@ -225,25 +240,50 @@ impl super::Enlightenment for HyperV {
225240

226241
fn attach(&self, mem_acc: &MemAccessor) {
227242
mem_acc.adopt(&self.acc_mem, Some(TYPE_NAME.to_owned()));
243+
let inner = self.inner.lock().unwrap();
244+
inner.overlay_manager.attach(&self.acc_mem);
228245
}
229246
}
230247

231-
fn write_overlay_page(mapping: &SubMapping<'_>, contents: &[u8; PAGE_SIZE]) {
232-
let written = mapping
233-
.write_bytes(contents)
234-
.expect("overlay pages are always writable");
235-
236-
assert_eq!(written, PAGE_SIZE, "overlay pages can be written completely");
237-
}
238-
239248
impl Lifecycle for HyperV {
240249
fn type_name(&self) -> &'static str {
241250
TYPE_NAME
242251
}
243252

244253
fn reset(&self) {
245254
let mut inner = self.inner.lock().unwrap();
246-
*inner = Inner::default();
255+
256+
// Create a new overlay manager that tracks no pages. The old overlay
257+
// manager needs to be dropped before any of the overlay pages that
258+
// referenced it, so explicitly replace the existing manager with a new
259+
// one (and drop the old one) before default-initializing the rest of
260+
// the enlightenment's state.
261+
let new_overlay_manager = Arc::new(OverlayManager::default());
262+
let _ = std::mem::replace(
263+
&mut inner.overlay_manager,
264+
new_overlay_manager.clone(),
265+
);
266+
267+
*inner = Inner {
268+
overlay_manager: new_overlay_manager,
269+
..Default::default()
270+
};
271+
272+
inner.overlay_manager.attach(&self.acc_mem);
273+
}
274+
275+
fn halt(&self) {
276+
let mut inner = self.inner.lock().unwrap();
277+
278+
// Create a new overlay manager and drop the reference to the old one.
279+
// This should be the only active reference to this manager, since all
280+
// overlay page operations happen during VM exits, and the vCPUs have
281+
// all quiesced by this point.
282+
//
283+
// This ensures that when this object is dropped, any overlay pages it
284+
// owns can be dropped safely.
285+
assert_eq!(Arc::strong_count(&inner.overlay_manager), 1);
286+
inner.overlay_manager = OverlayManager::new();
247287
}
248288

249289
fn migrate(&'_ self) -> Migrator<'_> {
@@ -267,9 +307,10 @@ impl MigrateSingle for HyperV {
267307
fn import(
268308
&self,
269309
mut offer: PayloadOffer,
270-
ctx: &MigrateCtx,
310+
_ctx: &MigrateCtx,
271311
) -> Result<(), MigrateStateError> {
272312
let data: migrate::HyperVEnlightenmentV1 = offer.parse()?;
313+
let mut inner = self.inner.lock().unwrap();
273314

274315
// A well-behaved source should ensure that the hypercall MSR value is
275316
// within the guest's PA range and that its Enabled bit agrees with the
@@ -285,20 +326,39 @@ impl MigrateSingle for HyperV {
285326
));
286327
}
287328

288-
let Some(mapping) = ctx
289-
.mem
290-
.writable_region(&GuestRegion(hypercall_msr.gpa(), PAGE_SIZE))
291-
else {
292-
return Err(MigrateStateError::ImportFailed(format!(
293-
"couldn't map hypercall page for MSR value \
294-
{hypercall_msr:?}"
295-
)));
296-
};
297-
298-
write_overlay_page(&mapping, &hypercall_page_contents());
329+
// TODO(#850): Registering a new overlay with the overlay manager is
330+
// the only way to get an `OverlayPage` for this overlay. However,
331+
// the page's contents were already migrated in the RAM transfer
332+
// phase, so it's not actually necessary to create a *new* overlay
333+
// here; in fact, it would be incorrect to do so if the hypercall
334+
// target PFN had multiple overlays and a different one was active!
335+
// Fortunately, there's only one overlay type right now, so there's
336+
// no way for a page to have multiple overlays.
337+
//
338+
// (It would also be incorrect to rewrite this page if the guest
339+
// wrote data to it expecting to retrieve it later, but per the
340+
// TLFS, writes to the hypercall page should #GP anyway, so guests
341+
// ought not to expect too much here.)
342+
//
343+
// A better approach is to have the overlay manager export and
344+
// import its contents and, on import, return the set of overlay
345+
// page registrations that it imported. This layer can then check
346+
// that these registrations are consistent with its MSR values
347+
// before proceeding.
348+
match inner.overlay_manager.add_overlay(
349+
hypercall_msr.gpfn(),
350+
OverlayKind::Hypercall,
351+
OverlayContents(Box::new(hypercall_page_contents())),
352+
) {
353+
Ok(overlay) => inner.hypercall_overlay = Some(overlay),
354+
Err(e) => {
355+
return Err(MigrateStateError::ImportFailed(format!(
356+
"failed to re-establish hypercall overlay: {e}"
357+
)))
358+
}
359+
}
299360
}
300361

301-
let mut inner = self.inner.lock().unwrap();
302362
inner.msr_guest_os_id_value = data.msr_guest_os_id;
303363
inner.msr_hypercall_value = hypercall_msr;
304364
Ok(())

0 commit comments

Comments
 (0)