1use std::io;
18use std::process::ExitCode;
19
20use super::protocol::{self, FrameRead, MAX_REQUEST_BYTES, Request, Response};
21
22pub fn worker_main(mode: &str, rest: &[String]) -> ExitCode {
25 if mode == "sandbox-helper" {
26 super::helper::helper_main(rest);
28 }
29 match run_mode(mode) {
30 Ok(response) => match emit(&response) {
31 Ok(()) => ExitCode::SUCCESS,
32 Err(e) => {
33 eprintln!("worker_write_failed: {e}");
34 ExitCode::from(1)
35 }
36 },
37 Err(exit) => exit.into(),
38 }
39}
40
41struct HardExit(u8);
44
45impl From<HardExit> for ExitCode {
46 fn from(exit: HardExit) -> ExitCode {
47 ExitCode::from(exit.0)
48 }
49}
50
51fn read_request() -> Result<Request, HardExit> {
52 let mut stdin = io::stdin().lock();
53 match protocol::read_frame(&mut stdin, MAX_REQUEST_BYTES) {
54 Ok(FrameRead::Complete(payload)) => serde_json::from_slice(&payload).map_err(|e| {
55 eprintln!("worker_bad_request: {e}");
56 HardExit(2)
57 }),
58 Ok(other) => {
59 eprintln!("worker_bad_request: no complete request frame ({other:?})");
60 Err(HardExit(2))
61 }
62 Err(e) => {
63 eprintln!("worker_read_failed: {e}");
64 Err(HardExit(2))
65 }
66 }
67}
68
69fn emit(response: &Response) -> io::Result<()> {
70 let mut stdout = io::stdout().lock();
71 protocol::write_frame(&mut stdout, response)
72}
73
74fn run_mode(mode: &str) -> Result<Response, HardExit> {
75 match mode {
76 "pdf" => production::pdf(read_request()?),
77 "probe-net" => production::probe_net(read_request()?),
78 "probe-seccomp" => production::probe_seccomp(read_request()?),
79 #[cfg(all(unix, feature = "records-worker"))]
82 "records" => records::records(read_request()?),
83 #[cfg(all(unix, feature = "image-ocr"))]
87 "image-ocr" => image_ocr::image_ocr(read_request()?),
88 #[cfg(all(unix, feature = "svg-provider"))]
93 "svg-raster" => svg_raster::svg_raster(read_request()?),
94 #[cfg(all(unix, feature = "image-metadata"))]
100 "image-metadata" => image_metadata::image_metadata(read_request()?),
101 #[cfg(all(unix, feature = "image-ocr", feature = "test-adapters"))]
106 "image-ocr-fake" => image_ocr::image_ocr_fake(read_request()?),
107 #[cfg(all(unix, feature = "audio-asr"))]
111 "asr" => audio::asr(read_request()?),
112 #[cfg(all(unix, feature = "audio-asr", feature = "test-adapters"))]
117 "asr-fake" => audio::asr_fake(read_request()?),
118 #[cfg(feature = "test-adapters")]
121 "harness-hold" => harness::hold(),
122 #[cfg(feature = "test-adapters")]
127 "harness-hold-closed" => harness::hold_closed(),
128 #[cfg(feature = "test-adapters")]
133 "harness-setsid-hold" => harness::setsid_hold(),
134 #[cfg(feature = "test-adapters")]
138 "harness-hold-balloon" => harness::hold_balloon(),
139 #[cfg(feature = "test-adapters")]
144 "harness-child-probe" => harness::child_probe(),
145 #[cfg(feature = "test-adapters")]
146 other => harness::run(other, read_request()?),
147 #[cfg(not(feature = "test-adapters"))]
148 _ => {
149 eprintln!("worker_unknown_mode");
150 Err(HardExit(2))
151 }
152 }
153}
154
155mod production {
156 use super::*;
157 use crate::convert::pdf::convert_pdf;
158 use crate::runner::protocol::bodies::{PdfOk, PdfRequest, ProbeNetRequest, ProbeReport};
159
160 pub(super) fn pdf(request: Request) -> Result<Response, HardExit> {
161 let body: PdfRequest = parse(&request)?;
162 let bytes = match std::fs::read(&body.input) {
163 Ok(bytes) => bytes,
164 Err(e) => {
165 return Ok(Response::err(
166 "io",
167 format!("cannot read the staged input {}: {e}", body.input),
168 ));
169 }
170 };
171 match convert_pdf(&bytes) {
172 Ok(conversion) => Ok(Response::ok(
173 serde_json::to_value(PdfOk {
174 text: conversion.text,
175 warnings: conversion.warnings,
176 segments: conversion.segments,
177 recovered: conversion.recovered,
178 })
179 .expect("PdfOk serializes"),
180 )),
181 Err(error) => Ok(Response::err(error.code, error.message)),
182 }
183 }
184
185 pub(super) fn probe_net(request: Request) -> Result<Response, HardExit> {
186 let body: ProbeNetRequest = parse(&request)?;
187 let report = ProbeReport {
188 attempts: super::net::attempt_all(&body),
189 };
190 Ok(Response::ok(
191 serde_json::to_value(report).expect("ProbeReport serializes"),
192 ))
193 }
194
195 pub(super) fn probe_seccomp(_request: Request) -> Result<Response, HardExit> {
201 let outcome = std::net::UdpSocket::bind("127.0.0.1:0")
202 .map(|_| "socket created, the filter did not engage".to_string())
203 .unwrap_or_else(|e| format!("socket refused by errno: {e}"));
204 Ok(Response::ok(serde_json::json!({ "outcome": outcome })))
205 }
206
207 fn parse<T: serde::de::DeserializeOwned>(request: &Request) -> Result<T, HardExit> {
208 serde_json::from_value(request.payload.clone()).map_err(|e| {
209 eprintln!("worker_bad_payload: {e}");
210 HardExit(2)
211 })
212 }
213}
214
215#[cfg(all(unix, feature = "records-worker"))]
217mod records {
218 use std::path::Path;
219
220 use super::*;
221 use crate::convert::records::{Ceilings, convert_records};
222 use crate::runner::protocol::bodies::{RecordsOk, RecordsRequest};
223
224 pub(super) fn records(request: Request) -> Result<Response, HardExit> {
225 let body: RecordsRequest =
226 serde_json::from_value(request.payload.clone()).map_err(|e| {
227 eprintln!("worker_bad_payload: {e}");
228 HardExit(2)
229 })?;
230 let ceilings = Ceilings {
231 max_records: body.max_records,
232 max_tables: body.max_tables,
233 max_output_bytes: body.max_output_bytes,
234 };
235 match convert_records(Path::new(&body.input), &body.format, &ceilings) {
236 Ok(conversion) => Ok(Response::ok(
237 serde_json::to_value(RecordsOk {
238 text: conversion.text,
239 warnings: conversion.warnings,
240 segments: conversion.segments,
241 })
242 .expect("RecordsOk serializes"),
243 )),
244 Err(error) => Ok(Response::err(error.code, error.message)),
245 }
246 }
247}
248
249#[cfg(all(unix, feature = "image-ocr"))]
253mod image_ocr {
254 use super::*;
255 use crate::convert::image_ocr::recognize;
256 use crate::runner::protocol::bodies::OcrRequest;
257
258 pub(super) fn image_ocr(request: Request) -> Result<Response, HardExit> {
259 let body: OcrRequest = serde_json::from_value(request.payload.clone()).map_err(|e| {
260 eprintln!("worker_bad_payload: {e}");
261 HardExit(2)
262 })?;
263 match recognize(&body) {
264 Ok(ok) => Ok(Response::ok(
265 serde_json::to_value(ok).expect("OcrOk serializes"),
266 )),
267 Err(error) => Ok(Response::err(error.code, error.message)),
268 }
269 }
270
271 #[cfg(feature = "test-adapters")]
275 pub(super) fn image_ocr_fake(request: Request) -> Result<Response, HardExit> {
276 use crate::convert::image_ocr::recognize_fake;
277 let body: OcrRequest = serde_json::from_value(request.payload.clone()).map_err(|e| {
278 eprintln!("worker_bad_payload: {e}");
279 HardExit(2)
280 })?;
281 match recognize_fake(&body) {
282 Ok(ok) => Ok(Response::ok(
283 serde_json::to_value(ok).expect("OcrOk serializes"),
284 )),
285 Err(error) => Ok(Response::err(error.code, error.message)),
286 }
287 }
288}
289
290#[cfg(all(unix, feature = "audio-asr"))]
294mod audio {
295 use super::*;
296 use crate::convert::audio_asr;
297 use crate::runner::protocol::bodies::AsrRequest;
298
299 pub(super) fn asr(request: Request) -> Result<Response, HardExit> {
300 let body: AsrRequest = serde_json::from_value(request.payload.clone()).map_err(|e| {
301 eprintln!("worker_bad_payload: {e}");
302 HardExit(2)
303 })?;
304 match audio_asr::transcribe(&body) {
305 Ok(ok) => Ok(Response::ok(
306 serde_json::to_value(ok).expect("AsrOk serializes"),
307 )),
308 Err(error) => Ok(Response::err(error.code, error.message)),
309 }
310 }
311
312 #[cfg(feature = "test-adapters")]
316 pub(super) fn asr_fake(request: Request) -> Result<Response, HardExit> {
317 let body: AsrRequest = serde_json::from_value(request.payload.clone()).map_err(|e| {
318 eprintln!("worker_bad_payload: {e}");
319 HardExit(2)
320 })?;
321 match audio_asr::transcribe_fake(&body) {
322 Ok(ok) => Ok(Response::ok(
323 serde_json::to_value(ok).expect("AsrOk serializes"),
324 )),
325 Err(error) => Ok(Response::err(error.code, error.message)),
326 }
327 }
328}
329
330#[cfg(all(unix, feature = "svg-provider"))]
334mod svg_raster {
335 use super::*;
336 use crate::convert::svg_raster::rasterize;
337 use crate::runner::protocol::bodies::SvgRasterRequest;
338
339 pub(super) fn svg_raster(request: Request) -> Result<Response, HardExit> {
340 let body: SvgRasterRequest =
341 serde_json::from_value(request.payload.clone()).map_err(|e| {
342 eprintln!("worker_bad_payload: {e}");
343 HardExit(2)
344 })?;
345 match rasterize(&body) {
346 Ok(ok) => Ok(Response::ok(
347 serde_json::to_value(ok).expect("SvgRasterOk serializes"),
348 )),
349 Err(error) => Ok(Response::err(error.code, error.message)),
350 }
351 }
352}
353
354#[cfg(all(unix, feature = "image-metadata"))]
357mod image_metadata {
358 use std::path::Path;
359
360 use super::*;
361 use crate::convert::image_metadata::extract::{Ceilings, extract};
362 use crate::runner::protocol::bodies::{ImageMetadataOk, ImageMetadataRequest};
363
364 pub(super) fn image_metadata(request: Request) -> Result<Response, HardExit> {
365 let body: ImageMetadataRequest =
366 serde_json::from_value(request.payload.clone()).map_err(|e| {
367 eprintln!("worker_bad_payload: {e}");
368 HardExit(2)
369 })?;
370 let ceilings = Ceilings {
371 max_decompressed_bytes: body.max_decompressed_bytes,
372 max_xml_depth: body.max_xml_depth,
373 max_xml_events: body.max_xml_events,
374 max_boxes: body.max_boxes,
375 max_rows: body.max_rows,
376 max_output_bytes: body.max_output_bytes,
377 };
378 match extract(Path::new(&body.input), &body.format, &ceilings) {
379 Ok(rows) => Ok(Response::ok(
380 serde_json::to_value(ImageMetadataOk { rows }).expect("ImageMetadataOk serializes"),
381 )),
382 Err(error) => Ok(Response::err(error.code, error.message)),
383 }
384 }
385}
386
387mod net {
395 use std::io;
396 use std::net::{SocketAddr, TcpStream, UdpSocket};
397 use std::time::Duration;
398
399 use crate::runner::protocol::bodies::{ProbeAttempt, ProbeName, ProbeNetRequest, ProbeOutcome};
400
401 const TIMEOUT: Duration = Duration::from_millis(600);
402
403 pub(super) fn attempt_all(request: &ProbeNetRequest) -> Vec<ProbeAttempt> {
404 vec![
405 tcp(ProbeName::Ipv4Tcp, &request.tcp4_target),
406 tcp(ProbeName::Ipv6Tcp, &request.tcp6_target),
407 unix_socket(&request.unix_target),
408 udp_send(&request.tcp4_target),
409 ]
410 }
411
412 fn classify(name: ProbeName, result: io::Result<()>) -> ProbeAttempt {
413 let (outcome, error_kind) = match result {
414 Ok(()) => (ProbeOutcome::Connected, io::ErrorKind::Other),
415 Err(e)
416 if matches!(
417 e.kind(),
418 io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
419 ) =>
420 {
421 (ProbeOutcome::Timeout, e.kind())
422 }
423 Err(e) => (ProbeOutcome::Denied, e.kind()),
424 };
425 ProbeAttempt {
426 name,
427 outcome,
428 error_kind,
429 }
430 }
431
432 fn tcp(name: ProbeName, target: &str) -> ProbeAttempt {
433 let address = match target.parse::<SocketAddr>() {
435 Ok(address) => address,
436 Err(_) => {
437 return ProbeAttempt {
438 name,
439 outcome: ProbeOutcome::InvalidTarget,
440 error_kind: io::ErrorKind::InvalidInput,
441 };
442 }
443 };
444 classify(
445 name,
446 TcpStream::connect_timeout(&address, TIMEOUT).map(drop),
447 )
448 }
449
450 #[cfg(target_os = "linux")]
451 fn unix_socket(target: &str) -> ProbeAttempt {
452 use std::os::linux::net::SocketAddrExt;
453 let result = std::os::unix::net::SocketAddr::from_abstract_name(target.as_bytes())
454 .and_then(|address| std::os::unix::net::UnixStream::connect_addr(&address))
455 .map(drop);
456 classify(ProbeName::UnixSocket, result)
457 }
458
459 #[cfg(not(target_os = "linux"))]
460 fn unix_socket(target: &str) -> ProbeAttempt {
461 classify(
462 ProbeName::UnixSocket,
463 std::os::unix::net::UnixStream::connect(target).map(drop),
464 )
465 }
466
467 fn udp_send(target: &str) -> ProbeAttempt {
471 let result = (|| {
472 let socket = UdpSocket::bind("0.0.0.0:0")?;
473 socket.connect(target)?;
474 socket.send(b"probe")?;
475 Ok(())
476 })();
477 classify(ProbeName::UdpSend, result)
478 }
479}
480
481#[cfg(feature = "test-adapters")]
482mod harness {
483 use std::io::Write;
484
485 use super::*;
486 use crate::runner::protocol::bodies::{
487 OcrInput, OcrOk, OcrRequest, OcrSpan, ScreenState, VideoOk, VideoRequest,
488 };
489
490 pub(super) fn run(mode: &str, request: Request) -> Result<Response, HardExit> {
491 match mode {
492 "ocr" => ocr(request),
493 "video" => video(request),
494 "harness-echo" => Ok(Response::ok(request.payload)),
495 "harness-env" => harness_env(),
496 "harness-escape" => harness_escape(request),
497 "harness-crash" => harness_crash(),
498 "harness-sleep" => harness_sleep(),
499 "harness-balloon" => harness_balloon(),
500 "harness-oversized" => harness_oversized(),
501 "harness-trailing" => harness_trailing(),
502 "harness-truncated" => harness_truncated(),
503 "harness-stderr-flood" => harness_stderr_flood(),
504 "harness-survivor" => harness_survivor(request),
505 "harness-balloon-survivor" => harness_balloon_survivor(request),
506 "harness-spawn-storm" => harness_spawn_storm(request),
507 "harness-fd-check" => harness_fd_check(request),
508 "harness-controls" => harness_controls(request),
509 "harness-spawn-probe" => harness_spawn_probe(request),
510 _ => {
511 eprintln!("worker_unknown_mode");
512 Err(HardExit(2))
513 }
514 }
515 }
516
517 pub(super) fn hold() -> ! {
521 loop {
522 std::thread::sleep(std::time::Duration::from_secs(3600));
523 }
524 }
525
526 pub(super) fn hold_closed() -> ! {
532 for fd in 0..=2 {
533 let _ = nix::unistd::close(fd);
534 }
535 loop {
536 std::thread::sleep(std::time::Duration::from_secs(3600));
537 }
538 }
539
540 pub(super) fn hold_balloon() -> ! {
546 for fd in 0..=2 {
547 let _ = nix::unistd::close(fd);
548 }
549 let mut balloon = vec![0u8; 192 * 1024 * 1024];
550 for index in (0..balloon.len()).step_by(4096) {
551 balloon[index] = (index % 251) as u8;
552 }
553 let _ = std::fs::write("ballooned", b"up");
554 loop {
555 std::hint::black_box(&balloon);
556 std::thread::sleep(std::time::Duration::from_secs(3600));
557 }
558 }
559
560 fn harness_balloon_survivor(request: Request) -> Result<Response, HardExit> {
565 #[derive(serde::Deserialize)]
566 #[serde(deny_unknown_fields)]
567 struct SurvivorRequest {
568 program: String,
569 }
570 let body: SurvivorRequest = parse(&request)?;
571 if let Err(e) = std::process::Command::new(&body.program)
572 .arg("harness-hold-balloon")
573 .spawn()
574 {
575 return Ok(Response::err(
576 "spawn_failed",
577 format!("cannot spawn the balloon: {e}"),
578 ));
579 }
580 for _ in 0..400 {
581 if std::fs::metadata("ballooned").is_ok() {
582 break;
583 }
584 std::thread::sleep(std::time::Duration::from_millis(5));
585 }
586 Ok(Response::ok(serde_json::json!({ "spawned": true })))
587 }
588
589 pub(super) fn setsid_hold() -> ! {
597 let _ = nix::unistd::setsid();
598 std::thread::sleep(std::time::Duration::from_secs(5));
599 std::process::exit(0);
600 }
601
602 fn ocr(request: Request) -> Result<Response, HardExit> {
606 let body: OcrRequest = parse(&request)?;
607 confirm_input(&body.input)?;
608 let label = match body.kind {
609 OcrInput::Image => "image".to_string(),
610 OcrInput::PageRender { page, dpi } => format!("page {page} at {dpi} dpi"),
611 };
612 let ok = OcrOk {
613 spans: vec![
614 OcrSpan {
615 text: format!("recognized from {label}"),
616 confidence: 0.94,
617 },
618 OcrSpan {
619 text: "faint line".to_string(),
620 confidence: 0.31,
621 },
622 ],
623 warnings: Vec::new(),
624 };
625 Ok(Response::ok(serde_json::to_value(ok).expect("OcrOk")))
626 }
627
628 fn video(request: Request) -> Result<Response, HardExit> {
631 let body: VideoRequest = parse(&request)?;
632 confirm_input(&body.input)?;
633 let ok = VideoOk {
634 states: vec![
635 ScreenState {
636 first_seen_seconds: 2.0,
637 text: "Title slide".to_string(),
638 },
639 ScreenState {
640 first_seen_seconds: 8.0,
641 text: "Quarterly revenue".to_string(),
642 },
643 ScreenState {
644 first_seen_seconds: 20.0,
645 text: "Quarterly revenue".to_string(),
646 },
647 ],
648 duration_seconds: Some(30.0),
649 };
650 Ok(Response::ok(serde_json::to_value(ok).expect("VideoOk")))
651 }
652
653 fn harness_env() -> Result<Response, HardExit> {
654 let vars: Vec<[String; 2]> = std::env::vars().map(|(k, v)| [k, v]).collect();
655 Ok(Response::ok(serde_json::json!({ "env": vars })))
656 }
657
658 fn harness_escape(request: Request) -> Result<Response, HardExit> {
663 #[derive(serde::Deserialize)]
664 #[serde(deny_unknown_fields)]
665 struct EscapeRequest {
666 read_path: String,
667 #[serde(default)]
671 list_path: Option<String>,
672 }
673 let body: EscapeRequest = parse(&request)?;
674 let read = std::fs::read(&body.read_path)
675 .map(|bytes| format!("read succeeded with {} bytes", bytes.len()))
676 .unwrap_or_else(|e| format!("read denied: {e}"));
677 let list = body.list_path.as_ref().map(|path| {
678 std::fs::read_dir(path)
679 .map(|entries| format!("list succeeded with {} entries", entries.count()))
680 .unwrap_or_else(|e| format!("list denied: {e}"))
681 });
682 let target = std::env::temp_dir().join("text-mirror-escape-probe");
683 let write = std::fs::write(&target, b"escaped")
684 .map(|_| "write succeeded".to_string())
685 .unwrap_or_else(|e| format!("write denied: {e}"));
686 Ok(Response::ok(serde_json::json!({
687 "read": read,
688 "write": write,
689 "list": list,
690 })))
691 }
692
693 fn harness_survivor(request: Request) -> Result<Response, HardExit> {
698 #[derive(serde::Deserialize)]
699 #[serde(deny_unknown_fields)]
700 struct SurvivorRequest {
701 program: String,
702 }
703 let body: SurvivorRequest = parse(&request)?;
704 match std::process::Command::new(&body.program)
708 .arg("harness-hold-closed")
709 .spawn()
710 {
711 Ok(child) => Ok(Response::ok(
712 serde_json::json!({ "survivor_pid": child.id() }),
713 )),
714 Err(e) => Ok(Response::err(
715 "spawn_failed",
716 format!("cannot spawn the survivor: {e}"),
717 )),
718 }
719 }
720
721 fn harness_spawn_storm(request: Request) -> Result<Response, HardExit> {
725 #[derive(serde::Deserialize)]
726 #[serde(deny_unknown_fields)]
727 struct StormRequest {
728 program: String,
729 attempts: u32,
730 }
731 let body: StormRequest = parse(&request)?;
732 let mut spawned = 0u32;
733 let mut failed = 0u32;
734 let mut first_error = String::new();
735 let mut children = Vec::new();
736 for _ in 0..body.attempts {
737 match std::process::Command::new(&body.program)
738 .arg("harness-hold-closed")
739 .spawn()
740 {
741 Ok(child) => {
742 spawned += 1;
743 children.push(child);
744 }
745 Err(e) => {
746 failed += 1;
747 if first_error.is_empty() {
748 first_error = e.to_string();
749 }
750 }
751 }
752 }
753 Ok(Response::ok(serde_json::json!({
754 "spawned": spawned,
755 "failed": failed,
756 "first_error": first_error,
757 })))
758 }
759
760 fn harness_controls(request: Request) -> Result<Response, HardExit> {
765 #[derive(serde::Deserialize)]
766 #[serde(deny_unknown_fields)]
767 struct ControlsRequest {
768 temp_root: String,
770 home: String,
772 cache_root: String,
774 outbound: String,
776 loopback: String,
778 shell_profile: String,
780 profile_file: String,
782 privileged: String,
784 program: String,
786 }
787 let body: ControlsRequest = parse(&request)?;
788 Ok(Response::ok(serde_json::json!({
789 "write-temp-root": attempt_write(&std::path::Path::new(&body.temp_root).join("text-mirror-control")),
790 "write-home": attempt_write(&std::path::Path::new(&body.home).join("text-mirror-control")),
791 "write-cache-root": attempt_write(&std::path::Path::new(&body.cache_root).join("text-mirror-control")),
792 "tcp-outbound": attempt_connect(&body.outbound),
793 "tcp-loopback-connect": attempt_connect(&body.loopback),
794 "tcp-listen": outcome(std::net::TcpListener::bind("127.0.0.1:0").map(|_| ())),
795 "read-shell-profile": outcome(std::fs::read(&body.shell_profile).map(|_| ())),
796 "read-resident-profile": outcome(std::fs::read(&body.profile_file).map(|_| ())),
797 "read-privileged": outcome(std::fs::read(&body.privileged).map(|_| ())),
798 "exec-undeclared": outcome(
799 std::process::Command::new(&body.program)
800 .stdout(std::process::Stdio::null())
801 .stderr(std::process::Stdio::null())
802 .status()
803 .map(|_| ())
804 ),
805 "write-inside-jail": attempt_write(std::path::Path::new("control-inside-jail")),
806 })))
807 }
808
809 fn attempt_write(path: &std::path::Path) -> String {
810 outcome(std::fs::write(path, b"control").map(|_| ()))
811 }
812
813 fn attempt_connect(target: &str) -> String {
814 let Ok(address) = target.parse::<std::net::SocketAddr>() else {
815 return "invalid target".to_string();
816 };
817 outcome(
818 std::net::TcpStream::connect_timeout(&address, std::time::Duration::from_millis(600))
819 .map(|_| ()),
820 )
821 }
822
823 fn outcome(result: std::io::Result<()>) -> String {
824 match result {
825 Ok(()) => "allowed".to_string(),
826 Err(e) => format!("denied: {e}"),
827 }
828 }
829
830 fn harness_spawn_probe(request: Request) -> Result<Response, HardExit> {
834 #[derive(serde::Deserialize)]
835 #[serde(deny_unknown_fields)]
836 struct SpawnProbeRequest {
837 program: String,
838 temp_root: String,
839 privileged: String,
840 }
841 let body: SpawnProbeRequest = parse(&request)?;
842 let status = std::process::Command::new(&body.program)
843 .arg("harness-child-probe")
844 .env("PROBE_TEMP_ROOT", &body.temp_root)
845 .env("PROBE_PRIVILEGED", &body.privileged)
846 .stdin(std::process::Stdio::null())
847 .stdout(std::process::Stdio::null())
848 .stderr(std::process::Stdio::null())
849 .status();
850 let Ok(status) = status else {
851 return Ok(Response::err(
852 "spawn_failed",
853 "the descendant could not be spawned".to_string(),
854 ));
855 };
856 let report = std::fs::read_to_string("child-probe.json").unwrap_or_default();
857 Ok(Response::ok(serde_json::json!({
858 "descendant_exited_cleanly": status.success(),
859 "report": serde_json::from_str::<serde_json::Value>(&report).unwrap_or(serde_json::Value::Null),
860 })))
861 }
862
863 pub(super) fn child_probe() -> ! {
867 let temp_root = std::env::var("PROBE_TEMP_ROOT").unwrap_or_default();
868 let privileged = std::env::var("PROBE_PRIVILEGED").unwrap_or_default();
869 let report = serde_json::json!({
870 "write-temp-root": attempt_write(&std::path::Path::new(&temp_root).join("text-mirror-descendant")),
871 "read-privileged": outcome(std::fs::read(&privileged).map(|_| ())),
872 "write-inside-jail": attempt_write(std::path::Path::new("descendant-inside-jail")),
873 });
874 let _ = std::fs::write("child-probe.json", report.to_string());
875 std::process::exit(0)
876 }
877
878 fn harness_fd_check(request: Request) -> Result<Response, HardExit> {
882 #[derive(serde::Deserialize)]
883 #[serde(deny_unknown_fields)]
884 struct FdCheckRequest {
885 fd: i32,
886 }
887 let body: FdCheckRequest = parse(&request)?;
888 let open = std::fs::metadata(format!("/dev/fd/{}", body.fd)).is_ok();
889 Ok(Response::ok(serde_json::json!({ "open": open })))
890 }
891
892 fn harness_crash() -> Result<Response, HardExit> {
896 let response = Response::ok(serde_json::json!({ "note": "plausible but doomed" }));
897 let mut stdout = io::stdout().lock();
898 let _ = protocol::write_frame(&mut stdout, &response);
899 std::process::exit(3);
900 }
901
902 fn harness_sleep() -> Result<Response, HardExit> {
903 loop {
904 std::thread::sleep(std::time::Duration::from_secs(3600));
905 }
906 }
907
908 fn harness_balloon() -> Result<Response, HardExit> {
913 const BALLOON_BYTES: usize = 192 * 1024 * 1024;
914 let mut balloon = vec![0u8; BALLOON_BYTES];
915 for index in (0..balloon.len()).step_by(4096) {
916 balloon[index] = (index % 251) as u8;
917 }
918 std::thread::sleep(std::time::Duration::from_secs(30));
919 drop(balloon);
920 Err(HardExit(0))
921 }
922
923 fn harness_oversized() -> Result<Response, HardExit> {
926 let mut stdout = io::stdout().lock();
927 let _ = stdout.write_all(&u32::MAX.to_be_bytes());
928 let _ = stdout.flush();
929 std::thread::sleep(std::time::Duration::from_secs(30));
930 Err(HardExit(0))
931 }
932
933 fn harness_trailing() -> Result<Response, HardExit> {
936 let response = Response::ok(serde_json::json!({ "note": "then noise" }));
937 let mut stdout = io::stdout().lock();
938 let _ = protocol::write_frame(&mut stdout, &response);
939 let _ = stdout.write_all(b"trailing noise past the frame");
940 let _ = stdout.flush();
941 std::thread::sleep(std::time::Duration::from_secs(30));
942 Err(HardExit(0))
943 }
944
945 fn harness_truncated() -> Result<Response, HardExit> {
948 let mut stdout = io::stdout().lock();
949 let _ = stdout.write_all(&64u32.to_be_bytes());
950 let _ = stdout.write_all(b"{\"partial\":");
951 let _ = stdout.flush();
952 std::process::exit(0);
953 }
954
955 fn harness_stderr_flood() -> Result<Response, HardExit> {
957 let chunk = [b'x'; 8192];
958 let mut stderr = io::stderr().lock();
959 for _ in 0..4096 {
960 if stderr.write_all(&chunk).is_err() {
961 break;
962 }
963 }
964 let _ = stderr.flush();
965 std::thread::sleep(std::time::Duration::from_secs(30));
966 Err(HardExit(0))
967 }
968
969 fn confirm_input(name: &str) -> Result<(), HardExit> {
970 if std::fs::metadata(name).is_ok() {
971 Ok(())
972 } else {
973 eprintln!("worker_missing_input: {name}");
974 Err(HardExit(4))
975 }
976 }
977
978 fn parse<T: serde::de::DeserializeOwned>(request: &Request) -> Result<T, HardExit> {
979 serde_json::from_value(request.payload.clone()).map_err(|e| {
980 eprintln!("worker_bad_payload: {e}");
981 HardExit(2)
982 })
983 }
984}