Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions host/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ base64 = "0.22"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29", features = ["fs"] }
libc = "0.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_System_IO", "Win32_System_Ioctl", "Win32_Storage_FileSystem"] }

35 changes: 34 additions & 1 deletion host/src/bin/pyhl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,9 @@ fn cmd_setup(args: SetupArgs) -> Result<()> {
mib(&dst_initrd)
);
eprintln!(
" snapshot: {} ({} MiB)",
" snapshot: {} ({} MiB on disk, {} MiB apparent)",
dst_snapshot.display(),
disk_mib(&dst_snapshot),
mib(&dst_snapshot)
);
Ok(())
Expand All @@ -384,6 +385,38 @@ fn mib(p: &Path) -> u64 {
fs::metadata(p).map(|m| m.len() / 1024 / 1024).unwrap_or(0)
}

#[cfg(unix)]
fn disk_mib(p: &Path) -> u64 {
use std::os::unix::fs::MetadataExt;
fs::metadata(p)
.map(|m| m.blocks() * 512 / 1024 / 1024)
.unwrap_or_else(|_| mib(p))
}

#[cfg(windows)]
fn disk_mib(p: &Path) -> u64 {
use std::os::windows::ffi::OsStrExt;
let wide: Vec<u16> = p
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut high: u32 = 0;
let low = unsafe {
windows_sys::Win32::Storage::FileSystem::GetCompressedFileSizeW(wide.as_ptr(), &mut high)
};
if low == u32::MAX {
return mib(p);
}
let bytes = ((high as u64) << 32) | (low as u64);
bytes / 1024 / 1024
}

#[cfg(not(any(unix, windows)))]
fn disk_mib(p: &Path) -> u64 {
mib(p)
}

/// Lightweight timestamp (seconds since epoch in ISO-8601-ish) so we don't
/// need to pull chrono just for the VERSION stamp.
fn now_iso8601() -> String {
Expand Down
164 changes: 159 additions & 5 deletions host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,17 +1355,19 @@ impl Sandbox {
Ok(())
}

/// Persist the current post-evolve (or post-`snapshot_now`) snapshot
/// to disk so a later process can skip evolve + init and go straight
/// to `call`. Uses hyperlight's `Snapshot::to_file` — the file
/// format and cross-platform mmap load are documented in
/// hyperlight/docs/snapshot-file-implementation-plan.md.
/// Persist the current snapshot to disk as a sparse file.
///
/// After writing the raw HLS snapshot, zero-filled 4 KiB pages are
/// punched out with `fallocate(PUNCH_HOLE)` so they consume no disk
/// space. The file is still mmap-loadable — holes read back as
/// zeros with no decompression overhead.
pub fn save_snapshot<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let snap = self
.snapshot
.as_ref()
.ok_or_else(|| anyhow!("no snapshot present; build() or snapshot_now() first"))?;
snap.to_file(path.as_ref())?;
sparsify_snapshot(path.as_ref())?;
Ok(())
}

Expand Down Expand Up @@ -1530,6 +1532,158 @@ pub fn run_vm_capture_output(
})
}

// ---------------------------------------------------------------------------
// Snapshot sparsification
// ---------------------------------------------------------------------------

/// Punch holes in zero-filled 4 KiB pages of a snapshot file.
///
/// The HLS snapshot format is a 4 KiB header followed by a dense memory
/// blob where ~80 % of pages are all-zeros (unused heap). Punching them
/// with `fallocate(PUNCH_HOLE)` turns the file sparse — the zeros still
/// read back via mmap but consume no disk blocks.
#[cfg(target_os = "linux")]
fn sparsify_snapshot(path: &Path) -> Result<()> {
use std::os::unix::io::AsRawFd;

let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)?;
let len = file.metadata()?.len();
let mmap = unsafe { memmap2::Mmap::map(&file)? };

const PAGE: usize = 4096;
const HEADER: usize = PAGE;
let zero_page = [0u8; PAGE];

let mut punched = 0u64;
let mut offset = HEADER;
while offset + PAGE <= len as usize {
if mmap[offset..offset + PAGE] == zero_page {
let ret = unsafe {
libc::fallocate(
file.as_raw_fd(),
libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
offset as i64,
PAGE as i64,
)
};
if ret == 0 {
punched += 1;
}
}
offset += PAGE;
}
drop(mmap);

if punched > 0 {
eprintln!(
" sparsified: punched {} zero pages ({} MiB saved on disk)",
punched,
punched * 4 / 1024
);
}

Ok(())
}

/// Windows equivalent: mark the file sparse with FSCTL_SET_SPARSE, then
/// punch zero ranges with FSCTL_SET_ZERO_DATA.
#[cfg(target_os = "windows")]
fn sparsify_snapshot(path: &Path) -> Result<()> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::System::Ioctl::{FSCTL_SET_SPARSE, FSCTL_SET_ZERO_DATA};
use windows_sys::Win32::System::IO::DeviceIoControl;

let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)?;
let len = file.metadata()?.len();
let handle = file.as_raw_handle();

// Mark file as sparse.
let ok = unsafe {
DeviceIoControl(
handle,
FSCTL_SET_SPARSE,
std::ptr::null(),
0,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if ok == 0 {
return Ok(());
}

let mmap = unsafe { memmap2::Mmap::map(&file)? };

const PAGE: usize = 4096;
const HEADER: usize = PAGE;
let zero_page = [0u8; PAGE];

// Coalesce contiguous zero pages into ranges for fewer syscalls.
let mut punched = 0u64;
let mut offset = HEADER;
while offset + PAGE <= len as usize {
if mmap[offset..offset + PAGE] != zero_page {
offset += PAGE;
continue;
}
let range_start = offset;
while offset + PAGE <= len as usize && mmap[offset..offset + PAGE] == zero_page {
offset += PAGE;
}
let range_end = offset;

#[repr(C)]
struct FileZeroDataInformation {
file_offset: i64,
beyond_final_zero: i64,
}

let info = FileZeroDataInformation {
file_offset: range_start as i64,
beyond_final_zero: range_end as i64,
};
let ok = unsafe {
DeviceIoControl(
handle,
FSCTL_SET_ZERO_DATA,
&info as *const _ as *const _,
std::mem::size_of::<FileZeroDataInformation>() as u32,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if ok != 0 {
punched += (range_end - range_start) as u64 / PAGE as u64;
}
}
drop(mmap);

if punched > 0 {
eprintln!(
" sparsified: punched {} zero pages ({} MiB saved on disk)",
punched,
punched * 4 / 1024
);
}

Ok(())
}

#[cfg(not(any(target_os = "linux", target_os = "windows")))]
fn sparsify_snapshot(_path: &Path) -> Result<()> {
Ok(())
}

// ---------------------------------------------------------------------------
// FsSandbox tests — prove that host-side path resolution rejects escapes.
//
Expand Down
Loading