#![cfg(feature = "daemon-identity")]
use std::path::PathBuf;
use std::time::Duration;
use kernal_api::daemon_identity::{
DaemonEndpoint, DaemonIdentity, DaemonIdentityRecord, DaemonVerifyError, LegacyPrefix,
ProbeMuxResult, ProbeResponder, ProbeSameEndpoint,
};
use kernal_api::platform::process::{ProcessIdentityAction, ProcessIdentityUnavailable};
const SLEEPER_ENV: &str = "KERNAL_API_DAEMON_CONTROL_SLEEPER";
const SLEEPER_TEST: &str = "daemon_identity_control::sleeper_child";
fn fixed_record() -> DaemonIdentityRecord {
DaemonIdentityRecord {
pid: 4242,
executable_path: PathBuf::from("/opt/d"),
blake3_digest: [0x11; 32],
legacy_sha256_digest: [0; 32],
boot_id: "boot-1".to_owned(),
endpoint: DaemonEndpoint::new("ns", "addr"),
started_at_unix_ms: 1000,
idle_timeout_secs: Some(30),
}
}
fn current(endpoint: DaemonEndpoint) -> DaemonIdentity {
DaemonIdentity::current_process(endpoint, None).expect("current process identity")
}
fn varint(mut value: u64, output: &mut Vec<u8>) {
while value >= 0x80 {
output.push((value as u8) | 0x80);
value >>= 7;
}
output.push(value as u8);
}
fn field_bytes(tag: u8, bytes: &[u8], output: &mut Vec<u8>) {
output.push(tag);
varint(bytes.len() as u64, output);
output.extend_from_slice(bytes);
}
fn framed(body: Vec<u8>) -> Vec<u8> {
let mut wire = vec![1];
wire.extend_from_slice(&u32::try_from(body.len()).expect("small body").to_le_bytes());
wire.extend(body);
wire
}
fn json_bytes(bytes: &[u8; 32]) -> String {
let lines = bytes
.iter()
.map(|byte| format!(" {byte}"))
.collect::<Vec<_>>()
.join(",\n");
format!("[\n{lines}\n ]")
}
#[test]
fn record_round_trips_every_field() {
let identity = DaemonIdentity::from_record(fixed_record());
assert_eq!(identity.to_record(), fixed_record());
assert_eq!(identity.pid(), 4242);
assert_eq!(identity.executable_path(), PathBuf::from("/opt/d"));
assert_eq!(identity.blake3_digest(), &[0x11; 32]);
assert_eq!(identity.legacy_sha256_digest(), &[0; 32]);
assert_eq!(identity.boot_id(), "boot-1");
assert_eq!(identity.endpoint(), DaemonEndpoint::new("ns", "addr"));
assert_eq!(identity.started_at_unix_ms(), 1000);
assert_eq!(identity.idle_timeout_secs(), Some(30));
let captured = current(DaemonEndpoint::new("record-ns", "record-addr"));
assert_eq!(DaemonIdentity::from_record(captured.to_record()), captured);
}
#[test]
fn sidecar_bytes_are_frozen() {
let directory = tempfile::tempdir().expect("temporary directory");
let sidecar = directory.path().join("daemon.running-process.json");
let mut record = fixed_record();
record.legacy_sha256_digest = [0x22; 32];
DaemonIdentity::from_record(record.clone())
.write_sidecar(&sidecar)
.expect("write sidecar");
let expected = format!(
"{{\n \"pid\": 4242,\n \"exe_path\": \"/opt/d\",\n \"exe_hash_algorithm\": \"blake3\",\n \
\"exe_hash\": {},\n \"legacy_exe_sha256\": {},\n \"boot_id\": \"boot-1\",\n \
\"ipc_endpoint\": {{\n \"namespace_id\": \"ns\",\n \"path\": \"addr\"\n }},\n \
\"started_at_unix_ms\": 1000,\n \"idle_timeout_secs\": 30\n}}",
json_bytes(&[0x11; 32]),
json_bytes(&[0x22; 32]),
);
let written = std::fs::read_to_string(&sidecar).expect("read sidecar");
assert_eq!(written, expected);
let compact = format!(
"{{\"pid\":4242,\"exe_path\":\"/opt/d\",\"exe_hash_algorithm\":\"blake3\",\
\"exe_hash\":{:?},\"boot_id\":\"boot-1\",\
\"ipc_endpoint\":{{\"namespace_id\":\"ns\",\"path\":\"addr\"}},\
\"started_at_unix_ms\":1000,\"idle_timeout_secs\":null}}",
[0x11_u8; 32]
);
std::fs::write(&sidecar, compact).expect("write compact sidecar");
let restored = DaemonIdentity::try_read_sidecar(&sidecar)
.expect("decode compact sidecar")
.expect("sidecar exists");
let mut expected_record = fixed_record();
expected_record.idle_timeout_secs = None;
assert_eq!(restored.to_record(), expected_record);
std::fs::write(
&sidecar,
written.replace(
"\"exe_hash_algorithm\": \"blake3\"",
"\"exe_hash_algorithm\": \"sha256\"",
),
)
.expect("write foreign-algorithm sidecar");
assert!(DaemonIdentity::try_read_sidecar(&sidecar).is_err());
assert_eq!(DaemonIdentity::read_sidecar(&sidecar), None);
}
#[test]
fn probe_reply_bytes_are_frozen() {
let responder = ProbeResponder::new(DaemonIdentity::from_record(fixed_record()), [0x7A63]);
let nonce = [0xA5; 32];
let mut request = vec![0x08, 1, 0x18];
varint(0xB232, &mut request);
field_bytes(0x22, &nonce, &mut request);
request.extend([0x28, 7]);
field_bytes(0x42, b"trace", &mut request);
field_bytes(0x4a, b"state", &mut request);
let request = framed(request);
let mut endpoint = Vec::new();
field_bytes(0x0a, b"ns", &mut endpoint);
field_bytes(0x12, b"addr", &mut endpoint);
let mut identity = vec![0x08];
varint(4242, &mut identity);
field_bytes(0x12, b"/opt/d", &mut identity);
field_bytes(0x22, &endpoint, &mut identity);
identity.push(0x28);
varint(1000, &mut identity);
field_bytes(0x32, b"boot-1", &mut identity);
identity.extend([0x38, 30]);
field_bytes(0x42, b"blake3", &mut identity);
field_bytes(0x4a, &[0x11; 32], &mut identity);
field_bytes(0x1a, &[0; 32], &mut identity);
let mut payload = nonce.to_vec();
payload.extend(identity);
let mut response = vec![0x08, 1, 0x10, 1, 0x18];
varint(0xB232, &mut response);
field_bytes(0x22, &payload, &mut response);
response.extend([0x28, 7]);
field_bytes(0x42, b"trace", &mut response);
field_bytes(0x4a, b"state", &mut response);
let expected = framed(response);
let ProbeMuxResult::ProbeReply { reply, consumed } = responder
.poll(&request, LegacyPrefix::NotLegacy)
.expect("answer probe")
else {
panic!("expected a probe reply");
};
assert_eq!(consumed, request.len());
assert_eq!(reply, expected);
}
#[test]
fn probe_refuses_an_endpoint_other_than_the_recorded_one() {
let identity = current(DaemonEndpoint::new("probe-ns", "probe-addr"));
for other in [
DaemonEndpoint::new("probe-ns", "other-addr"),
DaemonEndpoint::new("other-ns", "probe-addr"),
] {
assert_eq!(
identity.probe_endpoint_blocking(&other),
ProbeSameEndpoint::NotCurrent
);
}
}
#[cfg(unix)]
#[test]
fn blocking_probe_of_a_live_endpoint_is_current() {
use std::io::{Read as _, Write as _};
use std::os::unix::net::UnixListener;
let directory = tempfile::tempdir().expect("temporary endpoint directory");
let address = directory.path().join("blocking.sock");
let endpoint = DaemonEndpoint::new("kernal-api-blocking-probe", address.to_string_lossy());
let identity = current(endpoint.clone());
let responder = ProbeResponder::new(identity.clone(), []);
let listener = UnixListener::bind(&address).expect("bind endpoint");
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept probe");
let mut buffered = vec![0; 5];
stream.read_exact(&mut buffered).expect("read header");
let length = u32::from_le_bytes(buffered[1..].try_into().expect("length"));
let mut body = vec![0; length as usize];
stream.read_exact(&mut body).expect("read body");
buffered.extend(body);
let ProbeMuxResult::ProbeReply { reply, .. } = responder
.poll(&buffered, LegacyPrefix::NotLegacy)
.expect("answer probe")
else {
panic!("expected a probe reply");
};
stream.write_all(&reply).expect("write reply");
});
assert_eq!(
identity.probe_endpoint_blocking(&endpoint),
ProbeSameEndpoint::Current
);
server.join().expect("probe server exits");
}
#[test]
fn this_process_verifies_live_and_for_control() {
let identity = current(DaemonEndpoint::new("verify-ns", "verify-addr"));
let live = identity.verify_live().expect("this process verifies");
assert_eq!(live.pid(), std::process::id());
assert!(live.is_alive());
assert!(!live.has_exited().expect("observe a live daemon"));
assert!(matches!(
live.force_kill(),
Err(DaemonVerifyError::ControlNotRetained { pid }) if pid == std::process::id()
));
let control = identity
.verify_for_control()
.expect("this process verifies for control");
assert!(control.is_alive());
}
#[test]
fn verification_rejects_every_mismatched_field() {
let identity = current(DaemonEndpoint::new("mismatch-ns", "mismatch-addr"));
let pid = identity.pid();
let mut record = identity.to_record();
record.pid = 0;
assert!(matches!(
DaemonIdentity::from_record(record).verify_live(),
Err(DaemonVerifyError::InvalidPid(0))
));
let mut record = identity.to_record();
record.boot_id = "a-boot-that-never-happened".to_owned();
assert!(matches!(
DaemonIdentity::from_record(record).verify_live(),
Err(DaemonVerifyError::BootIdMismatch { expected, .. })
if expected == "a-boot-that-never-happened"
));
let mut record = identity.to_record();
record.boot_id.clear();
assert!(
DaemonIdentity::from_record(record).verify_live().is_ok(),
"an identity recorded before boot tracking is not a boot mismatch"
);
let mut record = identity.to_record();
record.executable_path = std::env::temp_dir().join("kernal-api-not-this-daemon");
assert!(matches!(
DaemonIdentity::from_record(record).verify_live(),
Err(DaemonVerifyError::ExecutablePathMismatch { pid: actual, .. }) if actual == pid
));
let mut record = identity.to_record();
record.blake3_digest[0] ^= 0xFF;
assert!(matches!(
DaemonIdentity::from_record(record).verify_for_control(),
Err(DaemonVerifyError::ExecutableHashMismatch { pid: actual }) if actual == pid
));
}
#[test]
#[ignore = "helper process for verified_child_is_killed_exactly_once"]
fn sleeper_child() {
if std::env::var_os(SLEEPER_ENV).is_some() {
std::thread::sleep(Duration::from_secs(60));
}
}
#[test]
fn verified_child_is_killed_exactly_once() {
let executable = std::env::current_exe().expect("test executable");
let mut child = std::process::Command::new(&executable)
.args([SLEEPER_TEST, "--exact", "--ignored", "--nocapture"])
.env(SLEEPER_ENV, "1")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn sleeper");
let mut record = current(DaemonEndpoint::new("child-ns", "child-addr")).to_record();
record.pid = child.id();
let identity = DaemonIdentity::from_record(record);
let verified = identity
.verify_for_control()
.expect("sleeper verifies for control");
assert_eq!(verified.pid(), child.id());
assert!(verified.is_alive());
assert!(!verified.has_exited().expect("observe the running sleeper"));
let first = verified.force_kill();
if cfg!(target_os = "macos") {
assert!(
matches!(
first,
Err(DaemonVerifyError::ControlUnavailable {
reason: ProcessIdentityUnavailable::Unsupported,
..
})
),
"macOS refuses identity-bound termination: {first:?}"
);
std::thread::sleep(Duration::from_millis(200));
assert!(
child.try_wait().expect("poll sleeper").is_none(),
"a refused kill leaves the verified process running"
);
assert!(!verified.has_exited().expect("observe the running sleeper"));
child.kill().expect("parent kills its own sleeper");
child.wait().expect("reap sleeper");
return;
}
assert_eq!(
first.expect("kill verified sleeper"),
ProcessIdentityAction::Performed
);
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while !verified
.has_exited()
.expect("observe the killed, unreaped sleeper")
{
assert!(
std::time::Instant::now() < deadline,
"a killed daemon must be observed as exited"
);
std::thread::sleep(Duration::from_millis(10));
}
let status = child.wait().expect("reap sleeper");
assert!(!status.success());
assert!(!verified.is_alive(), "the retained reference sees the exit");
assert!(
verified.has_exited().expect("observe the killed sleeper"),
"the fallible observation reports the exit, not an error"
);
assert!(
!matches!(verified.force_kill(), Ok(ProcessIdentityAction::Performed)),
"a second kill never reaches another process"
);
assert!(identity.verify_live().is_err());
}