@@ -491,7 +491,7 @@ fn open_root_directory() -> io::Result<File> {
491491 let fd = unsafe {
492492 libc:: open (
493493 path. as_ptr ( ) . cast ( ) ,
494- libc:: O_RDONLY | libc:: O_DIRECTORY | libc:: O_CLOEXEC ,
494+ libc:: O_RDONLY | libc:: O_DIRECTORY | libc:: O_NONBLOCK | libc :: O_CLOEXEC ,
495495 )
496496 } ;
497497 if fd < 0 {
@@ -504,12 +504,21 @@ fn open_root_directory() -> io::Result<File> {
504504
505505#[ cfg( target_os = "linux" ) ]
506506fn open_directory_at ( parent : & File , name : & OsStr ) -> io:: Result < File > {
507- let directory = open_at (
507+ let directory = match open_at (
508508 parent,
509509 name,
510- libc:: O_RDONLY | libc:: O_NOFOLLOW | libc:: O_CLOEXEC ,
510+ libc:: O_RDONLY | libc:: O_DIRECTORY | libc :: O_NONBLOCK | libc :: O_NOFOLLOW | libc:: O_CLOEXEC ,
511511 0 ,
512- ) ?;
512+ ) {
513+ Ok ( directory) => directory,
514+ Err ( error)
515+ if error. raw_os_error ( ) == Some ( libc:: ENOTDIR )
516+ && entry_is_symlink_at ( parent, name) . unwrap_or ( false ) =>
517+ {
518+ return Err ( io:: Error :: from_raw_os_error ( libc:: ELOOP ) ) ;
519+ }
520+ Err ( error) => return Err ( error) ,
521+ } ;
513522 let metadata = directory. metadata ( ) ?;
514523 if !metadata. is_dir ( ) {
515524 return Err ( io:: Error :: from_raw_os_error ( libc:: ENOTDIR ) ) ;
@@ -522,7 +531,7 @@ fn open_readonly_at(parent: &File, name: &OsStr) -> io::Result<File> {
522531 open_at (
523532 parent,
524533 name,
525- libc:: O_RDONLY | libc:: O_NOFOLLOW | libc:: O_CLOEXEC ,
534+ libc:: O_RDONLY | libc:: O_NONBLOCK | libc :: O_NOFOLLOW | libc:: O_CLOEXEC ,
526535 0 ,
527536 )
528537}
@@ -552,6 +561,29 @@ fn open_at(parent: &File, name: &OsStr, flags: i32, mode: libc::mode_t) -> io::R
552561 Ok ( unsafe { File :: from_raw_fd ( fd) } )
553562}
554563
564+ #[ cfg( target_os = "linux" ) ]
565+ fn entry_is_symlink_at ( parent : & File , name : & OsStr ) -> io:: Result < bool > {
566+ let name = CString :: new ( name. as_bytes ( ) )
567+ . map_err ( |_| io:: Error :: new ( io:: ErrorKind :: InvalidInput , "NUL in path component" ) ) ?;
568+ let mut metadata = std:: mem:: MaybeUninit :: < libc:: stat > :: uninit ( ) ;
569+ // SAFETY: name is NUL terminated, parent owns a live directory fd, and
570+ // metadata points to writable storage for the kernel result.
571+ let result = unsafe {
572+ libc:: fstatat (
573+ parent. as_raw_fd ( ) ,
574+ name. as_ptr ( ) ,
575+ metadata. as_mut_ptr ( ) ,
576+ libc:: AT_SYMLINK_NOFOLLOW ,
577+ )
578+ } ;
579+ if result < 0 {
580+ return Err ( io:: Error :: last_os_error ( ) ) ;
581+ }
582+ // SAFETY: fstatat initialized metadata after returning success.
583+ let metadata = unsafe { metadata. assume_init ( ) } ;
584+ Ok ( ( metadata. st_mode & libc:: S_IFMT ) == libc:: S_IFLNK )
585+ }
586+
555587#[ cfg( target_os = "linux" ) ]
556588fn create_directory_at ( parent : & File , name : & OsStr ) -> io:: Result < ( ) > {
557589 let name = CString :: new ( name. as_bytes ( ) )
@@ -569,7 +601,7 @@ fn read_dir_names(dir: &File) -> Result<Vec<OsString>> {
569601 let independent = open_at (
570602 dir,
571603 OsStr :: new ( "." ) ,
572- libc:: O_RDONLY | libc:: O_DIRECTORY | libc:: O_CLOEXEC ,
604+ libc:: O_RDONLY | libc:: O_DIRECTORY | libc:: O_NONBLOCK | libc :: O_CLOEXEC ,
573605 0 ,
574606 )
575607 . map_err ( |_| tree_error ( ERR_MODULE_TREE_WALK ) ) ?;
@@ -1176,6 +1208,160 @@ mod tests {
11761208 root
11771209 }
11781210
1211+ #[ cfg( target_os = "linux" ) ]
1212+ fn make_fifo ( path : & Path ) {
1213+ use std:: ffi:: CString ;
1214+ use std:: os:: unix:: ffi:: OsStrExt ;
1215+
1216+ let name = CString :: new ( path. as_os_str ( ) . as_bytes ( ) ) . expect ( "fifo path" ) ;
1217+ // SAFETY: name is NUL terminated and points to a valid output path.
1218+ let result = unsafe { libc:: mkfifo ( name. as_ptr ( ) , 0o600 ) } ;
1219+ assert_eq ! (
1220+ result,
1221+ 0 ,
1222+ "mkfifo {}: {}" ,
1223+ path. display( ) ,
1224+ io:: Error :: last_os_error( )
1225+ ) ;
1226+ }
1227+
1228+ #[ cfg( target_os = "linux" ) ]
1229+ fn run_fifo_case ( case : & str , root : & Path ) {
1230+ let _ = fs:: remove_dir_all ( root) ;
1231+ fs:: create_dir_all ( root) . expect ( "fifo case root" ) ;
1232+ match case {
1233+ "irrelevant-tree" => {
1234+ let rss = root. join ( "rss" ) ;
1235+ let agent = rss. join ( "agent" ) ;
1236+ let nested = rss. join ( "nested" ) ;
1237+ fs:: create_dir_all ( & agent) . expect ( "agent dir" ) ;
1238+ fs:: create_dir_all ( & nested) . expect ( "nested dir" ) ;
1239+ let entry = agent. join ( "main.rss" ) ;
1240+ fs:: write (
1241+ & entry,
1242+ "pub fn run(input: map) -> string { \" fifo-tree\" ; }\n " ,
1243+ )
1244+ . expect ( "entry" ) ;
1245+ fs:: write (
1246+ nested. join ( "helper.rss" ) ,
1247+ "pub fn helper() -> string { \" helper\" ; }\n " ,
1248+ )
1249+ . expect ( "helper" ) ;
1250+ make_fifo ( & rss. join ( "irrelevant.pipe" ) ) ;
1251+
1252+ let error = capture_module_snapshot ( & entry) . expect_err ( "irrelevant FIFO" ) ;
1253+ assert_eq ! (
1254+ error. to_string( ) ,
1255+ "RustScript compile error: module tree walk failed"
1256+ ) ;
1257+ assert ! ( !error. to_string( ) . contains( root. to_string_lossy( ) . as_ref( ) ) ) ;
1258+ fs:: remove_file ( rss. join ( "irrelevant.pipe" ) ) . expect ( "remove FIFO" ) ;
1259+ let snapshot = capture_module_snapshot ( & entry) . expect ( "regular tree" ) ;
1260+ assert_eq ! ( snapshot. files( ) . len( ) , 2 ) ;
1261+ assert_eq ! ( snapshot. entry_rel( ) , "agent/main.rss" ) ;
1262+ }
1263+ "rss-fifo" => {
1264+ let rss = root. join ( "rss" ) ;
1265+ fs:: create_dir_all ( & rss) . expect ( "rss dir" ) ;
1266+ let entry = rss. join ( "main.rss" ) ;
1267+ make_fifo ( & entry) ;
1268+
1269+ let error = module_tree_digest ( & entry) . expect_err ( "FIFO .rss entry" ) ;
1270+ assert_eq ! (
1271+ error. to_string( ) ,
1272+ "RustScript compile error: module tree walk failed"
1273+ ) ;
1274+ assert ! ( !error. to_string( ) . contains( root. to_string_lossy( ) . as_ref( ) ) ) ;
1275+ }
1276+ "ancestor-fifo" => {
1277+ let ancestor = root. join ( "fifo" ) ;
1278+ fs:: create_dir_all ( root) . expect ( "ancestor root" ) ;
1279+ make_fifo ( & ancestor) ;
1280+ let entry = ancestor. join ( "rss" ) . join ( "agent" ) . join ( "main.rss" ) ;
1281+
1282+ let error = module_tree_digest ( & entry) . expect_err ( "FIFO ancestor" ) ;
1283+ assert_eq ! (
1284+ error. to_string( ) ,
1285+ "RustScript compile error: module tree walk failed"
1286+ ) ;
1287+ assert ! ( !error. to_string( ) . contains( root. to_string_lossy( ) . as_ref( ) ) ) ;
1288+ }
1289+ "cleanup-fifo" => {
1290+ let rss = root. join ( "rss" ) ;
1291+ fs:: create_dir_all ( & rss) . expect ( "rss dir" ) ;
1292+ let entry = rss. join ( "main.rss" ) ;
1293+ fs:: write (
1294+ & entry,
1295+ "pub fn run(input: map) -> string { \" cleanup\" ; }\n " ,
1296+ )
1297+ . expect ( "entry" ) ;
1298+ let snapshot = capture_module_snapshot ( & entry) . expect ( "snapshot" ) ;
1299+ let materialized = snapshot. materialize ( ) . expect ( "materialize" ) ;
1300+ let sandbox = materialized. sandbox ( ) . to_path_buf ( ) ;
1301+ make_fifo ( & sandbox. join ( "leftover.pipe" ) ) ;
1302+ drop ( materialized) ;
1303+ assert ! ( !sandbox. exists( ) , "FIFO cleanup must remove the sandbox" ) ;
1304+ }
1305+ _ => panic ! ( "unknown FIFO test case: {case}" ) ,
1306+ }
1307+ let _ = fs:: remove_dir_all ( root) ;
1308+ }
1309+
1310+ #[ cfg( target_os = "linux" ) ]
1311+ #[ test]
1312+ fn fifo_special_files_are_bounded ( ) {
1313+ use std:: process:: { Command , Stdio } ;
1314+ use std:: time:: { Duration , Instant } ;
1315+
1316+ const CASE_ENV : & str = "RUSTSCRIPT_AGENT_FIFO_CASE" ;
1317+ const ROOT_ENV : & str = "RUSTSCRIPT_AGENT_FIFO_ROOT" ;
1318+ const CASES : [ & str ; 4 ] = [
1319+ "irrelevant-tree" ,
1320+ "rss-fifo" ,
1321+ "ancestor-fifo" ,
1322+ "cleanup-fifo" ,
1323+ ] ;
1324+
1325+ if let Some ( case) = std:: env:: var_os ( CASE_ENV ) {
1326+ let root = PathBuf :: from ( std:: env:: var_os ( ROOT_ENV ) . expect ( "FIFO case root" ) ) ;
1327+ run_fifo_case ( & case. to_string_lossy ( ) , & root) ;
1328+ return ;
1329+ }
1330+
1331+ for case in CASES {
1332+ let root = test_root ( & format ! ( "fifo-{case}" ) ) ;
1333+ let _ = fs:: remove_dir_all ( & root) ;
1334+ let mut child = Command :: new ( std:: env:: current_exe ( ) . expect ( "test executable path" ) )
1335+ . args ( [
1336+ "--exact" ,
1337+ "runtime::module_snapshot::tests::fifo_special_files_are_bounded" ,
1338+ "--nocapture" ,
1339+ ] )
1340+ . env ( CASE_ENV , case)
1341+ . env ( ROOT_ENV , & root)
1342+ . stdin ( Stdio :: null ( ) )
1343+ . stdout ( Stdio :: inherit ( ) )
1344+ . stderr ( Stdio :: inherit ( ) )
1345+ . spawn ( )
1346+ . expect ( "FIFO child should start" ) ;
1347+ let deadline = Instant :: now ( ) + Duration :: from_secs ( 3 ) ;
1348+ let status = loop {
1349+ if let Some ( status) = child. try_wait ( ) . expect ( "FIFO child status" ) {
1350+ break status;
1351+ }
1352+ if Instant :: now ( ) >= deadline {
1353+ let _ = child. kill ( ) ;
1354+ let status = child. wait ( ) . expect ( "FIFO child must be reaped" ) ;
1355+ let _ = fs:: remove_dir_all ( & root) ;
1356+ panic ! ( "FIFO case {case} blocked beyond the bound; child status {status:?}" ) ;
1357+ }
1358+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
1359+ } ;
1360+ let _ = fs:: remove_dir_all ( & root) ;
1361+ assert ! ( status. success( ) , "FIFO case {case} failed with {status:?}" ) ;
1362+ }
1363+ }
1364+
11791365 #[ test]
11801366 fn snapshot_rejects_oversize_file ( ) {
11811367 let root = test_root ( "oversize" ) ;
0 commit comments