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, OptionalParserCapability,
30 OptionalParserPackArtifactManifest, OptionalParserPackManifest,
31 OptionalParserPackManifestError, PackPlatform, 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_mins(20);
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 #[cfg(any(
1493 all(target_os = "linux", target_arch = "x86_64"),
1494 all(target_os = "windows", target_arch = "x86_64")
1495 ))]
1496 pack_root: PathBuf,
1497 platform: PackPlatform,
1499 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1501 containment_broker: Option<PathBuf>,
1502 accepted_grammars: Vec<String>,
1504 artifact: ParserArtifactIdentity,
1506 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1508 artifact_manifest_bytes: Vec<u8>,
1509 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1511 accepted_manifest_bytes: Vec<u8>,
1512 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1514 native_import_policy_bytes: Vec<u8>,
1515 artifact_manifest: FileObservation,
1517 payloads: Vec<PayloadObservation>,
1519 #[cfg(test)]
1521 currentness_blocker: Option<std::sync::Arc<MetadataProbeBlocker>>,
1522}
1523
1524struct ArtifactCurrentnessProbe {
1526 files: Vec<FileCurrentnessProbe>,
1528}
1529
1530const MAX_CURRENTNESS_PROBE_FILES: usize = 5;
1534
1535impl ArtifactCurrentnessProbe {
1536 fn is_current(
1538 &self,
1539 control: Option<&ArtifactIoControl<'_>>,
1540 ) -> Result<bool, ParserSupervisorError> {
1541 for file in &self.files {
1542 if let Some(control) = control {
1543 control.poll()?;
1544 }
1545 if !file.is_current()? {
1546 return Ok(false);
1547 }
1548 }
1549 Ok(true)
1550 }
1551}
1552
1553impl VerifiedParserPackLaunch {
1554 fn load(pack_root: &Path) -> Result<Self, ParserSupervisorError> {
1556 Self::load_inner(pack_root, None)
1557 }
1558
1559 fn load_controlled(
1561 pack_root: &Path,
1562 language_id: &str,
1563 last_progress: Instant,
1564 absolute_deadline: Instant,
1565 no_progress_timeout: Duration,
1566 cancellation: &IndexCancellation,
1567 ) -> Result<Self, ParserSupervisorError> {
1568 let control = ArtifactIoControl {
1569 absolute_deadline,
1570 last_progress,
1571 no_progress_timeout,
1572 cancellation,
1573 };
1574 let pack_root = pack_root.to_path_buf();
1575 let language_id = language_id.to_owned();
1576 let worker_cancellation = cancellation.clone();
1577 run_bounded_artifact_io(
1578 move || {
1579 let worker_control = ArtifactIoControl {
1580 absolute_deadline,
1581 last_progress,
1582 no_progress_timeout,
1583 cancellation: &worker_cancellation,
1584 };
1585 let refreshed = Self::load_inner(&pack_root, Some(&worker_control))?;
1586 if !refreshed
1587 .currentness_probe(&language_id)
1588 .is_current(Some(&worker_control))?
1589 {
1590 return Err(ParserSupervisorError::PayloadMismatch {
1591 path: pack_root,
1592 reason: "artifact changed during digest revalidation",
1593 });
1594 }
1595 Ok(refreshed)
1596 },
1597 &control,
1598 )
1599 }
1600
1601 fn load_inner(
1603 pack_root: &Path,
1604 control: Option<&ArtifactIoControl<'_>>,
1605 ) -> Result<Self, ParserSupervisorError> {
1606 if let Some(control) = control {
1607 control.poll()?;
1608 }
1609 let platform =
1610 host_pack_platform().ok_or(ParserSupervisorError::UnsupportedContainment {
1611 os: std::env::consts::OS,
1612 architecture: std::env::consts::ARCH,
1613 })?;
1614 let pack_root = canonical_directory(pack_root)?;
1615 let accepted_path = canonical_direct_file(&pack_root, ACCEPTED_MANIFEST_FILE_NAME)?;
1616 let artifact_path = canonical_direct_file(&pack_root, ARTIFACT_MANIFEST_FILE_NAME)?;
1617 let accepted_manifest_file = FileObservation::capture(accepted_path.clone())?;
1618 let artifact_manifest_file = FileObservation::capture(artifact_path.clone())?;
1619 let accepted_read = read_bounded_file(
1620 &accepted_path,
1621 accepted_manifest_file.epoch,
1622 u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES).unwrap_or(u64::MAX),
1623 control,
1624 )?;
1625 let artifact_read = read_bounded_file(
1626 &artifact_path,
1627 artifact_manifest_file.epoch,
1628 u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES).unwrap_or(u64::MAX),
1629 control,
1630 )?;
1631 let mut accepted_manifest = Some(accepted_manifest_file);
1632 let logical = OptionalParserPackManifest::from_json(&accepted_read.bytes)?;
1633 let artifact_manifest: OptionalParserPackArtifactManifest =
1634 serde_json::from_slice(&artifact_read.bytes)
1635 .map_err(|source| ParserSupervisorError::ArtifactManifestJson { source })?;
1636 artifact_manifest.validate(&logical)?;
1637 if artifact_manifest.platform != platform {
1638 return Err(ParserSupervisorError::PayloadMismatch {
1639 path: artifact_path,
1640 reason: "artifact target does not match the current host",
1641 });
1642 }
1643
1644 let mut worker = None;
1645 let mut containment_broker = None;
1646 let mut accepted_payload_sha256 = None;
1647 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1648 let mut native_import_policy_bytes = None;
1649 let mut payloads = Vec::with_capacity(artifact_manifest.files.len());
1650 for payload in &artifact_manifest.files {
1651 let path = canonical_payload_file(&pack_root, payload.path.as_str())?;
1652 let file = if matches!(payload.role, ParserPackPayloadRole::AcceptedManifest) {
1653 if path != accepted_path {
1654 return Err(ParserSupervisorError::PayloadMismatch {
1655 path,
1656 reason: "accepted capability manifest is not at its defined artifact path",
1657 });
1658 }
1659 accepted_manifest
1660 .take()
1661 .ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1662 path: path.clone(),
1663 reason: "artifact contains more than one accepted capability manifest",
1664 })?
1665 } else {
1666 FileObservation::capture(path.clone())?
1667 };
1668 let payload_read = read_bounded_file(&path, file.epoch, payload.bytes, control)?;
1669 if u64::try_from(payload_read.bytes.len()).ok() != Some(payload.bytes) {
1670 return Err(ParserSupervisorError::PayloadMismatch {
1671 path,
1672 reason: "payload byte count differs from the artifact manifest",
1673 });
1674 }
1675 if payload_read.sha256 != payload.sha256.as_str() {
1676 return Err(ParserSupervisorError::PayloadMismatch {
1677 path,
1678 reason: "payload SHA-256 differs from the artifact manifest",
1679 });
1680 }
1681 payloads.push(PayloadObservation {
1682 file,
1683 role: payload.role.clone(),
1684 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1685 bytes: payload.bytes,
1686 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1687 sha256: payload.sha256.as_str().to_owned(),
1688 });
1689 match &payload.role {
1690 ParserPackPayloadRole::Worker => worker = Some(path),
1691 ParserPackPayloadRole::ContainmentBroker => containment_broker = Some(path),
1692 ParserPackPayloadRole::AcceptedManifest => {
1693 accepted_payload_sha256 = Some(payload.sha256.as_str().to_owned());
1694 }
1695 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1696 ParserPackPayloadRole::NativeImportPolicy => {
1697 native_import_policy_bytes = Some(payload_read.bytes.clone());
1698 }
1699 ParserPackPayloadRole::FixtureCorpus
1700 | ParserPackPayloadRole::ProjectLicense
1701 | ParserPackPayloadRole::NativeAuditReport
1702 | ParserPackPayloadRole::GrammarLibrary { .. } => {}
1703 #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
1704 ParserPackPayloadRole::NativeImportPolicy => {}
1705 }
1706 }
1707
1708 let worker = worker.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1709 path: pack_root.join(platform.worker_file_name()),
1710 reason: "artifact does not contain its exact worker payload",
1711 })?;
1712 #[cfg(unix)]
1713 require_executable(&worker)?;
1714 let expected_worker = canonical_direct_file(&pack_root, platform.worker_file_name())?;
1715 if worker != expected_worker {
1716 return Err(ParserSupervisorError::PayloadMismatch {
1717 path: worker,
1718 reason: "worker is not at its platform-defined artifact path",
1719 });
1720 }
1721 let containment_broker = match platform.containment_broker_file_name() {
1722 Some(file_name) => {
1723 let broker =
1724 containment_broker.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1725 path: pack_root.join(file_name),
1726 reason: "artifact does not contain its required containment broker",
1727 })?;
1728 #[cfg(unix)]
1729 require_executable(&broker)?;
1730 let expected = canonical_direct_file(&pack_root, file_name)?;
1731 if broker != expected {
1732 return Err(ParserSupervisorError::PayloadMismatch {
1733 path: broker,
1734 reason: "containment broker is not at its platform-defined artifact path",
1735 });
1736 }
1737 Some(expected)
1738 }
1739 None if containment_broker.is_none() => None,
1740 None => {
1741 return Err(ParserSupervisorError::PayloadMismatch {
1742 path: pack_root,
1743 reason: "artifact contains an unsupported containment broker",
1744 });
1745 }
1746 };
1747 let accepted_manifest_sha256 =
1748 accepted_payload_sha256.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1749 path: accepted_path.clone(),
1750 reason: "artifact does not contain its accepted capability manifest",
1751 })?;
1752 if accepted_read.sha256 != accepted_manifest_sha256 {
1753 return Err(ParserSupervisorError::PayloadMismatch {
1754 path: accepted_path,
1755 reason: "accepted capability manifest does not match its artifact payload row",
1756 });
1757 }
1758 #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
1759 let _ = containment_broker;
1760 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1761 let native_import_policy_bytes =
1762 native_import_policy_bytes.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1763 path: pack_root.clone(),
1764 reason: "Linux artifact does not contain its native-import policy",
1765 })?;
1766
1767 Ok(Self {
1768 #[cfg(any(
1769 all(target_os = "linux", target_arch = "x86_64"),
1770 all(target_os = "windows", target_arch = "x86_64")
1771 ))]
1772 pack_root,
1773 platform,
1774 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1775 containment_broker,
1776 accepted_grammars: logical
1777 .grammars()
1778 .iter()
1779 .map(|grammar| grammar.language_id.clone())
1780 .collect(),
1781 artifact: ParserArtifactIdentity::for_bytes(&artifact_read.bytes),
1782 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1783 artifact_manifest_bytes: artifact_read.bytes,
1784 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1785 accepted_manifest_bytes: accepted_read.bytes,
1786 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1787 native_import_policy_bytes,
1788 artifact_manifest: artifact_manifest_file,
1789 payloads,
1790 #[cfg(test)]
1791 currentness_blocker: None,
1792 })
1793 }
1794
1795 fn currentness_probe(&self, language_id: &str) -> ArtifactCurrentnessProbe {
1797 let mut files = Vec::with_capacity(MAX_CURRENTNESS_PROBE_FILES);
1798 files.push(self.artifact_manifest.currentness_probe());
1799 files.extend(
1800 self.payloads
1801 .iter()
1802 .filter(|payload| payload.contributes_to_launch(language_id))
1803 .map(|payload| payload.file.currentness_probe()),
1804 );
1805 #[cfg(test)]
1806 if let (Some(file), Some(blocker)) = (files.first_mut(), &self.currentness_blocker) {
1807 file.blocker = Some(std::sync::Arc::clone(blocker));
1808 }
1809 ArtifactCurrentnessProbe { files }
1810 }
1811
1812 fn require_grammar(
1814 &self,
1815 language_id: &str,
1816 ) -> Result<ParserLanguageIdentity, ParserSupervisorError> {
1817 let language = ParserLanguageIdentity::new(language_id)?;
1818 if self
1819 .accepted_grammars
1820 .binary_search_by(|candidate| candidate.as_str().cmp(language.as_str()))
1821 .is_err()
1822 {
1823 return Err(ParserSupervisorError::GrammarNotAccepted {
1824 language_id: language_id.to_owned(),
1825 });
1826 }
1827 Ok(language)
1828 }
1829
1830 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1832 fn prepare_resident_launch_controlled(
1833 &self,
1834 language_id: &str,
1835 last_progress: Instant,
1836 absolute_deadline: Instant,
1837 no_progress_timeout: Duration,
1838 cancellation: &IndexCancellation,
1839 ) -> Result<LinuxResidentLaunchAuthority, ParserSupervisorError> {
1840 let mut workers = self
1841 .payloads
1842 .iter()
1843 .filter(|payload| matches!(payload.role, ParserPackPayloadRole::Worker));
1844 let worker = workers.next().map(PayloadObservation::linux_spec);
1845 if worker.is_none() || workers.next().is_some() {
1846 return Err(ParserSupervisorError::PayloadMismatch {
1847 path: self.pack_root.clone(),
1848 reason: "artifact must bind exactly one Linux worker payload",
1849 });
1850 }
1851 let mut grammars = self.payloads.iter().filter(|payload| {
1852 matches!(
1853 &payload.role,
1854 ParserPackPayloadRole::GrammarLibrary {
1855 language_id: payload_language
1856 } if payload_language == language_id
1857 )
1858 });
1859 let grammar = grammars.next().map(PayloadObservation::linux_spec);
1860 if grammar.is_none() || grammars.next().is_some() {
1861 return Err(ParserSupervisorError::PayloadMismatch {
1862 path: self.pack_root.clone(),
1863 reason: "artifact must bind exactly one selected grammar payload",
1864 });
1865 }
1866
1867 let worker = worker.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1868 path: self.pack_root.clone(),
1869 reason: "artifact has no Linux worker payload",
1870 })?;
1871 let grammar = grammar.ok_or_else(|| ParserSupervisorError::PayloadMismatch {
1872 path: self.pack_root.clone(),
1873 reason: "artifact has no selected grammar payload",
1874 })?;
1875 let artifact_manifest = self.artifact_manifest_bytes.clone();
1876 let accepted_manifest = self.accepted_manifest_bytes.clone();
1877 let native_import_policy = self.native_import_policy_bytes.clone();
1878 let worker_cancellation = cancellation.clone();
1879 let control = ArtifactIoControl {
1880 absolute_deadline,
1881 last_progress,
1882 no_progress_timeout,
1883 cancellation,
1884 };
1885 run_bounded_artifact_io(
1886 move || {
1887 let worker_control = ArtifactIoControl {
1888 absolute_deadline,
1889 last_progress,
1890 no_progress_timeout,
1891 cancellation: &worker_cancellation,
1892 };
1893 let worker_bytes = read_verified_linux_payload(&worker, &worker_control)?;
1894 let grammar_bytes = read_verified_linux_payload(&grammar, &worker_control)?;
1895 Ok(LinuxResidentLaunchAuthority {
1896 worker: SealedLinuxPayload::from_verified_bytes(
1897 "worker",
1898 "projectatlas-parser-worker",
1899 &worker_bytes,
1900 true,
1901 &worker_control,
1902 )?,
1903 artifact_manifest: SealedLinuxPayload::from_verified_bytes(
1904 "artifact manifest",
1905 "projectatlas-artifact-manifest",
1906 &artifact_manifest,
1907 false,
1908 &worker_control,
1909 )?,
1910 accepted_manifest: SealedLinuxPayload::from_verified_bytes(
1911 "accepted capability manifest",
1912 "projectatlas-accepted-manifest",
1913 &accepted_manifest,
1914 false,
1915 &worker_control,
1916 )?,
1917 native_import_policy: SealedLinuxPayload::from_verified_bytes(
1918 "native-import policy",
1919 "projectatlas-native-policy",
1920 &native_import_policy,
1921 false,
1922 &worker_control,
1923 )?,
1924 grammar: SealedLinuxPayload::from_verified_bytes(
1925 "selected grammar",
1926 "projectatlas-selected-grammar",
1927 &grammar_bytes,
1928 true,
1929 &worker_control,
1930 )?,
1931 })
1932 },
1933 &control,
1934 )
1935 }
1936}
1937
1938#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1940fn read_verified_linux_payload(
1941 spec: &VerifiedLinuxPayloadSpec,
1942 control: &ArtifactIoControl<'_>,
1943) -> Result<Vec<u8>, ParserSupervisorError> {
1944 let read = read_bounded_file(&spec.path, spec.epoch, spec.bytes, Some(control))?;
1945 if u64::try_from(read.bytes.len()).ok() != Some(spec.bytes) {
1946 return Err(ParserSupervisorError::PayloadMismatch {
1947 path: spec.path.clone(),
1948 reason: "launch payload byte count differs from the artifact manifest",
1949 });
1950 }
1951 if read.sha256 != spec.sha256 {
1952 return Err(ParserSupervisorError::PayloadMismatch {
1953 path: spec.path.clone(),
1954 reason: "launch payload SHA-256 differs from the artifact manifest",
1955 });
1956 }
1957 Ok(read.bytes)
1958}
1959
1960fn host_pack_platform() -> Option<PackPlatform> {
1962 OptionalParserCapability::current().pack_platform()
1963}
1964
1965fn canonical_directory(path: &Path) -> Result<PathBuf, ParserSupervisorError> {
1967 let canonical = fs::canonicalize(path).map_err(|source| ParserSupervisorError::PackPath {
1968 path: path.to_path_buf(),
1969 source,
1970 })?;
1971 let metadata = file_metadata(&canonical)?;
1972 if !canonical.is_absolute() || !metadata.is_dir() {
1973 return Err(ParserSupervisorError::InvalidPackPath {
1974 path: canonical,
1975 reason: "expected an absolute regular directory",
1976 });
1977 }
1978 Ok(canonical)
1979}
1980
1981fn canonical_direct_file(
1983 pack_root: &Path,
1984 file_name: &str,
1985) -> Result<PathBuf, ParserSupervisorError> {
1986 let path = canonical_payload_file(pack_root, file_name)?;
1987 if path.parent() != Some(pack_root) {
1988 return Err(ParserSupervisorError::InvalidPackPath {
1989 path,
1990 reason: "expected a direct artifact-root file",
1991 });
1992 }
1993 Ok(path)
1994}
1995
1996fn canonical_payload_file(
1998 pack_root: &Path,
1999 relative: &str,
2000) -> Result<PathBuf, ParserSupervisorError> {
2001 let relative_path = Path::new(relative);
2002 if relative_path.is_absolute()
2003 || relative_path
2004 .components()
2005 .any(|component| !matches!(component, std::path::Component::Normal(_)))
2006 {
2007 return Err(ParserSupervisorError::InvalidPackPath {
2008 path: relative_path.to_path_buf(),
2009 reason: "expected a normalized artifact-relative path",
2010 });
2011 }
2012 let requested = pack_root.join(relative_path);
2013 let mut component_path = pack_root.to_path_buf();
2014 for component in relative_path.components() {
2015 let std::path::Component::Normal(component) = component else {
2016 return Err(ParserSupervisorError::InvalidPackPath {
2017 path: requested,
2018 reason: "expected only normal relative components",
2019 });
2020 };
2021 component_path.push(component);
2022 let metadata = fs::symlink_metadata(&component_path).map_err(|source| {
2023 ParserSupervisorError::PackPath {
2024 path: component_path.clone(),
2025 source,
2026 }
2027 })?;
2028 if is_link_or_reparse_point(&metadata) {
2029 return Err(ParserSupervisorError::InvalidPackPath {
2030 path: component_path,
2031 reason: "symbolic links and reparse points are not accepted in immutable packs",
2032 });
2033 }
2034 }
2035 let canonical =
2036 fs::canonicalize(&requested).map_err(|source| ParserSupervisorError::PackPath {
2037 path: requested.clone(),
2038 source,
2039 })?;
2040 let metadata = file_metadata(&canonical)?;
2041 if !canonical.starts_with(pack_root) || !metadata.is_file() {
2042 return Err(ParserSupervisorError::InvalidPackPath {
2043 path: canonical,
2044 reason: "payload must be a regular file inside the canonical pack root",
2045 });
2046 }
2047 Ok(canonical)
2048}
2049
2050#[cfg(windows)]
2052fn is_link_or_reparse_point(metadata: &Metadata) -> bool {
2053 use std::os::windows::fs::MetadataExt;
2054
2055 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
2056 metadata.file_type().is_symlink()
2057 || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
2058}
2059
2060#[cfg(not(windows))]
2062fn is_link_or_reparse_point(metadata: &Metadata) -> bool {
2063 metadata.file_type().is_symlink()
2064}
2065
2066fn file_metadata(path: &Path) -> Result<Metadata, ParserSupervisorError> {
2068 fs::metadata(path).map_err(|source| ParserSupervisorError::PackPath {
2069 path: path.to_path_buf(),
2070 source,
2071 })
2072}
2073
2074fn read_bounded_file(
2076 path: &Path,
2077 expected_epoch: FileChangeEpoch,
2078 maximum: u64,
2079 control: Option<&ArtifactIoControl<'_>>,
2080) -> Result<BoundedArtifactRead, ParserSupervisorError> {
2081 let mut file = File::open(path).map_err(|source| ParserSupervisorError::ArtifactRead {
2082 path: path.to_path_buf(),
2083 source,
2084 })?;
2085 let metadata = file
2086 .metadata()
2087 .map_err(|source| ParserSupervisorError::ArtifactRead {
2088 path: path.to_path_buf(),
2089 source,
2090 })?;
2091 if !metadata.is_file() {
2092 return Err(ParserSupervisorError::InvalidPackPath {
2093 path: path.to_path_buf(),
2094 reason: "expected a regular artifact file",
2095 });
2096 }
2097 if FileChangeEpoch::from_metadata(&metadata) != expected_epoch {
2098 return Err(ParserSupervisorError::PayloadMismatch {
2099 path: path.to_path_buf(),
2100 reason: "artifact read handle does not match the captured file identity",
2101 });
2102 }
2103 if metadata.len() > maximum {
2104 return Err(ParserSupervisorError::ArtifactFileTooLarge {
2105 path: path.to_path_buf(),
2106 actual: metadata.len(),
2107 maximum,
2108 });
2109 }
2110 let capacity = usize::try_from(metadata.len()).unwrap_or(ARTIFACT_READ_CHUNK_BYTES);
2111 let mut bytes = Vec::with_capacity(capacity);
2112 let mut sha256 = Sha256::new();
2113 read_bounded_chunks(&mut file, path, maximum, &mut bytes, &mut sha256, control)?;
2114 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > maximum {
2115 return Err(ParserSupervisorError::ArtifactFileTooLarge {
2116 path: path.to_path_buf(),
2117 actual: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
2118 maximum,
2119 });
2120 }
2121 Ok(BoundedArtifactRead {
2122 bytes,
2123 sha256: encode_sha256(sha256.finalize()),
2124 })
2125}
2126
2127fn read_bounded_chunks(
2129 reader: &mut impl Read,
2130 path: &Path,
2131 maximum: u64,
2132 bytes: &mut Vec<u8>,
2133 sha256: &mut Sha256,
2134 control: Option<&ArtifactIoControl<'_>>,
2135) -> Result<(), ParserSupervisorError> {
2136 let mut chunk = vec![0_u8; ARTIFACT_READ_CHUNK_BYTES].into_boxed_slice();
2137 loop {
2138 if let Some(control) = control {
2139 control.poll()?;
2140 }
2141 let remaining = maximum
2142 .saturating_add(1)
2143 .saturating_sub(u64::try_from(bytes.len()).unwrap_or(u64::MAX));
2144 if remaining == 0 {
2145 break;
2146 }
2147 let limit = usize::try_from(remaining)
2148 .unwrap_or(ARTIFACT_READ_CHUNK_BYTES)
2149 .min(ARTIFACT_READ_CHUNK_BYTES);
2150 let read = match reader.read(&mut chunk[..limit]) {
2151 Ok(read) => read,
2152 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
2153 Err(source) => {
2154 return Err(ParserSupervisorError::ArtifactRead {
2155 path: path.to_path_buf(),
2156 source,
2157 });
2158 }
2159 };
2160 if read == 0 {
2161 break;
2162 }
2163 sha256.update(&chunk[..read]);
2164 bytes.extend_from_slice(&chunk[..read]);
2165 }
2166 Ok(())
2167}
2168
2169fn encode_sha256(digest: impl AsRef<[u8]>) -> String {
2171 const LOWER_HEX: &[u8; 16] = b"0123456789abcdef";
2172 let digest = digest.as_ref();
2173 let mut encoded = String::with_capacity(digest.len().saturating_mul(2));
2174 for byte in digest {
2175 encoded.push(char::from(LOWER_HEX[usize::from(*byte >> 4)]));
2176 encoded.push(char::from(LOWER_HEX[usize::from(*byte & 0x0f)]));
2177 }
2178 encoded
2179}
2180
2181#[cfg(unix)]
2183fn require_executable(path: &Path) -> Result<(), ParserSupervisorError> {
2184 use std::os::unix::fs::PermissionsExt;
2185
2186 if file_metadata(path)?.permissions().mode() & 0o111 == 0 {
2187 return Err(ParserSupervisorError::PayloadMismatch {
2188 path: path.to_path_buf(),
2189 reason: "executable payload has no execute permission",
2190 });
2191 }
2192 Ok(())
2193}
2194
2195#[derive(Debug, Error)]
2197enum ParserIoThreadError {
2198 #[error("{operation}: {source}")]
2200 Stream {
2201 operation: &'static str,
2203 #[source]
2205 source: io::Error,
2206 },
2207 #[error("frame header: {source}")]
2209 FrameHeader {
2210 #[source]
2212 source: ParserProtocolError,
2213 },
2214 #[error("Windows admission record mismatch")]
2216 AdmissionMismatch,
2217 #[error("unexpected diagnostic bytes: {diagnostic}")]
2219 UnexpectedDiagnostic {
2220 diagnostic: String,
2222 },
2223}
2224
2225#[derive(Debug)]
2227enum FrameReaderEvent {
2228 Frame(Vec<u8>),
2230 EndOfStream,
2232 Failure(ParserIoThreadError),
2234}
2235
2236#[derive(Debug)]
2238enum DiagnosticReaderEvent {
2239 AdmissionAccepted,
2241 FenceObserved,
2243 Failure(ParserIoThreadError),
2245}
2246
2247#[derive(Clone, Copy)]
2249struct DiagnosticFence([u8; PARSER_DIAGNOSTIC_FENCE_BYTES]);
2250
2251struct WriterCommand {
2253 bytes: Vec<u8>,
2255 acknowledgement: SyncSender<Result<(), ParserIoThreadError>>,
2257}
2258
2259fn read_one_frame(input: &mut impl Read) -> Result<Option<Vec<u8>>, ParserIoThreadError> {
2261 let mut header_bytes = [0_u8; PARSER_FRAME_HEADER_BYTES];
2262 let mut header_read = 0_usize;
2263 while header_read < header_bytes.len() {
2264 match input.read(&mut header_bytes[header_read..]) {
2265 Ok(0) if header_read == 0 => return Ok(None),
2266 Ok(0) => {
2267 return Err(ParserIoThreadError::Stream {
2268 operation: "read partial frame header",
2269 source: io::Error::new(io::ErrorKind::UnexpectedEof, "partial frame header"),
2270 });
2271 }
2272 Ok(count) => header_read = header_read.saturating_add(count),
2273 Err(source) if source.kind() == io::ErrorKind::Interrupted => {}
2274 Err(source) => {
2275 return Err(ParserIoThreadError::Stream {
2276 operation: "read frame header",
2277 source,
2278 });
2279 }
2280 }
2281 }
2282 let header = ParserFrameHeader::decode(&header_bytes)
2283 .map_err(|source| ParserIoThreadError::FrameHeader { source })?;
2284 let payload_len = header.payload_len() as usize;
2285 let frame_len = PARSER_FRAME_HEADER_BYTES.saturating_add(payload_len);
2286 let mut frame = Vec::with_capacity(frame_len);
2287 frame.extend_from_slice(&header_bytes);
2288 frame.resize(frame_len, 0);
2289 input
2290 .read_exact(&mut frame[PARSER_FRAME_HEADER_BYTES..])
2291 .map_err(|source| ParserIoThreadError::Stream {
2292 operation: "read frame payload",
2293 source,
2294 })?;
2295 Ok(Some(frame))
2296}
2297
2298fn frame_reader_loop(
2300 mut stdout: ChildStdout,
2301 mut diagnostic_fence_writer: impl Write,
2302 diagnostic_fence: DiagnosticFence,
2303 events: &SyncSender<FrameReaderEvent>,
2304) {
2305 loop {
2306 let event = match read_one_frame(&mut stdout) {
2307 Ok(Some(frame)) => FrameReaderEvent::Frame(frame),
2308 Ok(None) => FrameReaderEvent::EndOfStream,
2309 Err(error) => FrameReaderEvent::Failure(error),
2310 };
2311 let event = if matches!(event, FrameReaderEvent::EndOfStream) {
2312 event
2313 } else {
2314 match diagnostic_fence_writer
2315 .write_all(&diagnostic_fence.0)
2316 .and_then(|()| diagnostic_fence_writer.flush())
2317 {
2318 Ok(()) => event,
2319 Err(source) => FrameReaderEvent::Failure(ParserIoThreadError::Stream {
2320 operation: "write diagnostic fence",
2321 source,
2322 }),
2323 }
2324 };
2325 let terminal = !matches!(event, FrameReaderEvent::Frame(_));
2326 if events.send(event).is_err() || terminal {
2327 return;
2328 }
2329 }
2330}
2331
2332fn diagnostic_reader_loop(
2334 mut stderr: impl Read,
2335 expect_windows_admission: bool,
2336 diagnostic_fence: DiagnosticFence,
2337 events: &SyncSender<DiagnosticReaderEvent>,
2338) -> Result<Vec<u8>, ParserIoThreadError> {
2339 if expect_windows_admission {
2340 let mut observed = [0_u8; PARSER_WINDOWS_BROKER_ADMISSION_RECORD.len()];
2341 if let Err(source) = stderr.read_exact(&mut observed) {
2342 let message = source.to_string();
2343 return if events
2344 .send(DiagnosticReaderEvent::Failure(
2345 ParserIoThreadError::Stream {
2346 operation: "read Windows admission record",
2347 source,
2348 },
2349 ))
2350 .is_ok()
2351 {
2352 Ok(Vec::new())
2353 } else {
2354 Err(ParserIoThreadError::Stream {
2355 operation: "read Windows admission record",
2356 source: io::Error::other(message),
2357 })
2358 };
2359 }
2360 if observed != PARSER_WINDOWS_BROKER_ADMISSION_RECORD {
2361 let error = ParserIoThreadError::AdmissionMismatch;
2362 return if events.send(DiagnosticReaderEvent::Failure(error)).is_ok() {
2363 Ok(Vec::new())
2364 } else {
2365 Err(ParserIoThreadError::AdmissionMismatch)
2366 };
2367 }
2368 }
2369 if events
2370 .send(DiagnosticReaderEvent::AdmissionAccepted)
2371 .is_err()
2372 {
2373 return Ok(Vec::new());
2374 }
2375
2376 loop {
2377 let mut observed = [0_u8; PARSER_DIAGNOSTIC_FENCE_BYTES];
2378 let mut observed_len = 0_usize;
2379 while observed_len < observed.len() {
2380 match stderr.read(&mut observed[observed_len..]) {
2381 Ok(0) if observed_len == 0 => return Ok(Vec::new()),
2382 Ok(0) => break,
2383 Ok(count) => observed_len = observed_len.saturating_add(count),
2384 Err(source) if source.kind() == io::ErrorKind::Interrupted => {}
2385 Err(source) => {
2386 let message = source.to_string();
2387 return if events
2388 .send(DiagnosticReaderEvent::Failure(
2389 ParserIoThreadError::Stream {
2390 operation: "read diagnostic stream",
2391 source,
2392 },
2393 ))
2394 .is_ok()
2395 {
2396 Ok(observed[..observed_len].to_vec())
2397 } else {
2398 Err(ParserIoThreadError::Stream {
2399 operation: "read diagnostic stream",
2400 source: io::Error::other(message),
2401 })
2402 };
2403 }
2404 }
2405 }
2406 if observed_len == observed.len() && observed == diagnostic_fence.0 {
2407 if events.send(DiagnosticReaderEvent::FenceObserved).is_err() {
2408 return Ok(Vec::new());
2409 }
2410 continue;
2411 }
2412 let diagnostics = observed[..observed_len].to_vec();
2413 let diagnostic = bounded_diagnostic(&diagnostics);
2414 return if events
2415 .send(DiagnosticReaderEvent::Failure(
2416 ParserIoThreadError::UnexpectedDiagnostic {
2417 diagnostic: diagnostic.clone(),
2418 },
2419 ))
2420 .is_ok()
2421 {
2422 Ok(diagnostics)
2423 } else {
2424 Err(ParserIoThreadError::UnexpectedDiagnostic { diagnostic })
2425 };
2426 }
2427}
2428
2429fn writer_loop(mut stdin: impl Write, commands: &Receiver<WriterCommand>) {
2431 while let Ok(command) = commands.recv() {
2432 let result = stdin
2433 .write_all(&command.bytes)
2434 .and_then(|()| stdin.flush())
2435 .map_err(|source| ParserIoThreadError::Stream {
2436 operation: "write protocol frame",
2437 source,
2438 });
2439 let failed = result.is_err();
2440 if command.acknowledgement.send(result).is_err() || failed {
2441 return;
2442 }
2443 }
2444}
2445
2446struct FrameReader {
2448 events: Receiver<FrameReaderEvent>,
2450 handle: Option<JoinHandle<()>>,
2452}
2453
2454struct DiagnosticReader {
2456 events: Receiver<DiagnosticReaderEvent>,
2458 handle: Option<JoinHandle<Result<Vec<u8>, ParserIoThreadError>>>,
2460}
2461
2462#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2464struct LinuxMemoryBreach {
2465 accounting: ParserMemoryAccountingKind,
2467 observed_bytes: u64,
2469}
2470
2471#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2473enum LinuxMemoryObservation {
2474 Memory(Option<LinuxMemoryBreach>),
2476 ChildExited {
2478 code: Option<i32>,
2480 },
2481}
2482
2483#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2485struct LinuxChildExit {
2486 code: Option<i32>,
2488}
2489
2490#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2492enum LinuxProcessGroupTermination {
2493 Signalled,
2495 Absent,
2497}
2498
2499#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2501struct LinuxCgroupMemory {
2502 directory: PathBuf,
2504 initial_max_events: u64,
2506}
2507
2508#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2509impl LinuxCgroupMemory {
2510 fn try_attach(process_id: u32, maximum_bytes: u64) -> io::Result<Option<Self>> {
2512 for parent in delegated_cgroup_parents()? {
2513 let sequence = CGROUP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
2514 let directory = parent.join(format!(
2515 "projectatlas-parser-{}-{sequence}",
2516 std::process::id()
2517 ));
2518 if fs::create_dir(&directory).is_err() {
2519 continue;
2520 }
2521 let mut candidate = Self {
2522 directory,
2523 initial_max_events: 0,
2524 };
2525 if !prepare_delegated_memory_parent(&parent) {
2526 candidate.cleanup()?;
2527 continue;
2528 }
2529 if candidate.configure(maximum_bytes).is_err() {
2530 candidate.cleanup()?;
2531 continue;
2532 }
2533 let Ok(initial_max_events) = read_cgroup_max_events(&candidate.directory) else {
2534 candidate.cleanup()?;
2535 continue;
2536 };
2537 candidate.initial_max_events = initial_max_events;
2538 if fs::write(
2539 candidate.directory.join("cgroup.procs"),
2540 process_id.to_string(),
2541 )
2542 .is_err()
2543 {
2544 candidate.cleanup()?;
2545 continue;
2546 }
2547 return Ok(Some(candidate));
2548 }
2549 Ok(None)
2550 }
2551
2552 fn configure(&self, maximum_bytes: u64) -> io::Result<()> {
2554 let maximum = maximum_bytes.to_string();
2555 fs::write(self.directory.join("memory.max"), &maximum)?;
2556 if read_bounded_linux_text(
2557 &self.directory.join("memory.max"),
2558 LINUX_MEMORY_RECORD_MAX_BYTES,
2559 )?
2560 .trim()
2561 != maximum
2562 {
2563 return Err(io::Error::new(
2564 io::ErrorKind::InvalidData,
2565 "delegated cgroup memory.max did not retain the configured ceiling",
2566 ));
2567 }
2568 let oom_group = self.directory.join("memory.oom.group");
2569 if oom_group.is_file() {
2570 fs::write(oom_group, "1")?;
2571 }
2572 Ok(())
2573 }
2574
2575 fn observe(&self, maximum_bytes: u64) -> io::Result<Option<LinuxMemoryBreach>> {
2577 let observed_bytes = read_cgroup_current(&self.directory)?;
2578 let maximum_events = read_cgroup_max_events(&self.directory)?;
2579 if observed_bytes >= maximum_bytes || maximum_events > self.initial_max_events {
2580 Ok(Some(LinuxMemoryBreach {
2581 accounting: ParserMemoryAccountingKind::LinuxCgroupV2,
2582 observed_bytes,
2583 }))
2584 } else {
2585 Ok(None)
2586 }
2587 }
2588
2589 fn cleanup(&mut self) -> io::Result<()> {
2591 if !self.directory.exists() {
2592 return Ok(());
2593 }
2594 fs::remove_dir(&self.directory)
2595 }
2596}
2597
2598#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2599impl Drop for LinuxCgroupMemory {
2600 fn drop(&mut self) {
2601 drop(self.cleanup());
2602 }
2603}
2604
2605#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2607struct LinuxMemoryObserver {
2608 cgroup: Option<LinuxCgroupMemory>,
2610 observe_cgroup: bool,
2612 maximum_bytes: u64,
2614}
2615
2616#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2617impl LinuxMemoryObserver {
2618 fn attach(process_id: u32, maximum_bytes: u64) -> io::Result<Self> {
2620 let cgroup = LinuxCgroupMemory::try_attach(process_id, maximum_bytes)?;
2621 let observe_cgroup = cgroup.is_some();
2622 Ok(Self {
2623 cgroup,
2624 observe_cgroup,
2625 maximum_bytes,
2626 })
2627 }
2628
2629 fn sampled_rss(maximum_bytes: u64) -> Self {
2631 Self {
2632 cgroup: None,
2633 observe_cgroup: false,
2634 maximum_bytes,
2635 }
2636 }
2637
2638 fn observe(&mut self, process_id: u32) -> io::Result<Option<LinuxMemoryBreach>> {
2640 if self.observe_cgroup
2641 && let Some(cgroup) = self.cgroup.as_ref()
2642 {
2643 match cgroup.observe(self.maximum_bytes) {
2644 Ok(observation) => return Ok(observation),
2645 Err(_source) => self.observe_cgroup = false,
2646 }
2647 }
2648 let observed_bytes = read_process_rss(process_id)?;
2649 if observed_bytes >= self.maximum_bytes {
2650 Ok(Some(LinuxMemoryBreach {
2651 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
2652 observed_bytes,
2653 }))
2654 } else {
2655 Ok(None)
2656 }
2657 }
2658
2659 fn cleanup(&mut self) -> io::Result<()> {
2661 match self.cgroup.as_mut() {
2662 Some(cgroup) => cgroup.cleanup(),
2663 None => Ok(()),
2664 }
2665 }
2666
2667 fn accounting_kind(&self) -> ParserMemoryAccountingKind {
2669 if self.observe_cgroup {
2670 ParserMemoryAccountingKind::LinuxCgroupV2
2671 } else {
2672 ParserMemoryAccountingKind::LinuxProcStatus
2673 }
2674 }
2675}
2676
2677#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2679enum LinuxMemoryMonitorEvent {
2680 Limit {
2682 breach: LinuxMemoryBreach,
2684 termination_error: Option<String>,
2686 },
2687 ObservationFailed {
2689 accounting: ParserMemoryAccountingKind,
2691 message: String,
2693 },
2694}
2695
2696#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2698struct LinuxMemoryMonitor {
2699 stop: SyncSender<()>,
2701 events: Receiver<LinuxMemoryMonitorEvent>,
2703 handle: Option<JoinHandle<()>>,
2705}
2706
2707#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2708impl LinuxMemoryMonitor {
2709 fn start(process_id: u32, observer: Arc<Mutex<LinuxMemoryObserver>>) -> io::Result<Self> {
2711 let (stop, stop_receiver) = mpsc::sync_channel(1);
2712 let (event_sender, events) = mpsc::sync_channel(1);
2713 let handle = thread::Builder::new()
2714 .name("parser-supervisor-memory".to_owned())
2715 .spawn(move || {
2716 linux_memory_monitor_loop(process_id, &observer, &stop_receiver, &event_sender);
2717 })?;
2718 Ok(Self {
2719 stop,
2720 events,
2721 handle: Some(handle),
2722 })
2723 }
2724
2725 fn stop(&mut self) -> Result<Option<LinuxMemoryMonitorEvent>, ParserSupervisorError> {
2727 let _stop_signal_result = self.stop.try_send(());
2728 if let Some(handle) = self.handle.take() {
2729 handle
2730 .join()
2731 .map_err(|_panic| ParserSupervisorError::Cleanup {
2732 message: "Linux resident-memory monitor panicked".to_owned(),
2733 })?;
2734 }
2735 match self.events.try_recv() {
2736 Ok(event) => Ok(Some(event)),
2737 Err(TryRecvError::Empty | TryRecvError::Disconnected) => Ok(None),
2738 }
2739 }
2740}
2741
2742#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2744fn linux_memory_monitor_loop(
2745 process_id: u32,
2746 observer: &Arc<Mutex<LinuxMemoryObserver>>,
2747 stop: &Receiver<()>,
2748 events: &SyncSender<LinuxMemoryMonitorEvent>,
2749) {
2750 let mut next_observation = Instant::now();
2751 loop {
2752 let observation = match observer.lock() {
2753 Ok(mut observer) => {
2754 let observation = observer.observe(process_id);
2755 let accounting = observer.accounting_kind();
2756 observation.map_err(|source| (accounting, source))
2757 }
2758 Err(_poisoned) => Err((
2759 ParserMemoryAccountingKind::LinuxProcStatus,
2760 io::Error::other("Linux memory observer lock was poisoned"),
2761 )),
2762 };
2763 let event = match observation {
2764 Ok(None) => None,
2765 Ok(Some(breach)) => Some(LinuxMemoryMonitorEvent::Limit {
2766 breach,
2767 termination_error: linux_monitor_termination_error(process_id),
2768 }),
2769 Err((accounting, source)) => Some(LinuxMemoryMonitorEvent::ObservationFailed {
2770 accounting,
2771 message: bounded_message(source.to_string()),
2772 }),
2773 };
2774 if let Some(event) = event {
2775 drop(events.try_send(event));
2776 return;
2777 }
2778 next_observation = next_observation
2779 .checked_add(PARSER_LINUX_RSS_OBSERVATION_INTERVAL)
2780 .unwrap_or_else(Instant::now);
2781 let wait = next_observation.saturating_duration_since(Instant::now());
2782 match stop.recv_timeout(wait) {
2783 Ok(()) | Err(RecvTimeoutError::Disconnected) => return,
2784 Err(RecvTimeoutError::Timeout) => {}
2785 }
2786 }
2787}
2788
2789#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2791fn linux_monitor_termination_error(process_id: u32) -> Option<String> {
2792 match terminate_linux_process_group(process_id) {
2793 Ok(LinuxProcessGroupTermination::Signalled) => None,
2794 Ok(LinuxProcessGroupTermination::Absent) => {
2795 Some("worker process group was absent".to_owned())
2796 }
2797 Err(source) => Some(source),
2798 }
2799}
2800
2801#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2803fn delegated_cgroup_parents() -> io::Result<Vec<PathBuf>> {
2804 let membership = read_bounded_linux_text(
2805 Path::new("/proc/self/cgroup"),
2806 LINUX_MEMORY_RECORD_MAX_BYTES,
2807 )?;
2808 let Some(relative_path) = parse_unified_cgroup_path(&membership)? else {
2809 return Ok(Vec::new());
2810 };
2811 let root = Path::new(CGROUP_V2_ROOT);
2812 let mut candidate = root.join(relative_path);
2813 let mut candidates = Vec::new();
2814 loop {
2815 if !candidate.starts_with(root) || candidates.len() >= MAX_CGROUP_ANCESTORS {
2816 return Err(io::Error::new(
2817 io::ErrorKind::InvalidData,
2818 "unified cgroup membership exceeds its ancestor bound",
2819 ));
2820 }
2821 candidates.push(candidate.clone());
2822 if candidate == root {
2823 break;
2824 }
2825 if !candidate.pop() {
2826 return Err(io::Error::new(
2827 io::ErrorKind::InvalidData,
2828 "unified cgroup membership escaped its mount",
2829 ));
2830 }
2831 }
2832 Ok(candidates)
2833}
2834
2835#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2837fn parse_unified_cgroup_path(membership: &str) -> io::Result<Option<PathBuf>> {
2838 let mut unified = membership
2839 .lines()
2840 .filter_map(|line| line.strip_prefix("0::"));
2841 let Some(relative) = unified.next() else {
2842 return Ok(None);
2843 };
2844 if unified.next().is_some() {
2845 return Err(io::Error::new(
2846 io::ErrorKind::InvalidData,
2847 "multiple unified cgroup memberships were reported",
2848 ));
2849 }
2850 let relative = relative.trim_start_matches('/');
2851 let relative_path = Path::new(relative);
2852 let component_count = relative_path.components().count();
2853 if component_count > MAX_CGROUP_ANCESTORS.saturating_sub(1)
2854 || relative_path
2855 .components()
2856 .any(|component| !matches!(component, std::path::Component::Normal(_)))
2857 {
2858 return Err(io::Error::new(
2859 io::ErrorKind::InvalidData,
2860 "unified cgroup membership path is unsafe or too deep",
2861 ));
2862 }
2863 Ok(Some(relative_path.to_path_buf()))
2864}
2865
2866#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2868fn prepare_delegated_memory_parent(parent: &Path) -> bool {
2869 let controllers = read_bounded_linux_text(
2870 &parent.join("cgroup.controllers"),
2871 LINUX_MEMORY_RECORD_MAX_BYTES,
2872 );
2873 let Ok(controllers) = controllers else {
2874 return false;
2875 };
2876 if !has_cgroup_token(&controllers, "memory") {
2877 return false;
2878 }
2879 let subtree_path = parent.join("cgroup.subtree_control");
2880 let Ok(mut subtree_control) =
2881 read_bounded_linux_text(&subtree_path, LINUX_MEMORY_RECORD_MAX_BYTES)
2882 else {
2883 return false;
2884 };
2885 if !has_cgroup_token(&subtree_control, "memory") {
2886 if fs::write(&subtree_path, "+memory").is_err() {
2887 return false;
2888 }
2889 let Ok(observed) = read_bounded_linux_text(&subtree_path, LINUX_MEMORY_RECORD_MAX_BYTES)
2890 else {
2891 return false;
2892 };
2893 subtree_control = observed;
2894 if !has_cgroup_token(&subtree_control, "memory") {
2895 return false;
2896 }
2897 }
2898 true
2899}
2900
2901#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2903fn has_cgroup_token(values: &str, expected: &str) -> bool {
2904 values.split_whitespace().any(|value| {
2905 value == expected
2906 || value
2907 .strip_prefix('+')
2908 .is_some_and(|value| value == expected)
2909 })
2910}
2911
2912#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2914fn read_bounded_linux_text(path: &Path, maximum: u64) -> io::Result<String> {
2915 let mut bytes = Vec::new();
2916 File::open(path)?
2917 .take(maximum.saturating_add(1))
2918 .read_to_end(&mut bytes)?;
2919 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > maximum {
2920 return Err(io::Error::new(
2921 io::ErrorKind::InvalidData,
2922 "Linux memory accounting record exceeded its byte ceiling",
2923 ));
2924 }
2925 String::from_utf8(bytes).map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))
2926}
2927
2928#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2930fn read_process_rss(process_id: u32) -> io::Result<u64> {
2931 let status = read_bounded_linux_text(
2932 &PathBuf::from(format!("/proc/{process_id}/status")),
2933 LINUX_MEMORY_RECORD_MAX_BYTES,
2934 )?;
2935 parse_process_rss(&status)
2936}
2937
2938#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2941fn resolve_linux_memory_exit_transition(
2942 initial_error: io::Error,
2943 timeout: Duration,
2944 mut observe_memory: impl FnMut() -> io::Result<Option<LinuxMemoryBreach>>,
2945 mut observe_exit: impl FnMut() -> io::Result<Option<LinuxChildExit>>,
2946) -> io::Result<LinuxMemoryObservation> {
2947 let deadline = Instant::now()
2948 .checked_add(timeout)
2949 .unwrap_or_else(Instant::now);
2950 let mut memory_error = initial_error;
2951 loop {
2952 match observe_exit() {
2953 Ok(Some(exit)) => {
2954 return Ok(LinuxMemoryObservation::ChildExited { code: exit.code });
2955 }
2956 Ok(None) => {}
2957 Err(source) => {
2958 return Err(io::Error::other(format!(
2959 "memory observation failed: {memory_error}; child-state observation also failed: {source}"
2960 )));
2961 }
2962 }
2963 match observe_memory() {
2964 Ok(observation) => return Ok(LinuxMemoryObservation::Memory(observation)),
2965 Err(source) => memory_error = source,
2966 }
2967 let now = Instant::now();
2968 if now >= deadline {
2969 return Err(memory_error);
2970 }
2971 thread::sleep(Duration::from_millis(1).min(deadline.saturating_duration_since(now)));
2972 }
2973}
2974
2975#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
2977fn parse_process_rss(status: &str) -> io::Result<u64> {
2978 let value = status
2979 .lines()
2980 .find_map(|line| line.strip_prefix("VmRSS:"))
2981 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"))?;
2982 let mut fields = value.split_whitespace();
2983 let kibibytes = fields
2984 .next()
2985 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "VmRSS value is absent"))?
2986 .parse::<u64>()
2987 .map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))?;
2988 if fields.next() != Some("kB") || fields.next().is_some() {
2989 return Err(io::Error::new(
2990 io::ErrorKind::InvalidData,
2991 "VmRSS does not use the exact Linux kB unit",
2992 ));
2993 }
2994 kibibytes.checked_mul(1024).ok_or_else(|| {
2995 io::Error::new(
2996 io::ErrorKind::InvalidData,
2997 "VmRSS byte conversion overflowed",
2998 )
2999 })
3000}
3001
3002#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3004fn read_cgroup_current(directory: &Path) -> io::Result<u64> {
3005 let current = read_bounded_linux_text(
3006 &directory.join("memory.current"),
3007 LINUX_MEMORY_RECORD_MAX_BYTES,
3008 )?;
3009 current
3010 .trim()
3011 .parse::<u64>()
3012 .map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))
3013}
3014
3015#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3017fn read_cgroup_max_events(directory: &Path) -> io::Result<u64> {
3018 let events = read_bounded_linux_text(
3019 &directory.join("memory.events"),
3020 LINUX_MEMORY_RECORD_MAX_BYTES,
3021 )?;
3022 parse_cgroup_event(&events, "max")
3023}
3024
3025#[cfg(any(all(target_os = "linux", target_arch = "x86_64"), test))]
3027fn parse_cgroup_event(events: &str, name: &str) -> io::Result<u64> {
3028 let value = events
3029 .lines()
3030 .find_map(|line| {
3031 let mut fields = line.split_whitespace();
3032 (fields.next() == Some(name)).then(|| (fields.next(), fields.next()))
3033 })
3034 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "cgroup event is absent"))?;
3035 if value.1.is_some() {
3036 return Err(io::Error::new(
3037 io::ErrorKind::InvalidData,
3038 "cgroup event row has extra fields",
3039 ));
3040 }
3041 value
3042 .0
3043 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "cgroup event value is absent"))?
3044 .parse::<u64>()
3045 .map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))
3046}
3047
3048struct ResidentParserSession {
3050 child: Child,
3052 grammar: ParserLanguageIdentity,
3054 session: ParserSessionIdentity,
3056 artifact: ParserArtifactIdentity,
3058 next_request_id: u64,
3060 termination_requested: bool,
3062 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3064 memory_limits: ParserMemoryLimits,
3065 writer: Option<SyncSender<WriterCommand>>,
3067 writer_handle: Option<JoinHandle<()>>,
3069 frame_reader: FrameReader,
3071 diagnostic_reader: DiagnosticReader,
3073 pending_diagnostic_fences: usize,
3075 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3077 memory_observer: Arc<Mutex<LinuxMemoryObserver>>,
3078 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3080 memory_monitor: Option<LinuxMemoryMonitor>,
3081}
3082
3083impl ResidentParserSession {
3084 fn launch(
3086 launch: &VerifiedParserPackLaunch,
3087 grammar: ParserLanguageIdentity,
3088 memory_limits: ParserMemoryLimits,
3089 last_progress: Instant,
3090 absolute_deadline: Instant,
3091 no_progress_timeout: Duration,
3092 cancellation: &IndexCancellation,
3093 ) -> Result<Self, ParserSupervisorError> {
3094 let memory_limits = memory_limits.checked()?;
3095 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3096 let command = {
3097 let authority = launch.prepare_resident_launch_controlled(
3098 grammar.as_str(),
3099 last_progress,
3100 absolute_deadline,
3101 no_progress_timeout,
3102 cancellation,
3103 )?;
3104 #[cfg(debug_assertions)]
3105 invoke_linux_launch_test_hook()?;
3106 platform_command(launch, authority, memory_limits)?
3107 };
3108 #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
3109 let command = platform_command(launch, memory_limits)?;
3110 Self::launch_command(
3111 launch,
3112 grammar,
3113 memory_limits,
3114 last_progress,
3115 absolute_deadline,
3116 no_progress_timeout,
3117 cancellation,
3118 command,
3119 )
3120 }
3121
3122 fn launch_command(
3124 launch: &VerifiedParserPackLaunch,
3125 grammar: ParserLanguageIdentity,
3126 memory_limits: ParserMemoryLimits,
3127 last_progress: Instant,
3128 absolute_deadline: Instant,
3129 no_progress_timeout: Duration,
3130 cancellation: &IndexCancellation,
3131 mut command: Command,
3132 ) -> Result<Self, ParserSupervisorError> {
3133 let _ = memory_limits;
3134 poll_stop(
3135 PROCESS_LAUNCH_PHASE,
3136 absolute_deadline,
3137 last_progress,
3138 no_progress_timeout,
3139 cancellation,
3140 )?;
3141 let session = fresh_session_identity()?;
3142 let containment = containment_for_platform(launch.platform);
3143 let diagnostic_fence = fresh_diagnostic_fence()?;
3144 let (diagnostic_pipe, child_diagnostic_writer) =
3145 io::pipe().map_err(|source| ParserSupervisorError::IoThread {
3146 phase: "diagnostic pipe startup",
3147 message: source.to_string(),
3148 })?;
3149 let diagnostic_fence_writer = child_diagnostic_writer.try_clone().map_err(|source| {
3150 ParserSupervisorError::IoThread {
3151 phase: "diagnostic pipe startup",
3152 message: source.to_string(),
3153 }
3154 })?;
3155 command.stderr(Stdio::from(child_diagnostic_writer));
3156 #[cfg(debug_assertions)]
3157 invoke_pre_spawn_test_hook()?;
3158 let mut child = run_bounded_process_spawn(
3159 command,
3160 absolute_deadline,
3161 last_progress,
3162 no_progress_timeout,
3163 cancellation,
3164 )?;
3165 let stdin = child
3166 .stdin
3167 .take()
3168 .ok_or(ParserSupervisorError::MissingPipe { stream: "stdin" });
3169 let stdout = child
3170 .stdout
3171 .take()
3172 .ok_or(ParserSupervisorError::MissingPipe { stream: "stdout" });
3173 let (stdin, stdout) = match (stdin, stdout) {
3174 (Ok(stdin), Ok(stdout)) => (stdin, stdout),
3175 (stdin, stdout) => {
3176 let operation = stdin
3177 .err()
3178 .or_else(|| stdout.err())
3179 .unwrap_or(ParserSupervisorError::MissingPipe { stream: "unknown" });
3180 return Err(attach_cleanup(
3181 operation,
3182 cleanup_partial_launch(&mut child, Vec::new(), None, None, None),
3183 ));
3184 }
3185 };
3186
3187 let (writer_sender, writer_receiver) = mpsc::sync_channel(1);
3188 let writer_handle = thread::Builder::new()
3189 .name("parser-supervisor-writer".to_owned())
3190 .spawn(move || writer_loop(stdin, &writer_receiver))
3191 .map_err(|source| ParserSupervisorError::IoThread {
3192 phase: "writer startup",
3193 message: source.to_string(),
3194 });
3195 let writer_handle = match writer_handle {
3196 Ok(handle) => handle,
3197 Err(error) => {
3198 return Err(attach_cleanup(
3199 error,
3200 cleanup_partial_launch(&mut child, Vec::new(), None, None, None),
3201 ));
3202 }
3203 };
3204 let (frame_sender, frame_events) = mpsc::sync_channel(1);
3205 let frame_handle = thread::Builder::new()
3206 .name("parser-supervisor-stdout".to_owned())
3207 .spawn(move || {
3208 frame_reader_loop(
3209 stdout,
3210 diagnostic_fence_writer,
3211 diagnostic_fence,
3212 &frame_sender,
3213 );
3214 })
3215 .map_err(|source| ParserSupervisorError::IoThread {
3216 phase: "stdout reader startup",
3217 message: source.to_string(),
3218 });
3219 let frame_handle = match frame_handle {
3220 Ok(handle) => handle,
3221 Err(error) => {
3222 drop(writer_sender);
3223 return Err(attach_cleanup(
3224 error,
3225 cleanup_partial_launch(&mut child, vec![writer_handle], None, None, None),
3226 ));
3227 }
3228 };
3229 let (diagnostic_sender, diagnostic_events) = mpsc::sync_channel(1);
3230 let expect_windows_admission = launch.platform == PackPlatform::WindowsX86_64;
3231 let diagnostic_handle = thread::Builder::new()
3232 .name("parser-supervisor-stderr".to_owned())
3233 .spawn(move || {
3234 diagnostic_reader_loop(
3235 diagnostic_pipe,
3236 expect_windows_admission,
3237 diagnostic_fence,
3238 &diagnostic_sender,
3239 )
3240 })
3241 .map_err(|source| ParserSupervisorError::IoThread {
3242 phase: "diagnostic reader startup",
3243 message: source.to_string(),
3244 });
3245 let diagnostic_handle = match diagnostic_handle {
3246 Ok(handle) => handle,
3247 Err(error) => {
3248 drop(writer_sender);
3249 return Err(attach_cleanup(
3250 error,
3251 cleanup_partial_launch(
3252 &mut child,
3253 vec![writer_handle, frame_handle],
3254 None,
3255 Some(frame_events),
3256 None,
3257 ),
3258 ));
3259 }
3260 };
3261 if let Err(operation) = poll_stop(
3262 PROCESS_LAUNCH_PHASE,
3263 absolute_deadline,
3264 last_progress,
3265 no_progress_timeout,
3266 cancellation,
3267 ) {
3268 drop(writer_sender);
3269 return Err(attach_cleanup(
3270 operation,
3271 cleanup_partial_launch(
3272 &mut child,
3273 vec![writer_handle, frame_handle],
3274 Some(diagnostic_handle),
3275 Some(frame_events),
3276 Some(diagnostic_events),
3277 ),
3278 ));
3279 }
3280
3281 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3282 let (memory_observer, memory_attachment_error) =
3283 match LinuxMemoryObserver::attach(child.id(), memory_limits.process_bytes) {
3284 Ok(observer) => (observer, None),
3285 Err(source) => (
3286 LinuxMemoryObserver::sampled_rss(memory_limits.process_bytes),
3287 Some(source),
3288 ),
3289 };
3290 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3291 let memory_observer = Arc::new(Mutex::new(memory_observer));
3292 let mut resident = Self {
3293 child,
3294 grammar,
3295 session: session.clone(),
3296 artifact: launch.artifact.clone(),
3297 next_request_id: 1,
3298 termination_requested: false,
3299 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3300 memory_limits,
3301 writer: Some(writer_sender),
3302 writer_handle: Some(writer_handle),
3303 frame_reader: FrameReader {
3304 events: frame_events,
3305 handle: Some(frame_handle),
3306 },
3307 diagnostic_reader: DiagnosticReader {
3308 events: diagnostic_events,
3309 handle: Some(diagnostic_handle),
3310 },
3311 pending_diagnostic_fences: 0,
3312 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3313 memory_observer,
3314 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3315 memory_monitor: None,
3316 };
3317 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3318 if let Some(source) = memory_attachment_error {
3319 let operation = ParserSupervisorError::ResidentMemoryObservationFailed {
3320 phase: "delegated cgroup attachment cleanup",
3321 accounting: ParserMemoryAccountingKind::LinuxCgroupV2,
3322 message: bounded_message(source.to_string()),
3323 };
3324 return match resident.shutdown() {
3325 Ok(()) => Err(operation),
3326 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
3327 operation: Box::new(operation),
3328 cleanup: Box::new(cleanup),
3329 }),
3330 };
3331 }
3332 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3333 match LinuxMemoryMonitor::start(resident.child.id(), Arc::clone(&resident.memory_observer))
3334 {
3335 Ok(monitor) => resident.memory_monitor = Some(monitor),
3336 Err(source) => {
3337 let operation = ParserSupervisorError::IoThread {
3338 phase: "resident-memory monitor startup",
3339 message: source.to_string(),
3340 };
3341 return match resident.shutdown() {
3342 Ok(()) => Err(operation),
3343 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
3344 operation: Box::new(operation),
3345 cleanup: Box::new(cleanup),
3346 }),
3347 };
3348 }
3349 }
3350 let opening: Result<(), ParserSupervisorError> = (|| {
3351 resident.wait_for_admission(
3352 absolute_deadline,
3353 last_progress,
3354 no_progress_timeout,
3355 cancellation,
3356 )?;
3357 let session_open = encode_parser_control(&ParserControl::SessionOpen(
3358 ParserSessionOpen::new(session.clone()),
3359 ))?;
3360 resident.send_bytes(
3361 session_open,
3362 "SessionOpen write",
3363 absolute_deadline,
3364 last_progress,
3365 no_progress_timeout,
3366 cancellation,
3367 )?;
3368 let ready_bytes = resident.wait_for_frame(
3369 "READY",
3370 absolute_deadline,
3371 last_progress,
3372 no_progress_timeout,
3373 cancellation,
3374 )?;
3375 let ready_frame = ParserFrame::decode_exact(&ready_bytes)?;
3376 decode_parser_ready_for_launch(ready_frame, &session, &launch.artifact, containment)?;
3377 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3378 resident.enforce_memory_bound("READY", true)?;
3379 Ok(())
3380 })();
3381 if let Err(operation) = opening {
3382 if operation.is_caller_stop() {
3383 resident.termination_requested = true;
3384 }
3385 return match resident.shutdown() {
3386 Ok(()) => Err(operation),
3387 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
3388 operation: Box::new(operation),
3389 cleanup: Box::new(cleanup),
3390 }),
3391 };
3392 }
3393 Ok(resident)
3394 }
3395
3396 fn parse(
3398 &mut self,
3399 source: &[u8],
3400 source_identity: ParserSourceIdentity,
3401 limits: ParserRequestLimits,
3402 mut last_progress: Instant,
3403 absolute_deadline: Instant,
3404 no_progress_timeout: Duration,
3405 cancellation: &IndexCancellation,
3406 ) -> Result<ParserCompletionEvidence, ParserSupervisorError> {
3407 let request_id = ParserRequestIdentity::new(self.next_request_id)?;
3408 self.next_request_id = self
3409 .next_request_id
3410 .checked_add(1)
3411 .ok_or(ParserSupervisorError::RequestIdentityExhausted)?;
3412 let request = ParserRequest::new(
3413 self.session.clone(),
3414 request_id,
3415 self.artifact.clone(),
3416 self.grammar.clone(),
3417 source_identity,
3418 limits,
3419 );
3420 let mut request_bytes = encode_parser_control(&ParserControl::Request(request.clone()))?;
3421 let source_len = u32::try_from(source.len()).map_err(|_source| {
3422 ParserProtocolError::FramePayloadTooLarge {
3423 kind: ParserFrameKind::RawSource,
3424 actual: u32::MAX,
3425 maximum: ParserFrameKind::RawSource.maximum_payload_bytes(),
3426 }
3427 })?;
3428 let source_header = ParserFrameHeader::new(ParserFrameKind::RawSource, source_len)?;
3429 request_bytes.reserve(PARSER_FRAME_HEADER_BYTES.saturating_add(source.len()));
3430 request_bytes.extend_from_slice(&source_header.encode());
3431 request_bytes.extend_from_slice(source);
3432
3433 self.send_bytes(
3434 request_bytes,
3435 "request write",
3436 absolute_deadline,
3437 last_progress,
3438 no_progress_timeout,
3439 cancellation,
3440 )?;
3441 let mut previous_progress: Option<ParserProgress> = None;
3442 loop {
3443 let response_bytes = self.wait_for_frame(
3444 "request response",
3445 absolute_deadline,
3446 last_progress,
3447 no_progress_timeout,
3448 cancellation,
3449 )?;
3450 let frame = ParserFrame::decode_exact(&response_bytes)?;
3451 match frame.kind() {
3452 ParserFrameKind::Progress => {
3453 let (progress, disposition) = decode_parser_progress_for_request(
3454 frame,
3455 &request,
3456 previous_progress.as_ref(),
3457 )?;
3458 if disposition == ParserProgressDisposition::Advanced {
3459 last_progress = Instant::now();
3460 }
3461 previous_progress = Some(progress);
3462 }
3463 ParserFrameKind::Completion => {
3464 let completion = decode_parser_completion_for_request(frame, &request)?;
3465 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3466 self.enforce_memory_bound("request completion", true)?;
3467 return Ok(completion.evidence().clone());
3468 }
3469 ParserFrameKind::Failure => {
3470 let failure = decode_parser_failure_for_request(frame, &request)?;
3471 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3472 match self.enforce_memory_bound("request failure", true) {
3473 Ok(()) | Err(ParserSupervisorError::ChildExited { code: Some(0), .. }) => {}
3474 Err(error) => return Err(error),
3475 }
3476 return Err(ParserSupervisorError::WorkerFailure {
3477 code: failure.code(),
3478 });
3479 }
3480 kind => {
3481 return Err(ParserProtocolError::UnexpectedFrameKind { kind }.into());
3482 }
3483 }
3484 }
3485 }
3486
3487 fn wait_for_admission(
3489 &mut self,
3490 absolute_deadline: Instant,
3491 last_progress: Instant,
3492 no_progress_timeout: Duration,
3493 cancellation: &IndexCancellation,
3494 ) -> Result<(), ParserSupervisorError> {
3495 loop {
3496 poll_stop(
3497 "containment admission",
3498 absolute_deadline,
3499 last_progress,
3500 no_progress_timeout,
3501 cancellation,
3502 )?;
3503 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3504 self.enforce_memory_bound("containment admission", false)?;
3505 match self.diagnostic_reader.events.recv_timeout(next_poll_wait(
3506 absolute_deadline,
3507 last_progress,
3508 no_progress_timeout,
3509 )) {
3510 Ok(DiagnosticReaderEvent::AdmissionAccepted) => return Ok(()),
3511 Ok(DiagnosticReaderEvent::FenceObserved) => {
3512 return Err(ParserSupervisorError::IoThread {
3513 phase: "containment admission",
3514 message: "diagnostic fence arrived before admission".to_owned(),
3515 });
3516 }
3517 Ok(DiagnosticReaderEvent::Failure(ParserIoThreadError::AdmissionMismatch)) => {
3518 return Err(ParserSupervisorError::InvalidAdmission);
3519 }
3520 Ok(DiagnosticReaderEvent::Failure(error)) => {
3521 return Err(io_thread_error("containment admission", &error));
3522 }
3523 Err(RecvTimeoutError::Timeout) => self.require_child_running("admission")?,
3524 Err(RecvTimeoutError::Disconnected) => {
3525 return Err(ParserSupervisorError::IoThread {
3526 phase: "containment admission",
3527 message: "diagnostic reader closed before admission".to_owned(),
3528 });
3529 }
3530 }
3531 }
3532 }
3533
3534 fn send_bytes(
3536 &mut self,
3537 bytes: Vec<u8>,
3538 phase: &'static str,
3539 absolute_deadline: Instant,
3540 last_progress: Instant,
3541 no_progress_timeout: Duration,
3542 cancellation: &IndexCancellation,
3543 ) -> Result<(), ParserSupervisorError> {
3544 let (acknowledgement, result) = mpsc::sync_channel(1);
3545 let mut command = WriterCommand {
3546 bytes,
3547 acknowledgement,
3548 };
3549 loop {
3550 poll_stop(
3551 phase,
3552 absolute_deadline,
3553 last_progress,
3554 no_progress_timeout,
3555 cancellation,
3556 )?;
3557 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3558 self.enforce_memory_bound(phase, false)?;
3559 let Some(writer) = self.writer.as_ref() else {
3560 return Err(ParserSupervisorError::IoThread {
3561 phase,
3562 message: "writer was already closed".to_owned(),
3563 });
3564 };
3565 match writer.try_send(command) {
3566 Ok(()) => break,
3567 Err(TrySendError::Full(returned)) => {
3568 command = returned;
3569 self.require_child_running(phase)?;
3570 thread::sleep(next_poll_wait(
3571 absolute_deadline,
3572 last_progress,
3573 no_progress_timeout,
3574 ));
3575 }
3576 Err(TrySendError::Disconnected(_returned)) => {
3577 return Err(ParserSupervisorError::IoThread {
3578 phase,
3579 message: "writer thread closed".to_owned(),
3580 });
3581 }
3582 }
3583 }
3584 loop {
3585 poll_stop(
3586 phase,
3587 absolute_deadline,
3588 last_progress,
3589 no_progress_timeout,
3590 cancellation,
3591 )?;
3592 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3593 self.enforce_memory_bound(phase, false)?;
3594 match result.recv_timeout(next_poll_wait(
3595 absolute_deadline,
3596 last_progress,
3597 no_progress_timeout,
3598 )) {
3599 Ok(Ok(())) => return Ok(()),
3600 Ok(Err(error)) => return Err(io_thread_error(phase, &error)),
3601 Err(RecvTimeoutError::Timeout) => self.require_child_running(phase)?,
3602 Err(RecvTimeoutError::Disconnected) => {
3603 return Err(ParserSupervisorError::IoThread {
3604 phase,
3605 message: "writer acknowledgement closed".to_owned(),
3606 });
3607 }
3608 }
3609 }
3610 }
3611
3612 fn wait_for_frame(
3614 &mut self,
3615 phase: &'static str,
3616 absolute_deadline: Instant,
3617 last_progress: Instant,
3618 no_progress_timeout: Duration,
3619 cancellation: &IndexCancellation,
3620 ) -> Result<Vec<u8>, ParserSupervisorError> {
3621 loop {
3622 poll_stop(
3623 phase,
3624 absolute_deadline,
3625 last_progress,
3626 no_progress_timeout,
3627 cancellation,
3628 )?;
3629 if let Some(event) = try_frame_event(&self.frame_reader.events)? {
3630 self.synchronize_frame_event(
3631 &event,
3632 phase,
3633 absolute_deadline,
3634 last_progress,
3635 no_progress_timeout,
3636 cancellation,
3637 )?;
3638 return self.finish_frame_event(event, phase);
3639 }
3640 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3641 self.enforce_memory_bound(phase, false)?;
3642 self.check_diagnostic_reader(
3643 phase,
3644 absolute_deadline,
3645 last_progress,
3646 no_progress_timeout,
3647 cancellation,
3648 )?;
3649 match self.frame_reader.events.recv_timeout(next_poll_wait(
3650 absolute_deadline,
3651 last_progress,
3652 no_progress_timeout,
3653 )) {
3654 Ok(event) => {
3655 self.synchronize_frame_event(
3656 &event,
3657 phase,
3658 absolute_deadline,
3659 last_progress,
3660 no_progress_timeout,
3661 cancellation,
3662 )?;
3663 return self.finish_frame_event(event, phase);
3664 }
3665 Err(RecvTimeoutError::Timeout) => {}
3666 Err(RecvTimeoutError::Disconnected) => {
3667 return Err(ParserSupervisorError::IoThread {
3668 phase,
3669 message: "stdout reader closed".to_owned(),
3670 });
3671 }
3672 }
3673 }
3674 }
3675
3676 fn synchronize_frame_event(
3678 &mut self,
3679 event: &FrameReaderEvent,
3680 phase: &'static str,
3681 absolute_deadline: Instant,
3682 last_progress: Instant,
3683 no_progress_timeout: Duration,
3684 cancellation: &IndexCancellation,
3685 ) -> Result<(), ParserSupervisorError> {
3686 if matches!(event, FrameReaderEvent::EndOfStream) {
3687 self.wait_for_diagnostic_termination(
3688 phase,
3689 absolute_deadline,
3690 last_progress,
3691 no_progress_timeout,
3692 cancellation,
3693 )
3694 } else {
3695 self.wait_for_diagnostic_fence(
3696 phase,
3697 absolute_deadline,
3698 last_progress,
3699 no_progress_timeout,
3700 cancellation,
3701 )
3702 }
3703 }
3704
3705 fn wait_for_diagnostic_termination(
3707 &mut self,
3708 phase: &'static str,
3709 absolute_deadline: Instant,
3710 last_progress: Instant,
3711 no_progress_timeout: Duration,
3712 cancellation: &IndexCancellation,
3713 ) -> Result<(), ParserSupervisorError> {
3714 loop {
3715 poll_stop(
3716 phase,
3717 absolute_deadline,
3718 last_progress,
3719 no_progress_timeout,
3720 cancellation,
3721 )?;
3722 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3723 self.enforce_memory_bound(phase, false)?;
3724 self.check_diagnostic_reader(
3725 phase,
3726 absolute_deadline,
3727 last_progress,
3728 no_progress_timeout,
3729 cancellation,
3730 )?;
3731 if thread_finished(self.diagnostic_reader.handle.as_ref()) {
3732 self.check_diagnostic_reader(
3733 phase,
3734 absolute_deadline,
3735 last_progress,
3736 no_progress_timeout,
3737 cancellation,
3738 )?;
3739 return Ok(());
3740 }
3741 thread::sleep(next_poll_wait(
3742 absolute_deadline,
3743 last_progress,
3744 no_progress_timeout,
3745 ));
3746 }
3747 }
3748
3749 fn wait_for_diagnostic_fence(
3751 &mut self,
3752 phase: &'static str,
3753 absolute_deadline: Instant,
3754 last_progress: Instant,
3755 no_progress_timeout: Duration,
3756 cancellation: &IndexCancellation,
3757 ) -> Result<(), ParserSupervisorError> {
3758 if self.pending_diagnostic_fences > 0 {
3759 self.pending_diagnostic_fences = self.pending_diagnostic_fences.saturating_sub(1);
3760 return Ok(());
3761 }
3762 loop {
3763 poll_stop(
3764 phase,
3765 absolute_deadline,
3766 last_progress,
3767 no_progress_timeout,
3768 cancellation,
3769 )?;
3770 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3771 match self.enforce_memory_bound(phase, false) {
3772 Ok(()) | Err(ParserSupervisorError::ChildExited { code: Some(0), .. }) => {}
3773 Err(error) => return Err(error),
3774 }
3775 match self.diagnostic_reader.events.recv_timeout(next_poll_wait(
3776 absolute_deadline,
3777 last_progress,
3778 no_progress_timeout,
3779 )) {
3780 Ok(DiagnosticReaderEvent::FenceObserved) => return Ok(()),
3781 Ok(DiagnosticReaderEvent::Failure(error)) => {
3782 self.termination_requested = true;
3783 return Err(diagnostic_failure_after_exit_observation(
3784 &mut self.child,
3785 phase,
3786 &error,
3787 absolute_deadline,
3788 last_progress,
3789 no_progress_timeout,
3790 cancellation,
3791 ));
3792 }
3793 Ok(DiagnosticReaderEvent::AdmissionAccepted) | Err(RecvTimeoutError::Timeout) => {}
3794 Err(RecvTimeoutError::Disconnected) => {
3795 return Err(ParserSupervisorError::IoThread {
3796 phase,
3797 message: "diagnostic reader closed before frame fence".to_owned(),
3798 });
3799 }
3800 }
3801 }
3802 }
3803
3804 fn finish_frame_event(
3806 &mut self,
3807 event: FrameReaderEvent,
3808 phase: &'static str,
3809 ) -> Result<Vec<u8>, ParserSupervisorError> {
3810 let result = frame_event_result(event, &mut self.child, phase);
3811 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
3812 if matches!(
3813 &result,
3814 Err(ParserSupervisorError::WindowsJobMemoryLimitExceeded { .. })
3815 ) {
3816 self.termination_requested = true;
3817 }
3818 result
3819 }
3820
3821 fn check_diagnostic_reader(
3823 &mut self,
3824 phase: &'static str,
3825 absolute_deadline: Instant,
3826 last_progress: Instant,
3827 no_progress_timeout: Duration,
3828 cancellation: &IndexCancellation,
3829 ) -> Result<(), ParserSupervisorError> {
3830 loop {
3831 match self.diagnostic_reader.events.try_recv() {
3832 Ok(DiagnosticReaderEvent::Failure(error)) => {
3833 self.termination_requested = true;
3834 return Err(diagnostic_failure_after_exit_observation(
3835 &mut self.child,
3836 phase,
3837 &error,
3838 absolute_deadline,
3839 last_progress,
3840 no_progress_timeout,
3841 cancellation,
3842 ));
3843 }
3844 Ok(DiagnosticReaderEvent::FenceObserved) => {
3845 self.pending_diagnostic_fences = self
3846 .pending_diagnostic_fences
3847 .checked_add(1)
3848 .ok_or_else(|| ParserSupervisorError::IoThread {
3849 phase,
3850 message: "diagnostic fence count overflowed".to_owned(),
3851 })?;
3852 }
3853 Ok(DiagnosticReaderEvent::AdmissionAccepted) => {}
3854 Err(TryRecvError::Empty | TryRecvError::Disconnected) => return Ok(()),
3855 }
3856 }
3857 }
3858
3859 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3861 fn enforce_memory_bound(
3862 &mut self,
3863 phase: &'static str,
3864 force: bool,
3865 ) -> Result<(), ParserSupervisorError> {
3866 let monitor_event = self
3867 .memory_monitor
3868 .as_ref()
3869 .and_then(|monitor| monitor.events.try_recv().ok());
3870 if let Some(event) = monitor_event {
3871 return Err(self.memory_monitor_error(phase, event));
3872 }
3873 if !force {
3874 return Ok(());
3875 }
3876 let process_id = self.child.id();
3877 let observation = match self.memory_observer.lock() {
3878 Ok(mut observer) => observer.observe(process_id),
3879 Err(_poisoned) => Err(io::Error::other("Linux memory observer lock was poisoned")),
3880 };
3881 let observation = match observation {
3882 Ok(observation) => observation,
3883 Err(source) => {
3884 let observer = Arc::clone(&self.memory_observer);
3885 let child = &mut self.child;
3886 let transition = resolve_linux_memory_exit_transition(
3887 source,
3888 SUPERVISOR_POLL_INTERVAL,
3889 || match observer.try_lock() {
3890 Ok(mut observer) => observer.observe(process_id),
3891 Err(std::sync::TryLockError::WouldBlock) => Err(io::Error::new(
3892 io::ErrorKind::WouldBlock,
3893 "Linux memory observer is busy",
3894 )),
3895 Err(std::sync::TryLockError::Poisoned(_poisoned)) => {
3896 Err(io::Error::other("Linux memory observer lock was poisoned"))
3897 }
3898 },
3899 || {
3900 child.try_wait().map(|status| {
3901 status.map(|status| LinuxChildExit {
3902 code: status.code(),
3903 })
3904 })
3905 },
3906 );
3907 match transition {
3908 Ok(LinuxMemoryObservation::Memory(observation)) => observation,
3909 Ok(LinuxMemoryObservation::ChildExited { code }) => {
3910 return Err(ParserSupervisorError::ChildExited { phase, code });
3911 }
3912 Err(source) => {
3913 self.termination_requested = true;
3914 let operation = ParserSupervisorError::ResidentMemoryObservationFailed {
3915 phase,
3916 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
3917 message: bounded_message(source.to_string()),
3918 };
3919 return Err(attach_cleanup(
3920 operation,
3921 kill_direct_child(&mut self.child),
3922 ));
3923 }
3924 }
3925 }
3926 };
3927 let Some(breach) = observation else {
3928 return Ok(());
3929 };
3930 self.termination_requested = true;
3931 let observation_interval_millis =
3932 u64::try_from(PARSER_LINUX_RSS_OBSERVATION_INTERVAL.as_millis()).unwrap_or(u64::MAX);
3933 let operation = ParserSupervisorError::ResidentMemoryLimitExceeded {
3934 phase,
3935 accounting: breach.accounting,
3936 observed_bytes: breach.observed_bytes,
3937 maximum_bytes: self.memory_limits.process_bytes,
3938 observation_interval_millis,
3939 };
3940 Err(attach_cleanup(
3941 operation,
3942 kill_direct_child(&mut self.child),
3943 ))
3944 }
3945
3946 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3948 fn memory_monitor_error(
3949 &mut self,
3950 phase: &'static str,
3951 event: LinuxMemoryMonitorEvent,
3952 ) -> ParserSupervisorError {
3953 let (operation, termination_error) = match event {
3954 LinuxMemoryMonitorEvent::Limit {
3955 breach,
3956 termination_error,
3957 } => {
3958 self.termination_requested = true;
3959 (
3960 ParserSupervisorError::ResidentMemoryLimitExceeded {
3961 phase,
3962 accounting: breach.accounting,
3963 observed_bytes: breach.observed_bytes,
3964 maximum_bytes: self.memory_limits.process_bytes,
3965 observation_interval_millis: u64::try_from(
3966 PARSER_LINUX_RSS_OBSERVATION_INTERVAL.as_millis(),
3967 )
3968 .unwrap_or(u64::MAX),
3969 },
3970 termination_error,
3971 )
3972 }
3973 LinuxMemoryMonitorEvent::ObservationFailed {
3974 accounting,
3975 message,
3976 } => {
3977 let transition = resolve_linux_memory_exit_transition(
3978 io::Error::other(message.clone()),
3979 SUPERVISOR_POLL_INTERVAL,
3980 || Err(io::Error::other(message.clone())),
3981 || {
3982 self.child.try_wait().map(|status| {
3983 status.map(|status| LinuxChildExit {
3984 code: status.code(),
3985 })
3986 })
3987 },
3988 );
3989 if let Ok(LinuxMemoryObservation::ChildExited { code }) = transition {
3990 return ParserSupervisorError::ChildExited { phase, code };
3991 }
3992 self.termination_requested = true;
3993 return attach_cleanup(
3994 ParserSupervisorError::ResidentMemoryObservationFailed {
3995 phase,
3996 accounting,
3997 message,
3998 },
3999 kill_direct_child(&mut self.child),
4000 );
4001 }
4002 };
4003 let Some(termination_error) = termination_error else {
4004 return operation;
4005 };
4006 let retry = kill_direct_child(&mut self.child);
4007 let initial = ParserSupervisorError::Cleanup {
4008 message: format!(
4009 "continuous memory monitor could not terminate the worker process group: {termination_error}"
4010 ),
4011 };
4012 let cleanup = match retry {
4013 Ok(()) => initial,
4014 Err(retry) => ParserSupervisorError::OperationAndCleanup {
4015 operation: Box::new(initial),
4016 cleanup: Box::new(retry),
4017 },
4018 };
4019 ParserSupervisorError::OperationAndCleanup {
4020 operation: Box::new(operation),
4021 cleanup: Box::new(cleanup),
4022 }
4023 }
4024
4025 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4027 fn observe_shutdown_memory(&mut self, failure: &mut Option<ParserSupervisorError>) {
4028 if failure.is_some() {
4029 return;
4030 }
4031 match self.enforce_memory_bound("shutdown", true) {
4032 Ok(()) | Err(ParserSupervisorError::ChildExited { .. }) => {}
4033 Err(error) => *failure = Some(error),
4034 }
4035 }
4036
4037 fn require_child_running(&mut self, phase: &'static str) -> Result<(), ParserSupervisorError> {
4039 match self
4040 .child
4041 .try_wait()
4042 .map_err(|source| ParserSupervisorError::IoThread {
4043 phase,
4044 message: source.to_string(),
4045 })? {
4046 None => Ok(()),
4047 Some(status) => Err(ParserSupervisorError::ChildExited {
4048 phase,
4049 code: status.code(),
4050 }),
4051 }
4052 }
4053
4054 fn shutdown(mut self) -> Result<(), ParserSupervisorError> {
4056 let mut failures = Vec::new();
4057 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4058 let mut memory_failure = None;
4059 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4060 if let Some(mut monitor) = self.memory_monitor.take() {
4061 match monitor.stop() {
4062 Ok(Some(event)) => {
4063 memory_failure = Some(self.memory_monitor_error("idle resident", event));
4064 }
4065 Ok(None) => {}
4066 Err(error) => failures.push(error.to_string()),
4067 }
4068 }
4069 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4070 self.observe_shutdown_memory(&mut memory_failure);
4071 self.writer.take();
4072 let graceful_deadline = Instant::now()
4073 .checked_add(SUPERVISOR_GRACEFUL_CLOSE)
4074 .unwrap_or_else(Instant::now);
4075 while self
4076 .writer_handle
4077 .as_ref()
4078 .is_some_and(|handle| !handle.is_finished())
4079 && Instant::now() < graceful_deadline
4080 {
4081 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4082 self.observe_shutdown_memory(&mut memory_failure);
4083 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4084 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4085 }
4086 let mut forced = self.termination_requested;
4087 let mut status = match self.child.try_wait() {
4088 Ok(status) => status,
4089 Err(source) => {
4090 failures.push(source.to_string());
4091 None
4092 }
4093 };
4094 while status.is_none() && Instant::now() < graceful_deadline {
4095 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4096 self.observe_shutdown_memory(&mut memory_failure);
4097 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4098 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4099 match self.child.try_wait() {
4100 Ok(observed) => status = observed,
4101 Err(source) => {
4102 failures.push(source.to_string());
4103 break;
4104 }
4105 }
4106 }
4107 if status.is_none() {
4108 forced = true;
4109 if let Err(error) = kill_direct_child(&mut self.child) {
4110 failures.push(error.to_string());
4111 }
4112 }
4113 let cleanup_deadline = Instant::now()
4114 .checked_add(SUPERVISOR_CLEANUP_TIMEOUT)
4115 .unwrap_or_else(Instant::now);
4116 while status.is_none() && Instant::now() < cleanup_deadline {
4117 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4118 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4119 match self.child.try_wait() {
4120 Ok(observed) => status = observed,
4121 Err(source) => {
4122 failures.push(source.to_string());
4123 break;
4124 }
4125 }
4126 }
4127 if status.is_none() {
4128 failures.push("direct child was not reaped within the cleanup deadline".to_owned());
4129 }
4130 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4131 if status.is_some() {
4132 let cleanup = match self.memory_observer.lock() {
4133 Ok(mut observer) => observer.cleanup(),
4134 Err(_poisoned) => Err(io::Error::other(
4135 "Linux memory observer lock was poisoned during cleanup",
4136 )),
4137 };
4138 if let Err(source) = cleanup {
4139 failures.push(format!(
4140 "delegated Linux memory-accounting cleanup failed: {source}"
4141 ));
4142 }
4143 }
4144
4145 while (!thread_finished(self.writer_handle.as_ref())
4146 || !thread_finished(self.frame_reader.handle.as_ref())
4147 || !thread_finished(self.diagnostic_reader.handle.as_ref()))
4148 && Instant::now() < cleanup_deadline
4149 {
4150 drain_reader_events(&self.frame_reader.events, &self.diagnostic_reader.events);
4151 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4152 }
4153 if !thread_finished(self.writer_handle.as_ref())
4154 || !thread_finished(self.frame_reader.handle.as_ref())
4155 || !thread_finished(self.diagnostic_reader.handle.as_ref())
4156 {
4157 failures
4158 .push("protocol I/O threads did not drain within the cleanup deadline".to_owned());
4159 }
4160 if thread_finished(self.writer_handle.as_ref())
4161 && let Err(error) = join_unit_thread(self.writer_handle.take(), "writer")
4162 {
4163 failures.push(error.to_string());
4164 }
4165 if thread_finished(self.frame_reader.handle.as_ref())
4166 && let Err(error) = join_unit_thread(self.frame_reader.handle.take(), "stdout reader")
4167 {
4168 failures.push(error.to_string());
4169 }
4170 let diagnostics = if thread_finished(self.diagnostic_reader.handle.as_ref()) {
4171 match join_diagnostic_thread(self.diagnostic_reader.handle.take()) {
4172 Ok(diagnostics) => diagnostics,
4173 Err(error) => {
4174 failures.push(error.to_string());
4175 Vec::new()
4176 }
4177 }
4178 } else {
4179 Vec::new()
4180 };
4181 forced |= self.termination_requested;
4182 if !forced && status.as_ref().is_some_and(|status| !status.success()) {
4183 failures.push(format!(
4184 "direct child reported failed cleanup; diagnostic={}",
4185 bounded_diagnostic(&diagnostics)
4186 ));
4187 }
4188 if !forced && status.as_ref().is_some_and(ExitStatus::success) && !diagnostics.is_empty() {
4189 failures.push(format!(
4190 "direct child emitted unexpected diagnostic bytes: {}",
4191 bounded_diagnostic(&diagnostics)
4192 ));
4193 }
4194 let cleanup = (!failures.is_empty()).then(|| ParserSupervisorError::Cleanup {
4195 message: bounded_message(failures.join("; ")),
4196 });
4197 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4198 if let Some(operation) = memory_failure {
4199 return match cleanup {
4200 Some(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
4201 operation: Box::new(operation),
4202 cleanup: Box::new(cleanup),
4203 }),
4204 None => Err(operation),
4205 };
4206 }
4207 match cleanup {
4208 None => Ok(()),
4209 Some(cleanup) => Err(cleanup),
4210 }
4211 }
4212}
4213
4214#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4216#[expect(
4217 unsafe_code,
4218 reason = "Command has no safe descriptor-sanitizing hook; this pre-exec closure performs only async-signal-safe Linux syscalls"
4219)]
4220fn inherit_linux_authority_on_exec(command: &mut Command, authority: LinuxResidentLaunchAuthority) {
4221 use nix::libc;
4222 use std::os::unix::process::CommandExt;
4223
4224 let inherited = [
4225 authority.artifact_manifest.raw_fd(),
4226 authority.accepted_manifest.raw_fd(),
4227 authority.native_import_policy.raw_fd(),
4228 authority.grammar.raw_fd(),
4229 ];
4230 unsafe {
4235 command.pre_exec(move || {
4236 let _authority_guard = &authority;
4237 let result = libc::syscall(
4238 libc::SYS_close_range,
4239 3_u32,
4240 u32::MAX,
4241 libc::CLOSE_RANGE_CLOEXEC | libc::CLOSE_RANGE_UNSHARE,
4242 );
4243 if result != 0 {
4244 return Err(io::Error::last_os_error());
4245 }
4246 for descriptor in inherited {
4247 if libc::fcntl(descriptor, libc::F_SETFD, 0) != 0 {
4248 return Err(io::Error::last_os_error());
4249 }
4250 }
4251 Ok(())
4252 });
4253 }
4254}
4255
4256#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4258fn platform_command(
4259 launch: &VerifiedParserPackLaunch,
4260 authority: LinuxResidentLaunchAuthority,
4261 _memory_limits: ParserMemoryLimits,
4262) -> Result<Command, ParserSupervisorError> {
4263 use std::os::unix::process::CommandExt;
4264
4265 if launch.platform != PackPlatform::LinuxX86_64 {
4266 return Err(ParserSupervisorError::PayloadMismatch {
4267 path: launch.pack_root.clone(),
4268 reason: "Linux supervisor received another platform artifact",
4269 });
4270 }
4271 let worker_fd = authority.worker.raw_fd();
4272 let artifact_fd = authority.artifact_manifest.raw_fd();
4273 let accepted_fd = authority.accepted_manifest.raw_fd();
4274 let policy_fd = authority.native_import_policy.raw_fd();
4275 let grammar_fd = authority.grammar.raw_fd();
4276 let mut command = Command::new(format!("/proc/self/fd/{worker_fd}"));
4277 command
4278 .arg(SERVE_ARGUMENT)
4279 .arg(ARTIFACT_FD_ARGUMENT)
4280 .arg(artifact_fd.to_string())
4281 .arg(ACCEPTED_FD_ARGUMENT)
4282 .arg(accepted_fd.to_string())
4283 .arg(POLICY_FD_ARGUMENT)
4284 .arg(policy_fd.to_string())
4285 .arg(GRAMMAR_FD_ARGUMENT)
4286 .arg(grammar_fd.to_string())
4287 .current_dir(&launch.pack_root)
4288 .env_clear()
4289 .stdin(Stdio::piped())
4290 .stdout(Stdio::piped())
4291 .stderr(Stdio::piped())
4292 .process_group(0);
4293 inherit_linux_authority_on_exec(&mut command, authority);
4294 Ok(command)
4295}
4296
4297#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4299fn platform_command(
4300 launch: &VerifiedParserPackLaunch,
4301 memory_limits: ParserMemoryLimits,
4302) -> Result<Command, ParserSupervisorError> {
4303 use std::os::windows::process::CommandExt;
4304
4305 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
4306 if launch.platform != PackPlatform::WindowsX86_64 {
4307 return Err(ParserSupervisorError::PayloadMismatch {
4308 path: launch.pack_root.clone(),
4309 reason: "Windows supervisor received another platform artifact",
4310 });
4311 }
4312 let broker = launch.containment_broker.as_ref().ok_or_else(|| {
4313 ParserSupervisorError::PayloadMismatch {
4314 path: launch.pack_root.clone(),
4315 reason: "Windows artifact has no containment broker",
4316 }
4317 })?;
4318 let mut command = Command::new(broker);
4319 command
4320 .arg(BROKER_SERVE_ARGUMENT)
4321 .arg("--parent-pid")
4322 .arg(std::process::id().to_string())
4323 .arg("--process-memory-bytes")
4324 .arg(memory_limits.process_bytes.to_string())
4325 .arg("--job-memory-bytes")
4326 .arg(memory_limits.process_tree_bytes.to_string())
4327 .current_dir(&launch.pack_root)
4328 .env_clear()
4329 .stdin(Stdio::piped())
4330 .stdout(Stdio::piped())
4331 .stderr(Stdio::piped())
4332 .creation_flags(CREATE_NO_WINDOW);
4333 Ok(command)
4334}
4335
4336#[cfg(not(any(
4338 all(target_os = "linux", target_arch = "x86_64"),
4339 all(target_os = "windows", target_arch = "x86_64")
4340)))]
4341fn platform_command(
4342 _launch: &VerifiedParserPackLaunch,
4343 _memory_limits: ParserMemoryLimits,
4344) -> Result<Command, ParserSupervisorError> {
4345 Err(ParserSupervisorError::UnsupportedContainment {
4346 os: std::env::consts::OS,
4347 architecture: std::env::consts::ARCH,
4348 })
4349}
4350
4351const fn containment_for_platform(platform: PackPlatform) -> ParserContainmentKind {
4353 match platform {
4354 PackPlatform::LinuxX86_64 => ParserContainmentKind::LinuxLandlockSeccomp,
4355 PackPlatform::WindowsX86_64 => ParserContainmentKind::WindowsAppContainerJob,
4356 }
4357}
4358
4359fn fresh_session_identity() -> Result<ParserSessionIdentity, ParserSupervisorError> {
4361 let mut entropy = [0_u8; PARSER_SESSION_ENTROPY_BYTES];
4362 getrandom::fill(&mut entropy).map_err(|_source| ParserSupervisorError::EntropyUnavailable)?;
4363 Ok(ParserSessionIdentity::for_entropy(&entropy))
4364}
4365
4366fn fresh_diagnostic_fence() -> Result<DiagnosticFence, ParserSupervisorError> {
4368 let mut entropy = [0_u8; PARSER_DIAGNOSTIC_FENCE_BYTES];
4369 getrandom::fill(&mut entropy).map_err(|_source| ParserSupervisorError::EntropyUnavailable)?;
4370 Ok(DiagnosticFence(entropy))
4371}
4372
4373fn io_thread_error(phase: &'static str, error: &ParserIoThreadError) -> ParserSupervisorError {
4375 ParserSupervisorError::IoThread {
4376 phase,
4377 message: bounded_message(error.to_string()),
4378 }
4379}
4380
4381fn diagnostic_failure_after_exit_observation(
4383 child: &mut Child,
4384 phase: &'static str,
4385 error: &ParserIoThreadError,
4386 absolute_deadline: Instant,
4387 last_progress: Instant,
4388 no_progress_timeout: Duration,
4389 cancellation: &IndexCancellation,
4390) -> ParserSupervisorError {
4391 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4392 {
4393 let no_progress_deadline = last_progress
4394 .checked_add(no_progress_timeout)
4395 .unwrap_or(absolute_deadline);
4396 let observation_deadline = absolute_deadline.min(no_progress_deadline);
4397 loop {
4398 match child.try_wait() {
4399 Ok(Some(status))
4400 if status.code() == Some(PARSER_WINDOWS_BROKER_MEMORY_LIMIT_EXIT_CODE) =>
4401 {
4402 return ParserSupervisorError::WindowsJobMemoryLimitExceeded { phase };
4403 }
4404 Ok(Some(_)) | Err(_) => break,
4405 Ok(None) => {}
4406 }
4407 let now = Instant::now();
4408 if cancellation.is_cancelled() || now >= observation_deadline {
4409 break;
4410 }
4411 thread::sleep(SUPERVISOR_POLL_INTERVAL.min(observation_deadline.duration_since(now)));
4412 }
4413 }
4414 #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
4415 let _ = (
4416 child,
4417 absolute_deadline,
4418 last_progress,
4419 no_progress_timeout,
4420 cancellation,
4421 );
4422 io_thread_error(phase, error)
4423}
4424
4425fn bounded_message(mut message: String) -> String {
4427 let mut end = message.len().min(PARSER_MAX_STDERR_BYTES);
4428 while !message.is_char_boundary(end) {
4429 end = end.saturating_sub(1);
4430 }
4431 message.truncate(end);
4432 message
4433}
4434
4435fn bounded_diagnostic(bytes: &[u8]) -> String {
4437 bounded_message(String::from_utf8_lossy(bytes).into_owned())
4438}
4439
4440fn poll_stop(
4442 phase: &'static str,
4443 absolute_deadline: Instant,
4444 last_progress: Instant,
4445 no_progress_timeout: Duration,
4446 cancellation: &IndexCancellation,
4447) -> Result<(), ParserSupervisorError> {
4448 if cancellation.is_cancelled() {
4449 return Err(ParserSupervisorError::Cancelled { phase });
4450 }
4451 let now = Instant::now();
4452 if now >= absolute_deadline {
4453 return Err(ParserSupervisorError::DeadlineExceeded { phase });
4454 }
4455 if now.saturating_duration_since(last_progress) >= no_progress_timeout {
4456 return Err(ParserSupervisorError::NoProgress { phase });
4457 }
4458 Ok(())
4459}
4460
4461fn next_poll_wait(
4463 absolute_deadline: Instant,
4464 last_progress: Instant,
4465 no_progress_timeout: Duration,
4466) -> Duration {
4467 let now = Instant::now();
4468 let deadline_wait = absolute_deadline.saturating_duration_since(now);
4469 let progress_wait =
4470 no_progress_timeout.saturating_sub(now.saturating_duration_since(last_progress));
4471 SUPERVISOR_POLL_INTERVAL
4472 .min(deadline_wait)
4473 .min(progress_wait)
4474}
4475
4476fn try_frame_event(
4478 events: &Receiver<FrameReaderEvent>,
4479) -> Result<Option<FrameReaderEvent>, ParserSupervisorError> {
4480 match events.try_recv() {
4481 Ok(event) => Ok(Some(event)),
4482 Err(TryRecvError::Empty) => Ok(None),
4483 Err(TryRecvError::Disconnected) => Err(ParserSupervisorError::IoThread {
4484 phase: "stdout reader",
4485 message: "stdout reader closed without a terminal event".to_owned(),
4486 }),
4487 }
4488}
4489
4490fn frame_event_result(
4492 event: FrameReaderEvent,
4493 child: &mut Child,
4494 phase: &'static str,
4495) -> Result<Vec<u8>, ParserSupervisorError> {
4496 match event {
4497 FrameReaderEvent::Frame(frame) => Ok(frame),
4498 FrameReaderEvent::EndOfStream => {
4499 let status = wait_for_observed_exit(child, SUPERVISOR_POLL_INTERVAL)?;
4500 let code = status.and_then(|status| status.code());
4501 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4502 if code == Some(PARSER_WINDOWS_BROKER_MEMORY_LIMIT_EXIT_CODE) {
4503 return Err(ParserSupervisorError::WindowsJobMemoryLimitExceeded { phase });
4504 }
4505 Err(ParserSupervisorError::ChildExited { phase, code })
4506 }
4507 FrameReaderEvent::Failure(error) => Err(io_thread_error(phase, &error)),
4508 }
4509}
4510
4511fn wait_for_observed_exit(
4513 child: &mut Child,
4514 timeout: Duration,
4515) -> Result<Option<ExitStatus>, ParserSupervisorError> {
4516 let deadline = Instant::now()
4517 .checked_add(timeout)
4518 .unwrap_or_else(Instant::now);
4519 loop {
4520 if let Some(status) =
4521 child
4522 .try_wait()
4523 .map_err(|source| ParserSupervisorError::IoThread {
4524 phase: "child exit observation",
4525 message: source.to_string(),
4526 })?
4527 {
4528 return Ok(Some(status));
4529 }
4530 if Instant::now() >= deadline {
4531 return Ok(None);
4532 }
4533 thread::sleep(Duration::from_millis(1));
4534 }
4535}
4536
4537#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4539fn terminate_linux_process_group(process_id: u32) -> Result<LinuxProcessGroupTermination, String> {
4540 use nix::errno::Errno;
4541 use nix::sys::signal::{Signal, killpg};
4542 use nix::unistd::Pid;
4543
4544 let process_group = i32::try_from(process_id)
4545 .map_err(|_source| "worker process-group identity exceeds i32".to_owned())?;
4546 match killpg(Pid::from_raw(process_group), Signal::SIGKILL) {
4547 Ok(()) => Ok(LinuxProcessGroupTermination::Signalled),
4548 Err(Errno::ESRCH) => Ok(LinuxProcessGroupTermination::Absent),
4549 Err(source) => Err(source.to_string()),
4550 }
4551}
4552
4553#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
4555fn kill_direct_child(child: &mut Child) -> Result<(), ParserSupervisorError> {
4556 if child
4557 .try_wait()
4558 .map_err(|source| ParserSupervisorError::Cleanup {
4559 message: source.to_string(),
4560 })?
4561 .is_some()
4562 {
4563 return Ok(());
4564 }
4565 match terminate_linux_process_group(child.id()) {
4566 Ok(LinuxProcessGroupTermination::Signalled) => Ok(()),
4567 Ok(LinuxProcessGroupTermination::Absent) => {
4568 if child
4569 .try_wait()
4570 .map_err(|source| ParserSupervisorError::Cleanup {
4571 message: source.to_string(),
4572 })?
4573 .is_none()
4574 {
4575 child
4576 .kill()
4577 .map_err(|source| ParserSupervisorError::Cleanup {
4578 message: format!(
4579 "worker process group was absent and direct-child termination failed: {source}"
4580 ),
4581 })?;
4582 }
4583 Ok(())
4584 }
4585 Err(group_error) => {
4586 let direct_error = child.kill().err().map(|error| error.to_string());
4587 Err(ParserSupervisorError::Cleanup {
4588 message: direct_error.map_or_else(
4589 || format!("process-group termination failed: {group_error}"),
4590 |direct_error| {
4591 format!(
4592 "process-group termination failed: {group_error}; direct-child termination failed: {direct_error}"
4593 )
4594 },
4595 ),
4596 })
4597 }
4598 }
4599}
4600
4601#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
4603fn kill_direct_child(child: &mut Child) -> Result<(), ParserSupervisorError> {
4604 if child
4605 .try_wait()
4606 .map_err(|source| ParserSupervisorError::Cleanup {
4607 message: source.to_string(),
4608 })?
4609 .is_none()
4610 {
4611 child
4612 .kill()
4613 .map_err(|source| ParserSupervisorError::Cleanup {
4614 message: source.to_string(),
4615 })?;
4616 }
4617 Ok(())
4618}
4619
4620fn cleanup_partial_launch(
4622 child: &mut Child,
4623 handles: Vec<JoinHandle<()>>,
4624 diagnostic_handle: Option<JoinHandle<Result<Vec<u8>, ParserIoThreadError>>>,
4625 frame_events: Option<Receiver<FrameReaderEvent>>,
4626 diagnostic_events: Option<Receiver<DiagnosticReaderEvent>>,
4627) -> Result<(), ParserSupervisorError> {
4628 drop(frame_events);
4629 drop(diagnostic_events);
4630 let mut failures = Vec::new();
4631 if let Err(error) = kill_direct_child(child) {
4632 failures.push(error.to_string());
4633 }
4634 let deadline = Instant::now()
4635 .checked_add(SUPERVISOR_CLEANUP_TIMEOUT)
4636 .unwrap_or_else(Instant::now);
4637 let mut reaped = false;
4638 while Instant::now() < deadline {
4639 match child.try_wait() {
4640 Ok(Some(_status)) => reaped = true,
4641 Ok(None) => {}
4642 Err(source) => {
4643 failures.push(source.to_string());
4644 break;
4645 }
4646 }
4647 if reaped
4648 && handles.iter().all(JoinHandle::is_finished)
4649 && thread_finished(diagnostic_handle.as_ref())
4650 {
4651 break;
4652 }
4653 thread::sleep(SUPERVISOR_POLL_INTERVAL);
4654 }
4655 if !reaped {
4656 failures.push("incomplete direct child was not reaped".to_owned());
4657 }
4658 for handle in handles {
4659 if !handle.is_finished() {
4660 failures.push("incomplete launch thread did not terminate".to_owned());
4661 } else if handle.join().is_err() {
4662 failures.push("incomplete launch thread panicked".to_owned());
4663 }
4664 }
4665 if thread_finished(diagnostic_handle.as_ref()) {
4666 if let Err(error) = join_diagnostic_thread(diagnostic_handle) {
4667 failures.push(error.to_string());
4668 }
4669 } else if diagnostic_handle.is_some() {
4670 failures.push("incomplete diagnostic thread did not terminate".to_owned());
4671 }
4672 if failures.is_empty() {
4673 Ok(())
4674 } else {
4675 Err(ParserSupervisorError::Cleanup {
4676 message: bounded_message(failures.join("; ")),
4677 })
4678 }
4679}
4680
4681fn attach_cleanup(
4683 operation: ParserSupervisorError,
4684 cleanup: Result<(), ParserSupervisorError>,
4685) -> ParserSupervisorError {
4686 match cleanup {
4687 Ok(()) => operation,
4688 Err(cleanup) => ParserSupervisorError::OperationAndCleanup {
4689 operation: Box::new(operation),
4690 cleanup: Box::new(cleanup),
4691 },
4692 }
4693}
4694
4695fn drain_reader_events(
4697 frames: &Receiver<FrameReaderEvent>,
4698 diagnostics: &Receiver<DiagnosticReaderEvent>,
4699) {
4700 while frames.try_recv().is_ok() {}
4701 while diagnostics.try_recv().is_ok() {}
4702}
4703
4704fn thread_finished<T>(handle: Option<&JoinHandle<T>>) -> bool {
4706 handle.is_none_or(JoinHandle::is_finished)
4707}
4708
4709fn join_unit_thread(
4711 handle: Option<JoinHandle<()>>,
4712 name: &'static str,
4713) -> Result<(), ParserSupervisorError> {
4714 let Some(handle) = handle else {
4715 return Ok(());
4716 };
4717 handle
4718 .join()
4719 .map_err(|_panic| ParserSupervisorError::Cleanup {
4720 message: format!("{name} thread panicked"),
4721 })
4722}
4723
4724fn join_diagnostic_thread(
4726 handle: Option<JoinHandle<Result<Vec<u8>, ParserIoThreadError>>>,
4727) -> Result<Vec<u8>, ParserSupervisorError> {
4728 let Some(handle) = handle else {
4729 return Ok(Vec::new());
4730 };
4731 let result = handle
4732 .join()
4733 .map_err(|_panic| ParserSupervisorError::Cleanup {
4734 message: "diagnostic reader thread panicked".to_owned(),
4735 })?;
4736 result.map_err(|error| ParserSupervisorError::Cleanup {
4737 message: bounded_message(error.to_string()),
4738 })
4739}
4740
4741pub struct OptionalParserSupervisor {
4743 pack_root: PathBuf,
4745 launch: VerifiedParserPackLaunch,
4747 memory_limits: ParserMemoryLimits,
4749 resident: Option<ResidentParserSession>,
4751}
4752
4753impl OptionalParserSupervisor {
4754 pub fn open(pack_root: impl AsRef<Path>) -> Result<Self, ParserSupervisorError> {
4763 Self::open_with_memory_limits(pack_root, ParserMemoryLimits::PRODUCTION)
4764 }
4765
4766 fn open_with_memory_limits(
4768 pack_root: impl AsRef<Path>,
4769 memory_limits: ParserMemoryLimits,
4770 ) -> Result<Self, ParserSupervisorError> {
4771 let pack_root = pack_root.as_ref().to_path_buf();
4772 let launch = VerifiedParserPackLaunch::load(&pack_root)?;
4773 Ok(Self {
4774 pack_root,
4775 launch,
4776 memory_limits: memory_limits.checked()?,
4777 resident: None,
4778 })
4779 }
4780
4781 #[must_use]
4783 pub(crate) const fn artifact_identity(&self) -> &ParserArtifactIdentity {
4784 &self.launch.artifact
4785 }
4786
4787 #[must_use]
4789 pub(crate) fn pack_root(&self) -> &Path {
4790 &self.pack_root
4791 }
4792
4793 #[must_use]
4795 pub(crate) fn accepts_language(&self, language_id: &str) -> bool {
4796 self.launch
4797 .accepted_grammars
4798 .binary_search_by(|candidate| candidate.as_str().cmp(language_id))
4799 .is_ok()
4800 }
4801
4802 pub fn parse(
4816 &mut self,
4817 language_id: &str,
4818 source: &[u8],
4819 limits: ParserRequestLimits,
4820 absolute_deadline: Instant,
4821 no_progress_timeout: Duration,
4822 cancellation: &IndexCancellation,
4823 ) -> Result<ParserCompletionEvidence, ParserSupervisorError> {
4824 let last_progress = Instant::now();
4825 poll_stop(
4826 "request admission",
4827 absolute_deadline,
4828 last_progress,
4829 no_progress_timeout,
4830 cancellation,
4831 )?;
4832 let source_identity = ParserSourceIdentity::for_bytes(source)?;
4833 self.refresh_changed_artifact(
4834 language_id,
4835 last_progress,
4836 absolute_deadline,
4837 no_progress_timeout,
4838 cancellation,
4839 )?;
4840 if let Some(resident) = self.resident.as_ref() {
4841 let grammar_changed = self
4842 .launch
4843 .require_grammar(language_id)
4844 .map_or(true, |grammar| resident.grammar != grammar);
4845 if grammar_changed {
4846 self.shutdown_resident()?;
4847 }
4848 }
4849 let mut resident_opened = false;
4850 if self.resident.is_none() {
4851 let grammar = self.launch.require_grammar(language_id)?;
4852 self.resident = Some(ResidentParserSession::launch(
4853 &self.launch,
4854 grammar,
4855 self.memory_limits,
4856 last_progress,
4857 absolute_deadline,
4858 no_progress_timeout,
4859 cancellation,
4860 )?);
4861 resident_opened = true;
4862 }
4863 let request_last_progress = if resident_opened {
4864 Instant::now()
4865 } else {
4866 last_progress
4867 };
4868 let result = self
4869 .resident
4870 .as_mut()
4871 .ok_or_else(|| ParserSupervisorError::IoThread {
4872 phase: "request admission",
4873 message: "resident session was not retained".to_owned(),
4874 })?
4875 .parse(
4876 source,
4877 source_identity,
4878 limits,
4879 request_last_progress,
4880 absolute_deadline,
4881 no_progress_timeout,
4882 cancellation,
4883 );
4884 match result {
4885 Ok(evidence) => Ok(evidence),
4886 Err(operation) => {
4887 if operation.is_caller_stop()
4888 && let Some(resident) = self.resident.as_mut()
4889 {
4890 resident.termination_requested = true;
4891 }
4892 match self.take_and_shutdown_resident() {
4893 Ok(()) => Err(operation),
4894 Err(cleanup) => Err(ParserSupervisorError::OperationAndCleanup {
4895 operation: Box::new(operation),
4896 cleanup: Box::new(cleanup),
4897 }),
4898 }
4899 }
4900 }
4901 }
4902
4903 pub fn shutdown(&mut self) -> Result<(), ParserSupervisorError> {
4910 self.shutdown_resident()
4911 }
4912
4913 fn refresh_changed_artifact(
4915 &mut self,
4916 language_id: &str,
4917 last_progress: Instant,
4918 absolute_deadline: Instant,
4919 no_progress_timeout: Duration,
4920 cancellation: &IndexCancellation,
4921 ) -> Result<(), ParserSupervisorError> {
4922 let control = ArtifactIoControl {
4923 absolute_deadline,
4924 last_progress,
4925 no_progress_timeout,
4926 cancellation,
4927 };
4928 let probe = self.launch.currentness_probe(language_id);
4929 let current = match run_bounded_artifact_currentness(probe, &control) {
4930 Ok(current) => current,
4931 Err(operation) => {
4932 return Err(attach_cleanup(operation, self.take_and_shutdown_resident()));
4933 }
4934 };
4935 if current {
4936 return Ok(());
4937 }
4938 self.shutdown_resident()?;
4939 let refreshed = VerifiedParserPackLaunch::load_controlled(
4940 &self.pack_root,
4941 language_id,
4942 last_progress,
4943 absolute_deadline,
4944 no_progress_timeout,
4945 cancellation,
4946 )?;
4947 self.replace_verified_launch(refreshed)
4948 }
4949
4950 fn replace_verified_launch(
4952 &mut self,
4953 refreshed: VerifiedParserPackLaunch,
4954 ) -> Result<(), ParserSupervisorError> {
4955 if refreshed.artifact != self.launch.artifact {
4956 return Err(ParserSupervisorError::PayloadMismatch {
4957 path: self.pack_root.join(ARTIFACT_MANIFEST_FILE_NAME),
4958 reason: "artifact identity changed inside its immutable slot",
4959 });
4960 }
4961 self.launch = refreshed;
4962 Ok(())
4963 }
4964
4965 fn shutdown_resident(&mut self) -> Result<(), ParserSupervisorError> {
4967 self.take_and_shutdown_resident()
4968 }
4969
4970 fn take_and_shutdown_resident(&mut self) -> Result<(), ParserSupervisorError> {
4972 match self.resident.take() {
4973 Some(resident) => resident.shutdown(),
4974 None => Ok(()),
4975 }
4976 }
4977}
4978
4979pub fn probe_optional_parser_memory_boundary(
4990 pack_root: impl AsRef<Path>,
4991 logical: &OptionalParserPackManifest,
4992) -> Result<ParserPackMemoryProbe, ParserSupervisorError> {
4993 let pack_root = pack_root.as_ref();
4994 logical.validate()?;
4995 let grammar =
4996 logical
4997 .grammars()
4998 .first()
4999 .ok_or_else(|| ParserSupervisorError::PayloadMismatch {
5000 path: pack_root.to_path_buf(),
5001 reason: "optional parser manifest has no grammar for the memory probe",
5002 })?;
5003 let platform = host_pack_platform().ok_or(ParserSupervisorError::UnsupportedContainment {
5004 os: std::env::consts::OS,
5005 architecture: std::env::consts::ARCH,
5006 })?;
5007 let process_bytes = match platform {
5008 PackPlatform::LinuxX86_64 => OPTIONAL_PARSER_PACK_LINUX_MEMORY_PROBE_BYTES,
5009 PackPlatform::WindowsX86_64 => OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES,
5010 };
5011 let memory_limits = ParserMemoryLimits {
5012 process_bytes,
5013 process_tree_bytes: process_bytes,
5014 }
5015 .checked()?;
5016 let probe_source = memory_probe_source(platform, grammar.fixtures.positive.source.as_bytes());
5017 let mut supervisor =
5018 OptionalParserSupervisor::open_with_memory_limits(pack_root, memory_limits)?;
5019 let limits = ParserRequestLimits::new(
5020 PARSER_MAX_OUTPUT_BYTES,
5021 PARSER_MAX_NODE_COUNT,
5022 PARSER_MAX_TREE_DEPTH,
5023 )?;
5024 let deadline = Instant::now()
5025 .checked_add(ARTIFACT_ADMISSION_TIMEOUT)
5026 .ok_or(ParserSupervisorError::DeadlineExceeded {
5027 phase: "memory probe deadline calculation",
5028 })?;
5029 let operation = supervisor.parse(
5030 &grammar.language_id,
5031 &probe_source,
5032 limits,
5033 deadline,
5034 ARTIFACT_ADMISSION_NO_PROGRESS_TIMEOUT,
5035 &IndexCancellation::new(),
5036 );
5037 let cleanup = supervisor.shutdown();
5038 let failure = match operation {
5039 Ok(_evidence) => match cleanup {
5040 Ok(()) => {
5041 return Err(ParserSupervisorError::MemoryProbeDidNotBreach { process_bytes });
5042 }
5043 Err(error) => error,
5044 },
5045 Err(error) => attach_cleanup(error, cleanup),
5046 };
5047
5048 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5049 if let ParserSupervisorError::ResidentMemoryLimitExceeded {
5050 accounting,
5051 observed_bytes,
5052 maximum_bytes,
5053 observation_interval_millis,
5054 ..
5055 } = &failure
5056 {
5057 let (control, interval, peak, overshoot) = match *accounting {
5058 ParserMemoryAccountingKind::LinuxCgroupV2 => {
5059 (ParserPackMemoryControl::LinuxCgroupV2, None, None, None)
5060 }
5061 ParserMemoryAccountingKind::LinuxProcStatus => (
5062 ParserPackMemoryControl::LinuxProcStatus,
5063 Some(*observation_interval_millis),
5064 Some(*observed_bytes),
5065 Some(observed_bytes.saturating_sub(*maximum_bytes)),
5066 ),
5067 };
5068 return Ok(ParserPackMemoryProbe {
5069 control,
5070 process_limit_bytes: *maximum_bytes,
5071 process_tree_limit_bytes: memory_limits.process_tree_bytes,
5072 observation_interval_millis: interval,
5073 peak_observed_bytes: peak,
5074 maximum_observed_overshoot_bytes: overshoot,
5075 limit_enforced: ParserPackVerifiedControl::Verified,
5076 process_tree_cleaned: ParserPackVerifiedControl::Verified,
5077 });
5078 }
5079
5080 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5081 if matches!(
5082 &failure,
5083 ParserSupervisorError::WindowsJobMemoryLimitExceeded { .. }
5084 ) {
5085 return Ok(ParserPackMemoryProbe {
5086 control: ParserPackMemoryControl::WindowsJobObject,
5087 process_limit_bytes: memory_limits.process_bytes,
5088 process_tree_limit_bytes: memory_limits.process_tree_bytes,
5089 observation_interval_millis: None,
5090 peak_observed_bytes: None,
5091 maximum_observed_overshoot_bytes: None,
5092 limit_enforced: ParserPackVerifiedControl::Verified,
5093 process_tree_cleaned: ParserPackVerifiedControl::Verified,
5094 });
5095 }
5096
5097 Err(failure)
5098}
5099
5100fn memory_probe_source(platform: PackPlatform, fixture: &[u8]) -> Vec<u8> {
5102 match platform {
5103 PackPlatform::LinuxX86_64 => fixture.to_vec(),
5104 PackPlatform::WindowsX86_64 => fixture
5105 .iter()
5106 .copied()
5107 .cycle()
5108 .take(WINDOWS_MEMORY_PROBE_SOURCE_BYTES.min(PARSER_MAX_SOURCE_BYTES as usize))
5109 .collect(),
5110 }
5111}
5112
5113pub fn admit_optional_parser_artifact(
5125 mut supervisor: OptionalParserSupervisor,
5126 logical: &OptionalParserPackManifest,
5127) -> Result<(), ParserSupervisorError> {
5128 let cancellation = IndexCancellation::new();
5129 let limits = ParserRequestLimits::new(
5130 PARSER_MAX_OUTPUT_BYTES,
5131 PARSER_MAX_NODE_COUNT,
5132 PARSER_MAX_TREE_DEPTH,
5133 )?;
5134 let aggregate_deadline = Instant::now()
5135 .checked_add(ARTIFACT_ADMISSION_AGGREGATE_TIMEOUT)
5136 .ok_or(ParserSupervisorError::DeadlineExceeded {
5137 phase: "artifact admission aggregate deadline calculation",
5138 })?;
5139 let operation = (|| {
5140 for grammar in logical.grammars() {
5141 let grammar_deadline = Instant::now()
5142 .checked_add(ARTIFACT_ADMISSION_TIMEOUT)
5143 .ok_or(ParserSupervisorError::DeadlineExceeded {
5144 phase: "artifact admission deadline calculation",
5145 })?;
5146 let deadline = grammar_deadline.min(aggregate_deadline);
5147 for (fixture, expected) in [
5148 (&grammar.fixtures.positive, false),
5149 (&grammar.fixtures.negative, true),
5150 ] {
5151 let evidence = supervisor.parse(
5152 &grammar.language_id,
5153 fixture.source.as_bytes(),
5154 limits,
5155 deadline,
5156 ARTIFACT_ADMISSION_NO_PROGRESS_TIMEOUT,
5157 &cancellation,
5158 )?;
5159 let actual = evidence.root_has_error();
5160 if actual != expected {
5161 return Err(ParserSupervisorError::FixtureExpectationMismatch {
5162 language_id: grammar.language_id.clone(),
5163 case_name: fixture.case_name.clone(),
5164 actual,
5165 expected,
5166 });
5167 }
5168 }
5169 }
5170 Ok(())
5171 })();
5172 let cleanup = supervisor.shutdown();
5173 match operation {
5174 Ok(()) => cleanup,
5175 Err(operation) => Err(attach_cleanup(operation, cleanup)),
5176 }
5177}
5178
5179impl Drop for OptionalParserSupervisor {
5180 fn drop(&mut self) {
5181 drop(self.take_and_shutdown_resident());
5182 }
5183}
5184
5185#[cfg(test)]
5187#[allow(dead_code)]
5188pub(crate) fn run_adversarial_process_suite(peer: &Path) -> io::Result<()> {
5189 #[derive(Clone, Copy)]
5190 enum ExpectedFailure {
5191 Cancelled,
5192 Deadline(&'static str),
5193 InvalidAdmission,
5194 Io,
5195 NoProgress,
5196 Progress(&'static str),
5197 Ready(&'static str),
5198 Response(&'static str),
5199 Limit(&'static str),
5200 InvalidControl(ParserFrameKind),
5201 Worker(ParserFailureCode),
5202 }
5203
5204 const RECOVERY_ALLOWANCE_PROBE_AGE: Duration = Duration::from_millis(550);
5205
5206 struct Case {
5207 scenario: &'static str,
5208 expected: ExpectedFailure,
5209 source_bytes: usize,
5210 cancel_before_launch: bool,
5211 cancellation_after_launch: Option<Duration>,
5212 deadline: Duration,
5213 deadline_after_launch: Option<Duration>,
5214 no_progress: Duration,
5215 limits: ParserRequestLimits,
5216 }
5217
5218 fn default_limits() -> io::Result<ParserRequestLimits> {
5219 ParserRequestLimits::new(4 * 1024, 16, 16)
5220 .map_err(|error| io::Error::other(error.to_string()))
5221 }
5222
5223 fn case(scenario: &'static str, expected: ExpectedFailure) -> io::Result<Case> {
5224 Ok(Case {
5225 scenario,
5226 expected,
5227 source_bytes: 32,
5228 cancel_before_launch: false,
5229 cancellation_after_launch: None,
5230 deadline: Duration::from_secs(2),
5231 deadline_after_launch: None,
5232 no_progress: Duration::from_millis(500),
5233 limits: default_limits()?,
5234 })
5235 }
5236
5237 fn error_matches(error: &ParserSupervisorError, expected: ExpectedFailure) -> bool {
5238 match (error, expected) {
5239 (ParserSupervisorError::Cancelled { .. }, ExpectedFailure::Cancelled)
5240 | (ParserSupervisorError::InvalidAdmission, ExpectedFailure::InvalidAdmission)
5241 | (ParserSupervisorError::IoThread { .. }, ExpectedFailure::Io)
5242 | (ParserSupervisorError::NoProgress { .. }, ExpectedFailure::NoProgress) => true,
5243 (
5244 ParserSupervisorError::DeadlineExceeded { .. },
5245 ExpectedFailure::Deadline(expected_phase),
5246 ) => adversarial_deadline_matches(error, expected_phase),
5247 (
5248 ParserSupervisorError::Protocol {
5249 source: ParserProtocolError::ProgressRegression { field },
5250 },
5251 ExpectedFailure::Progress(expected),
5252 )
5253 | (
5254 ParserSupervisorError::Protocol {
5255 source: ParserProtocolError::ReadyIdentityMismatch { field },
5256 },
5257 ExpectedFailure::Ready(expected),
5258 )
5259 | (
5260 ParserSupervisorError::Protocol {
5261 source: ParserProtocolError::ResponseIdentityMismatch { field },
5262 },
5263 ExpectedFailure::Response(expected),
5264 )
5265 | (
5266 ParserSupervisorError::Protocol {
5267 source: ParserProtocolError::RequestLimitExceeded { field, .. },
5268 },
5269 ExpectedFailure::Limit(expected),
5270 ) => field == &expected,
5271 (ParserSupervisorError::WorkerFailure { code }, ExpectedFailure::Worker(expected)) => {
5272 code == &expected
5273 }
5274 (
5275 ParserSupervisorError::Protocol {
5276 source: ParserProtocolError::InvalidControlJson { kind, .. },
5277 },
5278 ExpectedFailure::InvalidControl(expected),
5279 ) => kind == &expected,
5280 _ => false,
5281 }
5282 }
5283
5284 fn command_for(peer: &Path, scenario: &str) -> io::Result<Command> {
5285 let current_dir = peer
5286 .parent()
5287 .ok_or_else(|| io::Error::other("hostile peer path has no parent"))?;
5288 let mut command = Command::new(peer);
5289 command
5290 .arg("--peer")
5291 .arg(scenario)
5292 .current_dir(current_dir)
5293 .env_clear()
5294 .stdin(Stdio::piped())
5295 .stdout(Stdio::piped())
5296 .stderr(Stdio::piped());
5297 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5298 {
5299 use std::os::unix::process::CommandExt;
5300
5301 command.process_group(0);
5302 }
5303 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5304 {
5305 use std::os::windows::process::CommandExt;
5306
5307 command.creation_flags(0x0800_0000);
5308 }
5309 Ok(command)
5310 }
5311
5312 fn test_launch(peer: &Path) -> io::Result<VerifiedParserPackLaunch> {
5313 let pack_root = peer
5314 .parent()
5315 .ok_or_else(|| io::Error::other("hostile peer path has no parent"))?
5316 .to_path_buf();
5317 let platform = host_pack_platform()
5318 .ok_or_else(|| io::Error::other("host has no optional-parser containment target"))?;
5319 Ok(VerifiedParserPackLaunch {
5320 #[cfg(any(
5321 all(target_os = "linux", target_arch = "x86_64"),
5322 all(target_os = "windows", target_arch = "x86_64")
5323 ))]
5324 pack_root: pack_root.clone(),
5325 platform,
5326 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5327 containment_broker: Some(peer.to_path_buf()),
5328 accepted_grammars: vec!["hostile".to_owned()],
5329 artifact: ParserArtifactIdentity::for_bytes(b"parser-supervisor-hostile-peer"),
5330 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5331 artifact_manifest_bytes: b"parser-supervisor-hostile-peer".to_vec(),
5332 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5333 accepted_manifest_bytes: Vec::new(),
5334 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5335 native_import_policy_bytes: Vec::new(),
5336 artifact_manifest: FileObservation::unavailable(
5337 pack_root.join(ARTIFACT_MANIFEST_FILE_NAME),
5338 ),
5339 payloads: Vec::new(),
5340 currentness_blocker: None,
5341 })
5342 }
5343
5344 fn require_reused_resident_payload_revalidation(peer: &Path) -> io::Result<()> {
5345 let temp = tempfile::tempdir()?;
5346 let accepted_bytes = b"accepted-manifest";
5347 fs::write(
5348 temp.path().join(ARTIFACT_MANIFEST_FILE_NAME),
5349 b"parser-supervisor-hostile-peer",
5350 )?;
5351 fs::write(
5352 temp.path().join(ACCEPTED_MANIFEST_FILE_NAME),
5353 accepted_bytes,
5354 )?;
5355 let payload_path = temp.path().join("hostile-grammar");
5356 fs::write(&payload_path, b"trusted")?;
5357 let modified = fs::metadata(&payload_path)?.modified()?;
5358
5359 let mut launch = test_launch(peer)?;
5360 #[cfg(any(
5361 all(target_os = "linux", target_arch = "x86_64"),
5362 all(target_os = "windows", target_arch = "x86_64")
5363 ))]
5364 {
5365 launch.pack_root = temp.path().to_path_buf();
5366 }
5367 launch.artifact_manifest =
5368 FileObservation::capture(temp.path().join(ARTIFACT_MANIFEST_FILE_NAME))
5369 .map_err(|error| io::Error::other(error.to_string()))?;
5370 launch.payloads = vec![PayloadObservation {
5371 file: FileObservation::capture(payload_path.clone())
5372 .map_err(|error| io::Error::other(error.to_string()))?,
5373 role: ParserPackPayloadRole::GrammarLibrary {
5374 language_id: "hostile".to_owned(),
5375 },
5376 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5377 bytes: 7,
5378 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5379 sha256: encode_sha256(Sha256::digest(b"trusted")),
5380 }];
5381 let grammar = ParserLanguageIdentity::new("hostile")
5382 .map_err(|error| io::Error::other(error.to_string()))?;
5383 let cancellation = IndexCancellation::new();
5384 let deadline = Instant::now()
5385 .checked_add(Duration::from_secs(2))
5386 .ok_or_else(|| io::Error::other("resident reuse deadline overflow"))?;
5387 let resident = ResidentParserSession::launch_command(
5388 &launch,
5389 grammar,
5390 ParserMemoryLimits::PRODUCTION,
5391 Instant::now(),
5392 deadline,
5393 ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS,
5394 &cancellation,
5395 command_for(peer, "idle-close")?,
5396 )
5397 .map_err(|error| {
5398 io::Error::other(format!(
5399 "resident reuse test launch failed before mutation: {error:?}"
5400 ))
5401 })?;
5402 let mut supervisor = OptionalParserSupervisor {
5403 pack_root: temp.path().to_path_buf(),
5404 launch,
5405 memory_limits: ParserMemoryLimits::PRODUCTION,
5406 resident: Some(resident),
5407 };
5408
5409 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
5410 let (release_sender, release_receiver) = mpsc::sync_channel(1);
5411 supervisor.launch.currentness_blocker = Some(std::sync::Arc::new(MetadataProbeBlocker {
5412 entered: entered_sender,
5413 release: std::sync::Mutex::new(release_receiver),
5414 }));
5415 let blocked_cancellation = IndexCancellation::new();
5416 let caller_cancellation = blocked_cancellation.clone();
5417 let blocked_deadline = Instant::now()
5418 .checked_add(Duration::from_secs(5))
5419 .ok_or_else(|| io::Error::other("blocked currentness deadline overflow"))?;
5420 let blocked_limits = default_limits()?;
5421 let (result_sender, result_receiver) = mpsc::sync_channel(1);
5422 let caller = thread::spawn(move || {
5423 let result = supervisor.parse(
5424 "hostile",
5425 &[b'x'; 32],
5426 blocked_limits,
5427 blocked_deadline,
5428 Duration::from_secs(5),
5429 &caller_cancellation,
5430 );
5431 let _send_result = result_sender.send((result, supervisor));
5432 });
5433 entered_receiver
5434 .recv_timeout(Duration::from_secs(1))
5435 .map_err(|source| io::Error::other(source.to_string()))?;
5436 blocked_cancellation.cancel();
5437 let (blocked_result, returned_supervisor) = result_receiver
5438 .recv_timeout(Duration::from_secs(1))
5439 .map_err(|source| io::Error::other(source.to_string()))?;
5440 if !matches!(
5441 blocked_result,
5442 Err(ParserSupervisorError::Cancelled {
5443 phase: ARTIFACT_IO_PHASE
5444 })
5445 ) {
5446 return Err(io::Error::other(format!(
5447 "blocked currentness returned the wrong result: {blocked_result:?}"
5448 )));
5449 }
5450 if returned_supervisor.resident.is_some() {
5451 return Err(io::Error::other(
5452 "blocked currentness retained the canceled resident",
5453 ));
5454 }
5455 if ArtifactIoLease::acquire().is_ok() {
5456 return Err(io::Error::other(
5457 "blocked currentness admitted a second artifact reader",
5458 ));
5459 }
5460 release_sender
5461 .send(())
5462 .map_err(|_closed| io::Error::other("metadata-probe release receiver closed"))?;
5463 caller
5464 .join()
5465 .map_err(|_panic| io::Error::other("blocked currentness caller panicked"))?;
5466 let permit_deadline = Instant::now() + Duration::from_secs(1);
5467 while ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) && Instant::now() < permit_deadline {
5468 thread::yield_now();
5469 }
5470 if ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) {
5471 return Err(io::Error::other(
5472 "blocked currentness did not release the artifact reader",
5473 ));
5474 }
5475
5476 supervisor = returned_supervisor;
5477 supervisor.launch.currentness_blocker = None;
5478 let cancellation = IndexCancellation::new();
5479 let deadline = Instant::now()
5480 .checked_add(Duration::from_secs(2))
5481 .ok_or_else(|| io::Error::other("resident reuse deadline overflow"))?;
5482 let grammar = ParserLanguageIdentity::new("hostile")
5483 .map_err(|error| io::Error::other(error.to_string()))?;
5484 supervisor.resident = Some(
5485 ResidentParserSession::launch_command(
5486 &supervisor.launch,
5487 grammar,
5488 ParserMemoryLimits::PRODUCTION,
5489 Instant::now(),
5490 deadline,
5491 ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS,
5492 &cancellation,
5493 command_for(peer, "idle-close")?,
5494 )
5495 .map_err(|error| {
5496 io::Error::other(format!(
5497 "resident reuse test relaunch failed after blocked currentness: {error:?}"
5498 ))
5499 })?,
5500 );
5501
5502 let mutation = fs::write(&payload_path, b"mutated");
5503 #[cfg(windows)]
5504 if mutation.is_err() {
5505 if fs::read(&payload_path)? != b"trusted" {
5506 return Err(io::Error::other(
5507 "Windows write guard reported failure after changing payload bytes",
5508 ));
5509 }
5510 supervisor
5511 .shutdown()
5512 .map_err(|error| io::Error::other(error.to_string()))?;
5513 return Ok(());
5514 }
5515 mutation?;
5516 File::options()
5517 .write(true)
5518 .open(&payload_path)?
5519 .set_times(fs::FileTimes::new().set_modified(modified))?;
5520 if fs::metadata(&payload_path)?.len() != 7
5521 || fs::metadata(&payload_path)?.modified()? != modified
5522 {
5523 return Err(io::Error::other(
5524 "resident reuse mutation did not preserve size and modification time",
5525 ));
5526 }
5527
5528 let source = vec![b'x'; 32];
5529 let operation = supervisor.parse(
5530 "hostile",
5531 &source,
5532 default_limits()?,
5533 deadline,
5534 Duration::from_millis(150),
5535 &cancellation,
5536 );
5537 let Err(error) = operation else {
5538 return Err(io::Error::other(
5539 "mutated launch payload was accepted by a reused resident",
5540 ));
5541 };
5542 if error.has_mandatory_cleanup_failure() {
5543 return Err(io::Error::other(format!(
5544 "mutated launch payload did not cleanly destroy the resident: {error:?}"
5545 )));
5546 }
5547 if supervisor.resident.is_some() {
5548 return Err(io::Error::other(
5549 "mutated launch payload retained the resident session",
5550 ));
5551 }
5552 Ok(())
5553 }
5554
5555 fn operate(
5556 peer: &Path,
5557 case: &Case,
5558 ) -> Result<ParserCompletionEvidence, ParserSupervisorError> {
5559 let launch = test_launch(peer).map_err(|source| ParserSupervisorError::IoThread {
5560 phase: "adversarial test launch",
5561 message: source.to_string(),
5562 })?;
5563 let grammar = ParserLanguageIdentity::new("hostile")?;
5564 let cancellation = IndexCancellation::new();
5565 if case.cancel_before_launch {
5566 cancellation.cancel();
5567 }
5568 let now = Instant::now();
5569 let deadline = now.checked_add(case.deadline).unwrap_or(now);
5570 let resident = ResidentParserSession::launch_command(
5571 &launch,
5572 grammar,
5573 ParserMemoryLimits::PRODUCTION,
5574 now,
5575 deadline,
5576 adversarial_launch_no_progress(case.scenario, case.no_progress),
5577 &cancellation,
5578 command_for(peer, case.scenario).map_err(|source| ParserSupervisorError::IoThread {
5579 phase: "adversarial test command",
5580 message: source.to_string(),
5581 })?,
5582 );
5583 let cancellation_thread;
5584 let result = match resident {
5585 Ok(mut resident) => {
5586 let source = vec![b'x'; case.source_bytes];
5587 let source_identity = ParserSourceIdentity::for_bytes(&source)?;
5588 let operation_started = Instant::now();
5589 let operation_deadline = case
5590 .deadline_after_launch
5591 .and_then(|duration| operation_started.checked_add(duration))
5592 .unwrap_or(deadline);
5593 cancellation_thread = case.cancellation_after_launch.map(|delay| {
5594 let cancellation = cancellation.clone();
5595 thread::spawn(move || {
5596 thread::sleep(delay);
5597 cancellation.cancel();
5598 })
5599 });
5600 let operation = resident.parse(
5601 &source,
5602 source_identity,
5603 case.limits,
5604 operation_started,
5605 operation_deadline,
5606 case.no_progress,
5607 &cancellation,
5608 );
5609 match operation {
5610 Ok(evidence) => resident.shutdown().map(|()| evidence),
5611 Err(operation) => {
5612 if operation.is_caller_stop() {
5613 resident.termination_requested = true;
5614 }
5615 Err(attach_cleanup(operation, resident.shutdown()))
5616 }
5617 }
5618 }
5619 Err(error) => {
5620 cancellation_thread = None;
5621 Err(error)
5622 }
5623 };
5624 if let Some(handle) = cancellation_thread {
5625 handle
5626 .join()
5627 .map_err(|_panic| ParserSupervisorError::Cleanup {
5628 message: "adversarial cancellation thread panicked".to_owned(),
5629 })?;
5630 }
5631 result
5632 }
5633
5634 fn require_failure(peer: &Path, hostile: &Case) -> io::Result<()> {
5635 let Err(error) = operate(peer, hostile) else {
5636 return Err(io::Error::other(format!(
5637 "hostile scenario {} unexpectedly succeeded",
5638 hostile.scenario
5639 )));
5640 };
5641 if error.has_mandatory_cleanup_failure() {
5642 return Err(io::Error::other(format!(
5643 "hostile scenario {} did not reap and join cleanly: {error:?}",
5644 hostile.scenario
5645 )));
5646 }
5647 if !error_matches(&error, hostile.expected) {
5648 return Err(io::Error::other(format!(
5649 "hostile scenario {} returned the wrong typed failure: {error:?}",
5650 hostile.scenario
5651 )));
5652 }
5653
5654 let mut healthy = case("healthy", ExpectedFailure::Io)?;
5655 healthy.no_progress = healthy.deadline;
5656 if hostile.scenario == "pre-ready-stall"
5657 && !(hostile.no_progress < RECOVERY_ALLOWANCE_PROBE_AGE
5658 && RECOVERY_ALLOWANCE_PROBE_AGE < healthy.no_progress)
5659 {
5660 return Err(io::Error::other(format!(
5661 "controlled recovery age {:?} must exceed hostile allowance {:?} and remain below healthy allowance {:?}",
5662 RECOVERY_ALLOWANCE_PROBE_AGE, hostile.no_progress, healthy.no_progress
5663 )));
5664 }
5665 let cleanup_deadline = Instant::now() + healthy.deadline;
5666 while PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) && Instant::now() < cleanup_deadline {
5667 thread::yield_now();
5668 }
5669 if PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) {
5670 return Err(io::Error::other(format!(
5671 "hostile scenario {} did not finish late process cleanup",
5672 hostile.scenario
5673 )));
5674 }
5675 require_process_spawn_cleanup_health()
5676 .map_err(|error| io::Error::other(error.to_string()))?;
5677 let evidence = operate(peer, &healthy).map_err(|error| {
5678 io::Error::other(format!(
5679 "healthy restart after {} failed: {error:?}",
5680 hostile.scenario
5681 ))
5682 })?;
5683 if evidence.root_kind().as_str() != "source_file" {
5684 return Err(io::Error::other("healthy restart returned other evidence"));
5685 }
5686 Ok(())
5687 }
5688
5689 require_reused_resident_payload_revalidation(peer)?;
5690
5691 let mut cases = vec![
5692 {
5693 let mut opening_cancel = case("opening-cancel", ExpectedFailure::Cancelled)?;
5694 opening_cancel.cancel_before_launch = true;
5695 opening_cancel
5696 },
5697 case("pre-ready-stall", ExpectedFailure::NoProgress)?,
5698 case("ready-session", ExpectedFailure::Ready("session"))?,
5699 case("ready-artifact", ExpectedFailure::Ready("artifact"))?,
5700 case("ready-containment", ExpectedFailure::Ready("containment"))?,
5701 case(
5702 "ready-malformed",
5703 ExpectedFailure::InvalidControl(ParserFrameKind::Ready),
5704 )?,
5705 case("ready-truncated", ExpectedFailure::Io)?,
5706 case("ready-oversized", ExpectedFailure::Io)?,
5707 case("progress-session", ExpectedFailure::Response("session"))?,
5708 case("progress-request", ExpectedFailure::Response("request_id"))?,
5709 case("progress-duplicate", ExpectedFailure::Progress("sequence"))?,
5710 case("progress-gap", ExpectedFailure::Progress("sequence"))?,
5711 case(
5712 "progress-regression",
5713 ExpectedFailure::Progress("completed_work"),
5714 )?,
5715 case(
5716 "progress-endless",
5717 ExpectedFailure::Deadline("request response"),
5718 )?,
5719 case("progress-no-work", ExpectedFailure::NoProgress)?,
5720 case(
5721 "completion-malformed",
5722 ExpectedFailure::InvalidControl(ParserFrameKind::Completion),
5723 )?,
5724 case("completion-truncated", ExpectedFailure::Io)?,
5725 case("completion-oversized", ExpectedFailure::Io)?,
5726 case(
5727 "failure-exit",
5728 ExpectedFailure::Worker(ParserFailureCode::ParseRejected),
5729 )?,
5730 case("stderr-flood", ExpectedFailure::Io)?,
5731 case("stderr-completion", ExpectedFailure::Io)?,
5732 case(
5733 "limit-output",
5734 ExpectedFailure::Limit("completion.output_bytes"),
5735 )?,
5736 case(
5737 "limit-source",
5738 ExpectedFailure::Limit("evidence.root_end_byte"),
5739 )?,
5740 case(
5741 "limit-nodes",
5742 ExpectedFailure::Limit("evidence.named_node_count"),
5743 )?,
5744 case(
5745 "limit-depth",
5746 ExpectedFailure::Limit("evidence.maximum_depth"),
5747 )?,
5748 ];
5749 let mut blocked_cancel = case("blocked-write", ExpectedFailure::Cancelled)?;
5750 blocked_cancel.source_bytes = 4 * 1024 * 1024;
5751 blocked_cancel.cancellation_after_launch = Some(Duration::from_millis(75));
5752 blocked_cancel.no_progress = Duration::from_secs(1);
5753 cases.push(blocked_cancel);
5754 let mut blocked_deadline = case("blocked-write", ExpectedFailure::Deadline("request write"))?;
5755 blocked_deadline.source_bytes = 4 * 1024 * 1024;
5756 blocked_deadline.deadline_after_launch = Some(Duration::from_millis(125));
5757 blocked_deadline.no_progress = Duration::from_secs(1);
5758 cases.push(blocked_deadline);
5759 if let Some(progress_endless) = cases
5760 .iter_mut()
5761 .find(|candidate| candidate.scenario == "progress-endless")
5762 {
5763 progress_endless.deadline_after_launch = Some(Duration::from_millis(250));
5764 progress_endless.no_progress = Duration::from_secs(1);
5765 }
5766 if let Some(output_limit) = cases
5767 .iter_mut()
5768 .find(|candidate| candidate.scenario == "limit-output")
5769 {
5770 output_limit.limits = ParserRequestLimits::new(64, 16, 16)
5771 .map_err(|error| io::Error::other(error.to_string()))?;
5772 }
5773
5774 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5775 cases.extend([
5776 case("admission-forged", ExpectedFailure::InvalidAdmission)?,
5777 case("admission-truncated", ExpectedFailure::Io)?,
5778 case("admission-stall", ExpectedFailure::NoProgress)?,
5779 case("admission-flood", ExpectedFailure::Io)?,
5780 ]);
5781
5782 for hostile in &cases {
5783 require_failure(peer, hostile)?;
5784 }
5785 Ok(())
5786}
5787
5788#[cfg(test)]
5789mod tests {
5790 use std::io::Cursor;
5793 use std::sync::Arc;
5794
5795 use projectatlas_core::optional_parser_protocol::{
5796 PARSER_MAX_SOURCE_BYTES, PARSER_PROTOCOL_VERSION, PARSER_WINDOWS_BROKER_ADMISSION_RECORD,
5797 ParserCompletion, ParserContentDigest, ParserResponseIdentity, ParserSyntaxKind,
5798 };
5799
5800 use super::*;
5801
5802 static ARTIFACT_IO_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
5804 static PROCESS_SPAWN_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
5806 const PROCESS_SPAWN_CHILD_ENV: &str = "PROJECTATLAS_PROCESS_SPAWN_CHILD";
5808 const PROCESS_SPAWN_CHILD_COMPLETED_ENV: &str = "PROJECTATLAS_PROCESS_SPAWN_CHILD_COMPLETED";
5810
5811 struct ProcessSpawnCleanupFailureReset;
5813
5814 impl Drop for ProcessSpawnCleanupFailureReset {
5815 fn drop(&mut self) {
5816 if let Ok(mut slot) = PROCESS_SPAWN_CLEANUP_FAILURE.lock() {
5817 *slot = None;
5818 }
5819 }
5820 }
5821
5822 struct ProcessSpawnTestHookReset;
5824
5825 impl Drop for ProcessSpawnTestHookReset {
5826 fn drop(&mut self) {
5827 for slot in [
5828 &PROCESS_SPAWN_AFTER_RENDEZVOUS_TEST_HOOK,
5829 &PROCESS_SPAWN_AFTER_FINAL_CHECK_TEST_HOOK,
5830 &PROCESS_SPAWN_BEFORE_CLEANUP_TEST_HOOK,
5831 ] {
5832 if let Ok(mut hook) = slot.lock() {
5833 *hook = None;
5834 }
5835 }
5836 }
5837 }
5838
5839 #[test]
5840 fn adversarial_launch_allowance_is_phase_specific_and_production_independent() {
5841 let operation_no_progress = Duration::from_millis(500);
5842 assert_eq!(
5843 adversarial_launch_no_progress("pre-ready-stall", operation_no_progress),
5844 operation_no_progress
5845 );
5846 assert_eq!(
5847 adversarial_launch_no_progress("admission-stall", operation_no_progress),
5848 operation_no_progress
5849 );
5850 for scenario in [
5851 "progress-no-work",
5852 "admission-forged",
5853 "completion-malformed",
5854 ] {
5855 assert_eq!(
5856 adversarial_launch_no_progress(scenario, operation_no_progress),
5857 ADVERSARIAL_NON_STALL_LAUNCH_NO_PROGRESS
5858 );
5859 }
5860 assert_eq!(ARTIFACT_ADMISSION_TIMEOUT, Duration::from_secs(15));
5861 assert_eq!(
5862 ARTIFACT_ADMISSION_AGGREGATE_TIMEOUT,
5863 Duration::from_mins(20)
5864 );
5865 assert_eq!(
5866 ARTIFACT_ADMISSION_NO_PROGRESS_TIMEOUT,
5867 Duration::from_secs(5)
5868 );
5869 }
5870
5871 #[test]
5872 fn adversarial_deadline_expectation_requires_the_exact_operation_phase() {
5873 let delayed_admission = ParserSupervisorError::DeadlineExceeded {
5874 phase: "containment admission",
5875 };
5876 assert!(!adversarial_deadline_matches(
5877 &delayed_admission,
5878 "request response"
5879 ));
5880
5881 let response_deadline = ParserSupervisorError::DeadlineExceeded {
5882 phase: "request response",
5883 };
5884 assert!(adversarial_deadline_matches(
5885 &response_deadline,
5886 "request response"
5887 ));
5888 }
5889
5890 #[test]
5891 fn blocked_process_spawn_child_fixture() {
5892 if std::env::var_os(PROCESS_SPAWN_CHILD_ENV).is_none() {
5893 return;
5894 }
5895 thread::sleep(Duration::from_secs(30));
5896 if let Some(path) = std::env::var_os(PROCESS_SPAWN_CHILD_COMPLETED_ENV) {
5897 let _write = fs::write(path, b"survived");
5898 }
5899 }
5900
5901 struct InterruptOnceReader {
5903 input: Cursor<Vec<u8>>,
5905 did_interrupt: bool,
5907 }
5908
5909 impl Read for InterruptOnceReader {
5910 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
5911 if !self.did_interrupt {
5912 self.did_interrupt = true;
5913 return Err(io::ErrorKind::Interrupted.into());
5914 }
5915 self.input.read(buffer)
5916 }
5917 }
5918
5919 struct BlockingReader {
5921 entered: SyncSender<()>,
5923 release: Receiver<()>,
5925 }
5926
5927 impl Read for BlockingReader {
5928 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
5929 self.entered
5930 .send(())
5931 .map_err(|_closed| io::Error::other("blocking reader entry receiver closed"))?;
5932 self.release
5933 .recv()
5934 .map_err(|_closed| io::Error::other("blocking reader release sender closed"))?;
5935 buffer[0] = b'x';
5936 Ok(1)
5937 }
5938 }
5939
5940 fn metadata_only_launch() -> VerifiedParserPackLaunch {
5942 let pack_root = PathBuf::from("metadata-only-pack");
5943 VerifiedParserPackLaunch {
5944 #[cfg(any(
5945 all(target_os = "linux", target_arch = "x86_64"),
5946 all(target_os = "windows", target_arch = "x86_64")
5947 ))]
5948 pack_root: pack_root.clone(),
5949 platform: PackPlatform::LinuxX86_64,
5950 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
5951 containment_broker: Some(pack_root.join("projectatlas-parser-containment.exe")),
5952 accepted_grammars: vec!["alpha".to_owned(), "zeta".to_owned()],
5953 artifact: ParserArtifactIdentity::for_bytes(b"artifact"),
5954 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5955 artifact_manifest_bytes: b"artifact".to_vec(),
5956 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5957 accepted_manifest_bytes: Vec::new(),
5958 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
5959 native_import_policy_bytes: Vec::new(),
5960 artifact_manifest: FileObservation::unavailable(
5961 pack_root.join(ARTIFACT_MANIFEST_FILE_NAME),
5962 ),
5963 payloads: Vec::new(),
5964 currentness_blocker: None,
5965 }
5966 }
5967
5968 fn metadata_only_supervisor() -> OptionalParserSupervisor {
5970 let launch = metadata_only_launch();
5971 OptionalParserSupervisor {
5972 pack_root: PathBuf::from("metadata-only-pack"),
5973 launch,
5974 memory_limits: ParserMemoryLimits::PRODUCTION,
5975 resident: None,
5976 }
5977 }
5978
5979 #[test]
5980 fn supervisor_exposes_only_verified_artifact_and_language_metadata() {
5981 let supervisor = metadata_only_supervisor();
5982 assert_eq!(
5983 supervisor.artifact_identity(),
5984 &ParserArtifactIdentity::for_bytes(b"artifact")
5985 );
5986 assert!(supervisor.accepts_language("alpha"));
5987 assert!(supervisor.accepts_language("zeta"));
5988 assert!(!supervisor.accepts_language("missing"));
5989 assert!(!supervisor.accepts_language("INVALID"));
5990 }
5991
5992 #[test]
5993 fn supervisor_rejects_artifact_identity_change_inside_selected_slot() {
5994 let mut supervisor = metadata_only_supervisor();
5995 let selected = supervisor.artifact_identity().clone();
5996 let mut replacement = metadata_only_launch();
5997 replacement.artifact = ParserArtifactIdentity::for_bytes(b"replacement-artifact");
5998
5999 assert!(matches!(
6000 supervisor.replace_verified_launch(replacement),
6001 Err(ParserSupervisorError::PayloadMismatch {
6002 reason: "artifact identity changed inside its immutable slot",
6003 ..
6004 })
6005 ));
6006 assert_eq!(supervisor.artifact_identity(), &selected);
6007 }
6008
6009 #[test]
6010 fn bounded_artifact_read_retries_interruption() -> Result<(), Box<dyn std::error::Error>> {
6011 let expected = vec![b'x'; ARTIFACT_READ_CHUNK_BYTES * 3];
6012 let expected_sha256 = encode_sha256(Sha256::digest(&expected));
6013 let mut reader = InterruptOnceReader {
6014 input: Cursor::new(expected.clone()),
6015 did_interrupt: false,
6016 };
6017 let mut bytes = Vec::new();
6018 let mut sha256 = Sha256::new();
6019 read_bounded_chunks(
6020 &mut reader,
6021 Path::new("changed-payload"),
6022 u64::try_from(ARTIFACT_READ_CHUNK_BYTES * 3)?,
6023 &mut bytes,
6024 &mut sha256,
6025 None,
6026 )?;
6027
6028 require_test(
6029 bytes == expected,
6030 "bounded artifact read changed bytes after an interrupted read",
6031 )?;
6032 require_test(
6033 encode_sha256(sha256.finalize()) == expected_sha256,
6034 "bounded artifact read changed its digest after an interrupted read",
6035 )
6036 .map_err(Into::into)
6037 }
6038
6039 #[test]
6040 fn changed_artifact_reload_returns_while_reader_is_blocked()
6041 -> Result<(), Box<dyn std::error::Error>> {
6042 let _test_guard = ARTIFACT_IO_TEST_LOCK
6043 .lock()
6044 .map_err(|_poisoned| io::Error::other("artifact I/O test lock was poisoned"))?;
6045 let cancellation = IndexCancellation::new();
6046 let started = Instant::now();
6047 let absolute_deadline = started + Duration::from_secs(5);
6048 let no_progress_timeout = Duration::from_secs(5);
6049 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
6050 let (release_sender, release_receiver) = mpsc::sync_channel(1);
6051 let (finished_sender, finished_receiver) = mpsc::sync_channel(1);
6052 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6053 let caller_cancellation = cancellation.clone();
6054 let worker_cancellation = cancellation.clone();
6055 let caller = thread::spawn(move || {
6056 let control = ArtifactIoControl {
6057 absolute_deadline,
6058 last_progress: started,
6059 no_progress_timeout,
6060 cancellation: &caller_cancellation,
6061 };
6062 let result = run_bounded_artifact_io(
6063 move || {
6064 let worker_control = ArtifactIoControl {
6065 absolute_deadline,
6066 last_progress: started,
6067 no_progress_timeout,
6068 cancellation: &worker_cancellation,
6069 };
6070 let mut reader = BlockingReader {
6071 entered: entered_sender,
6072 release: release_receiver,
6073 };
6074 let mut bytes = Vec::new();
6075 let mut sha256 = Sha256::new();
6076 let result = read_bounded_chunks(
6077 &mut reader,
6078 Path::new("blocked-payload"),
6079 u64::try_from(ARTIFACT_READ_CHUNK_BYTES).unwrap_or(u64::MAX),
6080 &mut bytes,
6081 &mut sha256,
6082 Some(&worker_control),
6083 );
6084 let worker_cancelled = matches!(
6085 &result,
6086 Err(ParserSupervisorError::Cancelled {
6087 phase: ARTIFACT_IO_PHASE
6088 })
6089 );
6090 let _finished_result = finished_sender.send(worker_cancelled);
6091 result
6092 },
6093 &control,
6094 );
6095 let _result_send = result_sender.send(result);
6096 });
6097
6098 let entered = entered_receiver.recv_timeout(Duration::from_secs(1));
6099 cancellation.cancel();
6100 let result = result_receiver.recv_timeout(Duration::from_secs(1));
6101 let returned_cancelled = matches!(
6102 result.as_ref(),
6103 Ok(Err(ParserSupervisorError::Cancelled {
6104 phase: ARTIFACT_IO_PHASE
6105 }))
6106 );
6107 let refused_second_reader = matches!(
6108 ArtifactIoLease::acquire(),
6109 Err(ParserSupervisorError::IoThread {
6110 phase: ARTIFACT_IO_PHASE,
6111 ..
6112 })
6113 );
6114
6115 let release_result = release_sender.send(());
6116 caller
6117 .join()
6118 .map_err(|_panic| io::Error::other("artifact revalidation caller panicked"))?;
6119 let worker_cancelled = finished_receiver.recv_timeout(Duration::from_secs(1));
6120 let permit_deadline = Instant::now() + Duration::from_secs(1);
6121 while ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) && Instant::now() < permit_deadline {
6122 thread::yield_now();
6123 }
6124
6125 entered.map_err(|source| io::Error::other(source.to_string()))?;
6126 let _returned = result.map_err(|source| io::Error::other(source.to_string()))?;
6127 release_result?;
6128 let worker_cancelled =
6129 worker_cancelled.map_err(|source| io::Error::other(source.to_string()))?;
6130 require_test(
6131 returned_cancelled,
6132 "blocked artifact read retained the canceled request",
6133 )?;
6134 require_test(
6135 refused_second_reader,
6136 "blocked artifact read permitted another reload worker",
6137 )?;
6138 require_test(
6139 worker_cancelled,
6140 "released artifact reader continued after request cancellation",
6141 )?;
6142 require_test(
6143 ArtifactIoLease::acquire().is_ok(),
6144 "artifact reader permit was not reusable after worker completion",
6145 )
6146 .map_err(Into::into)
6147 }
6148
6149 #[test]
6150 fn currentness_probe_returns_while_path_observer_is_blocked()
6151 -> Result<(), Box<dyn std::error::Error>> {
6152 let _test_guard = ARTIFACT_IO_TEST_LOCK
6153 .lock()
6154 .map_err(|_poisoned| io::Error::other("artifact I/O test lock was poisoned"))?;
6155 let cancellation = IndexCancellation::new();
6156 let started = Instant::now();
6157 let absolute_deadline = started + Duration::from_secs(5);
6158 let no_progress_timeout = Duration::from_secs(5);
6159 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
6160 let (release_sender, release_receiver) = mpsc::sync_channel(1);
6161 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6162 let temp = tempfile::tempdir()?;
6163 let observed_path = temp.path().join("artifact.json");
6164 fs::write(&observed_path, b"verified")?;
6165 let observation = FileObservation::capture(observed_path)?;
6166 let mut file_probe = observation.currentness_probe();
6167 file_probe.blocker = Some(std::sync::Arc::new(MetadataProbeBlocker {
6168 entered: entered_sender,
6169 release: std::sync::Mutex::new(release_receiver),
6170 }));
6171 let probe = ArtifactCurrentnessProbe {
6172 files: vec![file_probe],
6173 };
6174 let caller_cancellation = cancellation.clone();
6175 let caller = thread::spawn(move || {
6176 let control = ArtifactIoControl {
6177 absolute_deadline,
6178 last_progress: started,
6179 no_progress_timeout,
6180 cancellation: &caller_cancellation,
6181 };
6182 let result = run_bounded_artifact_currentness(probe, &control);
6183 let _result_send = result_sender.send(result);
6184 });
6185
6186 entered_receiver
6187 .recv_timeout(Duration::from_secs(1))
6188 .map_err(|source| io::Error::other(source.to_string()))?;
6189 cancellation.cancel();
6190 let result = result_receiver
6191 .recv_timeout(Duration::from_secs(1))
6192 .map_err(|source| io::Error::other(source.to_string()))?;
6193 let returned_cancelled = matches!(
6194 result,
6195 Err(ParserSupervisorError::Cancelled {
6196 phase: ARTIFACT_IO_PHASE
6197 })
6198 );
6199 let refused_second_reader = matches!(
6200 ArtifactIoLease::acquire(),
6201 Err(ParserSupervisorError::IoThread {
6202 phase: ARTIFACT_IO_PHASE,
6203 ..
6204 })
6205 );
6206 release_sender.send(())?;
6207 caller
6208 .join()
6209 .map_err(|_panic| io::Error::other("currentness caller panicked"))?;
6210 let permit_deadline = Instant::now() + Duration::from_secs(1);
6211 while ARTIFACT_IO_ACTIVE.load(Ordering::Acquire) && Instant::now() < permit_deadline {
6212 thread::yield_now();
6213 }
6214
6215 require_test(
6216 returned_cancelled,
6217 "blocked pathname observation retained the canceled request",
6218 )?;
6219 require_test(
6220 refused_second_reader,
6221 "blocked pathname observation permitted another artifact reader",
6222 )?;
6223 require_test(
6224 ArtifactIoLease::acquire().is_ok(),
6225 "artifact reader permit was not reusable after currentness completion",
6226 )
6227 .map_err(Into::into)
6228 }
6229
6230 #[test]
6231 fn changed_artifact_refresh_propagates_request_cancellation() {
6232 let mut supervisor = metadata_only_supervisor();
6233 let cancellation = IndexCancellation::new();
6234 cancellation.cancel();
6235
6236 assert!(matches!(
6237 supervisor.refresh_changed_artifact(
6238 "alpha",
6239 Instant::now(),
6240 Instant::now() + Duration::from_secs(1),
6241 Duration::from_secs(1),
6242 &cancellation,
6243 ),
6244 Err(ParserSupervisorError::Cancelled {
6245 phase: ARTIFACT_IO_PHASE
6246 })
6247 ));
6248 }
6249
6250 #[test]
6251 fn controlled_artifact_currentness_polls_before_metadata() {
6252 let launch = metadata_only_launch();
6253 let cancellation = IndexCancellation::new();
6254 cancellation.cancel();
6255 let control = ArtifactIoControl {
6256 absolute_deadline: Instant::now() + Duration::from_secs(1),
6257 last_progress: Instant::now(),
6258 no_progress_timeout: Duration::from_secs(1),
6259 cancellation: &cancellation,
6260 };
6261
6262 assert!(matches!(
6263 launch.currentness_probe("alpha").is_current(Some(&control)),
6264 Err(ParserSupervisorError::Cancelled {
6265 phase: ARTIFACT_IO_PHASE
6266 })
6267 ));
6268 }
6269
6270 #[test]
6271 fn stopped_process_launch_never_invokes_spawn() -> Result<(), Box<dyn std::error::Error>> {
6272 let _guard = PROCESS_SPAWN_TEST_LOCK
6273 .lock()
6274 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6275 require_process_spawn_cleanup_health()?;
6276 let cancellation = IndexCancellation::new();
6277 cancellation.cancel();
6278 let invoked = Arc::new(AtomicBool::new(false));
6279 let spawn_invoked = Arc::clone(&invoked);
6280 let started = Instant::now();
6281 let result = run_bounded_process_spawn_with(
6282 Command::new(std::env::current_exe()?),
6283 started + Duration::from_secs(1),
6284 started,
6285 Duration::from_secs(1),
6286 &cancellation,
6287 move |_command| {
6288 spawn_invoked.store(true, Ordering::Release);
6289 Err(io::Error::other("stopped launch invoked spawn"))
6290 },
6291 );
6292 if !matches!(
6293 result,
6294 Err(ParserSupervisorError::Cancelled {
6295 phase: PROCESS_LAUNCH_PHASE
6296 })
6297 ) {
6298 return Err(io::Error::other(format!(
6299 "stopped process launch returned the wrong result: {result:?}"
6300 ))
6301 .into());
6302 }
6303 if invoked.load(Ordering::Acquire) {
6304 return Err(io::Error::other("stopped process launch invoked spawn").into());
6305 }
6306 Ok(())
6307 }
6308
6309 #[test]
6310 fn sticky_late_cleanup_failure_refuses_future_spawn() -> Result<(), Box<dyn std::error::Error>>
6311 {
6312 let _guard = PROCESS_SPAWN_TEST_LOCK
6313 .lock()
6314 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6315 require_process_spawn_cleanup_health()?;
6316 let _reset = ProcessSpawnCleanupFailureReset;
6317 record_process_spawn_cleanup_failure(&ParserSupervisorError::Cleanup {
6318 message: "injected cleanup failure".to_owned(),
6319 });
6320 let invoked = Arc::new(AtomicBool::new(false));
6321 let spawn_invoked = Arc::clone(&invoked);
6322 let cancellation = IndexCancellation::new();
6323 let started = Instant::now();
6324 let result = run_bounded_process_spawn_with(
6325 Command::new(std::env::current_exe()?),
6326 started + Duration::from_secs(1),
6327 started,
6328 Duration::from_secs(1),
6329 &cancellation,
6330 move |_command| {
6331 spawn_invoked.store(true, Ordering::Release);
6332 Err(io::Error::other("unhealthy launch invoked spawn"))
6333 },
6334 );
6335 let message = match result {
6336 Err(ParserSupervisorError::Cleanup { message }) => message,
6337 other => {
6338 return Err(io::Error::other(format!(
6339 "sticky cleanup failure returned the wrong result: {other:?}"
6340 ))
6341 .into());
6342 }
6343 };
6344 if !message.contains("injected cleanup failure") {
6345 return Err(io::Error::other("sticky cleanup failure lost its diagnostic").into());
6346 }
6347 if invoked.load(Ordering::Acquire) {
6348 return Err(io::Error::other("unhealthy process launch invoked spawn").into());
6349 }
6350 Ok(())
6351 }
6352
6353 #[test]
6354 fn blocked_process_spawn_releases_caller_and_reaps_late_child()
6355 -> Result<(), Box<dyn std::error::Error>> {
6356 let _guard = PROCESS_SPAWN_TEST_LOCK
6357 .lock()
6358 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6359 require_process_spawn_cleanup_health()?;
6360 let temp = tempfile::tempdir()?;
6361 let completed = temp.path().join("child-completed");
6362 let mut command = Command::new(std::env::current_exe()?);
6363 command
6364 .arg("--exact")
6365 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6366 .arg("--nocapture")
6367 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6368 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6369 .stdin(Stdio::null())
6370 .stdout(Stdio::null())
6371 .stderr(Stdio::null());
6372
6373 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
6374 let (release_sender, release_receiver) = mpsc::sync_channel(1);
6375 let (pid_sender, pid_receiver) = mpsc::sync_channel(1);
6376 let cancellation = IndexCancellation::new();
6377 let caller_cancellation = cancellation.clone();
6378 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6379 let started = Instant::now();
6380 let caller = thread::spawn(move || {
6381 let result = run_bounded_process_spawn_with(
6382 command,
6383 started + Duration::from_secs(5),
6384 started,
6385 Duration::from_secs(5),
6386 &caller_cancellation,
6387 move |mut command| {
6388 let child = command.spawn()?;
6389 pid_sender
6390 .send(child.id())
6391 .map_err(|_closed| io::Error::other("spawn PID receiver closed"))?;
6392 entered_sender
6393 .send(())
6394 .map_err(|_closed| io::Error::other("spawn blocker receiver closed"))?;
6395 release_receiver
6396 .recv()
6397 .map_err(|_closed| io::Error::other("spawn release sender closed"))?;
6398 Ok(child)
6399 },
6400 );
6401 let _send = result_sender.send(result);
6402 });
6403
6404 entered_receiver.recv_timeout(Duration::from_secs(5))?;
6405 let _late_child_pid = pid_receiver.recv_timeout(Duration::from_secs(1))?;
6406 cancellation.cancel();
6407 let result = result_receiver.recv_timeout(Duration::from_secs(1))?;
6408 match result {
6409 Err(ParserSupervisorError::Cancelled {
6410 phase: PROCESS_LAUNCH_PHASE,
6411 }) => {}
6412 other => {
6413 return Err(io::Error::other(format!(
6414 "blocked process spawn returned the wrong result: {other:?}"
6415 ))
6416 .into());
6417 }
6418 }
6419 if ProcessSpawnLease::acquire().is_ok() {
6420 return Err(io::Error::other(
6421 "blocked process spawn released its process-wide lease early",
6422 )
6423 .into());
6424 }
6425 release_sender.send(())?;
6426 caller
6427 .join()
6428 .map_err(|_panic| io::Error::other("blocked-spawn caller panicked"))?;
6429
6430 let cleanup_deadline = Instant::now() + SUPERVISOR_CLEANUP_TIMEOUT;
6431 while PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) && Instant::now() < cleanup_deadline {
6432 thread::yield_now();
6433 }
6434 if PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) {
6435 return Err(io::Error::other(
6436 "late process spawn did not release its process-wide lease",
6437 )
6438 .into());
6439 }
6440 require_process_spawn_cleanup_health()?;
6441 if completed.exists() {
6442 return Err(io::Error::other("late child survived mandatory cleanup").into());
6443 }
6444 drop(ProcessSpawnLease::acquire()?);
6445 Ok(())
6446 }
6447
6448 #[test]
6449 fn completed_process_spawn_cancellation_detaches_after_rendezvous()
6450 -> Result<(), Box<dyn std::error::Error>> {
6451 let _guard = PROCESS_SPAWN_TEST_LOCK
6452 .lock()
6453 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6454 require_process_spawn_cleanup_health()?;
6455 let _hook_reset = ProcessSpawnTestHookReset;
6456 let temp = tempfile::tempdir()?;
6457 let completed = temp.path().join("completed-spawn-child-completed");
6458 let mut command = Command::new(std::env::current_exe()?);
6459 command
6460 .arg("--exact")
6461 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6462 .arg("--nocapture")
6463 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6464 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6465 .stdin(Stdio::null())
6466 .stdout(Stdio::null())
6467 .stderr(Stdio::null());
6468
6469 let (rendezvous_entered_sender, rendezvous_entered_receiver) = mpsc::sync_channel(1);
6470 let (release_rendezvous_sender, release_rendezvous_receiver) = mpsc::sync_channel(1);
6471 *PROCESS_SPAWN_AFTER_RENDEZVOUS_TEST_HOOK
6472 .lock()
6473 .map_err(|_poisoned| io::Error::other("rendezvous test hook lock is poisoned"))? =
6474 Some(Box::new(move || {
6475 let _entered = rendezvous_entered_sender.send(());
6476 let _release = release_rendezvous_receiver.recv();
6477 }));
6478
6479 let (cleanup_entered_sender, cleanup_entered_receiver) = mpsc::sync_channel(1);
6480 let (release_cleanup_sender, release_cleanup_receiver) = mpsc::sync_channel(1);
6481 *PROCESS_SPAWN_BEFORE_CLEANUP_TEST_HOOK
6482 .lock()
6483 .map_err(|_poisoned| io::Error::other("cleanup test hook lock is poisoned"))? =
6484 Some(Box::new(move || {
6485 let thread_name = thread::current().name().unwrap_or("unnamed").to_owned();
6486 let _entered = cleanup_entered_sender.send(thread_name);
6487 let _release = release_cleanup_receiver.recv();
6488 }));
6489
6490 let (pid_sender, pid_receiver) = mpsc::sync_channel(1);
6491 let cancellation = IndexCancellation::new();
6492 let caller_cancellation = cancellation.clone();
6493 let (result_sender, result_receiver) = mpsc::sync_channel(1);
6494 let started = Instant::now();
6495 let caller = thread::spawn(move || {
6496 let result = run_bounded_process_spawn_with(
6497 command,
6498 started + Duration::from_secs(5),
6499 started,
6500 Duration::from_secs(5),
6501 &caller_cancellation,
6502 move |mut command| {
6503 let child = command.spawn()?;
6504 pid_sender
6505 .send(child.id())
6506 .map_err(|_closed| io::Error::other("spawn PID receiver closed"))?;
6507 Ok(child)
6508 },
6509 );
6510 let _send = result_sender.send(result);
6511 });
6512
6513 rendezvous_entered_receiver.recv_timeout(Duration::from_secs(5))?;
6514 let _child_pid = pid_receiver.recv_timeout(Duration::from_secs(1))?;
6515 cancellation.cancel();
6516 release_rendezvous_sender.send(())?;
6517 let result = result_receiver.recv_timeout(Duration::from_secs(1))?;
6518 match result {
6519 Err(ParserSupervisorError::Cancelled {
6520 phase: PROCESS_LAUNCH_PHASE,
6521 }) => {}
6522 other => {
6523 return Err(io::Error::other(format!(
6524 "completed process spawn returned the wrong result: {other:?}"
6525 ))
6526 .into());
6527 }
6528 }
6529 caller
6530 .join()
6531 .map_err(|_panic| io::Error::other("completed-spawn caller panicked"))?;
6532
6533 let cleanup_thread = cleanup_entered_receiver.recv_timeout(Duration::from_secs(1))?;
6534 if cleanup_thread != "projectatlas-process-spawn" {
6535 return Err(io::Error::other(format!(
6536 "unadmitted child cleanup ran on {cleanup_thread:?}"
6537 ))
6538 .into());
6539 }
6540 if !PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) || ProcessSpawnLease::acquire().is_ok() {
6541 return Err(io::Error::other(
6542 "unadmitted child cleanup released its process-wide lease early",
6543 )
6544 .into());
6545 }
6546 if completed.exists() {
6547 return Err(io::Error::other("unadmitted child survived before cleanup").into());
6548 }
6549
6550 release_cleanup_sender.send(())?;
6551 let cleanup_deadline = Instant::now() + SUPERVISOR_CLEANUP_TIMEOUT;
6552 while PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) && Instant::now() < cleanup_deadline {
6553 thread::yield_now();
6554 }
6555 if PROCESS_SPAWN_ACTIVE.load(Ordering::Acquire) {
6556 return Err(io::Error::other(
6557 "unadmitted child cleanup did not release its process-wide lease",
6558 )
6559 .into());
6560 }
6561 require_process_spawn_cleanup_health()?;
6562 if completed.exists() {
6563 return Err(io::Error::other("unadmitted child survived mandatory cleanup").into());
6564 }
6565 drop(ProcessSpawnLease::acquire()?);
6566 Ok(())
6567 }
6568
6569 #[test]
6570 fn launch_command_cancellation_after_process_spawn_commit_returns_after_child_cleanup()
6571 -> Result<(), Box<dyn std::error::Error>> {
6572 let _guard = PROCESS_SPAWN_TEST_LOCK
6573 .lock()
6574 .map_err(|_poisoned| io::Error::other("process-spawn test lock is poisoned"))?;
6575 require_process_spawn_cleanup_health()?;
6576 let _hook_reset = ProcessSpawnTestHookReset;
6577 let temp = tempfile::tempdir()?;
6578 let completed = temp.path().join("post-commit-child-completed");
6579 let mut command = Command::new(std::env::current_exe()?);
6580 command
6581 .arg("--exact")
6582 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6583 .arg("--nocapture")
6584 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6585 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6586 .stdin(Stdio::piped())
6587 .stdout(Stdio::piped())
6588 .stderr(Stdio::null());
6589
6590 let started = Instant::now();
6591 let cancellation = IndexCancellation::new();
6592 let hook_cancellation = cancellation.clone();
6593 *PROCESS_SPAWN_AFTER_FINAL_CHECK_TEST_HOOK
6594 .lock()
6595 .map_err(|_poisoned| io::Error::other("final-check test hook lock is poisoned"))? =
6596 Some(Box::new(move || hook_cancellation.cancel()));
6597 let result = ResidentParserSession::launch_command(
6598 &metadata_only_launch(),
6599 ParserLanguageIdentity::new("alpha")?,
6600 ParserMemoryLimits::PRODUCTION,
6601 started,
6602 started + Duration::from_secs(5),
6603 Duration::from_secs(5),
6604 &cancellation,
6605 command,
6606 );
6607 if !cancellation.is_cancelled() {
6608 return Err(io::Error::other(
6609 "final-check test did not cancel after ownership commitment",
6610 )
6611 .into());
6612 }
6613 match result {
6614 Err(ParserSupervisorError::Cancelled {
6615 phase: PROCESS_LAUNCH_PHASE,
6616 }) => {}
6617 Err(other) => {
6618 return Err(io::Error::other(format!(
6619 "post-commit launch cancellation returned the wrong error: {other:?}"
6620 ))
6621 .into());
6622 }
6623 Ok(resident) => {
6624 resident.shutdown()?;
6625 return Err(io::Error::other(
6626 "post-commit launch cancellation returned a resident session",
6627 )
6628 .into());
6629 }
6630 }
6631 drop(ProcessSpawnLease::acquire()?);
6632 require_process_spawn_cleanup_health()?;
6633 if completed.exists() {
6634 return Err(io::Error::other("post-commit child survived launch cleanup").into());
6635 }
6636 Ok(())
6637 }
6638
6639 #[test]
6640 fn partial_launch_cleanup_releases_full_reader_channels()
6641 -> Result<(), Box<dyn std::error::Error>> {
6642 let temp = tempfile::tempdir()?;
6643 let completed = temp.path().join("partial-launch-child-completed");
6644 let mut child = Command::new(std::env::current_exe()?)
6645 .arg("--exact")
6646 .arg("parser_supervisor::tests::blocked_process_spawn_child_fixture")
6647 .arg("--nocapture")
6648 .env(PROCESS_SPAWN_CHILD_ENV, "1")
6649 .env(PROCESS_SPAWN_CHILD_COMPLETED_ENV, &completed)
6650 .stdin(Stdio::null())
6651 .stdout(Stdio::null())
6652 .stderr(Stdio::null())
6653 .spawn()?;
6654
6655 let (frame_sender, frame_events) = mpsc::sync_channel(1);
6656 let (frame_entered_sender, frame_entered_receiver) = mpsc::sync_channel(1);
6657 let frame_handle = thread::spawn(move || {
6658 let _first = frame_sender.send(FrameReaderEvent::Frame(vec![1]));
6659 let _entered = frame_entered_sender.send(());
6660 let _second = frame_sender.send(FrameReaderEvent::Frame(vec![2]));
6661 });
6662 let (diagnostic_sender, diagnostic_events) = mpsc::sync_channel(1);
6663 let (diagnostic_entered_sender, diagnostic_entered_receiver) = mpsc::sync_channel(1);
6664 let diagnostic_handle = thread::spawn(move || {
6665 let _first = diagnostic_sender.send(DiagnosticReaderEvent::AdmissionAccepted);
6666 let _entered = diagnostic_entered_sender.send(());
6667 let _second = diagnostic_sender.send(DiagnosticReaderEvent::AdmissionAccepted);
6668 Ok(Vec::new())
6669 });
6670 frame_entered_receiver.recv_timeout(Duration::from_secs(1))?;
6671 diagnostic_entered_receiver.recv_timeout(Duration::from_secs(1))?;
6672
6673 cleanup_partial_launch(
6674 &mut child,
6675 vec![frame_handle],
6676 Some(diagnostic_handle),
6677 Some(frame_events),
6678 Some(diagnostic_events),
6679 )?;
6680 if completed.exists() {
6681 return Err(io::Error::other("partial-launch child survived cleanup").into());
6682 }
6683 Ok(())
6684 }
6685
6686 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6687 #[test]
6688 fn memfd_creation_retries_only_unsupported_modern_flags() {
6689 use nix::errno::Errno;
6690 use nix::libc;
6691 use nix::sys::memfd::MFdFlags;
6692
6693 let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING;
6694 let requested = base | MFdFlags::from_bits_retain(libc::MFD_EXEC);
6695 let mut attempts = Vec::new();
6696 let descriptor = create_memfd_with_legacy_fallback(base, libc::MFD_EXEC, |flags| {
6697 attempts.push(flags);
6698 if attempts.len() == 1 {
6699 Err(Errno::EINVAL)
6700 } else {
6701 Ok(7)
6702 }
6703 });
6704 assert_eq!(descriptor, Ok(7));
6705 assert_eq!(attempts, vec![requested, base]);
6706
6707 let mut denied_attempts = Vec::new();
6708 let denied = create_memfd_with_legacy_fallback(base, libc::MFD_EXEC, |flags| {
6709 denied_attempts.push(flags);
6710 Err::<i32, _>(Errno::EPERM)
6711 });
6712 assert_eq!(denied, Err(Errno::EPERM));
6713 assert_eq!(denied_attempts, vec![requested]);
6714 }
6715
6716 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6717 #[test]
6718 fn sealed_linux_payload_is_read_only_complete_and_immutable()
6719 -> Result<(), Box<dyn std::error::Error>> {
6720 use std::os::unix::fs::PermissionsExt;
6721
6722 use nix::fcntl::{FcntlArg, OFlag, SealFlag, fcntl};
6723
6724 let cancellation = IndexCancellation::new();
6725 let control = ArtifactIoControl {
6726 absolute_deadline: Instant::now() + Duration::from_secs(1),
6727 last_progress: Instant::now(),
6728 no_progress_timeout: Duration::from_secs(1),
6729 cancellation: &cancellation,
6730 };
6731 let payload = SealedLinuxPayload::from_verified_bytes(
6732 "test payload",
6733 "projectatlas-sealed-payload-test",
6734 b"verified authority",
6735 false,
6736 &control,
6737 )?;
6738 require_test(
6739 payload.file.metadata()?.permissions().mode() & 0o777 == 0o400,
6740 "sealed document authority retained executable or writable mode bits",
6741 )?;
6742 let status = fcntl(&payload.file, FcntlArg::F_GETFL)?;
6743 require_test(
6744 status & OFlag::O_ACCMODE.bits() == OFlag::O_RDONLY.bits(),
6745 "sealed document authority was not reopened read-only",
6746 )?;
6747 let seals = fcntl(&payload.file, FcntlArg::F_GET_SEALS)?;
6748 let required = SealFlag::F_SEAL_WRITE
6749 | SealFlag::F_SEAL_GROW
6750 | SealFlag::F_SEAL_SHRINK
6751 | SealFlag::F_SEAL_SEAL;
6752 require_test(
6753 seals & required.bits() == required.bits(),
6754 "sealed document authority omitted a required seal",
6755 )?;
6756
6757 let mut reader = payload.file.try_clone()?;
6758 let mut bytes = Vec::new();
6759 reader.read_to_end(&mut bytes)?;
6760 require_test(
6761 bytes == b"verified authority",
6762 "sealed document authority changed after reopening",
6763 )?;
6764 require_test(
6765 payload.file.set_len(0).is_err(),
6766 "sealed document authority allowed truncation",
6767 )?;
6768 let mut writer = payload.file.try_clone()?;
6769 require_test(
6770 writer.write_all(b"attacker").is_err(),
6771 "sealed document authority allowed mutation",
6772 )?;
6773
6774 let executable = SealedLinuxPayload::from_verified_bytes(
6775 "test worker",
6776 "projectatlas-sealed-worker-test",
6777 b"verified executable authority",
6778 true,
6779 &control,
6780 )?;
6781 require_test(
6782 executable.file.metadata()?.permissions().mode() & 0o777 == 0o500,
6783 "sealed executable authority retained writable or unexpected mode bits",
6784 )?;
6785 Ok(())
6786 }
6787
6788 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6789 #[test]
6790 fn sealed_linux_payload_legacy_fallback_preserves_exact_modes()
6791 -> Result<(), Box<dyn std::error::Error>> {
6792 use std::os::unix::fs::PermissionsExt;
6793
6794 use nix::errno::Errno;
6795 use nix::libc;
6796 use nix::sys::memfd::{MFdFlags, memfd_create};
6797
6798 let cancellation = IndexCancellation::new();
6799 let control = ArtifactIoControl {
6800 absolute_deadline: Instant::now() + Duration::from_secs(1),
6801 last_progress: Instant::now(),
6802 no_progress_timeout: Duration::from_secs(1),
6803 cancellation: &cancellation,
6804 };
6805 let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING;
6806 for (name, executable, expected_mode) in [
6807 ("projectatlas-legacy-document-test", false, 0o400),
6808 ("projectatlas-legacy-executable-test", true, 0o500),
6809 ] {
6810 let mode_flag = if executable {
6811 libc::MFD_EXEC
6812 } else {
6813 libc::MFD_NOEXEC_SEAL
6814 };
6815 let mut attempts = Vec::new();
6816 let payload = SealedLinuxPayload::from_verified_bytes_with_create(
6817 "legacy payload",
6818 name,
6819 b"verified authority",
6820 executable,
6821 &control,
6822 |flags| {
6823 attempts.push(flags);
6824 if attempts.len() == 1 {
6825 Err(Errno::EINVAL)
6826 } else {
6827 memfd_create(name, flags | MFdFlags::from_bits_retain(libc::MFD_EXEC))
6830 }
6831 },
6832 )?;
6833 require_test(
6834 attempts == vec![base | MFdFlags::from_bits_retain(mode_flag), base],
6835 "legacy fallback did not retry exactly once with base flags",
6836 )?;
6837 require_test(
6838 payload.file.metadata()?.permissions().mode() & 0o777 == expected_mode,
6839 "legacy fallback retained an unexpected payload mode",
6840 )?;
6841 }
6842 Ok(())
6843 }
6844
6845 #[test]
6846 fn payload_observation_detects_same_size_same_mtime_change_epoch()
6847 -> Result<(), Box<dyn std::error::Error>> {
6848 let temp = tempfile::tempdir()?;
6849 let path = temp.path().join("worker");
6850 fs::write(&path, b"trusted")?;
6851 let modified = fs::metadata(&path)?.modified()?;
6852 let observation = PayloadObservation {
6853 file: FileObservation::capture(path.clone())?,
6854 role: ParserPackPayloadRole::Worker,
6855 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6856 bytes: 7,
6857 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6858 sha256: encode_sha256(Sha256::digest(b"trusted")),
6859 };
6860 require_test(observation.is_current()?, "initial payload was rejected")?;
6861
6862 let mutation = fs::write(&path, b"mutated");
6863 #[cfg(windows)]
6864 if mutation.is_err() {
6865 require_test(
6866 observation.is_current()? && fs::read(&path)? == b"trusted",
6867 "Windows write guard reported failure after changing payload identity",
6868 )?;
6869 return Ok(());
6870 }
6871 mutation?;
6872 File::options()
6873 .write(true)
6874 .open(&path)?
6875 .set_times(fs::FileTimes::new().set_modified(modified))?;
6876 require_test(
6877 fs::metadata(&path)?.len() == observation.file.epoch.bytes
6878 && fs::metadata(&path)?.modified()? == modified,
6879 "test mutation did not preserve size and modification time",
6880 )?;
6881 require_test(
6882 !observation.is_current()?,
6883 "same-size same-mtime payload mutation retained launch authority",
6884 )?;
6885 Ok(())
6886 }
6887
6888 #[test]
6889 fn bounded_artifact_read_rejects_mismatched_captured_epoch()
6890 -> Result<(), Box<dyn std::error::Error>> {
6891 let temp = tempfile::tempdir()?;
6892 let path = temp.path().join("worker");
6893 fs::write(&path, b"trusted")?;
6894 let observation = FileObservation::capture(path.clone())?;
6895
6896 #[cfg(unix)]
6897 let expected_epoch = {
6898 let replacement = temp.path().join("replacement");
6899 let retained = temp.path().join("retained");
6900 fs::write(&replacement, b"mutated")?;
6901 fs::rename(&path, retained)?;
6902 fs::rename(replacement, &path)?;
6903 observation.epoch
6904 };
6905 #[cfg(not(unix))]
6906 let expected_epoch = {
6907 let _write_guard = &observation;
6908 FileChangeEpoch::default()
6909 };
6910
6911 let Err(error) = read_bounded_file(&path, expected_epoch, 7, None) else {
6912 return Err(io::Error::other(
6913 "replacement bytes were accepted under the captured epoch",
6914 )
6915 .into());
6916 };
6917 require_test(
6918 matches!(
6919 error,
6920 ParserSupervisorError::PayloadMismatch {
6921 reason: "artifact read handle does not match the captured file identity",
6922 ..
6923 }
6924 ),
6925 "replacement read did not fail on the captured file epoch",
6926 )?;
6927 Ok(())
6928 }
6929
6930 #[cfg(windows)]
6931 #[test]
6932 fn windows_file_observation_releases_delete_share_on_drop()
6933 -> Result<(), Box<dyn std::error::Error>> {
6934 let temp = tempfile::tempdir()?;
6935 let path = temp.path().join("guarded-payload");
6936 fs::write(&path, b"trusted")?;
6937 let observation = FileObservation::capture(path.clone())?;
6938
6939 require_test(
6940 fs::remove_file(&path).is_err() && path.is_file(),
6941 "Windows observation did not deny payload deletion",
6942 )?;
6943 drop(observation);
6944 fs::remove_file(&path)?;
6945 require_test(
6946 !path.exists(),
6947 "dropping Windows observation did not release payload deletion",
6948 )?;
6949 Ok(())
6950 }
6951
6952 #[test]
6953 fn payload_observation_revalidates_only_launch_inputs() {
6954 let observation = |role| PayloadObservation {
6955 file: FileObservation::unavailable(PathBuf::new()),
6956 role,
6957 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6958 bytes: 0,
6959 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6960 sha256: String::new(),
6961 };
6962
6963 assert!(observation(ParserPackPayloadRole::Worker).contributes_to_launch("rust"));
6964 assert!(
6965 observation(ParserPackPayloadRole::ContainmentBroker).contributes_to_launch("rust")
6966 );
6967 assert!(observation(ParserPackPayloadRole::AcceptedManifest).contributes_to_launch("rust"));
6968 assert!(
6969 observation(ParserPackPayloadRole::GrammarLibrary {
6970 language_id: "rust".to_owned(),
6971 })
6972 .contributes_to_launch("rust")
6973 );
6974 assert!(
6975 !observation(ParserPackPayloadRole::GrammarLibrary {
6976 language_id: "python".to_owned(),
6977 })
6978 .contributes_to_launch("rust")
6979 );
6980 assert!(
6981 !observation(ParserPackPayloadRole::NativeAuditReport).contributes_to_launch("rust")
6982 );
6983 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6984 assert!(
6985 observation(ParserPackPayloadRole::NativeImportPolicy).contributes_to_launch("rust")
6986 );
6987 #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
6988 assert!(
6989 !observation(ParserPackPayloadRole::NativeImportPolicy).contributes_to_launch("rust")
6990 );
6991 }
6992
6993 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
6994 #[test]
6995 fn linux_currentness_probe_detects_native_policy_drift()
6996 -> Result<(), Box<dyn std::error::Error>> {
6997 let temp = tempfile::tempdir()?;
6998 let artifact_path = temp.path().join(ARTIFACT_MANIFEST_FILE_NAME);
6999 let policy_path = temp.path().join("native-policy");
7000 fs::write(&artifact_path, b"artifact")?;
7001 fs::write(&policy_path, b"trusted")?;
7002 let modified = fs::metadata(&policy_path)?.modified()?;
7003
7004 let mut launch = metadata_only_launch();
7005 #[cfg(any(
7006 all(target_os = "linux", target_arch = "x86_64"),
7007 all(target_os = "windows", target_arch = "x86_64")
7008 ))]
7009 {
7010 launch.pack_root = temp.path().to_path_buf();
7011 }
7012 launch.artifact_manifest = FileObservation::capture(artifact_path)?;
7013 launch.payloads = vec![PayloadObservation {
7014 file: FileObservation::capture(policy_path.clone())?,
7015 role: ParserPackPayloadRole::NativeImportPolicy,
7016 bytes: 7,
7017 sha256: encode_sha256(Sha256::digest(b"trusted")),
7018 }];
7019 let probe = launch.currentness_probe("alpha");
7020 require_test(
7021 probe.is_current(None)?,
7022 "initial native policy currentness probe failed",
7023 )?;
7024
7025 fs::write(&policy_path, b"changed")?;
7026 File::options()
7027 .write(true)
7028 .open(&policy_path)?
7029 .set_times(fs::FileTimes::new().set_modified(modified))?;
7030 require_test(
7031 fs::metadata(&policy_path)?.len() == 7,
7032 "native policy mutation did not preserve size",
7033 )?;
7034 require_test(
7035 fs::metadata(&policy_path)?.modified()? == modified,
7036 "native policy mutation did not preserve modification time",
7037 )?;
7038 require_test(
7039 !probe.is_current(None)?,
7040 "same-size same-mtime native policy drift retained launch authority",
7041 )?;
7042 Ok(())
7043 }
7044
7045 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
7046 #[test]
7047 fn linux_worker_launch_closes_inherited_descriptors_on_exec()
7048 -> Result<(), Box<dyn std::error::Error>> {
7049 use std::os::fd::AsRawFd;
7050
7051 use nix::fcntl::{FcntlArg, FdFlag, fcntl};
7052
7053 let cancellation = IndexCancellation::new();
7054 let control = ArtifactIoControl {
7055 absolute_deadline: Instant::now() + Duration::from_secs(1),
7056 last_progress: Instant::now(),
7057 no_progress_timeout: Duration::from_secs(1),
7058 cancellation: &cancellation,
7059 };
7060 let payload = |name| {
7061 SealedLinuxPayload::from_verified_bytes(
7062 "test payload",
7063 name,
7064 b"verified authority",
7065 false,
7066 &control,
7067 )
7068 };
7069 let authority = LinuxResidentLaunchAuthority {
7070 worker: payload("projectatlas-test-worker")?,
7071 artifact_manifest: payload("projectatlas-test-artifact")?,
7072 accepted_manifest: payload("projectatlas-test-accepted")?,
7073 native_import_policy: payload("projectatlas-test-policy")?,
7074 grammar: payload("projectatlas-test-grammar")?,
7075 };
7076 let inherited = File::open("/dev/null")?;
7077 fcntl(&inherited, FcntlArg::F_SETFD(FdFlag::empty()))?;
7078 let descriptor = inherited.as_raw_fd();
7079 let mut command = Command::new("/bin/sh");
7080 command
7081 .args([
7082 "-c",
7083 "test ! -e \"/proc/self/fd/$1\"",
7084 "parser-worker-fd-check",
7085 ])
7086 .arg(descriptor.to_string());
7087 inherit_linux_authority_on_exec(&mut command, authority);
7088
7089 if command.status()?.success() {
7090 Ok(())
7091 } else {
7092 Err(io::Error::other("worker inherited an injected descriptor").into())
7093 }
7094 }
7095
7096 #[test]
7097 fn supervisor_memory_limits_reject_zero_reversed_and_runtime_excess() {
7098 for candidate in [
7099 ParserMemoryLimits {
7100 process_bytes: 0,
7101 process_tree_bytes: 1,
7102 },
7103 ParserMemoryLimits {
7104 process_bytes: 2,
7105 process_tree_bytes: 1,
7106 },
7107 ParserMemoryLimits {
7108 process_bytes: PARSER_WORKER_PROCESS_MEMORY_BYTES.saturating_add(1),
7109 process_tree_bytes: PARSER_WORKER_JOB_MEMORY_BYTES.saturating_add(1),
7110 },
7111 ] {
7112 assert!(matches!(
7113 candidate.checked(),
7114 Err(ParserSupervisorError::InvalidMemoryLimits { .. })
7115 ));
7116 }
7117 assert!(ParserMemoryLimits::PRODUCTION.checked().is_ok());
7118 }
7119
7120 #[test]
7121 fn memory_probe_source_preserves_linux_and_bounds_windows() {
7122 let fixture = b".\n";
7123 assert_eq!(
7124 memory_probe_source(PackPlatform::LinuxX86_64, fixture),
7125 fixture
7126 );
7127 let windows = memory_probe_source(PackPlatform::WindowsX86_64, fixture);
7128 assert_eq!(windows.len(), WINDOWS_MEMORY_PROBE_SOURCE_BYTES);
7129 assert_eq!(&windows[..4], b".\n.\n");
7130 assert!(windows.len() <= PARSER_MAX_SOURCE_BYTES as usize);
7131 }
7132
7133 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
7134 #[test]
7135 fn windows_job_memory_exit_code_is_reserved_and_typed() -> Result<(), Box<dyn std::error::Error>>
7136 {
7137 let mut memory_command = Command::new("cmd.exe");
7138 memory_command
7139 .args(["/D", "/Q", "/C"])
7140 .arg(format!(
7141 "set /p _= & exit /B {PARSER_WINDOWS_BROKER_MEMORY_LIMIT_EXIT_CODE}"
7142 ))
7143 .stdin(Stdio::piped());
7144 let mut memory_exit = memory_command.spawn()?;
7145 let mut memory_input = memory_exit
7146 .stdin
7147 .take()
7148 .ok_or_else(|| io::Error::other("delayed broker memory-limit stdin is absent"))?;
7149 require_test(
7150 memory_exit.try_wait()?.is_none(),
7151 "delayed broker memory-limit process exited before observation",
7152 )?;
7153 let release_memory_exit = thread::spawn(move || {
7154 thread::sleep(Duration::from_millis(100));
7155 memory_input.write_all(b"\n")
7156 });
7157 let diagnostic = ParserIoThreadError::UnexpectedDiagnostic {
7158 diagnostic: "tree-sitter failed to allocate 8".to_owned(),
7159 };
7160 let observation_timeout = Duration::from_secs(5);
7161 let started = Instant::now();
7162 let memory_diagnostic_result = diagnostic_failure_after_exit_observation(
7163 &mut memory_exit,
7164 "request response",
7165 &diagnostic,
7166 started + observation_timeout,
7167 started,
7168 observation_timeout,
7169 &IndexCancellation::new(),
7170 );
7171 release_memory_exit
7172 .join()
7173 .map_err(|_panic| io::Error::other("delayed broker memory-limit release panicked"))??;
7174 if !matches!(
7175 memory_diagnostic_result,
7176 ParserSupervisorError::WindowsJobMemoryLimitExceeded {
7177 phase: "request response"
7178 }
7179 ) {
7180 return Err(std::io::Error::other(format!(
7181 "reserved broker memory-limit status did not override diagnostic bytes: {memory_diagnostic_result:?}"
7182 ))
7183 .into());
7184 }
7185 let memory_result =
7186 frame_event_result(FrameReaderEvent::EndOfStream, &mut memory_exit, "READY");
7187 if !matches!(
7188 memory_result,
7189 Err(ParserSupervisorError::WindowsJobMemoryLimitExceeded { phase: "READY" })
7190 ) {
7191 return Err(std::io::Error::other(format!(
7192 "reserved broker memory-limit status produced {memory_result:?}"
7193 ))
7194 .into());
7195 }
7196
7197 let mut ordinary_exit = Command::new("cmd.exe")
7198 .args(["/D", "/C", "exit", "125"])
7199 .spawn()?;
7200 ordinary_exit.wait()?;
7201 let ordinary_diagnostic_result = diagnostic_failure_after_exit_observation(
7202 &mut ordinary_exit,
7203 "request response",
7204 &diagnostic,
7205 started + Duration::from_secs(1),
7206 started,
7207 Duration::from_secs(1),
7208 &IndexCancellation::new(),
7209 );
7210 if !matches!(
7211 ordinary_diagnostic_result,
7212 ParserSupervisorError::IoThread {
7213 phase: "request response",
7214 ..
7215 }
7216 ) {
7217 return Err(std::io::Error::other(format!(
7218 "ordinary broker failure status replaced fail-closed diagnostics: {ordinary_diagnostic_result:?}"
7219 ))
7220 .into());
7221 }
7222 let ordinary_result =
7223 frame_event_result(FrameReaderEvent::EndOfStream, &mut ordinary_exit, "READY");
7224 if !matches!(
7225 ordinary_result,
7226 Err(ParserSupervisorError::ChildExited {
7227 phase: "READY",
7228 code: Some(125)
7229 })
7230 ) {
7231 return Err(std::io::Error::other(format!(
7232 "ordinary broker failure status produced {ordinary_result:?}"
7233 ))
7234 .into());
7235 }
7236 Ok(())
7237 }
7238
7239 #[test]
7240 fn cleanup_error_helper_includes_nested_combined_failures() {
7241 let nested = ParserSupervisorError::OperationAndCleanup {
7242 operation: Box::new(ParserSupervisorError::Cancelled { phase: "test" }),
7243 cleanup: Box::new(ParserSupervisorError::OperationAndCleanup {
7244 operation: Box::new(ParserSupervisorError::DeadlineExceeded { phase: "test" }),
7245 cleanup: Box::new(ParserSupervisorError::Cleanup {
7246 message: "reap failed".to_owned(),
7247 }),
7248 }),
7249 };
7250 assert!(nested.has_mandatory_cleanup_failure());
7251 assert_eq!(
7252 nested.to_string(),
7253 "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"
7254 );
7255 assert!(
7256 ParserSupervisorError::Cleanup {
7257 message: "drain failed".to_owned()
7258 }
7259 .has_mandatory_cleanup_failure()
7260 );
7261 assert!(
7262 !ParserSupervisorError::Cancelled { phase: "test" }.has_mandatory_cleanup_failure()
7263 );
7264 }
7265
7266 struct GateWriter {
7268 entered: SyncSender<()>,
7270 release: Receiver<()>,
7272 }
7273
7274 impl Write for GateWriter {
7275 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
7276 self.entered
7277 .send(())
7278 .map_err(|error| io::Error::other(error.to_string()))?;
7279 self.release
7280 .recv()
7281 .map_err(|error| io::Error::other(error.to_string()))?;
7282 Ok(bytes.len())
7283 }
7284
7285 fn flush(&mut self) -> io::Result<()> {
7286 Ok(())
7287 }
7288 }
7289
7290 fn writer_command(
7292 bytes: Vec<u8>,
7293 ) -> (WriterCommand, Receiver<Result<(), ParserIoThreadError>>) {
7294 let (acknowledgement, result) = mpsc::sync_channel(1);
7295 (
7296 WriterCommand {
7297 bytes,
7298 acknowledgement,
7299 },
7300 result,
7301 )
7302 }
7303
7304 fn require_test(condition: bool, message: &'static str) -> io::Result<()> {
7306 if condition {
7307 Ok(())
7308 } else {
7309 Err(io::Error::other(message))
7310 }
7311 }
7312
7313 #[test]
7315 fn frame_reader_bounds_header_before_payload_allocation() {
7316 let declared = PARSER_MAX_SOURCE_BYTES.saturating_add(1).to_be_bytes();
7317 let bytes = [
7318 b'P',
7319 b'A',
7320 PARSER_PROTOCOL_VERSION,
7321 ParserFrameKind::RawSource.as_u8(),
7322 declared[0],
7323 declared[1],
7324 declared[2],
7325 declared[3],
7326 b'x',
7327 ];
7328 let mut input = Cursor::new(bytes);
7329 assert!(matches!(
7330 read_one_frame(&mut input),
7331 Err(ParserIoThreadError::FrameHeader {
7332 source: ParserProtocolError::FramePayloadTooLarge { .. }
7333 })
7334 ));
7335 assert_eq!(input.position(), PARSER_FRAME_HEADER_BYTES as u64);
7336 }
7337
7338 #[test]
7340 fn writer_queue_has_one_pending_slot() -> Result<(), Box<dyn std::error::Error>> {
7341 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
7342 let (release_sender, release_receiver) = mpsc::sync_channel(1);
7343 let (commands, receiver) = mpsc::sync_channel(1);
7344 let handle = thread::spawn(move || {
7345 writer_loop(
7346 GateWriter {
7347 entered: entered_sender,
7348 release: release_receiver,
7349 },
7350 &receiver,
7351 );
7352 });
7353
7354 let (first, first_result) = writer_command(vec![1]);
7355 commands.send(first)?;
7356 entered_receiver.recv_timeout(Duration::from_secs(1))?;
7357 let (second, second_result) = writer_command(vec![2]);
7358 commands.send(second)?;
7359 let (third, _third_result) = writer_command(vec![3]);
7360 require_test(
7361 matches!(commands.try_send(third), Err(TrySendError::Full(_))),
7362 "writer queue accepted more than one pending write",
7363 )?;
7364
7365 release_sender.send(())?;
7366 first_result.recv_timeout(Duration::from_secs(1))??;
7367 entered_receiver.recv_timeout(Duration::from_secs(1))?;
7368 release_sender.send(())?;
7369 second_result.recv_timeout(Duration::from_secs(1))??;
7370 drop(commands);
7371 handle
7372 .join()
7373 .map_err(|_panic| io::Error::other("writer test thread panicked"))?;
7374 Ok(())
7375 }
7376
7377 #[test]
7379 fn stop_polling_preserves_independent_bounds() {
7380 let cancellation = IndexCancellation::new();
7381 let future = Instant::now()
7382 .checked_add(Duration::from_secs(1))
7383 .unwrap_or_else(Instant::now);
7384 assert!(
7385 poll_stop(
7386 "test",
7387 future,
7388 Instant::now(),
7389 Duration::from_secs(1),
7390 &cancellation,
7391 )
7392 .is_ok()
7393 );
7394 cancellation.cancel();
7395 assert!(matches!(
7396 poll_stop(
7397 "test",
7398 future,
7399 Instant::now(),
7400 Duration::from_secs(1),
7401 &cancellation,
7402 ),
7403 Err(ParserSupervisorError::Cancelled { .. })
7404 ));
7405 assert!(matches!(
7406 poll_stop(
7407 "test",
7408 Instant::now(),
7409 Instant::now(),
7410 Duration::from_secs(1),
7411 &IndexCancellation::new(),
7412 ),
7413 Err(ParserSupervisorError::DeadlineExceeded { .. })
7414 ));
7415 assert!(matches!(
7416 poll_stop(
7417 "test",
7418 future,
7419 Instant::now(),
7420 Duration::ZERO,
7421 &IndexCancellation::new(),
7422 ),
7423 Err(ParserSupervisorError::NoProgress { .. })
7424 ));
7425 }
7426
7427 #[test]
7429 fn completion_keeps_root_error_and_rejects_cross_session_replay()
7430 -> Result<(), Box<dyn std::error::Error>> {
7431 let source = b"broken";
7432 let session = ParserSessionIdentity::for_entropy(b"session-one");
7433 let artifact = ParserArtifactIdentity::new(ParserContentDigest::for_bytes(b"artifact"));
7434 let language = ParserLanguageIdentity::new("abl")?;
7435 let limits = ParserRequestLimits::new(1024, 100, 100)?;
7436 let request = ParserRequest::new(
7437 session,
7438 ParserRequestIdentity::new(1)?,
7439 artifact.clone(),
7440 language.clone(),
7441 ParserSourceIdentity::for_bytes(source)?,
7442 limits,
7443 );
7444 let evidence = ParserCompletionEvidence::new(
7445 ParserSyntaxKind::new("source_file")?,
7446 0,
7447 u32::try_from(source.len())?,
7448 true,
7449 1,
7450 1,
7451 0,
7452 1,
7453 )?;
7454 let encoded = encode_parser_control(&ParserControl::Completion(ParserCompletion::new(
7455 ParserResponseIdentity::for_request(&request),
7456 evidence,
7457 )))?;
7458 let decoded =
7459 decode_parser_completion_for_request(ParserFrame::decode_exact(&encoded)?, &request)?;
7460 require_test(
7461 decoded.evidence().root_has_error(),
7462 "completion lost root_has_error",
7463 )?;
7464
7465 let replay_target = ParserRequest::new(
7466 ParserSessionIdentity::for_entropy(b"session-two"),
7467 ParserRequestIdentity::new(1)?,
7468 artifact,
7469 language,
7470 ParserSourceIdentity::for_bytes(source)?,
7471 limits,
7472 );
7473 require_test(
7474 decode_parser_completion_for_request(
7475 ParserFrame::decode_exact(&encoded)?,
7476 &replay_target,
7477 )
7478 .is_err(),
7479 "cross-session completion replay was accepted",
7480 )?;
7481 Ok(())
7482 }
7483
7484 #[test]
7486 fn diagnostic_reader_validates_admission_before_diagnostics()
7487 -> Result<(), Box<dyn std::error::Error>> {
7488 let mut bytes = PARSER_WINDOWS_BROKER_ADMISSION_RECORD.to_vec();
7489 bytes.extend_from_slice(b"bounded diagnostic");
7490 let (events, receiver) = mpsc::sync_channel(2);
7491 let diagnostics = diagnostic_reader_loop(
7492 Cursor::new(bytes),
7493 true,
7494 DiagnosticFence([0xA5; PARSER_DIAGNOSTIC_FENCE_BYTES]),
7495 &events,
7496 )?;
7497 require_test(
7498 matches!(
7499 receiver.recv_timeout(Duration::from_secs(1))?,
7500 DiagnosticReaderEvent::AdmissionAccepted
7501 ),
7502 "diagnostics became visible before admission",
7503 )?;
7504 require_test(
7505 matches!(
7506 receiver.recv_timeout(Duration::from_secs(1))?,
7507 DiagnosticReaderEvent::Failure(ParserIoThreadError::UnexpectedDiagnostic { .. })
7508 ),
7509 "bounded diagnostic bytes did not fail closed",
7510 )?;
7511 require_test(
7512 diagnostics == b"bounded diagnostic",
7513 "diagnostic bytes changed",
7514 )?;
7515 Ok(())
7516 }
7517
7518 #[test]
7520 fn linux_memory_accounting_records_are_strict() {
7521 assert_eq!(
7522 parse_process_rss("Name:\tworker\nVmRSS:\t4096 kB\n").ok(),
7523 Some(4 * 1024 * 1024)
7524 );
7525 assert!(parse_process_rss("VmRSS:\t4096 MB\n").is_err());
7526 assert!(parse_process_rss("VmSize:\t4096 kB\n").is_err());
7527 assert_eq!(
7528 parse_cgroup_event("low 0\nhigh 1\nmax 7\noom 0\n", "max").ok(),
7529 Some(7)
7530 );
7531 assert!(parse_cgroup_event("max 7 extra\n", "max").is_err());
7532 assert!(has_cgroup_token("cpu io memory", "memory"));
7533 assert!(has_cgroup_token("+cpu +memory", "memory"));
7534 assert!(!has_cgroup_token("cpu memory.swap", "memory"));
7535 assert!(matches!(
7536 parse_unified_cgroup_path("0::/user.slice/projectatlas.scope/worker\n"),
7537 Ok(Some(path)) if path == Path::new("user.slice/projectatlas.scope/worker")
7538 ));
7539 assert!(matches!(
7540 parse_unified_cgroup_path("0::/\n"),
7541 Ok(Some(path)) if path.as_os_str().is_empty()
7542 ));
7543 assert!(parse_unified_cgroup_path("0::/safe/../escape\n").is_err());
7544 assert!(parse_unified_cgroup_path("0::/one\n0::/two\n").is_err());
7545 assert_eq!(
7546 PARSER_LINUX_RSS_OBSERVATION_INTERVAL,
7547 SUPERVISOR_POLL_INTERVAL
7548 );
7549 }
7550
7551 #[test]
7554 fn linux_memory_exit_transition_observes_waitable_child() -> io::Result<()> {
7555 let exit_checks = std::cell::Cell::new(0_u8);
7556 let observation = resolve_linux_memory_exit_transition(
7557 io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"),
7558 SUPERVISOR_POLL_INTERVAL,
7559 || {
7560 Err(io::Error::new(
7561 io::ErrorKind::InvalidData,
7562 "VmRSS is absent",
7563 ))
7564 },
7565 || {
7566 let checks = exit_checks.get();
7567 exit_checks.set(checks.saturating_add(1));
7568 Ok((checks > 0).then_some(LinuxChildExit { code: Some(17) }))
7569 },
7570 )?;
7571 require_test(
7572 matches!(
7573 observation,
7574 LinuxMemoryObservation::ChildExited { code: Some(17) }
7575 ),
7576 "exit transition did not become waitable",
7577 )
7578 }
7579
7580 #[test]
7582 fn linux_memory_exit_transition_enforces_recovered_observation() -> io::Result<()> {
7583 let observation = resolve_linux_memory_exit_transition(
7584 io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"),
7585 SUPERVISOR_POLL_INTERVAL,
7586 || {
7587 Ok(Some(LinuxMemoryBreach {
7588 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
7589 observed_bytes: 4096,
7590 }))
7591 },
7592 || Ok(None),
7593 )?;
7594 require_test(
7595 matches!(
7596 observation,
7597 LinuxMemoryObservation::Memory(Some(LinuxMemoryBreach {
7598 accounting: ParserMemoryAccountingKind::LinuxProcStatus,
7599 observed_bytes: 4096,
7600 }))
7601 ),
7602 "recovered memory accounting was not retained",
7603 )
7604 }
7605
7606 #[test]
7608 fn linux_memory_exit_transition_retains_unreadable_failure() -> io::Result<()> {
7609 let Err(error) = resolve_linux_memory_exit_transition(
7610 io::Error::new(io::ErrorKind::InvalidData, "VmRSS is absent"),
7611 Duration::ZERO,
7612 || {
7613 Err(io::Error::new(
7614 io::ErrorKind::InvalidData,
7615 "VmRSS remains absent",
7616 ))
7617 },
7618 || Ok(None),
7619 ) else {
7620 return Err(io::Error::other(
7621 "unreadable live accounting did not fail closed",
7622 ));
7623 };
7624 require_test(
7625 error.to_string() == "VmRSS remains absent",
7626 "unreadable memory failure changed",
7627 )
7628 }
7629}