Skip to content

Commit 18d4ff9

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

File tree

8 files changed

+228
-41
lines changed

8 files changed

+228
-41
lines changed

.github/workflows/ValidatePullRequest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
- docs-pr
6868
- build-guests
6969
strategy:
70-
fail-fast: true
70+
fail-fast: false
7171
matrix:
7272
hypervisor: [hyperv, 'hyperv-ws2025', mshv3, kvm]
7373
cpu: [amd, intel]

src/hyperlight_host/src/hypervisor/hyperlight_vm.rs

Lines changed: 147 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,7 @@ mod debug {
11181118

11191119
#[cfg(test)]
11201120
#[cfg(feature = "init-paging")]
1121+
#[allow(clippy::needless_range_loop)]
11211122
mod tests {
11221123
use std::sync::{Arc, Mutex};
11231124

@@ -1294,24 +1295,108 @@ mod tests {
12941295
}
12951296
}
12961297

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

13341419
// ST0-ST7/MM0-MM7 (bytes 32-159, indices 8-39)
1335-
for item in xsave.iter_mut().take(40).skip(8) {
1336-
*item = 0xCAFEBABE;
1420+
for i in 8..40 {
1421+
xsave[i] = 0xCAFEBABE;
13371422
}
13381423
// XMM0-XMM15 (bytes 160-415, indices 40-103)
1339-
for item in xsave.iter_mut().take(104).skip(40) {
1340-
*item = 0xDEADBEEF;
1424+
for i in 40..104 {
1425+
xsave[i] = 0xDEADBEEF;
13411426
}
13421427

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

13591475
xsave
13601476
}

src/hyperlight_host/src/hypervisor/hyperv_linux.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,26 @@ impl Hypervisor for MshvVm {
252252

253253
#[cfg(test)]
254254
#[cfg(feature = "init-paging")]
255-
fn set_xsave(&self, xsave: &[u32; 1024]) -> Result<()> {
256-
let mut buf = XSave::default();
255+
fn set_xsave(&self, xsave: &[u32]) -> Result<()> {
256+
const MSHV_XSAVE_SIZE: usize = 4096;
257+
if std::mem::size_of_val(xsave) != MSHV_XSAVE_SIZE {
258+
return Err(new_error!(
259+
"Provided xsave size {} does not match MSHV supported size {}",
260+
std::mem::size_of_val(xsave),
261+
MSHV_XSAVE_SIZE
262+
));
263+
}
264+
257265
// Safety: all valid u32 values are 4 valid u8 values
258266
let (prefix, bytes, suffix) = unsafe { xsave.align_to() };
259267
if !prefix.is_empty() || !suffix.is_empty() {
260268
return Err(new_error!("Invalid xsave buffer alignment"));
261269
}
262-
buf.buffer.copy_from_slice(bytes);
270+
let buf = XSave {
271+
buffer: bytes
272+
.try_into()
273+
.map_err(|_| new_error!("mshv xsave must be 4096 u8s"))?,
274+
};
263275
self.vcpu_fd.set_xsave(&buf)?;
264276
Ok(())
265277
}

src/hyperlight_host/src/hypervisor/hyperv_windows.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ impl Hypervisor 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/hypervisor/kvm.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,21 @@ impl Hypervisor 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/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,11 @@ pub(crate) trait Hypervisor: Debug + Send {
131131
fn xsave(&self) -> Result<Vec<u8>>;
132132
/// Reset xsave to default state
133133
fn reset_xsave(&self) -> Result<()>;
134-
/// Set xsave - only used for tests
134+
/// Set xsave - only used for tests.
135+
/// Note: On MSHV/WHP, XCOMP_BV must be preserved.
135136
#[cfg(test)]
136137
#[cfg(feature = "init-paging")]
137-
fn set_xsave(&self, xsave: &[u32; 1024]) -> Result<()>;
138+
fn set_xsave(&self, xsave: &[u32]) -> Result<()>;
138139

139140
/// Get the debug registers of the vCPU
140141
#[allow(dead_code)]

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

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

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

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

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

547+
#[guest_function("SetDr0")]
548+
fn set_dr0(value: u64) {
549+
unsafe { core::arch::asm!("mov dr0, {}", in(reg) value) };
550+
}
551+
552+
#[guest_function("GetDr0")]
553+
fn get_dr0() -> u64 {
554+
let value: u64;
555+
unsafe { core::arch::asm!("mov {}, dr0", out(reg) value) };
556+
value
557+
}
558+
547559
#[guest_function("Add")]
548560
fn add(a: i32, b: i32) -> Result<i32> {
549561
#[host_function("HostAdd")]

0 commit comments

Comments
 (0)