mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
test(cli): audit the strace trace by syscall name, not errno
The SPEC bar for T6 is failure on a single hit from the denied set; the errno-based check only caught attempts the snap's seccomp allowlist had already denied, so a host mount that succeeded would have passed. Exact-name matching over the traced call also stops "mount" matching "umount2" lines.
This commit is contained in:
1 parent
4a9d148a4b
commit
8047106733
1 file changed
+59
-6
@@ -328,6 +328,31 @@ fn fail(err: &anyhow::Error) -> std::process::ExitCode {
|
|||||||
/// runs in CI (`cargo test -- --ignored`).
|
/// runs in CI (`cargo test -- --ignored`).
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn traced_syscall_parses_strace_line_shapes() {
|
||||||
|
assert_eq!(
|
||||||
|
traced_syscall("1234 mount(\"/x\", \"/y\", 0) = 0"),
|
||||||
|
Some("mount")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
traced_syscall("[pid 1234] umount2(\"/x\", 0) = 0"),
|
||||||
|
Some("umount2")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
traced_syscall("1234 mount(\"/x\" <unfinished ...>"),
|
||||||
|
Some("mount")
|
||||||
|
);
|
||||||
|
// A resumed split half carries no call of its own: its '('
|
||||||
|
// was on the unfinished line above, which already named the
|
||||||
|
// syscall for the audit.
|
||||||
|
assert_eq!(
|
||||||
|
traced_syscall("[pid 1234] <... mount resumed>: \"/x\") = 0"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(traced_syscall("strace: Process 1234 attached"), None);
|
||||||
|
assert_eq!(traced_syscall("++++ exited with 0 ++++"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "needs the ecrou binary, libkrun, and strace; runs in CI"]
|
#[ignore = "needs the ecrou binary, libkrun, and strace; runs in CI"]
|
||||||
fn t6_no_mount_or_namespace_syscalls() {
|
fn t6_no_mount_or_namespace_syscalls() {
|
||||||
@@ -355,13 +380,41 @@ mod tests {
|
|||||||
assert!(out.success(), "the traced run failed; see the strace log");
|
assert!(out.success(), "the traced run failed; see the strace log");
|
||||||
let trace = std::fs::read_to_string(&trace_path).unwrap();
|
let trace = std::fs::read_to_string(&trace_path).unwrap();
|
||||||
let _ = std::fs::remove_file(&trace_path);
|
let _ = std::fs::remove_file(&trace_path);
|
||||||
|
// The SPEC's bar is zero hits, not zero failures: a mount that
|
||||||
|
// succeeded is as much a violation as one the snap's seccomp
|
||||||
|
// allowlist would have denied. The trace contains only the
|
||||||
|
// traced syscalls, so an exact name at the head of a line is a
|
||||||
|
// hit; exact comparison also keeps `mount` from matching
|
||||||
|
// `umount2`. Resumed-split lines (`<... mount resumed>`) carry
|
||||||
|
// the name behind a `<... ` prefix.
|
||||||
|
let denied: Vec<&str> = denied.split(',').collect();
|
||||||
for line in trace.lines() {
|
for line in trace.lines() {
|
||||||
let denied_call = denied.split(',').any(|syscall| line.contains(syscall));
|
let head = traced_syscall(line);
|
||||||
let failed = line.contains("= -1 EPERM") || line.contains("= -1 ENOSYS");
|
if let Some(head) = head {
|
||||||
assert!(
|
if denied.contains(&head) {
|
||||||
!(denied_call && failed),
|
panic!("denied syscall in the trace: {line}");
|
||||||
"denied syscall in the trace: {line}"
|
}
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The syscall name a strace output line traces, or None for
|
||||||
|
/// chatter. Line shapes covered: `1234 mount(…)`,
|
||||||
|
/// `[pid 1234] umount2(…)`, unfinished split halves, timestamps,
|
||||||
|
/// and lines without a call (`++++ exited with 0 ++++`). The call
|
||||||
|
/// is the last identifier-like token before the parenthesis; a
|
||||||
|
/// pid or a timestamp never qualifies (digits alone or
|
||||||
|
/// punctuation). A resumed split half has no parenthesis — the
|
||||||
|
/// unfinished half that names the call always precedes it, so
|
||||||
|
/// None loses the audit nothing.
|
||||||
|
fn traced_syscall(line: &str) -> Option<&str> {
|
||||||
|
let open = line.find('(')?;
|
||||||
|
let tokens: Vec<&str> = line[..open].split_whitespace().collect();
|
||||||
|
let is_call = |t: &str| {
|
||||||
|
!t.is_empty()
|
||||||
|
&& t.chars()
|
||||||
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
|
||||||
|
};
|
||||||
|
tokens.iter().rev().find(|t| is_call(t)).copied()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user