feat: implement worker lifecycle and sandbox
This commit is contained in:
parent
170ca32f58
commit
b9b9bfe05a
13 changed files with 2456 additions and 7 deletions
187
tests/worker_lifecycle.rs
Normal file
187
tests/worker_lifecycle.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#![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()
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue