Skip to content

Commit 8fcb7ea

Browse files
committed
fix: add i686-guest snapshot tests
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent bc9c0d6 commit 8fcb7ea

1 file changed

Lines changed: 342 additions & 4 deletions

File tree

src/hyperlight_host/src/sandbox/snapshot.rs

Lines changed: 342 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,12 @@ fn build_cow_map(
314314
scratch: &[u8],
315315
layout: SandboxMemoryLayout,
316316
kernel_root: u64,
317-
) -> std::collections::HashMap<u64, u64> {
317+
) -> crate::Result<std::collections::HashMap<u64, u64>> {
318318
use hyperlight_common::layout::scratch_base_gpa;
319319
let mut cow_map = std::collections::HashMap::new();
320320
let scratch_base = scratch_base_gpa(layout.get_scratch_size());
321321
let scratch_end = scratch_base + layout.get_scratch_size() as u64;
322-
let mem_size = layout.get_memory_size().unwrap_or(0) as u64;
322+
let mem_size = layout.get_memory_size()? as u64;
323323

324324
for pdi in 0..1024u64 {
325325
let pde_addr = kernel_root + pdi * 4;
@@ -347,7 +347,7 @@ fn build_cow_map(
347347
}
348348
}
349349
}
350-
cow_map
350+
Ok(cow_map)
351351
}
352352

353353
/// Helper for building i686 2-level page tables as a flat byte buffer.
@@ -925,7 +925,7 @@ impl Snapshot {
925925
let kernel_root = root_pt_gpas.first().copied().ok_or_else(|| {
926926
crate::new_error!("snapshot requires at least one page directory root")
927927
})?;
928-
build_cow_map(snap_c, scratch_c, layout, kernel_root)
928+
build_cow_map(snap_c, scratch_c, layout, kernel_root)?
929929
};
930930

