@@ -892,7 +892,7 @@ fn register_internal_tools(
892892 exit_code : & Arc < AtomicI32 > ,
893893 network : Option < & NetworkPolicy > ,
894894 listen_ports : Option < & ListenPorts > ,
895- ) {
895+ ) -> Option < Arc < Mutex < SocketTable > > > {
896896 let ec = exit_code. clone ( ) ;
897897 tools. register ( "__hl_exit" , move |args| {
898898 let code = args[ "code" ] . as_i64 ( ) . unwrap_or ( 1 ) as i32 ;
@@ -906,9 +906,7 @@ fn register_internal_tools(
906906 }
907907 Ok ( serde_json:: json!( { } ) )
908908 } ) ;
909- if let Some ( policy) = network {
910- register_net_tools ( tools, policy, listen_ports) ;
911- }
909+ network. map ( |policy| register_net_tools ( tools, policy, listen_ports) )
912910}
913911
914912// ---------------------------------------------------------------------------
@@ -923,6 +921,8 @@ enum HostSocket {
923921 Socket ( Socket , i32 ) ,
924922}
925923
924+ const MAX_SOCKETS : usize = 1024 ;
925+
926926struct SocketTable {
927927 sockets : HashMap < u64 , HostSocket > ,
928928 next_id : u64 ,
@@ -936,11 +936,19 @@ impl SocketTable {
936936 }
937937 }
938938
939- fn insert ( & mut self , sock : HostSocket ) -> u64 {
939+ fn insert ( & mut self , sock : HostSocket ) -> Result < u64 > {
940+ if self . sockets . len ( ) >= MAX_SOCKETS {
941+ return Err ( anyhow ! ( "socket limit reached (max {})" , MAX_SOCKETS ) ) ;
942+ }
940943 let id = self . next_id ;
941944 self . next_id += 1 ;
942945 self . sockets . insert ( id, sock) ;
943- id
946+ Ok ( id)
947+ }
948+
949+ fn clear ( & mut self ) {
950+ self . sockets . clear ( ) ;
951+ self . next_id = 1 ;
944952 }
945953
946954 fn get ( & self , fd : u64 ) -> Result < & HostSocket > {
@@ -994,7 +1002,7 @@ fn register_net_tools(
9941002 tools : & mut ToolRegistry ,
9951003 policy : & NetworkPolicy ,
9961004 listen_ports : Option < & ListenPorts > ,
997- ) {
1005+ ) -> Arc < Mutex < SocketTable > > {
9981006 use base64:: Engine ;
9991007 use serde_json:: json;
10001008
@@ -1027,7 +1035,7 @@ fn register_net_tools(
10271035 let fd = t
10281036 . lock ( )
10291037 . unwrap ( )
1030- . insert ( HostSocket :: Socket ( sock, sock_type) ) ;
1038+ . insert ( HostSocket :: Socket ( sock, sock_type) ) ? ;
10311039 Ok ( json ! ( { "fd" : fd } ) )
10321040 } ) ;
10331041
@@ -1088,7 +1096,7 @@ fn register_net_tools(
10881096 let new_fd = t
10891097 . lock ( )
10901098 . unwrap ( )
1091- . insert ( HostSocket :: Socket ( new_sock, parent_type) ) ;
1099+ . insert ( HostSocket :: Socket ( new_sock, parent_type) ) ? ;
10921100 let mut resp = json ! ( { "fd" : new_fd } ) ;
10931101 if let Some ( pa) = peer_addr {
10941102 resp[ "addr" ] = json ! ( pa. ip( ) . to_string( ) ) ;
@@ -1270,6 +1278,8 @@ fn register_net_tools(
12701278 Ok ( json ! ( { "addr" : "0.0.0.0" , "port" : 0 } ) )
12711279 }
12721280 } ) ;
1281+
1282+ table
12731283}
12741284
12751285/// Routes incoming fs_* tool calls to the matching `FsSandbox` by
@@ -1523,6 +1533,9 @@ pub struct Sandbox {
15231533 file_mapping_path : Option < std:: path:: PathBuf > ,
15241534 file_mapping_base : u64 ,
15251535 exit_code : Arc < AtomicI32 > ,
1536+ /// Shared socket table — cleared on [`Sandbox::restore`] so that
1537+ /// host-side fds don't leak across guest restore cycles.
1538+ socket_table : Option < Arc < Mutex < SocketTable > > > ,
15261539}
15271540
15281541/// Where the initrd comes from — either a file (zero-copy `map_file_cow`)
@@ -1732,14 +1745,14 @@ impl Sandbox {
17321745
17331746 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
17341747 let mut tools = build_tools ( tools, preopens) ?. unwrap_or_default ( ) ;
1735- register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
1748+ let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
17361749 let tools = Arc :: new ( tools) ;
17371750 let tools_ref = tools. clone ( ) ;
17381751 usbox. register_host_function ( "__dispatch" , move |payload : Vec < u8 > | -> Vec < u8 > {
17391752 tools_ref. dispatch ( & payload)
17401753 } ) ?;
17411754
1742- Self :: finish_evolve ( usbox, None , 0 , exit_code)
1755+ Self :: finish_evolve ( usbox, None , 0 , exit_code, socket_table )
17431756 }
17441757
17451758 /// Low-level: boot with a zero-copy mapped initrd file. Prefer the builder.
@@ -1784,7 +1797,7 @@ impl Sandbox {
17841797
17851798 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
17861799 let mut tools = build_tools ( tools, preopens) ?. unwrap_or_default ( ) ;
1787- register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
1800+ let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
17881801 let tools = Arc :: new ( tools) ;
17891802 let tools_ref = tools. clone ( ) ;
17901803 usbox. register_host_function ( "__dispatch" , move |payload : Vec < u8 > | -> Vec < u8 > {
@@ -1796,6 +1809,7 @@ impl Sandbox {
17961809 initrd_path. map ( |p| p. to_path_buf ( ) ) ,
17971810 INITRD_MAP_BASE ,
17981811 exit_code,
1812+ socket_table,
17991813 )
18001814 }
18011815
@@ -1804,6 +1818,7 @@ impl Sandbox {
18041818 file_mapping_path : Option < std:: path:: PathBuf > ,
18051819 file_mapping_base : u64 ,
18061820 exit_code : Arc < AtomicI32 > ,
1821+ socket_table : Option < Arc < Mutex < SocketTable > > > ,
18071822 ) -> Result < Self > {
18081823 let mut inner = usbox. evolve ( ) ?;
18091824 let snapshot = inner. snapshot ( ) . ok ( ) ;
@@ -1813,6 +1828,7 @@ impl Sandbox {
18131828 file_mapping_path,
18141829 file_mapping_base,
18151830 exit_code,
1831+ socket_table,
18161832 } )
18171833 }
18181834
@@ -1830,6 +1846,10 @@ impl Sandbox {
18301846 self . inner
18311847 . map_file_cow ( path, self . file_mapping_base , Some ( "initrd" ) ) ?;
18321848 }
1849+ // Close leaked host-side sockets the guest "forgot" about.
1850+ if let Some ( ref table) = self . socket_table {
1851+ table. lock ( ) . unwrap ( ) . clear ( ) ;
1852+ }
18331853 Ok ( ( ) )
18341854 }
18351855
@@ -1989,7 +2009,7 @@ impl Sandbox {
19892009
19902010 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
19912011 let mut tools = build_tools ( None , preopens) ?. unwrap_or_default ( ) ;
1992- register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
2012+ let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
19932013 let tools = Arc :: new ( tools) ;
19942014 let tools_ref = tools. clone ( ) ;
19952015
@@ -2011,6 +2031,7 @@ impl Sandbox {
20112031 file_mapping_path : initrd,
20122032 file_mapping_base : INITRD_MAP_BASE ,
20132033 exit_code,
2034+ socket_table,
20142035 } )
20152036 }
20162037}
@@ -2897,4 +2918,28 @@ mod tests {
28972918 "SO_TYPE should return 2 (DGRAM), got: {resp}"
28982919 ) ;
28992920 }
2921+
2922+ // -- SocketTable lifecycle tests ---------------------------------------------
2923+
2924+ #[ test]
2925+ fn net_socket_cap ( ) {
2926+ let mut table = SocketTable :: new ( ) ;
2927+ for _ in 0 ..MAX_SOCKETS {
2928+ let sock = Socket :: new ( Domain :: IPV4 , Type :: STREAM , None ) . unwrap ( ) ;
2929+ table. insert ( HostSocket :: Socket ( sock, 1 ) ) . unwrap ( ) ;
2930+ }
2931+ let sock = Socket :: new ( Domain :: IPV4 , Type :: STREAM , None ) . unwrap ( ) ;
2932+ assert ! ( table. insert( HostSocket :: Socket ( sock, 1 ) ) . is_err( ) ) ;
2933+ }
2934+
2935+ #[ test]
2936+ fn socket_table_clear ( ) {
2937+ let mut table = SocketTable :: new ( ) ;
2938+ let sock = Socket :: new ( Domain :: IPV4 , Type :: STREAM , None ) . unwrap ( ) ;
2939+ table. insert ( HostSocket :: Socket ( sock, 1 ) ) . unwrap ( ) ;
2940+ assert_eq ! ( table. sockets. len( ) , 1 ) ;
2941+ table. clear ( ) ;
2942+ assert_eq ! ( table. sockets. len( ) , 0 ) ;
2943+ assert_eq ! ( table. next_id, 1 ) ;
2944+ }
29002945}
0 commit comments