Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/agent-artifact-admission/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use crate::{
const MAX_AUDIT_LINE_BYTES: usize = 64 * 1024;
#[cfg(target_os = "linux")]
const LINUX_O_NOFOLLOW: i32 = 0o400000;
#[cfg(target_os = "linux")]
const LINUX_O_NONBLOCK: i32 = 0o4000;

/// Minimized content-addressed artifact identity persisted in audit evidence.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -157,7 +159,7 @@ impl FileAuditSink {
.create(true)
.append(true)
.mode(0o600)
.custom_flags(LINUX_O_NOFOLLOW)
.custom_flags(LINUX_O_NOFOLLOW | LINUX_O_NONBLOCK)
.open(Path::new(&self.path))?;
if !file.metadata()?.is_file() {
return Err(io::Error::new(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#![cfg(target_os = "linux")]

use std::fs;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use wardnet_agent_artifact_admission::{
AdmissionPolicy, AuditError, AuditRecord, AuditSink, FileAuditSink, InstallIntent,
admission_decision, build_audit_record,
};

const FIFO_HELPER_PATH: &str = "WARDNET_AUDIT_FIFO_HELPER_PATH";
const HELPER_DEADLINE: Duration = Duration::from_secs(3);

fn temp_path(label: &str) -> std::path::PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock must be after Unix epoch")
.as_nanos();
std::env::temp_dir().join(format!(
"wardnet-agent-admission-{label}-{}-{nonce}",
std::process::id()
))
}

fn blocked_record() -> AuditRecord {
let intent = InstallIntent::unowned_llms_package_for_test();
let decision = admission_decision(&AdmissionPolicy::deny_all_for_test(), &intent);
build_audit_record(&intent, &decision).expect("audit record must build")
}

#[test]
fn file_sink_rejects_fifo_without_blocking_on_open() {
let fifo_path = temp_path("audit-fifo");
let mkfifo = Command::new("mkfifo")
.arg(&fifo_path)
.status()
.expect("mkfifo must be available on the Linux test runner");
assert!(mkfifo.success(), "FIFO fixture must be created");

let executable = std::env::current_exe().expect("current test executable must resolve");
let mut child = Command::new(executable)
.arg("--exact")
.arg("audit_fifo_open_helper")
.arg("--nocapture")
.env(FIFO_HELPER_PATH, &fifo_path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("isolated FIFO helper must start");

let deadline = Instant::now() + HELPER_DEADLINE;
let status = loop {
match child
.try_wait()
.expect("FIFO helper status must be readable")
{
Some(status) => break Some(status),
None if Instant::now() < deadline => thread::sleep(Duration::from_millis(25)),
None => break None,
}
};

let _ = fs::remove_file(&fifo_path);

match status {
Some(status) => assert!(
status.success(),
"audit FIFO helper must reject the special file through the stable storage error"
),
None => {
let _ = child.kill();
let _ = child.wait();
panic!(
"audit append blocked while opening a FIFO; special audit files must fail closed promptly"
);
}
}
}

#[test]
fn audit_fifo_open_helper() {
let Ok(path) = std::env::var(FIFO_HELPER_PATH) else {
return;
};

let result = FileAuditSink::new(path).append(&blocked_record());
assert_eq!(result, Err(AuditError::StorageUnavailable));
}
Loading