@@ -214,6 +214,15 @@ fn process_group_alive(process_group: i32) -> bool {
214214 )
215215}
216216
217+ #[ cfg( any( test, target_os = "macos" ) ) ]
218+ fn macos_process_state_allows_escalation ( stat : & str ) -> bool {
219+ let Some ( stat) = stat. split_whitespace ( ) . next ( ) else {
220+ return false ;
221+ } ;
222+ matches ! ( stat. chars( ) . next( ) , Some ( 'I' | 'R' | 'S' | 'T' | 'U' ) )
223+ && !stat. chars ( ) . skip ( 1 ) . any ( |modifier| modifier == 'E' )
224+ }
225+
217226#[ cfg( unix) ]
218227pub ( crate ) fn process_alive ( pid : u32 ) -> bool {
219228 let Ok ( pid) = i32:: try_from ( pid) else {
@@ -246,9 +255,10 @@ pub(crate) fn process_alive(pid: u32) -> bool {
246255
247256 #[ cfg( target_os = "macos" ) ]
248257 {
249- // macOS also reports zombies as present to kill(0). Query the process
250- // state before using a leader PID to authenticate SIGKILL escalation;
251- // a failed/empty query means the process disappeared during the check.
258+ // macOS reports zombies as present to kill(0), and ps marks a process
259+ // that is trying to exit with the E modifier. Require a known live
260+ // state with no exit modifier before authenticating SIGKILL escalation;
261+ // a failed or unrecognized query must fail closed.
252262 let output = Command :: new ( "ps" )
253263 . args ( [ "-p" , & pid. to_string ( ) , "-o" , "stat=" ] )
254264 . output ( ) ;
@@ -258,11 +268,7 @@ pub(crate) fn process_alive(pid: u32) -> bool {
258268 if !output. status . success ( ) {
259269 return false ;
260270 }
261- return String :: from_utf8_lossy ( & output. stdout )
262- . trim_start ( )
263- . chars ( )
264- . next ( )
265- . is_some_and ( |state| state != 'Z' ) ;
271+ return macos_process_state_allows_escalation ( & String :: from_utf8_lossy ( & output. stdout ) ) ;
266272 }
267273
268274 #[ cfg( not( target_os = "macos" ) ) ]
@@ -390,6 +396,36 @@ mod tests {
390396 ) ) ;
391397 }
392398
399+ #[ test]
400+ fn macos_live_process_states_can_authenticate_sigkill_escalation ( ) {
401+ for stat in [ "I" , "R+" , "Ss" , "T" , "U" ] {
402+ assert ! (
403+ macos_process_state_allows_escalation( stat) ,
404+ "live state {stat} should authenticate escalation"
405+ ) ;
406+ }
407+ }
408+
409+ #[ test]
410+ fn macos_dead_or_unknown_process_states_cannot_authenticate_sigkill_escalation ( ) {
411+ for stat in [ "" , "Z" , "Z+" , "?" ] {
412+ assert ! (
413+ !macos_process_state_allows_escalation( stat) ,
414+ "dead state {stat:?} must not authenticate escalation"
415+ ) ;
416+ }
417+ }
418+
419+ #[ test]
420+ fn macos_exiting_process_states_cannot_authenticate_sigkill_escalation ( ) {
421+ for stat in [ "SE" , "UEs" ] {
422+ assert ! (
423+ !macos_process_state_allows_escalation( stat) ,
424+ "exiting state {stat} must not authenticate escalation"
425+ ) ;
426+ }
427+ }
428+
393429 #[ cfg( unix) ]
394430 #[ test]
395431 fn cancellation_does_not_signal_an_unverified_group_after_leader_exit ( ) {
0 commit comments