1111#![ cfg( target_os = "linux" ) ]
1212
1313use std:: collections:: HashSet ;
14- use std:: sync:: { LazyLock , Mutex } ;
14+ use std:: sync:: { LazyLock , Mutex , MutexGuard , PoisonError } ;
1515
1616static MANAGED_CHILDREN : LazyLock < Mutex < HashSet < i32 > > > =
1717 LazyLock :: new ( || Mutex :: new ( HashSet :: new ( ) ) ) ;
1818
19- /// Add `pid` to the supervised-child set. Non-positive or out-of-range values
20- /// are silently ignored.
21- pub fn register ( pid : u32 ) {
22- let Ok ( pid) = i32:: try_from ( pid) else {
19+ fn lock_children ( ) -> MutexGuard < ' static , HashSet < i32 > > {
20+ MANAGED_CHILDREN
21+ . lock ( )
22+ . unwrap_or_else ( PoisonError :: into_inner)
23+ }
24+
25+ fn insert ( children : & mut HashSet < i32 > , pid : Option < u32 > ) {
26+ let Some ( pid) = pid. and_then ( |pid| i32:: try_from ( pid) . ok ( ) ) else {
2327 return ;
2428 } ;
25- if pid <= 0 {
26- return ;
27- }
28- if let Ok ( mut children) = MANAGED_CHILDREN . lock ( ) {
29+ if pid > 0 {
2930 children. insert ( pid) ;
3031 }
3132}
3233
34+ /// Spawn a supervised child and register its PID before the orphan reaper can
35+ /// inspect it.
36+ ///
37+ /// The registry lock intentionally spans `spawn`. Otherwise a fast child can
38+ /// exit and be reaped after `spawn` returns but before its PID is registered,
39+ /// causing the child's explicit waiter to fail with `ECHILD`.
40+ pub fn spawn_registered < T , E > (
41+ spawn : impl FnOnce ( ) -> Result < T , E > ,
42+ pid : impl FnOnce ( & T ) -> Option < u32 > ,
43+ ) -> Result < T , E > {
44+ let mut children = lock_children ( ) ;
45+ let child = spawn ( ) ?;
46+ insert ( & mut children, pid ( & child) ) ;
47+ Ok ( child)
48+ }
49+
3350/// Remove `pid` from the supervised-child set. Non-positive or out-of-range
3451/// values are silently ignored.
3552pub fn unregister ( pid : u32 ) {
@@ -39,32 +56,58 @@ pub fn unregister(pid: u32) {
3956 if pid <= 0 {
4057 return ;
4158 }
42- if let Ok ( mut children) = MANAGED_CHILDREN . lock ( ) {
43- children. remove ( & pid) ;
44- }
59+ lock_children ( ) . remove ( & pid) ;
4560}
4661
47- /// Return `true` if `pid` is currently in the supervised-child set.
48- #[ must_use]
49- pub fn is_managed ( pid : i32 ) -> bool {
50- MANAGED_CHILDREN
51- . lock ( )
52- . is_ok_and ( |children| children. contains ( & pid) )
62+ /// Run `reap` only when `pid` is not a supervised child.
63+ ///
64+ /// The registry lock remains held while `reap` runs so child creation cannot
65+ /// open a spawn-to-registration window between the membership check and reap.
66+ pub fn reap_if_unmanaged < T > ( pid : i32 , reap : impl FnOnce ( ) -> T ) -> Option < T > {
67+ let children = lock_children ( ) ;
68+ if children. contains ( & pid) {
69+ None
70+ } else {
71+ Some ( reap ( ) )
72+ }
5373}
5474
5575#[ cfg( test) ]
5676mod tests {
57- use super :: { is_managed , register , unregister} ;
77+ use super :: { reap_if_unmanaged , spawn_registered , unregister} ;
5878 use nix:: sys:: wait:: { Id , WaitPidFlag , WaitStatus , waitid, waitpid} ;
5979 use nix:: unistd:: Pid ;
6080 use std:: process:: Command ;
6181
82+ #[ test]
83+ fn spawned_child_is_registered_before_returning ( ) {
84+ let pid = 1_000_000_u32 ;
85+ let child = spawn_registered ( || Ok :: < _ , ( ) > ( pid) , |pid| Some ( * pid) ) . unwrap ( ) ;
86+ let mut reaped = false ;
87+
88+ let result = reap_if_unmanaged ( i32:: try_from ( child) . unwrap ( ) , || {
89+ reaped = true ;
90+ } ) ;
91+
92+ assert ! ( result. is_none( ) ) ;
93+ assert ! ( !reaped) ;
94+ unregister ( pid) ;
95+ }
96+
97+ #[ test]
98+ fn unmanaged_child_can_be_reaped ( ) {
99+ let result = reap_if_unmanaged ( 1_000_001 , || "reaped" ) ;
100+
101+ assert_eq ! ( result, Some ( "reaped" ) ) ;
102+ }
103+
62104 #[ test]
63105 fn fast_child_remains_waitable_after_orphan_reap_attempt ( ) {
64- let mut child = Command :: new ( "sh" )
65- . args ( [ "-c" , "exit 11" ] )
66- . spawn ( )
67- . expect ( "spawn fast child" ) ;
106+ let mut child = spawn_registered (
107+ || Command :: new ( "sh" ) . args ( [ "-c" , "exit 11" ] ) . spawn ( ) ,
108+ |child| Some ( child. id ( ) ) ,
109+ )
110+ . expect ( "spawn and register fast child" ) ;
68111 let child_pid = child. id ( ) ;
69112 let pid = Pid :: from_raw ( i32:: try_from ( child_pid) . unwrap ( ) ) ;
70113
@@ -81,10 +124,11 @@ mod tests {
81124 }
82125 }
83126
84- if !is_managed ( pid. as_raw ( ) ) {
85- waitpid ( pid, Some ( WaitPidFlag :: WNOHANG ) ) . expect ( "orphan reaper consumes child" ) ;
86- }
87- register ( child_pid) ;
127+ let reaped = reap_if_unmanaged ( pid. as_raw ( ) , || waitpid ( pid, Some ( WaitPidFlag :: WNOHANG ) ) ) ;
128+ assert ! (
129+ reaped. is_none( ) ,
130+ "the orphan reaper must leave a registered child to its explicit waiter"
131+ ) ;
88132
89133 let wait = child. wait ( ) ;
90134 unregister ( child_pid) ;
0 commit comments