187 lines
5.9 KiB
Rust
187 lines
5.9 KiB
Rust
#![allow(clippy::expect_used)]
|
|
#![doc = "Controllable fake-child coverage for synchronous worker execution."]
|
|
|
|
use std::{
|
|
ffi::OsString,
|
|
fs,
|
|
os::unix::fs::PermissionsExt as _,
|
|
path::{Path, PathBuf},
|
|
sync::atomic::{AtomicU32, Ordering},
|
|
time::Duration,
|
|
};
|
|
|
|
use ghidra_cli::{
|
|
cli::SandboxMode,
|
|
domain::{InvocationId, Limits, PositiveU64},
|
|
operation::{ADAPTER_PROTOCOL_VERSION, Operation},
|
|
process::{
|
|
Cancellation, Invocation, InvocationFailureKind, NativePhaseTimeouts, NeverCancel, run,
|
|
},
|
|
protocol::{AdapterRequest, InvocationFiles, RequestArguments},
|
|
sandbox::WorkerCommand,
|
|
};
|
|
use tempfile::TempDir;
|
|
|
|
fn positive(value: u64) -> PositiveU64 {
|
|
PositiveU64::try_from(value).expect("positive")
|
|
}
|
|
|
|
fn request() -> AdapterRequest {
|
|
AdapterRequest {
|
|
protocol_version: ADAPTER_PROTOCOL_VERSION,
|
|
invocation_id: InvocationId::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").expect("identifier"),
|
|
operation: Operation::Doctor,
|
|
staged_sample: None,
|
|
analysis_path: None,
|
|
limits: Limits {
|
|
max_heap_mib: positive(2048),
|
|
max_cpu: positive(2),
|
|
analysis_timeout_seconds: positive(600),
|
|
decompile_timeout_seconds: None,
|
|
child_watchdog_seconds: positive(120),
|
|
max_sample_bytes: positive(1_073_741_824),
|
|
max_inline_bytes: positive(65_536),
|
|
},
|
|
arguments: RequestArguments::Doctor,
|
|
}
|
|
}
|
|
|
|
fn fixture(behavior: &str, extra: &[&str]) -> (TempDir, InvocationFiles, WorkerCommand) {
|
|
let root = tempfile::tempdir().expect("temporary directory");
|
|
let invocation_directory = root.path().join("invocation");
|
|
fs::create_dir(&invocation_directory).expect("invocation directory");
|
|
let files = InvocationFiles::new(invocation_directory).expect("protocol paths");
|
|
let mut arguments = vec![
|
|
OsString::from(behavior),
|
|
files.request_path().into_os_string(),
|
|
files.response_path().into_os_string(),
|
|
];
|
|
arguments.extend(extra.iter().map(OsString::from));
|
|
let command = WorkerCommand::for_policy(
|
|
SandboxMode::Off,
|
|
PathBuf::from(env!("CARGO_BIN_EXE_ghidr-fake-child")),
|
|
arguments,
|
|
None,
|
|
)
|
|
.expect("worker command");
|
|
(root, files, command)
|
|
}
|
|
|
|
fn invocation<'a>(
|
|
request: &'a AdapterRequest,
|
|
files: &'a InvocationFiles,
|
|
command: &'a WorkerCommand,
|
|
) -> Invocation<'a> {
|
|
Invocation::new(
|
|
request,
|
|
files,
|
|
command,
|
|
NativePhaseTimeouts {
|
|
analysis_seconds: None,
|
|
decompile_seconds: None,
|
|
},
|
|
)
|
|
.expect("derived Invocation")
|
|
}
|
|
|
|
#[test]
|
|
fn one_attempt_writes_private_request_and_accepts_only_strict_final_response() {
|
|
let (_root, files, command) = fixture("success", &[]);
|
|
let request = request();
|
|
let completed =
|
|
run(invocation(&request, &files, &command), &NeverCancel).expect("completed invocation");
|
|
assert_eq!(completed.response.operation, Operation::Doctor);
|
|
assert_eq!(
|
|
fs::metadata(files.request_path())
|
|
.expect("request metadata")
|
|
.permissions()
|
|
.mode()
|
|
& 0o777,
|
|
0o600,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn temporary_response_is_never_accepted() {
|
|
let (_root, files, command) = fixture("temporary-only", &[]);
|
|
let request = request();
|
|
let failure = run(invocation(&request, &files, &command), &NeverCancel)
|
|
.expect_err("temporary response must fail");
|
|
assert_eq!(failure.kind, InvocationFailureKind::ResponseProtocol);
|
|
}
|
|
|
|
#[test]
|
|
fn mismatched_echo_is_a_protocol_violation() {
|
|
let (_root, files, command) = fixture("mismatch", &[]);
|
|
let request = request();
|
|
let failure =
|
|
run(invocation(&request, &files, &command), &NeverCancel).expect_err("mismatch must fail");
|
|
assert_eq!(failure.kind, InvocationFailureKind::ResponseProtocol);
|
|
}
|
|
|
|
#[test]
|
|
fn both_full_pipes_are_drained_without_deadlock_and_capture_is_bounded() {
|
|
let count = (9 * 1024 * 1024).to_string();
|
|
let (_root, files, command) = fixture("flood", &[&count]);
|
|
let request = request();
|
|
let completed =
|
|
run(invocation(&request, &files, &command), &NeverCancel).expect("flood invocation");
|
|
assert!(completed.diagnostics.truncated());
|
|
}
|
|
|
|
#[test]
|
|
fn watchdog_terminates_and_reaps_the_process_group() {
|
|
let (_root, files, command) = fixture("sleep", &["5000"]);
|
|
let request = request();
|
|
let failure = run(
|
|
invocation(&request, &files, &command).with_test_watchdog(Duration::from_millis(30)),
|
|
&NeverCancel,
|
|
)
|
|
.expect_err("watchdog must fire");
|
|
assert_eq!(failure.kind, InvocationFailureKind::WatchdogTimeout);
|
|
}
|
|
|
|
struct Interrupts(AtomicU32);
|
|
|
|
impl Cancellation for Interrupts {
|
|
fn interrupt_count(&self) -> u32 {
|
|
self.0.load(Ordering::SeqCst)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn second_interrupt_escalates_without_waiting_for_grace() {
|
|
let (_root, files, command) = fixture("sleep", &["5000"]);
|
|
let request = request();
|
|
let interrupts = Interrupts(AtomicU32::new(2));
|
|
let before = std::time::Instant::now();
|
|
let failure =
|
|
run(invocation(&request, &files, &command), &interrupts).expect_err("interrupt must win");
|
|
assert_eq!(failure.kind, InvocationFailureKind::Interrupted);
|
|
assert!(before.elapsed() < Duration::from_secs(2));
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_paths_are_absolute_in_fixture() {
|
|
let (_root, files, _command) = fixture("success", &[]);
|
|
assert!(Path::new(&files.request_path()).is_absolute());
|
|
}
|
|
|
|
#[test]
|
|
fn invocation_rejects_a_competing_total_watchdog_value() {
|
|
let (_root, files, command) = fixture("success", &[]);
|
|
let mut request = request();
|
|
request.limits.child_watchdog_seconds = positive(121);
|
|
assert!(
|
|
Invocation::new(
|
|
&request,
|
|
&files,
|
|
&command,
|
|
NativePhaseTimeouts {
|
|
analysis_seconds: None,
|
|
decompile_seconds: None,
|
|
},
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|