@@ -1355,17 +1355,19 @@ impl Sandbox {
13551355 Ok ( ( ) )
13561356 }
13571357
1358- /// Persist the current post-evolve (or post-`snapshot_now`) snapshot
1359- /// to disk so a later process can skip evolve + init and go straight
1360- /// to `call`. Uses hyperlight's `Snapshot::to_file` — the file
1361- /// format and cross-platform mmap load are documented in
1362- /// hyperlight/docs/snapshot-file-implementation-plan.md.
1358+ /// Persist the current snapshot to disk as a sparse file.
1359+ ///
1360+ /// After writing the raw HLS snapshot, zero-filled 4 KiB pages are
1361+ /// punched out with `fallocate(PUNCH_HOLE)` so they consume no disk
1362+ /// space. The file is still mmap-loadable — holes read back as
1363+ /// zeros with no decompression overhead.
13631364 pub fn save_snapshot < P : AsRef < Path > > ( & self , path : P ) -> Result < ( ) > {
13641365 let snap = self
13651366 . snapshot
13661367 . as_ref ( )
13671368 . ok_or_else ( || anyhow ! ( "no snapshot present; build() or snapshot_now() first" ) ) ?;
13681369 snap. to_file ( path. as_ref ( ) ) ?;
1370+ sparsify_snapshot ( path. as_ref ( ) ) ?;
13691371 Ok ( ( ) )
13701372 }
13711373
@@ -1530,6 +1532,158 @@ pub fn run_vm_capture_output(
15301532 } )
15311533}
15321534
1535+ // ---------------------------------------------------------------------------
1536+ // Snapshot sparsification
1537+ // ---------------------------------------------------------------------------
1538+
1539+ /// Punch holes in zero-filled 4 KiB pages of a snapshot file.
1540+ ///
1541+ /// The HLS snapshot format is a 4 KiB header followed by a dense memory
1542+ /// blob where ~80 % of pages are all-zeros (unused heap). Punching them
1543+ /// with `fallocate(PUNCH_HOLE)` turns the file sparse — the zeros still
1544+ /// read back via mmap but consume no disk blocks.
1545+ #[ cfg( target_os = "linux" ) ]
1546+ fn sparsify_snapshot ( path : & Path ) -> Result < ( ) > {
1547+ use std:: os:: unix:: io:: AsRawFd ;
1548+
1549+ let file = std:: fs:: OpenOptions :: new ( )
1550+ . read ( true )
1551+ . write ( true )
1552+ . open ( path) ?;
1553+ let len = file. metadata ( ) ?. len ( ) ;
1554+ let mmap = unsafe { memmap2:: Mmap :: map ( & file) ? } ;
1555+
1556+ const PAGE : usize = 4096 ;
1557+ const HEADER : usize = PAGE ;
1558+ let zero_page = [ 0u8 ; PAGE ] ;
1559+
1560+ let mut punched = 0u64 ;
1561+ let mut offset = HEADER ;
1562+ while offset + PAGE <= len as usize {
1563+ if mmap[ offset..offset + PAGE ] == zero_page {
1564+ let ret = unsafe {
1565+ libc:: fallocate (
1566+ file. as_raw_fd ( ) ,
1567+ libc:: FALLOC_FL_PUNCH_HOLE | libc:: FALLOC_FL_KEEP_SIZE ,
1568+ offset as i64 ,
1569+ PAGE as i64 ,
1570+ )
1571+ } ;
1572+ if ret == 0 {
1573+ punched += 1 ;
1574+ }
1575+ }
1576+ offset += PAGE ;
1577+ }
1578+ drop ( mmap) ;
1579+
1580+ if punched > 0 {
1581+ eprintln ! (
1582+ " sparsified: punched {} zero pages ({} MiB saved on disk)" ,
1583+ punched,
1584+ punched * 4 / 1024
1585+ ) ;
1586+ }
1587+
1588+ Ok ( ( ) )
1589+ }
1590+
1591+ /// Windows equivalent: mark the file sparse with FSCTL_SET_SPARSE, then
1592+ /// punch zero ranges with FSCTL_SET_ZERO_DATA.
1593+ #[ cfg( target_os = "windows" ) ]
1594+ fn sparsify_snapshot ( path : & Path ) -> Result < ( ) > {
1595+ use std:: os:: windows:: io:: AsRawHandle ;
1596+ use windows_sys:: Win32 :: System :: Ioctl :: { FSCTL_SET_SPARSE , FSCTL_SET_ZERO_DATA } ;
1597+ use windows_sys:: Win32 :: System :: IO :: DeviceIoControl ;
1598+
1599+ let file = std:: fs:: OpenOptions :: new ( )
1600+ . read ( true )
1601+ . write ( true )
1602+ . open ( path) ?;
1603+ let len = file. metadata ( ) ?. len ( ) ;
1604+ let handle = file. as_raw_handle ( ) ;
1605+
1606+ // Mark file as sparse.
1607+ let ok = unsafe {
1608+ DeviceIoControl (
1609+ handle,
1610+ FSCTL_SET_SPARSE ,
1611+ std:: ptr:: null ( ) ,
1612+ 0 ,
1613+ std:: ptr:: null_mut ( ) ,
1614+ 0 ,
1615+ std:: ptr:: null_mut ( ) ,
1616+ std:: ptr:: null_mut ( ) ,
1617+ )
1618+ } ;
1619+ if ok == 0 {
1620+ return Ok ( ( ) ) ;
1621+ }
1622+
1623+ let mmap = unsafe { memmap2:: Mmap :: map ( & file) ? } ;
1624+
1625+ const PAGE : usize = 4096 ;
1626+ const HEADER : usize = PAGE ;
1627+ let zero_page = [ 0u8 ; PAGE ] ;
1628+
1629+ // Coalesce contiguous zero pages into ranges for fewer syscalls.
1630+ let mut punched = 0u64 ;
1631+ let mut offset = HEADER ;
1632+ while offset + PAGE <= len as usize {
1633+ if mmap[ offset..offset + PAGE ] != zero_page {
1634+ offset += PAGE ;
1635+ continue ;
1636+ }
1637+ let range_start = offset;
1638+ while offset + PAGE <= len as usize && mmap[ offset..offset + PAGE ] == zero_page {
1639+ offset += PAGE ;
1640+ }
1641+ let range_end = offset;
1642+
1643+ #[ repr( C ) ]
1644+ struct FileZeroDataInformation {
1645+ file_offset : i64 ,
1646+ beyond_final_zero : i64 ,
1647+ }
1648+
1649+ let info = FileZeroDataInformation {
1650+ file_offset : range_start as i64 ,
1651+ beyond_final_zero : range_end as i64 ,
1652+ } ;
1653+ let ok = unsafe {
1654+ DeviceIoControl (
1655+ handle,
1656+ FSCTL_SET_ZERO_DATA ,
1657+ & info as * const _ as * const _ ,
1658+ std:: mem:: size_of :: < FileZeroDataInformation > ( ) as u32 ,
1659+ std:: ptr:: null_mut ( ) ,
1660+ 0 ,
1661+ std:: ptr:: null_mut ( ) ,
1662+ std:: ptr:: null_mut ( ) ,
1663+ )
1664+ } ;
1665+ if ok != 0 {
1666+ punched += ( range_end - range_start) as u64 / PAGE as u64 ;
1667+ }
1668+ }
1669+ drop ( mmap) ;
1670+
1671+ if punched > 0 {
1672+ eprintln ! (
1673+ " sparsified: punched {} zero pages ({} MiB saved on disk)" ,
1674+ punched,
1675+ punched * 4 / 1024
1676+ ) ;
1677+ }
1678+
1679+ Ok ( ( ) )
1680+ }
1681+
1682+ #[ cfg( not( any( target_os = "linux" , target_os = "windows" ) ) ) ]
1683+ fn sparsify_snapshot ( _path : & Path ) -> Result < ( ) > {
1684+ Ok ( ( ) )
1685+ }
1686+
15331687// ---------------------------------------------------------------------------
15341688// FsSandbox tests — prove that host-side path resolution rejects escapes.
15351689//
0 commit comments