Handle native Mac extraction worker exit races

This commit is contained in:
karl committed 2026-10-23 09:49:17 +02:00
1 parent dd589e88ae
commit feec06bc41
3 files changed
+44 -27

No files matched your search

+21 -18
View File
@@ -50,29 +50,32 @@ enum Event {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn check_resident_memory(child: &mut Child, maximum: u64) -> Result<(), ProcessError> { fn check_resident_memory(child: &mut Child, maximum: u64) -> Result<(), ProcessError> {
let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::uninit();
let size = std::mem::size_of::<libc::proc_taskinfo>(); let size = std::mem::size_of::<libc::proc_taskinfo>();
// The child stays owned and unreaped until I/O completes. A failed query is // Task info can disappear just before an exiting child becomes waitable.
// accepted only when it has already exited; otherwise fail closed. // Retry that transition briefly; an alive uninspectable child fails closed.
let read = unsafe { for _ in 0..10 {
libc::proc_pidinfo( let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::uninit();
child.id() as i32, let read = unsafe {
libc::PROC_PIDTASKINFO, libc::proc_pidinfo(
0, child.id() as i32,
info.as_mut_ptr().cast(), libc::PROC_PIDTASKINFO,
size as i32, 0,
) info.as_mut_ptr().cast(),
}; size as i32,
if read != size as i32 { )
};
if read == size as i32 {
if unsafe { info.assume_init() }.pti_resident_size > maximum {
return Err(failed("Extraction worker resident memory exceeds budget"));
}
return Ok(());
}
if child.try_wait().map_err(failed)?.is_some() { if child.try_wait().map_err(failed)?.is_some() {
return Ok(()); return Ok(());
} }
return Err(failed("Cannot inspect extraction memory usage")); thread::sleep(Duration::from_millis(1));
} }
if unsafe { info.assume_init() }.pti_resident_size > maximum { Err(failed("Cannot inspect extraction memory usage"))
return Err(failed("Extraction worker resident memory exceeds budget"));
}
Ok(())
} }
/// Includes pipe writes, response EOF and process exit in the same deadline. /// Includes pipe writes, response EOF and process exit in the same deadline.
@@ -30,15 +30,24 @@ fn actual_parent_and_worker_return_original_docx_passage_and_digest() {
let input = document.finish().unwrap().into_inner(); let input = document.finish().unwrap().into_inner();
let request = RequestHeader::from_bytes(DocumentFormat::Docx, &input).unwrap(); let request = RequestHeader::from_bytes(DocumentFormat::Docx, &input).unwrap();
let expected_hash = request.input_sha256.clone(); let expected_hash = request.input_sha256.clone();
let response = run(&config(), request, input, &AtomicBool::new(false)).unwrap(); // Exercise short-lived native worker exit/task-inspection races too.
assert_eq!(response.input_sha256, expected_hash); for _ in 0..10 {
let ExtractionOutcome::Extracted { passages } = response.outcome else { let response = run(
panic!("Valid document rejected"); &config(),
}; request.clone(),
assert_eq!(passages.len(), 1); input.clone(),
assert_eq!(passages[0].text, "Cancel with sixty days written notice."); &AtomicBool::new(false),
assert_eq!(passages[0].source.kind, "paragraph"); )
assert_eq!(passages[0].source.number, 1); .unwrap();
assert_eq!(response.input_sha256, expected_hash);
let ExtractionOutcome::Extracted { passages } = response.outcome else {
panic!("Valid document rejected");
};
assert_eq!(passages.len(), 1);
assert_eq!(passages[0].text, "Cancel with sixty days written notice.");
assert_eq!(passages[0].source.kind, "paragraph");
assert_eq!(passages[0].source.number, 1);
}
} }
#[test] #[test]
+5
View File
@@ -114,3 +114,8 @@ it is not equivalent to a kernel-enforced hard address-space limit. Linux and
Windows retain their existing resource policies. An owned live-child test Windows retain their existing resource policies. An owned live-child test
verifies budget rejection; five process-owner tests and two actual parent/worker verifies budget rejection; five process-owner tests and two actual parent/worker
roundtrip/rejection tests pass on the Mac. roundtrip/rejection tests pass on the Mac.
Resident-memory inspection also retries the short exit-to-waitable transition
before treating a live child as uninspectable. Ten fresh installed CLI/helper
DOCX runs returned the expected passage; the native roundtrip test repeats that
short-lived worker path ten times to cover the race.