Skip to content

Commit ec16df1

Browse files
committed
Fix exit with parent on macOS
Implement exit with parent functionality for macOS, by porting the reference implementation in `nbdkit` to Rust. Fixes #29.
1 parent f0dfa85 commit ec16df1

3 files changed

Lines changed: 101 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
bump: patch
3+
type: fix
4+
---
5+
6+
Fix exit with parent on macOS. Previously, when the parent process was terminated by a signal which could not be caught, such as `SIGSTOP` or `SIGKILL`, the child process was not terminated on macOS.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/exit.rs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,100 @@ fn set_exit_with_parent() {
1818

1919
#[cfg(target_os = "macos")]
2020
fn set_exit_with_parent() {
21-
// macOS does not have the `prctl` function, so we do nothing.
22-
// This means that the child process will not be terminated when
23-
// the parent process exits.
21+
// macOS lacks `prctl`, so we use `kqueue`/`kevent` with `EVFILT_PROC`/`NOTE_EXIT`
22+
// to watch for the parent process exiting.
23+
//
24+
// Because this runs in `pre_exec` (after fork, before exec), any thread we spawn
25+
// would be killed by the subsequent `exec` call. Instead, we double-fork a detached
26+
// watcher process: the intermediate process exits immediately (so the watcher is
27+
// re-parented to PID 1), and the watcher blocks on `kevent` until either the parent
28+
// or the child exits.
29+
use libc::{c_int, close, fork, getpid, getppid, kevent, kqueue, pid_t, waitpid};
30+
use libc::{EVFILT_PROC, EV_ADD, EV_ENABLE, NOTE_EXIT, SIGTERM};
31+
use std::mem;
32+
33+
unsafe {
34+
let child_pid: pid_t = getpid();
35+
let parent_pid: pid_t = getppid();
36+
37+
let intermediate = fork();
38+
if intermediate < 0 {
39+
return;
40+
}
41+
42+
if intermediate == 0 {
43+
// Intermediate process: fork the watcher and exit immediately so the
44+
// watcher is re-parented to init/launchd and won't become a zombie.
45+
let watcher = fork();
46+
if watcher < 0 {
47+
libc::_exit(1);
48+
}
49+
if watcher == 0 {
50+
// Watcher process: use kqueue to watch parent_pid and child_pid.
51+
let kq = kqueue();
52+
if kq == -1 {
53+
libc::_exit(1);
54+
}
55+
56+
let changes: [libc::kevent; 2] = [
57+
libc::kevent {
58+
ident: parent_pid as libc::uintptr_t,
59+
filter: EVFILT_PROC,
60+
flags: EV_ADD | EV_ENABLE,
61+
fflags: NOTE_EXIT,
62+
data: 0,
63+
udata: std::ptr::null_mut(),
64+
},
65+
libc::kevent {
66+
ident: child_pid as libc::uintptr_t,
67+
filter: EVFILT_PROC,
68+
flags: EV_ADD | EV_ENABLE,
69+
fflags: NOTE_EXIT,
70+
data: 0,
71+
udata: std::ptr::null_mut(),
72+
},
73+
];
74+
75+
let r = kevent(
76+
kq,
77+
changes.as_ptr(),
78+
2,
79+
std::ptr::null_mut(),
80+
0,
81+
std::ptr::null(),
82+
);
83+
if r == -1 {
84+
close(kq);
85+
libc::_exit(1);
86+
}
87+
88+
loop {
89+
let mut res: libc::kevent = mem::zeroed();
90+
let n = kevent(kq, std::ptr::null(), 0, &mut res, 1, std::ptr::null());
91+
if n <= 0 {
92+
break;
93+
}
94+
if res.ident == parent_pid as libc::uintptr_t {
95+
// Parent exited: terminate the child.
96+
libc::kill(child_pid, SIGTERM);
97+
break;
98+
} else if res.ident == child_pid as libc::uintptr_t {
99+
// Child exited on its own: nothing to do.
100+
break;
101+
}
102+
}
103+
104+
close(kq);
105+
libc::_exit(0);
106+
} else {
107+
libc::_exit(0);
108+
}
109+
} else {
110+
// Original child: reap the intermediate process, then continue to exec.
111+
let mut status: c_int = 0;
112+
waitpid(intermediate, &mut status, 0);
113+
}
114+
}
24115
}
25116

26117
pub fn exit_with_parent() -> Result<(), std::io::Error> {

0 commit comments

Comments
 (0)