1use std::io::Read;
28use std::path::PathBuf;
29use std::process::{Child, Stdio};
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, Ordering};
32use std::time::{Duration, Instant};
33
34pub mod helper;
35pub mod jail;
36mod memory;
37pub mod protocol;
38pub mod worker;
39
40#[cfg(target_os = "linux")]
41pub mod linux;
42#[cfg(target_os = "macos")]
43pub mod macos;
44
45use jail::{JailBackend, ProviderJail, RuntimeProfile, SpawnSpec};
46use protocol::bodies::{ProbeName, ProbeOutcome};
47use protocol::{FrameRead, Request, RequestSchema, Response, ResponseValidationError};
48
49#[derive(Debug, Clone)]
51pub struct Limits {
52 pub wall_timeout: Duration,
54 pub max_response_bytes: u32,
57 pub max_stderr_bytes: u64,
59 pub address_space_bytes: Option<u64>,
64 pub cpu_seconds: u64,
66 pub file_size_bytes: u64,
70 pub max_processes: u64,
80 pub max_resident_bytes: Option<u64>,
89}
90
91impl Default for Limits {
92 fn default() -> Limits {
93 Limits {
94 wall_timeout: Duration::from_secs(120),
95 max_response_bytes: 192 * 1024 * 1024,
96 max_stderr_bytes: 1024 * 1024,
97 address_space_bytes: Some(4 * 1024 * 1024 * 1024),
98 cpu_seconds: 300,
99 file_size_bytes: 512 * 1024 * 1024,
100 max_processes: 16,
101 max_resident_bytes: None,
102 }
103 }
104}
105
106#[derive(Debug, Clone)]
115pub struct RunnerError {
116 pub code: &'static str,
118 message: RunnerMessage,
119}
120
121impl RunnerError {
122 fn new(code: &'static str, message: RunnerMessage) -> RunnerError {
123 RunnerError { code, message }
124 }
125
126 pub fn message(&self) -> String {
128 self.message.to_string()
129 }
130}
131
132impl std::fmt::Display for RunnerError {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 write!(f, "{}: {}", self.code, self.message)
135 }
136}
137
138impl std::error::Error for RunnerError {}
139
140#[derive(Debug, Clone, Copy)]
141enum GrantKind {
142 Executable,
143 Read,
144}
145
146#[derive(Debug, Clone, Copy)]
147enum GrantFault {
148 Symlink,
149 NotRegular,
150 Io(std::io::ErrorKind),
151}
152
153#[derive(Debug, Clone, Copy)]
154enum ClosureFault {
155 NotAbsolute,
156 NotNormal,
157 Symlink,
158 SharedRoot,
159 Unresolved,
160 NotFileOrDirectory,
161 ResolvedSharedRoot,
162}
163
164#[derive(Debug, Clone, Copy)]
165enum PathClass {
166 Jail,
167 Worker,
168 ExecutableGrantDirectory,
169 ExecutableGrant,
170 ReadGrant,
171}
172
173#[derive(Debug, Clone, Copy)]
174enum TemplateValue {
175 Jail,
176 Worker,
177 Closures,
178 Service,
179 Grants,
180}
181
182#[derive(Debug, Clone, Copy)]
183enum ExitDetail {
184 SandboxSetup(i32),
185 ProcessCount(i32),
186 WorkerCode(i32),
187 WorkerSignal(i32),
188 WorkerNoStatus,
189}
190
191#[allow(dead_code)]
192#[derive(Debug, Clone, Copy)]
193enum RunnerMessage {
194 CurrentExecutable(std::io::ErrorKind),
195 CurrentExecutableHasNoDirectory,
196 WorkerBinaryResolve(std::io::ErrorKind),
197 WorkerBinaryMissing,
198 Grant {
199 kind: GrantKind,
200 position: usize,
201 count: usize,
202 fault: GrantFault,
203 },
204 JailDirectoryCreate(std::io::ErrorKind),
205 JailDirectoryCanonicalize(std::io::ErrorKind),
206 InputNameNotBare,
207 StageInput(std::io::ErrorKind),
208 SpawnWorker(std::io::ErrorKind),
209 DrainTimeout(u64),
210 WallTimeout(u64),
211 ResponseFrameOversized {
212 declared: u64,
213 ceiling: u64,
214 },
215 StderrOverflow {
216 written: u64,
217 ceiling: u64,
218 },
219 TrailingStdout,
220 Exit(ExitDetail),
221 EmptyResponse,
222 TruncatedResponse,
223 ReadResponse(std::io::ErrorKind),
224 ParseResponse,
225 ValidateResponse(ResponseValidationError),
226 MemoryExceeded {
227 total: u64,
228 ceiling: u64,
229 },
230 Memory(memory::MemoryError),
231 Closure {
232 position: usize,
233 count: usize,
234 fault: ClosureFault,
235 },
236 ServiceNamespace,
237 TempDirectoryVariable,
238 PlatformBackendMissing,
239 WorkerHash(std::io::ErrorKind),
240 ProbeRequestEncode,
241 ProbeReportedFailure,
242 ProbeResponseParse,
243 ProbeAttemptMissing(ProbeName),
244 ProbeAttempt {
245 name: ProbeName,
246 outcome: ProbeOutcome,
247 error_kind: std::io::ErrorKind,
248 },
249 ProbeListener(std::io::ErrorKind),
250 TemplateBrace(TemplateValue),
251 ClosureNotUtf8 {
252 position: usize,
253 count: usize,
254 },
255 ClosureCannotBeQuoted {
256 position: usize,
257 count: usize,
258 },
259 PathNotUtf8(PathClass),
260 PathCannotBeQuoted(PathClass),
261 ProviderNeedsWiring,
262 SandboxHelperMissing,
263 ProviderPlatformUnsupported,
264 SyscallProbeAllowed,
265 SyscallProbeFailed,
266}
267
268impl std::fmt::Display for RunnerMessage {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 match *self {
271 RunnerMessage::CurrentExecutable(kind) => {
272 write!(f, "cannot resolve the current executable: {kind:?}")
273 }
274 RunnerMessage::CurrentExecutableHasNoDirectory => {
275 f.write_str("the current executable has no directory")
276 }
277 RunnerMessage::WorkerBinaryResolve(kind) => {
278 write!(f, "cannot resolve the worker binary: {kind:?}")
279 }
280 RunnerMessage::WorkerBinaryMissing => {
281 write!(f, "no {WORKER_BINARY} beside the current executable")
282 }
283 RunnerMessage::Grant {
284 kind,
285 position,
286 count,
287 fault,
288 } => {
289 let kind = match kind {
290 GrantKind::Executable => "executable grant",
291 GrantKind::Read => "read grant",
292 };
293 write!(
294 f,
295 "{kind} {position} of {count} is not a literal regular file: "
296 )?;
297 match fault {
298 GrantFault::Symlink => f.write_str("it is a symlink"),
299 GrantFault::NotRegular => f.write_str("it is not a regular file"),
300 GrantFault::Io(kind) => write!(f, "{kind:?}"),
301 }
302 }
303 RunnerMessage::JailDirectoryCreate(kind) => {
304 write!(f, "cannot create the jail directory: {kind:?}")
305 }
306 RunnerMessage::JailDirectoryCanonicalize(kind) => {
307 write!(f, "cannot canonicalize the jail directory: {kind:?}")
308 }
309 RunnerMessage::InputNameNotBare => f.write_str("an input name is not a bare file name"),
310 RunnerMessage::StageInput(kind) => write!(f, "cannot stage an input: {kind:?}"),
311 RunnerMessage::SpawnWorker(kind) => {
312 write!(f, "cannot spawn the jailed worker: {kind:?}")
313 }
314 RunnerMessage::DrainTimeout(milliseconds) => write!(
315 f,
316 "a pipe stayed open {milliseconds} milliseconds after the group kill, so a descendant escaped the process group. The drain is detached and all output is discarded"
317 ),
318 RunnerMessage::WallTimeout(milliseconds) => write!(
319 f,
320 "killed the process group after the {milliseconds} millisecond wall-clock ceiling, partial output discarded"
321 ),
322 RunnerMessage::ResponseFrameOversized { declared, ceiling } => write!(
323 f,
324 "response frame declared {declared} bytes over the {ceiling} byte ceiling, refused before allocation"
325 ),
326 RunnerMessage::StderrOverflow { written, ceiling } => {
327 write!(
328 f,
329 "stderr wrote {written} bytes over the {ceiling} byte cap"
330 )
331 }
332 RunnerMessage::TrailingStdout => {
333 f.write_str("bytes after the response frame, one frame is the whole stdout budget")
334 }
335 RunnerMessage::Exit(detail) => match detail {
336 ExitDetail::SandboxSetup(code) => write!(
337 f,
338 "the sandbox helper refused to engage the jail (exit {code})"
339 ),
340 ExitDetail::ProcessCount(code) => write!(
341 f,
342 "the sandbox helper could not measure the user's process count and refused the spawn (exit {code})"
343 ),
344 ExitDetail::WorkerCode(code) => write!(
345 f,
346 "worker exited with code {code}, any response frame is discarded"
347 ),
348 ExitDetail::WorkerSignal(signal) => write!(
349 f,
350 "worker died on signal {signal}, any response frame is discarded"
351 ),
352 ExitDetail::WorkerNoStatus => {
353 f.write_str("worker exited without a status, any response frame is discarded")
354 }
355 },
356 RunnerMessage::EmptyResponse => f.write_str("worker exited without a response frame"),
357 RunnerMessage::TruncatedResponse => {
358 f.write_str("worker exited with a truncated response frame")
359 }
360 RunnerMessage::ReadResponse(kind) => {
361 write!(f, "reading the response failed: {kind:?}")
362 }
363 RunnerMessage::ParseResponse => f.write_str("response frame did not parse"),
364 RunnerMessage::ValidateResponse(detail) => match detail {
365 ResponseValidationError::BothBodies => {
366 f.write_str("response carries both ok and error")
367 }
368 ResponseValidationError::NoBody => {
369 f.write_str("response carries neither ok nor error")
370 }
371 },
372 RunnerMessage::MemoryExceeded { total, ceiling } => write!(
373 f,
374 "the worker process group holds {total} resident bytes over the {ceiling} byte ceiling, killed"
375 ),
376 RunnerMessage::Memory(error) => match error {
377 memory::MemoryError::ProcessTable(kind) => {
378 write!(f, "cannot enumerate the process table: {kind:?}")
379 }
380 memory::MemoryError::ProcessGroup(kind) => {
381 write!(f, "cannot enumerate the process group: {kind:?}")
382 }
383 memory::MemoryError::ProcessGroupRecheck(kind) => {
384 write!(f, "cannot re-enumerate the process group: {kind:?}")
385 }
386 memory::MemoryError::GroupLeaderPid(pid) => {
387 write!(f, "group leader pid {pid} does not fit a pid_t")
388 }
389 memory::MemoryError::GroupMemberPid(pid) => {
390 write!(f, "group member pid {pid} does not fit a pid_t")
391 }
392 memory::MemoryError::ResidentSizeSum => {
393 f.write_str("the resident-size sum overflowed")
394 }
395 memory::MemoryError::ResidentSizeRead(pid) => {
396 write!(f, "cannot read the resident size of process {pid}")
397 }
398 memory::MemoryError::StatRecord(pid) => {
399 write!(f, "cannot parse the stat record of process {pid}")
400 }
401 memory::MemoryError::ProcessRecords(pid, kind) => {
402 write!(f, "cannot read the records of process {pid}: {kind:?}")
403 }
404 memory::MemoryError::ProcessRecheck(pid, kind) => write!(
405 f,
406 "cannot re-check process {pid} after a read failure: {kind:?}"
407 ),
408 memory::MemoryError::ResidentSizeValue => f.write_str("cannot parse a VmRSS value"),
409 memory::MemoryError::ResidentSizeUnit => {
410 f.write_str("a VmRSS line is not in kibibytes")
411 }
412 memory::MemoryError::ResidentSizeScale => f.write_str("a VmRSS value overflowed"),
413 memory::MemoryError::MonitorUnavailable => {
414 f.write_str("no resident-memory monitor exists for this platform")
415 }
416 },
417 RunnerMessage::Closure {
418 position,
419 count,
420 fault,
421 } => {
422 write!(f, "closure entry {position} of {count} ")?;
423 match fault {
424 ClosureFault::NotAbsolute => f.write_str("is not absolute"),
425 ClosureFault::NotNormal => f.write_str("is not in normal form"),
426 ClosureFault::Symlink => f.write_str("is a symlink, not the tree it names"),
427 ClosureFault::SharedRoot => {
428 f.write_str("is a shared root, not a measured dependency")
429 }
430 ClosureFault::Unresolved => f.write_str("cannot be resolved"),
431 ClosureFault::NotFileOrDirectory => {
432 f.write_str("is not a directory or a regular file")
433 }
434 ClosureFault::ResolvedSharedRoot => {
435 f.write_str("resolves to a shared root, not a measured dependency")
436 }
437 }
438 }
439 RunnerMessage::ServiceNamespace => {
440 f.write_str("the service namespace is not an anchored dotted prefix")
441 }
442 RunnerMessage::TempDirectoryVariable => {
443 f.write_str("the temp-directory variable is not a bounded identifier")
444 }
445 RunnerMessage::PlatformBackendMissing => {
446 f.write_str("no jail backend exists for this platform, refusing to run adapters")
447 }
448 RunnerMessage::WorkerHash(kind) => {
449 write!(f, "the worker binary cannot be hashed: {kind:?}")
450 }
451 RunnerMessage::ProbeRequestEncode => f.write_str("cannot encode probe request"),
452 RunnerMessage::ProbeReportedFailure => f.write_str("network probe reported a failure"),
453 RunnerMessage::ProbeResponseParse => {
454 f.write_str("network probe response did not parse")
455 }
456 RunnerMessage::ProbeAttemptMissing(name) => {
457 write!(f, "network probe skipped the {} attempt", probe_name(name))
458 }
459 RunnerMessage::ProbeAttempt {
460 name,
461 outcome,
462 error_kind,
463 } => write!(
464 f,
465 "network probe attempt {} ended {} ({error_kind:?}), the jail is not proven",
466 probe_name(name),
467 probe_outcome(outcome)
468 ),
469 RunnerMessage::ProbeListener(kind) => {
470 write!(f, "cannot stand up the Unix probe listener: {kind:?}")
471 }
472 RunnerMessage::TemplateBrace(value) => write!(
473 f,
474 "the {} value carries a brace and cannot be rendered",
475 template_value(value)
476 ),
477 RunnerMessage::ClosureNotUtf8 { position, count } => {
478 write!(f, "closure entry {position} of {count} is not UTF-8")
479 }
480 RunnerMessage::ClosureCannotBeQuoted { position, count } => write!(
481 f,
482 "closure entry {position} of {count} cannot be quoted in a jail profile"
483 ),
484 RunnerMessage::PathNotUtf8(class) => {
485 write!(f, "{} is not UTF-8", path_class(class))
486 }
487 RunnerMessage::PathCannotBeQuoted(class) => write!(
488 f,
489 "{} cannot be quoted in a Seatbelt profile",
490 path_class(class)
491 ),
492 RunnerMessage::ProviderNeedsWiring => {
493 f.write_str("the provider jail class needs a wired provider, refusing the run")
494 }
495 RunnerMessage::SandboxHelperMissing => {
496 f.write_str("the sandbox helper is missing, refusing to run adapters")
497 }
498 RunnerMessage::ProviderPlatformUnsupported => f.write_str(
499 "no provider jail profile is measured for this platform, refusing the run",
500 ),
501 RunnerMessage::SyscallProbeAllowed => {
502 f.write_str("the syscall filter let a socket creation through")
503 }
504 RunnerMessage::SyscallProbeFailed => {
505 f.write_str("the syscall filter probe failed to run")
506 }
507 }
508 }
509}
510
511fn probe_name(name: ProbeName) -> &'static str {
512 match name {
513 ProbeName::Ipv4Tcp => "ipv4_tcp",
514 ProbeName::Ipv6Tcp => "ipv6_tcp",
515 ProbeName::UnixSocket => "unix_socket",
516 ProbeName::UdpSend => "udp_send",
517 }
518}
519
520fn probe_outcome(outcome: ProbeOutcome) -> &'static str {
521 match outcome {
522 ProbeOutcome::Connected => "connected",
523 ProbeOutcome::Denied => "denied",
524 ProbeOutcome::Timeout => "timeout",
525 ProbeOutcome::InvalidTarget => "invalid_target",
526 }
527}
528
529fn path_class(class: PathClass) -> &'static str {
530 match class {
531 PathClass::Jail => "the jail path",
532 PathClass::Worker => "the worker path",
533 PathClass::ExecutableGrantDirectory => "an executable grant's directory",
534 PathClass::ExecutableGrant => "an executable grant",
535 PathClass::ReadGrant => "a read grant",
536 }
537}
538
539fn template_value(value: TemplateValue) -> &'static str {
540 match value {
541 TemplateValue::Jail => "jail template",
542 TemplateValue::Worker => "worker template",
543 TemplateValue::Closures => "closures template",
544 TemplateValue::Service => "service template",
545 TemplateValue::Grants => "grants template",
546 }
547}
548
549pub fn provider_profile_template_blake3() -> Option<String> {
553 #[cfg(target_os = "macos")]
554 {
555 Some(macos::provider_profile_template_blake3())
556 }
557 #[cfg(not(target_os = "macos"))]
558 {
559 None
560 }
561}
562
563pub const WORKER_BINARY: &str = "text-mirror-worker";
565
566pub const SANDBOX_SETUP_EXIT: i32 = 71;
569
570pub const PROCESS_COUNT_EXIT: i32 = 72;
574
575pub fn locate_worker() -> Result<PathBuf, RunnerError> {
578 let current = std::env::current_exe().map_err(|e| {
579 RunnerError::new(
580 "worker_not_found",
581 RunnerMessage::CurrentExecutable(e.kind()),
582 )
583 })?;
584 let Some(dir) = current.parent() else {
585 return Err(RunnerError::new(
586 "worker_not_found",
587 RunnerMessage::CurrentExecutableHasNoDirectory,
588 ));
589 };
590 let mut candidates = vec![dir.join(WORKER_BINARY)];
591 if let Some(parent) = dir.parent() {
592 candidates.push(parent.join(WORKER_BINARY));
593 }
594 for candidate in &candidates {
595 if candidate.is_file() {
596 return candidate.canonicalize().map_err(|e| {
597 RunnerError::new(
598 "worker_not_found",
599 RunnerMessage::WorkerBinaryResolve(e.kind()),
600 )
601 });
602 }
603 }
604 Err(RunnerError::new(
605 "worker_not_found",
606 RunnerMessage::WorkerBinaryMissing,
607 ))
608}
609
610pub struct Runner {
612 backend: Box<dyn JailBackend>,
613 worker: PathBuf,
614 limits: Limits,
615 exec_grants: Vec<PathBuf>,
616 read_grants: Vec<PathBuf>,
617 runtime_profile: RuntimeProfile,
618 provider: Option<ProviderJail>,
619}
620
621impl Runner {
622 pub fn new(backend: Box<dyn JailBackend>, worker: PathBuf, limits: Limits) -> Runner {
625 Runner::with_grants(
626 backend,
627 worker,
628 limits,
629 Vec::new(),
630 Vec::new(),
631 RuntimeProfile::Plain,
632 )
633 }
634
635 pub fn with_grants(
647 backend: Box<dyn JailBackend>,
648 worker: PathBuf,
649 limits: Limits,
650 exec_grants: Vec<PathBuf>,
651 read_grants: Vec<PathBuf>,
652 runtime_profile: RuntimeProfile,
653 ) -> Runner {
654 Runner {
655 backend,
656 worker,
657 limits,
658 exec_grants,
659 read_grants,
660 runtime_profile,
661 provider: None,
662 }
663 }
664
665 pub fn with_provider_jail(
674 backend: Box<dyn JailBackend>,
675 worker: PathBuf,
676 limits: Limits,
677 exec_grants: Vec<PathBuf>,
678 read_grants: Vec<PathBuf>,
679 provider: ProviderJail,
680 ) -> Runner {
681 Runner {
682 backend,
683 worker,
684 limits,
685 exec_grants,
686 read_grants,
687 runtime_profile: RuntimeProfile::Provider,
688 provider: Some(provider),
689 }
690 }
691
692 pub fn with_platform_defaults() -> Result<Runner, RunnerError> {
695 Ok(Runner::new(
696 jail::platform_backend()?,
697 locate_worker()?,
698 Limits::default(),
699 ))
700 }
701
702 pub fn backend(&self) -> &dyn JailBackend {
704 self.backend.as_ref()
705 }
706
707 pub fn limits(&self) -> &Limits {
709 &self.limits
710 }
711
712 #[cfg(test)]
715 pub(crate) fn grant_lists(&self) -> (&[PathBuf], &[PathBuf]) {
716 (&self.exec_grants, &self.read_grants)
717 }
718
719 #[cfg(test)]
722 pub(crate) fn runtime_profile(&self) -> RuntimeProfile {
723 self.runtime_profile
724 }
725
726 #[cfg(test)]
729 pub(crate) fn provider_jail(&self) -> Option<&ProviderJail> {
730 self.provider.as_ref()
731 }
732
733 pub fn policy_digest(&self) -> Result<String, RunnerError> {
735 jail::policy_digest(
736 self.backend.as_ref(),
737 &self.worker,
738 &self.exec_grants,
739 &self.read_grants,
740 self.runtime_profile,
741 self.provider.as_ref(),
742 )
743 }
744
745 pub fn run(
752 &self,
753 mode: &str,
754 payload: serde_json::Value,
755 files: &[(&str, &[u8])],
756 ) -> Result<Response, RunnerError> {
757 let digest = self.policy_digest()?;
758 jail::ensure_probed(self, &digest)?;
759 self.run_unprobed(mode, payload, files, true)
760 }
761
762 pub(crate) fn run_unprobed(
765 &self,
766 mode: &str,
767 payload: serde_json::Value,
768 files: &[(&str, &[u8])],
769 seccomp: bool,
770 ) -> Result<Response, RunnerError> {
771 let grants =
781 self.exec_grants
782 .iter()
783 .enumerate()
784 .map(|(index, grant)| {
785 (
786 GrantKind::Executable,
787 index + 1,
788 self.exec_grants.len(),
789 grant,
790 )
791 })
792 .chain(self.read_grants.iter().enumerate().map(|(index, grant)| {
793 (GrantKind::Read, index + 1, self.read_grants.len(), grant)
794 }));
795 for (kind, position, count, grant) in grants {
796 match std::fs::symlink_metadata(grant) {
797 Ok(metadata) if metadata.file_type().is_file() => {}
798 Ok(metadata) if metadata.file_type().is_symlink() => {
799 return Err(RunnerError::new(
800 "adapter_spawn_error",
801 RunnerMessage::Grant {
802 kind,
803 position,
804 count,
805 fault: GrantFault::Symlink,
806 },
807 ));
808 }
809 Ok(_) => {
810 return Err(RunnerError::new(
811 "adapter_spawn_error",
812 RunnerMessage::Grant {
813 kind,
814 position,
815 count,
816 fault: GrantFault::NotRegular,
817 },
818 ));
819 }
820 Err(e) => {
821 return Err(RunnerError::new(
822 "adapter_spawn_error",
823 RunnerMessage::Grant {
824 kind,
825 position,
826 count,
827 fault: GrantFault::Io(e.kind()),
828 },
829 ));
830 }
831 }
832 }
833 let jail_dir = tempfile::tempdir().map_err(|e| {
834 RunnerError::new(
835 "adapter_spawn_error",
836 RunnerMessage::JailDirectoryCreate(e.kind()),
837 )
838 })?;
839 let jail_path = jail_dir.path().canonicalize().map_err(|e| {
840 RunnerError::new(
841 "adapter_spawn_error",
842 RunnerMessage::JailDirectoryCanonicalize(e.kind()),
843 )
844 })?;
845 for (name, bytes) in files {
846 if name.contains('/') || name.contains("..") {
847 return Err(RunnerError::new(
848 "adapter_spawn_error",
849 RunnerMessage::InputNameNotBare,
850 ));
851 }
852 std::fs::write(jail_path.join(name), bytes).map_err(|e| {
853 RunnerError::new("adapter_spawn_error", RunnerMessage::StageInput(e.kind()))
854 })?;
855 }
856
857 let spec = SpawnSpec {
858 worker: &self.worker,
859 jail: &jail_path,
860 mode,
861 limits: &self.limits,
862 seccomp,
863 exec_grants: &self.exec_grants,
864 read_grants: &self.read_grants,
865 runtime_profile: self.runtime_profile,
866 provider: self.provider.as_ref(),
867 };
868 let mut command = self.backend.command(&spec)?;
869 command
870 .env_clear()
871 .current_dir(&jail_path)
872 .stdin(Stdio::piped())
873 .stdout(Stdio::piped())
874 .stderr(Stdio::piped());
875 #[cfg(unix)]
876 {
877 use std::os::unix::process::CommandExt;
878 command.process_group(0);
879 }
880 let mut child = command.spawn().map_err(|e| {
881 RunnerError::new("adapter_spawn_error", RunnerMessage::SpawnWorker(e.kind()))
882 })?;
883
884 let request = Request {
885 schema: RequestSchema,
886 adapter: mode.to_string(),
887 payload,
888 };
889 if let Some(mut stdin) = child.stdin.take() {
890 let _ = protocol::write_frame(&mut stdin, &request);
893 }
894
895 drive(&mut child, &self.limits)
896 }
897}
898
899enum StdoutEnd {
901 Frame(Vec<u8>),
902 Empty,
903 Truncated,
904 Oversized { declared: u64 },
905 Trailing,
906 ReadError(std::io::ErrorKind),
907}
908
909const DRAIN_JOIN_BOUND: Duration = Duration::from_secs(2);
914
915fn drive(child: &mut Child, limits: &Limits) -> Result<Response, RunnerError> {
925 let kill_now = Arc::new(AtomicBool::new(false));
926
927 let stdout = child.stdout.take().expect("stdout was piped");
928 let stdout_kill = kill_now.clone();
929 let max_response = limits.max_response_bytes;
930 let (stdout_done, stdout_result) = std::sync::mpsc::channel();
931 std::thread::spawn(move || {
932 let mut reader = stdout;
933 let end = match protocol::read_frame(&mut reader, max_response) {
934 Ok(FrameRead::Complete(payload)) => {
935 let mut probe = [0u8; 1];
938 match reader.read(&mut probe) {
939 Ok(0) => StdoutEnd::Frame(payload),
940 Ok(_) => StdoutEnd::Trailing,
941 Err(e) => StdoutEnd::ReadError(e.kind()),
942 }
943 }
944 Ok(FrameRead::Empty) => StdoutEnd::Empty,
945 Ok(FrameRead::Truncated) => StdoutEnd::Truncated,
946 Ok(FrameRead::Oversized { declared }) => StdoutEnd::Oversized { declared },
947 Err(e) => StdoutEnd::ReadError(e.kind()),
948 };
949 if matches!(
950 end,
951 StdoutEnd::Oversized { .. } | StdoutEnd::Trailing | StdoutEnd::ReadError(_)
952 ) {
953 stdout_kill.store(true, Ordering::SeqCst);
954 }
955 if matches!(end, StdoutEnd::Oversized { .. } | StdoutEnd::Trailing) {
956 let mut sink = [0u8; 8192];
960 while matches!(reader.read(&mut sink), Ok(n) if n > 0) {}
961 }
962 let _ = stdout_done.send(end);
963 });
964
965 let stderr = child.stderr.take().expect("stderr was piped");
966 let stderr_kill = kill_now.clone();
967 let max_stderr = limits.max_stderr_bytes;
968 let (stderr_done, stderr_result) = std::sync::mpsc::channel();
969 std::thread::spawn(move || {
970 let mut reader = stderr;
971 let mut kept = Vec::new();
972 let mut total: u64 = 0;
973 let mut chunk = [0u8; 8192];
974 loop {
975 match reader.read(&mut chunk) {
976 Ok(0) => break,
977 Ok(n) => {
978 total += n as u64;
979 if kept.len() < 4096 {
980 let take = (4096 - kept.len()).min(n);
981 kept.extend_from_slice(&chunk[..take]);
982 }
983 if total > max_stderr {
984 stderr_kill.store(true, Ordering::SeqCst);
985 }
987 }
988 Err(_) => break,
989 }
990 }
991 let _ = stderr_done.send((kept, total));
992 });
993
994 let deadline = Instant::now() + limits.wall_timeout;
995 let mut timed_out = false;
996 const MEMORY_POLL_INTERVAL: Duration = Duration::from_millis(100);
1003 let mut next_memory_check = Instant::now() + MEMORY_POLL_INTERVAL;
1004 let mut memory_failure: Option<RunnerError> = None;
1005 let status = loop {
1006 if let Some(status) = child.try_wait().ok().flatten() {
1007 break status;
1008 }
1009 if Instant::now() >= deadline {
1010 timed_out = true;
1011 kill_group(child);
1012 break wait_after_kill(child);
1013 }
1014 if kill_now.load(Ordering::SeqCst) {
1015 kill_group(child);
1016 break wait_after_kill(child);
1017 }
1018 if let Some(ceiling) = limits.max_resident_bytes
1019 && Instant::now() >= next_memory_check
1020 {
1021 next_memory_check = Instant::now() + MEMORY_POLL_INTERVAL;
1022 memory_failure = memory_verdict(memory::group_resident_bytes(child.id()), ceiling);
1023 if memory_failure.is_some() {
1024 kill_group(child);
1025 break wait_after_kill(child);
1026 }
1027 }
1028 std::thread::sleep(Duration::from_millis(2));
1029 };
1030
1031 if let Some(ceiling) = limits.max_resident_bytes
1040 && memory_failure.is_none()
1041 && !timed_out
1042 {
1043 memory_failure = memory_verdict(memory::group_resident_bytes(child.id()), ceiling);
1044 }
1045
1046 kill_group(child);
1052
1053 let join_deadline = Instant::now() + DRAIN_JOIN_BOUND;
1059 let stdout_end = stdout_result.recv_timeout(DRAIN_JOIN_BOUND).ok();
1060 let stderr_end = stderr_result
1061 .recv_timeout(join_deadline.saturating_duration_since(Instant::now()))
1062 .ok();
1063 let (Some(stdout_end), Some((stderr_kept, stderr_total))) = (stdout_end, stderr_end) else {
1064 return Err(RunnerError::new(
1065 "adapter_drain_timeout",
1066 RunnerMessage::DrainTimeout(
1067 u64::try_from(DRAIN_JOIN_BOUND.as_millis()).unwrap_or(u64::MAX),
1068 ),
1069 ));
1070 };
1071
1072 if let Some(failure) = memory_failure {
1073 return Err(failure);
1074 }
1075 if timed_out {
1076 return Err(RunnerError::new(
1077 "adapter_timeout",
1078 RunnerMessage::WallTimeout(
1079 u64::try_from(limits.wall_timeout.as_millis()).unwrap_or(u64::MAX),
1080 ),
1081 ));
1082 }
1083 if let StdoutEnd::Oversized { declared } = stdout_end {
1084 return Err(RunnerError::new(
1085 "adapter_frame_oversized",
1086 RunnerMessage::ResponseFrameOversized {
1087 declared,
1088 ceiling: u64::from(limits.max_response_bytes),
1089 },
1090 ));
1091 }
1092 if stderr_total > limits.max_stderr_bytes {
1093 return Err(RunnerError::new(
1094 "adapter_output_overflow",
1095 RunnerMessage::StderrOverflow {
1096 written: stderr_total,
1097 ceiling: limits.max_stderr_bytes,
1098 },
1099 ));
1100 }
1101 if matches!(stdout_end, StdoutEnd::Trailing) {
1102 return Err(RunnerError::new(
1103 "adapter_output_overflow",
1104 RunnerMessage::TrailingStdout,
1105 ));
1106 }
1107 if !status.success() {
1108 let stderr_text = String::from_utf8_lossy(&stderr_kept);
1109 let stderr_text = scrub_stderr(&stderr_text, limits.max_stderr_bytes);
1110 if !stderr_text.is_empty() {
1111 eprintln!("adapter stderr: {stderr_text}");
1112 }
1113 #[cfg(unix)]
1114 let refusal = helper_refusal(status.code());
1115 #[cfg(not(unix))]
1116 let refusal = None;
1117 if let Some(error) = refusal {
1118 return Err(error);
1119 }
1120 return Err(RunnerError::new(
1121 "adapter_crash",
1122 RunnerMessage::Exit(worker_crash_detail(&status)),
1123 ));
1124 }
1125 let payload = match stdout_end {
1126 StdoutEnd::Frame(payload) => payload,
1127 StdoutEnd::Empty => {
1128 return Err(RunnerError::new(
1129 "adapter_protocol_error",
1130 RunnerMessage::EmptyResponse,
1131 ));
1132 }
1133 StdoutEnd::Truncated => {
1134 return Err(RunnerError::new(
1135 "adapter_protocol_error",
1136 RunnerMessage::TruncatedResponse,
1137 ));
1138 }
1139 StdoutEnd::ReadError(detail) => {
1140 return Err(RunnerError::new(
1141 "adapter_protocol_error",
1142 RunnerMessage::ReadResponse(detail),
1143 ));
1144 }
1145 StdoutEnd::Oversized { .. } | StdoutEnd::Trailing => unreachable!("handled above"),
1146 };
1147 let response: Response = serde_json::from_slice(&payload)
1148 .map_err(|_| RunnerError::new("adapter_protocol_error", RunnerMessage::ParseResponse))?;
1149 response.validate().map_err(|detail| {
1150 RunnerError::new(
1151 "adapter_protocol_error",
1152 RunnerMessage::ValidateResponse(detail),
1153 )
1154 })?;
1155 Ok(response)
1156}
1157
1158fn scrub_stderr(stderr_text: &str, max_bytes: u64) -> String {
1163 const PATH_PLACEHOLDER: &str = "[path]";
1164
1165 let max_bytes = usize::try_from(max_bytes).unwrap_or(usize::MAX);
1166 let mut scrubbed = String::new();
1167 for line in stderr_text
1168 .lines()
1169 .map(str::trim)
1170 .filter(|line| !line.is_empty())
1171 {
1172 let line = if line.contains(['/', '\\']) {
1173 PATH_PLACEHOLDER
1174 } else {
1175 line
1176 };
1177 let separator = usize::from(!scrubbed.is_empty());
1178 if scrubbed
1179 .len()
1180 .saturating_add(separator)
1181 .saturating_add(line.len())
1182 > max_bytes
1183 {
1184 break;
1185 }
1186 if separator == 1 {
1187 scrubbed.push(' ');
1188 }
1189 scrubbed.push_str(line);
1190 }
1191 scrubbed
1192}
1193
1194#[cfg_attr(not(unix), allow(dead_code))]
1200fn helper_refusal(exit: Option<i32>) -> Option<RunnerError> {
1201 let exit = exit?;
1202 let (code, detail) = match exit {
1203 SANDBOX_SETUP_EXIT => (
1204 "sandbox_unavailable",
1205 ExitDetail::SandboxSetup(SANDBOX_SETUP_EXIT),
1206 ),
1207 PROCESS_COUNT_EXIT => (
1208 "process-count-unavailable",
1209 ExitDetail::ProcessCount(PROCESS_COUNT_EXIT),
1210 ),
1211 _ => return None,
1212 };
1213 Some(RunnerError::new(code, RunnerMessage::Exit(detail)))
1214}
1215
1216fn memory_verdict(
1221 measurement: Result<u64, memory::MemoryError>,
1222 ceiling: u64,
1223) -> Option<RunnerError> {
1224 match measurement {
1225 Ok(total) if total > ceiling => Some(RunnerError::new(
1226 "worker-memory-exceeded",
1227 RunnerMessage::MemoryExceeded { total, ceiling },
1228 )),
1229 Ok(_) => None,
1230 Err(error) => Some(RunnerError::new(
1231 "memory-monitor-failed",
1232 RunnerMessage::Memory(error),
1233 )),
1234 }
1235}
1236
1237fn kill_group(child: &mut Child) {
1239 #[cfg(unix)]
1240 {
1241 if let Ok(pid) = i32::try_from(child.id()) {
1242 let _ = nix::sys::signal::killpg(
1245 nix::unistd::Pid::from_raw(pid),
1246 nix::sys::signal::Signal::SIGKILL,
1247 );
1248 }
1249 }
1250 #[cfg(not(unix))]
1251 {
1252 let _ = child.kill();
1253 }
1254}
1255
1256fn wait_after_kill(child: &mut Child) -> std::process::ExitStatus {
1257 child.wait().unwrap_or_else(|_| {
1258 child
1260 .try_wait()
1261 .ok()
1262 .flatten()
1263 .expect("killed child has an exit status")
1264 })
1265}
1266
1267fn worker_crash_detail(status: &std::process::ExitStatus) -> ExitDetail {
1268 #[cfg(unix)]
1269 {
1270 use std::os::unix::process::ExitStatusExt;
1271 if let Some(signal) = status.signal() {
1272 return ExitDetail::WorkerSignal(signal);
1273 }
1274 }
1275 match status.code() {
1276 Some(code) => ExitDetail::WorkerCode(code),
1277 None => ExitDetail::WorkerNoStatus,
1278 }
1279}
1280
1281#[cfg(test)]
1282mod tests {
1283 use super::*;
1284
1285 #[test]
1286 fn a_helper_refusal_is_named_and_never_folded_into_a_crash() {
1287 let setup = helper_refusal(Some(SANDBOX_SETUP_EXIT)).expect("a setup exit is a refusal");
1289 assert_eq!(setup.code, "sandbox_unavailable");
1290 assert_eq!(
1291 setup.message(),
1292 "the sandbox helper refused to engage the jail (exit 71)"
1293 );
1294 let count = helper_refusal(Some(PROCESS_COUNT_EXIT)).expect("a count exit is a refusal");
1297 assert_eq!(count.code, "process-count-unavailable");
1298 assert_eq!(
1299 count.message(),
1300 "the sandbox helper could not measure the user's process count and refused the spawn (exit 72)"
1301 );
1302 assert!(helper_refusal(Some(1)).is_none());
1304 assert!(helper_refusal(None).is_none());
1305 }
1306
1307 #[test]
1308 fn stderr_log_scrub_replaces_plain_and_spaced_paths() {
1309 let absolute = "/runtime/worker";
1310 let spaced = "/runtime/private deploy secret";
1311 let detail = format!(
1312 "fixed helper detail\nsandbox_setup_failed: open {absolute}\nsandbox_setup_failed: open {spaced}"
1313 );
1314 let scrubbed = scrub_stderr(&detail, 1024);
1315 assert_eq!(scrubbed, "fixed helper detail [path] [path]");
1316 assert!(!scrubbed.contains(absolute), "{scrubbed}");
1317 for component in spaced.split_whitespace() {
1318 assert!(!scrubbed.contains(component), "{scrubbed}");
1319 }
1320 assert!(scrubbed.len() <= 1024);
1321 }
1322
1323 #[cfg(unix)]
1324 #[test]
1325 fn a_helper_failure_detail_never_enters_a_record_or_bundle() {
1326 use std::os::unix::process::CommandExt;
1327
1328 let marker = format!("spaced_path_{}", std::process::id());
1329 let pieces = [
1330 format!("{marker}_root"),
1331 format!("{marker}_first"),
1332 format!("{marker}_second"),
1333 format!("{marker}_third"),
1334 ];
1335 let private_path = std::path::Path::new("/")
1336 .join(&pieces[0])
1337 .join(format!("{} {} {}", pieces[1], pieces[2], pieces[3]));
1338 let stderr = format!(
1339 "sandbox_setup_failed: cannot open {}",
1340 private_path.display()
1341 );
1342 let mut command = std::process::Command::new("/bin/sh");
1343 command
1344 .arg("-c")
1345 .arg("printf '%s\\n' \"$1\" >&2; exit 71")
1346 .arg("helper")
1347 .arg(stderr)
1348 .stdin(Stdio::null())
1349 .stdout(Stdio::piped())
1350 .stderr(Stdio::piped())
1351 .process_group(0);
1352 let mut child = command.spawn().unwrap();
1353 let failure = drive(&mut child, &Limits::default()).expect_err("the helper refuses");
1354 assert_eq!(failure.code, "sandbox_unavailable");
1355 assert_eq!(
1356 failure.message(),
1357 "the sandbox helper refused to engage the jail (exit 71)"
1358 );
1359
1360 let dir = tempfile::tempdir().unwrap();
1361 let manifest_dir = dir.path().join("manifest");
1362 std::fs::create_dir_all(&manifest_dir).unwrap();
1363 let record = crate::manifest::Record {
1364 schema: crate::manifest::ManifestSchema,
1365 source_path: "input.bin".to_string(),
1366 source_hash: crate::hash::hash_bytes(b"input"),
1367 source_size: 5,
1368 declared_format: None,
1369 detected_format: "unknown".to_string(),
1370 format_mismatch: false,
1371 status: crate::manifest::Status::Failed,
1372 text_path: None,
1373 text_hash: None,
1374 artifact_kind: None,
1375 converter_id: Some("adapter".to_string()),
1376 converter_version: Some("1".to_string()),
1377 tool_version: None,
1378 rules_version: crate::pipeline::Rules::builtin()
1379 .unwrap()
1380 .version()
1381 .to_string(),
1382 media: None,
1383 parent_source: None,
1384 dedup_of: None,
1385 warnings: Vec::new(),
1386 error: Some(failure.to_string()),
1387 duration_ms: Some(0),
1388 };
1389 assert_eq!(
1390 record.error.as_deref(),
1391 Some("sandbox_unavailable: the sandbox helper refused to engage the jail (exit 71)")
1392 );
1393 let serialized = serde_json::to_vec(&record).unwrap();
1394 let shard = manifest_dir.join("alpha.jsonl");
1395 let mut writer = crate::manifest::ManifestWriter::open(&shard).unwrap();
1396 writer.append(&record).unwrap();
1397 drop(writer);
1398
1399 let bundle = dir.path().join("bundle");
1400 crate::bundle::bundle(&crate::bundle::BundleOptions {
1401 mirror_root: &dir.path().join("mirror"),
1402 manifest_dir: &manifest_dir,
1403 division: "alpha",
1404 output: &bundle,
1405 })
1406 .unwrap();
1407
1408 fn assert_absent(bytes: &[u8], piece: &[u8]) {
1409 assert!(
1410 !bytes.windows(piece.len()).any(|window| window == piece),
1411 "a path component reached stored bytes"
1412 );
1413 }
1414
1415 fn check_tree(dir: &std::path::Path, pieces: &[String]) {
1416 for entry in std::fs::read_dir(dir).unwrap() {
1417 let path = entry.unwrap().path();
1418 if path.is_dir() {
1419 check_tree(&path, pieces);
1420 } else {
1421 let bytes = std::fs::read(path).unwrap();
1422 for piece in pieces {
1423 assert_absent(&bytes, piece.as_bytes());
1424 }
1425 }
1426 }
1427 }
1428
1429 for piece in &pieces {
1430 assert_absent(&serialized, piece.as_bytes());
1431 }
1432 check_tree(&bundle, &pieces);
1433 }
1434
1435 #[cfg(unix)]
1436 #[test]
1437 fn a_worker_crash_message_carries_only_the_exit_classification() {
1438 use std::os::unix::process::CommandExt;
1439
1440 let marker = format!("worker_path_{}", std::process::id());
1441 let stderr = format!("worker failed while reading /{marker}/with spaces");
1442 let mut command = std::process::Command::new("/bin/sh");
1443 command
1444 .arg("-c")
1445 .arg("printf '%s\\n' \"$1\" >&2; exit 9")
1446 .arg("worker")
1447 .arg(stderr)
1448 .stdin(Stdio::null())
1449 .stdout(Stdio::piped())
1450 .stderr(Stdio::piped())
1451 .process_group(0);
1452 let mut child = command.spawn().unwrap();
1453 let failure = drive(&mut child, &Limits::default()).expect_err("the worker fails");
1454 assert_eq!(failure.code, "adapter_crash");
1455 assert_eq!(
1456 failure.message(),
1457 "worker exited with code 9, any response frame is discarded"
1458 );
1459 assert!(!failure.message().contains(&marker), "{failure}");
1460 }
1461
1462 #[test]
1463 fn a_missing_worker_hash_reports_only_the_error_kind() {
1464 let dir = tempfile::tempdir().unwrap();
1465 let missing = dir.path().join("missing-worker");
1466 let backend = jail::platform_backend().expect("this host has a jail backend");
1467 let error = jail::policy_digest(
1468 backend.as_ref(),
1469 &missing,
1470 &[],
1471 &[],
1472 RuntimeProfile::Plain,
1473 None,
1474 )
1475 .expect_err("the missing worker cannot be hashed");
1476 assert_eq!(error.code, "worker_not_found");
1477 assert_eq!(
1478 error.message(),
1479 "the worker binary cannot be hashed: NotFound"
1480 );
1481 let message = error.message();
1482 for component in missing.components() {
1483 let std::path::Component::Normal(component) = component else {
1484 continue;
1485 };
1486 assert!(
1487 !message
1488 .as_bytes()
1489 .windows(component.as_encoded_bytes().len())
1490 .any(|window| window == component.as_encoded_bytes()),
1491 "a worker path component reached the message"
1492 );
1493 }
1494 }
1495
1496 #[test]
1497 fn the_memory_verdict_kills_over_the_ceiling_and_fails_closed_on_a_query_error() {
1498 assert!(memory_verdict(Ok(100), 200).is_none());
1500 assert!(memory_verdict(Ok(200), 200).is_none());
1502 let over = memory_verdict(Ok(201), 200).expect("over the ceiling is a verdict");
1504 assert_eq!(over.code, "worker-memory-exceeded");
1505 let failed = memory_verdict(
1508 Err(memory::MemoryError::ProcessTable(
1509 std::io::ErrorKind::PermissionDenied,
1510 )),
1511 200,
1512 )
1513 .expect("a failed measurement is a verdict");
1514 assert_eq!(failed.code, "memory-monitor-failed");
1515 assert_eq!(
1516 failed.message(),
1517 "cannot enumerate the process table: PermissionDenied"
1518 );
1519 }
1520}
1521
1522#[cfg(test)]
1524mod runner_message_tests {
1525 use super::*;
1526 use std::io::ErrorKind;
1527
1528 const RUST_1_88_STABLE_IO_ERROR_KIND_COUNT: usize = 39;
1529 pub(super) const IO_KINDS: &[ErrorKind] = &[
1532 ErrorKind::NotFound,
1533 ErrorKind::PermissionDenied,
1534 ErrorKind::ConnectionRefused,
1535 ErrorKind::ConnectionReset,
1536 ErrorKind::HostUnreachable,
1537 ErrorKind::NetworkUnreachable,
1538 ErrorKind::ConnectionAborted,
1539 ErrorKind::NotConnected,
1540 ErrorKind::AddrInUse,
1541 ErrorKind::AddrNotAvailable,
1542 ErrorKind::NetworkDown,
1543 ErrorKind::BrokenPipe,
1544 ErrorKind::AlreadyExists,
1545 ErrorKind::WouldBlock,
1546 ErrorKind::NotADirectory,
1547 ErrorKind::IsADirectory,
1548 ErrorKind::DirectoryNotEmpty,
1549 ErrorKind::ReadOnlyFilesystem,
1550 ErrorKind::StaleNetworkFileHandle,
1551 ErrorKind::InvalidInput,
1552 ErrorKind::InvalidData,
1553 ErrorKind::TimedOut,
1554 ErrorKind::WriteZero,
1555 ErrorKind::StorageFull,
1556 ErrorKind::NotSeekable,
1557 ErrorKind::QuotaExceeded,
1558 ErrorKind::FileTooLarge,
1559 ErrorKind::ResourceBusy,
1560 ErrorKind::ExecutableFileBusy,
1561 ErrorKind::Deadlock,
1562 ErrorKind::CrossesDevices,
1563 ErrorKind::TooManyLinks,
1564 ErrorKind::InvalidFilename,
1565 ErrorKind::ArgumentListTooLong,
1566 ErrorKind::Interrupted,
1567 ErrorKind::Unsupported,
1568 ErrorKind::UnexpectedEof,
1569 ErrorKind::OutOfMemory,
1570 ErrorKind::Other,
1571 ];
1572
1573 #[test]
1574 fn io_kind_table_covers_every_rust_1_88_stable_kind() {
1575 assert_eq!(IO_KINDS.len(), RUST_1_88_STABLE_IO_ERROR_KIND_COUNT);
1576 }
1577
1578 #[test]
1579 fn every_runner_message_variant_is_path_free() {
1580 const USIZES: &[usize] = &[0, 1, usize::MAX];
1581 const U64S: &[u64] = &[0, 1, u64::MAX];
1582 const U32S: &[u32] = &[0, 1, u32::MAX];
1583 const I32S: &[i32] = &[i32::MIN, -1, 0, 1, i32::MAX];
1584 const PROBE_NAMES: &[ProbeName] = &[
1585 ProbeName::Ipv4Tcp,
1586 ProbeName::Ipv6Tcp,
1587 ProbeName::UnixSocket,
1588 ProbeName::UdpSend,
1589 ];
1590 const PROBE_OUTCOMES: &[ProbeOutcome] = &[
1591 ProbeOutcome::Connected,
1592 ProbeOutcome::Denied,
1593 ProbeOutcome::Timeout,
1594 ProbeOutcome::InvalidTarget,
1595 ];
1596
1597 let render = |message: RunnerMessage| {
1598 let text = message.to_string();
1599 assert!(!text.contains('/'), "separator in runner message: {text}");
1600 assert!(!text.contains('\\'), "separator in runner message: {text}");
1601 assert!(
1602 text.is_ascii(),
1603 "non-ASCII byte in runner message: {text:?}"
1604 );
1605 };
1606
1607 for &kind in IO_KINDS {
1608 render(RunnerMessage::CurrentExecutable(kind));
1609 render(RunnerMessage::WorkerBinaryResolve(kind));
1610 render(RunnerMessage::JailDirectoryCreate(kind));
1611 render(RunnerMessage::JailDirectoryCanonicalize(kind));
1612 render(RunnerMessage::StageInput(kind));
1613 render(RunnerMessage::SpawnWorker(kind));
1614 render(RunnerMessage::ReadResponse(kind));
1615 render(RunnerMessage::WorkerHash(kind));
1616 render(RunnerMessage::ProbeListener(kind));
1617 for &grant in &[GrantKind::Executable, GrantKind::Read] {
1618 render(RunnerMessage::Grant {
1619 kind: grant,
1620 position: usize::MAX,
1621 count: usize::MAX,
1622 fault: GrantFault::Io(kind),
1623 });
1624 }
1625 for &name in PROBE_NAMES {
1626 for &outcome in PROBE_OUTCOMES {
1627 render(RunnerMessage::ProbeAttempt {
1628 name,
1629 outcome,
1630 error_kind: kind,
1631 });
1632 }
1633 }
1634 for error in [
1635 memory::MemoryError::ProcessTable(kind),
1636 memory::MemoryError::ProcessGroup(kind),
1637 memory::MemoryError::ProcessGroupRecheck(kind),
1638 memory::MemoryError::ProcessRecords(u32::MAX, kind),
1639 memory::MemoryError::ProcessRecheck(u32::MAX, kind),
1640 ] {
1641 render(RunnerMessage::Memory(error));
1642 }
1643 }
1644
1645 render(RunnerMessage::CurrentExecutableHasNoDirectory);
1646 render(RunnerMessage::WorkerBinaryMissing);
1647 render(RunnerMessage::InputNameNotBare);
1648 render(RunnerMessage::TrailingStdout);
1649 render(RunnerMessage::EmptyResponse);
1650 render(RunnerMessage::TruncatedResponse);
1651 render(RunnerMessage::ParseResponse);
1652 render(RunnerMessage::ProbeRequestEncode);
1653 render(RunnerMessage::ProbeReportedFailure);
1654 render(RunnerMessage::ProbeResponseParse);
1655 render(RunnerMessage::ServiceNamespace);
1656 render(RunnerMessage::TempDirectoryVariable);
1657 render(RunnerMessage::PlatformBackendMissing);
1658 render(RunnerMessage::ProviderNeedsWiring);
1659 render(RunnerMessage::SandboxHelperMissing);
1660 render(RunnerMessage::ProviderPlatformUnsupported);
1661 render(RunnerMessage::SyscallProbeAllowed);
1662 render(RunnerMessage::SyscallProbeFailed);
1663
1664 for &value in USIZES {
1665 for &kind in &[GrantKind::Executable, GrantKind::Read] {
1666 for &fault in &[GrantFault::Symlink, GrantFault::NotRegular] {
1667 render(RunnerMessage::Grant {
1668 kind,
1669 position: value,
1670 count: value,
1671 fault,
1672 });
1673 }
1674 }
1675 for &fault in &[
1676 ClosureFault::NotAbsolute,
1677 ClosureFault::NotNormal,
1678 ClosureFault::Symlink,
1679 ClosureFault::SharedRoot,
1680 ClosureFault::Unresolved,
1681 ClosureFault::NotFileOrDirectory,
1682 ClosureFault::ResolvedSharedRoot,
1683 ] {
1684 render(RunnerMessage::Closure {
1685 position: value,
1686 count: value,
1687 fault,
1688 });
1689 }
1690 render(RunnerMessage::ClosureNotUtf8 {
1691 position: value,
1692 count: value,
1693 });
1694 render(RunnerMessage::ClosureCannotBeQuoted {
1695 position: value,
1696 count: value,
1697 });
1698 }
1699
1700 for &value in U64S {
1701 render(RunnerMessage::DrainTimeout(value));
1702 render(RunnerMessage::WallTimeout(value));
1703 render(RunnerMessage::ResponseFrameOversized {
1704 declared: value,
1705 ceiling: value,
1706 });
1707 render(RunnerMessage::StderrOverflow {
1708 written: value,
1709 ceiling: value,
1710 });
1711 render(RunnerMessage::MemoryExceeded {
1712 total: value,
1713 ceiling: value,
1714 });
1715 }
1716
1717 for &value in U32S {
1718 for error in [
1719 memory::MemoryError::GroupLeaderPid(value),
1720 memory::MemoryError::GroupMemberPid(value),
1721 memory::MemoryError::ResidentSizeRead(value),
1722 memory::MemoryError::StatRecord(value),
1723 ] {
1724 render(RunnerMessage::Memory(error));
1725 }
1726 }
1727 for error in [
1728 memory::MemoryError::ResidentSizeSum,
1729 memory::MemoryError::ResidentSizeValue,
1730 memory::MemoryError::ResidentSizeUnit,
1731 memory::MemoryError::ResidentSizeScale,
1732 memory::MemoryError::MonitorUnavailable,
1733 ] {
1734 render(RunnerMessage::Memory(error));
1735 }
1736
1737 for &value in I32S {
1738 render(RunnerMessage::Exit(ExitDetail::SandboxSetup(value)));
1739 render(RunnerMessage::Exit(ExitDetail::ProcessCount(value)));
1740 render(RunnerMessage::Exit(ExitDetail::WorkerCode(value)));
1741 render(RunnerMessage::Exit(ExitDetail::WorkerSignal(value)));
1742 }
1743 render(RunnerMessage::Exit(ExitDetail::WorkerNoStatus));
1744
1745 for detail in [
1746 ResponseValidationError::BothBodies,
1747 ResponseValidationError::NoBody,
1748 ] {
1749 render(RunnerMessage::ValidateResponse(detail));
1750 }
1751 for &name in PROBE_NAMES {
1752 render(RunnerMessage::ProbeAttemptMissing(name));
1753 }
1754 for value in [
1755 TemplateValue::Jail,
1756 TemplateValue::Worker,
1757 TemplateValue::Closures,
1758 TemplateValue::Service,
1759 TemplateValue::Grants,
1760 ] {
1761 render(RunnerMessage::TemplateBrace(value));
1762 }
1763 for class in [
1764 PathClass::Jail,
1765 PathClass::Worker,
1766 PathClass::ExecutableGrantDirectory,
1767 PathClass::ExecutableGrant,
1768 PathClass::ReadGrant,
1769 ] {
1770 render(RunnerMessage::PathNotUtf8(class));
1771 render(RunnerMessage::PathCannotBeQuoted(class));
1772 }
1773 }
1774}