From 5ec6dd1375624b7289eae8e4e6dd88d432aba256 Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 22 May 2026 19:01:29 +0000 Subject: [PATCH 1/3] fix: increase Hyperlight I/O buffer size for large hostfs writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Hyperlight SDK defaults input/output shared-memory stacks to 16 KiB each. The hostfs VFS layer chunks writes at 32 KiB, but after base64 encoding + JSON envelope + FlatBuffer framing, a single chunk occupies ~44 KiB — overflowing the 16 KiB output stack and causing hcall_push to fail with EIO on writes exceeding ~12 KiB. Set the I/O buffer size to 128 KiB (sufficient for any single-chunk RPC with headroom) and expose it as a configurable field on VmConfig and SandboxBuilder so embedders can tune it for their workload. Signed-off-by: danbugs --- host/src/lib.rs | 29 +++++++++++++++++++++++++++++ host/tests/pyhl_runtime.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/host/src/lib.rs b/host/src/lib.rs index 427e4c5..917cdcc 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -528,13 +528,23 @@ impl ListenPorts { pub struct VmConfig { pub heap_size: u64, pub stack_size: u64, + pub io_buffer_size: usize, } +/// Hyperlight I/O buffer size (128 KiB). Each host function call is serialized +/// into a FlatBuffer and pushed onto a shared-memory stack whose capacity is +/// `io_buffer_size`. The hostfs VFS layer chunks writes at 32 KiB, but after +/// base64 encoding + JSON envelope + FlatBuffer framing a single chunk +/// occupies ~44 KiB. The Hyperlight SDK default (16 KiB) is too small; 128 KiB +/// accommodates any single-chunk RPC with comfortable headroom. +const DEFAULT_IO_BUFFER_SIZE: usize = 128 * 1024; + impl Default for VmConfig { fn default() -> Self { Self { heap_size: 512 * 1024 * 1024, stack_size: 8 * 1024 * 1024, + io_buffer_size: DEFAULT_IO_BUFFER_SIZE, } } } @@ -553,9 +563,17 @@ impl VmConfig { self } + /// Set the I/O buffer size for host function calls (default 128 KiB). + pub fn with_io_buffer_size(mut self, size: usize) -> Self { + self.io_buffer_size = size; + self + } + fn sandbox_config(&self) -> SandboxConfiguration { let mut cfg = SandboxConfiguration::default(); cfg.set_heap_size(self.heap_size); + cfg.set_input_data_size(self.io_buffer_size); + cfg.set_output_data_size(self.io_buffer_size); // Scratch holds page tables + CoW copies of writable pages touched at // runtime. pt_estimate covers page tables; the base covers kernel @@ -2008,6 +2026,7 @@ pub struct SandboxBuilder { args: Vec, heap_size: Option, stack_size: Option, + io_buffer_size: Option, preopens: Vec, network: Option, listen_ports: Option, @@ -2057,6 +2076,14 @@ impl SandboxBuilder { self } + /// Shared-memory I/O buffer size for host function calls (default 128 KiB). + /// Must be large enough to hold a single base64-encoded hostfs write chunk + /// plus JSON and FlatBuffer framing (~44 KiB for the default 32 KiB chunk). + pub fn io_buffer_size(mut self, bytes: usize) -> Self { + self.io_buffer_size = Some(bytes); + self + } + /// Expose a host directory to the guest. `lib/hostfs` mounts each /// `preopen.host_dir` at `preopen.guest_path`; FS tool handlers /// cover all of them and route by guest path prefix. Repeatable — @@ -2101,6 +2128,7 @@ impl SandboxBuilder { let config = VmConfig { heap_size: self.heap_size.unwrap_or(512 * 1024 * 1024), stack_size: self.stack_size.unwrap_or(8 * 1024 * 1024), + io_buffer_size: self.io_buffer_size.unwrap_or(DEFAULT_IO_BUFFER_SIZE), }; let tools = if self.has_tools { Some(self.tools) @@ -2154,6 +2182,7 @@ impl Sandbox { args: Vec::new(), heap_size: None, stack_size: None, + io_buffer_size: None, preopens: Vec::new(), network: None, listen_ports: None, diff --git a/host/tests/pyhl_runtime.rs b/host/tests/pyhl_runtime.rs index 66f372d..c670742 100644 --- a/host/tests/pyhl_runtime.rs +++ b/host/tests/pyhl_runtime.rs @@ -280,6 +280,38 @@ fn runtime_filesystem_mkdir_and_stat() { cleanup(&tmp); } +// --------------------------------------------------------------------------- +// Large binary write +// --------------------------------------------------------------------------- + +#[test] +fn runtime_filesystem_large_binary_write() { + let tmp = tempdir("hl-rt-bigwrite"); + let preopen = match Preopen::new(&tmp, "/host") { + Ok(p) => p, + Err(e) => { + eprintln!("SKIP: cannot create preopen: {e}"); + cleanup(&tmp); + return; + } + }; + let Some(mut rt) = setup_with_mount(preopen) else { + cleanup(&tmp); + return; + }; + + let timing = rt + .run_code("with open('/host/big.bin', 'wb') as f: f.write(b'x' * 40000)") + .unwrap(); + assert_eq!(timing.exit_code, 0); + + let data = std::fs::read(tmp.join("big.bin")).unwrap(); + assert_eq!(data.len(), 40000); + assert!(data.iter().all(|&b| b == b'x')); + + cleanup(&tmp); +} + // --------------------------------------------------------------------------- // Network policy enforcement // --------------------------------------------------------------------------- From 71758f8994d88c420b0badc98c51a991999cf6ba Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 22 May 2026 19:06:36 +0000 Subject: [PATCH 2/3] fix: address Copilot review feedback - Mark VmConfig as #[non_exhaustive] to prevent semver breakage when new fields are added - Use explicit 40 KiB (40 * 1024) in the large binary write test to match the PR description and make the boundary condition clearer Signed-off-by: danbugs --- host/src/lib.rs | 1 + host/tests/pyhl_runtime.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/host/src/lib.rs b/host/src/lib.rs index 917cdcc..66c54a3 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -525,6 +525,7 @@ impl ListenPorts { // --------------------------------------------------------------------------- /// Configuration for a Unikraft VM. +#[non_exhaustive] pub struct VmConfig { pub heap_size: u64, pub stack_size: u64, diff --git a/host/tests/pyhl_runtime.rs b/host/tests/pyhl_runtime.rs index c670742..eef968c 100644 --- a/host/tests/pyhl_runtime.rs +++ b/host/tests/pyhl_runtime.rs @@ -300,13 +300,14 @@ fn runtime_filesystem_large_binary_write() { return; }; + let size = 40 * 1024; let timing = rt - .run_code("with open('/host/big.bin', 'wb') as f: f.write(b'x' * 40000)") + .run_code(&format!("with open('/host/big.bin', 'wb') as f: f.write(b'x' * {})", size)) .unwrap(); assert_eq!(timing.exit_code, 0); let data = std::fs::read(tmp.join("big.bin")).unwrap(); - assert_eq!(data.len(), 40000); + assert_eq!(data.len(), size); assert!(data.iter().all(|&b| b == b'x')); cleanup(&tmp); From dab60b0b9b853193a02888f35c8660f47b640564 Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 22 May 2026 19:07:36 +0000 Subject: [PATCH 3/3] style: fix cargo fmt Signed-off-by: danbugs --- host/tests/pyhl_runtime.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/host/tests/pyhl_runtime.rs b/host/tests/pyhl_runtime.rs index eef968c..20b371a 100644 --- a/host/tests/pyhl_runtime.rs +++ b/host/tests/pyhl_runtime.rs @@ -302,7 +302,10 @@ fn runtime_filesystem_large_binary_write() { let size = 40 * 1024; let timing = rt - .run_code(&format!("with open('/host/big.bin', 'wb') as f: f.write(b'x' * {})", size)) + .run_code(&format!( + "with open('/host/big.bin', 'wb') as f: f.write(b'x' * {})", + size + )) .unwrap(); assert_eq!(timing.exit_code, 0);