use common::toctou_check::{LinterAction, run_linter};
#[tokio::test]
async fn strict_resolution_rejects_symlinked_prefix() -> anyhow::Result<()> {
if !common::safedir::openat2_available() {
eprintln!("skipping: this kernel lacks openat2(2)");
return Ok(());
}
let tmp = tempfile::tempdir()?;
let tmp = tokio::fs::canonicalize(tmp.path()).await?;
tokio::fs::create_dir_all(tmp.join("real/sub")).await?;
tokio::fs::write(tmp.join("real/a.txt"), b"x").await?;
tokio::fs::symlink(tmp.join("real"), tmp.join("link")).await?;
common::safedir::enable_strict_operand_resolution();
let err =
common::safedir::Dir::open_root_dir(&tmp.join("link/sub"), false, common::Side::Source)
.await
.expect_err("strict resolution must refuse a symlinked prefix component");
assert_eq!(
err.raw_os_error(),
Some(libc::ELOOP),
"expected ELOOP, got: {err:?}"
);
let err = common::safedir::Dir::open_parent_dir(&tmp.join("link"), common::Side::Source)
.await
.expect_err("strict resolution must refuse a symlinked parent operand");
assert_eq!(
err.raw_os_error(),
Some(libc::ELOOP),
"expected ELOOP, got: {err:?}"
);
let root =
common::safedir::Dir::open_root_dir(&tmp.join("real"), false, common::Side::Source).await?;
let (_file, _meta) = root.open_file_read(std::ffi::OsStr::new("a.txt")).await?;
let parent = common::safedir::Dir::open_parent_dir(&tmp.join("real"), common::Side::Source)
.await?
.into_tree();
parent.open_dir(std::ffi::OsStr::new("sub")).await?;
Ok(())
}
#[tokio::test]
async fn strict_probe_separates_intermediate_from_final() -> anyhow::Result<()> {
use common::safedir::strict_probe_dst_kind;
if !common::safedir::openat2_available() {
eprintln!("skipping: this kernel lacks openat2(2)");
return Ok(());
}
let tmp = tempfile::tempdir()?;
let tmp = tokio::fs::canonicalize(tmp.path()).await?;
tokio::fs::create_dir_all(tmp.join("real/dir")).await?;
tokio::fs::write(tmp.join("real/file.txt"), b"x").await?;
tokio::fs::symlink(tmp.join("real"), tmp.join("prefixlink")).await?; tokio::fs::symlink(tmp.join("real/dir"), tmp.join("real/finallink")).await?;
common::safedir::enable_strict_operand_resolution();
let err = strict_probe_dst_kind(&tmp.join("prefixlink/file.txt"), common::Side::Destination)
.await
.expect_err("intermediate-prefix symlink must fail closed");
assert_eq!(err.raw_os_error(), Some(libc::ELOOP), "got: {err:?}");
assert_eq!(
strict_probe_dst_kind(&tmp.join("real/file.txt"), common::Side::Destination).await?,
Some(common::walk::EntryKind::File)
);
assert_eq!(
strict_probe_dst_kind(&tmp.join("real/finallink"), common::Side::Destination).await?,
Some(common::walk::EntryKind::Symlink)
);
assert_eq!(
strict_probe_dst_kind(&tmp.join("real/absent"), common::Side::Destination).await?,
None
);
Ok(())
}
#[test]
fn require_mode_arms_strict_resolution_on_proceed() {
let operands = vec![
std::path::PathBuf::from("/ok/src"),
std::path::PathBuf::from("/ok/dst"),
];
match run_linter(false, false, true, &operands) {
LinterAction::Proceed => {
assert!(
common::safedir::strict_operand_resolution(),
"linter Proceed in require mode must arm strict operand resolution"
);
}
LinterAction::Exit { output, code } => {
assert!(
!common::safedir::openat2_available(),
"good operands must proceed on openat2-capable kernels, got: {output}"
);
assert_eq!(code, 1);
assert!(output.contains("openat2"), "got: {output}");
}
}
}
#[test]
fn require_mode_with_no_operands_proceeds() {
match run_linter(false, false, true, &[]) {
LinterAction::Proceed => {
assert!(
common::safedir::strict_operand_resolution(),
"linter Proceed in require mode must arm strict operand resolution"
);
}
LinterAction::Exit { output, code } => {
assert!(
!common::safedir::openat2_available(),
"empty operand list must proceed on openat2-capable kernels, got: {output}"
);
assert_eq!(code, 1);
assert!(output.contains("openat2"), "got: {output}");
}
}
}