libgsh: complete scenario coverage for corpus_check execution paths

Adds the ReadFailed scenario (binary path resolves to a directory so
exists() succeeds but read() fails) and a scenarios coverage map at the
top of the test module. The map links each test to the audit fix
scenarios:

- valid CID, content matches: Allowed
- valid CID at admission, tampered content at execution: ContentMismatch
- missing binary where directory exists: Denied (sanity preserved)
- binary present but unreadable: ReadFailed (fail-closed)

Plus the existing sentinels for ungoverned-CID and corpus-not-mounted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
This commit is contained in:
Tyler J King 2026-04-25 03:18:56 -04:00
parent 13b393a7f1
commit 91f027ae61

View file

@ -149,11 +149,30 @@ pub fn corpus_check_with_base(corpus_cid: &str, command: &str, base_dir: &str) -
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
//! Scenario coverage map (execution half of the CID-content
//! verification audit fix):
//!
//! - **Valid CID, content matches: execution allowed** —
//! `binary_with_matching_content_is_allowed`.
//! - **Valid CID at admission, tampered content at execution:
//! execution denies** — `tampered_content_triggers_content_mismatch`.
//! - **Missing binary where directory exists: denied (existing
//! behavior preserved as sanity check)** —
//! `missing_binary_in_corpus_is_denied`.
//! - **Binary present but unreadable: denied fail-closed** —
//! `unreadable_binary_triggers_read_failed`.
//! - **Sentinel: ungoverned CID** — `ungoverned_skips_check`.
//! - **Sentinel: corpus directory not mounted on host** —
//! `missing_corpus_dir_reports_not_mounted`.
//!
//! The admission half (forged CID rejected at CRD reconcile) is
//! covered in corpus-operator::verifier.
use super::*; use super::*;
/// Write bytes to `dir/cid/name` and return the CID derived from those /// Write bytes to `dir/cid/name` and return the path so the caller can
/// bytes so the caller can pass a matching CID for the happy path or /// pass a matching CID for the happy path or a different one to
/// a different one to simulate tamper. /// simulate tamper.
fn write_binary(dir: &Path, cid: &str, name: &str, contents: &[u8]) -> PathBuf { fn write_binary(dir: &Path, cid: &str, name: &str, contents: &[u8]) -> PathBuf {
let corpus_dir = dir.join(cid); let corpus_dir = dir.join(cid);
std::fs::create_dir_all(&corpus_dir).unwrap(); std::fs::create_dir_all(&corpus_dir).unwrap();
@ -231,4 +250,30 @@ mod tests {
other => panic!("expected ContentMismatch, got {other:?}"), other => panic!("expected ContentMismatch, got {other:?}"),
} }
} }
/// Place a directory at the path where the binary should live; the
/// `exists()` check passes but `read()` fails. Verifies the fail-closed
/// path: an unreadable binary is denied rather than allowed.
#[test]
fn unreadable_binary_triggers_read_failed() {
let dir = tempfile::tempdir().unwrap();
let claimed = cid_of(b"any-content");
let corpus_dir = dir.path().join(&claimed);
// Make a directory at the binary path — it satisfies `exists()` but
// `read()` will fail with EISDIR or similar.
std::fs::create_dir_all(corpus_dir.join("kubectl")).unwrap();
let base = dir.path().to_str().unwrap();
match corpus_check_with_base(&claimed, "kubectl", base) {
CorpusCheckResult::ReadFailed {
corpus_cid,
command,
..
} => {
assert_eq!(corpus_cid, claimed);
assert_eq!(command, "kubectl");
}
other => panic!("expected ReadFailed, got {other:?}"),
}
}
} }