@@ -830,216 +830,6 @@ impl FsSandbox {
830830 }
831831 Ok ( out)
832832 }
833-
834- /// Register all FS tool handlers on `registry`:
835- ///
836- /// - `fs_read` / `fs_write` — UTF-8 text read/write (whole-file).
837- /// - `fs_read_bytes` / `fs_write_bytes` — binary read/write with
838- /// optional offset/length/append, base64-encoded payloads.
839- /// - `fs_list` — directory enumeration as `{name, is_dir, is_file, is_symlink}`.
840- /// - `fs_stat` — size + file/dir metadata.
841- /// - `fs_mkdir` / `fs_unlink` — create/remove directory or file.
842- /// - `fs_truncate` — set file length.
843- ///
844- /// Every handler resolves its `path` argument under [`root`](Self::root)
845- /// via `FsSandbox::resolve`, which rejects `..` escapes, absolute
846- /// paths that climb outside the root, and symlinks pointing outside.
847- ///
848- /// The handlers call through `std::fs`, which behaves differently on
849- /// Linux and Windows — `normalize_fs_error` smooths out the error
850- /// wording before responses go back to the guest, so the Unikraft
851- /// guest's substring-matching classifier works on both hosts.
852- pub fn register ( self , registry : & mut ToolRegistry ) {
853- use serde_json:: json;
854-
855- let s = self . clone ( ) ;
856- registry. register ( "fs_read" , move |args| {
857- let path = args[ "path" ]
858- . as_str ( )
859- . ok_or_else ( || anyhow ! ( "fs_read: missing 'path'" ) ) ?;
860- let target = s. resolve ( path) ?;
861- let text = std:: fs:: read_to_string ( & target)
862- . map_err ( |e| anyhow ! ( "fs_read {:?}: {}" , path, e) ) ?;
863- Ok ( json ! ( { "text" : text } ) )
864- } ) ;
865-
866- let s = self . clone ( ) ;
867- registry. register ( "fs_write" , move |args| {
868- let path = args[ "path" ]
869- . as_str ( )
870- . ok_or_else ( || anyhow ! ( "fs_write: missing 'path'" ) ) ?;
871- let text = args[ "text" ]
872- . as_str ( )
873- . ok_or_else ( || anyhow ! ( "fs_write: missing 'text'" ) ) ?;
874- let append = args[ "append" ] . as_bool ( ) . unwrap_or ( false ) ;
875- let target = s. resolve ( path) ?;
876- // Create parent dirs? No — guest must fs_mkdir explicitly.
877- use std:: io:: Write ;
878- let mut f = std:: fs:: OpenOptions :: new ( )
879- . write ( true )
880- . create ( true )
881- . truncate ( !append)
882- . append ( append)
883- . open ( & target)
884- . map_err ( |e| anyhow ! ( "fs_write {:?}: {}" , path, e) ) ?;
885- f. write_all ( text. as_bytes ( ) )
886- . map_err ( |e| anyhow ! ( "fs_write {:?}: {}" , path, e) ) ?;
887- Ok ( json ! ( { "bytes_written" : text. len( ) } ) )
888- } ) ;
889-
890- let s = self . clone ( ) ;
891- registry. register ( "fs_list" , move |args| {
892- let path = args[ "path" ] . as_str ( ) . unwrap_or ( "" ) ;
893- let target = s. resolve ( path) ?;
894- let mut entries = Vec :: new ( ) ;
895- for entry in
896- std:: fs:: read_dir ( & target) . map_err ( |e| anyhow ! ( "fs_list {:?}: {}" , path, e) ) ?
897- {
898- let entry = entry?;
899- let name = entry. file_name ( ) . to_string_lossy ( ) . into_owned ( ) ;
900- let ft = entry. file_type ( ) ?;
901- entries. push ( json ! ( {
902- "name" : name,
903- "is_dir" : ft. is_dir( ) ,
904- "is_file" : ft. is_file( ) ,
905- "is_symlink" : ft. is_symlink( ) ,
906- } ) ) ;
907- }
908- Ok ( json ! ( { "entries" : entries } ) )
909- } ) ;
910-
911- let s = self . clone ( ) ;
912- registry. register ( "fs_stat" , move |args| {
913- let path = args[ "path" ]
914- . as_str ( )
915- . ok_or_else ( || anyhow ! ( "fs_stat: missing 'path'" ) ) ?;
916- let target = s. resolve ( path) ?;
917- let md =
918- std:: fs:: metadata ( & target) . map_err ( |e| anyhow ! ( "fs_stat {:?}: {}" , path, e) ) ?;
919- Ok ( json ! ( {
920- "size" : md. len( ) ,
921- "is_dir" : md. is_dir( ) ,
922- "is_file" : md. is_file( ) ,
923- } ) )
924- } ) ;
925-
926- let s = self . clone ( ) ;
927- registry. register ( "fs_mkdir" , move |args| {
928- let path = args[ "path" ]
929- . as_str ( )
930- . ok_or_else ( || anyhow ! ( "fs_mkdir: missing 'path'" ) ) ?;
931- let parents = args[ "parents" ] . as_bool ( ) . unwrap_or ( false ) ;
932- let target = s. resolve ( path) ?;
933- if parents {
934- std:: fs:: create_dir_all ( & target)
935- } else {
936- std:: fs:: create_dir ( & target)
937- }
938- . map_err ( |e| anyhow ! ( "fs_mkdir {:?}: {}" , path, e) ) ?;
939- Ok ( json ! ( { } ) )
940- } ) ;
941-
942- // fs_read_bytes / fs_write_bytes — binary variants for the Phase B
943- // transparent POSIX shim. Bytes are base64-encoded in the JSON
944- // payload so arbitrary binary content round-trips intact.
945- //
946- // fs_read_bytes args: { path, offset?, len? } → { data: "<base64>", eof: bool }
947- // fs_write_bytes args: { path, data: "<base64>", offset?, append? } → { bytes_written }
948- let s = self . clone ( ) ;
949- registry. register ( "fs_read_bytes" , move |args| {
950- use base64:: Engine ;
951- use std:: io:: { Read , Seek , SeekFrom } ;
952- let path = args[ "path" ]
953- . as_str ( )
954- . ok_or_else ( || anyhow ! ( "fs_read_bytes: missing 'path'" ) ) ?;
955- let offset = args[ "offset" ] . as_u64 ( ) . unwrap_or ( 0 ) ;
956- let want = args[ "len" ] . as_u64 ( ) . unwrap_or ( 65536 ) . min ( MAX_FS_READ ) ;
957- let target = s. resolve ( path) ?;
958- let mut f = std:: fs:: File :: open ( & target)
959- . map_err ( |e| anyhow ! ( "fs_read_bytes {:?}: {}" , path, e) ) ?;
960- if offset > 0 {
961- f. seek ( SeekFrom :: Start ( offset) )
962- . map_err ( |e| anyhow ! ( "fs_read_bytes seek {:?}: {}" , path, e) ) ?;
963- }
964- let mut buf = vec ! [ 0u8 ; want as usize ] ;
965- let n = f
966- . read ( & mut buf)
967- . map_err ( |e| anyhow ! ( "fs_read_bytes {:?}: {}" , path, e) ) ?;
968- buf. truncate ( n) ;
969- let eof = n < want as usize ;
970- let encoded = base64:: engine:: general_purpose:: STANDARD . encode ( & buf) ;
971- Ok ( json ! ( { "data" : encoded, "eof" : eof, "bytes_read" : n } ) )
972- } ) ;
973-
974- let s = self . clone ( ) ;
975- registry. register ( "fs_write_bytes" , move |args| {
976- use base64:: Engine ;
977- use std:: io:: { Seek , SeekFrom , Write } ;
978- let path = args[ "path" ]
979- . as_str ( )
980- . ok_or_else ( || anyhow ! ( "fs_write_bytes: missing 'path'" ) ) ?;
981- let data_b64 = args[ "data" ]
982- . as_str ( )
983- . ok_or_else ( || anyhow ! ( "fs_write_bytes: missing 'data'" ) ) ?;
984- let data = base64:: engine:: general_purpose:: STANDARD
985- . decode ( data_b64)
986- . map_err ( |e| anyhow ! ( "fs_write_bytes: bad base64: {}" , e) ) ?;
987- let offset = args[ "offset" ] . as_u64 ( ) ;
988- let append = args[ "append" ] . as_bool ( ) . unwrap_or ( false ) ;
989- let target = s. resolve ( path) ?;
990- let mut f = std:: fs:: OpenOptions :: new ( )
991- . write ( true )
992- . create ( true )
993- . truncate ( offset. is_none ( ) && !append)
994- . append ( append)
995- . open ( & target)
996- . map_err ( |e| anyhow ! ( "fs_write_bytes {:?}: {}" , path, e) ) ?;
997- if let Some ( off) = offset {
998- if !append {
999- f. seek ( SeekFrom :: Start ( off) )
1000- . map_err ( |e| anyhow ! ( "fs_write_bytes seek {:?}: {}" , path, e) ) ?;
1001- }
1002- }
1003- f. write_all ( & data)
1004- . map_err ( |e| anyhow ! ( "fs_write_bytes {:?}: {}" , path, e) ) ?;
1005- Ok ( json ! ( { "bytes_written" : data. len( ) } ) )
1006- } ) ;
1007-
1008- let s = self . clone ( ) ;
1009- registry. register ( "fs_truncate" , move |args| {
1010- let path = args[ "path" ]
1011- . as_str ( )
1012- . ok_or_else ( || anyhow ! ( "fs_truncate: missing 'path'" ) ) ?;
1013- let length = args[ "length" ]
1014- . as_u64 ( )
1015- . ok_or_else ( || anyhow ! ( "fs_truncate: missing 'length'" ) ) ?;
1016- let target = s. resolve ( path) ?;
1017- let f = std:: fs:: OpenOptions :: new ( )
1018- . write ( true )
1019- . open ( & target)
1020- . map_err ( |e| anyhow ! ( "fs_truncate {:?}: {}" , path, e) ) ?;
1021- f. set_len ( length)
1022- . map_err ( |e| anyhow ! ( "fs_truncate {:?}: {}" , path, e) ) ?;
1023- Ok ( json ! ( { } ) )
1024- } ) ;
1025-
1026- let s = self . clone ( ) ;
1027- registry. register ( "fs_unlink" , move |args| {
1028- let path = args[ "path" ]
1029- . as_str ( )
1030- . ok_or_else ( || anyhow ! ( "fs_unlink: missing 'path'" ) ) ?;
1031- let target = s. resolve ( path) ?;
1032- let md =
1033- std:: fs:: metadata ( & target) . map_err ( |e| anyhow ! ( "fs_unlink {:?}: {}" , path, e) ) ?;
1034- if md. is_dir ( ) {
1035- std:: fs:: remove_dir ( & target)
1036- } else {
1037- std:: fs:: remove_file ( & target)
1038- }
1039- . map_err ( |e| anyhow ! ( "fs_unlink {:?}: {}" , path, e) ) ?;
1040- Ok ( json ! ( { } ) )
1041- } ) ;
1042- }
1043833}
1044834
1045835/// Internal helper: assemble the final tool registry from caller-supplied
@@ -1389,14 +1179,18 @@ fn register_net_tools(
13891179 // SOL_SOCKET=1, SO_REUSEADDR=2
13901180 if level == 1 && optname == 2 {
13911181 sock. set_reuse_address ( value != 0 ) ?;
1392- }
13931182 // SOL_SOCKET=1, SO_KEEPALIVE=9
1394- if level == 1 && optname == 9 {
1183+ } else if level == 1 && optname == 9 {
13951184 sock. set_keepalive ( value != 0 ) ?;
1396- }
13971185 // IPPROTO_TCP=6, TCP_NODELAY=1
1398- if level == 6 && optname == 1 {
1186+ } else if level == 6 && optname == 1 {
13991187 sock. set_nodelay ( value != 0 ) ?;
1188+ } else {
1189+ return Err ( anyhow ! (
1190+ "unsupported socket option: level={}, optname={}" ,
1191+ level,
1192+ optname
1193+ ) ) ;
14001194 }
14011195 Ok ( json ! ( { } ) )
14021196 } ) ;
@@ -1417,7 +1211,11 @@ fn register_net_tools(
14171211 } else if level == 6 && optname == 1 {
14181212 sock. nodelay ( ) ? as i32
14191213 } else {
1420- 0
1214+ return Err ( anyhow ! (
1215+ "unsupported socket option: level={}, optname={}" ,
1216+ level,
1217+ optname
1218+ ) ) ;
14211219 } ;
14221220 Ok ( json ! ( { "value" : val } ) )
14231221 } ) ;
@@ -2657,10 +2455,11 @@ mod tests {
26572455 // End-to-end through the tool registry: the error surface the
26582456 // guest actually sees.
26592457 let root = tmpdir ( "dispatch" ) ;
2458+ let preopens = vec ! [ Preopen :: new( & root, "/host" ) . unwrap( ) ] ;
26602459 let mut reg = ToolRegistry :: new ( ) ;
2661- FsSandbox :: new ( & root ) . unwrap ( ) . register ( & mut reg) ;
2460+ FsRouter :: new ( & preopens ) . unwrap ( ) . register ( & mut reg) ;
26622461
2663- let req = br#"{"name":"fs_read","args":{"path":"../outside.txt"}}"# ;
2462+ let req = br#"{"name":"fs_read","args":{"path":"/host/ ../outside.txt"}}"# ;
26642463 let resp = reg. dispatch ( req) ;
26652464 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
26662465 assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
@@ -2740,15 +2539,16 @@ mod tests {
27402539 #[ test]
27412540 fn fs_write_then_read_roundtrip ( ) {
27422541 let root = tmpdir ( "roundtrip" ) ;
2542+ let preopens = vec ! [ Preopen :: new( & root, "/host" ) . unwrap( ) ] ;
27432543 let mut reg = ToolRegistry :: new ( ) ;
2744- FsSandbox :: new ( & root ) . unwrap ( ) . register ( & mut reg) ;
2544+ FsRouter :: new ( & preopens ) . unwrap ( ) . register ( & mut reg) ;
27452545
2746- let w = br#"{"name":"fs_write","args":{"path":"hello.txt","text":"hi"}}"# ;
2546+ let w = br#"{"name":"fs_write","args":{"path":"/host/ hello.txt","text":"hi"}}"# ;
27472547 let resp = reg. dispatch ( w) ;
27482548 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
27492549 assert ! ( s. contains( "\" bytes_written\" :2" ) , "{s}" ) ;
27502550
2751- let r = br#"{"name":"fs_read","args":{"path":"hello.txt"}}"# ;
2551+ let r = br#"{"name":"fs_read","args":{"path":"/host/ hello.txt"}}"# ;
27522552 let resp = reg. dispatch ( r) ;
27532553 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
27542554 assert ! ( s. contains( "\" text\" :\" hi\" " ) , "{s}" ) ;
@@ -3006,10 +2806,12 @@ mod tests {
30062806 fn test_fs_read_bytes_capped ( ) {
30072807 let root = tmpdir ( "readcap" ) ;
30082808 fs:: write ( root. join ( "small.bin" ) , b"hello" ) . unwrap ( ) ;
2809+ let preopens = vec ! [ Preopen :: new( & root, "/host" ) . unwrap( ) ] ;
30092810 let mut reg = ToolRegistry :: new ( ) ;
3010- FsSandbox :: new ( & root ) . unwrap ( ) . register ( & mut reg) ;
2811+ FsRouter :: new ( & preopens ) . unwrap ( ) . register ( & mut reg) ;
30112812
3012- let req = br#"{"name":"fs_read_bytes","args":{"path":"small.bin","len":1099511627776}}"# ;
2813+ let req =
2814+ br#"{"name":"fs_read_bytes","args":{"path":"/host/small.bin","len":1099511627776}}"# ;
30132815 let resp = reg. dispatch ( req) ;
30142816 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
30152817 assert ! ( !s. contains( "\" error\" " ) , "should succeed: {s}" ) ;
0 commit comments