931931
// Pass 1: collect live pages
@@ -1203,3 +1203,341 @@ mod tests {
12031203
.unwrap();
12041204
}
12051205
}
1206+
1207+
#[cfg(test)]
1208+
#[cfg(feature = "i686-guest")]
1209+
mod tests {
1210+
use std::collections::HashMap;
1211+
1212+
use hyperlight_common::vmem::i686_guest::{PAGE_ACCESSED, PAGE_PRESENT, PAGE_RW};
1213+
use hyperlight_common::vmem::{BasicMapping, Mapping, MappingKind};
1214+
1215+
use super::i686_pt::{self, ADDR_MASK, PTE_COW, RW_FLAGS};
1216+
use crate::mem::layout::SandboxMemoryLayout;
1217+
use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegionFlags};
1218+
use crate::sandbox::SandboxConfiguration;
1219+
1220+
const PAGE_SIZE: usize = 4096;
1221+
1222+
struct TestEnv {
1223+
layout: SandboxMemoryLayout,
1224+
snap: Vec<u8>,
1225+
scratch: Vec<u8>,
1226+
pt_base: u64,
1227+
}
1228+
1229+
fn make_env(pt_bytes: &[u8]) -> TestEnv {
1230+
let mut cfg = SandboxConfiguration::default();
1231+
cfg.set_heap_size(PAGE_SIZE as u64);
1232+
let layout = SandboxMemoryLayout::new(cfg, PAGE_SIZE, PAGE_SIZE, None).unwrap();
1233+
let scratch_size = layout.get_scratch_size();
1234+
let snapshot_size = layout.get_memory_size().unwrap();
1235+
let snap = vec![0u8; snapshot_size];
1236+
let mut scratch = vec![0u8; scratch_size];
1237+
1238+
let pt_scratch_offset = layout.get_pt_base_scratch_offset();
1239+
assert!(pt_scratch_offset + pt_bytes.len() <= scratch.len(),);
1240+
1241+
scratch[pt_scratch_offset..pt_scratch_offset + pt_bytes.len()].copy_from_slice(pt_bytes);
1242+
1243+
TestEnv {
1244+
snap,
1245+
scratch,
1246+
layout,
1247+
pt_base: layout.get_pt_base_gpa(),
1248+
}
1249+
}
1250+
1251+
/// Decode a PTE from raw page table bytes at the given VA.
1252+
fn read_pte(pt_bytes: &[u8], pt_base_gpa: usize, va: u64) -> u32 {
1253+
let pdi = ((va >> 22) & 0x3FF) as usize;
1254+
let pti = ((va >> 12) & 0x3FF) as usize;
1255+
let pde = u32::from_le_bytes(pt_bytes[pdi * 4..pdi * 4 + 4].try_into().unwrap());
1256+
assert_ne!(
1257+
pde & PAGE_PRESENT as u32,
1258+
0,
1259+
"PDE for VA {va:#x} not present"
1260+
);
1261+
let pt_offset = (pde & ADDR_MASK) as usize - pt_base_gpa;
1262+
u32::from_le_bytes(
1263+
pt_bytes[pt_offset + pti * 4..pt_offset + pti * 4 + 4]
1264+
.try_into()
1265+
.unwrap(),
1266+
)
1267+
}
1268+
1269+
#[test]
1270+
fn builder_map_page_writes_pde_and_pte() {
1271+
let pd_base = 0x10_0000;
1272+
let mut b = i686_pt::Builder::new(pd_base);
1273+
let va = 0x0040_0000u64; // PD index 1, PT index 0
1274+
let pa = 0x0020_0000u64;
1275+
b.map_page(0, va, pa, RW_FLAGS);
1276+
1277+
let pde = b.read_u32(4);
1278+
assert_ne!(pde & PAGE_PRESENT as u32, 0, "PDE should be present");
1279+
assert_eq!((pde & ADDR_MASK) as usize, pd_base + PAGE_SIZE);
1280+
1281+
let pte = b.read_u32(PAGE_SIZE); // PT index 0
1282+
assert_eq!(pte & ADDR_MASK, pa as u32);
1283+
assert_eq!(pte & 0xFFF, RW_FLAGS);
1284+
1285+
// Map a second page in the same 4MB region - PT must be reused
1286+
b.map_page(0, 0x0040_1000, 0x20_1000, RW_FLAGS);
1287+
assert_eq!(b.bytes.len(), 2 * PAGE_SIZE, "PT should be reused");
1288+
let pte1 = b.read_u32(PAGE_SIZE + 4);
1289+
assert_eq!(pte1 & ADDR_MASK, 0x20_1000);
1290+
}
1291+
1292+
#[test]
1293+
fn builder_map_range_crosses_pde_boundary() {
1294+
let pd_base = 0x10_0000;
1295+
let mut b = i686_pt::Builder::new(pd_base);
1296+
// Last page of PD[0] to first page of PD[1]
1297+
let va_start = 0x003F_F000u64;
1298+
let pa_start = 0x5_0000u64;
1299+
b.map_range(0, va_start, pa_start, 2 * PAGE_SIZE as u64, RW_FLAGS);
1300+
1301+
assert_eq!(b.bytes.len(), 3 * PAGE_SIZE, "should allocate 2 PTs");
1302+
1303+
// Verify PTE contents across the boundary
1304+
let pt0_offset = (b.read_u32(0) & ADDR_MASK) as usize - pd_base;
1305+
let pte_last = b.read_u32(pt0_offset + 0x3FF * 4); // last entry in PT[0]
1306+
assert_eq!(pte_last & ADDR_MASK, pa_start as u32);
1307+
1308+
let pt1_offset = (b.read_u32(4) & ADDR_MASK) as usize - pd_base;
1309+
let pte_first = b.read_u32(pt1_offset); // first entry in PT[1]
1310+
assert_eq!(pte_first & ADDR_MASK, (pa_start + PAGE_SIZE as u64) as u32);
1311+
}
1312+
1313+
#[test]
1314+
fn builder_cow_flags_preserved_pde_stays_rw() {
1315+
let pd_base = 0x10_0000;
1316+
let mut b = i686_pt::Builder::new(pd_base);
1317+
let cow_flags = PAGE_PRESENT as u32 | PAGE_ACCESSED as u32 | PTE_COW;
1318+
b.map_page(0, 0x1000, 0x2000, cow_flags);
1319+
1320+
let pti = ((0x1000u64 >> 12) & 0x3FF) as usize;
1321+
let pte = b.read_u32(PAGE_SIZE + pti * 4);
1322+
assert_ne!(pte & PTE_COW, 0, "CoW bit should be set on PTE");
1323+
assert_eq!(pte & PAGE_RW as u32, 0, "RW should be clear for CoW PTE");
1324+
1325+
// PDE must remain RW so the CPU can walk the PT
1326+
let pde = b.read_u32(0);
1327+
assert_ne!(
1328+
pde & PAGE_RW as u32,
1329+
0,
1330+
"PDE must stay RW even for CoW PTEs"
1331+
);
1332+
}
1333+
1334+
#[test]
1335+
fn builder_multiple_pds_independent() {
1336+
let pd_base = 0x10_0000;
1337+
let mut b = i686_pt::Builder::with_pds(pd_base, 2);
1338+
b.map_page(0, 0x1000, 0xA000, RW_FLAGS);
1339+
b.map_page(PAGE_SIZE, 0x1000, 0xB000, RW_FLAGS);
1340+
1341+
// PTs start after the 2 PD pages
1342+
let pde0 = b.read_u32(0);
1343+
let pde1 = b.read_u32(PAGE_SIZE);
1344+
assert_eq!(
1345+
(pde0 & ADDR_MASK) as usize,
1346+
pd_base + 2 * PAGE_SIZE,
1347+
"PD[0] PT should be at first slot after PDs"
1348+
);
1349+
assert_eq!(
1350+
(pde1 & ADDR_MASK) as usize,
1351+
pd_base + 3 * PAGE_SIZE,
1352+
"PD[1] PT should be at second slot after PDs"
1353+
);
1354+
1355+
// Verify the PTEs point to the correct PAs
1356+
let pti = ((0x1000u64 >> 12) & 0x3FF) as usize;
1357+
let pte0 = b.read_u32(2 * PAGE_SIZE + pti * 4);
1358+
let pte1 = b.read_u32(3 * PAGE_SIZE + pti * 4);
1359+
assert_eq!(pte0 & ADDR_MASK, 0xA000);
1360+
assert_eq!(pte1 & ADDR_MASK, 0xB000);
1361+
}
1362+
1363+
#[test]
1364+
fn cow_map_finds_scratch_backed_pages() {
1365+
let cfg = SandboxConfiguration::default();
1366+
let scratch_size = cfg.get_scratch_size();
1367+
let scratch_base = hyperlight_common::layout::scratch_base_gpa(scratch_size);
1368+
let layout = SandboxMemoryLayout::new(cfg, PAGE_SIZE, PAGE_SIZE, None).unwrap();
1369+
let pt_base = layout.get_pt_base_gpa() as usize;
1370+
1371+
let mut b = i686_pt::Builder::new(pt_base);
1372+
let cow_frame = scratch_base + 0x5000;
1373+
let cow_va = 0x1000u64;
1374+
b.map_page(0, cow_va, cow_frame, RW_FLAGS);
1375+
1376+
let TestEnv {
1377+
snap,
1378+
scratch,
1379+
layout,
1380+
pt_base,
1381+
} = make_env(&b.into_bytes());
1382+
let cow_map = super::build_cow_map(&snap, &scratch, layout, pt_base).unwrap();
1383+
1384+
assert_eq!(cow_map.len(), 1);
1385+
assert_eq!(cow_map[&cow_va], cow_frame);
1386+
}
1387+
1388+
#[test]
1389+
fn cow_map_filtering() {
1390+
let cfg = SandboxConfiguration::default();
1391+
let scratch_size = cfg.get_scratch_size();
1392+
let scratch_base = hyperlight_common::layout::scratch_base_gpa(scratch_size);
1393+
let layout = SandboxMemoryLayout::new(cfg, PAGE_SIZE, PAGE_SIZE, None).unwrap();
1394+
let pt_base = layout.get_pt_base_gpa() as usize;
1395+
let mem_size = layout.get_memory_size().unwrap();
1396+
1397+
let mut b = i686_pt::Builder::new(pt_base);
1398+
b.map_page(
1399+
0,
1400+
0x1000,
1401+
SandboxMemoryLayout::BASE_ADDRESS as u64,
1402+
RW_FLAGS,
1403+
);
1404+
let far_va = (mem_size as u64).next_multiple_of(0x0040_0000);
1405+
b.map_page(0, far_va, scratch_base + 0x1000, RW_FLAGS);
1406+
1407+
let TestEnv {
1408+
snap,
1409+
scratch,
1410+
layout,
1411+
pt_base,
1412+
} = make_env(&b.into_bytes());
1413+
let cow_map = super::build_cow_map(&snap, &scratch, layout, pt_base).unwrap();
1414+
1415+
assert!(
1416+
cow_map.is_empty(),
1417+
"neither non-scratch nor beyond-mem-size VAs should appear"
1418+
);
1419+
}
1420+
1421+
#[test]
1422+
fn cow_map_empty_pd() {
1423+
let cfg = SandboxConfiguration::default();
1424+
let layout = SandboxMemoryLayout::new(cfg, PAGE_SIZE, PAGE_SIZE, None).unwrap();
1425+
let pt_base = layout.get_pt_base_gpa() as usize;
1426+
let b = i686_pt::Builder::new(pt_base);
1427+
1428+
let TestEnv {
1429+
snap,
1430+
scratch,
1431+
layout,
1432+
pt_base,
1433+
} = make_env(&b.into_bytes());
1434+
let cow_map = super::build_cow_map(&snap, &scratch, layout, pt_base).unwrap();
1435+
1436+
assert!(cow_map.is_empty());
1437+
}
1438+
1439+
#[test]
1440+
fn initial_pt_scratch_rw_and_region_flags() {
1441+
let cfg = SandboxConfiguration::default();
1442+
let layout = SandboxMemoryLayout::new(cfg, PAGE_SIZE, PAGE_SIZE, None).unwrap();
1443+
1444+
let pt_bytes = super::build_initial_i686_page_tables(&layout).unwrap();
1445+
let pt_base = layout.get_pt_base_gpa() as usize;
1446+
1447+
// Scratch must be mapped as RW without CoW
1448+
let scratch_size = layout.get_scratch_size();
1449+
let scratch_gva = hyperlight_common::layout::scratch_base_gva(scratch_size);
1450+
let scratch_pte = read_pte(&pt_bytes, pt_base, scratch_gva);
1451+
assert_ne!(scratch_pte & PAGE_PRESENT as u32, 0);
1452+
assert_ne!(scratch_pte & PAGE_RW as u32, 0, "scratch must be writable");
1453+
assert_eq!(scratch_pte & PTE_COW, 0, "scratch must not be CoW");
1454+
1455+
// Verify region permissions: writable -> CoW, read-only -> no CoW
1456+
let regions = layout.get_memory_regions_::<GuestMemoryRegion>(()).unwrap();
1457+
1458+
for rgn in &regions {
1459+
let is_writable = rgn.flags.contains(MemoryRegionFlags::WRITE);
1460+
let va = rgn.guest_region.start as u64;
1461+
let pte = read_pte(&pt_bytes, pt_base, va);
1462+
assert_ne!(pte & PAGE_PRESENT as u32, 0);
1463+
if is_writable {
1464+
assert_ne!(pte & PTE_COW, 0, "writable region at {va:#x} should be CoW");
1465+
assert_eq!(pte & PAGE_RW as u32, 0, "CoW at {va:#x} must clear RW");
1466+
} else {
1467+
assert_eq!(pte & PTE_COW, 0, "RO region at {va:#x} must not be CoW");
1468+
}
1469+
}
1470+
}
1471+
1472+
#[test]
1473+
fn compact_deduplicates_shared_physical_pages() {
1474+
let shared_phys = 0x2000u64;
1475+
let page_data = vec![0xAAu8; PAGE_SIZE];
1476+
1477+
let make_mapping = |virt_base: u64| Mapping {
1478+
phys_base: shared_phys,
1479+
virt_base,
1480+
len: PAGE_SIZE as u64,
1481+
kind: MappingKind::Basic(BasicMapping {
1482+
readable: true,
1483+
writable: true,
1484+
executable: false,
1485+
}),
1486+
};
1487+
1488+
let TestEnv {
1489+
snap,
1490+
scratch,
1491+
layout,
1492+
pt_base,
1493+
} = make_env(&[0u8; PAGE_SIZE]);
1494+
1495+
let cow_map = HashMap::new();
1496+
let mut phys_seen = HashMap::new();
1497+
1498+
let live_pages: Vec<(Mapping, &[u8])> = vec![
1499+
(make_mapping(0x1000), &page_data),
1500+
(make_mapping(0x5000), &page_data),
1501+
];
1502+
1503+
let (snapshot_mem, _pt_bytes) = super::compact_i686_snapshot(
1504+
&snap,
1505+
&scratch,
1506+
layout,
1507+
live_pages,
1508+
&[pt_base],
1509+
&cow_map,
1510+
&mut phys_seen,
1511+
)
1512+
.unwrap();
1513+
1514+
assert_eq!(
1515+
snapshot_mem.len(),
1516+
PAGE_SIZE,
1517+
"shared physical page should be deduplicated"
1518+
);
1519+
}
1520+
1521+
#[test]
1522+
fn compact_empty_roots_returns_error() {
1523+
let TestEnv {
1524+
snap,
1525+
scratch,
1526+
layout,
1527+
..
1528+
} = make_env(&[0u8; PAGE_SIZE]);
1529+
let cow_map = HashMap::new();
1530+
let mut phys_seen = HashMap::new();
1531+
1532+
let result = super::compact_i686_snapshot(
1533+
&snap,
1534+
&scratch,
1535+
layout,
1536+
Vec::new(),
1537+
&[],
1538+
&cow_map,
1539+
&mut phys_seen,
1540+
);
1541+
assert!(result.is_err(), "empty root_pt_gpas should return an error");
1542+
}
1543+
}

0 commit comments

Comments
 (0)