1use std::fs::{self, File, Metadata};
4#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5use std::io::Seek;
6use std::io::{self, Read, Write};
7#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
8use std::os::fd::AsRawFd;
9use std::path::{Path, PathBuf};
10use std::process::{Child, ChildStdout, Command, ExitStatus, Stdio};
11use std::sync::OnceLock;
12#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
13use std::sync::atomic::AtomicU64;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TryRecvError, TrySendError};
16#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
17use std::sync::{Arc, Mutex};
18use std::thread::{self, JoinHandle};
19use std::time::{Duration, Instant, SystemTime};
20
21#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
22use crate::parser_linux_authority::{
23 ACCEPTED_FD_ARGUMENT, ARTIFACT_FD_ARGUMENT, GRAMMAR_FD_ARGUMENT, POLICY_FD_ARGUMENT,
24 SERVE_ARGUMENT,
25};
26use projectatlas_core::IndexCancellation;
27use projectatlas_core::optional_parser_pack::{
28 OPTIONAL_PARSER_PACK_LINUX_MEMORY_PROBE_BYTES, OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES,
29 OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES, OptionalParserPackArtifactManifest,
30 OptionalParserPackManifest, OptionalParserPackManifestError, PackPlatform,
31 ParserPackMemoryProbe, ParserPackPayloadRole,
32};
33#[cfg(any(
34 all(target_os = "linux", target_arch = "x86_64"),
35 all(target_os = "windows", target_arch = "x86_64")
36))]
37use projectatlas_core::optional_parser_pack::{ParserPackMemoryControl, ParserPackVerifiedControl};
38#[cfg(windows)]
39use projectatlas_core::optional_parser_protocol::PARSER_WINDOWS_BROKER_MEMORY_LIMIT_EXIT_CODE;
40use projectatlas_core::optional_parser_protocol::{
41 PARSER_FRAME_HEADER_BYTES, PARSER_MAX_NODE_COUNT, PARSER_MAX_OUTPUT_BYTES,
42 PARSER_MAX_SOURCE_BYTES, PARSER_MAX_STDERR_BYTES, PARSER_MAX_TREE_DEPTH,
43 PARSER_SESSION_ENTROPY_BYTES, PARSER_WINDOWS_BROKER_ADMISSION_RECORD, ParserArtifactIdentity,
44 ParserCompletionEvidence, ParserContainmentKind, ParserControl, ParserFailureCode, ParserFrame,
45 ParserFrameHeader, ParserFrameKind, ParserLanguageIdentity, ParserProgress,
46 ParserProgressDisposition, ParserProtocolError, ParserRequest, ParserRequestIdentity,
47 ParserRequestLimits, ParserSessionIdentity, ParserSessionOpen, ParserSourceIdentity,
48 decode_parser_completion_for_request, decode_parser_failure_for_request,
49 decode_parser_progress_for_request, decode_parser_ready_for_launch, encode_parser_control,
50};
51use projectatlas_core::optional_parser_protocol::{
52 PARSER_WORKER_JOB_MEMORY_BYTES, PARSER_WORKER_PROCESS_MEMORY_BYTES,
53};
54use sha2::{Digest, Sha256};
55use thiserror::Error;
56
57const ACCEPTED_MANIFEST_FILE_NAME: &str = "accepted-capabilities.json";
59const ARTIFACT_MANIFEST_FILE_NAME: &str = "artifact-manifest.json";
61#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
63const BROKER_SERVE_ARGUMENT: &str = "serve-worker";
64const SUPERVISOR_POLL_INTERVAL: Duration = Duration::from_millis(20);
66const PARSER_DIAGNOSTIC_FENCE_BYTES: usize = 32;
68const SUPERVISOR_GRACEFUL_CLOSE: Duration = Duration::from_millis(500);
70const SUPERVISOR_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
72const ARTIFACT_ADMISSION_TIMEOUT: Duration = Duration::from_secs(15);
74const ARTIFACT_ADMISSION_AGGREGATE_TIMEOUT: Duration = Duration::from_secs(20 * 60);
76const ARTIFACT_ADMISSION_NO_PROGRESS_TIMEOUT: Duration = Duration::from_secs(5);
78#[cfg(test)]
80const ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS: Duration = Duration::from_secs(2);
81const WINDOWS_MEMORY_PROBE_SOURCE_BYTES: usize = 1024 * 1024;
83pub const PARSER_LINUX_RSS_OBSERVATION_INTERVAL: Duration = Duration::from_millis(20);
85
86#[cfg(test)]
88fn adversarial_launch_no_progress(scenario: &str, operation_no_progress: Duration) -> Duration {
89 match scenario {
90 "pre-ready-stall" | "admission-stall" => operation_no_progress,
91 _ => ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS,
92 }
93}
94
95#[cfg(test)]
97fn adversarial_deadline_matches(
98 error: &ParserSupervisorError,
99 expected_phase: &'static str,
100) -> bool {
101 matches!(
102 error,
103 ParserSupervisorError::DeadlineExceeded { phase } if *phase == expected_phase
104 )
105}
106
107#[derive(Clone, Copy)]
109struct ParserMemoryLimits {
110 process_bytes: u64,
112 process_tree_bytes: u64,
114}
115
116impl ParserMemoryLimits {
117 const PRODUCTION: Self = Self {
119 process_bytes: PARSER_WORKER_PROCESS_MEMORY_BYTES,
120 process_tree_bytes: PARSER_WORKER_JOB_MEMORY_BYTES,
121 };
122
123 fn checked(self) -> Result<Self, ParserSupervisorError> {
125 if self.process_bytes == 0
126 || self.process_bytes > PARSER_WORKER_PROCESS_MEMORY_BYTES
127 || self.process_tree_bytes < self.process_bytes
128 || self.process_tree_bytes > PARSER_WORKER_JOB_MEMORY_BYTES
129 {
130 return Err(ParserSupervisorError::InvalidMemoryLimits {
131 process_bytes: self.process_bytes,
132 process_tree_bytes: self.process_tree_bytes,
133 });
134 }
135 Ok(self)
136 }
137}
138const ARTIFACT_READ_CHUNK_BYTES: usize = 64 * 1024;
140const ARTIFACT_IO_PHASE: &str = "artifact authority";
142const PROCESS_LAUNCH_PHASE: &str = "process launch";
144static ARTIFACT_IO_ACTIVE: AtomicBool = AtomicBool::new(false);
147static PROCESS_SPAWN_ACTIVE: AtomicBool = AtomicBool::new(false);
149static PROCESS_SPAWN_CLEANUP_FAILURE: std::sync::Mutex<Option<String>> =
151 std::sync::Mutex::new(None);
152#[cfg(all(debug_assertions, target_os = "linux", target_arch = "x86_64"))]
154static LINUX_LAUNCH_TEST_HOOK: Mutex<Option<Box<dyn FnOnce() + Send>>> = Mutex::new(None);
155#[cfg(debug_assertions)]
157static CURRENTNESS_TEST_HOOK: std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>> =
158 std::sync::Mutex::new(None);
159#[cfg(debug_assertions)]
161static PRE_SPAWN_TEST_HOOK: std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>> =
162 std::sync::Mutex::new(None);
163#[cfg(test)]
165static PROCESS_SPAWN_AFTER_RENDEZVOUS_TEST_HOOK: std::sync::Mutex<
166 Option<Box<dyn FnOnce() + Send>>,
167> = std::sync::Mutex::new(None);
168#[cfg(test)]
170static PROCESS_SPAWN_AFTER_FINAL_CHECK_TEST_HOOK: std::sync::Mutex<
171 Option<Box<dyn FnOnce() + Send>>,
172> = std::sync::Mutex::new(None);
173#[cfg(test)]
175static PROCESS_SPAWN_BEFORE_CLEANUP_TEST_HOOK: std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>> =
176 std::sync::Mutex::new(None);
177#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
179const LINUX_MEMORY_RECORD_MAX_BYTES: u64 = 64 * 1024;
180#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
182const CGROUP_V2_ROOT: &str = "/sys/fs/cgroup";
183#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
185const MAX_CGROUP_ANCESTORS: usize = 32;
186#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
188static CGROUP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
189
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
192pub enum ParserMemoryAccountingKind {
193 LinuxCgroupV2,
195 LinuxProcStatus,
197}
198
199#[derive(Debug, Error)]
201pub enum ParserSupervisorError {
202 #[error("optional parser containment is unsupported on {os}/{architecture}")]
204 UnsupportedContainment {
205 os: &'static str,
207 architecture: &'static str,
209 },
210 #[error("could not inspect optional parser pack path {path:?}")]
212 PackPath {
213 path: PathBuf,
215 #[source]
217 source: io::Error,
218 },
219 #[error("optional parser pack path {path:?} is invalid: {reason}")]
221 InvalidPackPath {
222 path: PathBuf,
224 reason: &'static str,
226 },
227 #[error("could not read optional parser artifact file {path:?}")]
229 ArtifactRead {
230 path: PathBuf,
232 #[source]
234 source: io::Error,
235 },
236 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
238 #[error("could not construct sealed Linux parser launch authority for {role}")]
239 LinuxLaunchAuthority {
240 role: &'static str,
242 #[source]
244 source: io::Error,
245 },
246 #[error("optional parser artifact file {path:?} has {actual} bytes; maximum is {maximum}")]
248 ArtifactFileTooLarge {
249 path: PathBuf,
251 actual: u64,
253 maximum: u64,
255 },
256 #[error("optional parser artifact manifest is invalid")]
258 ArtifactManifestJson {
259 #[source]
261 source: serde_json::Error,
262 },
263 #[error("optional parser pack manifest validation failed")]
265 ManifestValidation {
266 #[source]
268 source: OptionalParserPackManifestError,
269 },
270 #[error("optional parser artifact payload {path:?} is invalid: {reason}")]
272 PayloadMismatch {
273 path: PathBuf,
275 reason: &'static str,
277 },
278 #[error("optional parser grammar {language_id:?} is not accepted by the verified artifact")]
280 GrammarNotAccepted {
281 language_id: String,
283 },
284 #[error(
286 "optional parser grammar {language_id:?} fixture {case_name:?} error state was {actual}; expected {expected}"
287 )]
288 FixtureExpectationMismatch {
289 language_id: String,
291 case_name: String,
293 actual: bool,
295 expected: bool,
297 },
298 #[error(
300 "optional parser memory limits are invalid: process {process_bytes} bytes; process tree {process_tree_bytes} bytes"
301 )]
302 InvalidMemoryLimits {
303 process_bytes: u64,
305 process_tree_bytes: u64,
307 },
308 #[error(
310 "optional parser memory-boundary probe did not breach its {process_bytes}-byte worker ceiling"
311 )]
312 MemoryProbeDidNotBreach {
313 process_bytes: u64,
315 },
316 #[error("optional parser protocol validation failed")]
318 Protocol {
319 #[source]
321 source: ParserProtocolError,
322 },
323 #[error("operating-system entropy was unavailable for the optional parser session")]
325 EntropyUnavailable,
326 #[error("could not launch optional parser program {program:?}")]
328 Spawn {
329 program: PathBuf,
331 #[source]
333 source: io::Error,
334 },
335 #[error("optional parser child did not expose its {stream} protocol pipe")]
337 MissingPipe {
338 stream: &'static str,
340 },
341 #[error("optional parser Windows containment admission did not validate")]
343 InvalidAdmission,
344 #[error("optional parser {phase} I/O failed: {message}")]
346 IoThread {
347 phase: &'static str,
349 message: String,
351 },
352 #[error("optional parser operation was cancelled during {phase}")]
354 Cancelled {
355 phase: &'static str,
357 },
358 #[error("optional parser absolute deadline elapsed during {phase}")]
360 DeadlineExceeded {
361 phase: &'static str,
363 },
364 #[error("optional parser made no progress during {phase}")]
366 NoProgress {
367 phase: &'static str,
369 },
370 #[error("optional parser child exited during {phase} with code {code:?}")]
372 ChildExited {
373 phase: &'static str,
375 code: Option<i32>,
377 },
378 #[error(
380 "optional parser resident-memory ceiling reached during {phase}: observed {observed_bytes} bytes with {accounting:?}; maximum {maximum_bytes} bytes; observation interval {observation_interval_millis} ms"
381 )]
382 ResidentMemoryLimitExceeded {
383 phase: &'static str,
385 accounting: ParserMemoryAccountingKind,
387 observed_bytes: u64,
389 maximum_bytes: u64,
391 observation_interval_millis: u64,
393 },
394 #[error(
396 "optional parser resident-memory observation failed during {phase} with {accounting:?}: {message}"
397 )]
398 ResidentMemoryObservationFailed {
399 phase: &'static str,
401 accounting: ParserMemoryAccountingKind,
403 message: String,
405 },
406 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
408 #[error("optional parser Windows Job memory ceiling was reached during {phase}")]
409 WindowsJobMemoryLimitExceeded {
410 phase: &'static str,
412 },
413 #[error("optional parser worker returned {code:?}")]
415 WorkerFailure {
416 code: ParserFailureCode,
418 },
419 #[error("optional parser request identity space was exhausted")]
421 RequestIdentityExhausted,
422 #[error("optional parser cleanup failed: {message}")]
424 Cleanup {
425 message: String,
427 },
428 #[error("optional parser operation failed: {operation}; cleanup also failed: {cleanup}")]
430 OperationAndCleanup {
431 operation: Box<Self>,
433 cleanup: Box<Self>,
435 },
436}
437
438#[cfg(all(debug_assertions, target_os = "linux", target_arch = "x86_64"))]
440#[doc(hidden)]
441pub fn install_linux_launch_test_hook(
442 hook: impl FnOnce() + Send + 'static,
443) -> Result<(), ParserSupervisorError> {
444 let mut slot =
445 LINUX_LAUNCH_TEST_HOOK
446 .lock()
447 .map_err(|_poisoned| ParserSupervisorError::IoThread {
448 phase: "Linux launch test hook",
449 message: "test hook lock is poisoned".to_owned(),
450 })?;
451 if slot.is_some() {
452 return Err(ParserSupervisorError::IoThread {
453 phase: "Linux launch test hook",
454 message: "another test hook is already installed".to_owned(),
455 });
456 }
457 *slot = Some(Box::new(hook));
458 Ok(())
459}
460
461#[cfg(all(debug_assertions, target_os = "linux", target_arch = "x86_64"))]
463fn invoke_linux_launch_test_hook() -> Result<(), ParserSupervisorError> {
464 let hook = LINUX_LAUNCH_TEST_HOOK
465 .lock()
466 .map_err(|_poisoned| ParserSupervisorError::IoThread {
467 phase: "Linux launch test hook",
468 message: "test hook lock is poisoned".to_owned(),
469 })?
470 .take();
471 if let Some(hook) = hook {
472 hook();
473 }
474 Ok(())
475}
476
477#[cfg(debug_assertions)]
479#[doc(hidden)]
480pub fn install_currentness_test_hook(
481 hook: impl FnOnce() + Send + 'static,
482) -> Result<(), ParserSupervisorError> {
483 let mut slot =
484 CURRENTNESS_TEST_HOOK
485 .lock()
486 .map_err(|_poisoned| ParserSupervisorError::IoThread {
487 phase: ARTIFACT_IO_PHASE,
488 message: "currentness test hook lock is poisoned".to_owned(),
489 })?;
490 if slot.is_some() {
491 return Err(ParserSupervisorError::IoThread {
492 phase: ARTIFACT_IO_PHASE,
493 message: "another currentness test hook is already installed".to_owned(),
494 });
495 }
496 *slot = Some(Box::new(hook));
497 Ok(())
498}
499
500#[cfg(debug_assertions)]
502fn invoke_currentness_test_hook() -> Result<(), ParserSupervisorError> {
503 let hook = CURRENTNESS_TEST_HOOK
504 .lock()
505 .map_err(|_poisoned| ParserSupervisorError::IoThread {
506 phase: ARTIFACT_IO_PHASE,
507 message: "currentness test hook lock is poisoned".to_owned(),
508 })?
509 .take();
510 if let Some(hook) = hook {
511 hook();
512 }
513 Ok(())
514}
515
516#[cfg(debug_assertions)]
518#[doc(hidden)]
519pub fn install_pre_spawn_test_hook(
520 hook: impl FnOnce() + Send + 'static,
521) -> Result<(), ParserSupervisorError> {
522 let mut slot =
523 PRE_SPAWN_TEST_HOOK
524 .lock()
525 .map_err(|_poisoned| ParserSupervisorError::IoThread {
526 phase: PROCESS_LAUNCH_PHASE,
527 message: "pre-spawn test hook lock is poisoned".to_owned(),
528 })?;
529 if slot.is_some() {
530 return Err(ParserSupervisorError::IoThread {
531 phase: PROCESS_LAUNCH_PHASE,
532 message: "another pre-spawn test hook is already installed".to_owned(),
533 });
534 }
535 *slot = Some(Box::new(hook));
536 Ok(())
537}
538
539#[cfg(debug_assertions)]
541fn invoke_pre_spawn_test_hook() -> Result<(), ParserSupervisorError> {
542 let hook = PRE_SPAWN_TEST_HOOK
543 .lock()
544 .map_err(|_poisoned| ParserSupervisorError::IoThread {
545 phase: PROCESS_LAUNCH_PHASE,
546 message: "pre-spawn test hook lock is poisoned".to_owned(),
547 })?
548 .take();
549 if let Some(hook) = hook {
550 hook();
551 }
552 Ok(())
553}
554
555impl ParserSupervisorError {
556 const fn is_caller_stop(&self) -> bool {
558 matches!(
559 self,
560 Self::Cancelled { .. } | Self::DeadlineExceeded { .. } | Self::NoProgress { .. }
561 )
562 }
563
564 #[must_use]
569 pub const fn has_mandatory_cleanup_failure(&self) -> bool {
570 matches!(
571 self,
572 Self::Cleanup { .. } | Self::OperationAndCleanup { .. }
573 )
574 }
575}
576
577impl From<ParserProtocolError> for ParserSupervisorError {
578 fn from(source: ParserProtocolError) -> Self {
579 Self::Protocol { source }
580 }
581}
582
583impl From<OptionalParserPackManifestError> for ParserSupervisorError {
584 fn from(source: OptionalParserPackManifestError) -> Self {
585 Self::ManifestValidation { source }
586 }
587}
588
589#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
591struct FileChangeEpoch {
592 bytes: u64,
594 modified: Option<SystemTime>,
596 #[cfg(unix)]
598 device: u64,
599 #[cfg(unix)]
601 inode: u64,
602 #[cfg(unix)]
604 changed_seconds: i64,
605 #[cfg(unix)]
607 changed_nanoseconds: i64,
608 #[cfg(windows)]
610 attributes: u32,
611 #[cfg(windows)]
613 created: u64,
614}
615
616impl FileChangeEpoch {
617 fn from_metadata(metadata: &Metadata) -> Self {
619 if !metadata.is_file() {
620 return Self::default();
621 }
622
623 #[cfg(unix)]
624 {
625 use std::os::unix::fs::MetadataExt;
626
627 Self {
628 bytes: metadata.len(),
629 modified: metadata.modified().ok(),
630 device: metadata.dev(),
631 inode: metadata.ino(),
632 changed_seconds: metadata.ctime(),
633 changed_nanoseconds: metadata.ctime_nsec(),
634 }
635 }
636 #[cfg(windows)]
637 {
638 use std::os::windows::fs::MetadataExt;
639
640 Self {
641 bytes: metadata.len(),
642 modified: metadata.modified().ok(),
643 attributes: metadata.file_attributes(),
644 created: metadata.creation_time(),
645 }
646 }
647 #[cfg(not(any(unix, windows)))]
648 {
649 Self {
650 bytes: metadata.len(),
651 modified: metadata.modified().ok(),
652 }
653 }
654 }
655}
656
657fn open_observed_file(path: &Path) -> Result<File, ParserSupervisorError> {
659 let mut options = fs::OpenOptions::new();
660 options.read(true);
661 #[cfg(windows)]
662 {
663 use std::os::windows::fs::OpenOptionsExt;
664
665 const FILE_SHARE_READ: u32 = 1;
666 options.share_mode(FILE_SHARE_READ);
667 }
668 options
669 .open(path)
670 .map_err(|source| ParserSupervisorError::ArtifactRead {
671 path: path.to_path_buf(),
672 source,
673 })
674}
675
676#[derive(Debug)]
678struct FileObservation {
679 path: PathBuf,
681 epoch: FileChangeEpoch,
683 #[cfg(windows)]
685 write_guard: Option<File>,
686}
687
688#[derive(Debug)]
690struct FileCurrentnessProbe {
691 path: PathBuf,
693 epoch: FileChangeEpoch,
695 #[cfg(windows)]
697 guarded: bool,
698 #[cfg(test)]
700 blocker: Option<std::sync::Arc<MetadataProbeBlocker>>,
701}
702
703#[cfg(test)]
705#[derive(Debug)]
706struct MetadataProbeBlocker {
707 entered: SyncSender<()>,
709 release: std::sync::Mutex<Receiver<()>>,
711}
712
713#[cfg(test)]
714impl MetadataProbeBlocker {
715 fn wait(&self) -> Result<(), ParserSupervisorError> {
717 self.entered
718 .send(())
719 .map_err(|_closed| ParserSupervisorError::IoThread {
720 phase: ARTIFACT_IO_PHASE,
721 message: "metadata-probe entry receiver closed".to_owned(),
722 })?;
723 self.release
724 .lock()
725 .map_err(|_poisoned| ParserSupervisorError::IoThread {
726 phase: ARTIFACT_IO_PHASE,
727 message: "metadata-probe release lock was poisoned".to_owned(),
728 })?
729 .recv()
730 .map_err(|_closed| ParserSupervisorError::IoThread {
731 phase: ARTIFACT_IO_PHASE,
732 message: "metadata-probe release sender closed".to_owned(),
733 })
734 }
735}
736
737impl FileCurrentnessProbe {
738 fn is_current(&self) -> Result<bool, ParserSupervisorError> {
740 #[cfg(debug_assertions)]
741 invoke_currentness_test_hook()?;
742 #[cfg(test)]
743 if let Some(blocker) = &self.blocker {
744 blocker.wait()?;
745 }
746 #[cfg(windows)]
747 if !self.guarded {
748 return Ok(false);
749 }
750 let metadata =
751 fs::metadata(&self.path).map_err(|source| ParserSupervisorError::ArtifactRead {
752 path: self.path.clone(),
753 source,
754 })?;
755 Ok(metadata.is_file() && FileChangeEpoch::from_metadata(&metadata) == self.epoch)
756 }
757}
758
759impl FileObservation {
760 fn capture(path: PathBuf) -> Result<Self, ParserSupervisorError> {
762 let write_guard = open_observed_file(&path)?;
763 let metadata =
764 write_guard
765 .metadata()
766 .map_err(|source| ParserSupervisorError::ArtifactRead {
767 path: path.clone(),
768 source,
769 })?;
770 if !metadata.is_file() {
771 return Err(ParserSupervisorError::PayloadMismatch {
772 path,
773 reason: "payload is not a regular file",
774 });
775 }
776 Ok(Self {
777 path,
778 epoch: FileChangeEpoch::from_metadata(&metadata),
779 #[cfg(windows)]
780 write_guard: Some(write_guard),
781 })
782 }
783
784 #[cfg(test)]
786 fn is_current(&self) -> Result<bool, ParserSupervisorError> {
787 self.currentness_probe().is_current()
788 }
789
790 fn currentness_probe(&self) -> FileCurrentnessProbe {
792 FileCurrentnessProbe {
793 path: self.path.clone(),
794 epoch: self.epoch,
795 #[cfg(windows)]
796 guarded: self.write_guard.is_some(),
797 #[cfg(test)]
798 blocker: None,
799 }
800 }
801
802 #[cfg(test)]
804 fn unavailable(path: PathBuf) -> Self {
805 Self {
806 path,
807 epoch: FileChangeEpoch::default(),
808 #[cfg(windows)]
809 write_guard: None,
810 }
811 }
812}
813
814#[derive(Debug)]
816struct PayloadObservation {
817 file: FileObservation,
819 role: ParserPackPayloadRole,
821 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
823 bytes: u64,
824 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
826 sha256: String,
827}
828
829impl PayloadObservation {
830 fn contributes_to_launch(&self, language_id: &str) -> bool {
832 let shared_launch_input = matches!(
833 &self.role,
834 ParserPackPayloadRole::Worker
835 | ParserPackPayloadRole::ContainmentBroker
836 | ParserPackPayloadRole::AcceptedManifest
837 );
838 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
839 let shared_launch_input =
840 shared_launch_input || matches!(&self.role, ParserPackPayloadRole::NativeImportPolicy);
841 shared_launch_input
842 || matches!(
843 &self.role,
844 ParserPackPayloadRole::GrammarLibrary {
845 language_id: payload_language
846 } if payload_language == language_id
847 )
848 }
849
850 #[cfg(test)]
852 fn is_current(&self) -> Result<bool, ParserSupervisorError> {
853 self.file.is_current()
854 }
855
856 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
858 fn linux_spec(&self) -> VerifiedLinuxPayloadSpec {
859 VerifiedLinuxPayloadSpec {
860 path: self.file.path.clone(),
861 epoch: self.file.epoch,
862 bytes: self.bytes,
863 sha256: self.sha256.clone(),
864 }
865 }
866}
867
868struct ArtifactIoControl<'a> {
870 absolute_deadline: Instant,
872 last_progress: Instant,
874 no_progress_timeout: Duration,
876 cancellation: &'a IndexCancellation,
878}
879
880impl ArtifactIoControl<'_> {
881 fn poll(&self) -> Result<(), ParserSupervisorError> {
883 poll_stop(
884 ARTIFACT_IO_PHASE,
885 self.absolute_deadline,
886 self.last_progress,
887 self.no_progress_timeout,
888 self.cancellation,
889 )
890 }
891}
892
893struct ArtifactIoLease;
895
896impl ArtifactIoLease {
897 fn acquire() -> Result<Self, ParserSupervisorError> {
899 ARTIFACT_IO_ACTIVE
900 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
901 .map(|_inactive| Self)
902 .map_err(|_active| ParserSupervisorError::IoThread {
903 phase: ARTIFACT_IO_PHASE,
904 message: "another parser-pack artifact reader is still active".to_owned(),
905 })
906 }
907}
908
909impl Drop for ArtifactIoLease {
910 fn drop(&mut self) {
911 ARTIFACT_IO_ACTIVE.store(false, Ordering::Release);
912 }
913}
914
915struct ArtifactCurrentnessRequest {
917 probe: ArtifactCurrentnessProbe,
919 absolute_deadline: Instant,
921 last_progress: Instant,
923 no_progress_timeout: Duration,
925 cancellation: IndexCancellation,
927 response: SyncSender<Result<bool, ParserSupervisorError>>,
929 lease: ArtifactIoLease,
931}
932
933fn artifact_currentness_sender()
935-> Result<&'static SyncSender<ArtifactCurrentnessRequest>, ParserSupervisorError> {
936 static WORKER: OnceLock<Result<SyncSender<ArtifactCurrentnessRequest>, String>> =
937 OnceLock::new();
938
939 match WORKER.get_or_init(|| {
940 let (sender, receiver) = mpsc::sync_channel::<ArtifactCurrentnessRequest>(1);
941 thread::Builder::new()
942 .name("projectatlas-artifact-currentness".to_owned())
943 .spawn(move || {
944 while let Ok(request) = receiver.recv() {
945 let ArtifactCurrentnessRequest {
946 probe,
947 absolute_deadline,
948 last_progress,
949 no_progress_timeout,
950 cancellation,
951 response,
952 lease,
953 } = request;
954 let control = ArtifactIoControl {
955 absolute_deadline,
956 last_progress,
957 no_progress_timeout,
958 cancellation: &cancellation,
959 };
960 let result = probe.is_current(Some(&control));
961 drop(lease);
962 let _send_result = response.try_send(result);
963 }
964 })
965 .map(|worker| {
966 drop(worker);
967 sender
968 })
969 .map_err(|source| bounded_message(source.to_string()))
970 }) {
971 Ok(sender) => Ok(sender),
972 Err(message) => Err(ParserSupervisorError::IoThread {
973 phase: ARTIFACT_IO_PHASE,
974 message: message.clone(),
975 }),
976 }
977}
978
979fn run_bounded_artifact_currentness(
981 probe: ArtifactCurrentnessProbe,
982 control: &ArtifactIoControl<'_>,
983) -> Result<bool, ParserSupervisorError> {
984 control.poll()?;
985 let sender = artifact_currentness_sender()?;
986 let lease = ArtifactIoLease::acquire()?;
987 let (response, receiver) = mpsc::sync_channel(1);
988 let request = ArtifactCurrentnessRequest {
989 probe,
990 absolute_deadline: control.absolute_deadline,
991 last_progress: control.last_progress,
992 no_progress_timeout: control.no_progress_timeout,
993 cancellation: control.cancellation.clone(),
994 response,
995 lease,
996 };
997 match sender.try_send(request) {
998 Ok(()) => {}
999 Err(TrySendError::Full(_request)) => {
1000 return Err(ParserSupervisorError::IoThread {
1001 phase: ARTIFACT_IO_PHASE,
1002 message: "parser-pack currentness worker is still active".to_owned(),
1003 });
1004 }
1005 Err(TrySendError::Disconnected(_request)) => {
1006 return Err(ParserSupervisorError::IoThread {
1007 phase: ARTIFACT_IO_PHASE,
1008 message: "parser-pack currentness worker disconnected".to_owned(),
1009 });
1010 }
1011 }
1012
1013 loop {
1014 control.poll()?;
1015 match receiver.recv_timeout(next_poll_wait(
1016 control.absolute_deadline,
1017 control.last_progress,
1018 control.no_progress_timeout,
1019 )) {
1020 Ok(result) => {
1021 control.poll()?;
1022 return result;
1023 }
1024 Err(RecvTimeoutError::Timeout) => {}
1025 Err(RecvTimeoutError::Disconnected) => {
1026 return Err(ParserSupervisorError::IoThread {
1027 phase: ARTIFACT_IO_PHASE,
1028 message: "parser-pack currentness response disconnected".to_owned(),
1029 });
1030 }
1031 }
1032 }
1033}
1034
1035fn run_bounded_artifact_io<T>(
1037 operation: impl FnOnce() -> Result<T, ParserSupervisorError> + Send + 'static,
1038 control: &ArtifactIoControl<'_>,
1039) -> Result<T, ParserSupervisorError>
1040where
1041 T: Send + 'static,
1042{
1043 control.poll()?;
1044 let lease = ArtifactIoLease::acquire()?;
1045 let (sender, receiver) = mpsc::sync_channel(1);
1046 let worker = thread::Builder::new()
1047 .name("projectatlas-artifact-authority".to_owned())
1048 .spawn(move || {
1049 let result = operation();
1050 drop(lease);
1051 let _send_result = sender.send(result);
1052 })
1053 .map_err(|source| ParserSupervisorError::IoThread {
1054 phase: ARTIFACT_IO_PHASE,
1055 message: bounded_message(source.to_string()),
1056 })?;
1057 drop(worker);
1058
1059 loop {
1060 control.poll()?;
1061 match receiver.recv_timeout(next_poll_wait(
1062 control.absolute_deadline,
1063 control.last_progress,
1064 control.no_progress_timeout,
1065 )) {
1066 Ok(result) => {
1067 control.poll()?;
1068 return result;
1069 }
1070 Err(RecvTimeoutError::Timeout) => {}
1071 Err(RecvTimeoutError::Disconnected) => {
1072 return Err(ParserSupervisorError::IoThread {
1073 phase: ARTIFACT_IO_PHASE,
1074 message: "parser-pack artifact reader disconnected".to_owned(),
1075 });
1076 }
1077 }
1078 }
1079}
1080
1081struct ProcessSpawnLease;
1083
1084impl ProcessSpawnLease {
1085 fn acquire() -> Result<Self, ParserSupervisorError> {
1087 PROCESS_SPAWN_ACTIVE
1088 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1089 .map(|_inactive| Self)
1090 .map_err(|_active| ParserSupervisorError::IoThread {
1091 phase: PROCESS_LAUNCH_PHASE,
1092 message: "another optional-parser process creation is still active".to_owned(),
1093 })
1094 }
1095}
1096
1097impl Drop for ProcessSpawnLease {
1098 fn drop(&mut self) {
1099 PROCESS_SPAWN_ACTIVE.store(false, Ordering::Release);
1100 }
1101}
1102
1103struct UnadmittedChild {
1105 child: Option<Child>,
1107 _lease: ProcessSpawnLease,
1109}
1110
1111impl UnadmittedChild {
1112 const fn new(child: Child, lease: ProcessSpawnLease) -> Self {
1114 Self {
1115 child: Some(child),
1116 _lease: lease,
1117 }
1118 }
1119
1120 fn admit(mut self) -> Result<Child, ParserSupervisorError> {
1122 self.child
1123 .take()
1124 .ok_or_else(|| ParserSupervisorError::IoThread {
1125 phase: PROCESS_LAUNCH_PHASE,
1126 message: "process-spawn worker returned no child".to_owned(),
1127 })
1128 }
1129}
1130
1131impl Drop for UnadmittedChild {
1132 fn drop(&mut self) {
1133 let Some(mut child) = self.child.take() else {
1134 return;
1135 };
1136 #[cfg(test)]
1137 if let Ok(mut slot) = PROCESS_SPAWN_BEFORE_CLEANUP_TEST_HOOK.lock()
1138 && let Some(hook) = slot.take()
1139 {
1140 hook();
1141 }
1142 if let Err(error) = cleanup_partial_launch(&mut child, Vec::new(), None, None, None) {
1143 record_process_spawn_cleanup_failure(&error);
1144 }
1145 }
1146}
1147
1148fn record_process_spawn_cleanup_failure(error: &ParserSupervisorError) {
1150 if let Ok(mut slot) = PROCESS_SPAWN_CLEANUP_FAILURE.lock()
1151 && slot.is_none()
1152 {
1153 *slot = Some(bounded_message(format!(
1154 "late optional-parser process cleanup failed: {error}"
1155 )));
1156 }
1157}
1158
1159fn require_process_spawn_cleanup_health() -> Result<(), ParserSupervisorError> {
1161 let slot = PROCESS_SPAWN_CLEANUP_FAILURE.lock().map_err(|_poisoned| {
1162 ParserSupervisorError::IoThread {
1163 phase: PROCESS_LAUNCH_PHASE,
1164 message: "process-spawn cleanup state is poisoned".to_owned(),
1165 }
1166 })?;
1167 if let Some(message) = slot.as_ref() {
1168 return Err(ParserSupervisorError::Cleanup {
1169 message: message.clone(),
1170 });
1171 }
1172 Ok(())
1173}
1174
1175fn run_bounded_process_spawn(
1177 command: Command,
1178 absolute_deadline: Instant,
1179 last_progress: Instant,
1180 no_progress_timeout: Duration,
1181 cancellation: &IndexCancellation,
1182) -> Result<Child, ParserSupervisorError> {
1183 run_bounded_process_spawn_with(
1184 command,
1185 absolute_deadline,
1186 last_progress,
1187 no_progress_timeout,
1188 cancellation,
1189 |mut command| command.spawn(),
1190 )
1191}
1192
1193fn run_bounded_process_spawn_with(
1195 command: Command,
1196 absolute_deadline: Instant,
1197 last_progress: Instant,
1198 no_progress_timeout: Duration,
1199 cancellation: &IndexCancellation,
1200 spawn: impl FnOnce(Command) -> io::Result<Child> + Send + 'static,
1201) -> Result<Child, ParserSupervisorError> {
1202 poll_stop(
1203 PROCESS_LAUNCH_PHASE,
1204 absolute_deadline,
1205 last_progress,
1206 no_progress_timeout,
1207 cancellation,
1208 )?;
1209 require_process_spawn_cleanup_health()?;
1210 let lease = ProcessSpawnLease::acquire()?;
1211 let program = PathBuf::from(command.get_program());
1212 let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
1213 let (rendezvous_sender, rendezvous_receiver) = mpsc::sync_channel(0);
1214 let (handoff_commit_sender, handoff_commit_receiver) = mpsc::sync_channel(1);
1215 let (child_sender, child_receiver) = mpsc::sync_channel(0);
1216 let worker = thread::Builder::new()
1217 .name("projectatlas-process-spawn".to_owned())
1218 .spawn(move || {
1219 let child = match spawn(command) {
1220 Ok(child) => UnadmittedChild::new(child, lease),
1221 Err(source) => {
1222 let _undelivered =
1223 ready_sender.send(Err(ParserSupervisorError::Spawn { program, source }));
1224 return;
1225 }
1226 };
1227 if ready_sender.send(Ok(())).is_err() {
1228 return;
1229 }
1230 if rendezvous_sender.send(()).is_err() {
1231 return;
1232 }
1233 if handoff_commit_receiver.recv().is_err() {
1234 return;
1235 }
1236 if let Err(undelivered) = child_sender.send(child) {
1237 drop(undelivered);
1238 }
1239 })
1240 .map_err(|source| ParserSupervisorError::IoThread {
1241 phase: PROCESS_LAUNCH_PHASE,
1242 message: bounded_message(source.to_string()),
1243 })?;
1244 drop(worker);
1245
1246 loop {
1247 poll_stop(
1248 PROCESS_LAUNCH_PHASE,
1249 absolute_deadline,
1250 last_progress,
1251 no_progress_timeout,
1252 cancellation,
1253 )?;
1254 match ready_receiver.recv_timeout(next_poll_wait(
1255 absolute_deadline,
1256 last_progress,
1257 no_progress_timeout,
1258 )) {
1259 Ok(ready) => {
1260 ready?;
1261 poll_stop(
1262 PROCESS_LAUNCH_PHASE,
1263 absolute_deadline,
1264 last_progress,
1265 no_progress_timeout,
1266 cancellation,
1267 )?;
1268 loop {
1269 poll_stop(
1270 PROCESS_LAUNCH_PHASE,
1271 absolute_deadline,
1272 last_progress,
1273 no_progress_timeout,
1274 cancellation,
1275 )?;
1276 match rendezvous_receiver.recv_timeout(next_poll_wait(
1277 absolute_deadline,
1278 last_progress,
1279 no_progress_timeout,
1280 )) {
1281 Ok(()) => {
1282 #[cfg(test)]
1283 if let Ok(mut slot) = PROCESS_SPAWN_AFTER_RENDEZVOUS_TEST_HOOK.lock()
1284 && let Some(hook) = slot.take()
1285 {
1286 hook();
1287 }
1288 poll_stop(
1289 PROCESS_LAUNCH_PHASE,
1290 absolute_deadline,
1291 last_progress,
1292 no_progress_timeout,
1293 cancellation,
1294 )?;
1295 #[cfg(test)]
1299 if let Ok(mut slot) = PROCESS_SPAWN_AFTER_FINAL_CHECK_TEST_HOOK.lock()
1300 && let Some(hook) = slot.take()
1301 {
1302 hook();
1303 }
1304 handoff_commit_sender.send(()).map_err(|_closed| {
1305 ParserSupervisorError::IoThread {
1306 phase: PROCESS_LAUNCH_PHASE,
1307 message:
1308 "process-spawn owner disconnected before handoff commit"
1309 .to_owned(),
1310 }
1311 })?;
1312 return child_receiver
1313 .recv()
1314 .map_err(|_closed| ParserSupervisorError::IoThread {
1315 phase: PROCESS_LAUNCH_PHASE,
1316 message:
1317 "process-spawn owner disconnected during committed handoff"
1318 .to_owned(),
1319 })?
1320 .admit();
1321 }
1322 Err(RecvTimeoutError::Timeout) => {}
1323 Err(RecvTimeoutError::Disconnected) => {
1324 return Err(ParserSupervisorError::IoThread {
1325 phase: PROCESS_LAUNCH_PHASE,
1326 message: "process-spawn owner disconnected during rendezvous"
1327 .to_owned(),
1328 });
1329 }
1330 }
1331 }
1332 }
1333 Err(RecvTimeoutError::Timeout) => {}
1334 Err(RecvTimeoutError::Disconnected) => {
1335 return Err(ParserSupervisorError::IoThread {
1336 phase: PROCESS_LAUNCH_PHASE,
1337 message: "process-spawn worker disconnected".to_owned(),
1338 });
1339 }
1340 }
1341 }
1342}
1343
1344struct BoundedArtifactRead {
1346 bytes: Vec<u8>,
1348 sha256: String,
1350}
1351
1352#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1354#[derive(Clone, Debug)]
1355struct VerifiedLinuxPayloadSpec {
1356 path: PathBuf,
1358 epoch: FileChangeEpoch,
1360 bytes: u64,
1362 sha256: String,
1364}
1365
1366#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1368#[derive(Debug)]
1369struct SealedLinuxPayload {
1370 file: File,
1372}
1373
1374#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1376fn create_memfd_with_legacy_fallback<T>(
1377 flags: nix::sys::memfd::MFdFlags,
1378 mode_flag: nix::libc::c_uint,
1379 mut create: impl FnMut(nix::sys::memfd::MFdFlags) -> Result<T, nix::errno::Errno>,
1380) -> Result<T, nix::errno::Errno> {
1381 let requested_flags = flags | nix::sys::memfd::MFdFlags::from_bits_retain(mode_flag);
1382 match create(requested_flags) {
1383 Err(nix::errno::Errno::EINVAL) => create(flags),
1384 result => result,
1385 }
1386}
1387
1388#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1389impl SealedLinuxPayload {
1390 fn from_verified_bytes(
1392 role: &'static str,
1393 name: &str,
1394 bytes: &[u8],
1395 executable: bool,
1396 control: &ArtifactIoControl<'_>,
1397 ) -> Result<Self, ParserSupervisorError> {
1398 use nix::sys::memfd::memfd_create;
1399
1400 Self::from_verified_bytes_with_create(role, name, bytes, executable, control, |flags| {
1401 memfd_create(name, flags)
1402 })
1403 }
1404
1405 fn from_verified_bytes_with_create(
1407 role: &'static str,
1408 name: &str,
1409 bytes: &[u8],
1410 executable: bool,
1411 control: &ArtifactIoControl<'_>,
1412 create: impl FnMut(nix::sys::memfd::MFdFlags) -> Result<std::os::fd::OwnedFd, nix::errno::Errno>,
1413 ) -> Result<Self, ParserSupervisorError> {
1414 use nix::fcntl::{FcntlArg, SealFlag, fcntl};
1415 use nix::libc;
1416 use nix::sys::memfd::MFdFlags;
1417 use nix::sys::stat::{Mode, fchmod};
1418
1419 let authority_error =
1420 |source: nix::errno::Errno| ParserSupervisorError::LinuxLaunchAuthority {
1421 role,
1422 source: io::Error::from_raw_os_error(source as i32),
1423 };
1424 let flags = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING;
1425 let mode_flag = if executable {
1426 libc::MFD_EXEC
1427 } else {
1428 libc::MFD_NOEXEC_SEAL
1429 };
1430 let descriptor =
1431 create_memfd_with_legacy_fallback(flags, mode_flag, create).map_err(authority_error)?;
1432 let mut file = File::from(descriptor);
1433 let mode = if executable {
1434 Mode::S_IRUSR | Mode::S_IXUSR
1435 } else {
1436 Mode::S_IRUSR
1437 };
1438 fchmod(&file, mode).map_err(authority_error)?;
1439 for chunk in bytes.chunks(ARTIFACT_READ_CHUNK_BYTES) {
1440 control.poll()?;
1441 file.write_all(chunk)
1442 .map_err(|source| ParserSupervisorError::LinuxLaunchAuthority { role, source })?;
1443 }
1444 file.rewind()
1445 .map_err(|source| ParserSupervisorError::LinuxLaunchAuthority { role, source })?;
1446 let required = SealFlag::F_SEAL_WRITE
1447 | SealFlag::F_SEAL_GROW
1448 | SealFlag::F_SEAL_SHRINK
1449 | SealFlag::F_SEAL_SEAL;
1450 fcntl(&file, FcntlArg::F_ADD_SEALS(required)).map_err(authority_error)?;
1451 let observed = fcntl(&file, FcntlArg::F_GET_SEALS).map_err(authority_error)?;
1452 if observed & required.bits() != required.bits() {
1453 return Err(ParserSupervisorError::PayloadMismatch {
1454 path: PathBuf::from(name),
1455 reason: "Linux launch authority does not carry the complete seal set",
1456 });
1457 }
1458
1459 let read_only_path = PathBuf::from(format!("/proc/self/fd/{}", file.as_raw_fd()));
1460 let read_only = File::open(&read_only_path)
1461 .map_err(|source| ParserSupervisorError::LinuxLaunchAuthority { role, source })?;
1462 drop(file);
1463 Ok(Self { file: read_only })
1464 }
1465
1466 fn raw_fd(&self) -> i32 {
1468 self.file.as_raw_fd()
1469 }
1470}
1471
1472#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1474#[derive(Debug)]
1475struct LinuxResidentLaunchAuthority {
1476 worker: SealedLinuxPayload,
1478 artifact_manifest: SealedLinuxPayload,
1480 accepted_manifest: SealedLinuxPayload,
1482 native_import_policy: SealedLinuxPayload,
1484 grammar: SealedLinuxPayload,
1486}
1487
1488#[derive(Debug)]
1490struct VerifiedParserPackLaunch {
1491 pack_root: PathBuf,
1493 platform: PackPlatform,
1495 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1497 containment_broker: Option<PathBuf>,
1498 accepted_grammars: Vec<String>,
1500 artifact: ParserArtifactIdentity,
1502 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1504 artifact_manifest_bytes: Vec<u8>,
1505 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1507 accepted_manifest_bytes: Vec<u8>,
1508 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1510 native_import_policy_bytes: Vec<u8>,
1511 artifact_manifest: FileObservation,
1513 payloads: Vec<PayloadObservation>,
1515 #[cfg(test)]
1517 currentness_blocker: Option<std::sync::Arc<MetadataProbeBlocker>>,
1518}
1519
1520struct ArtifactCurrentnessProbe {
1522 files: Vec<FileCurrentnessProbe>,
1524}
1525
1526const MAX_CURRENTNESS_PROBE_FILES: usize = 5;
1530
1531impl ArtifactCurrentnessProbe {
1532 fn is_current(
1534 &self,
1535 control: Option<&ArtifactIoControl<'_>>,
1536 ) -> Result<bool, ParserSupervisorError> {
1537 for file in &self.files {
1538 if let Some(control) = control {
1539 control.poll()?;
1540 }
1541 if !file.is_current()? {
1542 return Ok(false);
1543 }
1544 }
1545 Ok(true)
1546 }
1547}
1548
1549impl VerifiedParserPackLaunch {
1550 fn load(pack_root: &Path) -> Result<Self, ParserSupervisorError> {
1552 Self::load_inner(pack_root, None)
1553 }
1554
1555 fn load_controlled(
1557 pack_root: &Path,
1558 language_id: &str,
1559 last_progress: Instant,
1560 absolute_deadline: Instant,
1561 no_progress_timeout: Duration,
1562 cancellation: &IndexCancellation,
1563 ) -> Result<Self, ParserSupervisorError> {
1564 let control = ArtifactIoControl {
1565 absolute_deadline,
1566 last_progress,
1567 no_progress_timeout,
1568 cancellation,
1569 };
1570 let pack_root = pack_root.to_path_buf();
1571 let language_id = language_id.to_owned();
1572 let worker_cancellation = cancellation.clone();
1573 run_bounded_artifact_io(
1574 move || {
1575 let worker_control = ArtifactIoControl {
1576 absolute_deadline,
1577 last_progress,
1578 no_progress_timeout,
1579 cancellation: &worker_cancellation,
1580 };
1581 let refreshed = Self::load_inner(&pack_root, Some(&worker_control))?;
1582 if !refreshed
1583 .currentness_probe(&language_id)
1584 .is_current(Some(&worker_control))?
1585 {
1586 return Err(ParserSupervisorError::PayloadMismatch {
1587 path: pack_root,
1588 reason: "artifact changed during digest revalidation",
1589 });
1590 }
1591 Ok(refreshed)
1592 },
1593 &control,
1594 )
1595 }
1596
1597 fn load_inner(
1599 pack_root: &Path,
1600 control: Option<&ArtifactIoControl<'_>>,
1601 ) -> Result<Self, ParserSupervisorError> {
1602 if let Some(control) = control {
1603 control.poll()?;
1604 }
1605 let platform =
1606 host_pack_platform().ok_or(ParserSupervisorError::UnsupportedContainment {
1607 os: std::env::consts::OS,
1608 architecture: std::env::consts::ARCH,
1609 })?;
1610 let pack_root = canonical_directory(pack_root)?;
1611 let accepted_path = canonical_direct_file(&pack_root, ACCEPTED_MANIFEST_FILE_NAME)?;
1612 let artifact_path = canonical_direct_file(&pack_root, ARTIFACT_MANIFEST_FILE_NAME)?;
1613 let accepted_manifest_file = FileObservation::capture(accepted_path.clone())?;
1614 let artifact_manifest_file = FileObservation::capture(artifact_path.clone())?;
1615 let accepted_read = read_bounded_file(
1616 &accepted_path,
1617 accepted_manifest_file.epoch,
1618 u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES).unwrap_or(u64::MAX),
1619 control,
1620 )?;
1621 let artifact_read = read_bounded_file(
1622 &artifact_path,
1623 artifact_manifest_file.epoch,
1624 u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES).unwrap_or(u64::MAX),
1625 control,
1626 )?;
1627 let mut accepted_manifest = Some(accepted_manifest_file);
1628 let logical = OptionalParserPackManifest::from_json(&accepted_read.bytes)?;
1629 let artifact_manifest: OptionalParserPackArtifactManifest =
1630 serde_json::from_slice(&artifact_read.bytes)
1631 .map_err(|source| ParserSupervisorError::ArtifactManifestJson { source })?;
1632 artifact_manifest.validate(&logical)?;
1633 if artifact_manifest.platform != platform {
1634 return Err(ParserSupervisorError::PayloadMismatch {
1635 path: artifact_path,
1636 reason: "artifact target does not match the current host",
1637 });
1638 }
1639
1640 let mut worker = None;
1641 let mut containment_broker = None;
1642 let mut accepted_payload_sha256 = None;
1643 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1644 let mut native_import_policy_bytes = None;
1645 let mut payloads = Vec::with_capacity(artifact_manifest.files.len());
1646 for payload in &artifact_manifest.files {
1647 let path = canonical_payload_file(&pack_root, payload.path.as_str())?;
1648 let file = if matches!(payload.role, ParserPackPayloadRole::AcceptedManifest) {
1649 if path != accepted_path {
1650 return Err(ParserSupervisorError::PayloadMismatch {
1651 path,
1652 reason: "accepted capability manifest is not at its defined artifact path",
1653 });
1654 }
1655 accepted_manifest
1656 .take()
1657 .ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1658 path: path.clone(),
1659 reason: "artifact contains more than one accepted capability manifest",
1660 })?
1661 } else {
1662 FileObservation::capture(path.clone())?
1663 };
1664 let payload_read = read_bounded_file(&path, file.epoch, payload.bytes, control)?;
1665 if u64::try_from(payload_read.bytes.len()).ok() != Some(payload.bytes) {
1666 return Err(ParserSupervisorError::PayloadMismatch {
1667 path,
1668 reason: "payload byte count differs from the artifact manifest",
1669 });
1670 }
1671 if payload_read.sha256 != payload.sha256.as_str() {
1672 return Err(ParserSupervisorError::PayloadMismatch {
1673 path,
1674 reason: "payload SHA-256 differs from the artifact manifest",
1675 });
1676 }
1677 payloads.push(PayloadObservation {
1678 file,
1679 role: payload.role.clone(),
1680 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1681 bytes: payload.bytes,
1682 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1683 sha256: payload.sha256.as_str().to_owned(),
1684 });
1685 match &payload.role {
1686 ParserPackPayloadRole::Worker => worker = Some(path),
1687 ParserPackPayloadRole::ContainmentBroker => containment_broker = Some(path),
1688 ParserPackPayloadRole::AcceptedManifest => {
1689 accepted_payload_sha256 = Some(payload.sha256.as_str().to_owned());
1690 }
1691 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1692 ParserPackPayloadRole::NativeImportPolicy => {
1693 native_import_policy_bytes = Some(payload_read.bytes.clone());
1694 }
1695 ParserPackPayloadRole::FixtureCorpus
1696 | ParserPackPayloadRole::ProjectLicense
1697 | ParserPackPayloadRole::NativeAuditReport
1698 | ParserPackPayloadRole::GrammarLibrary { .. } => {}
1699 #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
1700 ParserPackPayloadRole::NativeImportPolicy => {}
1701 }
1702 }
1703
1704 let worker = worker.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1705 path: pack_root.join(platform.worker_file_name()),
1706 reason: "artifact does not contain its exact worker payload",
1707 })?;
1708 #[cfg(unix)]
1709 require_executable(&worker)?;
1710 let expected_worker = canonical_direct_file(&pack_root, platform.worker_file_name())?;
1711 if worker != expected_worker {
1712 return Err(ParserSupervisorError::PayloadMismatch {
1713 path: worker,
1714 reason: "worker is not at its platform-defined artifact path",
1715 });
1716 }
1717 let containment_broker = match platform.containment_broker_file_name() {
1718 Some(file_name) => {
1719 let broker =
1720 containment_broker.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1721 path: pack_root.join(file_name),
1722 reason: "artifact does not contain its required containment broker",
1723 })?;
1724 #[cfg(unix)]
1725 require_executable(&broker)?;
1726 let expected = canonical_direct_file(&pack_root, file_name)?;
1727 if broker != expected {
1728 return Err(ParserSupervisorError::PayloadMismatch {
1729 path: broker,
1730 reason: "containment broker is not at its platform-defined artifact path",
1731 });
1732 }
1733 Some(expected)
1734 }
1735 None if containment_broker.is_none() => None,
1736 None => {
1737 return Err(ParserSupervisorError::PayloadMismatch {
1738 path: pack_root,
1739 reason: "artifact contains an unsupported containment broker",
1740 });
1741 }
1742 };
1743 let accepted_manifest_sha256 =
1744 accepted_payload_sha256.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1745 path: accepted_path.clone(),
1746 reason: "artifact does not contain its accepted capability manifest",
1747 })?;
1748 if accepted_read.sha256 != accepted_manifest_sha256 {
1749 return Err(ParserSupervisorError::PayloadMismatch {
1750 path: accepted_path,
1751 reason: "accepted capability manifest does not match its artifact payload row",
1752 });
1753 }
1754 #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
1755 let _ = containment_broker;
1756 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1757 let native_import_policy_bytes =
1758 native_import_policy_bytes.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1759 path: pack_root.clone(),
1760 reason: "Linux artifact does not contain its native-import policy",
1761 })?;
1762
1763 Ok(Self {
1764 pack_root,
1765 platform,
1766 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1767 containment_broker,
1768 accepted_grammars: logical
1769 .grammars()
1770 .iter()
1771 .map(|grammar| grammar.language_id.clone())
1772 .collect(),
1773 artifact: ParserArtifactIdentity::for_bytes(&artifact_read.bytes),
1774 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1775 artifact_manifest_bytes: artifact_read.bytes,
1776 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1777 accepted_manifest_bytes: accepted_read.bytes,
1778 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1779 native_import_policy_bytes,
1780 artifact_manifest: artifact_manifest_file,
1781 payloads,
1782 #[cfg(test)]
1783 currentness_blocker: None,
1784 })
1785 }
1786
1787 fn currentness_probe(&self, language_id: &str) -> ArtifactCurrentnessProbe {
1789 let mut files = Vec::with_capacity(MAX_CURRENTNESS_PROBE_FILES);
1790 files.push(self.artifact_manifest.currentness_probe());
1791 files.extend(
1792 self.payloads
1793 .iter()
1794 .filter(|payload| payload.contributes_to_launch(language_id))
1795 .map(|payload| payload.file.currentness_probe()),
1796 );
1797 #[cfg(test)]
1798 if let (Some(file), Some(blocker)) = (files.first_mut(), &self.currentness_blocker) {
1799 file.blocker = Some(std::sync::Arc::clone(blocker));
1800 }
1801 ArtifactCurrentnessProbe { files }
1802 }
1803
1804 fn require_grammar(
1806 &self,
1807 language_id: &str,
1808 ) -> Result<ParserLanguageIdentity, ParserSupervisorError> {
1809 let language = ParserLanguageIdentity::new(language_id)?;
1810 if self
1811 .accepted_grammars
1812 .binary_search_by(|candidate| candidate.as_str().cmp(language.as_str()))
1813 .is_err()
1814 {
1815 return Err(ParserSupervisorError::GrammarNotAccepted {
1816 language_id: language_id.to_owned(),
1817 });
1818 }
1819 Ok(language)
1820 }
1821
1822 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1824 fn prepare_resident_launch_controlled(
1825 &self,
1826 language_id: &str,
1827 last_progress: Instant,
1828 absolute_deadline: Instant,
1829 no_progress_timeout: Duration,
1830 cancellation: &IndexCancellation,
1831 ) -> Result<LinuxResidentLaunchAuthority, ParserSupervisorError> {
1832 let mut workers = self
1833 .payloads
1834 .iter()
1835 .filter(|payload| matches!(payload.role, ParserPackPayloadRole::Worker));
1836 let worker = workers.next().map(PayloadObservation::linux_spec);
1837 if worker.is_none() || workers.next().is_some() {
1838 return Err(ParserSupervisorError::PayloadMismatch {
1839 path: self.pack_root.clone(),
1840 reason: "artifact must bind exactly one Linux worker payload",
1841 });
1842 }
1843 let mut grammars = self.payloads.iter().filter(|payload| {
1844 matches!(
1845 &payload.role,
1846 ParserPackPayloadRole::GrammarLibrary {
1847 language_id: payload_language
1848 } if payload_language == language_id
1849 )
1850 });
1851 let grammar = grammars.next().map(PayloadObservation::linux_spec);
1852 if grammar.is_none() || grammars.next().is_some() {
1853 return Err(ParserSupervisorError::PayloadMismatch {
1854 path: self.pack_root.clone(),
1855 reason: "artifact must bind exactly one selected grammar payload",
1856 });
1857 }
1858
1859 let worker = worker.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1860 path: self.pack_root.clone(),
1861 reason: "artifact has no Linux worker payload",
1862 })?;
1863 let grammar = grammar.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1864 path: self.pack_root.clone(),
1865 reason: "artifact has no selected grammar payload",
1866 })?;
1867 let artifact_manifest = self.artifact_manifest_bytes.clone();
1868 let accepted_manifest = self.accepted_manifest_bytes.clone();
1869 let native_import_policy = self.native_import_policy_bytes.clone();
1870 let worker_cancellation = cancellation.clone();
1871 let control = ArtifactIoControl {
1872 absolute_deadline,
1873 last_progress,
1874 no_progress_timeout,
1875 cancellation,
1876 };
1877 run_bounded_artifact_io(
1878 move || {
1879 let worker_control = ArtifactIoControl {
1880 absolute_deadline,
1881 last_progress,
1882 no_progress_timeout,
1883 cancellation: &worker_cancellation,
1884 };
1885 let worker_bytes = read_verified_linux_payload(&worker, &worker_control)?;
1886 let grammar_bytes = read_verified_linux_payload(&grammar, &worker_control)?;
1887 Ok(LinuxResidentLaunchAuthority {
1888 worker: SealedLinuxPayload::from_verified_bytes(
1889 "worker",
1890 "projectatlas-parser-worker",
1891 &worker_bytes,
1892 true,
1893 &worker_control,
1894 )?,
1895 artifact_manifest: SealedLinuxPayload::from_verified_bytes(
1896 "artifact manifest",
1897 "projectatlas-artifact-manifest",
1898 &artifact_manifest,
1899 false,
1900 &worker_control,
1901 )?,
1902 accepted_manifest: SealedLinuxPayload::from_verified_bytes(
1903 "accepted capability manifest",
1904 "projectatlas-accepted-manifest",
1905 &accepted_manifest,
1906 false,
1907 &worker_control,
1908 )?,
1909 native_import_policy: SealedLinuxPayload::from_verified_bytes(
1910 "native-import policy",
1911 "projectatlas-native-policy",
1912 &native_import_policy,
1913 false,
1914 &worker_control,
1915 )?,
1916 grammar: SealedLinuxPayload::from_verified_bytes(
1917 "selected grammar",
1918 "projectatlas-selected-grammar",
1919 &grammar_bytes,
1920 true,
1921 &worker_control,
1922 )?,
1923 })
1924 },
1925 &control,
1926 )
1927 }
1928}
1929
1930#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1932fn read_verified_linux_payload(
1933 spec: &VerifiedLinuxPayloadSpec,
1934 control: &ArtifactIoControl<'_>,
1935) -> Result<Vec<u8>, ParserSupervisorError> {
1936 let read = read_bounded_file(&spec.path, spec.epoch, spec.bytes, Some(control))?;
1937 if u64::try_from(read.bytes.len()).ok() != Some(spec.bytes) {
1938 return Err(ParserSupervisorError::PayloadMismatch {
1939 path: spec.path.clone(),
1940 reason: "launch payload byte count differs from the artifact manifest",
1941 });
1942 }
1943 if read.sha256 != spec.sha256 {
1944 return Err(ParserSupervisorError::PayloadMismatch {
1945 path: spec.path.clone(),
1946 reason: "launch payload SHA-256 differs from the artifact manifest",
1947 });
1948 }
1949 Ok(read.bytes)
1950}
1951
1952fn host_pack_platform() -> Option<PackPlatform> {
1954 match (std::env::consts::OS, std::env::consts::ARCH) {
1955 ("linux", "x86_64") => Some(PackPlatform::LinuxX86_64),
1956 ("windows", "x86_64") => Some(PackPlatform::WindowsX86_64),
1957 _ => None,
1958 }
1959}
1960
1961fn canonical_directory(path: &Path) -> Result<PathBuf, ParserSupervisorError> {
1963 let canonical = fs::canonicalize(path).map_err(|source| ParserSupervisorError::PackPath {
1964 path: path.to_path_buf(),
1965 source,
1966 })?;
1967 let metadata = file_metadata(&canonical)?;
1968 if !canonical.is_absolute() || !metadata.is_dir() {
1969 return Err(ParserSupervisorError::InvalidPackPath {
1970 path: canonical,
1971 reason: "expected an absolute regular directory",
1972 });
1973 }
1974 Ok(canonical)
1975}
1976
1977fn canonical_direct_file(
1979 pack_root: &Path,
1980 file_name: &str,
1981) -> Result<PathBuf, ParserSupervisorError> {
1982 let path = canonical_payload_file(pack_root, file_name)?;
1983 if path.parent() != Some(pack_root) {
1984 return Err(ParserSupervisorError::InvalidPackPath {
1985 path,
1986 reason: "expected a direct artifact-root file",
1987 });
1988 }
1989 Ok(path)
1990}
1991
1992fn canonical_payload_file(
1994 pack_root: &Path,
1995 relative: &str,
1996) -> Result<PathBuf, ParserSupervisorError> {
1997 let relative_path = Path::new(relative);
1998 if relative_path.is_absolute()
1999 || relative_path
2000 .components()
2001 .any(|component| !matches!(component, std::path::Component::Normal(_)))
2002 {
2003 return Err(ParserSupervisorError::InvalidPackPath {
2004 path: relative_path.to_path_buf(),
2005 reason: "expected a normalized artifact-relative path",
2006 });
2007 }
2008 let requested = pack_root.join(relative_path);
2009 let mut component_path = pack_root.to_path_buf();
2010 for component in relative_path.components() {
2011 let std::path::Component::Normal(component) = component else {
2012 return Err(ParserSupervisorError::InvalidPackPath {
2013 path: requested,
2014 reason: "expected only normal relative components",
2015 });
2016 };
2017 component_path.push(component);
2018 let metadata = fs::symlink_metadata(&component_path).map_err(|source| {
2019 ParserSupervisorError::PackPath {
2020 path: component_path.clone(),
2021 source,
2022 }
2023 })?;
2024 if is_link_or_reparse_point(&metadata) {
2025 return Err(ParserSupervisorError::InvalidPackPath {
2026 path: component_path,
2027 reason: "symbolic links and reparse points are not accepted in immutable packs",
2028 });
2029 }
2030 }
2031 let canonical =
2032 fs::canonicalize(&requested).map_err(|source| ParserSupervisorError::PackPath {
2033 path: requested.clone(),
2034 source,
2035 })?;
2036 let metadata = file_metadata(&canonical)?;
2037 if !canonical.starts_with(pack_root) || !metadata.is_file() {
2038 return Err(ParserSupervisorError::InvalidPackPath {
2039 path: canonical,
2040 reason: "payload must be a regular file inside the canonical pack root",
2041 });
2042 }
2043 Ok(canonical)
2044}
2045
2046#[cfg(windows)]
2048fn is_link_or_reparse_point(metadata: &Metadata) -> bool {
2049 use std::os::windows::fs::MetadataExt;
2050
2051 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
2052 metadata.file_type().is_symlink()
2053 || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
2054}
2055
2056#[cfg(not(windows))]
2058fn is_link_or_reparse_point(metadata: &Metadata) -> bool {
2059 metadata.file_type().is_symlink()
2060}
2061
2062fn file_metadata(path: &Path) -> Result<Metadata, ParserSupervisorError> {
2064 fs::metadata(path).map_err(|source| ParserSupervisorError::PackPath {
2065 path: path.to_path_buf(),
2066 source,
2067 })
2068}
2069
2070fn read_bounded_file(
2072 path: &Path,
2073 expected_epoch: FileChangeEpoch,
2074 maximum: u64,
2075 control: Option<&ArtifactIoControl<'_>>,
2076) -> Result<BoundedArtifactRead, ParserSupervisorError> {
2077 let mut file = File::open(path).map_err(|source| ParserSupervisorError::ArtifactRead {
2078 path: path.to_path_buf(),
2079 source,
2080 })?;
2081 let metadata = file
2082 .metadata()
2083 .map_err(|source| ParserSupervisorError::ArtifactRead {
2084 path: path.to_path_buf(),
2085 source,
2086 })?;
2087 if !metadata.is_file() {
2088 return Err(ParserSupervisorError::InvalidPackPath {
2089 path: path.to_path_buf(),
2090 reason: "expected a regular artifact file",
2091 });
2092 }
2093 if FileChangeEpoch::from_metadata(&metadata) != expected_epoch {
2094 return Err(ParserSupervisorError::PayloadMismatch {
2095 path: path.to_path_buf(),
2096 reason: "artifact read handle does not match the captured file identity",
2097 });
2098 }
2099 if metadata.len() > maximum {
2100 return Err(ParserSupervisorError::ArtifactFileTooLarge {
2101 path: path.to_path_buf(),
2102 actual: metadata.len(),
2103 maximum,
2104 });
2105 }
2106 let capacity = usize::try_from(metadata.len()).unwrap_or(ARTIFACT_READ_CHUNK_BYTES);
2107 let mut bytes = Vec::with_capacity(capacity);
2108 let mut sha256 = Sha256::new();
2109 read_bounded_chunks(&mut file, path, maximum, &mut bytes, &mut sha256, control)?;
2110 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > maximum {
2111 return Err(ParserSupervisorError::ArtifactFileTooLarge {
2112 path: path.to_path_buf(),
2113 actual: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
2114 maximum,
2115 });
2116 }
2117 Ok(BoundedArtifactRead {
2118 bytes,
2119 sha256: encode_sha256(sha256.finalize()),
2120 })
2121}
2122
2123fn read_bounded_chunks(
2125 reader: &mut impl Read,
2126 path: &Path,
2127 maximum: u64,
2128 bytes: &mut Vec<u8>,
2129 sha256: &mut Sha256,
2130 control: Option<&ArtifactIoControl<'_>>,
2131) -> Result<(), ParserSupervisorError> {
2132 let mut chunk = vec![0_u8; ARTIFACT_READ_CHUNK_BYTES].into_boxed_slice();
2133 loop {
2134 if let Some(control) = control {
2135 control.poll()?;
2136 }
2137 let remaining = maximum
2138 .saturating_add(1)
2139 .saturating_sub(u64::try_from(bytes.len()).unwrap_or(u64::MAX));
2140 if remaining == 0 {
2141 break;
2142 }
2143 let limit = usize::try_from(remaining)
2144 .unwrap_or(ARTIFACT_READ_CHUNK_BYTES)
2145 .min(ARTIFACT_READ_CHUNK_BYTES);
2146 let read = match reader.read(&mut chunk[..limit]) {
2147 Ok(read) => read,
2148 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
2149 Err(source) => {
2150 return Err(ParserSupervisorError::ArtifactRead {
2151 path: path.to_path_buf(),
2152 source,
2153 });
2154 }
2155 };
2156 if read == 0 {
2157 break;
2158 }
2159 sha256.update(&chunk[..read]);
2160 bytes.extend_from_slice(&chunk[..read]);
2161 }
2162 Ok(())
2163}
2164
2165fn encode_sha256(digest: impl AsRef<[u8]>) -> String {
2167 const LOWER_HEX: &[u8; 16] = b"0123456789abcdef";
2168 let digest = digest.as_ref();
2169 let mut encoded = String::with_capacity(digest.len().saturating_mul(2));
2170 for byte in digest {
2171 encoded.push(char::from(LOWER_HEX[usize::from(*byte >> 4)]));
2172 encoded.push(char::from(LOWER_HEX[usize::from(*byte & 0x0f)]));
2173 }
2174 encoded
2175}
2176
2177#[cfg(unix)]
2179fn require_executable(path: &Path) -> Result<(), ParserSupervisorError> {
2180 use std::os::unix::fs::PermissionsExt;
2181
2182 if file_metadata(path)?.permissions().mode() & 0o111 == 0 {
2183 return Err(ParserSupervisorError::PayloadMismatch {
2184 path: path.to_path_buf(),
2185 reason: "executable payload has no execute permission",
2186 });
2187 }
2188 Ok(())
2189}
2190
2191#[derive(Debug, Error)]
2193enum ParserIoThreadError {
2194 #[error("{operation}: {source}")]
2196 Stream {
2197 operation: &'static str,
2199 #[source]
2201 source: io::Error,
2202 },
2203 #[error("frame header: {source}")]
2205 FrameHeader {
2206 #[source]
2208 source: ParserProtocolError,
2209 },
2210 #[error("Windows admission record mismatch")]
2212 AdmissionMismatch,
2213 #[error("unexpected diagnostic bytes: {diagnostic}")]
2215 UnexpectedDiagnostic {
2216 diagnostic: String,
2218 },
2219}
2220
2221#[derive(Debug)]
2223enum FrameReaderEvent {
2224 Frame(Vec<u8>),
2226 EndOfStream,
2228 Failure(ParserIoThreadError),
2230}
2231
2232#[derive(Debug)]
2234enum DiagnosticReaderEvent {
2235 AdmissionAccepted,
2237 FenceObserved,
2239 Failure(ParserIoThreadError),
2241}
2242
2243#[derive(Clone, Copy)]
2245struct DiagnosticFence([u8; PARSER_DIAGNOSTIC_FENCE_BYTES]);
2246
2247struct WriterCommand {
2249 bytes: Vec<u8>,
2251 acknowledgement: SyncSender<Result<(), ParserIoThreadError>>,
2253}
2254
2255fn read_one_frame(input: &mut impl Read) -> Result<Option<Vec<u8>>, ParserIoThreadError> {
2257 let mut header_bytes = [0_u8; PARSER_FRAME_HEADER_BYTES];
2258 let mut header_read = 0_usize;
2259 while header_read < header_bytes.len() {
2260 match input.read(&mut header_bytes[header_read..]) {
2261 Ok(0) if header_read == 0 => return Ok(None),
2262 Ok(0) => {
2263 return Err(ParserIoThreadError::Stream {
2264 operation: "read partial frame header",
2265 source: io::Error::new(io::ErrorKind::UnexpectedEof, "partial frame header"),
2266 });
2267 }
2268 Ok(count) => header_read = header_read.saturating_add(count),
2269 Err(source) if source.kind() == io::ErrorKind::Interrupted => {}
2270 Err(source) => {
2271 return Err(ParserIoThreadError::Stream {
2272 operation: "read frame header",
2273 source,
2274 });
2275 }
2276 }
2277 }
2278 let header = ParserFrameHeader::decode(&header_bytes)
2279 .map_err(|source| ParserIoThreadError::FrameHeader { source })?;
2280 let payload_len = header.payload_len() as usize;
2281 let frame_len = PARSER_FRAME_HEADER_BYTES.saturating_add(payload_len);
2282 let mut frame = Vec::with_capacity(frame_len);
2283 frame.extend_from_slice(&header_bytes);
2284 frame.resize(frame_len, 0);
2285 input
2286 .read_exact(&mut frame[PARSER_FRAME_HEADER_BYTES..])
2287 .map_err(|source| ParserIoThreadError::Stream {
2288 operation: "read frame payload",
2289 source,
2290 })?;
2291 Ok(Some(frame))
2292}
2293
2294fn frame_reader_loop(
2296 mut stdout: ChildStdout,
2297 mut diagnostic_fence_writer: impl Write,
2298 diagnostic_fence: DiagnosticFence,
2299 events: &SyncSender<FrameReaderEvent>,
2300) {
2301 loop {
2302 let event = match read_one_frame(&mut stdout) {
2303 Ok(Some(frame)) => FrameReaderEvent::Frame(frame),
2304 Ok(None) => FrameReaderEvent::EndOfStream,
2305 Err(error) => FrameReaderEvent::Failure(error),
2306 };
2307 let event = if matches!(event, FrameReaderEvent::EndOfStream) {
2308 event
2309 } else {
2310 match diagnostic_fence_writer
2311 .write_all(&diagnostic_fence.0)
2312 .and_then(|()| diagnostic_fence_writer.flush())
2313 {
2314 Ok(()) => event,
2315 Err(source) => FrameReaderEvent::Failure(ParserIoThreadError::Stream {
2316 operation: "write diagnostic fence",
2317 source,
2318 }),
2319 }
2320 };
2321 let terminal = !matches!(event, FrameReaderEvent::Frame(_));
2322 if events.send(event).is_err() || terminal {
2323 return;
2324 }
2325 }
2326}
2327
2328fn diagnostic_reader_loop(
2330 mut stderr: impl Read,
2331 expect_windows_admission: bool,
2332 diagnostic_fence: DiagnosticFence,
2333 events: &SyncSender<DiagnosticReaderEvent>,
2334) -> Result<Vec<u8>, ParserIoThreadError> {
2335 if expect_windows_admission {
2336 let mut observed = [0_u8; PARSER_WINDOWS_BROKER_ADMISSION_RECORD.len()];
2337 if let Err(source) = stderr.read_exact(&mut observed) {
2338 let message = source.to_string();
2339 return if events
2340 .send(DiagnosticReaderEvent::Failure(
2341 ParserIoThreadError::Stream {
2342 operation: "read Windows admission record",
2343 source,
2344 },
2345 ))
2346 .is_ok()
2347 {
2348 Ok(Vec::new())
2349 } else {
2350 Err(ParserIoThreadError::Stream {
2351 operation: "read Windows admission record",
2352 source: io::Error::other(message),
2353 })
2354 };
2355 }
2356 if observed != PARSER_WINDOWS_BROKER_ADMISSION_RECORD {
2357 let error = ParserIoThreadError::AdmissionMismatch;
2358 return if events.send(DiagnosticReaderEvent::Failure(error)).is_ok() {
2359 Ok(Vec::new())
2360 } else {
2361 Err(ParserIoThreadError::AdmissionMismatch)
2362 };
2363 }
2364 }
2365 if events
2366 .send(DiagnosticReaderEvent::AdmissionAccepted)
2367 .is_err()
2368 {
2369 return Ok(Vec::new());
2370 }
2371
2372 loop {
2373 let mut observed = [0_u8; PARSER_DIAGNOSTIC_FENCE_BYTES];
2374 let mut observed_len = 0_usize;
2375 while observed_len < observed.len() {
2376 match stderr.read(&mut observed[observed_len..]) {
2377 Ok(0) if observed_len == 0 => return Ok(Vec::new()),
2378 Ok(0) => break,
2379 Ok(count) => observed_len = observed_len.saturating_add(count),
2380 Err(source) if source.kind() == io::ErrorKind::Interrupted => {}
2381 Err(source) => {
2382 let message = source.to_string();
2383 return if events
2384 .send(DiagnosticReaderEvent::Failure(
2385 ParserIoThreadError::Stream {
2386 operation: "read diagnostic stream",
2387 source,
2388 },
2389 ))
2390 .is_ok()
2391 {
2392 Ok(observed[..observed_len].to_vec())
2393 } else {
2394 Err(ParserIoThreadError::Stream {
2395 operation: "read diagnostic stream",
2396 source: io::Error::other(message),
2397 })
2398 };
2399 }
2400 }
2401 }
2402 if observed_len == observed.len() && observed == diagnostic_fence.0 {
2403 if events.send(DiagnosticReaderEvent::FenceObserved).is_err() {
2404 return Ok(Vec::new());
2405 }
2406 continue;
2407 }
2408 let diagnostics = observed[..observed_len].to_vec();
2409 let diagnostic = bounded_diagnostic(&diagnostics);
2410 return if events
2411 .send(DiagnosticReaderEvent::Failure(
2412 ParserIoThreadError::UnexpectedDiagnostic {
2413 diagnostic: diagnostic.clone(),
2414 },
2415 ))
2416 .is_ok()
2417 {
2418 Ok(diagnostics)
2419 } else {
2420 Err(ParserIoThreadError::UnexpectedDiagnostic { diagnostic })
2421 };
2422 }
2423}
2424
2425fn writer_loop(mut stdin: impl Write, commands: &Receiver<WriterCommand>) {
2427 while let Ok(command) = commands.recv() {
2428 let result = stdin
2429 .write_all(&command.bytes)
2430 .and_then(|()| stdin.flush())
2431 .map_err(|source| ParserIoThreadError::Stream {
2432 operation: "write protocol frame",
2433 source,
2434 });
2435 let failed = result.is_err();
2436 if command.acknowledgement.send(result).is_err() || failed {
2437 return;
2438 }
2439 }
2440}
2441
2442struct FrameReader {
2444 events: Receiver<FrameReaderEvent>,
2446 handle: Option<JoinHandle<()>>,
2448}
2449
2450struct DiagnosticReader {
2452 events: Receiver<DiagnosticReaderEvent>,
2454 handle: Option<JoinHandle<Result<Vec<u8>, ParserIoThreadError>>>,
2456}
2457
2458#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2460struct LinuxMemoryBreach {
2461 accounting: ParserMemoryAccountingKind,
2463 observed_bytes: u64,
2465}
2466
2467#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2469enum LinuxMemoryObservation {
2470 Memory(Option<LinuxMemoryBreach>),
2472 ChildExited {
2474 code: Option<i32>,
2476 },
2477}
2478
2479#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2481struct LinuxChildExit {
2482 code: Option<i32>,
2484}
2485
2486#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2488enum LinuxProcessGroupTermination {
2489 Signalled,
2491 Absent,
2493}
2494
2495#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2497struct LinuxCgroupMemory {
2498 directory: PathBuf,
2500 initial_max_events: u64,
2502}
2503
2504#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2505impl LinuxCgroupMemory {
2506 fn try_attach(process_id: u32, maximum_bytes: u64) -> io::Result<Option<Self>> {
2508 for parent in delegated_cgroup_parents()? {
2509 let sequence = CGROUP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
2510 let directory = parent.join(format!(
2511 "projectatlas-parser-{}-{sequence}",
2512 std::process::id()
2513 ));
2514 if fs::create_dir(&directory).is_err() {
2515 continue;
2516 }
2517 let mut candidate = Self {
2518 directory,
2519 initial_max_events: 0,
2520 };
2521 if !prepare_delegated_memory_parent(&parent) {
2522 candidate.cleanup()?;
2523 continue;
2524 }
2525 if candidate.configure(maximum_bytes).is_err() {
2526 candidate.cleanup()?;
2527 continue;
2528 }
2529 let Ok(initial_max_events) = read_cgroup_max_events(&candidate.directory) else {
2530 candidate.cleanup()?;
2531 continue;
2532 };
2533 candidate.initial_max_events = initial_max_events;
2534 if fs::write(
2535 candidate.directory.join("cgroup.procs"),
2536 process_id.to_string(),
2537 )
2538 .is_err()
2539 {
2540 candidate.cleanup()?;
2541 continue;
2542 }
2543 return Ok(Some(candidate));
2544 }
2545 Ok(None)
2546 }
2547
2548 fn configure(&self, maximum_bytes: u64) -> io::Result<()> {
2550 let maximum = maximum_bytes.to_string();
2551 fs::write(self.directory.join("memory.max"), &maximum)?;
2552 if read_bounded_linux_text(
2553 &self.directory.join("memory.max"),
2554 LINUX_MEMORY_RECORD_MAX_BYTES,
2555 )?
2556 .trim()
2557 != maximum
2558 {
2559 return Err(io::Error::new(
2560 io::ErrorKind::InvalidData,
2561 "delegated cgroup memory.max did not retain the configured ceiling",
2562 ));
2563 }
2564 let oom_group = self.directory.join("memory.oom.group");
2565 if oom_group.is_file() {
2566 fs::write(oom_group, "1")?;
2567 }
2568 Ok(())
2569 }
2570
2571 fn observe(&self, maximum_bytes: u64) -> io::Result<Option<LinuxMemoryBreach>> {
2573 let observed_bytes = read_cgroup_current(&self.directory)?;
2574 let maximum_events = read_cgroup_max_events(&self.directory)?;
2575 if observed_bytes >= maximum_bytes || maximum_events > self.initial_max_events {
2576 Ok(Some(LinuxMemoryBreach {
2577 accounting: ParserMemoryAccountingKind::LinuxCgroupV2,
2578 observed_bytes,
2579 }))
2580 } else {
2581 Ok(None)
2582 }
2583 }
2584
2585 fn cleanup(&mut self) -> io::Result<()> {
2587 if !self.directory.exists() {
2588 return Ok(());
2589 }
2590 fs::remove_dir(&self.directory)
2591 }
2592}
2593
2594#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2595impl Drop for LinuxCgroupMemory {
2596 fn drop(&mut self) {
2597 drop(self.cleanup());
2598 }
2599}
2600
2601#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2603struct LinuxMemoryObserver {
2604 cgroup: Option<LinuxCgroupMemory>,
2606 observe_cgroup: bool,
2608 maximum_bytes: u64,
2610}
2611
2612#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2613impl LinuxMemoryObserver {
2614 fn attach(process_id: u32, maximum_bytes: u64) -> io::Result<Self> {
2616 let cgroup = LinuxCgroupMemory::try_attach(process_id, maximum_bytes)?;
2617 let observe_cgroup = cgroup.is_some();
2618 Ok(Self {
2619 cgroup,
2620 observe_cgroup,
2621 maximum_bytes,
2622 })
2623 }
2624
2625 fn sampled_rss(maximum_bytes: u64) -> Self {
2627 Self {
2628 cgroup: None,
2629 observe_cgroup: false,
2630 maximum_bytes,
2631 }
2632 }
2633
2634 fn observe(&mut self, process_id: u32) -> io::Result<Option<LinuxMemoryBreach>> {
2636 if self.observe_cgroup
2637 && let Some(cgroup) = self.cgroup.as_ref()
2638 {
2639 match cgroup.observe(self.maximum_bytes) {
2640 Ok(observation) => return Ok(observation),
2641 Err(_source) => self.observe_cgroup = false,
2642 }
2643 }
2644 let observed_bytes = read_process_rss(process_id)?;
2645 if observed_bytes >= self.maximum_bytes {
2646 Ok(Some(LinuxMemoryBreach {
2647 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
2648 observed_bytes,
2649 }))
2650 } else {
2651 Ok(None)
2652 }
2653 }
2654
2655 fn cleanup(&mut self) -> io::Result<()> {
2657 match self.cgroup.as_mut() {
2658 Some(cgroup) => cgroup.cleanup(),
2659 None => Ok(()),
2660 }
2661 }
2662
2663 fn accounting_kind(&self) -> ParserMemoryAccountingKind {
2665 if self.observe_cgroup {
2666 ParserMemoryAccountingKind::LinuxCgroupV2
2667 } else {
2668 ParserMemoryAccountingKind::LinuxProcStatus
2669 }
2670 }
2671}
2672
2673#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2675enum LinuxMemoryMonitorEvent {
2676 Limit {
2678 breach: LinuxMemoryBreach,
2680 termination_error: Option<String>,
2682 },
2683 ObservationFailed {
2685 accounting: ParserMemoryAccountingKind,
2687 message: String,
2689 },
2690}
2691
2692#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2694struct LinuxMemoryMonitor {
2695 stop: SyncSender<()>,
2697 events: Receiver<LinuxMemoryMonitorEvent>,
2699 handle: Option<JoinHandle<()>>,
2701}
2702
2703#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2704impl LinuxMemoryMonitor {
2705 fn start(process_id: u32, observer: Arc<Mutex<LinuxMemoryObserver>>) -> io::Result<Self> {
2707 let (stop, stop_receiver) = mpsc::sync_channel(1);
2708 let (event_sender, events) = mpsc::sync_channel(1);
2709 let handle = thread::Builder::new()
2710 .name("parser-supervisor-memory".to_owned())
2711 .spawn(move || {
2712 linux_memory_monitor_loop(process_id, &observer, &stop_receiver, &event_sender);
2713 })?;
2714 Ok(Self {
2715 stop,
2716 events,
2717 handle: Some(handle),
2718 })
2719 }
2720
2721 fn stop(&mut self) -> Result<Option<LinuxMemoryMonitorEvent>, ParserSupervisorError> {
2723 let _stop_signal_result = self.stop.try_send(());
2724 if let Some(handle) = self.handle.take() {
2725 handle
2726 .join()
2727 .map_err(|_panic| ParserSupervisorError::Cleanup {
2728 message: "Linux resident-memory monitor panicked".to_owned(),
2729 })?;
2730 }
2731 match self.events.try_recv() {
2732 Ok(event) => Ok(Some(event)),
2733 Err(TryRecvError::Empty | TryRecvError::Disconnected) => Ok(None),
2734 }
2735 }
2736}
2737
2738#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2740fn linux_memory_monitor_loop(
2741 process_id: u32,
2742 observer: &Arc<Mutex<LinuxMemoryObserver>>,
2743 stop: &Receiver<()>,
2744 events: &SyncSender<LinuxMemoryMonitorEvent>,
2745) {
2746 let mut next_observation = Instant::now();
2747 loop {
2748 let observation = match observer.lock() {
2749 Ok(mut observer) => {
2750 let observation = observer.observe(process_id);
2751 let accounting = observer.accounting_kind();
2752 observation.map_err(|source| (accounting, source))
2753 }
2754 Err(_poisoned) => Err((
2755 ParserMemoryAccountingKind::LinuxProcStatus,
2756 io::Error::other("Linux memory observer lock was poisoned"),
2757 )),
2758 };
2759 let event = match observation {
2760 Ok(None) => None,
2761 Ok(Some(breach)) => Some(LinuxMemoryMonitorEvent::Limit {
2762 breach,
2763 termination_error: linux_monitor_termination_error(process_id),
2764 }),
2765 Err((accounting, source)) => Some(LinuxMemoryMonitorEvent::ObservationFailed {
2766 accounting,
2767 message: bounded_message(source.to_string()),
2768 }),
2769 };
2770 if let Some(event) = event {
2771 drop(events.try_send(event));
2772 return;
2773 }
2774 next_observation = next_observation
2775 .checked_add(PARSER_LINUX_RSS_OBSERVATION_INTERVAL)
2776 .unwrap_or_else(Instant::now);
2777 let wait = next_observation.saturating_duration_since(Instant::now());
2778 match stop.recv_timeout(wait) {
2779 Ok(()) | Err(RecvTimeoutError::Disconnected) => return,
2780 Err(RecvTimeoutError::Timeout) => {}
2781 }
2782 }
2783}
2784
2785#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2787fn linux_monitor_termination_error(process_id: u32) -> Option<String> {
2788 match terminate_linux_process_group(process_id) {
2789 Ok(LinuxProcessGroupTermination::Signalled) => None,
2790 Ok(LinuxProcessGroupTermination::Absent) => {
2791 Some("worker process group was absent".to_owned())
2792 }
2793 Err(source) => Some(source),
2794 }
2795}
2796
2797#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2799fn delegated_cgroup_parents() -> io::Result<Vec<PathBuf>> {
2800 let membership = read_bounded_linux_text(
2801 Path::new("/proc/self/cgroup"),
2802 LINUX_MEMORY_RECORD_MAX_BYTES,
2803 )?;
2804 let Some(relative_path) = parse_unified_cgroup_path(&membership)? else {
2805 return Ok(Vec::new());
2806 };
2807 let root = Path::new(CGROUP_V2_ROOT);
2808 let mut candidate = root.join(relative_path);
2809 let mut candidates = Vec::new();
2810 loop {
2811 if !candidate.starts_with(root) || candidates.len() >= MAX_CGROUP_ANCESTORS {
2812 return Err(io::Error::new(
2813 io::ErrorKind::InvalidData,
2814 "unified cgroup membership exceeds its ancestor bound",
2815 ));
2816 }
2817 candidates.push(candidate.clone());
2818 if candidate == root {
2819 break;
2820 }
2821 if !candidate.pop() {
2822 return Err(io::Error::new(
2823 io::ErrorKind::InvalidData,
2824 "unified cgroup membership escaped its mount",
2825 ));
2826 }
2827 }
2828 Ok(candidates)
2829}
2830
2831#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2833fn parse_unified_cgroup_path(membership: &str) -> io::Result<Option<PathBuf>> {
2834 let mut unified = membership
2835 .lines()
2836 .filter_map(|line| line.strip_prefix("0::"));
2837 let Some(relative) = unified.next() else {
2838 return Ok(None);
2839 };
2840 if unified.next().is_some() {
2841 return Err(io::Error::new(
2842 io::ErrorKind::InvalidData,
2843 "multiple unified cgroup memberships were reported",
2844 ));
2845 }
2846 let relative = relative.trim_start_matches('/');
2847 let relative_path = Path::new(relative);
2848 let component_count = relative_path.components().count();
2849 if component_count > MAX_CGROUP_ANCESTORS.saturating_sub(1)
2850 || relative_path
2851 .components()
2852 .any(|component| !matches!(component, std::path::Component::Normal(_)))
2853 {
2854 return Err(io::Error::new(
2855 io::ErrorKind::InvalidData,
2856 "unified cgroup membership path is unsafe or too deep",
2857 ));
2858 }
2859 Ok(Some(relative_path.to_path_buf()))
2860}
2861
2862#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2864fn prepare_delegated_memory_parent(parent: &Path) -> bool {
2865 let controllers = read_bounded_linux_text(
2866 &parent.join("cgroup.controllers"),
2867 LINUX_MEMORY_RECORD_MAX_BYTES,
2868 );
2869 let Ok(controllers) = controllers else {
2870 return false;
2871 };
2872 if !has_cgroup_token(&controllers, "memory") {
2873 return false;
2874 }
2875 let subtree_path = parent.join("cgroup.subtree_control");
2876 let Ok(mut subtree_control) =
2877 read_bounded_linux_text(&subtree_path, LINUX_MEMORY_RECORD_MAX_BYTES)
2878 else {
2879 return false;
2880 };
2881 if !has_cgroup_token(&subtree_control, "memory") {
2882 if fs::write(&subtree_path, "+memory").is_err() {
2883 return false;
2884 }
2885 let Ok(observed) = read_bounded_linux_text(&subtree_path, LINUX_MEMORY_RECORD_MAX_BYTES)
2886 else {
2887 return false;
2888 };
2889 subtree_control = observed;
2890 if !has_cgroup_token(&subtree_control, "memory") {
2891 return false;
2892 }
2893 }
2894 true
2895}
2896
2897#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2899fn has_cgroup_token(values: &str, expected: &str) -> bool {
2900 values.split_whitespace().any(|value| {
2901 value == expected
2902 || value
2903 .strip_prefix('+')
2904 .is_some_and(|value| value == expected)
2905 })
2906}
2907
2908#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2910fn read_bounded_linux_text(path: &Path, maximum: u64) -> io::Result<String> {
2911 let mut bytes = Vec::new();
2912 File::open(path)?
2913 .take(maximum.saturating_add(1))
2914 .read_to_end(&mut bytes)?;
2915 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > maximum {
2916 return Err(io::Error::new(
2917 io::ErrorKind::InvalidData,
2918 "Linux memory accounting record exceeded its byte ceiling",
2919 ));
2920 }
2921 String::from_utf8(bytes).map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))
2922}
2923
2924#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2926fn read_process_rss(process_id: u32) -> io::Result<u64> {
2927 let status = read_bounded_linux_text(
2928 &PathBuf::from(format!("/proc/{process_id}/status")),
2929 LINUX_MEMORY_RECORD_MAX_BYTES,
2930 )?;
2931 parse_process_rss(&status)
2932}
2933
2934#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2937fn resolve_linux_memory_exit_transition(
2938 initial_error: io::Error,
2939 timeout: Duration,
2940 mut observe_memory: impl FnMut() -> io::Result<Option<LinuxMemoryBreach>>,
2941 mut observe_exit: impl FnMut() -> io::Result<Option<LinuxChildExit>>,
2942) -> io::Result<LinuxMemoryObservation> {
2943 let deadline = Instant::now()
2944 .checked_add(timeout)
2945 .unwrap_or_else(Instant::now);
2946 let mut memory_error = initial_error;
2947 loop {
2948 match observe_exit() {
2949 Ok(Some(exit)) => {
2950 return Ok(LinuxMemoryObservation::ChildExited { code: exit.code });
2951 }
2952 Ok(None) => {}
2953 Err(source) => {
2954 return Err(io::Error::other(format!(
2955 "memory observation failed: {memory_error}; child-state observation also failed: {source}"
2956 )));
2957 }
2958 }
2959 match observe_memory() {
2960 Ok(observation) => return Ok(LinuxMemoryObservation::Memory(observation)),
2961 Err(source) => memory_error = source,
2962 }
2963 let now = Instant::now();
2964 if now >= deadline {
2965 return Err(memory_error);
2966 }
2967 thread::sleep(Duration::from_millis(1).min(deadline.saturating_duration_since(now)));
2968 }
2969}
2970
2971#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2973fn parse_process_rss(status: &str) -> io::Result<u64> {
2974 let value = status
2975 .lines()
2976 .find_map(|line| line.strip_prefix("VmRSS:"))
2977 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"))?;
2978 let mut fields = value.split_whitespace();
2979 let kibibytes = fields
2980 .next()
2981 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "VmRSS value is absent"))?
2982 .parse::<u64>()
2983 .map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))?;
2984 if fields.next() != Some("kB") || fields.next().is_some() {
2985 return Err(io::Error::new(
2986 io::ErrorKind::InvalidData,
2987 "VmRSS does not use the exact Linux kB unit",
2988 ));
2989 }
2990 kibibytes.checked_mul(1024).ok_or_else(|| {
2991 io::Error::new(
2992 io::ErrorKind::InvalidData,
2993 "VmRSS byte conversion overflowed",
2994 )
2995 })
2996}
2997
2998#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3000fn read_cgroup_current(directory: &Path) -> io::Result<u64> {
3001 let current = read_bounded_linux_text(
3002 &directory.join("memory.current"),
3003 LINUX_MEMORY_RECORD_MAX_BYTES,
3004 )?;
3005 current
3006 .trim()
3007 .parse::<u64>()
3008 .map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))
3009}
3010
3011#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3013fn read_cgroup_max_events(directory: &Path) -> io::Result<u64> {
3014 let events = read_bounded_linux_text(
3015 &directory.join("memory.events"),
3016 LINUX_MEMORY_RECORD_MAX_BYTES,
3017 )?;
3018 parse_cgroup_event(&events, "max")
3019}
3020
3021#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
3023fn parse_cgroup_event(events: &str, name: &str) -> io::Result<u64> {
3024 let value = events
3025 .lines()
3026 .find_map(|line| {
3027 let mut fields = line.split_whitespace();
3028 (fields.next() == Some(name)).then(|| (fields.next(), fields.next()))
3029 })
3030 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "cgroup event is absent"))?;
3031 if value.1.is_some() {
3032 return Err(io::Error::new(
3033 io::ErrorKind::InvalidData,
3034 "cgroup event row has extra fields",
3035 ));
3036 }
3037 value
3038 .0
3039 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "cgroup event value is absent"))?
3040 .parse::<u64>()
3041 .map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))
3042}
3043
3044struct ResidentParserSession {
3046 child: Child,
3048 grammar: ParserLanguageIdentity,
3050 session: ParserSessionIdentity,
3052 artifact: ParserArtifactIdentity,
3054 next_request_id: u64,
3056 termination_requested: bool,
3058 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3060 memory_limits: ParserMemoryLimits,
3061 writer: Option<SyncSender<WriterCommand>>,
3063 writer_handle: Option<JoinHandle<()>>,
3065 frame_reader: FrameReader,
3067 diagnostic_reader: DiagnosticReader,
3069 pending_diagnostic_fences: usize,
3071 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3073 memory_observer: Arc<Mutex<LinuxMemoryObserver>>,
3074 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3076 memory_monitor: Option<LinuxMemoryMonitor>,
3077}
3078
3079impl ResidentParserSession {
3080 fn launch(
3082 launch: &VerifiedParserPackLaunch,
3083 grammar: ParserLanguageIdentity,
3084 memory_limits: ParserMemoryLimits,
3085 last_progress: Instant,
3086 absolute_deadline: Instant,
3087 no_progress_timeout: Duration,
3088 cancellation: &IndexCancellation,
3089 ) -> Result<Self, ParserSupervisorError> {
3090 let memory_limits = memory_limits.checked()?;
3091 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3092 let command = {
3093 let authority = launch.prepare_resident_launch_controlled(
3094 grammar.as_str(),
3095 last_progress,
3096 absolute_deadline,
3097 no_progress_timeout,
3098 cancellation,
3099 )?;
3100 #[cfg(debug_assertions)]
3101 invoke_linux_launch_test_hook()?;
3102 platform_command(launch, authority, memory_limits)?
3103 };
3104 #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
3105 let command = platform_command(launch, memory_limits)?;
3106 Self::launch_command(
3107 launch,
3108 grammar,
3109 memory_limits,
3110 last_progress,
3111 absolute_deadline,
3112 no_progress_timeout,
3113 cancellation,
3114 command,
3115 )
3116 }
3117
3118 fn launch_command(
3120 launch: &VerifiedParserPackLaunch,
3121 grammar: ParserLanguageIdentity,
3122 memory_limits: ParserMemoryLimits,
3123 last_progress: Instant,
3124 absolute_deadline: Instant,
3125 no_progress_timeout: Duration,
3126 cancellation: &IndexCancellation,
3127 mut command: Command,
3128 ) -> Result<Self, ParserSupervisorError> {
3129 let _ = memory_limits;
3130 poll_stop(
3131 PROCESS_LAUNCH_PHASE,
3132 absolute_deadline,
3133 last_progress,
3134 no_progress_timeout,
3135 cancellation,
3136 )?;
3137 let session = fresh_session_identity()?;
3138 let containment = containment_for_platform(launch.platform);
3139 let diagnostic_fence = fresh_diagnostic_fence()?;
3140 let (diagnostic_pipe, child_diagnostic_writer) =
3141 io::pipe().map_err(|source| ParserSupervisorError::IoThread {
3142 phase: "diagnostic pipe startup",
3143 message: source.to_string(),
3144 })?;
3145 let diagnostic_fence_writer = child_diagnostic_writer.try_clone().map_err(|source| {
3146 ParserSupervisorError::IoThread {
3147 phase: "diagnostic pipe startup",
3148 message: source.to_string(),
3149 }
3150 })?;
3151 command.stderr(Stdio::from(child_diagnostic_writer));
3152 #[cfg(debug_assertions)]
3153 invoke_pre_spawn_test_hook()?;
3154 let mut child = run_bounded_process_spawn(
3155 command,
3156 absolute_deadline,
3157 last_progress,
3158 no_progress_timeout,
3159 cancellation,
3160 )?;
3161 let stdin = child
3162 .stdin
3163 .take()
3164 .ok_or(ParserSupervisorError::MissingPipe { stream: "stdin" });
3165 let stdout = child
3166 .stdout
3167 .take()
3168 .ok_or(ParserSupervisorError::MissingPipe { stream: "stdout" });
3169 let (stdin, stdout) = match (stdin, stdout) {
3170 (Ok(stdin), Ok(stdout)) => (stdin, stdout),
3171 (stdin, stdout) => {
3172 let operation = stdin
3173 .err()
3174 .or_else(|| stdout.err())
3175 .unwrap_or(ParserSupervisorError::MissingPipe { stream: "unknown" });
3176 return Err(attach_cleanup(
3177 operation,
3178 cleanup_partial_launch(&mut child, Vec::new(), None, None, None),
3179 ));
3180 }
3181 };
3182
3183 let (writer_sender, writer_receiver) = mpsc::sync_channel(1);
3184 let writer_handle = thread::Builder::new()
3185 .name("parser-supervisor-writer".to_owned())
3186 .spawn(move || writer_loop(stdin, &writer_receiver))
3187 .map_err(|source| ParserSupervisorError::IoThread {
3188 phase: "writer startup",
3189 message: source.to_string(),
3190 });
3191 let writer_handle = match writer_handle {
3192 Ok(handle) => handle,
3193 Err(error) => {
3194 return Err(attach_cleanup(
3195 error,
3196 cleanup_partial_launch(&mut child, Vec::new(), None, None, None),
3197 ));
3198 }
3199 };
3200 let (frame_sender, frame_events) = mpsc::sync_channel(1);
3201 let frame_handle = thread::Builder::new()
3202 .name("parser-supervisor-stdout".to_owned())
3203 .spawn(move || {
3204 frame_reader_loop(
3205 stdout,
3206 diagnostic_fence_writer,
3207 diagnostic_fence,
3208 &frame_sender,
3209 );
3210 })
3211 .map_err(|source| ParserSupervisorError::IoThread {
3212 phase: "stdout reader startup",
3213 message: source.to_string(),
3214 });
3215 let frame_handle = match frame_handle {
3216 Ok(handle) => handle,
3217 Err(error) => {
3218 drop(writer_sender);
3219 return Err(attach_cleanup(
3220 error,
3221 cleanup_partial_launch(&mut child, vec![writer_handle], None, None, None),
3222 ));
3223 }
3224 };
3225 let (diagnostic_sender, diagnostic_events) = mpsc::sync_channel(1);
3226 let expect_windows_admission = launch.platform == PackPlatform::WindowsX86_64;
3227 let diagnostic_handle = thread::Builder::new()
3228 .name("parser-supervisor-stderr".to_owned())
3229 .spawn(move || {
3230 diagnostic_reader_loop(
3231 diagnostic_pipe,
3232 expect_windows_admission,
3233 diagnostic_fence,
3234 &diagnostic_sender,
3235 )
3236 })
3237 .map_err(|source| ParserSupervisorError::IoThread {
3238 phase: "diagnostic reader startup",
3239 message: source.to_string(),
3240 });
3241 let diagnostic_handle = match diagnostic_handle {
3242 Ok(handle) => handle,
3243 Err(error) => {
3244 drop(writer_sender);
3245 return Err(attach_cleanup(
3246 error,
3247 cleanup_partial_launch(
3248 &mut child,
3249 vec![writer_handle, frame_handle],
3250 None,
3251 Some(frame_events),
3252 None,
3253 ),
3254 ));
3255 }
3256 };
3257 if let Err(operation) = poll_stop(
3258 PROCESS_LAUNCH_PHASE,
3259 absolute_deadline,
3260 last_progress,
3261 no_progress_timeout,
3262 cancellation,
3263 ) {
3264 drop(writer_sender);
3265 return Err(attach_cleanup(
3266 operation,
3267 cleanup_partial_launch(
3268 &mut child,
3269 vec![writer_handle, frame_handle],
3270 Some(diagnostic_handle),
3271 Some(frame_events),
3272 Some(diagnostic_events),
3273 ),
3274 ));
3275 }
3276
3277 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3278 let (memory_observer, memory_attachment_error) =
3279 match LinuxMemoryObserver::attach(child.id(), memory_limits.process_bytes) {
3280 Ok(observer) => (observer, None),
3281 Err(source) => (
3282 LinuxMemoryObserver::sampled_rss(memory_limits.process_bytes),
3283 Some(source),
3284 ),
3285 };
3286 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3287 let memory_observer = Arc::new(Mutex::new(memory_observer));
3288 let mut resident = Self {
3289 child,
3290 grammar,
3291 session: session.clone(),
3292 artifact: launch.artifact.clone(),
3293 next_request_id: 1,
3294 termination_requested: false,
3295 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3296 memory_limits,
3297 writer: Some(writer_sender),
3298 writer_handle: Some(writer_handle),
3299 frame_reader: FrameReader {
3300 events: frame_events,
3301 handle: Some(frame_handle),
3302 },
3303 diagnostic_reader: DiagnosticReader {
3304 events: diagnostic_events,
3305 handle: Some(diagnostic_handle),
3306 },
3307 pending_diagnostic_fences: 0,
3308 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3309 memory_observer,
3310 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3311 memory_monitor: None,
3312 };
3313 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3314 if let Some(source) = memory_attachment_error {
3315 let operation = ParserSupervisorError::ResidentMemoryObservationFailed {
3316 phase: "delegated cgroup attachment cleanup",
3317 accounting: ParserMemoryAccountingKind::LinuxCgroupV2,
3318 message: bounded_message(source.to_string()),
3319 };
3320 return match resident.shutdown() {
3321 Ok(()) => Err(operation),
3322 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
3323 operation: Box::new(operation),
3324 cleanup: Box::new(cleanup),
3325 }),
3326 };
3327 }
3328 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3329 match LinuxMemoryMonitor::start(resident.child.id(), Arc::clone(&resident.memory_observer))
3330 {
3331 Ok(monitor) => resident.memory_monitor = Some(monitor),
3332 Err(source) => {
3333 let operation = ParserSupervisorError::IoThread {
3334 phase: "resident-memory monitor startup",
3335 message: source.to_string(),
3336 };
3337 return match resident.shutdown() {
3338 Ok(()) => Err(operation),
3339 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
3340 operation: Box::new(operation),
3341 cleanup: Box::new(cleanup),
3342 }),
3343 };
3344 }
3345 }
3346 let opening: Result<(), ParserSupervisorError> = (|| {
3347 resident.wait_for_admission(
3348 absolute_deadline,
3349 last_progress,
3350 no_progress_timeout,
3351 cancellation,
3352 )?;
3353 let session_open = encode_parser_control(&ParserControl::SessionOpen(
3354 ParserSessionOpen::new(session.clone()),
3355 ))?;
3356 resident.send_bytes(
3357 session_open,
3358 "SessionOpen write",
3359 absolute_deadline,
3360 last_progress,
3361 no_progress_timeout,
3362 cancellation,
3363 )?;
3364 let ready_bytes = resident.wait_for_frame(
3365 "READY",
3366 absolute_deadline,
3367 last_progress,
3368 no_progress_timeout,
3369 cancellation,
3370 )?;
3371 let ready_frame = ParserFrame::decode_exact(&ready_bytes)?;
3372 decode_parser_ready_for_launch(ready_frame, &session, &launch.artifact, containment)?;
3373 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3374 resident.enforce_memory_bound("READY", true)?;
3375 Ok(())
3376 })();
3377 if let Err(operation) = opening {
3378 if operation.is_caller_stop() {
3379 resident.termination_requested = true;
3380 }
3381 return match resident.shutdown() {
3382 Ok(()) => Err(operation),
3383 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
3384 operation: Box::new(operation),
3385 cleanup: Box::new(cleanup),
3386 }),
3387 };
3388 }
3389 Ok(resident)
3390 }
3391
3392 fn parse(
3394 &mut self,
3395 source: &[u8],
3396 source_identity: ParserSourceIdentity,
3397 limits: ParserRequestLimits,
3398 mut last_progress: Instant,
3399 absolute_deadline: Instant,
3400 no_progress_timeout: Duration,
3401 cancellation: &IndexCancellation,
3402 ) -> Result<ParserCompletionEvidence, ParserSupervisorError> {
3403 let request_id = ParserRequestIdentity::new(self.next_request_id)?;
3404 self.next_request_id = self
3405 .next_request_id
3406 .checked_add(1)
3407 .ok_or(ParserSupervisorError::RequestIdentityExhausted)?;
3408 let request = ParserRequest::new(
3409 self.session.clone(),
3410 request_id,
3411 self.artifact.clone(),
3412 self.grammar.clone(),
3413 source_identity,
3414 limits,
3415 );
3416 let mut request_bytes = encode_parser_control(&ParserControl::Request(request.clone()))?;
3417 let source_len = u32::try_from(source.len()).map_err(|_source| {
3418 ParserProtocolError::FramePayloadTooLarge {
3419 kind: ParserFrameKind::RawSource,
3420 actual: u32::MAX,
3421 maximum: ParserFrameKind::RawSource.maximum_payload_bytes(),
3422 }
3423 })?;
3424 let source_header = ParserFrameHeader::new(ParserFrameKind::RawSource, source_len)?;
3425 request_bytes.reserve(PARSER_FRAME_HEADER_BYTES.saturating_add(source.len()));
3426 request_bytes.extend_from_slice(&source_header.encode());
3427 request_bytes.extend_from_slice(source);
3428
3429 self.send_bytes(
3430 request_bytes,
3431 "request write",
3432 absolute_deadline,
3433 last_progress,
3434 no_progress_timeout,
3435 cancellation,
3436 )?;
3437 let mut previous_progress: Option<ParserProgress> = None;
3438 loop {
3439 let response_bytes = self.wait_for_frame(
3440 "request response",
3441 absolute_deadline,
3442 last_progress,
3443 no_progress_timeout,
3444 cancellation,
3445 )?;
3446 let frame = ParserFrame::decode_exact(&response_bytes)?;
3447 match frame.kind() {
3448 ParserFrameKind::Progress => {
3449 let (progress, disposition) = decode_parser_progress_for_request(
3450 frame,
3451 &request,
3452 previous_progress.as_ref(),
3453 )?;
3454 if disposition == ParserProgressDisposition::Advanced {
3455 last_progress = Instant::now();
3456 }
3457 previous_progress = Some(progress);
3458 }
3459 ParserFrameKind::Completion => {
3460 let completion = decode_parser_completion_for_request(frame, &request)?;
3461 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3462 self.enforce_memory_bound("request completion", true)?;
3463 return Ok(completion.evidence().clone());
3464 }
3465 ParserFrameKind::Failure => {
3466 let failure = decode_parser_failure_for_request(frame, &request)?;
3467 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3468 match self.enforce_memory_bound("request failure", true) {
3469 Ok(()) | Err(ParserSupervisorError::ChildExited { code: Some(0), .. }) => {}
3470 Err(error) => return Err(error),
3471 }
3472 return Err(ParserSupervisorError::WorkerFailure {
3473 code: failure.code(),
3474 });
3475 }
3476 kind => {
3477 return Err(ParserProtocolError::UnexpectedFrameKind { kind }.into());
3478 }
3479 }
3480 }
3481 }
3482
3483 fn wait_for_admission(
3485 &mut self,
3486 absolute_deadline: Instant,
3487 last_progress: Instant,
3488 no_progress_timeout: Duration,
3489 cancellation: &IndexCancellation,
3490 ) -> Result<(), ParserSupervisorError> {
3491 loop {
3492 poll_stop(
3493 "containment admission",
3494 absolute_deadline,
3495 last_progress,
3496 no_progress_timeout,
3497 cancellation,
3498 )?;
3499 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3500 self.enforce_memory_bound("containment admission", false)?;
3501 match self.diagnostic_reader.events.recv_timeout(next_poll_wait(
3502 absolute_deadline,
3503 last_progress,
3504 no_progress_timeout,
3505 )) {
3506 Ok(DiagnosticReaderEvent::AdmissionAccepted) => return Ok(()),
3507 Ok(DiagnosticReaderEvent::FenceObserved) => {
3508 return Err(ParserSupervisorError::IoThread {
3509 phase: "containment admission",
3510 message: "diagnostic fence arrived before admission".to_owned(),
3511 });
3512 }
3513 Ok(DiagnosticReaderEvent::Failure(ParserIoThreadError::AdmissionMismatch)) => {
3514 return Err(ParserSupervisorError::InvalidAdmission);
3515 }
3516 Ok(DiagnosticReaderEvent::Failure(error)) => {
3517 return Err(io_thread_error("containment admission", &error));
3518 }
3519 Err(RecvTimeoutError::Timeout) => self.require_child_running("admission")?,
3520 Err(RecvTimeoutError::Disconnected) => {
3521 return Err(ParserSupervisorError::IoThread {
3522 phase: "containment admission",
3523 message: "diagnostic reader closed before admission".to_owned(),
3524 });
3525 }
3526 }
3527 }
3528 }
3529
3530 fn send_bytes(
3532 &mut self,
3533 bytes: Vec<u8>,
3534 phase: &'static str,
3535 absolute_deadline: Instant,
3536 last_progress: Instant,
3537 no_progress_timeout: Duration,
3538 cancellation: &IndexCancellation,
3539 ) -> Result<(), ParserSupervisorError> {
3540 let (acknowledgement, result) = mpsc::sync_channel(1);
3541 let mut command = WriterCommand {
3542 bytes,
3543 acknowledgement,
3544 };
3545 loop {
3546 poll_stop(
3547 phase,
3548 absolute_deadline,
3549 last_progress,
3550 no_progress_timeout,
3551 cancellation,
3552 )?;
3553 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3554 self.enforce_memory_bound(phase, false)?;
3555 let Some(writer) = self.writer.as_ref() else {
3556 return Err(ParserSupervisorError::IoThread {
3557 phase,
3558 message: "writer was already closed".to_owned(),
3559 });
3560 };
3561 match writer.try_send(command) {
3562 Ok(()) => break,
3563 Err(TrySendError::Full(returned)) => {
3564 command = returned;
3565 self.require_child_running(phase)?;
3566 thread::sleep(next_poll_wait(
3567 absolute_deadline,
3568 last_progress,
3569 no_progress_timeout,
3570 ));
3571 }
3572 Err(TrySendError::Disconnected(_returned)) => {
3573 return Err(ParserSupervisorError::IoThread {
3574 phase,
3575 message: "writer thread closed".to_owned(),
3576 });
3577 }
3578 }
3579 }
3580 loop {
3581 poll_stop(
3582 phase,
3583 absolute_deadline,
3584 last_progress,
3585 no_progress_timeout,
3586 cancellation,
3587 )?;
3588 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3589 self.enforce_memory_bound(phase, false)?;
3590 match result.recv_timeout(next_poll_wait(
3591 absolute_deadline,
3592 last_progress,
3593 no_progress_timeout,
3594 )) {
3595 Ok(Ok(())) => return Ok(()),
3596 Ok(Err(error)) => return Err(io_thread_error(phase, &error)),
3597 Err(RecvTimeoutError::Timeout) => self.require_child_running(phase)?,
3598 Err(RecvTimeoutError::Disconnected) => {
3599 return Err(ParserSupervisorError::IoThread {
3600 phase,
3601 message: "writer acknowledgement closed".to_owned(),
3602 });
3603 }
3604 }
3605 }
3606 }
3607
3608 fn wait_for_frame(
3610 &mut self,
3611 phase: &'static str,
3612 absolute_deadline: Instant,
3613 last_progress: Instant,
3614 no_progress_timeout: Duration,
3615 cancellation: &IndexCancellation,
3616 ) -> Result<Vec<u8>, ParserSupervisorError> {
3617 loop {
3618 poll_stop(
3619 phase,
3620 absolute_deadline,
3621 last_progress,
3622 no_progress_timeout,
3623 cancellation,
3624 )?;
3625 if let Some(event) = try_frame_event(&self.frame_reader.events)? {
3626 self.synchronize_frame_event(
3627 &event,
3628 phase,
3629 absolute_deadline,
3630 last_progress,
3631 no_progress_timeout,
3632 cancellation,
3633 )?;
3634 return self.finish_frame_event(event, phase);
3635 }
3636 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3637 self.enforce_memory_bound(phase, false)?;
3638 self.check_diagnostic_reader(
3639 phase,
3640 absolute_deadline,
3641 last_progress,
3642 no_progress_timeout,
3643 cancellation,
3644 )?;
3645 match self.frame_reader.events.recv_timeout(next_poll_wait(
3646 absolute_deadline,
3647 last_progress,
3648 no_progress_timeout,
3649 )) {
3650 Ok(event) => {
3651 self.synchronize_frame_event(
3652 &event,
3653 phase,
3654 absolute_deadline,
3655 last_progress,
3656 no_progress_timeout,
3657 cancellation,
3658 )?;
3659 return self.finish_frame_event(event, phase);
3660 }
3661 Err(RecvTimeoutError::Timeout) => {}
3662 Err(RecvTimeoutError::Disconnected) => {
3663 return Err(ParserSupervisorError::IoThread {
3664 phase,
3665 message: "stdout reader closed".to_owned(),
3666 });
3667 }
3668 }
3669 }
3670 }
3671
3672 fn synchronize_frame_event(
3674 &mut self,
3675 event: &FrameReaderEvent,
3676 phase: &'static str,
3677 absolute_deadline: Instant,
3678 last_progress: Instant,
3679 no_progress_timeout: Duration,
3680 cancellation: &IndexCancellation,
3681 ) -> Result<(), ParserSupervisorError> {
3682 if matches!(event, FrameReaderEvent::EndOfStream) {
3683 self.wait_for_diagnostic_termination(
3684 phase,
3685 absolute_deadline,
3686 last_progress,
3687 no_progress_timeout,
3688 cancellation,
3689 )
3690 } else {
3691 self.wait_for_diagnostic_fence(
3692 phase,
3693 absolute_deadline,
3694 last_progress,
3695 no_progress_timeout,
3696 cancellation,
3697 )
3698 }
3699 }
3700
3701 fn wait_for_diagnostic_termination(
3703 &mut self,
3704 phase: &'static str,
3705 absolute_deadline: Instant,
3706 last_progress: Instant,
3707 no_progress_timeout: Duration,
3708 cancellation: &IndexCancellation,
3709 ) -> Result<(), ParserSupervisorError> {
3710 loop {
3711 poll_stop(
3712 phase,
3713 absolute_deadline,
3714 last_progress,
3715 no_progress_timeout,
3716 cancellation,
3717 )?;
3718 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3719 self.enforce_memory_bound(phase, false)?;
3720 self.check_diagnostic_reader(
3721 phase,
3722 absolute_deadline,
3723 last_progress,
3724 no_progress_timeout,
3725 cancellation,
3726 )?;
3727 if thread_finished(self.diagnostic_reader.handle.as_ref()) {
3728 self.check_diagnostic_reader(
3729 phase,
3730 absolute_deadline,
3731 last_progress,
3732 no_progress_timeout,
3733 cancellation,
3734 )?;
3735 return Ok(());
3736 }
3737 thread::sleep(next_poll_wait(
3738 absolute_deadline,
3739 last_progress,
3740 no_progress_timeout,
3741 ));
3742 }
3743 }
3744
3745 fn wait_for_diagnostic_fence(
3747 &mut self,
3748 phase: &'static str,
3749 absolute_deadline: Instant,
3750 last_progress: Instant,
3751 no_progress_timeout: Duration,
3752 cancellation: &IndexCancellation,
3753 ) -> Result<(), ParserSupervisorError> {
3754 if self.pending_diagnostic_fences > 0 {
3755 self.pending_diagnostic_fences = self.pending_diagnostic_fences.saturating_sub(1);
3756 return Ok(());
3757 }
3758 loop {
3759 poll_stop(
3760 phase,
3761 absolute_deadline,
3762 last_progress,
3763 no_progress_timeout,
3764 cancellation,
3765 )?;
3766 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3767 match self.enforce_memory_bound(phase, false) {
3768 Ok(()) | Err(ParserSupervisorError::ChildExited { code: Some(0), .. }) => {}
3769 Err(error) => return Err(error),
3770 }
3771 match self.diagnostic_reader.events.recv_timeout(next_poll_wait(
3772 absolute_deadline,
3773 last_progress,
3774 no_progress_timeout,
3775 )) {
3776 Ok(DiagnosticReaderEvent::FenceObserved) => return Ok(()),
3777 Ok(DiagnosticReaderEvent::Failure(error)) => {
3778 self.termination_requested = true;
3779 return Err(diagnostic_failure_after_exit_observation(
3780 &mut self.child,
3781 phase,
3782 &error,
3783 absolute_deadline,
3784 last_progress,
3785 no_progress_timeout,
3786 cancellation,
3787 ));
3788 }
3789 Ok(DiagnosticReaderEvent::AdmissionAccepted) | Err(RecvTimeoutError::Timeout) => {}
3790 Err(RecvTimeoutError::Disconnected) => {
3791 return Err(ParserSupervisorError::IoThread {
3792 phase,
3793 message: "diagnostic reader closed before frame fence".to_owned(),
3794 });
3795 }
3796 }
3797 }
3798 }
3799
3800 fn finish_frame_event(
3802 &mut self,
3803 event: FrameReaderEvent,
3804 phase: &'static str,
3805 ) -> Result<Vec<u8>, ParserSupervisorError> {
3806 let result = frame_event_result(event, &mut self.child, phase);
3807 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
3808 if matches!(
3809 &result,
3810 Err(ParserSupervisorError::WindowsJobMemoryLimitExceeded { .. })
3811 ) {
3812 self.termination_requested = true;
3813 }
3814 result
3815 }
3816
3817 fn check_diagnostic_reader(
3819 &mut self,
3820 phase: &'static str,
3821 absolute_deadline: Instant,
3822 last_progress: Instant,
3823 no_progress_timeout: Duration,
3824 cancellation: &IndexCancellation,
3825 ) -> Result<(), ParserSupervisorError> {
3826 loop {
3827 match self.diagnostic_reader.events.try_recv() {
3828 Ok(DiagnosticReaderEvent::Failure(error)) => {
3829 self.termination_requested = true;
3830 return Err(diagnostic_failure_after_exit_observation(
3831 &mut self.child,
3832 phase,
3833 &error,
3834 absolute_deadline,
3835 last_progress,
3836 no_progress_timeout,
3837 cancellation,
3838 ));
3839 }
3840 Ok(DiagnosticReaderEvent::FenceObserved) => {
3841 self.pending_diagnostic_fences = self
3842 .pending_diagnostic_fences
3843 .checked_add(1)
3844 .ok_or_else(|| ParserSupervisorError::IoThread {
3845 phase,
3846 message: "diagnostic fence count overflowed".to_owned(),
3847 })?;
3848 }
3849 Ok(DiagnosticReaderEvent::AdmissionAccepted) => {}
3850 Err(TryRecvError::Empty | TryRecvError::Disconnected) => return Ok(()),
3851 }
3852 }
3853 }
3854
3855 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3857 fn enforce_memory_bound(
3858 &mut self,
3859 phase: &'static str,
3860 force: bool,
3861 ) -> Result<(), ParserSupervisorError> {
3862 let monitor_event = self
3863 .memory_monitor
3864 .as_ref()
3865 .and_then(|monitor| monitor.events.try_recv().ok());
3866 if let Some(event) = monitor_event {
3867 return Err(self.memory_monitor_error(phase, event));
3868 }
3869 if !force {
3870 return Ok(());
3871 }
3872 let process_id = self.child.id();
3873 let observation = match self.memory_observer.lock() {
3874 Ok(mut observer) => observer.observe(process_id),
3875 Err(_poisoned) => Err(io::Error::other("Linux memory observer lock was poisoned")),
3876 };
3877 let observation = match observation {
3878 Ok(observation) => observation,
3879 Err(source) => {
3880 let observer = Arc::clone(&self.memory_observer);
3881 let child = &mut self.child;
3882 let transition = resolve_linux_memory_exit_transition(
3883 source,
3884 SUPERVISOR_POLL_INTERVAL,
3885 || match observer.try_lock() {
3886 Ok(mut observer) => observer.observe(process_id),
3887 Err(std::sync::TryLockError::WouldBlock) => Err(io::Error::new(
3888 io::ErrorKind::WouldBlock,
3889 "Linux memory observer is busy",
3890 )),
3891 Err(std::sync::TryLockError::Poisoned(_poisoned)) => {
3892 Err(io::Error::other("Linux memory observer lock was poisoned"))
3893 }
3894 },
3895 || {
3896 child.try_wait().map(|status| {
3897 status.map(|status| LinuxChildExit {
3898 code: status.code(),
3899 })
3900 })
3901 },
3902 );
3903 match transition {
3904 Ok(LinuxMemoryObservation::Memory(observation)) => observation,
3905 Ok(LinuxMemoryObservation::ChildExited { code }) => {
3906 return Err(ParserSupervisorError::ChildExited { phase, code });
3907 }
3908 Err(source) => {
3909 self.termination_requested = true;
3910 let operation = ParserSupervisorError::ResidentMemoryObservationFailed {
3911 phase,
3912 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
3913 message: bounded_message(source.to_string()),
3914 };
3915 return Err(attach_cleanup(
3916 operation,
3917 kill_direct_child(&mut self.child),
3918 ));
3919 }
3920 }
3921 }
3922 };
3923 let Some(breach) = observation else {
3924 return Ok(());
3925 };
3926 self.termination_requested = true;
3927 let observation_interval_millis =
3928 u64::try_from(PARSER_LINUX_RSS_OBSERVATION_INTERVAL.as_millis()).unwrap_or(u64::MAX);
3929 let operation = ParserSupervisorError::ResidentMemoryLimitExceeded {
3930 phase,
3931 accounting: breach.accounting,
3932 observed_bytes: breach.observed_bytes,
3933 maximum_bytes: self.memory_limits.process_bytes,
3934 observation_interval_millis,
3935 };
3936 Err(attach_cleanup(
3937 operation,
3938 kill_direct_child(&mut self.child),
3939 ))
3940 }
3941
3942 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3944 fn memory_monitor_error(
3945 &mut self,
3946 phase: &'static str,
3947 event: LinuxMemoryMonitorEvent,
3948 ) -> ParserSupervisorError {
3949 let (operation, termination_error) = match event {
3950 LinuxMemoryMonitorEvent::Limit {
3951 breach,
3952 termination_error,
3953 } => {
3954 self.termination_requested = true;
3955 (
3956 ParserSupervisorError::ResidentMemoryLimitExceeded {
3957 phase,
3958 accounting: breach.accounting,
3959 observed_bytes: breach.observed_bytes,
3960 maximum_bytes: self.memory_limits.process_bytes,
3961 observation_interval_millis: u64::try_from(
3962 PARSER_LINUX_RSS_OBSERVATION_INTERVAL.as_millis(),
3963 )
3964 .unwrap_or(u64::MAX),
3965 },
3966 termination_error,
3967 )
3968 }
3969 LinuxMemoryMonitorEvent::ObservationFailed {
3970 accounting,
3971 message,
3972 } => {
3973 let transition = resolve_linux_memory_exit_transition(
3974 io::Error::other(message.clone()),
3975 SUPERVISOR_POLL_INTERVAL,
3976 || Err(io::Error::other(message.clone())),
3977 || {
3978 self.child.try_wait().map(|status| {
3979 status.map(|status| LinuxChildExit {
3980 code: status.code(),
3981 })
3982 })
3983 },
3984 );
3985 if let Ok(LinuxMemoryObservation::ChildExited { code }) = transition {
3986 return ParserSupervisorError::ChildExited { phase, code };
3987 }
3988 self.termination_requested = true;
3989 return attach_cleanup(
3990 ParserSupervisorError::ResidentMemoryObservationFailed {
3991 phase,
3992 accounting,
3993 message,
3994 },
3995 kill_direct_child(&mut self.child),
3996 );
3997 }
3998 };
3999 let Some(termination_error) = termination_error else {
4000 return operation;
4001 };
4002 let retry = kill_direct_child(&mut self.child);
4003 let initial = ParserSupervisorError::Cleanup {
4004 message: format!(
4005 "continuous memory monitor could not terminate the worker process group: {termination_error}"
4006 ),
4007 };
4008 let cleanup = match retry {
4009 Ok(()) => initial,
4010 Err(retry) => ParserSupervisorError::OperationAndCleanup {
4011 operation: Box::new(initial),
4012 cleanup: Box::new(retry),
4013 },
4014 };
4015 ParserSupervisorError::OperationAndCleanup {
4016 operation: Box::new(operation),
4017 cleanup: Box::new(cleanup),
4018 }
4019 }
4020
4021 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4023 fn observe_shutdown_memory(&mut self, failure: &mut Option<ParserSupervisorError>) {
4024 if failure.is_some() {
4025 return;
4026 }
4027 match self.enforce_memory_bound("shutdown", true) {
4028 Ok(()) | Err(ParserSupervisorError::ChildExited { .. }) => {}
4029 Err(error) => *failure = Some(error),
4030 }
4031 }
4032
4033 fn require_child_running(&mut self, phase: &'static str) -> Result<(), ParserSupervisorError> {
4035 match self
4036 .child
4037 .try_wait()
4038 .map_err(|source| ParserSupervisorError::IoThread {
4039 phase,
4040 message: source.to_string(),
4041 })? {
4042 None => Ok(()),
4043 Some(status) => Err(ParserSupervisorError::ChildExited {
4044 phase,
4045 code: status.code(),
4046 }),
4047 }
4048 }
4049
4050 fn shutdown(mut self) -> Result<(), ParserSupervisorError> {
4052 let mut failures = Vec::new();
4053 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4054 let mut memory_failure = None;
4055 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4056 if let Some(mut monitor) = self.memory_monitor.take() {
4057 match monitor.stop() {
4058 Ok(Some(event)) => {
4059 memory_failure = Some(self.memory_monitor_error("idle resident", event));
4060 }
4061 Ok(None) => {}
4062 Err(error) => failures.push(error.to_string()),
4063 }
4064 }
4065 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4066 self.observe_shutdown_memory(&mut memory_failure);
4067 self.writer.take();
4068 let graceful_deadline = Instant::now()
4069 .checked_add(SUPERVISOR_GRACEFUL_CLOSE)
4070 .unwrap_or_else(Instant::now);
4071 while self
4072 .writer_handle
4073 .as_ref()
4074 .is_some_and(|handle| !handle.is_finished())
4075 && Instant::now() < graceful_deadline
4076 {
4077 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4078 self.observe_shutdown_memory(&mut memory_failure);
4079 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4080 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4081 }
4082 let mut forced = self.termination_requested;
4083 let mut status = match self.child.try_wait() {
4084 Ok(status) => status,
4085 Err(source) => {
4086 failures.push(source.to_string());
4087 None
4088 }
4089 };
4090 while status.is_none() && Instant::now() < graceful_deadline {
4091 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4092 self.observe_shutdown_memory(&mut memory_failure);
4093 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4094 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4095 match self.child.try_wait() {
4096 Ok(observed) => status = observed,
4097 Err(source) => {
4098 failures.push(source.to_string());
4099 break;
4100 }
4101 }
4102 }
4103 if status.is_none() {
4104 forced = true;
4105 if let Err(error) = kill_direct_child(&mut self.child) {
4106 failures.push(error.to_string());
4107 }
4108 }
4109 let cleanup_deadline = Instant::now()
4110 .checked_add(SUPERVISOR_CLEANUP_TIMEOUT)
4111 .unwrap_or_else(Instant::now);
4112 while status.is_none() && Instant::now() < cleanup_deadline {
4113 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4114 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4115 match self.child.try_wait() {
4116 Ok(observed) => status = observed,
4117 Err(source) => {
4118 failures.push(source.to_string());
4119 break;
4120 }
4121 }
4122 }
4123 if status.is_none() {
4124 failures.push("direct child was not reaped within the cleanup deadline".to_owned());
4125 }
4126 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4127 if status.is_some() {
4128 let cleanup = match self.memory_observer.lock() {
4129 Ok(mut observer) => observer.cleanup(),
4130 Err(_poisoned) => Err(io::Error::other(
4131 "Linux memory observer lock was poisoned during cleanup",
4132 )),
4133 };
4134 if let Err(source) = cleanup {
4135 failures.push(format!(
4136 "delegated Linux memory-accounting cleanup failed: {source}"
4137 ));
4138 }
4139 }
4140
4141 while (!thread_finished(self.writer_handle.as_ref())
4142 || !thread_finished(self.frame_reader.handle.as_ref())
4143 || !thread_finished(self.diagnostic_reader.handle.as_ref()))
4144 && Instant::now() < cleanup_deadline
4145 {
4146 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4147 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4148 }
4149 if !thread_finished(self.writer_handle.as_ref())
4150 || !thread_finished(self.frame_reader.handle.as_ref())
4151 || !thread_finished(self.diagnostic_reader.handle.as_ref())
4152 {
4153 failures
4154 .push("protocol I/O threads did not drain within the cleanup deadline".to_owned());
4155 }
4156 if thread_finished(self.writer_handle.as_ref())
4157 && let Err(error) = join_unit_thread(self.writer_handle.take(), "writer")
4158 {
4159 failures.push(error.to_string());
4160 }
4161 if thread_finished(self.frame_reader.handle.as_ref())
4162 && let Err(error) = join_unit_thread(self.frame_reader.handle.take(), "stdout reader")
4163 {
4164 failures.push(error.to_string());
4165 }
4166 let diagnostics = if thread_finished(self.diagnostic_reader.handle.as_ref()) {
4167 match join_diagnostic_thread(self.diagnostic_reader.handle.take()) {
4168 Ok(diagnostics) => diagnostics,
4169 Err(error) => {
4170 failures.push(error.to_string());
4171 Vec::new()
4172 }
4173 }
4174 } else {
4175 Vec::new()
4176 };
4177 forced |= self.termination_requested;
4178 if !forced && status.as_ref().is_some_and(|status| !status.success()) {
4179 failures.push(format!(
4180 "direct child reported failed cleanup; diagnostic={}",
4181 bounded_diagnostic(&diagnostics)
4182 ));
4183 }
4184 if !forced && status.as_ref().is_some_and(ExitStatus::success) && !diagnostics.is_empty() {
4185 failures.push(format!(
4186 "direct child emitted unexpected diagnostic bytes: {}",
4187 bounded_diagnostic(&diagnostics)
4188 ));
4189 }
4190 let cleanup = (!failures.is_empty()).then(|| ParserSupervisorError::Cleanup {
4191 message: bounded_message(failures.join("; ")),
4192 });
4193 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4194 if let Some(operation) = memory_failure {
4195 return match cleanup {
4196 Some(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
4197 operation: Box::new(operation),
4198 cleanup: Box::new(cleanup),
4199 }),
4200 None => Err(operation),
4201 };
4202 }
4203 match cleanup {
4204 None => Ok(()),
4205 Some(cleanup) => Err(cleanup),
4206 }
4207 }
4208}
4209
4210#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4212#[expect(
4213 unsafe_code,
4214 reason = "Command has no safe descriptor-sanitizing hook; this pre-exec closure performs only async-signal-safe Linux syscalls"
4215)]
4216fn inherit_linux_authority_on_exec(command: &mut Command, authority: LinuxResidentLaunchAuthority) {
4217 use nix::libc;
4218 use std::os::unix::process::CommandExt;
4219
4220 let inherited = [
4221 authority.artifact_manifest.raw_fd(),
4222 authority.accepted_manifest.raw_fd(),
4223 authority.native_import_policy.raw_fd(),
4224 authority.grammar.raw_fd(),
4225 ];
4226 unsafe {
4231 command.pre_exec(move || {
4232 let _authority_guard = &authority;
4233 let result = libc::syscall(
4234 libc::SYS_close_range,
4235 3_u32,
4236 u32::MAX,
4237 libc::CLOSE_RANGE_CLOEXEC | libc::CLOSE_RANGE_UNSHARE,
4238 );
4239 if result != 0 {
4240 return Err(io::Error::last_os_error());
4241 }
4242 for descriptor in inherited {
4243 if libc::fcntl(descriptor, libc::F_SETFD, 0) != 0 {
4244 return Err(io::Error::last_os_error());
4245 }
4246 }
4247 Ok(())
4248 });
4249 }
4250}
4251
4252#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4254fn platform_command(
4255 launch: &VerifiedParserPackLaunch,
4256 authority: LinuxResidentLaunchAuthority,
4257 _memory_limits: ParserMemoryLimits,
4258) -> Result<Command, ParserSupervisorError> {
4259 use std::os::unix::process::CommandExt;
4260
4261 if launch.platform != PackPlatform::LinuxX86_64 {
4262 return Err(ParserSupervisorError::PayloadMismatch {
4263 path: launch.pack_root.clone(),
4264 reason: "Linux supervisor received another platform artifact",
4265 });
4266 }
4267 let worker_fd = authority.worker.raw_fd();
4268 let artifact_fd = authority.artifact_manifest.raw_fd();
4269 let accepted_fd = authority.accepted_manifest.raw_fd();
4270 let policy_fd = authority.native_import_policy.raw_fd();
4271 let grammar_fd = authority.grammar.raw_fd();
4272 let mut command = Command::new(format!("/proc/self/fd/{worker_fd}"));
4273 command
4274 .arg(SERVE_ARGUMENT)
4275 .arg(ARTIFACT_FD_ARGUMENT)
4276 .arg(artifact_fd.to_string())
4277 .arg(ACCEPTED_FD_ARGUMENT)
4278 .arg(accepted_fd.to_string())
4279 .arg(POLICY_FD_ARGUMENT)
4280 .arg(policy_fd.to_string())
4281 .arg(GRAMMAR_FD_ARGUMENT)
4282 .arg(grammar_fd.to_string())
4283 .current_dir(&launch.pack_root)
4284 .env_clear()
4285 .stdin(Stdio::piped())
4286 .stdout(Stdio::piped())
4287 .stderr(Stdio::piped())
4288 .process_group(0);
4289 inherit_linux_authority_on_exec(&mut command, authority);
4290 Ok(command)
4291}
4292
4293#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4295fn platform_command(
4296 launch: &VerifiedParserPackLaunch,
4297 memory_limits: ParserMemoryLimits,
4298) -> Result<Command, ParserSupervisorError> {
4299 use std::os::windows::process::CommandExt;
4300
4301 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
4302 if launch.platform != PackPlatform::WindowsX86_64 {
4303 return Err(ParserSupervisorError::PayloadMismatch {
4304 path: launch.pack_root.clone(),
4305 reason: "Windows supervisor received another platform artifact",
4306 });
4307 }
4308 let broker = launch.containment_broker.as_ref().ok_or_else(|| {
4309 ParserSupervisorError::PayloadMismatch {
4310 path: launch.pack_root.clone(),
4311 reason: "Windows artifact has no containment broker",
4312 }
4313 })?;
4314 let mut command = Command::new(broker);
4315 command
4316 .arg(BROKER_SERVE_ARGUMENT)
4317 .arg("--parent-pid")
4318 .arg(std::process::id().to_string())
4319 .arg("--process-memory-bytes")
4320 .arg(memory_limits.process_bytes.to_string())
4321 .arg("--job-memory-bytes")
4322 .arg(memory_limits.process_tree_bytes.to_string())
4323 .current_dir(&launch.pack_root)
4324 .env_clear()
4325 .stdin(Stdio::piped())
4326 .stdout(Stdio::piped())
4327 .stderr(Stdio::piped())
4328 .creation_flags(CREATE_NO_WINDOW);
4329 Ok(command)
4330}
4331
4332#[cfg(not(any(
4334 all(target_os = "linux", target_arch = "x86_64"),
4335 all(target_os = "windows", target_arch = "x86_64")
4336)))]
4337fn platform_command(
4338 _launch: &VerifiedParserPackLaunch,
4339 _memory_limits: ParserMemoryLimits,
4340) -> Result<Command, ParserSupervisorError> {
4341 Err(ParserSupervisorError::UnsupportedContainment {
4342 os: std::env::consts::OS,
4343 architecture: std::env::consts::ARCH,
4344 })
4345}
4346
4347const fn containment_for_platform(platform: PackPlatform) -> ParserContainmentKind {
4349 match platform {
4350 PackPlatform::LinuxX86_64 => ParserContainmentKind::LinuxLandlockSeccomp,
4351 PackPlatform::WindowsX86_64 => ParserContainmentKind::WindowsAppContainerJob,
4352 }
4353}
4354
4355fn fresh_session_identity() -> Result<ParserSessionIdentity, ParserSupervisorError> {
4357 let mut entropy = [0_u8; PARSER_SESSION_ENTROPY_BYTES];
4358 getrandom::fill(&mut entropy).map_err(|_source| ParserSupervisorError::EntropyUnavailable)?;
4359 Ok(ParserSessionIdentity::for_entropy(&entropy))
4360}
4361
4362fn fresh_diagnostic_fence() -> Result<DiagnosticFence, ParserSupervisorError> {
4364 let mut entropy = [0_u8; PARSER_DIAGNOSTIC_FENCE_BYTES];
4365 getrandom::fill(&mut entropy).map_err(|_source| ParserSupervisorError::EntropyUnavailable)?;
4366 Ok(DiagnosticFence(entropy))
4367}
4368
4369fn io_thread_error(phase: &'static str, error: &ParserIoThreadError) -> ParserSupervisorError {
4371 ParserSupervisorError::IoThread {
4372 phase,
4373 message: bounded_message(error.to_string()),
4374 }
4375}
4376
4377fn diagnostic_failure_after_exit_observation(
4379 child: &mut Child,
4380 phase: &'static str,
4381 error: &ParserIoThreadError,
4382 absolute_deadline: Instant,
4383 last_progress: Instant,
4384 no_progress_timeout: Duration,
4385 cancellation: &IndexCancellation,
4386) -> ParserSupervisorError {
4387 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4388 {
4389 let no_progress_deadline = last_progress
4390 .checked_add(no_progress_timeout)
4391 .unwrap_or(absolute_deadline);
4392 let observation_deadline = absolute_deadline.min(no_progress_deadline);
4393 loop {
4394 match child.try_wait() {
4395 Ok(Some(status))
4396 if status.code() == Some(PARSER_WINDOWS_BROKER_MEMORY_LIMIT_EXIT_CODE) =>
4397 {
4398 return ParserSupervisorError::WindowsJobMemoryLimitExceeded { phase };
4399 }
4400 Ok(Some(_)) | Err(_) => break,
4401 Ok(None) => {}
4402 }
4403 let now = Instant::now();
4404 if cancellation.is_cancelled() || now >= observation_deadline {
4405 break;
4406 }
4407 thread::sleep(SUPERVISOR_POLL_INTERVAL.min(observation_deadline.duration_since(now)));
4408 }
4409 }
4410 #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
4411 let _ = (
4412 child,
4413 absolute_deadline,
4414 last_progress,
4415 no_progress_timeout,
4416 cancellation,
4417 );
4418 io_thread_error(phase, error)
4419}
4420
4421fn bounded_message(mut message: String) -> String {
4423 let mut end = message.len().min(PARSER_MAX_STDERR_BYTES);
4424 while !message.is_char_boundary(end) {
4425 end = end.saturating_sub(1);
4426 }
4427 message.truncate(end);
4428 message
4429}
4430
4431fn bounded_diagnostic(bytes: &[u8]) -> String {
4433 bounded_message(String::from_utf8_lossy(bytes).into_owned())
4434}
4435
4436fn poll_stop(
4438 phase: &'static str,
4439 absolute_deadline: Instant,
4440 last_progress: Instant,
4441 no_progress_timeout: Duration,
4442 cancellation: &IndexCancellation,
4443) -> Result<(), ParserSupervisorError> {
4444 if cancellation.is_cancelled() {
4445 return Err(ParserSupervisorError::Cancelled { phase });
4446 }
4447 let now = Instant::now();
4448 if now >= absolute_deadline {
4449 return Err(ParserSupervisorError::DeadlineExceeded { phase });
4450 }
4451 if now.saturating_duration_since(last_progress) >= no_progress_timeout {
4452 return Err(ParserSupervisorError::NoProgress { phase });
4453 }
4454 Ok(())
4455}
4456
4457fn next_poll_wait(
4459 absolute_deadline: Instant,
4460 last_progress: Instant,
4461 no_progress_timeout: Duration,
4462) -> Duration {
4463 let now = Instant::now();
4464 let deadline_wait = absolute_deadline.saturating_duration_since(now);
4465 let progress_wait =
4466 no_progress_timeout.saturating_sub(now.saturating_duration_since(last_progress));
4467 SUPERVISOR_POLL_INTERVAL
4468 .min(deadline_wait)
4469 .min(progress_wait)
4470}
4471
4472fn try_frame_event(
4474 events: &Receiver<FrameReaderEvent>,
4475) -> Result<Option<FrameReaderEvent>, ParserSupervisorError> {
4476 match events.try_recv() {
4477 Ok(event) => Ok(Some(event)),
4478 Err(TryRecvError::Empty) => Ok(None),
4479 Err(TryRecvError::Disconnected) => Err(ParserSupervisorError::IoThread {
4480 phase: "stdout reader",
4481 message: "stdout reader closed without a terminal event".to_owned(),
4482 }),
4483 }
4484}
4485
4486fn frame_event_result(
4488 event: FrameReaderEvent,
4489 child: &mut Child,
4490 phase: &'static str,
4491) -> Result<Vec<u8>, ParserSupervisorError> {
4492 match event {
4493 FrameReaderEvent::Frame(frame) => Ok(frame),
4494 FrameReaderEvent::EndOfStream => {
4495 let status = wait_for_observed_exit(child, SUPERVISOR_POLL_INTERVAL)?;
4496 let code = status.and_then(|status| status.code());
4497 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4498 if code == Some(PARSER_WINDOWS_BROKER_MEMORY_LIMIT_EXIT_CODE) {
4499 return Err(ParserSupervisorError::WindowsJobMemoryLimitExceeded { phase });
4500 }
4501 Err(ParserSupervisorError::ChildExited { phase, code })
4502 }
4503 FrameReaderEvent::Failure(error) => Err(io_thread_error(phase, &error)),
4504 }
4505}
4506
4507fn wait_for_observed_exit(
4509 child: &mut Child,
4510 timeout: Duration,
4511) -> Result<Option<ExitStatus>, ParserSupervisorError> {
4512 let deadline = Instant::now()
4513 .checked_add(timeout)
4514 .unwrap_or_else(Instant::now);
4515 loop {
4516 if let Some(status) =
4517 child
4518 .try_wait()
4519 .map_err(|source| ParserSupervisorError::IoThread {
4520 phase: "child exit observation",
4521 message: source.to_string(),
4522 })?
4523 {
4524 return Ok(Some(status));
4525 }
4526 if Instant::now() >= deadline {
4527 return Ok(None);
4528 }
4529 thread::sleep(Duration::from_millis(1));
4530 }
4531}
4532
4533#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4535fn terminate_linux_process_group(process_id: u32) -> Result<LinuxProcessGroupTermination, String> {
4536 use nix::errno::Errno;
4537 use nix::sys::signal::{Signal, killpg};
4538 use nix::unistd::Pid;
4539
4540 let process_group = i32::try_from(process_id)
4541 .map_err(|_source| "worker process-group identity exceeds i32".to_owned())?;
4542 match killpg(Pid::from_raw(process_group), Signal::SIGKILL) {
4543 Ok(()) => Ok(LinuxProcessGroupTermination::Signalled),
4544 Err(Errno::ESRCH) => Ok(LinuxProcessGroupTermination::Absent),
4545 Err(source) => Err(source.to_string()),
4546 }
4547}
4548
4549#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4551fn kill_direct_child(child: &mut Child) -> Result<(), ParserSupervisorError> {
4552 if child
4553 .try_wait()
4554 .map_err(|source| ParserSupervisorError::Cleanup {
4555 message: source.to_string(),
4556 })?
4557 .is_some()
4558 {
4559 return Ok(());
4560 }
4561 match terminate_linux_process_group(child.id()) {
4562 Ok(LinuxProcessGroupTermination::Signalled) => Ok(()),
4563 Ok(LinuxProcessGroupTermination::Absent) => {
4564 if child
4565 .try_wait()
4566 .map_err(|source| ParserSupervisorError::Cleanup {
4567 message: source.to_string(),
4568 })?
4569 .is_none()
4570 {
4571 child
4572 .kill()
4573 .map_err(|source| ParserSupervisorError::Cleanup {
4574 message: format!(
4575 "worker process group was absent and direct-child termination failed: {source}"
4576 ),
4577 })?;
4578 }
4579 Ok(())
4580 }
4581 Err(group_error) => {
4582 let direct_error = child.kill().err().map(|error| error.to_string());
4583 Err(ParserSupervisorError::Cleanup {
4584 message: direct_error.map_or_else(
4585 || format!("process-group termination failed: {group_error}"),
4586 |direct_error| {
4587 format!(
4588 "process-group termination failed: {group_error}; direct-child termination failed: {direct_error}"
4589 )
4590 },
4591 ),
4592 })
4593 }
4594 }
4595}
4596
4597#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
4599fn kill_direct_child(child: &mut Child) -> Result<(), ParserSupervisorError> {
4600 if child
4601 .try_wait()
4602 .map_err(|source| ParserSupervisorError::Cleanup {
4603 message: source.to_string(),
4604 })?
4605 .is_none()
4606 {
4607 child
4608 .kill()
4609 .map_err(|source| ParserSupervisorError::Cleanup {
4610 message: source.to_string(),
4611 })?;
4612 }
4613 Ok(())
4614}
4615
4616fn cleanup_partial_launch(
4618 child: &mut Child,
4619 handles: Vec<JoinHandle<()>>,
4620 diagnostic_handle: Option<JoinHandle<Result<Vec<u8>, ParserIoThreadError>>>,
4621 frame_events: Option<Receiver<FrameReaderEvent>>,
4622 diagnostic_events: Option<Receiver<DiagnosticReaderEvent>>,
4623) -> Result<(), ParserSupervisorError> {
4624 drop(frame_events);
4625 drop(diagnostic_events);
4626 let mut failures = Vec::new();
4627 if let Err(error) = kill_direct_child(child) {
4628 failures.push(error.to_string());
4629 }
4630 let deadline = Instant::now()
4631 .checked_add(SUPERVISOR_CLEANUP_TIMEOUT)
4632 .unwrap_or_else(Instant::now);
4633 let mut reaped = false;
4634 while Instant::now() < deadline {
4635 match child.try_wait() {
4636 Ok(Some(_status)) => reaped = true,
4637 Ok(None) => {}
4638 Err(source) => {
4639 failures.push(source.to_string());
4640 break;
4641 }
4642 }
4643 if reaped
4644 && handles.iter().all(JoinHandle::is_finished)
4645 && thread_finished(diagnostic_handle.as_ref())
4646 {
4647 break;
4648 }
4649 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4650 }
4651 if !reaped {
4652 failures.push("incomplete direct child was not reaped".to_owned());
4653 }
4654 for handle in handles {
4655 if !handle.is_finished() {
4656 failures.push("incomplete launch thread did not terminate".to_owned());
4657 } else if handle.join().is_err() {
4658 failures.push("incomplete launch thread panicked".to_owned());
4659 }
4660 }
4661 if thread_finished(diagnostic_handle.as_ref()) {
4662 if let Err(error) = join_diagnostic_thread(diagnostic_handle) {
4663 failures.push(error.to_string());
4664 }
4665 } else if diagnostic_handle.is_some() {
4666 failures.push("incomplete diagnostic thread did not terminate".to_owned());
4667 }
4668 if failures.is_empty() {
4669 Ok(())
4670 } else {
4671 Err(ParserSupervisorError::Cleanup {
4672 message: bounded_message(failures.join("; ")),
4673 })
4674 }
4675}
4676
4677fn attach_cleanup(
4679 operation: ParserSupervisorError,
4680 cleanup: Result<(), ParserSupervisorError>,
4681) -> ParserSupervisorError {
4682 match cleanup {
4683 Ok(()) => operation,
4684 Err(cleanup) => ParserSupervisorError::OperationAndCleanup {
4685 operation: Box::new(operation),
4686 cleanup: Box::new(cleanup),
4687 },
4688 }
4689}
4690
4691fn drain_reader_events(
4693 frames: &Receiver<FrameReaderEvent>,
4694 diagnostics: &Receiver<DiagnosticReaderEvent>,
4695) {
4696 while frames.try_recv().is_ok() {}
4697 while diagnostics.try_recv().is_ok() {}
4698}
4699
4700fn thread_finished<T>(handle: Option<&JoinHandle<T>>) -> bool {
4702 handle.is_none_or(JoinHandle::is_finished)
4703}
4704
4705fn join_unit_thread(
4707 handle: Option<JoinHandle<()>>,
4708 name: &'static str,
4709) -> Result<(), ParserSupervisorError> {
4710 let Some(handle) = handle else {
4711 return Ok(());
4712 };
4713 handle
4714 .join()
4715 .map_err(|_panic| ParserSupervisorError::Cleanup {
4716 message: format!("{name} thread panicked"),
4717 })
4718}
4719
4720fn join_diagnostic_thread(
4722 handle: Option<JoinHandle<Result<Vec<u8>, ParserIoThreadError>>>,
4723) -> Result<Vec<u8>, ParserSupervisorError> {
4724 let Some(handle) = handle else {
4725 return Ok(Vec::new());
4726 };
4727 let result = handle
4728 .join()
4729 .map_err(|_panic| ParserSupervisorError::Cleanup {
4730 message: "diagnostic reader thread panicked".to_owned(),
4731 })?;
4732 result.map_err(|error| ParserSupervisorError::Cleanup {
4733 message: bounded_message(error.to_string()),
4734 })
4735}
4736
4737pub struct OptionalParserSupervisor {
4739 pack_root: PathBuf,
4741 launch: VerifiedParserPackLaunch,
4743 memory_limits: ParserMemoryLimits,
4745 resident: Option<ResidentParserSession>,
4747}
4748
4749impl OptionalParserSupervisor {
4750 pub fn open(pack_root: impl AsRef<Path>) -> Result<Self, ParserSupervisorError> {
4759 Self::open_with_memory_limits(pack_root, ParserMemoryLimits::PRODUCTION)
4760 }
4761
4762 fn open_with_memory_limits(
4764 pack_root: impl AsRef<Path>,
4765 memory_limits: ParserMemoryLimits,
4766 ) -> Result<Self, ParserSupervisorError> {
4767 let pack_root = pack_root.as_ref().to_path_buf();
4768 let launch = VerifiedParserPackLaunch::load(&pack_root)?;
4769 Ok(Self {
4770 pack_root,
4771 launch,
4772 memory_limits: memory_limits.checked()?,
4773 resident: None,
4774 })
4775 }
4776
4777 #[must_use]
4779 pub(crate) const fn artifact_identity(&self) -> &ParserArtifactIdentity {
4780 &self.launch.artifact
4781 }
4782
4783 #[must_use]
4785 pub(crate) fn pack_root(&self) -> &Path {
4786 &self.pack_root
4787 }
4788
4789 #[must_use]
4791 pub(crate) fn accepts_language(&self, language_id: &str) -> bool {
4792 self.launch
4793 .accepted_grammars
4794 .binary_search_by(|candidate| candidate.as_str().cmp(language_id))
4795 .is_ok()
4796 }
4797
4798 pub fn parse(
4812 &mut self,
4813 language_id: &str,
4814 source: &[u8],
4815 limits: ParserRequestLimits,
4816 absolute_deadline: Instant,
4817 no_progress_timeout: Duration,
4818 cancellation: &IndexCancellation,
4819 ) -> Result<ParserCompletionEvidence, ParserSupervisorError> {
4820 let last_progress = Instant::now();
4821 poll_stop(
4822 "request admission",
4823 absolute_deadline,
4824 last_progress,
4825 no_progress_timeout,
4826 cancellation,
4827 )?;
4828 let source_identity = ParserSourceIdentity::for_bytes(source)?;
4829 self.refresh_changed_artifact(
4830 language_id,
4831 last_progress,
4832 absolute_deadline,
4833 no_progress_timeout,
4834 cancellation,
4835 )?;
4836 if let Some(resident) = self.resident.as_ref() {
4837 let grammar_changed = self
4838 .launch
4839 .require_grammar(language_id)
4840 .map_or(true, |grammar| resident.grammar != grammar);
4841 if grammar_changed {
4842 self.shutdown_resident()?;
4843 }
4844 }
4845 let mut resident_opened = false;
4846 if self.resident.is_none() {
4847 let grammar = self.launch.require_grammar(language_id)?;
4848 self.resident = Some(ResidentParserSession::launch(
4849 &self.launch,
4850 grammar,
4851 self.memory_limits,
4852 last_progress,
4853 absolute_deadline,
4854 no_progress_timeout,
4855 cancellation,
4856 )?);
4857 resident_opened = true;
4858 }
4859 let request_last_progress = if resident_opened {
4860 Instant::now()
4861 } else {
4862 last_progress
4863 };
4864 let result = self
4865 .resident
4866 .as_mut()
4867 .ok_or_else(|| ParserSupervisorError::IoThread {
4868 phase: "request admission",
4869 message: "resident session was not retained".to_owned(),
4870 })?
4871 .parse(
4872 source,
4873 source_identity,
4874 limits,
4875 request_last_progress,
4876 absolute_deadline,
4877 no_progress_timeout,
4878 cancellation,
4879 );
4880 match result {
4881 Ok(evidence) => Ok(evidence),
4882 Err(operation) => {
4883 if operation.is_caller_stop()
4884 && let Some(resident) = self.resident.as_mut()
4885 {
4886 resident.termination_requested = true;
4887 }
4888 match self.take_and_shutdown_resident() {
4889 Ok(()) => Err(operation),
4890 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
4891 operation: Box::new(operation),
4892 cleanup: Box::new(cleanup),
4893 }),
4894 }
4895 }
4896 }
4897 }
4898
4899 pub fn shutdown(&mut self) -> Result<(), ParserSupervisorError> {
4906 self.shutdown_resident()
4907 }
4908
4909 fn refresh_changed_artifact(
4911 &mut self,
4912 language_id: &str,
4913 last_progress: Instant,
4914 absolute_deadline: Instant,
4915 no_progress_timeout: Duration,
4916 cancellation: &IndexCancellation,
4917 ) -> Result<(), ParserSupervisorError> {
4918 let control = ArtifactIoControl {
4919 absolute_deadline,
4920 last_progress,
4921 no_progress_timeout,
4922 cancellation,
4923 };
4924 let probe = self.launch.currentness_probe(language_id);
4925 let current = match run_bounded_artifact_currentness(probe, &control) {
4926 Ok(current) => current,
4927 Err(operation) => {
4928 return Err(attach_cleanup(operation, self.take_and_shutdown_resident()));
4929 }
4930 };
4931 if current {
4932 return Ok(());
4933 }
4934 self.shutdown_resident()?;
4935 let refreshed = VerifiedParserPackLaunch::load_controlled(
4936 &self.pack_root,
4937 language_id,
4938 last_progress,
4939 absolute_deadline,
4940 no_progress_timeout,
4941 cancellation,
4942 )?;
4943 self.replace_verified_launch(refreshed)
4944 }
4945
4946 fn replace_verified_launch(
4948 &mut self,
4949 refreshed: VerifiedParserPackLaunch,
4950 ) -> Result<(), ParserSupervisorError> {
4951 if refreshed.artifact != self.launch.artifact {
4952 return Err(ParserSupervisorError::PayloadMismatch {
4953 path: self.pack_root.join(ARTIFACT_MANIFEST_FILE_NAME),
4954 reason: "artifact identity changed inside its immutable slot",
4955 });
4956 }
4957 self.launch = refreshed;
4958 Ok(())
4959 }
4960
4961 fn shutdown_resident(&mut self) -> Result<(), ParserSupervisorError> {
4963 self.take_and_shutdown_resident()
4964 }
4965
4966 fn take_and_shutdown_resident(&mut self) -> Result<(), ParserSupervisorError> {
4968 match self.resident.take() {
4969 Some(resident) => resident.shutdown(),
4970 None => Ok(()),
4971 }
4972 }
4973}
4974
4975pub fn probe_optional_parser_memory_boundary(
4986 pack_root: impl AsRef<Path>,
4987 logical: &OptionalParserPackManifest,
4988) -> Result<ParserPackMemoryProbe, ParserSupervisorError> {
4989 let pack_root = pack_root.as_ref();
4990 logical.validate()?;
4991 let grammar =
4992 logical
4993 .grammars()
4994 .first()
4995 .ok_or_else(|| ParserSupervisorError::PayloadMismatch {
4996 path: pack_root.to_path_buf(),
4997 reason: "optional parser manifest has no grammar for the memory probe",
4998 })?;
4999 let platform = host_pack_platform().ok_or(ParserSupervisorError::UnsupportedContainment {
5000 os: std::env::consts::OS,
5001 architecture: std::env::consts::ARCH,
5002 })?;
5003 let process_bytes = match platform {
5004 PackPlatform::LinuxX86_64 => OPTIONAL_PARSER_PACK_LINUX_MEMORY_PROBE_BYTES,
5005 PackPlatform::WindowsX86_64 => OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES,
5006 };
5007 let memory_limits = ParserMemoryLimits {
5008 process_bytes,
5009 process_tree_bytes: process_bytes,
5010 }
5011 .checked()?;
5012 let probe_source = memory_probe_source(platform, grammar.fixtures.positive.source.as_bytes());
5013 let mut supervisor =
5014 OptionalParserSupervisor::open_with_memory_limits(pack_root, memory_limits)?;
5015 let limits = ParserRequestLimits::new(
5016 PARSER_MAX_OUTPUT_BYTES,
5017 PARSER_MAX_NODE_COUNT,
5018 PARSER_MAX_TREE_DEPTH,
5019 )?;
5020 let deadline = Instant::now()
5021 .checked_add(ARTIFACT_ADMISSION_TIMEOUT)
5022 .ok_or(ParserSupervisorError::DeadlineExceeded {
5023 phase: "memory probe deadline calculation",
5024 })?;
5025 let operation = supervisor.parse(
5026 &grammar.language_id,
5027 &probe_source,
5028 limits,
5029 deadline,
5030 ARTIFACT_ADMISSION_NO_PROGRESS_TIMEOUT,
5031 &IndexCancellation::new(),
5032 );
5033 let cleanup = supervisor.shutdown();
5034 let failure = match operation {
5035 Ok(_evidence) => match cleanup {
5036 Ok(()) => {
5037 return Err(ParserSupervisorError::MemoryProbeDidNotBreach { process_bytes });
5038 }
5039 Err(error) => error,
5040 },
5041 Err(error) => attach_cleanup(error, cleanup),
5042 };
5043
5044 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5045 if let ParserSupervisorError::ResidentMemoryLimitExceeded {
5046 accounting,
5047 observed_bytes,
5048 maximum_bytes,
5049 observation_interval_millis,
5050 ..
5051 } = &failure
5052 {
5053 let (control, interval, peak, overshoot) = match *accounting {
5054 ParserMemoryAccountingKind::LinuxCgroupV2 => {
5055 (ParserPackMemoryControl::LinuxCgroupV2, None, None, None)
5056 }
5057 ParserMemoryAccountingKind::LinuxProcStatus => (
5058 ParserPackMemoryControl::LinuxProcStatus,
5059 Some(*observation_interval_millis),
5060 Some(*observed_bytes),
5061 Some(observed_bytes.saturating_sub(*maximum_bytes)),
5062 ),
5063 };
5064 return Ok(ParserPackMemoryProbe {
5065 control,
5066 process_limit_bytes: *maximum_bytes,
5067 process_tree_limit_bytes: memory_limits.process_tree_bytes,
5068 observation_interval_millis: interval,
5069 peak_observed_bytes: peak,
5070 maximum_observed_overshoot_bytes: overshoot,
5071 limit_enforced: ParserPackVerifiedControl::Verified,
5072 process_tree_cleaned: ParserPackVerifiedControl::Verified,
5073 });
5074 }
5075
5076 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5077 if matches!(
5078 &failure,
5079 ParserSupervisorError::WindowsJobMemoryLimitExceeded { .. }
5080 ) {
5081 return Ok(ParserPackMemoryProbe {
5082 control: ParserPackMemoryControl::WindowsJobObject,
5083 process_limit_bytes: memory_limits.process_bytes,
5084 process_tree_limit_bytes: memory_limits.process_tree_bytes,
5085 observation_interval_millis: None,
5086 peak_observed_bytes: None,
5087 maximum_observed_overshoot_bytes: None,
5088 limit_enforced: ParserPackVerifiedControl::Verified,
5089 process_tree_cleaned: ParserPackVerifiedControl::Verified,
5090 });
5091 }
5092
5093 Err(failure)
5094}
5095
5096fn memory_probe_source(platform: PackPlatform, fixture: &[u8]) -> Vec<u8> {
5098 match platform {
5099 PackPlatform::LinuxX86_64 => fixture.to_vec(),
5100 PackPlatform::WindowsX86_64 => fixture
5101 .iter()
5102 .copied()
5103 .cycle()
5104 .take(WINDOWS_MEMORY_PROBE_SOURCE_BYTES.min(PARSER_MAX_SOURCE_BYTES as usize))
5105 .collect(),
5106 }
5107}
5108
5109pub fn admit_optional_parser_artifact(
5121 mut supervisor: OptionalParserSupervisor,
5122 logical: &OptionalParserPackManifest,
5123) -> Result<(), ParserSupervisorError> {
5124 let cancellation = IndexCancellation::new();
5125 let limits = ParserRequestLimits::new(
5126 PARSER_MAX_OUTPUT_BYTES,
5127 PARSER_MAX_NODE_COUNT,
5128 PARSER_MAX_TREE_DEPTH,
5129 )?;
5130 let aggregate_deadline = Instant::now()
5131 .checked_add(ARTIFACT_ADMISSION_AGGREGATE_TIMEOUT)
5132 .ok_or(ParserSupervisorError::DeadlineExceeded {
5133 phase: "artifact admission aggregate deadline calculation",
5134 })?;
5135 let operation = (|| {
5136 for grammar in logical.grammars() {
5137 let grammar_deadline = Instant::now()
5138 .checked_add(ARTIFACT_ADMISSION_TIMEOUT)
5139 .ok_or(ParserSupervisorError::DeadlineExceeded {
5140 phase: "artifact admission deadline calculation",
5141 })?;
5142 let deadline = grammar_deadline.min(aggregate_deadline);
5143 for (fixture, expected) in [
5144 (&grammar.fixtures.positive, false),
5145 (&grammar.fixtures.negative, true),
5146 ] {
5147 let evidence = supervisor.parse(
5148 &grammar.language_id,
5149 fixture.source.as_bytes(),
5150 limits,
5151 deadline,
5152 ARTIFACT_ADMISSION_NO_PROGRESS_TIMEOUT,
5153 &cancellation,
5154 )?;
5155 let actual = evidence.root_has_error();
5156 if actual != expected {
5157 return Err(ParserSupervisorError::FixtureExpectationMismatch {
5158 language_id: grammar.language_id.clone(),
5159 case_name: fixture.case_name.clone(),
5160 actual,
5161 expected,
5162 });
5163 }
5164 }
5165 }
5166 Ok(())
5167 })();
5168 let cleanup = supervisor.shutdown();
5169 match operation {
5170 Ok(()) => cleanup,
5171 Err(operation) => Err(attach_cleanup(operation, cleanup)),
5172 }
5173}
5174
5175impl Drop for OptionalParserSupervisor {
5176 fn drop(&mut self) {
5177 drop(self.take_and_shutdown_resident());
5178 }
5179}
5180
5181#[cfg(test)]
5183#[allow(dead_code)]
5184pub(crate) fn run_adversarial_process_suite(peer: &Path) -> io::Result<()> {
5185 #[derive(Clone, Copy)]
5186 enum ExpectedFailure {
5187 Cancelled,
5188 Deadline(&'static str),
5189 InvalidAdmission,
5190 Io,
5191 NoProgress,
5192 Progress(&'static str),
5193 Ready(&'static str),
5194 Response(&'static str),
5195 Limit(&'static str),
5196 InvalidControl(ParserFrameKind),
5197 Worker(ParserFailureCode),
5198 }
5199
5200 const RECOVERY_ALLOWANCE_PROBE_AGE: Duration = Duration::from_millis(550);
5201
5202 struct Case {
5203 scenario: &'static str,
5204 expected: ExpectedFailure,
5205 source_bytes: usize,
5206 cancel_before_launch: bool,
5207 cancellation_after_launch: Option<Duration>,
5208 deadline: Duration,
5209 deadline_after_launch: Option<Duration>,
5210 no_progress: Duration,
5211 limits: ParserRequestLimits,
5212 }
5213
5214 fn default_limits() -> io::Result<ParserRequestLimits> {
5215 ParserRequestLimits::new(4 * 1024, 16, 16)
5216 .map_err(|error| io::Error::other(error.to_string()))
5217 }
5218
5219 fn case(scenario: &'static str, expected: ExpectedFailure) -> io::Result<Case> {
5220 Ok(Case {
5221 scenario,
5222 expected,
5223 source_bytes: 32,
5224 cancel_before_launch: false,
5225 cancellation_after_launch: None,
5226 deadline: Duration::from_secs(2),
5227 deadline_after_launch: None,
5228 no_progress: Duration::from_millis(500),
5229 limits: default_limits()?,
5230 })
5231 }
5232
5233 fn error_matches(error: &ParserSupervisorError, expected: ExpectedFailure) -> bool {
5234 match (error, expected) {
5235 (ParserSupervisorError::Cancelled { .. }, ExpectedFailure::Cancelled)
5236 | (ParserSupervisorError::InvalidAdmission, ExpectedFailure::InvalidAdmission)
5237 | (ParserSupervisorError::IoThread { .. }, ExpectedFailure::Io)
5238 | (ParserSupervisorError::NoProgress { .. }, ExpectedFailure::NoProgress) => true,
5239 (
5240 ParserSupervisorError::DeadlineExceeded { .. },
5241 ExpectedFailure::Deadline(expected_phase),
5242 ) => adversarial_deadline_matches(error, expected_phase),
5243 (
5244 ParserSupervisorError::Protocol {
5245 source: ParserProtocolError::ProgressRegression { field },
5246 },
5247 ExpectedFailure::Progress(expected),
5248 )
5249 | (
5250 ParserSupervisorError::Protocol {
5251 source: ParserProtocolError::ReadyIdentityMismatch { field },
5252 },
5253 ExpectedFailure::Ready(expected),
5254 )
5255 | (
5256 ParserSupervisorError::Protocol {
5257 source: ParserProtocolError::ResponseIdentityMismatch { field },
5258 },
5259 ExpectedFailure::Response(expected),
5260 )
5261 | (
5262 ParserSupervisorError::Protocol {
5263 source: ParserProtocolError::RequestLimitExceeded { field, .. },
5264 },
5265 ExpectedFailure::Limit(expected),
5266 ) => field == &expected,
5267 (ParserSupervisorError::WorkerFailure { code }, ExpectedFailure::Worker(expected)) => {
5268 code == &expected
5269 }
5270 (
5271 ParserSupervisorError::Protocol {
5272 source: ParserProtocolError::InvalidControlJson { kind, .. },
5273 },
5274 ExpectedFailure::InvalidControl(expected),
5275 ) => kind == &expected,
5276 _ => false,
5277 }
5278 }
5279
5280 fn command_for(peer: &Path, scenario: &str) -> io::Result<Command> {
5281 let current_dir = peer
5282 .parent()
5283 .ok_or_else(|| io::Error::other("hostile peer path has no parent"))?;
5284 let mut command = Command::new(peer);
5285 command
5286 .arg("--peer")
5287 .arg(scenario)
5288 .current_dir(current_dir)
5289 .env_clear()
5290 .stdin(Stdio::piped())
5291 .stdout(Stdio::piped())
5292 .stderr(Stdio::piped());
5293 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5294 {
5295 use std::os::unix::process::CommandExt;
5296
5297 command.process_group(0);
5298 }
5299 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5300 {
5301 use std::os::windows::process::CommandExt;
5302
5303 command.creation_flags(0x0800_0000);
5304 }
5305 Ok(command)
5306 }
5307
5308 fn test_launch(peer: &Path) -> io::Result<VerifiedParserPackLaunch> {
5309 let pack_root = peer
5310 .parent()
5311 .ok_or_else(|| io::Error::other("hostile peer path has no parent"))?
5312 .to_path_buf();
5313 let platform = host_pack_platform()
5314 .ok_or_else(|| io::Error::other("host has no optional-parser containment target"))?;
5315 Ok(VerifiedParserPackLaunch {
5316 pack_root: pack_root.clone(),
5317 platform,
5318 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5319 containment_broker: Some(peer.to_path_buf()),
5320 accepted_grammars: vec!["hostile".to_owned()],
5321 artifact: ParserArtifactIdentity::for_bytes(b"parser-supervisor-hostile-peer"),
5322 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5323 artifact_manifest_bytes: b"parser-supervisor-hostile-peer".to_vec(),
5324 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5325 accepted_manifest_bytes: Vec::new(),
5326 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5327 native_import_policy_bytes: Vec::new(),
5328 artifact_manifest: FileObservation::unavailable(
5329 pack_root.join(ARTIFACT_MANIFEST_FILE_NAME),
5330 ),
5331 payloads: Vec::new(),
5332 currentness_blocker: None,
5333 })
5334 }
5335
5336 fn require_reused_resident_payload_revalidation(peer: &Path) -> io::Result<()> {
5337 let temp = tempfile::tempdir()?;
5338 let accepted_bytes = b"accepted-manifest";
5339 fs::write(
5340 temp.path().join(ARTIFACT_MANIFEST_FILE_NAME),
5341 b"parser-supervisor-hostile-peer",
5342 )?;
5343 fs::write(
5344 temp.path().join(ACCEPTED_MANIFEST_FILE_NAME),
5345 accepted_bytes,
5346 )?;
5347 let payload_path = temp.path().join("hostile-grammar");
5348 fs::write(&payload_path, b"trusted")?;
5349 let modified = fs::metadata(&payload_path)?.modified()?;
5350
5351 let mut launch = test_launch(peer)?;
5352 launch.pack_root = temp.path().to_path_buf();
5353 launch.artifact_manifest =
5354 FileObservation::capture(temp.path().join(ARTIFACT_MANIFEST_FILE_NAME))
5355 .map_err(|error| io::Error::other(error.to_string()))?;
5356 launch.payloads = vec![PayloadObservation {
5357 file: FileObservation::capture(payload_path.clone())
5358 .map_err(|error| io::Error::other(error.to_string()))?,
5359 role: ParserPackPayloadRole::GrammarLibrary {
5360 language_id: "hostile".to_owned(),
5361 },
5362 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5363 bytes: 7,
5364 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5365 sha256: encode_sha256(Sha256::digest(b"trusted")),
5366 }];
5367 let grammar = ParserLanguageIdentity::new("hostile")
5368 .map_err(|error| io::Error::other(error.to_string()))?;
5369 let cancellation = IndexCancellation::new();
5370 let deadline = Instant::now()
5371 .checked_add(Duration::from_secs(2))
5372 .ok_or_else(|| io::Error::other("resident reuse deadline overflow"))?;
5373 let resident = ResidentParserSession::launch_command(
5374 &launch,
5375 grammar,
5376 ParserMemoryLimits::PRODUCTION,
5377 Instant::now(),
5378 deadline,
5379 ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS,
5380 &cancellation,
5381 command_for(peer, "idle-close")?,
5382 )
5383 .map_err(|error| {
5384 io::Error::other(format!(
5385 "resident reuse test launch failed before mutation: {error:?}"
5386 ))
5387 })?;
5388 let mut supervisor = OptionalParserSupervisor {
5389 pack_root: temp.path().to_path_buf(),
5390 launch,
5391 memory_limits: ParserMemoryLimits::PRODUCTION,
5392 resident: Some(resident),
5393 };
5394
5395 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
5396 let (release_sender, release_receiver) = mpsc::sync_channel(1);
5397 supervisor.launch.currentness_blocker = Some(std::sync::Arc::new(MetadataProbeBlocker {
5398 entered: entered_sender,
5399 release: std::sync::Mutex::new(release_receiver),
5400 }));
5401 let blocked_cancellation = IndexCancellation::new();
5402 let caller_cancellation = blocked_cancellation.clone();
5403 let blocked_deadline = Instant::now()
5404 .checked_add(Duration::from_secs(5))
5405 .ok_or_else(|| io::Error::other("blocked currentness deadline overflow"))?;
5406 let blocked_limits = default_limits()?;
5407 let (result_sender, result_receiver) = mpsc::sync_channel(1);
5408 let caller = thread::spawn(move || {
5409 let result = supervisor.parse(
5410 "hostile",
5411 &[b'x'; 32],
5412 blocked_limits,
5413 blocked_deadline,
5414 Duration::from_secs(5),
5415 &caller_cancellation,
5416 );
5417 let _send_result = result_sender.send((result, supervisor));
5418 });
5419 entered_receiver
5420 .recv_timeout(Duration::from_secs(1))
5421 .map_err(|source| io::Error::other(source.to_string()))?;
5422 blocked_cancellation.cancel();
5423 let (blocked_result, returned_supervisor) = result_receiver
5424 .recv_timeout(Duration::from_secs(1))
5425 .map_err(|source| io::Error::other(source.to_string()))?;
5426 if !matches!(
5427 blocked_result,
5428 Err(ParserSupervisorError::Cancelled {
5429 phase: ARTIFACT_IO_PHASE
5430 })
5431 ) {
5432 return Err(io::Error::other(format!(
5433 "blocked currentness returned the wrong result: {blocked_result:?}"
5434 )));
5435 }
5436 if returned_supervisor.resident.is_some() {
5437 return Err(io::Error::other(
5438 "blocked currentness retained the canceled resident",
5439 ));
5440 }
5441 if ArtifactIoLease::acquire().is_ok() {
5442 return Err(io::Error::other(
5443 "blocked currentness admitted a second artifact reader",
5444 ));
5445 }
5446 release_sender
5447 .send(())
5448 .map_err(|_closed| io::Error::other("metadata-probe release receiver closed"))?;
5449 caller
5450 .join()
5451 .map_err(|_panic| io::Error::other("blocked currentness caller panicked"))?;
5452 let permit_deadline = Instant::now() + Duration::from_secs(1);
5453 while ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) && Instant::now() < permit_deadline {
5454 thread::yield_now();
5455 }
5456 if ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) {
5457 return Err(io::Error::other(
5458 "blocked currentness did not release the artifact reader",
5459 ));
5460 }
5461
5462 supervisor = returned_supervisor;
5463 supervisor.launch.currentness_blocker = None;
5464 let cancellation = IndexCancellation::new();
5465 let deadline = Instant::now()
5466 .checked_add(Duration::from_secs(2))
5467 .ok_or_else(|| io::Error::other("resident reuse deadline overflow"))?;
5468 let grammar = ParserLanguageIdentity::new("hostile")
5469 .map_err(|error| io::Error::other(error.to_string()))?;
5470 supervisor.resident = Some(
5471 ResidentParserSession::launch_command(
5472 &supervisor.launch,
5473 grammar,
5474 ParserMemoryLimits::PRODUCTION,
5475 Instant::now(),
5476 deadline,
5477 ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS,
5478 &cancellation,
5479 command_for(peer, "idle-close")?,
5480 )
5481 .map_err(|error| {
5482 io::Error::other(format!(
5483 "resident reuse test relaunch failed after blocked currentness: {error:?}"
5484 ))
5485 })?,
5486 );
5487
5488 let mutation = fs::write(&payload_path, b"mutated");
5489 #[cfg(windows)]
5490 if mutation.is_err() {
5491 if fs::read(&payload_path)? != b"trusted" {
5492 return Err(io::Error::other(
5493 "Windows write guard reported failure after changing payload bytes",
5494 ));
5495 }
5496 supervisor
5497 .shutdown()
5498 .map_err(|error| io::Error::other(error.to_string()))?;
5499 return Ok(());
5500 }
5501 mutation?;
5502 File::options()
5503 .write(true)
5504 .open(&payload_path)?
5505 .set_times(fs::FileTimes::new().set_modified(modified))?;
5506 if fs::metadata(&payload_path)?.len() != 7
5507 || fs::metadata(&payload_path)?.modified()? != modified
5508 {
5509 return Err(io::Error::other(
5510 "resident reuse mutation did not preserve size and modification time",
5511 ));
5512 }
5513
5514 let source = vec![b'x'; 32];
5515 let operation = supervisor.parse(
5516 "hostile",
5517 &source,
5518 default_limits()?,
5519 deadline,
5520 Duration::from_millis(150),
5521 &cancellation,
5522 );
5523 let Err(error) = operation else {
5524 return Err(io::Error::other(
5525 "mutated launch payload was accepted by a reused resident",
5526 ));
5527 };
5528 if error.has_mandatory_cleanup_failure() {
5529 return Err(io::Error::other(format!(
5530 "mutated launch payload did not cleanly destroy the resident: {error:?}"
5531 )));
5532 }
5533 if supervisor.resident.is_some() {
5534 return Err(io::Error::other(
5535 "mutated launch payload retained the resident session",
5536 ));
5537 }
5538 Ok(())
5539 }
5540
5541 fn operate(
5542 peer: &Path,
5543 case: &Case,
5544 ) -> Result<ParserCompletionEvidence, ParserSupervisorError> {
5545 let launch = test_launch(peer).map_err(|source| ParserSupervisorError::IoThread {
5546 phase: "adversarial test launch",
5547 message: source.to_string(),
5548 })?;
5549 let grammar = ParserLanguageIdentity::new("hostile")?;
5550 let cancellation = IndexCancellation::new();
5551 if case.cancel_before_launch {
5552 cancellation.cancel();
5553 }
5554 let now = Instant::now();
5555 let deadline = now.checked_add(case.deadline).unwrap_or(now);
5556 let resident = ResidentParserSession::launch_command(
5557 &launch,
5558 grammar,
5559 ParserMemoryLimits::PRODUCTION,
5560 now,
5561 deadline,
5562 adversarial_launch_no_progress(case.scenario, case.no_progress),
5563 &cancellation,
5564 command_for(peer, case.scenario).map_err(|source| ParserSupervisorError::IoThread {
5565 phase: "adversarial test command",
5566 message: source.to_string(),
5567 })?,
5568 );
5569 let cancellation_thread;
5570 let result = match resident {
5571 Ok(mut resident) => {
5572 let source = vec![b'x'; case.source_bytes];
5573 let source_identity = ParserSourceIdentity::for_bytes(&source)?;
5574 let operation_started = Instant::now();
5575 let operation_deadline = case
5576 .deadline_after_launch
5577 .and_then(|duration| operation_started.checked_add(duration))
5578 .unwrap_or(deadline);
5579 cancellation_thread = case.cancellation_after_launch.map(|delay| {
5580 let cancellation = cancellation.clone();
5581 thread::spawn(move || {
5582 thread::sleep(delay);
5583 cancellation.cancel();
5584 })
5585 });
5586 let operation = resident.parse(
5587 &source,
5588 source_identity,
5589 case.limits,
5590 operation_started,
5591 operation_deadline,
5592 case.no_progress,
5593 &cancellation,
5594 );
5595 match operation {
5596 Ok(evidence) => resident.shutdown().map(|()| evidence),
5597 Err(operation) => {
5598 if operation.is_caller_stop() {
5599 resident.termination_requested = true;
5600 }
5601 Err(attach_cleanup(operation, resident.shutdown()))
5602 }
5603 }
5604 }
5605 Err(error) => {
5606 cancellation_thread = None;
5607 Err(error)
5608 }
5609 };
5610 if let Some(handle) = cancellation_thread {
5611 handle
5612 .join()
5613 .map_err(|_panic| ParserSupervisorError::Cleanup {
5614 message: "adversarial cancellation thread panicked".to_owned(),
5615 })?;
5616 }
5617 result
5618 }
5619
5620 fn require_failure(peer: &Path, hostile: &Case) -> io::Result<()> {
5621 let Err(error) = operate(peer, hostile) else {
5622 return Err(io::Error::other(format!(
5623 "hostile scenario {} unexpectedly succeeded",
5624 hostile.scenario
5625 )));
5626 };
5627 if error.has_mandatory_cleanup_failure() {
5628 return Err(io::Error::other(format!(
5629 "hostile scenario {} did not reap and join cleanly: {error:?}",
5630 hostile.scenario
5631 )));
5632 }
5633 if !error_matches(&error, hostile.expected) {
5634 return Err(io::Error::other(format!(
5635 "hostile scenario {} returned the wrong typed failure: {error:?}",
5636 hostile.scenario
5637 )));
5638 }
5639
5640 let mut healthy = case("healthy", ExpectedFailure::Io)?;
5641 healthy.no_progress = healthy.deadline;
5642 if hostile.scenario == "pre-ready-stall"
5643 && !(hostile.no_progress < RECOVERY_ALLOWANCE_PROBE_AGE
5644 && RECOVERY_ALLOWANCE_PROBE_AGE < healthy.no_progress)
5645 {
5646 return Err(io::Error::other(format!(
5647 "controlled recovery age {:?} must exceed hostile allowance {:?} and remain below healthy allowance {:?}",
5648 RECOVERY_ALLOWANCE_PROBE_AGE, hostile.no_progress, healthy.no_progress
5649 )));
5650 }
5651 let cleanup_deadline = Instant::now() + healthy.deadline;
5652 while PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) && Instant::now() < cleanup_deadline {
5653 thread::yield_now();
5654 }
5655 if PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) {
5656 return Err(io::Error::other(format!(
5657 "hostile scenario {} did not finish late process cleanup",
5658 hostile.scenario
5659 )));
5660 }
5661 require_process_spawn_cleanup_health()
5662 .map_err(|error| io::Error::other(error.to_string()))?;
5663 let evidence = operate(peer, &healthy).map_err(|error| {
5664 io::Error::other(format!(
5665 "healthy restart after {} failed: {error:?}",
5666 hostile.scenario
5667 ))
5668 })?;
5669 if evidence.root_kind().as_str() != "source_file" {
5670 return Err(io::Error::other("healthy restart returned other evidence"));
5671 }
5672 Ok(())
5673 }
5674
5675 require_reused_resident_payload_revalidation(peer)?;
5676
5677 let mut cases = vec![
5678 {
5679 let mut opening_cancel = case("opening-cancel", ExpectedFailure::Cancelled)?;
5680 opening_cancel.cancel_before_launch = true;
5681 opening_cancel
5682 },
5683 case("pre-ready-stall", ExpectedFailure::NoProgress)?,
5684 case("ready-session", ExpectedFailure::Ready("session"))?,
5685 case("ready-artifact", ExpectedFailure::Ready("artifact"))?,
5686 case("ready-containment", ExpectedFailure::Ready("containment"))?,
5687 case(
5688 "ready-malformed",
5689 ExpectedFailure::InvalidControl(ParserFrameKind::Ready),
5690 )?,
5691 case("ready-truncated", ExpectedFailure::Io)?,
5692 case("ready-oversized", ExpectedFailure::Io)?,
5693 case("progress-session", ExpectedFailure::Response("session"))?,
5694 case("progress-request", ExpectedFailure::Response("request_id"))?,
5695 case("progress-duplicate", ExpectedFailure::Progress("sequence"))?,
5696 case("progress-gap", ExpectedFailure::Progress("sequence"))?,
5697 case(
5698 "progress-regression",
5699 ExpectedFailure::Progress("completed_work"),
5700 )?,
5701 case(
5702 "progress-endless",
5703 ExpectedFailure::Deadline("request response"),
5704 )?,
5705 case("progress-no-work", ExpectedFailure::NoProgress)?,
5706 case(
5707 "completion-malformed",
5708 ExpectedFailure::InvalidControl(ParserFrameKind::Completion),
5709 )?,
5710 case("completion-truncated", ExpectedFailure::Io)?,
5711 case("completion-oversized", ExpectedFailure::Io)?,
5712 case(
5713 "failure-exit",
5714 ExpectedFailure::Worker(ParserFailureCode::ParseRejected),
5715 )?,
5716 case("stderr-flood", ExpectedFailure::Io)?,
5717 case("stderr-completion", ExpectedFailure::Io)?,
5718 case(
5719 "limit-output",
5720 ExpectedFailure::Limit("completion.output_bytes"),
5721 )?,
5722 case(
5723 "limit-source",
5724 ExpectedFailure::Limit("evidence.root_end_byte"),
5725 )?,
5726 case(
5727 "limit-nodes",
5728 ExpectedFailure::Limit("evidence.named_node_count"),
5729 )?,
5730 case(
5731 "limit-depth",
5732 ExpectedFailure::Limit("evidence.maximum_depth"),
5733 )?,
5734 ];
5735 let mut blocked_cancel = case("blocked-write", ExpectedFailure::Cancelled)?;
5736 blocked_cancel.source_bytes = 4 * 1024 * 1024;
5737 blocked_cancel.cancellation_after_launch = Some(Duration::from_millis(75));
5738 blocked_cancel.no_progress = Duration::from_secs(1);
5739 cases.push(blocked_cancel);
5740 let mut blocked_deadline = case("blocked-write", ExpectedFailure::Deadline("request write"))?;
5741 blocked_deadline.source_bytes = 4 * 1024 * 1024;
5742 blocked_deadline.deadline_after_launch = Some(Duration::from_millis(125));
5743 blocked_deadline.no_progress = Duration::from_secs(1);
5744 cases.push(blocked_deadline);
5745 if let Some(progress_endless) = cases
5746 .iter_mut()
5747 .find(|candidate| candidate.scenario == "progress-endless")
5748 {
5749 progress_endless.deadline_after_launch = Some(Duration::from_millis(250));
5750 progress_endless.no_progress = Duration::from_secs(1);
5751 }
5752 if let Some(output_limit) = cases
5753 .iter_mut()
5754 .find(|candidate| candidate.scenario == "limit-output")
5755 {
5756 output_limit.limits = ParserRequestLimits::new(64, 16, 16)
5757 .map_err(|error| io::Error::other(error.to_string()))?;
5758 }
5759
5760 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5761 cases.extend([
5762 case("admission-forged", ExpectedFailure::InvalidAdmission)?,
5763 case("admission-truncated", ExpectedFailure::Io)?,
5764 case("admission-stall", ExpectedFailure::NoProgress)?,
5765 case("admission-flood", ExpectedFailure::Io)?,
5766 ]);
5767
5768 for hostile in &cases {
5769 require_failure(peer, hostile)?;
5770 }
5771 Ok(())
5772}
5773
5774#[cfg(test)]
5775mod tests {
5776 use std::io::Cursor;
5779 use std::sync::Arc;
5780
5781 use projectatlas_core::optional_parser_protocol::{
5782 PARSER_MAX_SOURCE_BYTES, PARSER_PROTOCOL_VERSION, PARSER_WINDOWS_BROKER_ADMISSION_RECORD,
5783 ParserCompletion, ParserContentDigest, ParserResponseIdentity, ParserSyntaxKind,
5784 };
5785
5786 use super::*;
5787
5788 static ARTIFACT_IO_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
5790 static PROCESS_SPAWN_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
5792 const PROCESS_SPAWN_CHILD_ENV: &str = "PROJECTATLAS_PROCESS_SPAWN_CHILD";
5794 const PROCESS_SPAWN_CHILD_COMPLETED_ENV: &str = "PROJECTATLAS_PROCESS_SPAWN_CHILD_COMPLETED";
5796
5797 struct ProcessSpawnCleanupFailureReset;
5799
5800 impl Drop for ProcessSpawnCleanupFailureReset {
5801 fn drop(&mut self) {
5802 if let Ok(mut slot) = PROCESS_SPAWN_CLEANUP_FAILURE.lock() {
5803 *slot = None;
5804 }
5805 }
5806 }
5807
5808 struct ProcessSpawnTestHookReset;
5810
5811 impl Drop for ProcessSpawnTestHookReset {
5812 fn drop(&mut self) {
5813 for slot in [
5814 &PROCESS_SPAWN_AFTER_RENDEZVOUS_TEST_HOOK,
5815 &PROCESS_SPAWN_AFTER_FINAL_CHECK_TEST_HOOK,
5816 &PROCESS_SPAWN_BEFORE_CLEANUP_TEST_HOOK,
5817 ] {
5818 if let Ok(mut hook) = slot.lock() {
5819 *hook = None;
5820 }
5821 }
5822 }
5823 }
5824
5825 #[test]
5826 fn adversarial_launch_allowance_is_phase_specific_and_production_independent() {
5827 let operation_no_progress = Duration::from_millis(500);
5828 assert_eq!(
5829 adversarial_launch_no_progress("pre-ready-stall", operation_no_progress),
5830 operation_no_progress
5831 );
5832 assert_eq!(
5833 adversarial_launch_no_progress("admission-stall", operation_no_progress),
5834 operation_no_progress
5835 );
5836 for scenario in [
5837 "progress-no-work",
5838 "admission-forged",
5839 "completion-malformed",
5840 ] {
5841 assert_eq!(
5842 adversarial_launch_no_progress(scenario, operation_no_progress),
5843 ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS
5844 );
5845 }
5846 assert_eq!(ARTIFACT_ADMISSION_TIMEOUT, Duration::from_secs(15));
5847 assert_eq!(
5848 ARTIFACT_ADMISSION_AGGREGATE_TIMEOUT,
5849 Duration::from_secs(20 * 60)
5850 );
5851 assert_eq!(
5852 ARTIFACT_ADMISSION_NO_PROGRESS_TIMEOUT,
5853 Duration::from_secs(5)
5854 );
5855 }
5856
5857 #[test]
5858 fn adversarial_deadline_expectation_requires_the_exact_operation_phase() {
5859 let delayed_admission = ParserSupervisorError::DeadlineExceeded {
5860 phase: "containment admission",
5861 };
5862 assert!(!adversarial_deadline_matches(
5863 &delayed_admission,
5864 "request response"
5865 ));
5866
5867 let response_deadline = ParserSupervisorError::DeadlineExceeded {
5868 phase: "request response",
5869 };
5870 assert!(adversarial_deadline_matches(
5871 &response_deadline,
5872 "request response"
5873 ));
5874 }
5875
5876 #[test]
5877 fn blocked_process_spawn_child_fixture() {
5878 if std::env::var_os(PROCESS_SPAWN_CHILD_ENV).is_none() {
5879 return;
5880 }
5881 thread::sleep(Duration::from_secs(30));
5882 if let Some(path) = std::env::var_os(PROCESS_SPAWN_CHILD_COMPLETED_ENV) {
5883 let _write = fs::write(path, b"survived");
5884 }
5885 }
5886
5887 struct InterruptOnceReader {
5889 input: Cursor<Vec<u8>>,
5891 did_interrupt: bool,
5893 }
5894
5895 impl Read for InterruptOnceReader {
5896 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
5897 if !self.did_interrupt {
5898 self.did_interrupt = true;
5899 return Err(io::ErrorKind::Interrupted.into());
5900 }
5901 self.input.read(buffer)
5902 }
5903 }
5904
5905 struct BlockingReader {
5907 entered: SyncSender<()>,
5909 release: Receiver<()>,
5911 }
5912
5913 impl Read for BlockingReader {
5914 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
5915 self.entered
5916 .send(())
5917 .map_err(|_closed| io::Error::other("blocking reader entry receiver closed"))?;
5918 self.release
5919 .recv()
5920 .map_err(|_closed| io::Error::other("blocking reader release sender closed"))?;
5921 buffer[0] = b'x';
5922 Ok(1)
5923 }
5924 }
5925
5926 fn metadata_only_launch() -> VerifiedParserPackLaunch {
5928 let pack_root = PathBuf::from("metadata-only-pack");
5929 VerifiedParserPackLaunch {
5930 pack_root: pack_root.clone(),
5931 platform: PackPlatform::LinuxX86_64,
5932 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5933 containment_broker: Some(pack_root.join("projectatlas-parser-containment.exe")),
5934 accepted_grammars: vec!["alpha".to_owned(), "zeta".to_owned()],
5935 artifact: ParserArtifactIdentity::for_bytes(b"artifact"),
5936 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5937 artifact_manifest_bytes: b"artifact".to_vec(),
5938 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5939 accepted_manifest_bytes: Vec::new(),
5940 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5941 native_import_policy_bytes: Vec::new(),
5942 artifact_manifest: FileObservation::unavailable(
5943 pack_root.join(ARTIFACT_MANIFEST_FILE_NAME),
5944 ),
5945 payloads: Vec::new(),
5946 currentness_blocker: None,
5947 }
5948 }
5949
5950 fn metadata_only_supervisor() -> OptionalParserSupervisor {
5952 let launch = metadata_only_launch();
5953 OptionalParserSupervisor {
5954 pack_root: launch.pack_root.clone(),
5955 launch,
5956 memory_limits: ParserMemoryLimits::PRODUCTION,
5957 resident: None,
5958 }
5959 }
5960
5961 #[test]
5962 fn supervisor_exposes_only_verified_artifact_and_language_metadata() {
5963 let supervisor = metadata_only_supervisor();
5964 assert_eq!(
5965 supervisor.artifact_identity(),
5966 &ParserArtifactIdentity::for_bytes(b"artifact")
5967 );
5968 assert!(supervisor.accepts_language("alpha"));
5969 assert!(supervisor.accepts_language("zeta"));
5970 assert!(!supervisor.accepts_language("missing"));
5971 assert!(!supervisor.accepts_language("INVALID"));
5972 }
5973
5974 #[test]
5975 fn supervisor_rejects_artifact_identity_change_inside_selected_slot() {
5976 let mut supervisor = metadata_only_supervisor();
5977 let selected = supervisor.artifact_identity().clone();
5978 let mut replacement = metadata_only_launch();
5979 replacement.artifact = ParserArtifactIdentity::for_bytes(b"replacement-artifact");
5980
5981 assert!(matches!(
5982 supervisor.replace_verified_launch(replacement),
5983 Err(ParserSupervisorError::PayloadMismatch {
5984 reason: "artifact identity changed inside its immutable slot",
5985 ..
5986 })
5987 ));
5988 assert_eq!(supervisor.artifact_identity(), &selected);
5989 }
5990
5991 #[test]
5992 fn bounded_artifact_read_retries_interruption() -> Result<(), Box<dyn std::error::Error>> {
5993 let expected = vec![b'x'; ARTIFACT_READ_CHUNK_BYTES * 3];
5994 let expected_sha256 = encode_sha256(Sha256::digest(&expected));
5995 let mut reader = InterruptOnceReader {
5996 input: Cursor::new(expected.clone()),
5997 did_interrupt: false,
5998 };
5999 let mut bytes = Vec::new();
6000 let mut sha256 = Sha256::new();
6001 read_bounded_chunks(
6002 &mut reader,
6003 Path::new("changed-payload"),
6004 u64::try_from(ARTIFACT_READ_CHUNK_BYTES * 3)?,
6005 &mut bytes,
6006 &mut sha256,
6007 None,
6008 )?;
6009
6010 require_test(
6011 bytes == expected,
6012 "bounded artifact read changed bytes after an interrupted read",
6013 )?;
6014 require_test(
6015 encode_sha256(sha256.finalize()) == expected_sha256,
6016 "bounded artifact read changed its digest after an interrupted read",
6017 )
6018 .map_err(Into::into)
6019 }
6020
6021 #[test]
6022 fn changed_artifact_reload_returns_while_reader_is_blocked()
6023 -> Result<(), Box<dyn std::error::Error>> {
6024 let _test_guard = ARTIFACT_IO_TEST_LOCK
6025 .lock()
6026 .map_err(|_poisoned| io::Error::other("artifact I/O test lock was poisoned"))?;
6027 let cancellation = IndexCancellation::new();
6028 let started = Instant::now();
6029 let absolute_deadline = started + Duration::from_secs(5);
6030 let no_progress_timeout = Duration::from_secs(5);
6031 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
6032 let (release_sender, release_receiver) = mpsc::sync_channel(1);
6033 let (finished_sender, finished_receiver) = mpsc::sync_channel(1);
6034 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6035 let caller_cancellation = cancellation.clone();
6036 let worker_cancellation = cancellation.clone();
6037 let caller = thread::spawn(move || {
6038 let control = ArtifactIoControl {
6039 absolute_deadline,
6040 last_progress: started,
6041 no_progress_timeout,
6042 cancellation: &caller_cancellation,
6043 };
6044 let result = run_bounded_artifact_io(
6045 move || {
6046 let worker_control = ArtifactIoControl {
6047 absolute_deadline,
6048 last_progress: started,
6049 no_progress_timeout,
6050 cancellation: &worker_cancellation,
6051 };
6052 let mut reader = BlockingReader {
6053 entered: entered_sender,
6054 release: release_receiver,
6055 };
6056 let mut bytes = Vec::new();
6057 let mut sha256 = Sha256::new();
6058 let result = read_bounded_chunks(
6059 &mut reader,
6060 Path::new("blocked-payload"),
6061 u64::try_from(ARTIFACT_READ_CHUNK_BYTES).unwrap_or(u64::MAX),
6062 &mut bytes,
6063 &mut sha256,
6064 Some(&worker_control),
6065 );
6066 let worker_cancelled = matches!(
6067 &result,
6068 Err(ParserSupervisorError::Cancelled {
6069 phase: ARTIFACT_IO_PHASE
6070 })
6071 );
6072 let _finished_result = finished_sender.send(worker_cancelled);
6073 result
6074 },
6075 &control,
6076 );
6077 let _result_send = result_sender.send(result);
6078 });
6079
6080 let entered = entered_receiver.recv_timeout(Duration::from_secs(1));
6081 cancellation.cancel();
6082 let result = result_receiver.recv_timeout(Duration::from_secs(1));
6083 let returned_cancelled = matches!(
6084 result.as_ref(),
6085 Ok(Err(ParserSupervisorError::Cancelled {
6086 phase: ARTIFACT_IO_PHASE
6087 }))
6088 );
6089 let refused_second_reader = matches!(
6090 ArtifactIoLease::acquire(),
6091 Err(ParserSupervisorError::IoThread {
6092 phase: ARTIFACT_IO_PHASE,
6093 ..
6094 })
6095 );
6096
6097 let release_result = release_sender.send(());
6098 caller
6099 .join()
6100 .map_err(|_panic| io::Error::other("artifact revalidation caller panicked"))?;
6101 let worker_cancelled = finished_receiver.recv_timeout(Duration::from_secs(1));
6102 let permit_deadline = Instant::now() + Duration::from_secs(1);
6103 while ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) && Instant::now() < permit_deadline {
6104 thread::yield_now();
6105 }
6106
6107 entered.map_err(|source| io::Error::other(source.to_string()))?;
6108 let _returned = result.map_err(|source| io::Error::other(source.to_string()))?;
6109 release_result?;
6110 let worker_cancelled =
6111 worker_cancelled.map_err(|source| io::Error::other(source.to_string()))?;
6112 require_test(
6113 returned_cancelled,
6114 "blocked artifact read retained the canceled request",
6115 )?;
6116 require_test(
6117 refused_second_reader,
6118 "blocked artifact read permitted another reload worker",
6119 )?;
6120 require_test(
6121 worker_cancelled,
6122 "released artifact reader continued after request cancellation",
6123 )?;
6124 require_test(
6125 ArtifactIoLease::acquire().is_ok(),
6126 "artifact reader permit was not reusable after worker completion",
6127 )
6128 .map_err(Into::into)
6129 }
6130
6131 #[test]
6132 fn currentness_probe_returns_while_path_observer_is_blocked()
6133 -> Result<(), Box<dyn std::error::Error>> {
6134 let _test_guard = ARTIFACT_IO_TEST_LOCK
6135 .lock()
6136 .map_err(|_poisoned| io::Error::other("artifact I/O test lock was poisoned"))?;
6137 let cancellation = IndexCancellation::new();
6138 let started = Instant::now();
6139 let absolute_deadline = started + Duration::from_secs(5);
6140 let no_progress_timeout = Duration::from_secs(5);
6141 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
6142 let (release_sender, release_receiver) = mpsc::sync_channel(1);
6143 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6144 let temp = tempfile::tempdir()?;
6145 let observed_path = temp.path().join("artifact.json");
6146 fs::write(&observed_path, b"verified")?;
6147 let observation = FileObservation::capture(observed_path)?;
6148 let mut file_probe = observation.currentness_probe();
6149 file_probe.blocker = Some(std::sync::Arc::new(MetadataProbeBlocker {
6150 entered: entered_sender,
6151 release: std::sync::Mutex::new(release_receiver),
6152 }));
6153 let probe = ArtifactCurrentnessProbe {
6154 files: vec![file_probe],
6155 };
6156 let caller_cancellation = cancellation.clone();
6157 let caller = thread::spawn(move || {
6158 let control = ArtifactIoControl {
6159 absolute_deadline,
6160 last_progress: started,
6161 no_progress_timeout,
6162 cancellation: &caller_cancellation,
6163 };
6164 let result = run_bounded_artifact_currentness(probe, &control);
6165 let _result_send = result_sender.send(result);
6166 });
6167
6168 entered_receiver
6169 .recv_timeout(Duration::from_secs(1))
6170 .map_err(|source| io::Error::other(source.to_string()))?;
6171 cancellation.cancel();
6172 let result = result_receiver
6173 .recv_timeout(Duration::from_secs(1))
6174 .map_err(|source| io::Error::other(source.to_string()))?;
6175 let returned_cancelled = matches!(
6176 result,
6177 Err(ParserSupervisorError::Cancelled {
6178 phase: ARTIFACT_IO_PHASE
6179 })
6180 );
6181 let refused_second_reader = matches!(
6182 ArtifactIoLease::acquire(),
6183 Err(ParserSupervisorError::IoThread {
6184 phase: ARTIFACT_IO_PHASE,
6185 ..
6186 })
6187 );
6188 release_sender.send(())?;
6189 caller
6190 .join()
6191 .map_err(|_panic| io::Error::other("currentness caller panicked"))?;
6192 let permit_deadline = Instant::now() + Duration::from_secs(1);
6193 while ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) && Instant::now() < permit_deadline {
6194 thread::yield_now();
6195 }
6196
6197 require_test(
6198 returned_cancelled,
6199 "blocked pathname observation retained the canceled request",
6200 )?;
6201 require_test(
6202 refused_second_reader,
6203 "blocked pathname observation permitted another artifact reader",
6204 )?;
6205 require_test(
6206 ArtifactIoLease::acquire().is_ok(),
6207 "artifact reader permit was not reusable after currentness completion",
6208 )
6209 .map_err(Into::into)
6210 }
6211
6212 #[test]
6213 fn changed_artifact_refresh_propagates_request_cancellation() {
6214 let mut supervisor = metadata_only_supervisor();
6215 let cancellation = IndexCancellation::new();
6216 cancellation.cancel();
6217
6218 assert!(matches!(
6219 supervisor.refresh_changed_artifact(
6220 "alpha",
6221 Instant::now(),
6222 Instant::now() + Duration::from_secs(1),
6223 Duration::from_secs(1),
6224 &cancellation,
6225 ),
6226 Err(ParserSupervisorError::Cancelled {
6227 phase: ARTIFACT_IO_PHASE
6228 })
6229 ));
6230 }
6231
6232 #[test]
6233 fn controlled_artifact_currentness_polls_before_metadata() {
6234 let launch = metadata_only_launch();
6235 let cancellation = IndexCancellation::new();
6236 cancellation.cancel();
6237 let control = ArtifactIoControl {
6238 absolute_deadline: Instant::now() + Duration::from_secs(1),
6239 last_progress: Instant::now(),
6240 no_progress_timeout: Duration::from_secs(1),
6241 cancellation: &cancellation,
6242 };
6243
6244 assert!(matches!(
6245 launch.currentness_probe("alpha").is_current(Some(&control)),
6246 Err(ParserSupervisorError::Cancelled {
6247 phase: ARTIFACT_IO_PHASE
6248 })
6249 ));
6250 }
6251
6252 #[test]
6253 fn stopped_process_launch_never_invokes_spawn() -> Result<(), Box<dyn std::error::Error>> {
6254 let _guard = PROCESS_SPAWN_TEST_LOCK
6255 .lock()
6256 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6257 require_process_spawn_cleanup_health()?;
6258 let cancellation = IndexCancellation::new();
6259 cancellation.cancel();
6260 let invoked = Arc::new(AtomicBool::new(false));
6261 let spawn_invoked = Arc::clone(&invoked);
6262 let started = Instant::now();
6263 let result = run_bounded_process_spawn_with(
6264 Command::new(std::env::current_exe()?),
6265 started + Duration::from_secs(1),
6266 started,
6267 Duration::from_secs(1),
6268 &cancellation,
6269 move |_command| {
6270 spawn_invoked.store(true, Ordering::Release);
6271 Err(io::Error::other("stopped launch invoked spawn"))
6272 },
6273 );
6274 if !matches!(
6275 result,
6276 Err(ParserSupervisorError::Cancelled {
6277 phase: PROCESS_LAUNCH_PHASE
6278 })
6279 ) {
6280 return Err(io::Error::other(format!(
6281 "stopped process launch returned the wrong result: {result:?}"
6282 ))
6283 .into());
6284 }
6285 if invoked.load(Ordering::Acquire) {
6286 return Err(io::Error::other("stopped process launch invoked spawn").into());
6287 }
6288 Ok(())
6289 }
6290
6291 #[test]
6292 fn sticky_late_cleanup_failure_refuses_future_spawn() -> Result<(), Box<dyn std::error::Error>>
6293 {
6294 let _guard = PROCESS_SPAWN_TEST_LOCK
6295 .lock()
6296 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6297 require_process_spawn_cleanup_health()?;
6298 let _reset = ProcessSpawnCleanupFailureReset;
6299 record_process_spawn_cleanup_failure(&ParserSupervisorError::Cleanup {
6300 message: "injected cleanup failure".to_owned(),
6301 });
6302 let invoked = Arc::new(AtomicBool::new(false));
6303 let spawn_invoked = Arc::clone(&invoked);
6304 let cancellation = IndexCancellation::new();
6305 let started = Instant::now();
6306 let result = run_bounded_process_spawn_with(
6307 Command::new(std::env::current_exe()?),
6308 started + Duration::from_secs(1),
6309 started,
6310 Duration::from_secs(1),
6311 &cancellation,
6312 move |_command| {
6313 spawn_invoked.store(true, Ordering::Release);
6314 Err(io::Error::other("unhealthy launch invoked spawn"))
6315 },
6316 );
6317 let message = match result {
6318 Err(ParserSupervisorError::Cleanup { message }) => message,
6319 other => {
6320 return Err(io::Error::other(format!(
6321 "sticky cleanup failure returned the wrong result: {other:?}"
6322 ))
6323 .into());
6324 }
6325 };
6326 if !message.contains("injected cleanup failure") {
6327 return Err(io::Error::other("sticky cleanup failure lost its diagnostic").into());
6328 }
6329 if invoked.load(Ordering::Acquire) {
6330 return Err(io::Error::other("unhealthy process launch invoked spawn").into());
6331 }
6332 Ok(())
6333 }
6334
6335 #[test]
6336 fn blocked_process_spawn_releases_caller_and_reaps_late_child()
6337 -> Result<(), Box<dyn std::error::Error>> {
6338 let _guard = PROCESS_SPAWN_TEST_LOCK
6339 .lock()
6340 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6341 require_process_spawn_cleanup_health()?;
6342 let temp = tempfile::tempdir()?;
6343 let completed = temp.path().join("child-completed");
6344 let mut command = Command::new(std::env::current_exe()?);
6345 command
6346 .arg("--exact")
6347 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6348 .arg("--nocapture")
6349 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6350 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6351 .stdin(Stdio::null())
6352 .stdout(Stdio::null())
6353 .stderr(Stdio::null());
6354
6355 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
6356 let (release_sender, release_receiver) = mpsc::sync_channel(1);
6357 let (pid_sender, pid_receiver) = mpsc::sync_channel(1);
6358 let cancellation = IndexCancellation::new();
6359 let caller_cancellation = cancellation.clone();
6360 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6361 let started = Instant::now();
6362 let caller = thread::spawn(move || {
6363 let result = run_bounded_process_spawn_with(
6364 command,
6365 started + Duration::from_secs(5),
6366 started,
6367 Duration::from_secs(5),
6368 &caller_cancellation,
6369 move |mut command| {
6370 let child = command.spawn()?;
6371 pid_sender
6372 .send(child.id())
6373 .map_err(|_closed| io::Error::other("spawn PID receiver closed"))?;
6374 entered_sender
6375 .send(())
6376 .map_err(|_closed| io::Error::other("spawn blocker receiver closed"))?;
6377 release_receiver
6378 .recv()
6379 .map_err(|_closed| io::Error::other("spawn release sender closed"))?;
6380 Ok(child)
6381 },
6382 );
6383 let _send = result_sender.send(result);
6384 });
6385
6386 entered_receiver.recv_timeout(Duration::from_secs(5))?;
6387 let _late_child_pid = pid_receiver.recv_timeout(Duration::from_secs(1))?;
6388 cancellation.cancel();
6389 let result = result_receiver.recv_timeout(Duration::from_secs(1))?;
6390 match result {
6391 Err(ParserSupervisorError::Cancelled {
6392 phase: PROCESS_LAUNCH_PHASE,
6393 }) => {}
6394 other => {
6395 return Err(io::Error::other(format!(
6396 "blocked process spawn returned the wrong result: {other:?}"
6397 ))
6398 .into());
6399 }
6400 }
6401 if ProcessSpawnLease::acquire().is_ok() {
6402 return Err(io::Error::other(
6403 "blocked process spawn released its process-wide lease early",
6404 )
6405 .into());
6406 }
6407 release_sender.send(())?;
6408 caller
6409 .join()
6410 .map_err(|_panic| io::Error::other("blocked-spawn caller panicked"))?;
6411
6412 let cleanup_deadline = Instant::now() + SUPERVISOR_CLEANUP_TIMEOUT;
6413 while PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) && Instant::now() < cleanup_deadline {
6414 thread::yield_now();
6415 }
6416 if PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) {
6417 return Err(io::Error::other(
6418 "late process spawn did not release its process-wide lease",
6419 )
6420 .into());
6421 }
6422 require_process_spawn_cleanup_health()?;
6423 if completed.exists() {
6424 return Err(io::Error::other("late child survived mandatory cleanup").into());
6425 }
6426 drop(ProcessSpawnLease::acquire()?);
6427 Ok(())
6428 }
6429
6430 #[test]
6431 fn completed_process_spawn_cancellation_detaches_after_rendezvous()
6432 -> Result<(), Box<dyn std::error::Error>> {
6433 let _guard = PROCESS_SPAWN_TEST_LOCK
6434 .lock()
6435 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6436 require_process_spawn_cleanup_health()?;
6437 let _hook_reset = ProcessSpawnTestHookReset;
6438 let temp = tempfile::tempdir()?;
6439 let completed = temp.path().join("completed-spawn-child-completed");
6440 let mut command = Command::new(std::env::current_exe()?);
6441 command
6442 .arg("--exact")
6443 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6444 .arg("--nocapture")
6445 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6446 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6447 .stdin(Stdio::null())
6448 .stdout(Stdio::null())
6449 .stderr(Stdio::null());
6450
6451 let (rendezvous_entered_sender, rendezvous_entered_receiver) = mpsc::sync_channel(1);
6452 let (release_rendezvous_sender, release_rendezvous_receiver) = mpsc::sync_channel(1);
6453 *PROCESS_SPAWN_AFTER_RENDEZVOUS_TEST_HOOK
6454 .lock()
6455 .map_err(|_poisoned| io::Error::other("rendezvous test hook lock is poisoned"))? =
6456 Some(Box::new(move || {
6457 let _entered = rendezvous_entered_sender.send(());
6458 let _release = release_rendezvous_receiver.recv();
6459 }));
6460
6461 let (cleanup_entered_sender, cleanup_entered_receiver) = mpsc::sync_channel(1);
6462 let (release_cleanup_sender, release_cleanup_receiver) = mpsc::sync_channel(1);
6463 *PROCESS_SPAWN_BEFORE_CLEANUP_TEST_HOOK
6464 .lock()
6465 .map_err(|_poisoned| io::Error::other("cleanup test hook lock is poisoned"))? =
6466 Some(Box::new(move || {
6467 let thread_name = thread::current().name().unwrap_or("unnamed").to_owned();
6468 let _entered = cleanup_entered_sender.send(thread_name);
6469 let _release = release_cleanup_receiver.recv();
6470 }));
6471
6472 let (pid_sender, pid_receiver) = mpsc::sync_channel(1);
6473 let cancellation = IndexCancellation::new();
6474 let caller_cancellation = cancellation.clone();
6475 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6476 let started = Instant::now();
6477 let caller = thread::spawn(move || {
6478 let result = run_bounded_process_spawn_with(
6479 command,
6480 started + Duration::from_secs(5),
6481 started,
6482 Duration::from_secs(5),
6483 &caller_cancellation,
6484 move |mut command| {
6485 let child = command.spawn()?;
6486 pid_sender
6487 .send(child.id())
6488 .map_err(|_closed| io::Error::other("spawn PID receiver closed"))?;
6489 Ok(child)
6490 },
6491 );
6492 let _send = result_sender.send(result);
6493 });
6494
6495 rendezvous_entered_receiver.recv_timeout(Duration::from_secs(5))?;
6496 let _child_pid = pid_receiver.recv_timeout(Duration::from_secs(1))?;
6497 cancellation.cancel();
6498 release_rendezvous_sender.send(())?;
6499 let result = result_receiver.recv_timeout(Duration::from_secs(1))?;
6500 match result {
6501 Err(ParserSupervisorError::Cancelled {
6502 phase: PROCESS_LAUNCH_PHASE,
6503 }) => {}
6504 other => {
6505 return Err(io::Error::other(format!(
6506 "completed process spawn returned the wrong result: {other:?}"
6507 ))
6508 .into());
6509 }
6510 }
6511 caller
6512 .join()
6513 .map_err(|_panic| io::Error::other("completed-spawn caller panicked"))?;
6514
6515 let cleanup_thread = cleanup_entered_receiver.recv_timeout(Duration::from_secs(1))?;
6516 if cleanup_thread != "projectatlas-process-spawn" {
6517 return Err(io::Error::other(format!(
6518 "unadmitted child cleanup ran on {cleanup_thread:?}"
6519 ))
6520 .into());
6521 }
6522 if !PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) || ProcessSpawnLease::acquire().is_ok() {
6523 return Err(io::Error::other(
6524 "unadmitted child cleanup released its process-wide lease early",
6525 )
6526 .into());
6527 }
6528 if completed.exists() {
6529 return Err(io::Error::other("unadmitted child survived before cleanup").into());
6530 }
6531
6532 release_cleanup_sender.send(())?;
6533 let cleanup_deadline = Instant::now() + SUPERVISOR_CLEANUP_TIMEOUT;
6534 while PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) && Instant::now() < cleanup_deadline {
6535 thread::yield_now();
6536 }
6537 if PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) {
6538 return Err(io::Error::other(
6539 "unadmitted child cleanup did not release its process-wide lease",
6540 )
6541 .into());
6542 }
6543 require_process_spawn_cleanup_health()?;
6544 if completed.exists() {
6545 return Err(io::Error::other("unadmitted child survived mandatory cleanup").into());
6546 }
6547 drop(ProcessSpawnLease::acquire()?);
6548 Ok(())
6549 }
6550
6551 #[test]
6552 fn launch_command_cancellation_after_process_spawn_commit_returns_after_child_cleanup()
6553 -> Result<(), Box<dyn std::error::Error>> {
6554 let _guard = PROCESS_SPAWN_TEST_LOCK
6555 .lock()
6556 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6557 require_process_spawn_cleanup_health()?;
6558 let _hook_reset = ProcessSpawnTestHookReset;
6559 let temp = tempfile::tempdir()?;
6560 let completed = temp.path().join("post-commit-child-completed");
6561 let mut command = Command::new(std::env::current_exe()?);
6562 command
6563 .arg("--exact")
6564 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6565 .arg("--nocapture")
6566 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6567 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6568 .stdin(Stdio::piped())
6569 .stdout(Stdio::piped())
6570 .stderr(Stdio::null());
6571
6572 let started = Instant::now();
6573 let cancellation = IndexCancellation::new();
6574 let hook_cancellation = cancellation.clone();
6575 *PROCESS_SPAWN_AFTER_FINAL_CHECK_TEST_HOOK
6576 .lock()
6577 .map_err(|_poisoned| io::Error::other("final-check test hook lock is poisoned"))? =
6578 Some(Box::new(move || hook_cancellation.cancel()));
6579 let result = ResidentParserSession::launch_command(
6580 &metadata_only_launch(),
6581 ParserLanguageIdentity::new("alpha")?,
6582 ParserMemoryLimits::PRODUCTION,
6583 started,
6584 started + Duration::from_secs(5),
6585 Duration::from_secs(5),
6586 &cancellation,
6587 command,
6588 );
6589 if !cancellation.is_cancelled() {
6590 return Err(io::Error::other(
6591 "final-check test did not cancel after ownership commitment",
6592 )
6593 .into());
6594 }
6595 match result {
6596 Err(ParserSupervisorError::Cancelled {
6597 phase: PROCESS_LAUNCH_PHASE,
6598 }) => {}
6599 Err(other) => {
6600 return Err(io::Error::other(format!(
6601 "post-commit launch cancellation returned the wrong error: {other:?}"
6602 ))
6603 .into());
6604 }
6605 Ok(resident) => {
6606 resident.shutdown()?;
6607 return Err(io::Error::other(
6608 "post-commit launch cancellation returned a resident session",
6609 )
6610 .into());
6611 }
6612 }
6613 drop(ProcessSpawnLease::acquire()?);
6614 require_process_spawn_cleanup_health()?;
6615 if completed.exists() {
6616 return Err(io::Error::other("post-commit child survived launch cleanup").into());
6617 }
6618 Ok(())
6619 }
6620
6621 #[test]
6622 fn partial_launch_cleanup_releases_full_reader_channels()
6623 -> Result<(), Box<dyn std::error::Error>> {
6624 let temp = tempfile::tempdir()?;
6625 let completed = temp.path().join("partial-launch-child-completed");
6626 let mut child = Command::new(std::env::current_exe()?)
6627 .arg("--exact")
6628 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6629 .arg("--nocapture")
6630 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6631 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6632 .stdin(Stdio::null())
6633 .stdout(Stdio::null())
6634 .stderr(Stdio::null())
6635 .spawn()?;
6636
6637 let (frame_sender, frame_events) = mpsc::sync_channel(1);
6638 let (frame_entered_sender, frame_entered_receiver) = mpsc::sync_channel(1);
6639 let frame_handle = thread::spawn(move || {
6640 let _first = frame_sender.send(FrameReaderEvent::Frame(vec![1]));
6641 let _entered = frame_entered_sender.send(());
6642 let _second = frame_sender.send(FrameReaderEvent::Frame(vec![2]));
6643 });
6644 let (diagnostic_sender, diagnostic_events) = mpsc::sync_channel(1);
6645 let (diagnostic_entered_sender, diagnostic_entered_receiver) = mpsc::sync_channel(1);
6646 let diagnostic_handle = thread::spawn(move || {
6647 let _first = diagnostic_sender.send(DiagnosticReaderEvent::AdmissionAccepted);
6648 let _entered = diagnostic_entered_sender.send(());
6649 let _second = diagnostic_sender.send(DiagnosticReaderEvent::AdmissionAccepted);
6650 Ok(Vec::new())
6651 });
6652 frame_entered_receiver.recv_timeout(Duration::from_secs(1))?;
6653 diagnostic_entered_receiver.recv_timeout(Duration::from_secs(1))?;
6654
6655 cleanup_partial_launch(
6656 &mut child,
6657 vec![frame_handle],
6658 Some(diagnostic_handle),
6659 Some(frame_events),
6660 Some(diagnostic_events),
6661 )?;
6662 if completed.exists() {
6663 return Err(io::Error::other("partial-launch child survived cleanup").into());
6664 }
6665 Ok(())
6666 }
6667
6668 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6669 #[test]
6670 fn memfd_creation_retries_only_unsupported_modern_flags() {
6671 use nix::errno::Errno;
6672 use nix::libc;
6673 use nix::sys::memfd::MFdFlags;
6674
6675 let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING;
6676 let requested = base | MFdFlags::from_bits_retain(libc::MFD_EXEC);
6677 let mut attempts = Vec::new();
6678 let descriptor = create_memfd_with_legacy_fallback(base, libc::MFD_EXEC, |flags| {
6679 attempts.push(flags);
6680 if attempts.len() == 1 {
6681 Err(Errno::EINVAL)
6682 } else {
6683 Ok(7)
6684 }
6685 });
6686 assert_eq!(descriptor, Ok(7));
6687 assert_eq!(attempts, vec![requested, base]);
6688
6689 let mut denied_attempts = Vec::new();
6690 let denied = create_memfd_with_legacy_fallback(base, libc::MFD_EXEC, |flags| {
6691 denied_attempts.push(flags);
6692 Err::<i32, _>(Errno::EPERM)
6693 });
6694 assert_eq!(denied, Err(Errno::EPERM));
6695 assert_eq!(denied_attempts, vec![requested]);
6696 }
6697
6698 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6699 #[test]
6700 fn sealed_linux_payload_is_read_only_complete_and_immutable()
6701 -> Result<(), Box<dyn std::error::Error>> {
6702 use std::os::unix::fs::PermissionsExt;
6703
6704 use nix::fcntl::{FcntlArg, OFlag, SealFlag, fcntl};
6705
6706 let cancellation = IndexCancellation::new();
6707 let control = ArtifactIoControl {
6708 absolute_deadline: Instant::now() + Duration::from_secs(1),
6709 last_progress: Instant::now(),
6710 no_progress_timeout: Duration::from_secs(1),
6711 cancellation: &cancellation,
6712 };
6713 let payload = SealedLinuxPayload::from_verified_bytes(
6714 "test payload",
6715 "projectatlas-sealed-payload-test",
6716 b"verified authority",
6717 false,
6718 &control,
6719 )?;
6720 require_test(
6721 payload.file.metadata()?.permissions().mode() & 0o777 == 0o400,
6722 "sealed document authority retained executable or writable mode bits",
6723 )?;
6724 let status = fcntl(&payload.file, FcntlArg::F_GETFL)?;
6725 require_test(
6726 status & OFlag::O_ACCMODE.bits() == OFlag::O_RDONLY.bits(),
6727 "sealed document authority was not reopened read-only",
6728 )?;
6729 let seals = fcntl(&payload.file, FcntlArg::F_GET_SEALS)?;
6730 let required = SealFlag::F_SEAL_WRITE
6731 | SealFlag::F_SEAL_GROW
6732 | SealFlag::F_SEAL_SHRINK
6733 | SealFlag::F_SEAL_SEAL;
6734 require_test(
6735 seals & required.bits() == required.bits(),
6736 "sealed document authority omitted a required seal",
6737 )?;
6738
6739 let mut reader = payload.file.try_clone()?;
6740 let mut bytes = Vec::new();
6741 reader.read_to_end(&mut bytes)?;
6742 require_test(
6743 bytes == b"verified authority",
6744 "sealed document authority changed after reopening",
6745 )?;
6746 require_test(
6747 payload.file.set_len(0).is_err(),
6748 "sealed document authority allowed truncation",
6749 )?;
6750 let mut writer = payload.file.try_clone()?;
6751 require_test(
6752 writer.write_all(b"attacker").is_err(),
6753 "sealed document authority allowed mutation",
6754 )?;
6755
6756 let executable = SealedLinuxPayload::from_verified_bytes(
6757 "test worker",
6758 "projectatlas-sealed-worker-test",
6759 b"verified executable authority",
6760 true,
6761 &control,
6762 )?;
6763 require_test(
6764 executable.file.metadata()?.permissions().mode() & 0o777 == 0o500,
6765 "sealed executable authority retained writable or unexpected mode bits",
6766 )?;
6767 Ok(())
6768 }
6769
6770 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6771 #[test]
6772 fn sealed_linux_payload_legacy_fallback_preserves_exact_modes()
6773 -> Result<(), Box<dyn std::error::Error>> {
6774 use std::os::unix::fs::PermissionsExt;
6775
6776 use nix::errno::Errno;
6777 use nix::libc;
6778 use nix::sys::memfd::{MFdFlags, memfd_create};
6779
6780 let cancellation = IndexCancellation::new();
6781 let control = ArtifactIoControl {
6782 absolute_deadline: Instant::now() + Duration::from_secs(1),
6783 last_progress: Instant::now(),
6784 no_progress_timeout: Duration::from_secs(1),
6785 cancellation: &cancellation,
6786 };
6787 let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING;
6788 for (name, executable, expected_mode) in [
6789 ("projectatlas-legacy-document-test", false, 0o400),
6790 ("projectatlas-legacy-executable-test", true, 0o500),
6791 ] {
6792 let mode_flag = if executable {
6793 libc::MFD_EXEC
6794 } else {
6795 libc::MFD_NOEXEC_SEAL
6796 };
6797 let mut attempts = Vec::new();
6798 let payload = SealedLinuxPayload::from_verified_bytes_with_create(
6799 "legacy payload",
6800 name,
6801 b"verified authority",
6802 executable,
6803 &control,
6804 |flags| {
6805 attempts.push(flags);
6806 if attempts.len() == 1 {
6807 Err(Errno::EINVAL)
6808 } else {
6809 memfd_create(name, flags | MFdFlags::from_bits_retain(libc::MFD_EXEC))
6812 }
6813 },
6814 )?;
6815 require_test(
6816 attempts == vec![base | MFdFlags::from_bits_retain(mode_flag), base],
6817 "legacy fallback did not retry exactly once with base flags",
6818 )?;
6819 require_test(
6820 payload.file.metadata()?.permissions().mode() & 0o777 == expected_mode,
6821 "legacy fallback retained an unexpected payload mode",
6822 )?;
6823 }
6824 Ok(())
6825 }
6826
6827 #[test]
6828 fn payload_observation_detects_same_size_same_mtime_change_epoch()
6829 -> Result<(), Box<dyn std::error::Error>> {
6830 let temp = tempfile::tempdir()?;
6831 let path = temp.path().join("worker");
6832 fs::write(&path, b"trusted")?;
6833 let modified = fs::metadata(&path)?.modified()?;
6834 let observation = PayloadObservation {
6835 file: FileObservation::capture(path.clone())?,
6836 role: ParserPackPayloadRole::Worker,
6837 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6838 bytes: 7,
6839 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6840 sha256: encode_sha256(Sha256::digest(b"trusted")),
6841 };
6842 require_test(observation.is_current()?, "initial payload was rejected")?;
6843
6844 let mutation = fs::write(&path, b"mutated");
6845 #[cfg(windows)]
6846 if mutation.is_err() {
6847 require_test(
6848 observation.is_current()? && fs::read(&path)? == b"trusted",
6849 "Windows write guard reported failure after changing payload identity",
6850 )?;
6851 return Ok(());
6852 }
6853 mutation?;
6854 File::options()
6855 .write(true)
6856 .open(&path)?
6857 .set_times(fs::FileTimes::new().set_modified(modified))?;
6858 require_test(
6859 fs::metadata(&path)?.len() == observation.file.epoch.bytes
6860 && fs::metadata(&path)?.modified()? == modified,
6861 "test mutation did not preserve size and modification time",
6862 )?;
6863 require_test(
6864 !observation.is_current()?,
6865 "same-size same-mtime payload mutation retained launch authority",
6866 )?;
6867 Ok(())
6868 }
6869
6870 #[test]
6871 fn bounded_artifact_read_rejects_mismatched_captured_epoch()
6872 -> Result<(), Box<dyn std::error::Error>> {
6873 let temp = tempfile::tempdir()?;
6874 let path = temp.path().join("worker");
6875 fs::write(&path, b"trusted")?;
6876 let observation = FileObservation::capture(path.clone())?;
6877
6878 #[cfg(unix)]
6879 let expected_epoch = {
6880 let replacement = temp.path().join("replacement");
6881 let retained = temp.path().join("retained");
6882 fs::write(&replacement, b"mutated")?;
6883 fs::rename(&path, retained)?;
6884 fs::rename(replacement, &path)?;
6885 observation.epoch
6886 };
6887 #[cfg(not(unix))]
6888 let expected_epoch = {
6889 let _write_guard = &observation;
6890 FileChangeEpoch::default()
6891 };
6892
6893 let Err(error) = read_bounded_file(&path, expected_epoch, 7, None) else {
6894 return Err(io::Error::other(
6895 "replacement bytes were accepted under the captured epoch",
6896 )
6897 .into());
6898 };
6899 require_test(
6900 matches!(
6901 error,
6902 ParserSupervisorError::PayloadMismatch {
6903 reason: "artifact read handle does not match the captured file identity",
6904 ..
6905 }
6906 ),
6907 "replacement read did not fail on the captured file epoch",
6908 )?;
6909 Ok(())
6910 }
6911
6912 #[cfg(windows)]
6913 #[test]
6914 fn windows_file_observation_releases_delete_share_on_drop()
6915 -> Result<(), Box<dyn std::error::Error>> {
6916 let temp = tempfile::tempdir()?;
6917 let path = temp.path().join("guarded-payload");
6918 fs::write(&path, b"trusted")?;
6919 let observation = FileObservation::capture(path.clone())?;
6920
6921 require_test(
6922 fs::remove_file(&path).is_err() && path.is_file(),
6923 "Windows observation did not deny payload deletion",
6924 )?;
6925 drop(observation);
6926 fs::remove_file(&path)?;
6927 require_test(
6928 !path.exists(),
6929 "dropping Windows observation did not release payload deletion",
6930 )?;
6931 Ok(())
6932 }
6933
6934 #[test]
6935 fn payload_observation_revalidates_only_launch_inputs() {
6936 let observation = |role| PayloadObservation {
6937 file: FileObservation::unavailable(PathBuf::new()),
6938 role,
6939 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6940 bytes: 0,
6941 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6942 sha256: String::new(),
6943 };
6944
6945 assert!(observation(ParserPackPayloadRole::Worker).contributes_to_launch("rust"));
6946 assert!(
6947 observation(ParserPackPayloadRole::ContainmentBroker).contributes_to_launch("rust")
6948 );
6949 assert!(observation(ParserPackPayloadRole::AcceptedManifest).contributes_to_launch("rust"));
6950 assert!(
6951 observation(ParserPackPayloadRole::GrammarLibrary {
6952 language_id: "rust".to_owned(),
6953 })
6954 .contributes_to_launch("rust")
6955 );
6956 assert!(
6957 !observation(ParserPackPayloadRole::GrammarLibrary {
6958 language_id: "python".to_owned(),
6959 })
6960 .contributes_to_launch("rust")
6961 );
6962 assert!(
6963 !observation(ParserPackPayloadRole::NativeAuditReport).contributes_to_launch("rust")
6964 );
6965 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6966 assert!(
6967 observation(ParserPackPayloadRole::NativeImportPolicy).contributes_to_launch("rust")
6968 );
6969 #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
6970 assert!(
6971 !observation(ParserPackPayloadRole::NativeImportPolicy).contributes_to_launch("rust")
6972 );
6973 }
6974
6975 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6976 #[test]
6977 fn linux_currentness_probe_detects_native_policy_drift()
6978 -> Result<(), Box<dyn std::error::Error>> {
6979 let temp = tempfile::tempdir()?;
6980 let artifact_path = temp.path().join(ARTIFACT_MANIFEST_FILE_NAME);
6981 let policy_path = temp.path().join("native-policy");
6982 fs::write(&artifact_path, b"artifact")?;
6983 fs::write(&policy_path, b"trusted")?;
6984 let modified = fs::metadata(&policy_path)?.modified()?;
6985
6986 let mut launch = metadata_only_launch();
6987 launch.pack_root = temp.path().to_path_buf();
6988 launch.artifact_manifest = FileObservation::capture(artifact_path)?;
6989 launch.payloads = vec![PayloadObservation {
6990 file: FileObservation::capture(policy_path.clone())?,
6991 role: ParserPackPayloadRole::NativeImportPolicy,
6992 bytes: 7,
6993 sha256: encode_sha256(Sha256::digest(b"trusted")),
6994 }];
6995 let probe = launch.currentness_probe("alpha");
6996 require_test(
6997 probe.is_current(None)?,
6998 "initial native policy currentness probe failed",
6999 )?;
7000
7001 fs::write(&policy_path, b"changed")?;
7002 File::options()
7003 .write(true)
7004 .open(&policy_path)?
7005 .set_times(fs::FileTimes::new().set_modified(modified))?;
7006 require_test(
7007 fs::metadata(&policy_path)?.len() == 7,
7008 "native policy mutation did not preserve size",
7009 )?;
7010 require_test(
7011 fs::metadata(&policy_path)?.modified()? == modified,
7012 "native policy mutation did not preserve modification time",
7013 )?;
7014 require_test(
7015 !probe.is_current(None)?,
7016 "same-size same-mtime native policy drift retained launch authority",
7017 )?;
7018 Ok(())
7019 }
7020
7021 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
7022 #[test]
7023 fn linux_worker_launch_closes_inherited_descriptors_on_exec()
7024 -> Result<(), Box<dyn std::error::Error>> {
7025 use std::os::fd::AsRawFd;
7026
7027 use nix::fcntl::{FcntlArg, FdFlag, fcntl};
7028
7029 let cancellation = IndexCancellation::new();
7030 let control = ArtifactIoControl {
7031 absolute_deadline: Instant::now() + Duration::from_secs(1),
7032 last_progress: Instant::now(),
7033 no_progress_timeout: Duration::from_secs(1),
7034 cancellation: &cancellation,
7035 };
7036 let payload = |name| {
7037 SealedLinuxPayload::from_verified_bytes(
7038 "test payload",
7039 name,
7040 b"verified authority",
7041 false,
7042 &control,
7043 )
7044 };
7045 let authority = LinuxResidentLaunchAuthority {
7046 worker: payload("projectatlas-test-worker")?,
7047 artifact_manifest: payload("projectatlas-test-artifact")?,
7048 accepted_manifest: payload("projectatlas-test-accepted")?,
7049 native_import_policy: payload("projectatlas-test-policy")?,
7050 grammar: payload("projectatlas-test-grammar")?,
7051 };
7052 let inherited = File::open("/dev/null")?;
7053 fcntl(&inherited, FcntlArg::F_SETFD(FdFlag::empty()))?;
7054 let descriptor = inherited.as_raw_fd();
7055 let mut command = Command::new("/bin/sh");
7056 command
7057 .args([
7058 "-c",
7059 "test ! -e \"/proc/self/fd/$1\"",
7060 "parser-worker-fd-check",
7061 ])
7062 .arg(descriptor.to_string());
7063 inherit_linux_authority_on_exec(&mut command, authority);
7064
7065 if command.status()?.success() {
7066 Ok(())
7067 } else {
7068 Err(io::Error::other("worker inherited an injected descriptor").into())
7069 }
7070 }
7071
7072 #[test]
7073 fn supervisor_memory_limits_reject_zero_reversed_and_runtime_excess() {
7074 for candidate in [
7075 ParserMemoryLimits {
7076 process_bytes: 0,
7077 process_tree_bytes: 1,
7078 },
7079 ParserMemoryLimits {
7080 process_bytes: 2,
7081 process_tree_bytes: 1,
7082 },
7083 ParserMemoryLimits {
7084 process_bytes: PARSER_WORKER_PROCESS_MEMORY_BYTES.saturating_add(1),
7085 process_tree_bytes: PARSER_WORKER_JOB_MEMORY_BYTES.saturating_add(1),
7086 },
7087 ] {
7088 assert!(matches!(
7089 candidate.checked(),
7090 Err(ParserSupervisorError::InvalidMemoryLimits { .. })
7091 ));
7092 }
7093 assert!(ParserMemoryLimits::PRODUCTION.checked().is_ok());
7094 }
7095
7096 #[test]
7097 fn memory_probe_source_preserves_linux_and_bounds_windows() {
7098 let fixture = b".\n";
7099 assert_eq!(
7100 memory_probe_source(PackPlatform::LinuxX86_64, fixture),
7101 fixture
7102 );
7103 let windows = memory_probe_source(PackPlatform::WindowsX86_64, fixture);
7104 assert_eq!(windows.len(), WINDOWS_MEMORY_PROBE_SOURCE_BYTES);
7105 assert_eq!(&windows[..4], b".\n.\n");
7106 assert!(windows.len() <= PARSER_MAX_SOURCE_BYTES as usize);
7107 }
7108
7109 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
7110 #[test]
7111 fn windows_job_memory_exit_code_is_reserved_and_typed() -> Result<(), Box<dyn std::error::Error>>
7112 {
7113 let mut memory_command = Command::new("cmd.exe");
7114 memory_command
7115 .args(["/D", "/Q", "/C"])
7116 .arg(format!(
7117 "set /p _= & exit /B {PARSER_WINDOWS_BROKER_MEMORY_LIMIT_EXIT_CODE}"
7118 ))
7119 .stdin(Stdio::piped());
7120 let mut memory_exit = memory_command.spawn()?;
7121 let mut memory_input = memory_exit
7122 .stdin
7123 .take()
7124 .ok_or_else(|| io::Error::other("delayed broker memory-limit stdin is absent"))?;
7125 require_test(
7126 memory_exit.try_wait()?.is_none(),
7127 "delayed broker memory-limit process exited before observation",
7128 )?;
7129 let release_memory_exit = thread::spawn(move || {
7130 thread::sleep(Duration::from_millis(100));
7131 memory_input.write_all(b"\n")
7132 });
7133 let diagnostic = ParserIoThreadError::UnexpectedDiagnostic {
7134 diagnostic: "tree-sitter failed to allocate 8".to_owned(),
7135 };
7136 let observation_timeout = Duration::from_secs(5);
7137 let started = Instant::now();
7138 let memory_diagnostic_result = diagnostic_failure_after_exit_observation(
7139 &mut memory_exit,
7140 "request response",
7141 &diagnostic,
7142 started + observation_timeout,
7143 started,
7144 observation_timeout,
7145 &IndexCancellation::new(),
7146 );
7147 release_memory_exit
7148 .join()
7149 .map_err(|_panic| io::Error::other("delayed broker memory-limit release panicked"))??;
7150 if !matches!(
7151 memory_diagnostic_result,
7152 ParserSupervisorError::WindowsJobMemoryLimitExceeded {
7153 phase: "request response"
7154 }
7155 ) {
7156 return Err(std::io::Error::other(format!(
7157 "reserved broker memory-limit status did not override diagnostic bytes: {memory_diagnostic_result:?}"
7158 ))
7159 .into());
7160 }
7161 let memory_result =
7162 frame_event_result(FrameReaderEvent::EndOfStream, &mut memory_exit, "READY");
7163 if !matches!(
7164 memory_result,
7165 Err(ParserSupervisorError::WindowsJobMemoryLimitExceeded { phase: "READY" })
7166 ) {
7167 return Err(std::io::Error::other(format!(
7168 "reserved broker memory-limit status produced {memory_result:?}"
7169 ))
7170 .into());
7171 }
7172
7173 let mut ordinary_exit = Command::new("cmd.exe")
7174 .args(["/D", "/C", "exit", "125"])
7175 .spawn()?;
7176 ordinary_exit.wait()?;
7177 let ordinary_diagnostic_result = diagnostic_failure_after_exit_observation(
7178 &mut ordinary_exit,
7179 "request response",
7180 &diagnostic,
7181 started + Duration::from_secs(1),
7182 started,
7183 Duration::from_secs(1),
7184 &IndexCancellation::new(),
7185 );
7186 if !matches!(
7187 ordinary_diagnostic_result,
7188 ParserSupervisorError::IoThread {
7189 phase: "request response",
7190 ..
7191 }
7192 ) {
7193 return Err(std::io::Error::other(format!(
7194 "ordinary broker failure status replaced fail-closed diagnostics: {ordinary_diagnostic_result:?}"
7195 ))
7196 .into());
7197 }
7198 let ordinary_result =
7199 frame_event_result(FrameReaderEvent::EndOfStream, &mut ordinary_exit, "READY");
7200 if !matches!(
7201 ordinary_result,
7202 Err(ParserSupervisorError::ChildExited {
7203 phase: "READY",
7204 code: Some(125)
7205 })
7206 ) {
7207 return Err(std::io::Error::other(format!(
7208 "ordinary broker failure status produced {ordinary_result:?}"
7209 ))
7210 .into());
7211 }
7212 Ok(())
7213 }
7214
7215 #[test]
7216 fn cleanup_error_helper_includes_nested_combined_failures() {
7217 let nested = ParserSupervisorError::OperationAndCleanup {
7218 operation: Box::new(ParserSupervisorError::Cancelled { phase: "test" }),
7219 cleanup: Box::new(ParserSupervisorError::OperationAndCleanup {
7220 operation: Box::new(ParserSupervisorError::DeadlineExceeded { phase: "test" }),
7221 cleanup: Box::new(ParserSupervisorError::Cleanup {
7222 message: "reap failed".to_owned(),
7223 }),
7224 }),
7225 };
7226 assert!(nested.has_mandatory_cleanup_failure());
7227 assert_eq!(
7228 nested.to_string(),
7229 "optional parser operation failed: optional parser operation was cancelled during test; cleanup also failed: optional parser operation failed: optional parser absolute deadline elapsed during test; cleanup also failed: optional parser cleanup failed: reap failed"
7230 );
7231 assert!(
7232 ParserSupervisorError::Cleanup {
7233 message: "drain failed".to_owned()
7234 }
7235 .has_mandatory_cleanup_failure()
7236 );
7237 assert!(
7238 !ParserSupervisorError::Cancelled { phase: "test" }.has_mandatory_cleanup_failure()
7239 );
7240 }
7241
7242 struct GateWriter {
7244 entered: SyncSender<()>,
7246 release: Receiver<()>,
7248 }
7249
7250 impl Write for GateWriter {
7251 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
7252 self.entered
7253 .send(())
7254 .map_err(|error| io::Error::other(error.to_string()))?;
7255 self.release
7256 .recv()
7257 .map_err(|error| io::Error::other(error.to_string()))?;
7258 Ok(bytes.len())
7259 }
7260
7261 fn flush(&mut self) -> io::Result<()> {
7262 Ok(())
7263 }
7264 }
7265
7266 fn writer_command(
7268 bytes: Vec<u8>,
7269 ) -> (WriterCommand, Receiver<Result<(), ParserIoThreadError>>) {
7270 let (acknowledgement, result) = mpsc::sync_channel(1);
7271 (
7272 WriterCommand {
7273 bytes,
7274 acknowledgement,
7275 },
7276 result,
7277 )
7278 }
7279
7280 fn require_test(condition: bool, message: &'static str) -> io::Result<()> {
7282 if condition {
7283 Ok(())
7284 } else {
7285 Err(io::Error::other(message))
7286 }
7287 }
7288
7289 #[test]
7291 fn frame_reader_bounds_header_before_payload_allocation() {
7292 let declared = PARSER_MAX_SOURCE_BYTES.saturating_add(1).to_be_bytes();
7293 let bytes = [
7294 b'P',
7295 b'A',
7296 PARSER_PROTOCOL_VERSION,
7297 ParserFrameKind::RawSource.as_u8(),
7298 declared[0],
7299 declared[1],
7300 declared[2],
7301 declared[3],
7302 b'x',
7303 ];
7304 let mut input = Cursor::new(bytes);
7305 assert!(matches!(
7306 read_one_frame(&mut input),
7307 Err(ParserIoThreadError::FrameHeader {
7308 source: ParserProtocolError::FramePayloadTooLarge { .. }
7309 })
7310 ));
7311 assert_eq!(input.position(), PARSER_FRAME_HEADER_BYTES as u64);
7312 }
7313
7314 #[test]
7316 fn writer_queue_has_one_pending_slot() -> Result<(), Box<dyn std::error::Error>> {
7317 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
7318 let (release_sender, release_receiver) = mpsc::sync_channel(1);
7319 let (commands, receiver) = mpsc::sync_channel(1);
7320 let handle = thread::spawn(move || {
7321 writer_loop(
7322 GateWriter {
7323 entered: entered_sender,
7324 release: release_receiver,
7325 },
7326 &receiver,
7327 );
7328 });
7329
7330 let (first, first_result) = writer_command(vec![1]);
7331 commands.send(first)?;
7332 entered_receiver.recv_timeout(Duration::from_secs(1))?;
7333 let (second, second_result) = writer_command(vec![2]);
7334 commands.send(second)?;
7335 let (third, _third_result) = writer_command(vec![3]);
7336 require_test(
7337 matches!(commands.try_send(third), Err(TrySendError::Full(_))),
7338 "writer queue accepted more than one pending write",
7339 )?;
7340
7341 release_sender.send(())?;
7342 first_result.recv_timeout(Duration::from_secs(1))??;
7343 entered_receiver.recv_timeout(Duration::from_secs(1))?;
7344 release_sender.send(())?;
7345 second_result.recv_timeout(Duration::from_secs(1))??;
7346 drop(commands);
7347 handle
7348 .join()
7349 .map_err(|_panic| io::Error::other("writer test thread panicked"))?;
7350 Ok(())
7351 }
7352
7353 #[test]
7355 fn stop_polling_preserves_independent_bounds() {
7356 let cancellation = IndexCancellation::new();
7357 let future = Instant::now()
7358 .checked_add(Duration::from_secs(1))
7359 .unwrap_or_else(Instant::now);
7360 assert!(
7361 poll_stop(
7362 "test",
7363 future,
7364 Instant::now(),
7365 Duration::from_secs(1),
7366 &cancellation,
7367 )
7368 .is_ok()
7369 );
7370 cancellation.cancel();
7371 assert!(matches!(
7372 poll_stop(
7373 "test",
7374 future,
7375 Instant::now(),
7376 Duration::from_secs(1),
7377 &cancellation,
7378 ),
7379 Err(ParserSupervisorError::Cancelled { .. })
7380 ));
7381 assert!(matches!(
7382 poll_stop(
7383 "test",
7384 Instant::now(),
7385 Instant::now(),
7386 Duration::from_secs(1),
7387 &IndexCancellation::new(),
7388 ),
7389 Err(ParserSupervisorError::DeadlineExceeded { .. })
7390 ));
7391 assert!(matches!(
7392 poll_stop(
7393 "test",
7394 future,
7395 Instant::now(),
7396 Duration::ZERO,
7397 &IndexCancellation::new(),
7398 ),
7399 Err(ParserSupervisorError::NoProgress { .. })
7400 ));
7401 }
7402
7403 #[test]
7405 fn completion_keeps_root_error_and_rejects_cross_session_replay()
7406 -> Result<(), Box<dyn std::error::Error>> {
7407 let source = b"broken";
7408 let session = ParserSessionIdentity::for_entropy(b"session-one");
7409 let artifact = ParserArtifactIdentity::new(ParserContentDigest::for_bytes(b"artifact"));
7410 let language = ParserLanguageIdentity::new("abl")?;
7411 let limits = ParserRequestLimits::new(1024, 100, 100)?;
7412 let request = ParserRequest::new(
7413 session,
7414 ParserRequestIdentity::new(1)?,
7415 artifact.clone(),
7416 language.clone(),
7417 ParserSourceIdentity::for_bytes(source)?,
7418 limits,
7419 );
7420 let evidence = ParserCompletionEvidence::new(
7421 ParserSyntaxKind::new("source_file")?,
7422 0,
7423 u32::try_from(source.len())?,
7424 true,
7425 1,
7426 1,
7427 0,
7428 1,
7429 )?;
7430 let encoded = encode_parser_control(&ParserControl::Completion(ParserCompletion::new(
7431 ParserResponseIdentity::for_request(&request),
7432 evidence,
7433 )))?;
7434 let decoded =
7435 decode_parser_completion_for_request(ParserFrame::decode_exact(&encoded)?, &request)?;
7436 require_test(
7437 decoded.evidence().root_has_error(),
7438 "completion lost root_has_error",
7439 )?;
7440
7441 let replay_target = ParserRequest::new(
7442 ParserSessionIdentity::for_entropy(b"session-two"),
7443 ParserRequestIdentity::new(1)?,
7444 artifact,
7445 language,
7446 ParserSourceIdentity::for_bytes(source)?,
7447 limits,
7448 );
7449 require_test(
7450 decode_parser_completion_for_request(
7451 ParserFrame::decode_exact(&encoded)?,
7452 &replay_target,
7453 )
7454 .is_err(),
7455 "cross-session completion replay was accepted",
7456 )?;
7457 Ok(())
7458 }
7459
7460 #[test]
7462 fn diagnostic_reader_validates_admission_before_diagnostics()
7463 -> Result<(), Box<dyn std::error::Error>> {
7464 let mut bytes = PARSER_WINDOWS_BROKER_ADMISSION_RECORD.to_vec();
7465 bytes.extend_from_slice(b"bounded diagnostic");
7466 let (events, receiver) = mpsc::sync_channel(2);
7467 let diagnostics = diagnostic_reader_loop(
7468 Cursor::new(bytes),
7469 true,
7470 DiagnosticFence([0xA5; PARSER_DIAGNOSTIC_FENCE_BYTES]),
7471 &events,
7472 )?;
7473 require_test(
7474 matches!(
7475 receiver.recv_timeout(Duration::from_secs(1))?,
7476 DiagnosticReaderEvent::AdmissionAccepted
7477 ),
7478 "diagnostics became visible before admission",
7479 )?;
7480 require_test(
7481 matches!(
7482 receiver.recv_timeout(Duration::from_secs(1))?,
7483 DiagnosticReaderEvent::Failure(ParserIoThreadError::UnexpectedDiagnostic { .. })
7484 ),
7485 "bounded diagnostic bytes did not fail closed",
7486 )?;
7487 require_test(
7488 diagnostics == b"bounded diagnostic",
7489 "diagnostic bytes changed",
7490 )?;
7491 Ok(())
7492 }
7493
7494 #[test]
7496 fn linux_memory_accounting_records_are_strict() {
7497 assert_eq!(
7498 parse_process_rss("Name:\tworker\nVmRSS:\t4096 kB\n").ok(),
7499 Some(4 * 1024 * 1024)
7500 );
7501 assert!(parse_process_rss("VmRSS:\t4096 MB\n").is_err());
7502 assert!(parse_process_rss("VmSize:\t4096 kB\n").is_err());
7503 assert_eq!(
7504 parse_cgroup_event("low 0\nhigh 1\nmax 7\noom 0\n", "max").ok(),
7505 Some(7)
7506 );
7507 assert!(parse_cgroup_event("max 7 extra\n", "max").is_err());
7508 assert!(has_cgroup_token("cpu io memory", "memory"));
7509 assert!(has_cgroup_token("+cpu +memory", "memory"));
7510 assert!(!has_cgroup_token("cpu memory.swap", "memory"));
7511 assert!(matches!(
7512 parse_unified_cgroup_path("0::/user.slice/projectatlas.scope/worker\n"),
7513 Ok(Some(path)) if path == Path::new("user.slice/projectatlas.scope/worker")
7514 ));
7515 assert!(matches!(
7516 parse_unified_cgroup_path("0::/\n"),
7517 Ok(Some(path)) if path.as_os_str().is_empty()
7518 ));
7519 assert!(parse_unified_cgroup_path("0::/safe/../escape\n").is_err());
7520 assert!(parse_unified_cgroup_path("0::/one\n0::/two\n").is_err());
7521 assert_eq!(
7522 PARSER_LINUX_RSS_OBSERVATION_INTERVAL,
7523 SUPERVISOR_POLL_INTERVAL
7524 );
7525 }
7526
7527 #[test]
7530 fn linux_memory_exit_transition_observes_waitable_child() -> io::Result<()> {
7531 let exit_checks = std::cell::Cell::new(0_u8);
7532 let observation = resolve_linux_memory_exit_transition(
7533 io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"),
7534 SUPERVISOR_POLL_INTERVAL,
7535 || {
7536 Err(io::Error::new(
7537 io::ErrorKind::InvalidData,
7538 "VmRSS is absent",
7539 ))
7540 },
7541 || {
7542 let checks = exit_checks.get();
7543 exit_checks.set(checks.saturating_add(1));
7544 Ok((checks > 0).then_some(LinuxChildExit { code: Some(17) }))
7545 },
7546 )?;
7547 require_test(
7548 matches!(
7549 observation,
7550 LinuxMemoryObservation::ChildExited { code: Some(17) }
7551 ),
7552 "exit transition did not become waitable",
7553 )
7554 }
7555
7556 #[test]
7558 fn linux_memory_exit_transition_enforces_recovered_observation() -> io::Result<()> {
7559 let observation = resolve_linux_memory_exit_transition(
7560 io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"),
7561 SUPERVISOR_POLL_INTERVAL,
7562 || {
7563 Ok(Some(LinuxMemoryBreach {
7564 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
7565 observed_bytes: 4096,
7566 }))
7567 },
7568 || Ok(None),
7569 )?;
7570 require_test(
7571 matches!(
7572 observation,
7573 LinuxMemoryObservation::Memory(Some(LinuxMemoryBreach {
7574 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
7575 observed_bytes: 4096,
7576 }))
7577 ),
7578 "recovered memory accounting was not retained",
7579 )
7580 }
7581
7582 #[test]
7584 fn linux_memory_exit_transition_retains_unreadable_failure() -> io::Result<()> {
7585 let Err(error) = resolve_linux_memory_exit_transition(
7586 io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"),
7587 Duration::ZERO,
7588 || {
7589 Err(io::Error::new(
7590 io::ErrorKind::InvalidData,
7591 "VmRSS remains absent",
7592 ))
7593 },
7594 || Ok(None),
7595 ) else {
7596 return Err(io::Error::other(
7597 "unreadable live accounting did not fail closed",
7598 ));
7599 };
7600 require_test(
7601 error.to_string() == "VmRSS remains absent",
7602 "unreadable memory failure changed",
7603 )
7604 }
7605}