@@ -910,7 +910,7 @@ fn register_internal_tools(
910910 exit_code : & Arc < AtomicI32 > ,
911911 network : Option < & NetworkPolicy > ,
912912 listen_ports : Option < & ListenPorts > ,
913- ) {
913+ ) -> Option < Arc < Mutex < SocketTable > > > {
914914 let ec = exit_code. clone ( ) ;
915915 tools. register ( "__hl_exit" , move |args| {
916916 let code = args[ "code" ] . as_i64 ( ) . unwrap_or ( 1 ) as i32 ;
@@ -924,9 +924,7 @@ fn register_internal_tools(
924924 }
925925 Ok ( serde_json:: json!( { } ) )
926926 } ) ;
927- if let Some ( policy) = network {
928- register_net_tools ( tools, policy, listen_ports) ;
929- }
927+ network. map ( |policy| register_net_tools ( tools, policy, listen_ports) )
930928}
931929
932930// ---------------------------------------------------------------------------
@@ -941,6 +939,8 @@ enum HostSocket {
941939 Socket ( Socket , i32 ) ,
942940}
943941
942+ const MAX_SOCKETS : usize = 1024 ;
943+
944944struct SocketTable {
945945 sockets : HashMap < u64 , HostSocket > ,
946946 next_id : u64 ,
@@ -954,11 +954,19 @@ impl SocketTable {
954954 }
955955 }
956956
957- fn insert ( & mut self , sock : HostSocket ) -> u64 {
957+ fn insert ( & mut self , sock : HostSocket ) -> Result < u64 > {
958+ if self . sockets . len ( ) >= MAX_SOCKETS {
959+ return Err ( anyhow ! ( "socket limit reached (max {})" , MAX_SOCKETS ) ) ;
960+ }
958961 let id = self . next_id ;
959962 self . next_id += 1 ;
960963 self . sockets . insert ( id, sock) ;
961- id
964+ Ok ( id)
965+ }
966+
967+ fn clear ( & mut self ) {
968+ self . sockets . clear ( ) ;
969+ self . next_id = 1 ;
962970 }
963971
964972 fn get ( & self , fd : u64 ) -> Result < & HostSocket > {
@@ -1012,7 +1020,7 @@ fn register_net_tools(
10121020 tools : & mut ToolRegistry ,
10131021 policy : & NetworkPolicy ,
10141022 listen_ports : Option < & ListenPorts > ,
1015- ) {
1023+ ) -> Arc < Mutex < SocketTable > > {
10161024 use base64:: Engine ;
10171025 use serde_json:: json;
10181026
@@ -1045,7 +1053,7 @@ fn register_net_tools(
10451053 let fd = t
10461054 . lock ( )
10471055 . unwrap ( )
1048- . insert ( HostSocket :: Socket ( sock, sock_type) ) ;
1056+ . insert ( HostSocket :: Socket ( sock, sock_type) ) ? ;
10491057 Ok ( json ! ( { "fd" : fd } ) )
10501058 } ) ;
10511059
@@ -1106,7 +1114,7 @@ fn register_net_tools(
11061114 let new_fd = t
11071115 . lock ( )
11081116 . unwrap ( )
1109- . insert ( HostSocket :: Socket ( new_sock, parent_type) ) ;
1117+ . insert ( HostSocket :: Socket ( new_sock, parent_type) ) ? ;
11101118 let mut resp = json ! ( { "fd" : new_fd } ) ;
11111119 if let Some ( pa) = peer_addr {
11121120 resp[ "addr" ] = json ! ( pa. ip( ) . to_string( ) ) ;
@@ -1288,6 +1296,8 @@ fn register_net_tools(
12881296 Ok ( json ! ( { "addr" : "0.0.0.0" , "port" : 0 } ) )
12891297 }
12901298 } ) ;
1299+
1300+ table
12911301}
12921302
12931303/// Routes incoming fs_* tool calls to the matching `FsSandbox` by
@@ -1541,6 +1551,9 @@ pub struct Sandbox {
15411551 file_mapping_path : Option < std:: path:: PathBuf > ,
15421552 file_mapping_base : u64 ,
15431553 exit_code : Arc < AtomicI32 > ,
1554+ /// Shared socket table — cleared on [`Sandbox::restore`] so that
1555+ /// host-side fds don't leak across guest restore cycles.
1556+ socket_table : Option < Arc < Mutex < SocketTable > > > ,
15441557}
15451558
15461559/// Where the initrd comes from — either a file (zero-copy `map_file_cow`)
@@ -1750,14 +1763,14 @@ impl Sandbox {
17501763
17511764 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
17521765 let mut tools = build_tools ( tools, preopens) ?. unwrap_or_default ( ) ;
1753- register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
1766+ let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
17541767 let tools = Arc :: new ( tools) ;
17551768 let tools_ref = tools. clone ( ) ;
17561769 usbox. register_host_function ( "__dispatch" , move |payload : Vec < u8 > | -> Vec < u8 > {
17571770 tools_ref. dispatch ( & payload)
17581771 } ) ?;
17591772
1760- Self :: finish_evolve ( usbox, None , 0 , exit_code)
1773+ Self :: finish_evolve ( usbox, None , 0 , exit_code, socket_table )
17611774 }
17621775
17631776 /// Low-level: boot with a zero-copy mapped initrd file. Prefer the builder.
@@ -1802,7 +1815,7 @@ impl Sandbox {
18021815
18031816 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
18041817 let mut tools = build_tools ( tools, preopens) ?. unwrap_or_default ( ) ;
1805- register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
1818+ let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
18061819 let tools = Arc :: new ( tools) ;
18071820 let tools_ref = tools. clone ( ) ;
18081821 usbox. register_host_function ( "__dispatch" , move |payload : Vec < u8 > | -> Vec < u8 > {
@@ -1814,6 +1827,7 @@ impl Sandbox {
18141827 initrd_path. map ( |p| p. to_path_buf ( ) ) ,
18151828 INITRD_MAP_BASE ,
18161829 exit_code,
1830+ socket_table,
18171831 )
18181832 }
18191833
@@ -1822,6 +1836,7 @@ impl Sandbox {
18221836 file_mapping_path : Option < std:: path:: PathBuf > ,
18231837 file_mapping_base : u64 ,
18241838 exit_code : Arc < AtomicI32 > ,
1839+ socket_table : Option < Arc < Mutex < SocketTable > > > ,
18251840 ) -> Result < Self > {
18261841 let mut inner = usbox. evolve ( ) ?;
18271842 let snapshot = inner. snapshot ( ) . ok ( ) ;
@@ -1831,6 +1846,7 @@ impl Sandbox {
18311846 file_mapping_path,
18321847 file_mapping_base,
18331848 exit_code,
1849+ socket_table,
18341850 } )
18351851 }
18361852
@@ -1848,6 +1864,10 @@ impl Sandbox {
18481864 self . inner
18491865 . map_file_cow ( path, self . file_mapping_base , Some ( "initrd" ) ) ?;
18501866 }
1867+ // Close leaked host-side sockets the guest "forgot" about.
1868+ if let Some ( ref table) = self . socket_table {
1869+ table. lock ( ) . unwrap ( ) . clear ( ) ;
1870+ }
18511871 Ok ( ( ) )
18521872 }
18531873
@@ -2007,7 +2027,7 @@ impl Sandbox {
20072027
20082028 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
20092029 let mut tools = build_tools ( None , preopens) ?. unwrap_or_default ( ) ;
2010- register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
2030+ let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
20112031 let tools = Arc :: new ( tools) ;
20122032 let tools_ref = tools. clone ( ) ;
20132033
@@ -2029,6 +2049,7 @@ impl Sandbox {
20292049 file_mapping_path : initrd,
20302050 file_mapping_base : INITRD_MAP_BASE ,
20312051 exit_code,
2052+ socket_table,
20322053 } )
20332054 }
20342055}
@@ -2946,4 +2967,28 @@ mod tests {
29462967 "SO_TYPE should return 2 (DGRAM), got: {resp}"
29472968 ) ;
29482969 }
2970+
2971+ // -- SocketTable lifecycle tests ---------------------------------------------
2972+
2973+ #[ test]
2974+ fn net_socket_cap ( ) {
2975+ let mut table = SocketTable :: new ( ) ;
2976+ for _ in 0 ..MAX_SOCKETS {
2977+ let sock = Socket :: new ( Domain :: IPV4 , Type :: STREAM , None ) . unwrap ( ) ;
2978+ table. insert ( HostSocket :: Socket ( sock, 1 ) ) . unwrap ( ) ;
2979+ }
2980+ let sock = Socket :: new ( Domain :: IPV4 , Type :: STREAM , None ) . unwrap ( ) ;
2981+ assert ! ( table. insert( HostSocket :: Socket ( sock, 1 ) ) . is_err( ) ) ;
2982+ }
2983+
2984+ #[ test]
2985+ fn socket_table_clear ( ) {
2986+ let mut table = SocketTable :: new ( ) ;
2987+ let sock = Socket :: new ( Domain :: IPV4 , Type :: STREAM , None ) . unwrap ( ) ;
2988+ table. insert ( HostSocket :: Socket ( sock, 1 ) ) . unwrap ( ) ;
2989+ assert_eq ! ( table. sockets. len( ) , 1 ) ;
2990+ table. clear ( ) ;
2991+ assert_eq ! ( table. sockets. len( ) , 0 ) ;
2992+ assert_eq ! ( table. next_id, 1 ) ;
2993+ }
29492994}
0 commit comments