@@ -91,6 +91,12 @@ const PAGE_SIZE: usize = 4096;
9191/// Reject these early on the host before we even boot the guest.
9292const RESERVED_GUEST_MOUNTPOINTS : & [ & str ] = & [ "/" , "/bin" , "/dev" , "/proc" , "/sys" , "/usr" ] ;
9393
94+ /// Cap for `fs_read_bytes` allocation to prevent guest-controlled OOM (16 MiB).
95+ const MAX_FS_READ : u64 = 16 * 1024 * 1024 ;
96+
97+ /// Cap for `__hl_sleep` duration to prevent unbounded host-thread blocking (60 s).
98+ const MAX_SLEEP_NS : u64 = 60_000_000_000 ;
99+
94100/// A preopened host directory exposed to the guest.
95101///
96102/// Semantics mirror Wasmtime's `preopened_dir`: `host_dir` is canonicalised
@@ -888,7 +894,7 @@ impl FsSandbox {
888894 . as_str ( )
889895 . ok_or_else ( || anyhow ! ( "fs_read_bytes: missing 'path'" ) ) ?;
890896 let offset = args[ "offset" ] . as_u64 ( ) . unwrap_or ( 0 ) ;
891- let want = args[ "len" ] . as_u64 ( ) . unwrap_or ( 65536 ) ;
897+ let want = args[ "len" ] . as_u64 ( ) . unwrap_or ( 65536 ) . min ( MAX_FS_READ ) ;
892898 let target = s. resolve ( path) ?;
893899 let mut f = std:: fs:: File :: open ( & target)
894900 . map_err ( |e| anyhow ! ( "fs_read_bytes {:?}: {}" , path, e) ) ?;
@@ -1013,7 +1019,7 @@ fn register_internal_tools(
10131019 Ok ( serde_json:: json!( { } ) )
10141020 } ) ;
10151021 tools. register ( "__hl_sleep" , |args| {
1016- let ns = args[ "ns" ] . as_u64 ( ) . unwrap_or ( 0 ) ;
1022+ let ns = args[ "ns" ] . as_u64 ( ) . unwrap_or ( 0 ) . min ( MAX_SLEEP_NS ) ;
10171023 if ns > 0 {
10181024 std:: thread:: sleep ( std:: time:: Duration :: from_nanos ( ns) ) ;
10191025 }
@@ -1492,7 +1498,7 @@ impl FsRouter {
14921498 . as_str ( )
14931499 . ok_or_else ( || anyhow ! ( "fs_read_bytes: missing 'path'" ) ) ?;
14941500 let offset = args[ "offset" ] . as_u64 ( ) . unwrap_or ( 0 ) ;
1495- let want = args[ "len" ] . as_u64 ( ) . unwrap_or ( 65536 ) ;
1501+ let want = args[ "len" ] . as_u64 ( ) . unwrap_or ( 65536 ) . min ( MAX_FS_READ ) ;
14961502 let ( fs, rel) = r. route ( path) ?;
14971503 let target = fs. resolve ( rel) ?;
14981504 let mut f = std:: fs:: File :: open ( & target)
@@ -2790,4 +2796,43 @@ mod tests {
27902796 assert ! ( s. contains( "\" error\" " ) , "net_bind should be denied: {s}" ) ;
27912797 assert ! ( s. contains( "Permission denied" ) , "{s}" ) ;
27922798 }
2799+
2800+ // -- Resource-limit tests ---------------------------------------------------
2801+
2802+ #[ test]
2803+ fn test_fs_read_bytes_capped ( ) {
2804+ // Request a huge len (well above MAX_FS_READ) on a small file.
2805+ // Without the cap this would try to allocate terabytes and OOM.
2806+ let root = tmpdir ( "readcap" ) ;
2807+ fs:: write ( root. join ( "small.bin" ) , b"hello" ) . unwrap ( ) ;
2808+ let mut reg = ToolRegistry :: new ( ) ;
2809+ FsSandbox :: new ( & root) . unwrap ( ) . register ( & mut reg) ;
2810+
2811+ // Ask for 1 TiB — the cap should silently clamp to MAX_FS_READ.
2812+ let req = br#"{"name":"fs_read_bytes","args":{"path":"small.bin","len":1099511627776}}"# ;
2813+ let resp = reg. dispatch ( req) ;
2814+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
2815+ assert ! ( !s. contains( "\" error\" " ) , "should succeed: {s}" ) ;
2816+ assert ! ( s. contains( "\" bytes_read\" :5" ) , "{s}" ) ;
2817+ }
2818+
2819+ #[ test]
2820+ fn test_sleep_capped ( ) {
2821+ // Verify the cap constant and that sleeping with a huge value
2822+ // completes quickly (the cap brings it down to 60 s max, but we
2823+ // pass 0 to keep the test instant — the important thing is
2824+ // confirming the cap constant exists and has the right value).
2825+ assert_eq ! ( MAX_SLEEP_NS , 60_000_000_000 ) ;
2826+
2827+ // Dispatch a sleep with ns=0 through the real handler to confirm
2828+ // the code path works.
2829+ let mut tools = ToolRegistry :: new ( ) ;
2830+ let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
2831+ register_internal_tools ( & mut tools, & exit_code, None , None ) ;
2832+
2833+ let req = br#"{"name":"__hl_sleep","args":{"ns":0}}"# ;
2834+ let resp = tools. dispatch ( req) ;
2835+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
2836+ assert ! ( !s. contains( "\" error\" " ) , "sleep(0) should succeed: {s}" ) ;
2837+ }
27932838}
0 commit comments