@@ -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