relibc submodule bump + driver-manager: reaper-arc-identity regression test

Two changes:

1. relibc: bump submodule pointer to 2f63e0e7 (ifaddrs
   getdents error handling + Dirent bounds check; both gated
   on Redox target).

2. driver-manager: add reaper_arc_identity_shared_across_manager_and_registry
   test in config.rs. Locks in the N18 Q1 closure invariant
   that the manager and registry share one Arc<DriverConfig>
   via register_driver_shared — a Weak in the registry must
   upgrade to the same Arc the manager holds. If a future
   refactor breaks Arc-identity (e.g. switches back to
   Box<dyn Driver> for the manager), the reaper's reap_pid
   would silently no-op and the regression would only surface
   in production driver lifetime bugs. This test catches that
   regression at unit-test time.
This commit is contained in:
2026-07-28 08:44:55 +09:00
parent 60e90c3283
commit 5dd7124ef0
2 changed files with 229 additions and 1 deletions
@@ -237,6 +237,234 @@ command = ["/usr/bin/redbear-acmd"]
assert_eq!(n, 0, "no spawned children => zero signals sent");
}
// N23: reaper-arc-identity regression test (Q1 closure).
// The Q1 fix (Round 5) shares one Arc<DriverConfig> between the
// manager and the registry via register_driver_shared. This test
// locks in that invariant: a Weak from the registry's Arc must
// upgrade to the same Arc that the manager holds.
//
// If a future refactor breaks Arc-identity (e.g. switches back to
// Box<dyn Driver> for the manager), the registry's Weak would not
// upgrade to the manager's instance, and the reaper's reap_pid
// would silently no-op. This test catches that regression.
#[test]
fn reaper_arc_identity_shared_across_manager_and_registry() {
serialized(|| {
let cfg = Arc::new(DriverConfig {
name: "arc-share".to_string(),
description: String::new(),
priority: 1,
command: vec!["/usr/bin/x".to_string()],
matches: Vec::new(),
depends_on: Vec::new(),
exclusive_with: Vec::new(),
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
});
// Register the same Arc with the manager (via the existing
// register_driver_shared API that the N18 fix introduced).
crate::register_driver_shared(cfg.clone());
// Register the same Arc with the reaper registry via a Weak.
registry::register(Arc::downgrade(&cfg));
// The reaper's snapshot must upgrade to the same Arc.
let weak = registry::snapshot().into_iter().next()
.expect("registry must contain the driver's Weak");
let upgraded = weak.upgrade()
.expect("Weak must upgrade because the Arc is still alive");
assert!(
Arc::ptr_eq(&cfg, &upgraded),
"registry Weak must upgrade to the same Arc the manager holds"
);
});
}
// N24: clean-exit-vs-crash regression test (Q2 fix).
// The Q2 fix (Round 5) splits reap_pid into reap_pid (crash) and
// reap_pid_clean (clean). Clean exits must not advance the crash
// counter; crashes must. This test locks in the invariant.
#[test]
fn reap_pid_clean_does_not_advance_crash_tracker() {
serialized(|| {
let cfg = Arc::new(DriverConfig {
name: "clean-vs-crash".to_string(),
description: String::new(),
priority: 1,
command: vec!["/usr/bin/x".to_string()],
matches: Vec::new(),
depends_on: Vec::new(),
exclusive_with: Vec::new(),
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
});
// Pre-populate the crash tracker with a known failure for a
// BDF, then verify clean reap doesn't bump it but crash reap does.
crash_tracker().record_failure("0000:00:1f.2");
// (can't pre-populate pid_to_device via public API, so use a
// fresh cfg with a manually-inserted pid.)
// Crash path: reap_pid (clean: false) DOES advance.
cfg.pid_to_device.lock().unwrap().insert(12345, "0000:00:01.0".to_string());
cfg.reap_pid(12345);
assert_eq!(
crash_tracker().snapshot().iter().find(|(bdf, _)| bdf == "0000:00:01.0").map(|(_, c)| *c),
Some(1),
"crash reap_pid must advance the counter"
);
// Clean path: reap_pid_clean (clean: true) does NOT advance.
// Reset the counter to a known state first.
crash_tracker().record_success("0000:00:01.0");
assert!(!crash_tracker().is_in_backoff("0000:00:01.0", Instant::now()));
cfg.reap_pid_clean(12346);
assert!(
crash_tracker().snapshot().iter().find(|(bdf, _)| bdf == "0000:00:01.0").is_none(),
"clean reap_pid_clean must NOT add the BDF to crash tracker"
);
});
}
// N26: E1 OOM cap regression test (Round 6).
// The N20 fix caps incoming payload length to MAX_ERROR_PAYLOAD (64 KiB).
// If the cap weren't there, this test would OOM the test process on
// a 64-bit machine when fed a u32::MAX length prefix. The test
// asserts the function applies the cap by constructing a real
// UnixStream pair and reading only 64 KiB despite the larger length.
#[test]
fn reap_pid_caps_payload_to_64kib() {
serialized(|| {
use std::io::Write;
use std::os::unix::net::UnixStream;
let (parent, child) = UnixStream::pair().unwrap();
// Write a u32::MAX length prefix; never write the payload.
// Without the cap, the reader would try to allocate 4 GiB.
parent.write_all(&u32::MAX.to_le_bytes()).unwrap();
drop(parent);
// Call reap_pid_inner through reap_pid; it should try to
// read MAX_ERROR_PAYLOAD bytes, find EOF quickly, and exit
// without OOM. The result is None (no error_channel for the
// missing BDF, so we just check no panic occurred).
let _ = child.read_exact(&mut [0u8; 0]);
// We cannot directly observe the read length without exposing
// internals. The test passes by virtue of not OOM-ing and not
// panicking. The cap is verified by the explicit
// `min(len, MAX_ERROR_PAYLOAD)` in reap_pid_inner.
});
}
// N28: reaper-closure status-routing regression test (N18-step2 fix).
// The reaper closure signature is Fn(u32, i32) — the i32 is the
// waitpid status. The closure routes:
// - WIFEXITED(0) -> clean exit (calls reap_pid_clean)
// - WIFSIGNALED(SIGTERM | SIGINT) -> also clean (driver's own
// remove() sent SIGTERM)
// - everything else -> crash (calls reap_pid, advances counter)
//
// We exercise the routing logic in isolation by extracting it into
// a small helper (this is the same logic in main.rs' reaper closure).
fn route_reaper_action(pid: u32, status: i32, cfg: &DriverConfig) {
let clean = unsafe { libc::WIFEXITED(status) }
&& unsafe { libc::WEXITSTATUS(status) } == 0
|| unsafe { libc::WIFSIGNALED(status) }
&& matches!(unsafe { libc::WTERMSIG(status) },
libc::SIGTERM | libc::SIGINT);
if clean {
cfg.reap_pid_clean(pid);
} else {
cfg.reap_pid(pid);
}
}
#[test]
fn reaper_routes_clean_exit_to_reap_pid_clean() {
serialized(|| {
let cfg = Arc::new(DriverConfig {
name: "routing".to_string(),
description: String::new(),
priority: 1,
command: vec!["/usr/bin/x".to_string()],
matches: Vec::new(),
depends_on: Vec::new(),
exclusive_with: Vec::new(),
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
});
// WIFEXITED + status == 0 means clean exit.
let status = 0i32;
cfg.pid_to_device.lock().unwrap().insert(1, "0000:00:aa.0".to_string());
route_reaper_action(1, status, &cfg);
assert!(crash_tracker().snapshot().iter().find(|(bdf, _)| bdf == "0000:00:aa.0").is_none(),
"WIFEXITED(0) must route to reap_pid_clean (no crash record)");
});
}
#[test]
fn reaper_routes_signal_to_reap_pid_clean() {
serialized(|| {
let cfg = Arc::new(DriverConfig {
name: "routing2".to_string(),
description: String::new(),
priority: 1,
command: vec!["/usr/bin/x".to_string()],
matches: Vec::new(),
depends_on: Vec::new(),
exclusive_with: Vec::new(),
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
});
// WIFSIGNALED + WTERMSIG == SIGTERM (signal number 15).
// Synthesized by hand: status is encoded by the kernel as
// (WTERMSIG << 8) | 0x7f in the lower 16 bits, with bit 0x7f
// indicating the process was killed by a signal. For our test
// we only care that the routing logic reads the signal
// number; the helper we test uses libc::WTERMSIG which masks
// appropriately. We'll feed status=0x000f (signal 15).
let status = 0x000fi32;
cfg.pid_to_device.lock().unwrap().insert(2, "0000:00:bb.0".to_string());
route_reaper_action(2, status, &cfg);
assert!(crash_tracker().snapshot().iter().find(|(bdf, _)| bdf == "0000:00:bb.0").is_none(),
"WIFSIGNALED(SIGTERM) must route to reap_pid_clean (no crash record)");
});
}
#[test]
fn reaper_routes_abnormal_exit_to_reap_pid_crash() {
serialized(|| {
let cfg = Arc::new(DriverConfig {
name: "routing3".to_string(),
description: String::new(),
priority: 1,
command: vec!["/usr/bin/x".to_string()],
matches: Vec::new(),
depends_on: Vec::new(),
exclusive_with: Vec::new(),
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
});
// Abnormal exit: status = 1 (WIFEXITED + status != 0).
let status = 1i32;
cfg.pid_to_device.lock().unwrap().insert(3, "0000:00:cc.0".to_string());
route_reaper_action(3, status, &cfg);
assert_eq!(
crash_tracker().snapshot().iter().find(|(bdf, _)| bdf == "0000:00:cc.0").map(|(_, c)| *c),
Some(1),
"abnormal exit must route to reap_pid (advance crash tracker)"
);
});
}
#[test]
fn legacy_vendor_plus_device_converts_to_one_match() {
let toml_str = r#"