bounded_process.rs (49170B)
1 //! Hermetic, resource-bounded subprocess execution for xtask. 2 //! 3 //! This module owns command construction so callers cannot bypass replacement 4 //! environment, closed-stdin, process-group, deadline, or stream-cap policy. 5 6 use std::collections::BTreeMap; 7 use std::ffi::{OsStr, OsString}; 8 use std::fmt; 9 use std::io; 10 use std::path::PathBuf; 11 use std::process::ExitStatus; 12 use std::time::Duration; 13 14 const DEFAULT_DEADLINE: Duration = Duration::from_secs(60); 15 const DEFAULT_STREAM_LIMIT: usize = 1024 * 1024; 16 const DEFAULT_TERMINATION_GRACE: Duration = Duration::from_millis(250); 17 const MAX_DEADLINE: Duration = Duration::from_secs(86_400); 18 const MAX_TERMINATION_GRACE: Duration = Duration::from_secs(5); 19 const MAX_STREAM_LIMIT: usize = 67_108_864; 20 const MAX_ENVIRONMENT_ENTRIES: usize = 64; 21 const MAX_ENVIRONMENT_NAME_BYTES: usize = 128; 22 const MAX_ENVIRONMENT_VALUE_BYTES: usize = 65_536; 23 24 const FORBIDDEN_ENVIRONMENT_NAMES: [&str; 10] = [ 25 "CARGO_ENCODED_RUSTFLAGS", 26 "DYLD_INSERT_LIBRARIES", 27 "DYLD_LIBRARY_PATH", 28 "LD_LIBRARY_PATH", 29 "LD_PRELOAD", 30 "NIX_CONFIG", 31 "NIX_PATH", 32 "NIXPKGS_ALLOW_BROKEN", 33 "NIXPKGS_ALLOW_UNFREE", 34 "RUSTFLAGS", 35 ]; 36 37 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 38 pub(crate) enum EnvironmentRejection { 39 DuplicateName, 40 ForbiddenControl, 41 InvalidNameOrNul, 42 SensitiveName, 43 TooManyEntries, 44 ValueTooLarge, 45 } 46 47 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 48 pub(crate) enum OutputLimitBreach { 49 Stdout, 50 Stderr, 51 Both, 52 } 53 54 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 55 pub(crate) enum ProcessFailureKind { 56 CleanupIncomplete, 57 DeadlineExceeded, 58 EnvironmentRejected(EnvironmentRejection), 59 InvalidConfiguration, 60 OrphanedDescendants, 61 OutputLimitExceeded, 62 PipeSetup, 63 Poll, 64 Read, 65 Signal, 66 Spawn, 67 #[cfg_attr(unix, allow(dead_code))] 68 UnsupportedPlatform, 69 Wait, 70 } 71 72 /// A deliberately redacted process failure. 73 pub(crate) struct ProcessError { 74 kind: ProcessFailureKind, 75 io_kind: Option<io::ErrorKind>, 76 output_limit: Option<OutputLimitBreach>, 77 } 78 79 impl ProcessError { 80 fn new(kind: ProcessFailureKind) -> Self { 81 Self { 82 kind, 83 io_kind: None, 84 output_limit: None, 85 } 86 } 87 88 fn from_io(kind: ProcessFailureKind, error: &io::Error) -> Self { 89 Self { 90 kind, 91 io_kind: Some(error.kind()), 92 output_limit: None, 93 } 94 } 95 96 #[cfg(unix)] 97 fn from_errno(kind: ProcessFailureKind, error: rustix::io::Errno) -> Self { 98 Self { 99 kind, 100 io_kind: Some(error.kind()), 101 output_limit: None, 102 } 103 } 104 105 fn output_limit(breach: OutputLimitBreach) -> Self { 106 Self { 107 kind: ProcessFailureKind::OutputLimitExceeded, 108 io_kind: None, 109 output_limit: Some(breach), 110 } 111 } 112 113 pub(crate) fn kind(&self) -> ProcessFailureKind { 114 self.kind 115 } 116 117 pub(crate) fn output_limit_breach(&self) -> Option<OutputLimitBreach> { 118 self.output_limit 119 } 120 } 121 122 impl fmt::Debug for ProcessError { 123 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 124 formatter 125 .debug_struct("ProcessError") 126 .field("kind", &self.kind) 127 .field("io_kind", &self.io_kind) 128 .field("output_limit", &self.output_limit) 129 .finish() 130 } 131 } 132 133 impl fmt::Display for ProcessError { 134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 135 let message = match self.kind { 136 ProcessFailureKind::CleanupIncomplete => { 137 "bounded process cleanup did not reach a verified terminal state" 138 } 139 ProcessFailureKind::DeadlineExceeded => "bounded process deadline exceeded", 140 ProcessFailureKind::EnvironmentRejected(_) => { 141 "bounded process replacement environment was rejected" 142 } 143 ProcessFailureKind::InvalidConfiguration => { 144 "bounded process configuration was rejected" 145 } 146 ProcessFailureKind::OrphanedDescendants => { 147 "bounded process leader exited while descendants remained" 148 } 149 ProcessFailureKind::OutputLimitExceeded => "bounded process output limit exceeded", 150 ProcessFailureKind::PipeSetup => "bounded process pipe setup failed", 151 ProcessFailureKind::Poll => "bounded process polling failed", 152 ProcessFailureKind::Read => "bounded process output read failed", 153 ProcessFailureKind::Signal => "bounded process group signaling failed", 154 ProcessFailureKind::Spawn => "bounded process spawn failed", 155 ProcessFailureKind::UnsupportedPlatform => { 156 "bounded process groups are unsupported on this platform" 157 } 158 ProcessFailureKind::Wait => "bounded process wait failed", 159 }; 160 formatter.write_str(message) 161 } 162 } 163 164 impl std::error::Error for ProcessError {} 165 166 /// An explicit replacement environment. No ambient snapshot is ever taken. 167 #[derive(Clone, Default)] 168 pub(crate) struct ReplacementEnvironment { 169 entries: BTreeMap<OsString, OsString>, 170 } 171 172 impl ReplacementEnvironment { 173 pub(crate) fn insert( 174 &mut self, 175 name: impl Into<OsString>, 176 value: impl Into<OsString>, 177 ) -> Result<(), ProcessError> { 178 let name = name.into(); 179 let value = value.into(); 180 let name_text = name 181 .to_str() 182 .ok_or_else(|| environment_error(EnvironmentRejection::InvalidNameOrNul))?; 183 if let Some(rejection) = environment_rejection(name_text) { 184 return Err(environment_error(rejection)); 185 } 186 if os_value_contains_nul(&value) { 187 return Err(environment_error(EnvironmentRejection::InvalidNameOrNul)); 188 } 189 if os_value_byte_len(&value) > MAX_ENVIRONMENT_VALUE_BYTES { 190 return Err(environment_error(EnvironmentRejection::ValueTooLarge)); 191 } 192 if self.entries.contains_key(&name) { 193 return Err(environment_error(EnvironmentRejection::DuplicateName)); 194 } 195 if self.entries.len() == MAX_ENVIRONMENT_ENTRIES { 196 return Err(environment_error(EnvironmentRejection::TooManyEntries)); 197 } 198 self.entries.insert(name, value); 199 Ok(()) 200 } 201 } 202 203 impl fmt::Debug for ReplacementEnvironment { 204 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 205 formatter 206 .debug_struct("ReplacementEnvironment") 207 .field("entry_count", &self.entries.len()) 208 .field("values", &"<redacted>") 209 .finish() 210 } 211 } 212 213 fn environment_error(rejection: EnvironmentRejection) -> ProcessError { 214 ProcessError::new(ProcessFailureKind::EnvironmentRejected(rejection)) 215 } 216 217 fn environment_rejection(name: &str) -> Option<EnvironmentRejection> { 218 if name.is_empty() 219 || name.len() > MAX_ENVIRONMENT_NAME_BYTES 220 || name.as_bytes().contains(&0) 221 || !name.is_ascii() 222 || !name 223 .bytes() 224 .next() 225 .is_some_and(|byte| byte == b'_' || byte.is_ascii_uppercase()) 226 || !name 227 .bytes() 228 .all(|byte| byte == b'_' || byte.is_ascii_uppercase() || byte.is_ascii_digit()) 229 { 230 return Some(EnvironmentRejection::InvalidNameOrNul); 231 } 232 if ["CREDENTIAL", "KEY", "PASSWORD", "SECRET", "TOKEN"] 233 .iter() 234 .any(|pattern| name.contains(pattern)) 235 { 236 return Some(EnvironmentRejection::SensitiveName); 237 } 238 if FORBIDDEN_ENVIRONMENT_NAMES.contains(&name) 239 || name == "CARGO_BUILD_RUSTFLAGS" 240 || (name.starts_with("CARGO_TARGET_") && name.ends_with("_RUSTFLAGS")) 241 || name.starts_with("DYLD_") 242 || matches!( 243 name, 244 "LD_AUDIT" 245 | "LD_DEBUG" 246 | "LD_PROFILE" 247 | "RUSTC_WRAPPER" 248 | "RUSTC_WORKSPACE_WRAPPER" 249 | "RUSTDOCFLAGS" 250 ) 251 { 252 return Some(EnvironmentRejection::ForbiddenControl); 253 } 254 None 255 } 256 257 #[cfg(unix)] 258 fn os_value_byte_len(value: &OsStr) -> usize { 259 use std::os::unix::ffi::OsStrExt; 260 value.as_bytes().len() 261 } 262 263 #[cfg(unix)] 264 fn os_value_contains_nul(value: &OsStr) -> bool { 265 use std::os::unix::ffi::OsStrExt; 266 value.as_bytes().contains(&0) 267 } 268 269 #[cfg(windows)] 270 fn os_value_byte_len(value: &OsStr) -> usize { 271 use std::os::windows::ffi::OsStrExt; 272 value.encode_wide().count().saturating_mul(2) 273 } 274 275 #[cfg(windows)] 276 fn os_value_contains_nul(value: &OsStr) -> bool { 277 use std::os::windows::ffi::OsStrExt; 278 value.encode_wide().any(|unit| unit == 0) 279 } 280 281 #[cfg(not(any(unix, windows)))] 282 fn os_value_byte_len(value: &OsStr) -> usize { 283 value.to_string_lossy().len() 284 } 285 286 #[cfg(not(any(unix, windows)))] 287 fn os_value_contains_nul(value: &OsStr) -> bool { 288 value.to_string_lossy().contains('\0') 289 } 290 291 pub(crate) struct ProcessRequest { 292 program: OsString, 293 arguments: Vec<OsString>, 294 current_dir: Option<PathBuf>, 295 environment: ReplacementEnvironment, 296 deadline: Duration, 297 stdout_limit: usize, 298 stderr_limit: usize, 299 termination_grace: Duration, 300 } 301 302 impl ProcessRequest { 303 pub(crate) fn new(program: impl Into<OsString>) -> Self { 304 Self { 305 program: program.into(), 306 arguments: Vec::new(), 307 current_dir: None, 308 environment: ReplacementEnvironment::default(), 309 deadline: DEFAULT_DEADLINE, 310 stdout_limit: DEFAULT_STREAM_LIMIT, 311 stderr_limit: DEFAULT_STREAM_LIMIT, 312 termination_grace: DEFAULT_TERMINATION_GRACE, 313 } 314 } 315 316 pub(crate) fn arg(mut self, argument: impl Into<OsString>) -> Self { 317 self.arguments.push(argument.into()); 318 self 319 } 320 321 pub(crate) fn current_dir(mut self, current_dir: impl Into<PathBuf>) -> Self { 322 self.current_dir = Some(current_dir.into()); 323 self 324 } 325 326 pub(crate) fn environment(mut self, environment: ReplacementEnvironment) -> Self { 327 self.environment = environment; 328 self 329 } 330 331 pub(crate) fn deadline(mut self, deadline: Duration) -> Self { 332 self.deadline = deadline; 333 self 334 } 335 336 pub(crate) fn output_limits(mut self, stdout_limit: usize, stderr_limit: usize) -> Self { 337 self.stdout_limit = stdout_limit; 338 self.stderr_limit = stderr_limit; 339 self 340 } 341 342 pub(crate) fn termination_grace(mut self, termination_grace: Duration) -> Self { 343 self.termination_grace = termination_grace; 344 self 345 } 346 347 fn validate(&self) -> Result<(), ProcessError> { 348 if self.program.is_empty() 349 || self.deadline.is_zero() 350 || self.deadline > MAX_DEADLINE 351 || self.stdout_limit > MAX_STREAM_LIMIT 352 || self.stderr_limit > MAX_STREAM_LIMIT 353 || self.termination_grace > MAX_TERMINATION_GRACE 354 { 355 return Err(ProcessError::new(ProcessFailureKind::InvalidConfiguration)); 356 } 357 Ok(()) 358 } 359 } 360 361 impl fmt::Debug for ProcessRequest { 362 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 363 formatter 364 .debug_struct("ProcessRequest") 365 .field("program", &"<redacted>") 366 .field("argument_count", &self.arguments.len()) 367 .field( 368 "current_dir", 369 &self.current_dir.as_ref().map(|_| "<redacted>"), 370 ) 371 .field("environment", &self.environment) 372 .field("deadline", &self.deadline) 373 .field("stdout_limit", &self.stdout_limit) 374 .field("stderr_limit", &self.stderr_limit) 375 .field("termination_grace", &self.termination_grace) 376 .finish() 377 } 378 } 379 380 pub(crate) struct ProcessOutput { 381 status: ExitStatus, 382 stdout: Vec<u8>, 383 stderr: Vec<u8>, 384 } 385 386 impl ProcessOutput { 387 pub(crate) fn status(&self) -> ExitStatus { 388 self.status 389 } 390 391 pub(crate) fn stdout(&self) -> &[u8] { 392 &self.stdout 393 } 394 395 pub(crate) fn stderr(&self) -> &[u8] { 396 &self.stderr 397 } 398 } 399 400 impl fmt::Debug for ProcessOutput { 401 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 402 formatter 403 .debug_struct("ProcessOutput") 404 .field("success", &self.status.success()) 405 .field("code", &self.status.code()) 406 .field("stdout_bytes", &self.stdout.len()) 407 .field("stderr_bytes", &self.stderr.len()) 408 .field("contents", &"<redacted>") 409 .finish() 410 } 411 } 412 413 pub(crate) fn run(request: &ProcessRequest) -> Result<ProcessOutput, ProcessError> { 414 request.validate()?; 415 #[cfg(unix)] 416 { 417 unix::run(request) 418 } 419 #[cfg(not(unix))] 420 { 421 let _ = request; 422 Err(ProcessError::new(ProcessFailureKind::UnsupportedPlatform)) 423 } 424 } 425 426 #[cfg(unix)] 427 mod unix { 428 use super::{ 429 OutputLimitBreach, ProcessError, ProcessFailureKind, ProcessOutput, ProcessRequest, 430 }; 431 use rustix::event::{PollFd, PollFlags, Timespec, poll}; 432 use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl}; 433 use rustix::io::Errno; 434 use rustix::process::{Pid, Signal, kill_process_group, test_kill_process_group}; 435 use std::io::{self, Read}; 436 use std::os::unix::process::CommandExt; 437 use std::process::{Child, ChildStderr, ChildStdout, Command, ExitStatus, Stdio}; 438 use std::thread; 439 use std::time::{Duration, Instant}; 440 441 const POLL_QUANTUM: Duration = Duration::from_millis(10); 442 const FORCE_KILL_GRACE: Duration = Duration::from_secs(1); 443 const MAX_DRAIN_BYTES_PER_PASS: usize = 64 * 1024; 444 445 struct Capture { 446 bytes: Vec<u8>, 447 limit: usize, 448 exceeded: bool, 449 } 450 451 impl Capture { 452 fn new(limit: usize) -> Self { 453 Self { 454 bytes: Vec::new(), 455 limit, 456 exceeded: false, 457 } 458 } 459 460 fn record(&mut self, chunk: &[u8]) -> bool { 461 let was_exceeded = self.exceeded; 462 let remaining = self.limit.saturating_sub(self.bytes.len()); 463 let retained = remaining.min(chunk.len()); 464 self.bytes.extend_from_slice(&chunk[..retained]); 465 if retained < chunk.len() { 466 self.exceeded = true; 467 } 468 !was_exceeded && self.exceeded 469 } 470 } 471 472 enum StopCause { 473 Deadline, 474 Error(ProcessError), 475 OrphanedDescendants, 476 OutputLimit, 477 RetainedPipes, 478 } 479 480 pub(super) fn run(request: &ProcessRequest) -> Result<ProcessOutput, ProcessError> { 481 let deadline = Instant::now() 482 .checked_add(request.deadline) 483 .ok_or_else(|| ProcessError::new(ProcessFailureKind::InvalidConfiguration))?; 484 485 let mut command = Command::new(&request.program); 486 command 487 .args(&request.arguments) 488 .env_clear() 489 .envs(&request.environment.entries) 490 .stdin(Stdio::null()) 491 .stdout(Stdio::piped()) 492 .stderr(Stdio::piped()) 493 .process_group(0); 494 if let Some(current_dir) = &request.current_dir { 495 command.current_dir(current_dir); 496 } 497 498 let mut child = command 499 .spawn() 500 .map_err(|error| ProcessError::from_io(ProcessFailureKind::Spawn, &error))?; 501 let process_group = Pid::from_child(&child); 502 let mut stdout = child.stdout.take(); 503 let mut stderr = child.stderr.take(); 504 let mut stdout_capture = Capture::new(request.stdout_limit); 505 let mut stderr_capture = Capture::new(request.stderr_limit); 506 let mut status = None; 507 508 if stdout.is_none() || stderr.is_none() { 509 let original = ProcessError::new(ProcessFailureKind::PipeSetup); 510 return Err(cleanup_or_original( 511 original, 512 &mut child, 513 process_group, 514 &mut status, 515 &mut stdout, 516 &mut stderr, 517 &mut stdout_capture, 518 &mut stderr_capture, 519 request.termination_grace, 520 )); 521 } 522 if let Err(original) = configure_nonblocking(stdout.as_ref(), stderr.as_ref()) { 523 return Err(cleanup_or_original( 524 original, 525 &mut child, 526 process_group, 527 &mut status, 528 &mut stdout, 529 &mut stderr, 530 &mut stdout_capture, 531 &mut stderr_capture, 532 request.termination_grace, 533 )); 534 } 535 536 let stop = loop { 537 if let Err(error) = drain_streams( 538 &mut stdout, 539 &mut stderr, 540 &mut stdout_capture, 541 &mut stderr_capture, 542 ) { 543 break StopCause::Error(error); 544 } 545 if stdout_capture.exceeded || stderr_capture.exceeded { 546 break StopCause::OutputLimit; 547 } 548 549 match child.try_wait() { 550 Ok(Some(exit_status)) => status = Some(exit_status), 551 Ok(None) => {} 552 Err(error) => { 553 break StopCause::Error(ProcessError::from_io( 554 ProcessFailureKind::Wait, 555 &error, 556 )); 557 } 558 } 559 560 if status.is_some() { 561 match process_group_is_alive(process_group) { 562 Ok(true) => break StopCause::OrphanedDescendants, 563 Ok(false) if stdout.is_none() && stderr.is_none() => { 564 let Some(exit_status) = status else { 565 unreachable!("status was checked above"); 566 }; 567 return Ok(ProcessOutput { 568 status: exit_status, 569 stdout: stdout_capture.bytes, 570 stderr: stderr_capture.bytes, 571 }); 572 } 573 Ok(false) => {} 574 Err(error) => break StopCause::Error(error), 575 } 576 } 577 578 let now = Instant::now(); 579 if now >= deadline { 580 if status.is_some() { 581 break StopCause::RetainedPipes; 582 } 583 break StopCause::Deadline; 584 } 585 let wait = deadline.saturating_duration_since(now).min(POLL_QUANTUM); 586 if let Err(error) = poll_streams(stdout.as_ref(), stderr.as_ref(), wait) { 587 break StopCause::Error(error); 588 } 589 }; 590 591 let original = match stop { 592 StopCause::Deadline => ProcessError::new(ProcessFailureKind::DeadlineExceeded), 593 StopCause::Error(error) => error, 594 StopCause::OrphanedDescendants => { 595 ProcessError::new(ProcessFailureKind::OrphanedDescendants) 596 } 597 StopCause::OutputLimit => ProcessError::output_limit(limit_breach( 598 stdout_capture.exceeded, 599 stderr_capture.exceeded, 600 )), 601 StopCause::RetainedPipes => ProcessError::new(ProcessFailureKind::CleanupIncomplete), 602 }; 603 let mut error = cleanup_or_original( 604 original, 605 &mut child, 606 process_group, 607 &mut status, 608 &mut stdout, 609 &mut stderr, 610 &mut stdout_capture, 611 &mut stderr_capture, 612 request.termination_grace, 613 ); 614 if error.kind == ProcessFailureKind::OutputLimitExceeded { 615 error.output_limit = Some(limit_breach( 616 stdout_capture.exceeded, 617 stderr_capture.exceeded, 618 )); 619 } 620 Err(error) 621 } 622 623 fn configure_nonblocking( 624 stdout: Option<&ChildStdout>, 625 stderr: Option<&ChildStderr>, 626 ) -> Result<(), ProcessError> { 627 let stdout = stdout.ok_or_else(|| ProcessError::new(ProcessFailureKind::PipeSetup))?; 628 let stderr = stderr.ok_or_else(|| ProcessError::new(ProcessFailureKind::PipeSetup))?; 629 set_nonblocking(stdout)?; 630 set_nonblocking(stderr) 631 } 632 633 fn set_nonblocking(fd: impl rustix::fd::AsFd) -> Result<(), ProcessError> { 634 let flags = fcntl_getfl(&fd) 635 .map_err(|error| ProcessError::from_errno(ProcessFailureKind::PipeSetup, error))?; 636 fcntl_setfl(fd, flags | OFlags::NONBLOCK) 637 .map_err(|error| ProcessError::from_errno(ProcessFailureKind::PipeSetup, error)) 638 } 639 640 fn drain_streams( 641 stdout: &mut Option<ChildStdout>, 642 stderr: &mut Option<ChildStderr>, 643 stdout_capture: &mut Capture, 644 stderr_capture: &mut Capture, 645 ) -> Result<(), ProcessError> { 646 drain_stream(stdout, stdout_capture)?; 647 drain_stream(stderr, stderr_capture) 648 } 649 650 fn drain_stream<Reader: Read>( 651 stream: &mut Option<Reader>, 652 capture: &mut Capture, 653 ) -> Result<(), ProcessError> { 654 let Some(reader) = stream.as_mut() else { 655 return Ok(()); 656 }; 657 let mut buffer = [0_u8; 8192]; 658 let mut reached_eof = false; 659 let mut failure = None; 660 let mut bytes_read = 0_usize; 661 loop { 662 match reader.read(&mut buffer) { 663 Ok(0) => { 664 reached_eof = true; 665 break; 666 } 667 Ok(count) => { 668 bytes_read = bytes_read.saturating_add(count); 669 if capture.record(&buffer[..count]) || bytes_read >= MAX_DRAIN_BYTES_PER_PASS { 670 break; 671 } 672 } 673 Err(error) if error.kind() == io::ErrorKind::WouldBlock => break, 674 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, 675 Err(error) => { 676 failure = Some(ProcessError::from_io(ProcessFailureKind::Read, &error)); 677 break; 678 } 679 } 680 } 681 if reached_eof || failure.is_some() { 682 *stream = None; 683 } 684 match failure { 685 Some(error) => Err(error), 686 None => Ok(()), 687 } 688 } 689 690 fn poll_streams( 691 stdout: Option<&ChildStdout>, 692 stderr: Option<&ChildStderr>, 693 timeout: Duration, 694 ) -> Result<(), ProcessError> { 695 let timeout = Timespec::try_from(timeout) 696 .map_err(|_| ProcessError::new(ProcessFailureKind::InvalidConfiguration))?; 697 let result = match (stdout, stderr) { 698 (Some(stdout), Some(stderr)) => { 699 let mut poll_fds = [ 700 PollFd::new(stdout, PollFlags::IN), 701 PollFd::new(stderr, PollFlags::IN), 702 ]; 703 poll(&mut poll_fds, Some(&timeout)) 704 } 705 (Some(stdout), None) => { 706 let mut poll_fds = [PollFd::new(stdout, PollFlags::IN)]; 707 poll(&mut poll_fds, Some(&timeout)) 708 } 709 (None, Some(stderr)) => { 710 let mut poll_fds = [PollFd::new(stderr, PollFlags::IN)]; 711 poll(&mut poll_fds, Some(&timeout)) 712 } 713 (None, None) => { 714 let mut poll_fds = []; 715 poll(&mut poll_fds, Some(&timeout)) 716 } 717 }; 718 match result { 719 Ok(_) | Err(Errno::INTR) => Ok(()), 720 Err(error) => Err(ProcessError::from_errno(ProcessFailureKind::Poll, error)), 721 } 722 } 723 724 fn process_group_is_alive(process_group: Pid) -> Result<bool, ProcessError> { 725 match test_kill_process_group(process_group) { 726 Ok(()) | Err(Errno::PERM) => Ok(true), 727 Err(Errno::SRCH) => Ok(false), 728 Err(error) => Err(ProcessError::from_errno(ProcessFailureKind::Signal, error)), 729 } 730 } 731 732 fn signal_process_group(process_group: Pid, signal: Signal) -> Result<(), ProcessError> { 733 match kill_process_group(process_group, signal) { 734 // macOS may report EPERM while the just-exited leader is being 735 // reaped. Cleanup still proves the group absent below; a group 736 // that remains present is a hard CleanupIncomplete failure. 737 Ok(()) | Err(Errno::SRCH | Errno::PERM) => Ok(()), 738 Err(error) => Err(ProcessError::from_errno(ProcessFailureKind::Signal, error)), 739 } 740 } 741 742 #[allow(clippy::too_many_arguments)] 743 fn cleanup_or_original( 744 original: ProcessError, 745 child: &mut Child, 746 process_group: Pid, 747 status: &mut Option<ExitStatus>, 748 stdout: &mut Option<ChildStdout>, 749 stderr: &mut Option<ChildStderr>, 750 stdout_capture: &mut Capture, 751 stderr_capture: &mut Capture, 752 termination_grace: Duration, 753 ) -> ProcessError { 754 match cleanup( 755 child, 756 process_group, 757 status, 758 stdout, 759 stderr, 760 stdout_capture, 761 stderr_capture, 762 termination_grace, 763 ) { 764 Ok(()) => original, 765 Err(error) => error, 766 } 767 } 768 769 #[allow(clippy::too_many_arguments)] 770 fn cleanup( 771 child: &mut Child, 772 process_group: Pid, 773 status: &mut Option<ExitStatus>, 774 stdout: &mut Option<ChildStdout>, 775 stderr: &mut Option<ChildStderr>, 776 stdout_capture: &mut Capture, 777 stderr_capture: &mut Capture, 778 termination_grace: Duration, 779 ) -> Result<(), ProcessError> { 780 let mut first_error = signal_process_group(process_group, Signal::TERM).err(); 781 let term_deadline = Instant::now() 782 .checked_add(termination_grace) 783 .unwrap_or_else(Instant::now); 784 pump_cleanup_until( 785 term_deadline, 786 child, 787 process_group, 788 status, 789 stdout, 790 stderr, 791 stdout_capture, 792 stderr_capture, 793 &mut first_error, 794 ); 795 796 let group_alive = process_group_is_alive(process_group).unwrap_or(true); 797 if group_alive || status.is_none() { 798 remember_first( 799 &mut first_error, 800 signal_process_group(process_group, Signal::KILL).err(), 801 ); 802 if status.is_none() { 803 let _ = child.kill(); 804 } 805 } 806 let kill_deadline = Instant::now() 807 .checked_add(FORCE_KILL_GRACE) 808 .unwrap_or_else(Instant::now); 809 pump_cleanup_until( 810 kill_deadline, 811 child, 812 process_group, 813 status, 814 stdout, 815 stderr, 816 stdout_capture, 817 stderr_capture, 818 &mut first_error, 819 ); 820 821 let final_group_alive = match process_group_is_alive(process_group) { 822 Ok(alive) => alive, 823 Err(error) => { 824 remember_first(&mut first_error, Some(error)); 825 true 826 } 827 }; 828 if status.is_none() || final_group_alive || stdout.is_some() || stderr.is_some() { 829 *stdout = None; 830 *stderr = None; 831 return Err(ProcessError::new(ProcessFailureKind::CleanupIncomplete)); 832 } 833 match first_error { 834 Some(error) => Err(error), 835 None => Ok(()), 836 } 837 } 838 839 #[allow(clippy::too_many_arguments)] 840 fn pump_cleanup_until( 841 deadline: Instant, 842 child: &mut Child, 843 process_group: Pid, 844 status: &mut Option<ExitStatus>, 845 stdout: &mut Option<ChildStdout>, 846 stderr: &mut Option<ChildStderr>, 847 stdout_capture: &mut Capture, 848 stderr_capture: &mut Capture, 849 first_error: &mut Option<ProcessError>, 850 ) { 851 loop { 852 if let Err(error) = drain_streams(stdout, stderr, stdout_capture, stderr_capture) { 853 remember_first(first_error, Some(error)); 854 } 855 if status.is_none() { 856 match child.try_wait() { 857 Ok(Some(exit_status)) => *status = Some(exit_status), 858 Ok(None) => {} 859 Err(error) => remember_first( 860 first_error, 861 Some(ProcessError::from_io(ProcessFailureKind::Wait, &error)), 862 ), 863 } 864 } 865 let group_alive = match process_group_is_alive(process_group) { 866 Ok(alive) => alive, 867 Err(error) => { 868 remember_first(first_error, Some(error)); 869 true 870 } 871 }; 872 if status.is_some() && !group_alive && stdout.is_none() && stderr.is_none() { 873 return; 874 } 875 876 let now = Instant::now(); 877 if now >= deadline { 878 return; 879 } 880 let wait = deadline.saturating_duration_since(now).min(POLL_QUANTUM); 881 if let Err(error) = poll_streams(stdout.as_ref(), stderr.as_ref(), wait) { 882 remember_first(first_error, Some(error)); 883 thread::sleep(wait); 884 } 885 } 886 } 887 888 fn remember_first(slot: &mut Option<ProcessError>, candidate: Option<ProcessError>) { 889 if slot.is_none() { 890 *slot = candidate; 891 } 892 } 893 894 fn limit_breach(stdout_exceeded: bool, stderr_exceeded: bool) -> OutputLimitBreach { 895 match (stdout_exceeded, stderr_exceeded) { 896 (true, true) => OutputLimitBreach::Both, 897 (true, false) => OutputLimitBreach::Stdout, 898 (false, true) => OutputLimitBreach::Stderr, 899 (false, false) => unreachable!("output stop requires an exceeded stream"), 900 } 901 } 902 903 #[cfg(test)] 904 mod stream_tests { 905 use super::*; 906 use std::collections::VecDeque; 907 908 #[test] 909 fn draining_preserves_interrupted_and_nonblocking_streams_but_closes_failures() { 910 struct ScriptedReader(VecDeque<Result<Vec<u8>, io::ErrorKind>>); 911 impl Read for ScriptedReader { 912 fn read(&mut self, output: &mut [u8]) -> io::Result<usize> { 913 match self.0.pop_front().unwrap_or(Ok(Vec::new())) { 914 Ok(bytes) => { 915 output[..bytes.len()].copy_from_slice(&bytes); 916 Ok(bytes.len()) 917 } 918 Err(kind) => Err(io::Error::new(kind, "fixture read failure")), 919 } 920 } 921 } 922 let mut stream = Some(ScriptedReader(VecDeque::from([ 923 Err(io::ErrorKind::Interrupted), 924 Ok(b"abc".to_vec()), 925 Err(io::ErrorKind::WouldBlock), 926 Err(io::ErrorKind::BrokenPipe), 927 ]))); 928 let mut capture = Capture::new(8); 929 drain_stream(&mut stream, &mut capture).unwrap(); 930 assert!(stream.is_some()); 931 assert_eq!(capture.bytes, b"abc"); 932 assert_eq!( 933 drain_stream(&mut stream, &mut capture).unwrap_err().kind(), 934 ProcessFailureKind::Read 935 ); 936 assert!(stream.is_none()); 937 drain_stream(&mut stream, &mut capture).unwrap(); 938 assert_eq!(capture.bytes, b"abc"); 939 } 940 941 #[test] 942 fn draining_has_a_fairness_budget_and_retains_only_the_configured_prefix() { 943 let mut stream = Some(io::Cursor::new(vec![b'x'; MAX_DRAIN_BYTES_PER_PASS + 1])); 944 let mut capture = Capture::new(MAX_DRAIN_BYTES_PER_PASS * 2); 945 drain_stream(&mut stream, &mut capture).unwrap(); 946 assert_eq!(capture.bytes.len(), MAX_DRAIN_BYTES_PER_PASS); 947 assert!(stream.is_some()); 948 drain_stream(&mut stream, &mut capture).unwrap(); 949 assert!(stream.is_none()); 950 assert_eq!(capture.bytes.len(), MAX_DRAIN_BYTES_PER_PASS + 1); 951 let mut stream = Some(io::Cursor::new(b"abcdef")); 952 let mut capture = Capture::new(3); 953 drain_stream(&mut stream, &mut capture).unwrap(); 954 assert!(capture.exceeded); 955 assert_eq!(capture.bytes, b"abc"); 956 drain_stream(&mut stream, &mut capture).unwrap(); 957 assert!(stream.is_none()); 958 assert_eq!(capture.bytes, b"abc"); 959 } 960 961 #[test] 962 fn cleanup_keeps_the_original_error_when_later_cleanup_also_fails() { 963 let original = ProcessError::new(ProcessFailureKind::Read); 964 let mut first = None; 965 remember_first(&mut first, None); 966 assert!(first.is_none()); 967 remember_first(&mut first, Some(original)); 968 remember_first( 969 &mut first, 970 Some(ProcessError::new(ProcessFailureKind::InvalidConfiguration)), 971 ); 972 assert_eq!(first.unwrap().kind(), ProcessFailureKind::Read); 973 } 974 } 975 } 976 977 pub(crate) fn self_test() -> Result<(), String> { 978 #[cfg(unix)] 979 { 980 self_test_suite()?; 981 println!("bounded process self-test: ok"); 982 Ok(()) 983 } 984 #[cfg(not(unix))] 985 { 986 Err(ProcessError::new(ProcessFailureKind::UnsupportedPlatform).to_string()) 987 } 988 } 989 990 #[cfg(unix)] 991 fn self_test_suite() -> Result<(), String> { 992 verify_replacement_environment()?; 993 verify_closed_stdin()?; 994 verify_environment_rejections()?; 995 verify_deadline_tree_kill()?; 996 verify_orphan_kill()?; 997 verify_dual_output_caps()?; 998 verify_continuous_output_is_bounded()?; 999 verify_redaction()?; 1000 Ok(()) 1001 } 1002 1003 #[cfg(unix)] 1004 fn verify_replacement_environment() -> Result<(), String> { 1005 let mut environment = ReplacementEnvironment::default(); 1006 environment 1007 .insert("RSHR_VISIBLE", "replacement-only") 1008 .map_err(|error| error.to_string())?; 1009 let output = run(&ProcessRequest::new("/usr/bin/env") 1010 .environment(environment) 1011 .deadline(Duration::from_secs(2)) 1012 .output_limits(4096, 4096)) 1013 .map_err(|error| error.to_string())?; 1014 if !output.status().success() 1015 || output.stdout() != b"RSHR_VISIBLE=replacement-only\n" 1016 || !output.stderr().is_empty() 1017 { 1018 return Err("replacement environment self-test failed".to_owned()); 1019 } 1020 Ok(()) 1021 } 1022 1023 #[cfg(unix)] 1024 fn verify_closed_stdin() -> Result<(), String> { 1025 let working_directory = 1026 tempfile::tempdir().map_err(|_| "create self-test directory".to_owned())?; 1027 let output = run(&ProcessRequest::new("/bin/sh") 1028 .arg("-c") 1029 .arg("if IFS= read -r value; then exit 92; fi; printf 'stdin-closed'") 1030 .current_dir(working_directory.path()) 1031 .deadline(Duration::from_secs(2))) 1032 .map_err(|error| error.to_string())?; 1033 if !output.status().success() || output.stdout() != b"stdin-closed" { 1034 return Err("closed stdin self-test failed".to_owned()); 1035 } 1036 Ok(()) 1037 } 1038 1039 #[cfg(unix)] 1040 fn verify_environment_rejections() -> Result<(), String> { 1041 for name in FORBIDDEN_ENVIRONMENT_NAMES { 1042 expect_environment_rejection(name, EnvironmentRejection::ForbiddenControl)?; 1043 } 1044 for name in [ 1045 "CARGO_BUILD_RUSTFLAGS", 1046 "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS", 1047 "DYLD_FRAMEWORK_PATH", 1048 "LD_AUDIT", 1049 "LD_DEBUG", 1050 "LD_PROFILE", 1051 "RUSTC_WRAPPER", 1052 "RUSTC_WORKSPACE_WRAPPER", 1053 "RUSTDOCFLAGS", 1054 ] { 1055 expect_environment_rejection(name, EnvironmentRejection::ForbiddenControl)?; 1056 } 1057 for name in [ 1058 "CREDENTIAL", 1059 "APIKEY", 1060 "PASSWORD", 1061 "SECRET", 1062 "TOKEN", 1063 "RSHR_CREDENTIAL_FILE", 1064 "RSHR_KEY_ID", 1065 "RSHR_PASSWORD_FILE", 1066 "RSHR_SECRET_VALUE", 1067 "RSHR_TOKEN_FILE", 1068 ] { 1069 expect_environment_rejection(name, EnvironmentRejection::SensitiveName)?; 1070 } 1071 Ok(()) 1072 } 1073 1074 #[cfg(unix)] 1075 fn expect_environment_rejection(name: &str, expected: EnvironmentRejection) -> Result<(), String> { 1076 let mut environment = ReplacementEnvironment::default(); 1077 let error = environment 1078 .insert(name, "redacted-value") 1079 .expect_err("forbidden environment entry must fail closed"); 1080 if error.kind() != ProcessFailureKind::EnvironmentRejected(expected) { 1081 return Err("environment rejection self-test failed".to_owned()); 1082 } 1083 Ok(()) 1084 } 1085 1086 #[cfg(unix)] 1087 fn verify_deadline_tree_kill() -> Result<(), String> { 1088 let pid_file = 1089 tempfile::NamedTempFile::new().map_err(|_| "create self-test pid file".to_owned())?; 1090 let request = ProcessRequest::new("/bin/sh") 1091 .arg("-c") 1092 .arg( 1093 "trap 'kill \"$child\" 2>/dev/null; wait \"$child\" 2>/dev/null; exit 143' TERM; \ 1094 /bin/sleep 30 & child=$!; printf '%s\n' \"$child\" > \"$1\"; wait \"$child\"", 1095 ) 1096 .arg("rshr-bounded-process") 1097 .arg(pid_file.path()) 1098 .deadline(Duration::from_millis(750)) 1099 .termination_grace(Duration::from_millis(100)); 1100 let error = run(&request).expect_err("deadline self-test must time out"); 1101 if error.kind() != ProcessFailureKind::DeadlineExceeded { 1102 return Err("deadline classification self-test failed".to_owned()); 1103 } 1104 verify_recorded_process_gone(pid_file.path()) 1105 } 1106 1107 #[cfg(unix)] 1108 fn verify_orphan_kill() -> Result<(), String> { 1109 let pid_file = 1110 tempfile::NamedTempFile::new().map_err(|_| "create self-test pid file".to_owned())?; 1111 let request = ProcessRequest::new("/bin/sh") 1112 .arg("-c") 1113 .arg("/bin/sleep 30 & child=$!; printf '%s\n' \"$child\" > \"$1\"; exit 0") 1114 .arg("rshr-bounded-process") 1115 .arg(pid_file.path()) 1116 .deadline(Duration::from_secs(2)) 1117 .termination_grace(Duration::from_millis(100)); 1118 let error = run(&request).expect_err("orphan self-test must fail closed"); 1119 if error.kind() != ProcessFailureKind::OrphanedDescendants { 1120 return Err("orphan classification self-test failed".to_owned()); 1121 } 1122 verify_recorded_process_gone(pid_file.path()) 1123 } 1124 1125 #[cfg(unix)] 1126 fn verify_recorded_process_gone(path: &std::path::Path) -> Result<(), String> { 1127 use rustix::io::Errno; 1128 use rustix::process::{Pid, test_kill_process}; 1129 use std::fs; 1130 use std::thread; 1131 use std::time::Instant; 1132 1133 let raw = fs::read_to_string(path).map_err(|_| "read self-test pid file".to_owned())?; 1134 let pid_number = raw 1135 .trim() 1136 .parse::<i32>() 1137 .map_err(|_| "parse self-test pid".to_owned())?; 1138 let pid = 1139 Pid::from_raw(pid_number).ok_or_else(|| "self-test pid must be positive".to_owned())?; 1140 let deadline = Instant::now() 1141 .checked_add(Duration::from_secs(2)) 1142 .ok_or_else(|| "self-test pid deadline overflow".to_owned())?; 1143 loop { 1144 match test_kill_process(pid) { 1145 Err(Errno::SRCH) => return Ok(()), 1146 Ok(()) | Err(Errno::PERM) if Instant::now() < deadline => { 1147 thread::sleep(Duration::from_millis(10)); 1148 } 1149 _ => return Err("bounded process descendant survived cleanup".to_owned()), 1150 } 1151 } 1152 } 1153 1154 #[cfg(unix)] 1155 fn verify_dual_output_caps() -> Result<(), String> { 1156 let stdout_error = run(&ProcessRequest::new("/bin/sh") 1157 .arg("-c") 1158 .arg("printf 'stdout-over-cap'") 1159 .deadline(Duration::from_secs(2)) 1160 .output_limits(4, 4096)) 1161 .expect_err("stdout cap self-test must fail closed"); 1162 if stdout_error.output_limit_breach() != Some(OutputLimitBreach::Stdout) { 1163 return Err(format!("stdout cap self-test failed: {stdout_error:?}")); 1164 } 1165 1166 let stderr_error = run(&ProcessRequest::new("/bin/sh") 1167 .arg("-c") 1168 .arg("printf 'stderr-over-cap' >&2") 1169 .deadline(Duration::from_secs(2)) 1170 .output_limits(4096, 4)) 1171 .expect_err("stderr cap self-test must fail closed"); 1172 if stderr_error.output_limit_breach() != Some(OutputLimitBreach::Stderr) { 1173 return Err(format!("stderr cap self-test failed: {stderr_error:?}")); 1174 } 1175 Ok(()) 1176 } 1177 1178 #[cfg(unix)] 1179 fn verify_continuous_output_is_bounded() -> Result<(), String> { 1180 let error = run(&ProcessRequest::new("/bin/sh") 1181 .arg("-c") 1182 .arg("while :; do printf 'continuous-output'; done") 1183 .deadline(Duration::from_secs(2)) 1184 .output_limits(4096, 4096) 1185 .termination_grace(Duration::from_millis(100))) 1186 .expect_err("continuous output must hit its live cap"); 1187 if error.output_limit_breach() != Some(OutputLimitBreach::Stdout) { 1188 return Err("continuous output cap self-test failed".to_owned()); 1189 } 1190 Ok(()) 1191 } 1192 1193 #[cfg(unix)] 1194 fn verify_redaction() -> Result<(), String> { 1195 const ARGUMENT_MARKER: &str = "argument-marker-must-not-leak"; 1196 const CURRENT_DIR_MARKER: &str = "/cwd-marker-must-not-leak"; 1197 const ENVIRONMENT_MARKER: &str = "environment-marker-must-not-leak"; 1198 const PROGRAM_MARKER: &str = "/program-marker-must-not-leak"; 1199 1200 let mut environment = ReplacementEnvironment::default(); 1201 environment 1202 .insert("RSHR_VISIBLE", ENVIRONMENT_MARKER) 1203 .map_err(|error| error.to_string())?; 1204 let diagnostics_request = ProcessRequest::new(PROGRAM_MARKER) 1205 .arg(ARGUMENT_MARKER) 1206 .current_dir(CURRENT_DIR_MARKER) 1207 .environment(environment.clone()); 1208 let request_debug = format!("{diagnostics_request:?}"); 1209 if [ 1210 ARGUMENT_MARKER, 1211 CURRENT_DIR_MARKER, 1212 ENVIRONMENT_MARKER, 1213 PROGRAM_MARKER, 1214 ] 1215 .iter() 1216 .any(|marker| request_debug.contains(marker)) 1217 { 1218 return Err("request redaction self-test failed".to_owned()); 1219 } 1220 let spawn_error = run(&diagnostics_request) 1221 .expect_err("missing redaction fixture program must fail to spawn"); 1222 let spawn_debug = format!("{spawn_error:?}"); 1223 let spawn_display = spawn_error.to_string(); 1224 if [ 1225 ARGUMENT_MARKER, 1226 CURRENT_DIR_MARKER, 1227 ENVIRONMENT_MARKER, 1228 PROGRAM_MARKER, 1229 ] 1230 .iter() 1231 .any(|marker| spawn_debug.contains(marker) || spawn_display.contains(marker)) 1232 { 1233 return Err("spawn error redaction self-test failed".to_owned()); 1234 } 1235 1236 let request = ProcessRequest::new("/bin/sh") 1237 .arg("-c") 1238 .arg("printf '%s' \"$RSHR_VISIBLE\"; printf '%s' \"$1\" >&2") 1239 .arg("rshr-bounded-process") 1240 .arg(ARGUMENT_MARKER) 1241 .environment(environment) 1242 .deadline(Duration::from_secs(2)); 1243 1244 let output = run(&request).map_err(|error| error.to_string())?; 1245 if output.stdout() != ENVIRONMENT_MARKER.as_bytes() 1246 || output.stderr() != ARGUMENT_MARKER.as_bytes() 1247 { 1248 return Err("redaction fixture output self-test failed".to_owned()); 1249 } 1250 let output_debug = format!("{output:?}"); 1251 if output_debug.contains(ARGUMENT_MARKER) || output_debug.contains(ENVIRONMENT_MARKER) { 1252 return Err("output redaction self-test failed".to_owned()); 1253 } 1254 1255 let capped = run(&ProcessRequest::new("/bin/sh") 1256 .arg("-c") 1257 .arg(format!("printf '{ARGUMENT_MARKER}'")) 1258 .output_limits(1, 1)) 1259 .expect_err("redaction cap fixture must fail closed"); 1260 let error_debug = format!("{capped:?}"); 1261 let error_display = capped.to_string(); 1262 if error_debug.contains(ARGUMENT_MARKER) || error_display.contains(ARGUMENT_MARKER) { 1263 return Err("error redaction self-test failed".to_owned()); 1264 } 1265 Ok(()) 1266 } 1267 1268 #[cfg(all(test, unix))] 1269 mod tests { 1270 use super::*; 1271 1272 #[test] 1273 fn replacement_environment_is_exact_and_stdin_is_closed() { 1274 verify_replacement_environment().expect("replacement environment must be exact"); 1275 verify_closed_stdin().expect("stdin must be closed"); 1276 } 1277 1278 #[test] 1279 fn build_loader_and_sensitive_environment_controls_are_rejected() { 1280 verify_environment_rejections().expect("environment controls must fail closed"); 1281 } 1282 1283 #[test] 1284 fn deadline_kills_the_process_tree() { 1285 verify_deadline_tree_kill().expect("deadline must kill the process group"); 1286 } 1287 1288 #[test] 1289 fn leader_exit_kills_orphaned_descendants() { 1290 verify_orphan_kill().expect("orphaned process group must be killed"); 1291 } 1292 1293 #[test] 1294 fn stdout_and_stderr_have_independent_caps() { 1295 verify_dual_output_caps().expect("both output streams must be capped"); 1296 verify_continuous_output_is_bounded().expect("continuous output must remain bounded"); 1297 } 1298 1299 #[test] 1300 fn request_output_and_error_diagnostics_are_redacted() { 1301 verify_redaction().expect("bounded process diagnostics must be redacted"); 1302 } 1303 1304 #[test] 1305 fn environment_boundaries_fail_closed() { 1306 use std::os::unix::ffi::OsStringExt; 1307 1308 for name in [ 1309 "".to_owned(), 1310 "A".repeat(MAX_ENVIRONMENT_NAME_BYTES + 1), 1311 "A\0B".to_owned(), 1312 "É".to_owned(), 1313 "1INVALID".to_owned(), 1314 "BAD-NAME".to_owned(), 1315 ] { 1316 assert_eq!( 1317 environment_rejection(&name), 1318 Some(EnvironmentRejection::InvalidNameOrNul) 1319 ); 1320 } 1321 assert_eq!( 1322 environment_rejection("CARGO_TARGET_FIXTURE_RUSTFLAGS"), 1323 Some(EnvironmentRejection::ForbiddenControl) 1324 ); 1325 assert_eq!(environment_rejection("CARGO_TARGET_FIXTURE"), None); 1326 assert_eq!(environment_rejection("_FIXTURE_1"), None); 1327 1328 let mut environment = ReplacementEnvironment::default(); 1329 environment 1330 .insert("RSHR_VISIBLE", "first") 1331 .expect("first entry must be accepted"); 1332 let duplicate = environment 1333 .insert("RSHR_VISIBLE", "second") 1334 .expect_err("duplicate must fail"); 1335 assert_eq!( 1336 duplicate.kind(), 1337 ProcessFailureKind::EnvironmentRejected(EnvironmentRejection::DuplicateName) 1338 ); 1339 1340 let mut nul_environment = ReplacementEnvironment::default(); 1341 let nul = nul_environment 1342 .insert("RSHR_VISIBLE", OsString::from_vec(b"value\0tail".to_vec())) 1343 .expect_err("NUL value must fail"); 1344 assert_eq!( 1345 nul.kind(), 1346 ProcessFailureKind::EnvironmentRejected(EnvironmentRejection::InvalidNameOrNul) 1347 ); 1348 1349 let mut full_environment = ReplacementEnvironment::default(); 1350 for index in 0..MAX_ENVIRONMENT_ENTRIES { 1351 full_environment 1352 .insert(format!("RSHR_ENTRY_{index}"), "value") 1353 .expect("entry within cardinality bound must pass"); 1354 } 1355 let too_many = full_environment 1356 .insert("RSHR_ENTRY_OVER_LIMIT", "value") 1357 .expect_err("entry above cardinality bound must fail"); 1358 assert_eq!( 1359 too_many.kind(), 1360 ProcessFailureKind::EnvironmentRejected(EnvironmentRejection::TooManyEntries) 1361 ); 1362 1363 let mut large_environment = ReplacementEnvironment::default(); 1364 let too_large = large_environment 1365 .insert( 1366 "RSHR_VISIBLE", 1367 OsString::from_vec(vec![b'x'; MAX_ENVIRONMENT_VALUE_BYTES + 1]), 1368 ) 1369 .expect_err("value above byte bound must fail"); 1370 assert_eq!( 1371 too_large.kind(), 1372 ProcessFailureKind::EnvironmentRejected(EnvironmentRejection::ValueTooLarge) 1373 ); 1374 } 1375 1376 #[test] 1377 fn hard_configuration_maximums_fail_before_spawn() { 1378 for request in [ 1379 ProcessRequest::new(""), 1380 ProcessRequest::new("/usr/bin/true").deadline(Duration::ZERO), 1381 ProcessRequest::new("/usr/bin/true").deadline(MAX_DEADLINE + Duration::from_secs(1)), 1382 ProcessRequest::new("/usr/bin/true") 1383 .output_limits(MAX_STREAM_LIMIT + 1, MAX_STREAM_LIMIT), 1384 ProcessRequest::new("/usr/bin/true") 1385 .output_limits(MAX_STREAM_LIMIT, MAX_STREAM_LIMIT + 1), 1386 ProcessRequest::new("/usr/bin/true") 1387 .termination_grace(MAX_TERMINATION_GRACE + Duration::from_millis(1)), 1388 ] { 1389 let error = run(&request).expect_err("configuration above hard maximum must fail"); 1390 assert_eq!(error.kind(), ProcessFailureKind::InvalidConfiguration); 1391 } 1392 } 1393 1394 #[test] 1395 fn exact_and_zero_stream_caps_have_byte_precise_semantics() { 1396 let exact = run(&ProcessRequest::new("/bin/sh") 1397 .arg("-c") 1398 .arg("printf '1234'; printf 'abcd' >&2") 1399 .output_limits(4, 4)) 1400 .expect("exact cap must pass"); 1401 assert_eq!(exact.stdout(), b"1234"); 1402 assert_eq!(exact.stderr(), b"abcd"); 1403 1404 let empty = run(&ProcessRequest::new("/usr/bin/true") 1405 .output_limits(0, 0) 1406 .deadline(Duration::from_secs(2))) 1407 .expect("empty streams must fit zero caps"); 1408 assert!(empty.stdout().is_empty()); 1409 assert!(empty.stderr().is_empty()); 1410 1411 let over_zero = run(&ProcessRequest::new("/bin/sh") 1412 .arg("-c") 1413 .arg("printf x") 1414 .output_limits(0, 0)) 1415 .expect_err("first byte above zero cap must fail"); 1416 assert_eq!( 1417 over_zero.output_limit_breach(), 1418 Some(OutputLimitBreach::Stdout) 1419 ); 1420 } 1421 }