Keep isolated document extraction working on macOS

This commit is contained in:
karl committed 2026-10-23 09:47:08 +02:00
1 parent 5b93dd67a7
commit dd589e88ae
3 files changed
+60

No files matched your search

@@ -48,6 +48,33 @@ enum Event {
Stderr, Stderr,
} }
#[cfg(target_os = "macos")]
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>();
// The child stays owned and unreaped until I/O completes. A failed query is
// accepted only when it has already exited; otherwise fail closed.
let read = unsafe {
libc::proc_pidinfo(
child.id() as i32,
libc::PROC_PIDTASKINFO,
0,
info.as_mut_ptr().cast(),
size as i32,
)
};
if read != size as i32 {
if child.try_wait().map_err(failed)?.is_some() {
return Ok(());
}
return Err(failed("Cannot inspect extraction memory usage"));
}
if unsafe { info.assume_init() }.pti_resident_size > maximum {
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.
/// No parser fallback occurs when limits, transport or worker execution fail. /// No parser fallback occurs when limits, transport or worker execution fail.
pub fn run( pub fn run(
@@ -161,6 +188,8 @@ pub fn run(
if started.elapsed() >= config.timeout { if started.elapsed() >= config.timeout {
return Err(ProcessError::TimedOut); return Err(ProcessError::TimedOut);
} }
#[cfg(target_os = "macos")]
check_resident_memory(&mut owned.child, 512 * 1024 * 1024)?;
match rx.recv_timeout( match rx.recv_timeout(
Duration::from_millis(5).min(config.timeout.saturating_sub(started.elapsed())), Duration::from_millis(5).min(config.timeout.saturating_sub(started.elapsed())),
) { ) {
@@ -288,6 +317,20 @@ mod tests {
.unwrap_or_else(|poisoned| poisoned.into_inner()) .unwrap_or_else(|poisoned| poisoned.into_inner())
} }
#[cfg(target_os = "macos")]
#[test]
fn mac_memory_budget_rejects_a_live_owned_child() {
let mut child = Command::new("/bin/sleep").arg("30").spawn().unwrap();
// Wait for exec before measuring, then impose a deliberately tiny budget.
thread::sleep(Duration::from_millis(50));
let result = check_resident_memory(&mut child, 1);
let _ = child.kill();
child.wait().unwrap();
assert!(
matches!(result, Err(ProcessError::Failed(message)) if message.contains("exceeds budget"))
);
}
fn fixtures() -> tempfile::TempDir { fn fixtures() -> tempfile::TempDir {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let source = directory.path().join("fake.rs"); let source = directory.path().join("fake.rs");
@@ -1,5 +1,6 @@
use anyhow::{ensure, Result}; use anyhow::{ensure, Result};
#[cfg(not(target_os = "macos"))]
const MEMORY_BYTES: usize = 512 * 1024 * 1024; const MEMORY_BYTES: usize = 512 * 1024 * 1024;
#[cfg(windows)] #[cfg(windows)]
@@ -37,7 +38,10 @@ pub fn apply() -> Result<()> {
#[cfg(unix)] #[cfg(unix)]
pub fn apply() -> Result<()> { pub fn apply() -> Result<()> {
// Darwin rejects finite AS/DATA limits. The parent enforces a sampled RSS
// budget there; retain the CPU limit and bounded path-free transport here.
for (resource, maximum) in [ for (resource, maximum) in [
#[cfg(not(target_os = "macos"))]
(libc::RLIMIT_AS, MEMORY_BYTES as libc::rlim_t), (libc::RLIMIT_AS, MEMORY_BYTES as libc::rlim_t),
(libc::RLIMIT_CPU, 30 as libc::rlim_t), (libc::RLIMIT_CPU, 30 as libc::rlim_t),
] { ] {
+13
View File
@@ -101,3 +101,16 @@ The broader workspace run exposes an existing stale permission-denial rollback
expectation in `targeted_reconciliation` (current engine skips unreadable inputs). expectation in `targeted_reconciliation` (current engine skips unreadable inputs).
Strict current-toolchain Clippy also flags unchanged permission-literal and Strict current-toolchain Clippy also flags unchanged permission-literal and
ANN-cache boolean expressions. These failures are not qualified as passing gates. ANN-cache boolean expressions. These failures are not qualified as passing gates.
### Native document extraction policy
Mac parent/worker DOCX roundtrip and malformed-PDF rejection tests now pass.
Darwin rejects finite `RLIMIT_AS` and `RLIMIT_DATA` on the tested system. The
worker retains the30-second CPU limit; the parent checks resident memory through
`proc_pidinfo` during its existing5ms polling loop and rejects usage above512MiB,
killing/reaping its owned process group. Input/output bounds and the parent
wall deadline remain active. This sampled RSS guard may overshoot between polls;
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
verifies budget rejection; five process-owner tests and two actual parent/worker
roundtrip/rejection tests pass on the Mac.