char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f4a0f5b
main
1use crate :: scope:: Escalation ; 2use crate ::{ self as sys, Child , Scope }; 3use avril_caps::{ Capabilities , PathTarget , env_name}; 4use serde::{ Deserialize , Serialize }; 5use std:: collections::{ BTreeMap , BTreeSet }; 6use std:: ffi::{ OsStr , OsString }; 7use std:: io; 8use std:: path::{ Path , PathBuf }; 9use std:: pin:: Pin ; 10use std:: process:: ExitStatus ; 11use std:: sync:: atomic::{ AtomicUsize , Ordering }; 12use std:: sync::{ Arc , Condvar , Mutex }; 13use std:: time::{ Duration , Instant }; 14use tokio:: io::{ AsyncRead , AsyncReadExt , AsyncWrite , AsyncWriteExt }; 15use tokio:: runtime:: Handle ; 16use tokio:: sync::{ Mutex as AsyncMutex , Notify }; 17use tokio_util:: sync:: CancellationToken ; 18 19type Reader =Pin < Box < dyn AsyncRead +Send >>; 20type Writer =Pin < Box < dyn AsyncWrite +Send >>; 21const CHUNK : usize =64 * 1024 ; 22fn invalid ( message : & str ) -> io:: Error { 23 io:: Error :: new ( io:: ErrorKind :: InvalidInput , message) 24} 25fn missing () -> io:: Error { 26 io:: Error :: new ( io:: ErrorKind :: NotConnected , "Subprocess handle is closed" ) 27} 28fn native_env_name ( name : & OsStr ) ->OsString { 29if cfg! ( windows) { 30 name. to_ascii_uppercase () 31} else { 32 name. to_owned () 33} 34} 35 36# [ derive ( Default , Deserialize )] 37# [ serde ( rename_all = "camelCase" , deny_unknown_fields )] 38pub struct Options { 39pub command : String , 40pub args : Vec < String >, 41pub cwd : Option < PathBuf >, 42pub search_path : bool , 43pub clear_env : bool , 44pub env : Vec <( String , String )>, 45pub stdin : StdioSpec , 46pub stdout : StdioSpec , 47pub stderr : StdioSpec , 48pub extra_stdio : Vec <( u32 , StdioSpec )>, 49pub terminal : Option < TerminalSize >, 50pub timeout_ms : Option < u64 >, 51/// Caps bytes produced on outputs collected natively, which only runSync 52/// does; JS enforces the collected limit everywhere else. 53pub max_collected_bytes : Option < u64 >, 54pub max_buffered_bytes : u64 , 55pub termination : Termination , 56pub drain_timeout_ms : u64 , 57/// Outputs whose reader keeps up deliberately: at the unread limit the 58/// pump stops reading, so the child blocks instead of being terminated. 59# [ serde ( default )] 60pub backpressure : Vec < u32 >, 61} 62impl Options { 63pub fn new ( command : impl Into < String >, args : Vec < String >) ->Self { 64Self { 65command : command. into (), 66 args, 67search_path : true , 68max_buffered_bytes : 1024 * 1024 , 69drain_timeout_ms : 1000 , 70termination : Termination { 71scope : "group" . into (), 72signal : "SIGTERM" . into (), 73grace_ms : 1000 , 74}, 75 ..Self :: default () 76} 77} 78fn stdio ( & self ) ->impl Iterator < Item =( u32 , & StdioSpec , bool )> { 79let standard =[ 80( 0 , & self . stdin , true ), 81( 1 , & self . stdout , false ), 82( 2 , & self . stderr , false ), 83]; 84let extra =self . extra_stdio . iter (). map ( |( fd, spec) |{ 85( 86* fd, 87 spec, 88 spec. direction . as_deref () ==Some ( "parent-to-child" ), 89) 90}); 91 standard. into_iter (). chain ( extra) 92} 93} 94# [ derive ( Deserialize )] 95# [ serde ( rename_all = "camelCase" , deny_unknown_fields )] 96pub struct StdioSpec { 97pub mode : String , 98pub file : Option < String >, 99pub file_mode : Option < String >, 100pub handle : Option < u64 >, 101pub direction : Option < String >, 102} 103impl Default for StdioSpec { 104fn default () ->Self { 105Self { 106mode : "inherit" . into (), 107file : None , 108file_mode : None , 109handle : None , 110direction : None , 111} 112} 113} 114# [ derive ( Deserialize )] 115pub struct TerminalSize { 116pub columns : u16 , 117pub rows : u16 , 118} 119# [ derive ( Default , Deserialize )] 120# [ serde ( rename_all = "camelCase" , deny_unknown_fields )] 121pub struct Termination { 122pub scope : String , 123pub signal : String , 124pub grace_ms : u64 , 125} 126 127enum StdioPlan < ' a > { 128Inherit , 129Null , 130Pipe , 131Stdout , 132File ( &' a StdioSpec ), 133Handle ( &' a StdioSpec ), 134} 135 136enum TerminationScope { 137Child , 138Group , 139} 140 141struct SpawnPlan < ' a > { 142stdio : Vec <( u32 , StdioPlan < ' a >, bool )>, 143scope : TerminationScope , 144terminal : Option < &' a TerminalSize >, 145} 146# [ derive ( Clone , Serialize )] 147pub struct Status { 148pub code : Option < i32 >, 149pub signal : Option < String >, 150pub success : bool , 151pub reason : Option < String >, 152pub error : Option < String >, 153} 154# [ derive ( Default )] 155struct Queue { 156bytes : bytes:: BytesMut , 157closed : bool , 158} 159# [ derive ( Default )] 160pub struct Output { 161queue : Mutex < Queue >, 162notify : Notify , 163backpressure : bool , 164/// Signals the pump when a backpressured reader frees queue space. 165space : Notify , 166} 167impl Output { 168fn new ( backpressure : bool ) ->Self { 169Self { backpressure, ..Self :: default () } 170} 171pub async fn read ( & self ) ->Option < bytes:: Bytes > { 172loop { 173let wake =self . notify . notified (); 174 tokio:: pin!( wake); 175 wake. as_mut (). enable (); 176{ 177let mut queue =self . queue . lock (). unwrap (); 178if !queue. bytes . is_empty () { 179let count = queue. bytes . len (). min ( CHUNK ); 180let bytes = queue. bytes . split_to ( count). freeze (); 181self . space . notify_one (); 182return Some ( bytes); 183} 184if queue. closed { 185return None ; 186} 187} 188 wake. await ; 189} 190} 191pub fn cancel ( & self ) { 192let mut queue =self . queue . lock (). unwrap (); 193 queue. closed =true ; 194 queue. bytes = bytes:: BytesMut :: new (); 195self . notify . notify_waiters (); 196self . space . notify_one (); 197} 198} 199pub struct Input { 200writer : AsyncMutex < Option < Writer >>, 201stop : CancellationToken , 202} 203impl Input { 204pub async fn write ( & self , bytes : Vec < u8 >) -> io:: Result <()> { 205 tokio:: select!{ 206 biased; 207 _ =self . stop . cancelled () =>Err ( io:: Error :: new ( io:: ErrorKind :: BrokenPipe , "Subprocess input is closed" )), 208 result =async { self . writer . lock (). await . as_mut (). ok_or_else ( missing) ?. write_all ( & bytes). await } => result, 209} 210} 211pub async fn close ( & self ) { 212self . stop . cancel (); 213self . writer . lock (). await . take (); 214} 215} 216struct Core { 217child : Option < Child >, 218exit : Option < ExitStatus >, 219completed : bool , 220reason : Option < String >, 221error : Option < String >, 222stop : Escalation , 223epoch : u32 , 224produced : u64 , 225} 226impl Core { 227fn observe ( & mut self ) -> io:: Result < bool > { 228if self . exit . is_some () { 229return Ok ( false ); 230} 231self . exit =self . child . as_ref (). map_or ( Ok ( None ), Child :: observe_exit) ?; 232Ok ( self . exit . is_some ()) 233} 234fn report ( & mut self , reason : & str , error : Option < String >) { 235if self . reason . is_none () { 236self . reason =Some ( reason. into ()); 237self . error = error; 238} 239} 240} 241pub struct Process { 242pub pid : u32 , 243group : bool , 244drain : Duration , 245timeout : Option < Duration >, 246limit : Option < u64 >, 247pending_limit : u64 , 248core : Mutex < Core >, 249done : Condvar , 250changed : Notify , 251wake : Notify , 252pipe_stop : CancellationToken , 253pub outputs : BTreeMap < u32 , Arc < Output >>, 254pub inputs : BTreeMap < u32 , Arc < Input >>, 255pumps : AtomicUsize , 256/// Pumps waiting for a backpressured reader to free space. 257parked : AtomicUsize , 258owners : Vec < CancellationToken >, 259window : Option < sys:: Window >, 260} 261impl Process { 262pub fn wait_blocking ( & self , timeout : Duration ) { 263let core =self . core . lock (). unwrap (); 264let _ =self 265. done 266. wait_timeout_while ( core, timeout, |core| !core. completed ); 267} 268pub fn invalidate_waiters ( & self ) ->u32 { 269let mut core =self . core . lock (). unwrap (); 270 core. epoch = core. epoch . wrapping_add ( 1 ); 271self . changed . notify_waiters (); 272 core. epoch 273} 274pub fn resize ( & self , columns : u16 , rows : u16 ) -> io:: Result <()> { 275 sys:: resize ( 276self . window 277. as_ref () 278. ok_or_else ( ||invalid ( "Subprocess has no terminal" )) ?, 279 columns, 280 rows, 281) 282} 283 284fn status ( core : & Core ) ->Option < Status > { 285let exit = core. exit ?; 286Some ( Status { 287code : exit. code (), 288signal : sys:: exit_signal ( & exit), 289success : exit. success () && core. reason . is_none (), 290reason : core. reason . clone (), 291error : core. error . clone (), 292}) 293} 294pub fn request ( & self , reason : & str , error : Option < String >, force : bool ) { 295let mut core =self . core . lock (). unwrap (); 296if core. completed { 297return ; 298} 299// Supervision polls for owner shutdown, so repeat requests must not 300// re-notify and spin the supervisor into starving pipe tasks. 301 core. report ( reason, error); 302let changed = core. stop . request ( force); 303if changed{ 304self . wake . notify_one (); 305} 306} 307/// Synchronously SIGKILLs the termination scope. Used on the dying path 308/// so grace-resisting descendants cannot outlive the owner. 309pub fn kill_scope ( & self ) { 310let mut core =self . core . lock (). unwrap (); 311let _ =self . signal_locked ( & mut core, "SIGKILL" ); 312} 313fn signal_locked ( & self , core : & mut Core , signal : & str ) -> io:: Result < bool > { 314match & core. child { 315Some ( child) => child. signal ( self . group , signal), 316None =>Ok ( false ), 317} 318} 319pub fn kill ( & self , signal : & str ) -> io:: Result < bool > { 320let mut core =self . core . lock (). unwrap (); 321if core. exit . is_some () || core. completed { 322return Ok ( false ); 323} 324if core. observe () ?{ 325self . changed . notify_waiters (); 326return Ok ( false ); 327} 328self . signal_locked ( & mut core, signal) 329} 330pub async fn wait ( & self , completed : bool , epoch : Option < u32 >) -> io:: Result < Option < Status >> { 331loop { 332let wake =self . changed . notified (); 333 tokio:: pin!( wake); 334 wake. as_mut (). enable (); 335{ 336let core =self . core . lock (). unwrap (); 337if epoch. is_some_and ( |epoch| epoch != core. epoch ) { 338return Ok ( None ); 339} 340if ( !completed && core. exit . is_some ()) || core. completed { 341return Ok ( Self :: status ( & core)); 342} 343} 344 wake. await ; 345} 346} 347/// Waits while a backpressured output is full; false once pipes stop. 348async fn room ( & self , output : & Output ) ->bool { 349loop { 350let space = output. space . notified (); 351 tokio:: pin!( space); 352 space. as_mut (). enable (); 353{ 354let queue = output. queue . lock (). unwrap (); 355if queue. closed ||( queue. bytes . len () as u64 ) <self . pending_limit { 356return true ; 357} 358} 359self . parked . fetch_add ( 1 , Ordering :: AcqRel ); 360self . wake . notify_one (); 361let stopped = tokio:: select!{ 362 biased; 363 _ =self . pipe_stop . cancelled () =>true , 364 _ = space =>false , 365}; 366self . parked . fetch_sub ( 1 , Ordering :: AcqRel ); 367self . wake . notify_one (); 368if stopped{ 369return false ; 370} 371} 372} 373async fn pump ( self : Arc < Self >, output : Arc < Output >, mut reader : Reader ) { 374let mut bytes =vec! [ 0 ; CHUNK ]; 375loop { 376if output. backpressure && !self . room ( & output). await { 377break ; 378} 379let count = tokio:: select!{ biased; _ =self . pipe_stop . cancelled () =>break , 380 result = reader. read ( & mut bytes) =>match result{ Ok ( 0 ) =>break , Ok ( count) => count, 381Err ( error) =>{ self . request ( "io-error" , Some ( error. to_string ()), false ); break ; } } }; 382let overflow; 383{ 384let mut core =self . core . lock (). unwrap (); 385let mut queue = output. queue . lock (). unwrap (); 386if core. reason . as_deref () ==Some ( "output-limit" ) { 387continue ; 388} 389let allowed =self 390. limit 391. map_or ( countas u64 , |limit| limit. saturating_sub ( core. produced )) 392. min ( countas u64 ); 393 core. produced = core. produced . saturating_add ( countas u64 ); 394let allowed =if queue. closed || output. backpressure { 395 allowed 396} else { 397 allowed. min ( self . pending_limit . saturating_sub ( queue. bytes . len () as u64 )) 398}; 399 overflow = allowed < countas u64 ; 400if !queue. closed && allowed >0 { 401 queue. bytes . extend_from_slice ( & bytes[ ..allowedas usize ]); 402} 403} 404 output. notify . notify_waiters (); 405if overflow{ 406self . request ( 407"output-limit" , 408Some ( "Subprocess output exceeded its configured limit" . into ()), 409false , 410); 411} 412} 413 output. queue . lock (). unwrap (). closed =true ; 414 output. notify . notify_waiters (); 415self . pumps . fetch_sub ( 1 , Ordering :: AcqRel ); 416self . wake . notify_one (); 417} 418async fn supervise ( self : Arc < Self >) { 419let started =Instant :: now (); 420let mut drain_from =None ; 421loop { 422if self . owners . iter (). any ( CancellationToken :: is_cancelled) { 423self . request ( "owner" , None , true ); 424} 425{ 426let mut core =self . core . lock (). unwrap (); 427match core. observe () { 428Ok ( true ) =>self . changed . notify_waiters (), 429Ok ( false ) =>(), 430Err ( error) if error. kind () == io:: ErrorKind :: Interrupted =>(), 431Err ( error) =>{ 432// waitid only fails persistently when the child is already 433// gone (ECHILD) or the arguments are invalid; either way no 434// exit will ever be observed, so drain pipes and finish 435// instead of spinning forever. 436 core. report ( "io-error" , Some ( error. to_string ())); 437 core. stop . request ( false ); 438 drain_from. get_or_insert_with ( Instant :: now); 439} 440} 441if core. exit . is_some () { 442 drain_from. get_or_insert_with ( Instant :: now); 443} 444if core. reason . is_none () 445 && core. exit . is_none () 446 &&self . timeout . is_some_and ( |limit| started. elapsed () >= limit) 447{ 448 core. reason =Some ( "timeout" . into ()); 449 core. stop . request ( false ); 450} 451let Core { child, stop, ..} =& mut * core; 452if let Err ( error) = stop. advance ( |signal|{ 453 child 454. as_ref () 455. map_or ( Ok ( false ), |child| child. signal ( self . group , signal)) 456}) { 457 core. error . get_or_insert ( error. to_string ()); 458} 459if let Some ( since) =& mut drain_from{ 460let pumps =self . pumps . load ( Ordering :: Acquire ); 461// A natural run's pumps waiting on a slow reader are not 462// pipes held open by stragglers; the drain clock restarts 463// once they move again. 464if core. reason . is_none () 465 && pumps !=0 466 &&self . parked . load ( Ordering :: Acquire ) == pumps 467{ 468* since =Instant :: now (); 469} 470// Keep the leader unreaped until group cleanup, so its PID cannot be reused. 471if since. elapsed () >=self . drain && pumps !=0 { 472if self . group { 473let _ =self . signal_locked ( & mut core, "SIGKILL" ); 474} 475 core. report ( 476"drain-timeout" , 477Some ( "Subprocess pipes remained open after exit" . into ()), 478); 479self . pipe_stop . cancel (); 480} 481if self . pumps . load ( Ordering :: Acquire ) ==0 { 482if self . group { 483let _ =self . signal_locked ( & mut core, "SIGKILL" ); 484} 485break ; 486} 487} 488if core. stop . forced { 489self . pipe_stop . cancel (); 490} 491} 492if drain_from. is_some () { 493for inputin self . inputs . values () { 494 input. close (). await ; 495} 496} 497 tokio:: select!{ _ =self . wake . notified () =>(), _ = tokio:: time:: sleep ( Duration :: from_millis ( 5 )) =>() } 498} 499for inputin self . inputs . values () { 500 input. close (). await ; 501} 502let mut core =self . core . lock (). unwrap (); 503if let Some ( mut child) = core. child . take () 504 &&let Err ( error) = child. wait () 505{ 506 core. error . get_or_insert ( error. to_string ()); 507} 508 core. completed =true ; 509self . changed . notify_waiters (); 510self . done . notify_all (); 511} 512} 513 514pub struct Launcher { 515pub runtime : Handle , 516pub caps : Arc < Capabilities >, 517pub owners : Vec < CancellationToken >, 518} 519impl Launcher { 520pub fn spawn ( 521& self , 522options : Options , 523environment : impl IntoIterator < Item =( OsString , OsString )>, 524scope : Option < & Scope >, 525file : impl Fn ( & StdioSpec , bool , & Path ) -> io:: Result < std:: fs:: File >, 526) -> io:: Result < Arc < Process >> { 527let plan =validate ( & options) ?; 528if self . owners . iter (). any ( CancellationToken :: is_cancelled) { 529return Err ( missing ()); 530} 531let _guard =self . runtime . enter (); 532let cwd = options. cwd . as_ref (). map_or_else ( 533 ||self . caps . cwd (). to_owned (), 534 |cwd|self . caps . cwd (). join ( cwd), 535); 536// Restricted denials share one message so success and failure cannot 537// be used to probe whether directories exist. 538let cwd = cwd. canonicalize (). map_err ( |error|{ 539if self . caps . unrestricted_run () { 540 error 541} else { 542denied () 543} 544}) ?; 545let mut env =BTreeMap :: new (); 546if !options. clear_env { 547for ( name, value) in environment{ 548if self . caps . unrestricted_env () 549 || name 550. to_str () 551. is_some_and ( |name|self . caps . check_env ( name). is_ok ()) 552{ 553 env. insert ( native_env_name ( & name), ( name, value)); 554} 555} 556} 557for ( name, value) in & options. env { 558 env. insert ( 559native_env_name ( OsStr :: new ( name)), 560( name. into (), value. into ()), 561); 562} 563let path = env 564. get ( & native_env_name ( OsStr :: new ( "PATH" ))) 565. map ( |( _, value) | value. as_os_str ()); 566let ( executable, target) =resolve ( 567& options. command , 568 options. search_path , 569& cwd, 570 path, 571& self . caps , 572) ?; 573// A child that inherits standard streams joins the parent's process 574// group instead of its own, but only with a terminal attached: only 575// the foreground group can use it (inherited TTYs, or /dev/tty 576// opened by the child), and seizing the foreground here would 577// suspend this process when restoring it. Headless setups keep 578// their own group so timeouts and kills reach all descendants. 579// Windows consoles deliver to every attached process regardless, 580// so job objects never need to give way. 581let shares_terminal =cfg! ( unix) 582 &&matches! ( plan. scope , TerminationScope :: Group ) 583 && plan. terminal . is_none () 584 &&{ 585use std:: io:: IsTerminal ; 586// Options::stdio() yields fds 0, 1, 2 first and in order, 587// so the leading plan entries are the standard streams. 588let inherited =[ 589matches! ( plan. stdio [ 0 ]. 1 , StdioPlan :: Inherit ), 590matches! ( plan. stdio [ 1 ]. 1 , StdioPlan :: Inherit ), 591matches! ( plan. stdio [ 2 ]. 1 , StdioPlan :: Inherit ), 592]; 593let terminals =[ 594 std:: io:: stdin (). is_terminal (), 595 std:: io:: stdout (). is_terminal (), 596 std:: io:: stderr (). is_terminal (), 597]; 598 inherited. contains ( & true ) 599 &&( inherited 600. into_iter () 601. zip ( terminals) 602. any ( |( inherit, tty) | inherit && tty) 603 || sys:: controlling_foreground ()) 604}; 605let group = 606 scope. is_none () &&matches! ( plan. scope , TerminationScope :: Group ) && !shares_terminal; 607let mut readers: BTreeMap < u32 , Reader > =BTreeMap :: new (); 608let mut writers: BTreeMap < u32 , Writer > =BTreeMap :: new (); 609let mut endpoints =BTreeMap :: new (); 610let window =if let Some ( size) = plan. terminal { 611let pty = sys:: pty ( size. columns , size. rows ) ?; 612 readers. insert ( 1 , Box :: pin ( pty. reader )); 613 writers. insert ( 0 , Box :: pin ( pty. writer )); 614for ( fd, endpoint) in pty. endpoints . into_iter (). enumerate () { 615 endpoints. insert ( fdas u32 , endpoint); 616} 617Some ( pty. window ) 618} else { 619for ( fd, kind, read) in & plan. stdio { 620let endpoint =match kind{ 621StdioPlan :: Pipe =>{ 622let ( pipe, endpoint) = sys:: pipe ( * read) ?; 623if * read{ 624 writers. insert ( * fd, Box :: pin ( pipe)); 625} else { 626 readers. insert ( * fd, Box :: pin ( pipe)); 627} 628 endpoint 629} 630StdioPlan :: Null => sys:: null ( * read) ?, 631StdioPlan :: Inherit => sys:: inherit ( * fd) ?, 632StdioPlan :: Stdout => sys:: duplicate ( & endpoints[ & 1 ]) ?, 633StdioPlan :: File ( spec) |StdioPlan :: Handle ( spec) =>{ 634file ( spec, * read, & cwd) ?. into () 635} 636}; 637 endpoints. insert ( * fd, endpoint); 638} 639None 640}; 641if options 642. backpressure 643. iter () 644. any ( |fd| !readers. contains_key ( fd)) 645{ 646return Err ( invalid ( "Backpressure requires a piped output" )); 647} 648self . caps . check_run ( & target). map_err ( |_|denied ()) ?; 649if self . owners . iter (). any ( CancellationToken :: is_cancelled) { 650return Err ( missing ()); 651} 652let launch = sys:: Launch { 653 executable, 654program : target. as_path (), 655args : & options. args , 656env : env. into_values (). collect (), 657cwd : & cwd, 658 group, 659join : None , 660terminal : window. is_some (), 661endpoints : endpoints. into_iter (). collect (), 662}; 663let child =match & scope{ 664Some ( scope) => scope. spawn ( launch) ?, 665None => sys:: spawn ( launch) ?, 666}; 667let pid = child. id (); 668let outputs: BTreeMap < _ , _ > = readers 669. keys () 670. map ( |fd|{ 671( 672* fd, 673Arc :: new ( Output :: new ( options. backpressure . contains ( fd))), 674) 675}) 676. collect (); 677let inputs = writers 678. into_iter () 679. map ( |( fd, writer) |{ 680( 681 fd, 682Arc :: new ( Input { 683writer : AsyncMutex :: new ( Some ( writer)), 684stop : CancellationToken :: new (), 685}), 686) 687}) 688. collect (); 689let process =Arc :: new ( Process { 690 group, 691drain : Duration :: from_millis ( options. drain_timeout_ms ), 692timeout : options. timeout_ms . map ( Duration :: from_millis), 693limit : options. max_collected_bytes , 694pending_limit : options. max_buffered_bytes , 695core : Mutex :: new ( Core { 696child : Some ( child), 697exit : None , 698completed : false , 699reason : None , 700error : None , 701stop : Escalation :: new ( vec! [( 702 options. termination . signal , 703Duration :: from_millis ( options. termination . grace_ms ), 704)]), 705epoch : 0 , 706produced : 0 , 707}), 708done : Condvar :: new (), 709changed : Notify :: new (), 710wake : Notify :: new (), 711pipe_stop : CancellationToken :: new (), 712pumps : AtomicUsize :: new ( readers. len ()), 713parked : AtomicUsize :: new ( 0 ), 714owners : self . owners . clone (), 715 pid, 716 outputs, 717 inputs, 718 window, 719}); 720for ( fd, reader) in readers{ 721self . runtime 722. spawn ( process. clone (). pump ( process. outputs [ & fd]. clone (), reader)); 723} 724self . runtime . spawn ( process. clone (). supervise ()); 725Ok ( process) 726} 727} 728fn validate ( options : & Options ) -> io:: Result < SpawnPlan < ' _ >> { 729if options. command . is_empty () 730 || options. command . contains ( '\0' ) 731 || options. args . iter (). any ( |arg| arg. contains ( '\0' )) 732{ 733return Err ( invalid ( "Commands and arguments must not contain NUL" )); 734} 735if options. cwd . as_ref (). is_some_and ( |cwd|{ 736 cwd. as_os_str (). is_empty () || cwd. as_os_str (). as_encoded_bytes (). contains ( & 0 ) 737}) { 738return Err ( invalid ( "Invalid subprocess cwd" )); 739} 740for ( name, value) in & options. env { 741env_name ( name). map_err ( |e|invalid ( & e. to_string ())) ?; 742if value. contains ( '\0' ) { 743return Err ( invalid ( "Environment values must not contain NUL" )); 744} 745} 746if options. max_buffered_bytes ==0 747 || options. max_buffered_bytes >( 1 <<53 ) -1 748 || options 749. max_collected_bytes 750. is_some_and ( |limit| limit ==0 || limit >( 1 <<53 ) -1 ) 751{ 752return Err ( invalid ( "Invalid subprocess output limit" )); 753} 754let scope =match options. termination . scope . as_str () { 755"group" =>TerminationScope :: Group , 756"child" =>TerminationScope :: Child , 757 _ =>return Err ( invalid ( "Invalid termination scope" )), 758}; 759 sys:: validate_signal ( & options. termination . signal ) ?; 760fn validate_spec ( 761spec : & StdioSpec , 762read : bool , 763stderr : bool , 764extra : bool , 765) -> io:: Result < StdioPlan < ' _ >> { 766let plan =match spec. mode . as_str () { 767"inherit" =>StdioPlan :: Inherit , 768"null" =>StdioPlan :: Null , 769"piped" =>StdioPlan :: Pipe , 770"stdout" if stderr =>StdioPlan :: Stdout , 771"file" =>StdioPlan :: File ( spec), 772"handle" =>StdioPlan :: Handle ( spec), 773 _ =>return Err ( invalid ( "Invalid stdio mode" )), 774}; 775if matches! ( plan, StdioPlan :: File ( _)) { 776if spec 777. file 778. as_ref () 779. is_none_or ( |path| path. is_empty () || path. contains ( '\0' )) 780 || spec. handle . is_some () 781{ 782return Err ( invalid ( "Invalid stdio file" )); 783} 784if read{ 785if spec. file_mode . is_some () { 786return Err ( invalid ( "Input file accepts no file mode" )); 787} 788} else if !matches! ( 789 spec. file_mode . as_deref (), 790Some ( "append" |"truncate" |"create-new" ) 791) { 792return Err ( invalid ( "Invalid output file mode" )); 793} 794} else if spec. file . is_some () 795 || spec. file_mode . is_some () 796 ||matches! ( plan, StdioPlan :: Handle ( _)) != spec. handle . is_some () 797{ 798return Err ( invalid ( "Invalid stdio specification" )); 799} 800if extra{ 801if matches! ( plan, StdioPlan :: Inherit ) { 802return Err ( invalid ( "Extra stdio cannot inherit internal descriptors" )); 803} 804if !matches! ( 805 spec. direction . as_deref (), 806Some ( "parent-to-child" |"child-to-parent" ) 807) { 808return Err ( invalid ( "Extra stdio requires a direction" )); 809} 810} else if spec. direction . is_some () { 811return Err ( invalid ( "Unexpected stdio direction" )); 812} 813Ok ( plan) 814} 815let mut descriptors =BTreeSet :: new (); 816let mut stdio =Vec :: new (); 817for ( fd, spec, read) in options. stdio () { 818if fd > sys:: MAX_DESCRIPTOR || !descriptors. insert ( fd) { 819return Err ( invalid ( "Invalid additional descriptor" )); 820} 821let plan =validate_spec ( spec, read, fd ==2 , fd >=3 ) ?; 822 stdio. push (( fd, plan, read)); 823} 824if let Some ( window) =& options. terminal 825 &&( window. columns ==0 826 || window. rows ==0 827 || !matches! ( stdio[ 0 ]. 1 , StdioPlan :: Pipe ) 828 || !matches! ( stdio[ 1 ]. 1 , StdioPlan :: Pipe ) 829 || !matches! ( stdio[ 2 ]. 1 , StdioPlan :: Stdout ) 830 || !options. extra_stdio . is_empty ()) 831{ 832return Err ( invalid ( "Terminal stdio must be piped and merged" )); 833} 834Ok ( SpawnPlan { 835 stdio, 836 scope, 837terminal : options. terminal . as_ref (), 838}) 839} 840 841/// Restricted launch denials share one message so they cannot be used to 842/// probe whether directories or executables exist. 843fn denied () -> io:: Error { 844 io:: Error :: new ( 845 io:: ErrorKind :: PermissionDenied , 846"Executable is not authorized" , 847) 848} 849 850fn resolve ( 851command : & str , 852search : bool , 853cwd : & Path , 854path : Option < & OsStr >, 855caps : & Capabilities , 856) -> io:: Result <( sys:: Executable , PathTarget )> { 857// Entry-based authorization: the parent is canonicalized but the final 858// component stays lexical, so one granted multi-call entry (busybox, 859// coreutils) does not authorize its siblings. 860let bare =Path :: new ( command). components (). count () ==1 861 && !command. contains ( '/' ) 862 && !command. contains ( '\\' ); 863let candidates =if search && bare{ 864// An absent PATH under restricted authority must not become an oracle. 865let path = path. ok_or_else ( ||{ 866if caps. unrestricted_run () { 867 io:: Error :: new ( 868 io:: ErrorKind :: NotFound , 869"PATH is absent from the subprocess environment" , 870) 871} else { 872denied () 873} 874}) ?; 875 std:: env:: split_paths ( path) 876. flat_map ( |directory| sys:: expand ( cwd. join ( directory). join ( command))) 877. collect ::< Vec < _ >>() 878} else { 879let candidate = cwd. join ( command); 880vec! [ 881 sys:: expand ( candidate. clone ()) 882. into_iter () 883. find ( |path| path. symlink_metadata (). is_ok ()) 884. unwrap_or ( candidate), 885] 886}; 887for candidatein candidates{ 888let target =match PathTarget :: for_executable ( & candidate) { 889Ok ( target) => target, 890Err ( error) 891if search 892 && bare 893 &&matches! ( 894 error. kind (), 895 io:: ErrorKind :: NotFound | io:: ErrorKind :: NotADirectory 896) => 897{ 898continue ; 899} 900Err ( error) =>{ 901let permitted =PathTarget :: for_creation ( & candidate) 902. ok () 903. is_some_and ( |target| caps. check_run ( & target). is_ok ()); 904return Err ( if permitted{ error} else { denied () }); 905} 906}; 907if !( search && bare) { 908 caps. check_run ( & target). map_err ( |_|denied ()) ?; 909} 910let metadata =match std:: fs:: metadata ( target. as_path ()) { 911Ok ( metadata) => metadata, 912Err ( error) 913if search 914 && bare 915 &&matches! ( 916 error. kind (), 917 io:: ErrorKind :: NotFound 918 | io:: ErrorKind :: NotADirectory 919 | io:: ErrorKind :: PermissionDenied 920) => 921{ 922continue ; 923} 924Err ( error) =>return Err ( error), 925}; 926let executable = sys:: is_executable ( & metadata); 927if !executable && search && bare{ 928continue ; 929} 930 caps. check_run ( & target). map_err ( |_|denied ()) ?; 931# [ cfg ( target_os = "linux" )] 932let ( executable, metadata) ={ 933let ( directory, name) = avril_caps:: fs:: parent ( target. as_path ()) ?; 934 sys:: Executable :: open_at ( & directory, & name) ? 935}; 936# [ cfg ( not ( target_os = "linux" ))] 937let ( executable, metadata) = sys:: Executable :: open ( target. as_path ()) ?; 938if PathTarget :: for_executable ( & candidate) ?. as_path () != target. as_path () { 939return Err ( io:: Error :: other ( "Executable changed during resolution" )); 940} 941if !sys:: is_executable ( & metadata) { 942return Err ( io:: Error :: new ( 943 io:: ErrorKind :: PermissionDenied , 944"Not an executable file" , 945)); 946} 947return Ok (( executable, target)); 948} 949if caps. unrestricted_run () { 950Err ( io:: Error :: new ( 951 io:: ErrorKind :: NotFound , 952"Executable was not found" , 953)) 954} else { 955Err ( denied ()) 956} 957}