Skip to main content

reverie_process/
exit_status.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use std::os::unix::process::ExitStatusExt;
10
11use nix::sys::signal;
12use nix::sys::signal::SigHandler;
13use nix::sys::signal::SigSet;
14use nix::sys::signal::SigmaskHow;
15use nix::sys::signal::Signal;
16
17/// Describes the result of a process after it has exited.
18///
19/// This is similar to `std::process::ExitStatus`, but is easier to match
20/// against and provides additional functionality like `raise_or_exit` that
21/// helps with propagating an exit status.
22#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
23pub enum ExitStatus {
24    /// Program exited with an exit code.
25    Exited(i32),
26    /// Program killed by signal, with or without a coredump.
27    Signaled(Signal, bool),
28}
29
30impl ExitStatus {
31    /// A successful exit status.
32    pub const SUCCESS: Self = ExitStatus::Exited(0);
33
34    /// Construct an `ExitStatus` from a raw exit code.
35    pub fn from_raw(code: i32) -> Self {
36        if libc::WIFEXITED(code) {
37            ExitStatus::Exited(libc::WEXITSTATUS(code))
38        } else {
39            ExitStatus::Signaled(
40                Signal::try_from(libc::WTERMSIG(code)).unwrap(),
41                libc::WCOREDUMP(code),
42            )
43        }
44    }
45
46    /// Converts the exit status into a raw number.
47    pub fn into_raw(self) -> i32 {
48        match self {
49            ExitStatus::Exited(code) => code << 8,
50            ExitStatus::Signaled(sig, coredump) => {
51                if coredump {
52                    (sig as i32 | 0x80) & 0xff
53                } else {
54                    sig as i32 & 0x7f
55                }
56            }
57        }
58    }
59
60    /// If the process was terminated by a signal, returns that signal.
61    pub fn signal(&self) -> Option<i32> {
62        match self {
63            ExitStatus::Exited(_) => None,
64            ExitStatus::Signaled(sig, _) => Some(*sig as i32 & 0x7f),
65        }
66    }
67
68    /// Was termination successful? Signal termination is not considered a
69    /// success, and success is defined as a zero exit status.
70    pub fn success(&self) -> bool {
71        self == &ExitStatus::Exited(0)
72    }
73
74    /// Returns the exit code of the process, if any. If the process was
75    /// terminated by a signal, this will return `None`.
76    pub fn code(&self) -> Option<i32> {
77        if let ExitStatus::Exited(code) = *self {
78            Some(code)
79        } else {
80            None
81        }
82    }
83
84    /// Propagate the exit status such that the current process exits in the same
85    /// way that the child process exited.
86    pub fn raise_or_exit(self) -> ! {
87        match self {
88            ExitStatus::Signaled(signal, core_dump) => {
89                if core_dump {
90                    // Prevent the current process from producing a core dump as
91                    // well when the signal is propagated.
92                    let limit = libc::rlimit {
93                        rlim_cur: 0,
94                        rlim_max: 0,
95                    };
96                    unsafe { libc::setrlimit(libc::RLIMIT_CORE, &limit) };
97                }
98
99                // Raise the same signal, which may or may not be fatal.
100                let _ = unsafe { signal::signal(signal, SigHandler::SigDfl) };
101                let _ = signal::raise(signal);
102
103                // Unblock the signal.
104                let mut mask = SigSet::empty();
105                mask.add(signal);
106                let _ = signal::sigprocmask(SigmaskHow::SIG_UNBLOCK, Some(&mask), None);
107
108                // Incase the signal is not fatal:
109                std::process::exit(signal as i32 + 128);
110            }
111            ExitStatus::Exited(code) => std::process::exit(code),
112        }
113    }
114}
115
116impl From<ExitStatus> for std::process::ExitStatus {
117    fn from(status: ExitStatus) -> Self {
118        Self::from_raw(status.into_raw())
119    }
120}
121
122impl From<std::process::ExitStatus> for ExitStatus {
123    fn from(status: std::process::ExitStatus) -> Self {
124        if let Some(sig) = status.signal() {
125            ExitStatus::Signaled(Signal::try_from(sig).unwrap(), true)
126        } else {
127            ExitStatus::Exited(status.code().unwrap_or(255))
128        }
129    }
130}
131
132impl serde::Serialize for ExitStatus {
133    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134    where
135        S: serde::ser::Serializer,
136    {
137        serializer.serialize_i32(self.into_raw())
138    }
139}
140
141impl<'de> serde::Deserialize<'de> for ExitStatus {
142    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
143    where
144        D: serde::de::Deserializer<'de>,
145    {
146        let value = i32::deserialize(deserializer)?;
147        Ok(ExitStatus::from_raw(value))
148    }
149}
150
151// `sanitized` is a Meta-internal cfg (set for sanitizer builds to skip these
152// fork-based tests); it is never set in the open-source build. It is declared
153// via `check-cfg` in this crate's Cargo.toml so it is a known cfg name.
154#[cfg(all(test, not(sanitized)))]
155mod tests_non_sanitized {
156    use nix::sys::signal;
157    use nix::sys::signal::Signal;
158    use nix::sys::wait::WaitStatus;
159    use nix::sys::wait::waitpid;
160    use nix::unistd::ForkResult;
161    use nix::unistd::fork;
162
163    use super::*;
164
165    // Runs a closure in a forked process and reports the exit status.
166    fn run_forked<F>(f: F) -> nix::Result<ExitStatus>
167    where
168        F: FnOnce() -> nix::Result<()>,
169    {
170        match unsafe { fork() }? {
171            ForkResult::Parent { child, .. } => {
172                // Simply wait for the child to exit.
173                match waitpid(child, None)? {
174                    WaitStatus::Exited(_, code) => Ok(ExitStatus::Exited(code)),
175                    WaitStatus::Signaled(_, sig, coredump) => {
176                        Ok(ExitStatus::Signaled(sig, coredump))
177                    }
178                    wait_status => unreachable!("Got unexpected wait status: {:?}", wait_status),
179                }
180            }
181            ForkResult::Child => {
182                // Suppress core dumps for testing purposes.
183                let limit = libc::rlimit {
184                    rlim_cur: 0,
185                    rlim_max: 0,
186                };
187                unsafe {
188                    // restore some sighandlers to default
189                    for &sig in &[libc::SIGALRM, libc::SIGINT, libc::SIGVTALRM] {
190                        libc::signal(sig, libc::SIG_DFL);
191                    }
192                    // disable coredump
193                    libc::setrlimit(libc::RLIMIT_CORE, &limit)
194                };
195
196                // Run the child.
197                let code = match f() {
198                    Ok(()) => 0,
199                    Err(err) => {
200                        eprintln!("{}", err);
201                        1
202                    }
203                };
204
205                // The closure should have called `exit` by this point, but just
206                // in case it didn't, call it ourselves.
207                //
208                // Note: We also can't use the normal exit function here because we
209                // don't want to call atexit handlers since `execve` was never
210                // called.
211                unsafe { ::libc::_exit(code) };
212            }
213        }
214    }
215
216    #[test]
217    fn normal_exit() {
218        if crate::test_runs_in_own_process() {
219            return;
220        }
221        assert_eq!(
222            run_forked(|| { unsafe { libc::_exit(0) } }),
223            Ok(ExitStatus::Exited(0))
224        );
225
226        assert_eq!(
227            run_forked(|| { unsafe { libc::_exit(42) } }),
228            Ok(ExitStatus::Exited(42))
229        );
230
231        // Thread exit
232        assert_eq!(
233            run_forked(|| {
234                unsafe { libc::syscall(libc::SYS_exit, 42) };
235                unreachable!();
236            }),
237            Ok(ExitStatus::Exited(42))
238        );
239
240        // exit_group. Should be identical to `libc::_exit`.
241        assert_eq!(
242            run_forked(|| {
243                unsafe { libc::syscall(libc::SYS_exit_group, 42) };
244                unreachable!();
245            }),
246            Ok(ExitStatus::Exited(42))
247        );
248    }
249
250    #[test]
251    fn exit_by_signal() {
252        if crate::test_runs_in_own_process() {
253            return;
254        }
255        assert_eq!(
256            run_forked(|| {
257                signal::raise(Signal::SIGALRM)?;
258                unreachable!();
259            }),
260            Ok(ExitStatus::Signaled(Signal::SIGALRM, false))
261        );
262
263        assert_eq!(
264            run_forked(|| {
265                signal::raise(Signal::SIGILL)?;
266                unreachable!();
267            }),
268            Ok(ExitStatus::Signaled(Signal::SIGILL, true))
269        );
270    }
271
272    #[test]
273    fn propagate_exit() {
274        if crate::test_runs_in_own_process() {
275            return;
276        }
277        // NOTE: These tests fail under a sanitized build. ASAN leak detection
278        // must be disabled for this to run correctly. To disable ASAN leak
279        // detection, set the `ASAN_OPTIONS=detect_leaks=0` environment variable
280        // *before* the test starts up. (This is currently done in the TARGETS
281        // file.) Alternatively, we *could* bypass the atexit handler that ASAN
282        // sets up by calling `libc::_exit`, but that may have unintended
283        // consequences for real code.
284        assert_eq!(
285            run_forked(|| { ExitStatus::Exited(0).raise_or_exit() }),
286            Ok(ExitStatus::Exited(0))
287        );
288        assert_eq!(
289            run_forked(|| { ExitStatus::Exited(42).raise_or_exit() }),
290            Ok(ExitStatus::Exited(42))
291        );
292    }
293
294    #[test]
295    fn propagate_signal() {
296        if crate::test_runs_in_own_process() {
297            return;
298        }
299        assert_eq!(
300            run_forked(|| { ExitStatus::Signaled(Signal::SIGILL, true).raise_or_exit() }),
301            Ok(ExitStatus::Signaled(Signal::SIGILL, true))
302        );
303        assert_eq!(
304            run_forked(|| { ExitStatus::Signaled(Signal::SIGALRM, false).raise_or_exit() }),
305            Ok(ExitStatus::Signaled(Signal::SIGALRM, false))
306        );
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn exit_code_into_raw() {
316        assert_eq!(ExitStatus::Exited(1).into_raw(), 0x1 << 8);
317        assert_eq!(
318            ExitStatus::Signaled(Signal::SIGINT, false).into_raw(),
319            Signal::SIGINT as i32
320        );
321        assert_eq!(
322            ExitStatus::Signaled(Signal::SIGILL, true).into_raw(),
323            0x80 | Signal::SIGILL as i32
324        );
325        assert_ne!(
326            ExitStatus::Exited(2).into_raw(),
327            ExitStatus::Signaled(Signal::SIGINT, false).into_raw()
328        );
329    }
330
331    #[test]
332    fn exit_status_from_raw() {
333        assert_eq!(ExitStatus::from_raw(0x100).code(), Some(1));
334        assert_eq!(ExitStatus::from_raw(0x100).signal(), None);
335        assert_eq!(ExitStatus::from_raw(0x84).code(), None);
336        assert_eq!(ExitStatus::from_raw(0x84).signal(), Some(4));
337    }
338}