Skip to content

Commit 2e2a9c3

Browse files
committed
fix(runtime): reject blocking special module files
1 parent 7312667 commit 2e2a9c3

1 file changed

Lines changed: 192 additions & 6 deletions

File tree

‎src/runtime/module_snapshot.rs‎

Lines changed: 192 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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")]
506506
fn 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")]
556588
fn 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

Comments
 (0)