From 5adc5d57891af96fafcbe55bbba14f89560dc34a Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 15 Jul 2026 12:14:17 -0300 Subject: [PATCH] harden(syscalls): clamp private-input length to MAX_PRIVATE_INPUT_SIZE get_private_input_slice read a prover-controlled u32 length prefix and built a slice of that length with no upper bound. Clamp it to 64 MiB (the same cap the host enforces at store time). An honest length is always within bound, so this never changes behavior for real inputs; it only bounds the slice when a malformed/forged prefix claims more. Defense-in-depth only: on 64-bit the u32 length can't overflow the pointer range, and no writable region overlaps the oversized span today, so this is a documented-invariant / robustness guard, not a fix for a reachable bug. --- syscalls/src/syscalls.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index 71d15ba40..ad9947855 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -8,6 +8,15 @@ use core::arch::asm; #[cfg(target_arch = "riscv64")] pub const PRIVATE_INPUT_START: usize = 0xFF000000; +/// Maximum private-input length the guest will read, in bytes (64 MiB). +/// The host caps stored input at this size in `Memory::store_private_inputs`, +/// so an honest length prefix is always `<=` this bound; a larger value can only +/// come from a malformed or forged prefix. The reader clamps to this cap so a +/// bogus length can never make the guest fabricate an arbitrarily long slice. +/// Must match `executor::vm::memory::MAX_PRIVATE_INPUT_SIZE`. +#[cfg(target_arch = "riscv64")] +const MAX_PRIVATE_INPUT_SIZE: usize = 64 * 1024 * 1024; + #[cfg(target_arch = "riscv64")] pub enum SyscallNumbers { Print = 1, @@ -104,7 +113,12 @@ pub fn get_private_input_slice() -> &'static [u8] { // for the `'static` lifetime of the guest's single-threaded execution // region, which stays mapped and unmodified for the whole execution. let len_ptr = PRIVATE_INPUT_START as *const u32; - let len = unsafe { core::ptr::read_volatile(len_ptr) } as usize; + // Clamp the prover-written length prefix to `MAX_PRIVATE_INPUT_SIZE`. An + // honest prefix (written by the host, which caps stored input at this size) + // is always within bound, so clamping never changes behavior for real + // inputs — it only bounds the slice length when a malformed or forged prefix + // claims more, keeping the read deterministic. + let len = (unsafe { core::ptr::read_volatile(len_ptr) } as usize).min(MAX_PRIVATE_INPUT_SIZE); let data_ptr = (PRIVATE_INPUT_START + 4) as *const u8; unsafe { core::slice::from_raw_parts(data_ptr, len) } }