xtask/tests/advisory_floor.rs
//! The SSH stack's advisory floor over edda's committed `Cargo.lock` (lane OSSF-NEXT,
//! 2026-10-30).
//!
//! `nornir-git` is the git-over-SSH leaf every product links (Gunnar, Holger's estate,
//! dwarves), so the russh it resolves is the russh they ship. Two advisories name the
//! russh 0.63 line this lock carried: GHSA-j7h2-66j4-ghjf (a server accepted a publickey
//! signature made with another algorithm than the one offered; fixed in 0.64.0) and
//! GHSA-4wc5-f2rc-q74m (unbounded allocation from crafted agent constraints; fixed in
//! 0.64.1). pageant below 0.2.3 is GHSA-g4mp-vgx3-xrvm (out-of-bounds read in
//! `MemoryMap::read`).
//!
//! The four russh advisories of 2026-10-30 are fixed only in 0.65.0: GHSA-fw5c-8223-6h42
//! (a peer-declared zero maximum packet size stalls channel writes for good; servers too),
//! GHSA-4p9f-7p9m-5vfg (unbounded allocation from EXT_INFO in the client),
//! GHSA-9wfm-jcj8-jfv9 and GHSA-vp5q-ff62-jwqv (the agent server).
//!
//! gix 0.88.0 up to 0.90.0 is GHSA-q54r-jf2p-rfr2 (an untrusted `.git/config` turns off
//! the `.git` path protection checks; fixed in 0.90.0): `nornir-git` opens and checks out
//! worktrees with it, and Gunnar's client links that (row GIX-090, 2026-10-31).
//!
//! The test reads the lock, not the manifests: a `^0.64` requirement with a stale lock
//! still builds the vulnerable version. A floor that is raised later is one row here.
use std::path::Path;
/// (crate, lowest version without a known advisory, the advisories below it)
const FLOORS: &[(&str, (u64, u64, u64), &str)] = &[
("russh", (0, 65, 0), "GHSA-j7h2-66j4-ghjf, GHSA-4wc5-f2rc-q74m, GHSA-fw5c-8223-6h42, GHSA-4p9f-7p9m-5vfg, GHSA-9wfm-jcj8-jfv9, GHSA-vp5q-ff62-jwqv"),
("pageant", (0, 2, 3), "GHSA-g4mp-vgx3-xrvm"),
("gix", (0, 90, 0), "GHSA-q54r-jf2p-rfr2"),
];
fn parse(v: &str) -> (u64, u64, u64) {
let core = v.split(['-', '+']).next().unwrap_or(v);
let mut it = core.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
(it.next().unwrap_or(0), it.next().unwrap_or(0), it.next().unwrap_or(0))
}
/// Every `(name, version)` in a Cargo.lock, without a TOML dependency.
fn packages(lock: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut name: Option<String> = None;
for line in lock.lines() {
let line = line.trim();
if line == "[[package]]" {
name = None;
} else if let Some(v) = line.strip_prefix("name = ") {
name = Some(v.trim_matches('"').to_string());
} else if let Some(v) = line.strip_prefix("version = ") {
if let Some(n) = name.take() {
out.push((n, v.trim_matches('"').to_string()));
}
}
}
out
}
fn below_floor(lock: &str) -> Vec<String> {
let mut bad = Vec::new();
for (n, v) in packages(lock) {
for (crate_name, floor, ids) in FLOORS {
if n == *crate_name && parse(&v) < *floor {
bad.push(format!("{n} {v} < {}.{}.{} ({ids})", floor.0, floor.1, floor.2));
}
}
}
bad
}
#[test]
fn the_reader_sees_a_vulnerable_russh() {
// measure, never assert: the check must go red on the version it exists to refuse
let lock = "[[package]]\nname = \"russh\"\nversion = \"0.63.3\"\n\n[[package]]\nname = \"pageant\"\nversion = \"0.2.5\"\n";
let bad = below_floor(lock);
assert_eq!(bad.len(), 1, "{bad:?}");
assert!(bad[0].contains("GHSA-j7h2-66j4-ghjf"));
assert!(below_floor("[[package]]\nname = \"russh\"\nversion = \"0.65.0\"\n").is_empty());
// 0.64.1 fixed the agent constraints, and is still under the 2026-10-30 advisories
let bad = below_floor("[[package]]\nname = \"russh\"\nversion = \"0.64.1\"\n");
assert_eq!(bad.len(), 1, "{bad:?}");
assert!(bad[0].contains("GHSA-fw5c-8223-6h42"), "{bad:?}");
}
#[test]
fn the_reader_sees_a_vulnerable_gix() {
let bad = below_floor("[[package]]\nname = \"gix\"\nversion = \"0.88.0\"\n");
assert_eq!(bad.len(), 1, "{bad:?}");
assert!(bad[0].contains("GHSA-q54r-jf2p-rfr2"));
assert!(below_floor("[[package]]\nname = \"gix\"\nversion = \"0.90.0\"\n").is_empty());
// a gix-* plumbing crate is not gix itself
assert!(below_floor("[[package]]\nname = \"gix-hash\"\nversion = \"0.27.0\"\n").is_empty());
}
#[test]
fn edda_lock_has_no_ssh_stack_below_its_advisory_floor() {
let lock_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../Cargo.lock");
let lock = std::fs::read_to_string(&lock_path).expect("edda's Cargo.lock");
let bad = below_floor(&lock);
assert!(bad.is_empty(), "edda's lock carries crates below their advisory floor:\n{}", bad.join("\n"));
}