Skip to content

Commit 180efe7

Browse files
committed
Make set_xsave take slice
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent ea63830 commit 180efe7

File tree

9 files changed

+235
-51
lines changed

9 files changed

+235
-51
lines changed

src/hyperlight_host/src/hypervisor/hyperlight_vm.rs

Lines changed: 147 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,7 @@ mod debug {
11121112

11131113
#[cfg(test)]
11141114
#[cfg(feature = "init-paging")]
1115+
#[allow(clippy::needless_range_loop)]
11151116
mod tests {
11161117
use std::sync::{Arc, Mutex};
11171118

@@ -1122,8 +1123,6 @@ mod tests {
11221123
#[cfg(kvm)]
11231124
use crate::hypervisor::regs::FP_CONTROL_WORD_DEFAULT;
11241125
use crate::hypervisor::regs::{CommonSegmentRegister, CommonTableRegister, MXCSR_DEFAULT};
1125-
#[cfg(target_os = "windows")]
1126-
use crate::hypervisor::wrappers::HandleWrapper;
11271126
use crate::mem::layout::SandboxMemoryLayout;
11281127
use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegionFlags};
11291128
use crate::mem::mgr::{GuestPageTableBuffer, SandboxMemoryManager};
@@ -1289,24 +1288,108 @@ mod tests {
12891288
}
12901289
}
12911290

1292-
/// Build a dirty XSAVE area for testing reset_vcpu.
1293-
/// Can't use all 1s because XSAVE has reserved fields that must be zero
1294-
/// (header at 512-575, MXCSR reserved bits, etc.)
1295-
///
1296-
/// Takes the current xsave state to preserve hypervisor-specific header fields
1297-
/// like XCOMP_BV which MSHV/WHP require to be set correctly.
1291+
/// Query CPUID.0DH.n for XSAVE component info.
1292+
/// Returns (size, offset, align_64) for the given component:
1293+
/// - size: CPUID.0DH.n:EAX - size in bytes
1294+
/// - offset: CPUID.0DH.n:EBX - offset from XSAVE base (standard format only)
1295+
/// - align_64: CPUID.0DH.n:ECX bit 1 - true if 64-byte aligned (compacted format)
1296+
fn xsave_component_info(comp_id: u32) -> (usize, usize, bool) {
1297+
let result = unsafe { std::arch::x86_64::__cpuid_count(0xD, comp_id) };
1298+
let size = result.eax as usize;
1299+
let offset = result.ebx as usize;
1300+
let align_64 = (result.ecx & 0b10) != 0;
1301+
(size, offset, align_64)
1302+
}
1303+
1304+
/// Query CPUID.0DH.00H for the bitmap of supported user state components.
1305+
/// EDX:EAX forms a 64-bit bitmap where bit i indicates support for component i.
1306+
fn xsave_supported_components() -> u64 {
1307+
let result = unsafe { std::arch::x86_64::__cpuid_count(0xD, 0) };
1308+
(result.edx as u64) << 32 | (result.eax as u64)
1309+
}
1310+
1311+
/// Dirty extended state components using compacted XSAVE format (MSHV/WHP).
1312+
/// Components are stored contiguously starting at byte 576, with alignment
1313+
/// requirements from CPUID.0DH.n:ECX[1].
1314+
/// Returns a bitmask of components that were actually dirtied.
1315+
fn dirty_xsave_extended_compacted(
1316+
xsave: &mut [u32],
1317+
xcomp_bv: u64,
1318+
supported_components: u64,
1319+
) -> u64 {
1320+
let mut dirtied_mask = 0u64;
1321+
let mut offset = 576usize;
1322+
1323+
for comp_id in 2..63u32 {
1324+
// Skip if component not supported by CPU or not enabled in XCOMP_BV
1325+
if (supported_components & (1u64 << comp_id)) == 0 {
1326+
continue;
1327+
}
1328+
if (xcomp_bv & (1u64 << comp_id)) == 0 {
1329+
continue;
1330+
}
1331+
1332+
let (size, _, align_64) = xsave_component_info(comp_id);
1333+
1334+
// ECX[1]=1 means 64-byte aligned; ECX[1]=0 means immediately after previous
1335+
if align_64 {
1336+
offset = offset.next_multiple_of(64);
1337+
}
1338+
1339+
// Dirty this component's data area (only if it fits in the buffer)
1340+
let start_idx = offset / 4;
1341+
let end_idx = (offset + size) / 4;
1342+
if end_idx <= xsave.len() {
1343+
for i in start_idx..end_idx {
1344+
xsave[i] = 0x12345678 ^ comp_id.wrapping_mul(0x11111111);
1345+
}
1346+
dirtied_mask |= 1u64 << comp_id;
1347+
}
1348+
1349+
offset += size;
1350+
}
1351+
1352+
dirtied_mask
1353+
}
1354+
1355+
/// Dirty extended state components using standard XSAVE format (KVM).
1356+
/// Components are at fixed offsets from CPUID.0DH.n:EBX.
1357+
/// Returns a bitmask of components that were actually dirtied.
1358+
fn dirty_xsave_extended_standard(xsave: &mut [u32], supported_components: u64) -> u64 {
1359+
let mut dirtied_mask = 0u64;
1360+
1361+
for comp_id in 2..63u32 {
1362+
// Skip if component not supported by CPU
1363+
if (supported_components & (1u64 << comp_id)) == 0 {
1364+
continue;
1365+
}
1366+
1367+
let (size, fixed_offset, _) = xsave_component_info(comp_id);
1368+
1369+
let start_idx = fixed_offset / 4;
1370+
let end_idx = (fixed_offset + size) / 4;
1371+
if end_idx <= xsave.len() {
1372+
for i in start_idx..end_idx {
1373+
xsave[i] = 0x12345678 ^ comp_id.wrapping_mul(0x11111111);
1374+
}
1375+
dirtied_mask |= 1u64 << comp_id;
1376+
}
1377+
}
1378+
1379+
dirtied_mask
1380+
}
1381+
1382+
/// Dirty the legacy XSAVE region (bytes 0-511) for testing reset_vcpu.
1383+
/// This includes FPU/x87 state, SSE state, and reserved areas.
12981384
///
12991385
/// Layout (from Intel SDM Table 13-1):
13001386
/// Bytes 0-1: FCW, 2-3: FSW, 4: FTW, 5: reserved, 6-7: FOP
13011387
/// Bytes 8-15: FIP, 16-23: FDP
1302-
/// Bytes 24-27: MXCSR, 28-31: MXCSR_MASK (don't touch)
1303-
/// Bytes 32-159: ST0-ST7/MM0-MM7 (8 regs × 16 bytes, only 10 bytes used per reg)
1388+
/// Bytes 24-27: MXCSR, 28-31: MXCSR_MASK (preserve - hardware defined)
1389+
/// Bytes 32-159: ST0-ST7/MM0-MM7 (8 regs × 16 bytes)
13041390
/// Bytes 160-415: XMM0-XMM15 (16 regs × 16 bytes)
13051391
/// Bytes 416-511: Reserved
1306-
/// Bytes 512-575: XSAVE header (preserve XCOMP_BV at 520-527)
1307-
fn dirty_xsave(current_xsave: &[u8]) -> [u32; 1024] {
1308-
let mut xsave = [0u32; 1024];
1309-
1392+
fn dirty_xsave_legacy(xsave: &mut [u32], current_xsave: &[u8]) {
13101393
// FCW (bytes 0-1) + FSW (bytes 2-3) - pack into xsave[0]
13111394
// FCW = 0x0F7F (different from default 0x037F), FSW = 0x1234
13121395
xsave[0] = 0x0F7F | (0x1234 << 16);
@@ -1321,35 +1404,66 @@ mod tests {
13211404
xsave[5] = 0xBABE0004;
13221405
// MXCSR (bytes 24-27) - xsave[6], use valid value different from default
13231406
xsave[6] = 0x3F80;
1324-
// xsave[7] is MXCSR_MASK - preserve from current
1407+
// xsave[7] is MXCSR_MASK - preserve from current (hardware defined, read-only)
13251408
if current_xsave.len() >= 32 {
13261409
xsave[7] = u32::from_le_bytes(current_xsave[28..32].try_into().unwrap());
13271410
}
13281411

13291412
// ST0-ST7/MM0-MM7 (bytes 32-159, indices 8-39)
1330-
for item in xsave.iter_mut().take(40).skip(8) {
1331-
*item = 0xCAFEBABE;
1413+
for i in 8..40 {
1414+
xsave[i] = 0xCAFEBABE;
13321415
}
13331416
// XMM0-XMM15 (bytes 160-415, indices 40-103)
1334-
for item in xsave.iter_mut().take(104).skip(40) {
1335-
*item = 0xDEADBEEF;
1417+
for i in 40..104 {
1418+
xsave[i] = 0xDEADBEEF;
13361419
}
13371420

1338-
// Preserve entire XSAVE header (bytes 512-575, indices 128-143) from current state
1339-
// This includes XSTATE_BV (512-519) and XCOMP_BV (520-527) which
1340-
// MSHV/WHP require to be set correctly for compacted format.
1341-
// Failure to do this will cause set_xsave to fail on MSHV.
1342-
if current_xsave.len() >= 576 {
1343-
#[allow(clippy::needless_range_loop)]
1344-
for i in 128..144 {
1345-
let byte_offset = i * 4;
1346-
xsave[i] = u32::from_le_bytes(
1347-
current_xsave[byte_offset..byte_offset + 4]
1348-
.try_into()
1349-
.unwrap(),
1350-
);
1351-
}
1421+
// Reserved area (bytes 416-511, indices 104-127)
1422+
for i in 104..128 {
1423+
xsave[i] = 0xABCDEF12;
1424+
}
1425+
}
1426+
1427+
/// Preserve XSAVE header (bytes 512-575) from current state.
1428+
/// This includes XSTATE_BV and XCOMP_BV which hypervisors require.
1429+
fn preserve_xsave_header(xsave: &mut [u32], current_xsave: &[u8]) {
1430+
for i in 128..144 {
1431+
let byte_offset = i * 4;
1432+
xsave[i] = u32::from_le_bytes(
1433+
current_xsave[byte_offset..byte_offset + 4]
1434+
.try_into()
1435+
.unwrap(),
1436+
);
13521437
}
1438+
}
1439+
1440+
fn dirty_xsave(current_xsave: &[u8]) -> Vec<u32> {
1441+
let mut xsave = vec![0u32; current_xsave.len() / 4];
1442+
1443+
dirty_xsave_legacy(&mut xsave, current_xsave);
1444+
preserve_xsave_header(&mut xsave, current_xsave);
1445+
1446+
let xcomp_bv = u64::from_le_bytes(current_xsave[520..528].try_into().unwrap());
1447+
let supported_components = xsave_supported_components();
1448+
1449+
// Dirty extended components and get mask of what was actually dirtied
1450+
let extended_mask = if (xcomp_bv & (1u64 << 63)) != 0 {
1451+
// Compacted format (MSHV/WHP)
1452+
dirty_xsave_extended_compacted(&mut xsave, xcomp_bv, supported_components)
1453+
} else {
1454+
// Standard format (KVM)
1455+
dirty_xsave_extended_standard(&mut xsave, supported_components)
1456+
};
1457+
1458+
// UPDATE XSTATE_BV to indicate dirtied components have valid data.
1459+
// WHP validates consistency between XSTATE_BV and actual data in the buffer.
1460+
// Bits 0,1 = legacy x87/SSE (always set after dirty_xsave_legacy)
1461+
// Bits 2+ = extended components that we actually dirtied
1462+
let xstate_bv = 0x3 | extended_mask;
1463+
1464+
// Write XSTATE_BV to bytes 512-519 (u32 indices 128-129)
1465+
xsave[128] = (xstate_bv & 0xFFFFFFFF) as u32;
1466+
xsave[129] = (xstate_bv >> 32) as u32;
13531467

13541468
xsave
13551469
}

src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,21 @@ impl VirtualMachine for KvmVm {
207207

208208
#[cfg(test)]
209209
#[cfg(feature = "init-paging")]
210-
fn set_xsave(&self, xsave: &[u32; 1024]) -> Result<()> {
210+
fn set_xsave(&self, xsave: &[u32]) -> Result<()> {
211+
const KVM_XSAVE_SIZE: usize = 4096;
212+
213+
if std::mem::size_of_val(xsave) != KVM_XSAVE_SIZE {
214+
return Err(new_error!(
215+
"Provided xsave size {} does not match KVM supported size {}",
216+
std::mem::size_of_val(xsave),
217+
KVM_XSAVE_SIZE
218+
));
219+
}
211220
let xsave = kvm_xsave {
212-
region: *xsave,
213-
..Default::default() // zeroed 4KB buffer with no FAM
221+
region: xsave
222+
.try_into()
223+
.map_err(|_| new_error!("kvm xsave must be 1024 u32s"))?,
224+
..Default::default()
214225
};
215226
// Safety: Safe because we only copy 4096 bytes
216227
// and have not enabled any dynamic xsave features

src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ pub(crate) trait VirtualMachine: Debug + Send {
175175
/// Set xsave - only used for tests
176176
#[cfg(test)]
177177
#[cfg(feature = "init-paging")]
178-
fn set_xsave(&self, xsave: &[u32; 1024]) -> Result<()>;
178+
fn set_xsave(&self, xsave: &[u32]) -> Result<()>;
179179

180180
/// Get the debug registers of the vCPU
181181
#[allow(dead_code)]

src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,26 @@ impl VirtualMachine for MshvVm {
255255

256256
#[cfg(test)]
257257
#[cfg(feature = "init-paging")]
258-
fn set_xsave(&self, xsave: &[u32; 1024]) -> Result<()> {
259-
let mut buf = XSave::default();
258+
fn set_xsave(&self, xsave: &[u32]) -> Result<()> {
259+
const MSHV_XSAVE_SIZE: usize = 4096;
260+
if std::mem::size_of_val(xsave) != MSHV_XSAVE_SIZE {
261+
return Err(new_error!(
262+
"Provided xsave size {} does not match MSHV supported size {}",
263+
std::mem::size_of_val(xsave),
264+
MSHV_XSAVE_SIZE
265+
));
266+
}
267+
260268
// Safety: all valid u32 values are 4 valid u8 values
261269
let (prefix, bytes, suffix) = unsafe { xsave.align_to() };
262270
if !prefix.is_empty() || !suffix.is_empty() {
263271
return Err(new_error!("Invalid xsave buffer alignment"));
264272
}
265-
buf.buffer.copy_from_slice(bytes);
273+
let buf = XSave {
274+
buffer: bytes
275+
.try_into()
276+
.map_err(|_| new_error!("mshv xsave must be 4096 u8s"))?,
277+
};
266278
self.vcpu_fd.set_xsave(&buf)?;
267279
Ok(())
268280
}

src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ impl VirtualMachine for WhpVm {
556556

557557
#[cfg(test)]
558558
#[cfg(feature = "init-paging")]
559-
fn set_xsave(&self, xsave: &[u32; 1024]) -> Result<()> {
559+
fn set_xsave(&self, xsave: &[u32]) -> Result<()> {
560560
use crate::HyperlightError;
561561

562562
// Get the required buffer size by calling with NULL buffer.

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,41 @@ mod tests {
12981298
assert_ne!(sandbox3.id, sandbox_id);
12991299
}
13001300

1301+
/// Test that snapshot restore properly resets vCPU debug registers. This test verifies
1302+
/// that restore() calls reset_vcpu.
1303+
#[test]
1304+
fn snapshot_restore_resets_debug_registers() {
1305+
let mut sandbox: MultiUseSandbox = {
1306+
let path = simple_guest_as_string().unwrap();
1307+
let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1308+
u_sbox.evolve().unwrap()
1309+
};
1310+
1311+
let snapshot = sandbox.snapshot().unwrap();
1312+
1313+
// Verify DR0 is initially 0 (clean state)
1314+
let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap();
1315+
assert_eq!(dr0_initial, 0, "DR0 should initially be 0");
1316+
1317+
// Dirty DR0 by setting it to a known non-zero value
1318+
const DIRTY_VALUE: u64 = 0xDEAD_BEEF_CAFE_BABE;
1319+
sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap();
1320+
let dr0_dirty: u64 = sandbox.call("GetDr0", ()).unwrap();
1321+
assert_eq!(
1322+
dr0_dirty, DIRTY_VALUE,
1323+
"DR0 should be dirty after SetDr0 call"
1324+
);
1325+
1326+
// Restore to the snapshot - this should reset vCPU state including debug registers
1327+
sandbox.restore(snapshot).unwrap();
1328+
1329+
let dr0_after_restore: u64 = sandbox.call("GetDr0", ()).unwrap();
1330+
assert_eq!(
1331+
dr0_after_restore, 0,
1332+
"DR0 should be 0 after restore (reset_vcpu should have been called)"
1333+
);
1334+
}
1335+
13011336
/// Test that sandboxes can be created and evolved with different heap sizes
13021337
#[test]
13031338
fn test_sandbox_creation_various_sizes() {

src/tests/rust_guests/dummyguest/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tests/rust_guests/simpleguest/src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,18 @@ fn use_sse2_registers() {
557557
unsafe { core::arch::asm!("movss xmm1, DWORD PTR [{0}]", in(reg) &val) };
558558
}
559559

560+
#[guest_function("SetDr0")]
561+
fn set_dr0(value: u64) {
562+
unsafe { core::arch::asm!("mov dr0, {}", in(reg) value) };
563+
}
564+
565+
#[guest_function("GetDr0")]
566+
fn get_dr0() -> u64 {
567+
let value: u64;
568+
unsafe { core::arch::asm!("mov {}, dr0", out(reg) value) };
569+
value
570+
}
571+
560572
#[guest_function("Add")]
561573
fn add(a: i32, b: i32) -> Result<i32> {
562574
#[host_function("HostAdd")]

0 commit comments

Comments
 (0)