diff --git a/crates/lib/src/lints.rs b/crates/lib/src/lints.rs index e997f0ca2..46c8efc6d 100644 --- a/crates/lib/src/lints.rs +++ b/crates/lib/src/lints.rs @@ -345,7 +345,9 @@ fn lint_inner<'skip>( let name = lint.name; let r = match r { Ok(r) => r, - Err(e) => anyhow::bail!("Unexpected runtime error running lint {name}: {e}"), + Err(e) => { + return Err(e.context(format!("Unexpected runtime error running lint {name}"))); + } }; if let Err(e) = r { @@ -1586,4 +1588,55 @@ mod tests { ); Ok(()) } + + #[test] + fn test_runtime_deps_escape() -> Result<()> { + let root = &fixture()?; + root.create_dir("usr")?; + // Same shape as /usr/local -> ../../var/usrlocal, on the directory + // runtime dependency lookup actually searches. + root.symlink_contents("../../var/usrbin", "usr/bin")?; + + let config = &LintExecutionConfig::default(); + let mut out = Vec::new(); + let err = lint_inner( + root, + RootType::Alternative, + config, + LINTS + .iter() + .filter(|lint| lint.name != "runtime-deps") + .map(|lint| lint.name), + &mut out, + ) + .expect_err("lookup failure must remain a runtime error"); + + let bin = resolved_runtime_bins() + .into_iter() + .next() + .expect("runtime dependency"); + let attempted = format!("usr/bin/{bin}"); + assert_eq!( + err.to_string(), + "Unexpected runtime error running lint runtime-deps" + ); + assert!( + err.chain() + .any(|source| source.to_string().contains(&attempted)), + "{err:?}" + ); + assert!( + err.chain().any(|source| { + source + .downcast_ref::() + .is_some_and(|io_err| { + io_err + .to_string() + .contains("a path led outside of the filesystem") + }) + }), + "{err:?}" + ); + Ok(()) + } } diff --git a/crates/lib/src/utils.rs b/crates/lib/src/utils.rs index 544415e0f..fd0d613b7 100644 --- a/crates/lib/src/utils.rs +++ b/crates/lib/src/utils.rs @@ -93,7 +93,10 @@ fn executable_candidates(name: &str) -> Vec { pub fn have_executable_in_root(root: &Dir, name: &str) -> Result { for candidate in executable_candidates(name) { let candidate = candidate.strip_prefix("/").unwrap_or(&candidate); - if root.try_exists(candidate)? { + if root + .try_exists(candidate) + .with_context(|| format!("checking whether {} exists", candidate.display()))? + { return Ok(true); } } @@ -368,6 +371,61 @@ mod tests { root.write("usr/bin/podman", "")?; assert!(have_executable_in_root(&root, "podman")?); assert!(!have_executable_in_root(&root, "missing")?); + + // An absolute name replaces each PATH entry via PathBuf::push, so the + // candidate is independent of the process PATH. + const PROBE: &str = "/usr/local/bin/probe"; + let cases = [ + ("present", Some(true)), + ("absent", Some(false)), + ("escapes", None), + ("contained-symlink", Some(true)), + ]; + for (label, expected) in cases { + let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + match label { + "present" => { + root.create_dir_all("usr/local/bin")?; + root.write("usr/local/bin/probe", "")?; + } + "absent" => {} + "escapes" => { + root.create_dir("usr")?; + root.symlink_contents("../../var/usrlocal", "usr/local")?; + } + "contained-symlink" => { + root.create_dir_all("var/usrlocal/bin")?; + root.write("var/usrlocal/bin/probe", "")?; + root.create_dir("usr")?; + root.symlink_contents("../var/usrlocal", "usr/local")?; + } + _ => unreachable!("{label}"), + } + let found = have_executable_in_root(&root, PROBE); + match expected { + Some(expected) => assert_eq!(found?, expected, "{label}"), + None => { + let err = found.expect_err(label); + assert!( + err.chain() + .any(|source| source.to_string().contains("usr/local/bin/probe")), + "{label}: {err:?}" + ); + assert!( + err.chain().any(|source| { + source + .downcast_ref::() + .is_some_and(|io_err| { + io_err + .to_string() + .contains("a path led outside of the filesystem") + }) + }), + "{label}: {err:?}" + ); + } + } + } Ok(()) } }