Skip to content

Commit eadedad

Browse files
committed
fix(cli): reject exiting dispatch leaders on macOS
1 parent 675df88 commit eadedad

1 file changed

Lines changed: 44 additions & 8 deletions

File tree

src/apps/cli/src/dispatch/runner.rs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
218227
pub(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

Comments
 (0)