Skip to content

Commit a17eab7

Browse files
ijacksonm-ou-se
andcommitted
panic ui test: Provide comprehensive test for panic after fork
This tests that we can indeed safely panic after fork, both a raw libc::fork and in a Command pre_exec hook. Signed-off-by: Ian Jackson <[email protected]> Co-authored-by: Mara Bos <[email protected]>
1 parent f801506 commit a17eab7

File tree

3 files changed

+179
-22
lines changed

3 files changed

+179
-22
lines changed

library/std/src/sys/unix/process/process_unix/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use crate::os::unix::process::{CommandExt, ExitStatusExt};
22
use crate::panic::catch_unwind;
33
use crate::process::Command;
44

5+
// Many of the other aspects of this situation, including heap alloc concurrency
6+
// safety etc., are tested in src/test/ui/process/process-panic-after-fork.rs
7+
58
#[test]
69
fn exitstatus_display_tests() {
710
// In practice this is the same on every Unix.

src/test/ui/panics/abort-on-panic.rs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,41 +23,45 @@ extern "Rust" fn panic_in_rust_abi() {
2323
panic!("TestRust");
2424
}
2525

26-
fn test() {
27-
let _ = panic::catch_unwind(|| { panic_in_ffi(); });
28-
// The process should have aborted by now.
26+
fn should_have_aborted() {
2927
io::stdout().write(b"This should never be printed.\n");
3028
let _ = io::stdout().flush();
3129
}
3230

31+
fn test() {
32+
let _ = panic::catch_unwind(|| { panic_in_ffi(); });
33+
should_have_aborted();
34+
}
35+
3336
fn testrust() {
3437
let _ = panic::catch_unwind(|| { panic_in_rust_abi(); });
35-
// The process should have aborted by now.
36-
io::stdout().write(b"This should never be printed.\n");
37-
let _ = io::stdout().flush();
38+
should_have_aborted();
3839
}
3940

4041
fn main() {
42+
let tests: &[(_, fn())] = &[
43+
("test", test),
44+
("testrust", testrust),
45+
];
46+
4147
let args: Vec<String> = env::args().collect();
4248
if args.len() > 1 {
4349
// This is inside the self-executed command.
44-
match &*args[1] {
45-
"test" => return test(),
46-
"testrust" => return testrust(),
47-
_ => panic!("bad test"),
50+
for (a,f) in tests {
51+
if &args[1] == a { return f() }
4852
}
53+
panic!("bad test");
4954
}
5055

51-
// These end up calling the self-execution branches above.
52-
let mut p = Command::new(&args[0])
53-
.stdout(Stdio::piped())
54-
.stdin(Stdio::piped())
55-
.arg("test").spawn().unwrap();
56-
assert!(!p.wait().unwrap().success());
57-
58-
let mut p = Command::new(&args[0])
59-
.stdout(Stdio::piped())
60-
.stdin(Stdio::piped())
61-
.arg("testrust").spawn().unwrap();
62-
assert!(!p.wait().unwrap().success());
56+
let execute_self_expecting_abort = |arg| {
57+
let mut p = Command::new(&args[0])
58+
.stdout(Stdio::piped())
59+
.stdin(Stdio::piped())
60+
.arg(arg).spawn().unwrap();
61+
assert!(!p.wait().unwrap().success());
62+
};
63+
64+
for (a,_f) in tests {
65+
execute_self_expecting_abort(a);
66+
}
6367
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// run-pass
2+
// no-prefer-dynamic
3+
// ignore-wasm32-bare no libc
4+
// ignore-windows
5+
// ignore-sgx no libc
6+
// ignore-emscripten no processes
7+
// ignore-sgx no processes
8+
9+
#![feature(bench_black_box)]
10+
#![feature(rustc_private)]
11+
#![feature(never_type)]
12+
#![feature(panic_always_abort)]
13+
14+
extern crate libc;
15+
16+
use std::alloc::{GlobalAlloc, Layout};
17+
use std::fmt;
18+
use std::panic::{self, panic_any};
19+
use std::os::unix::process::{CommandExt, ExitStatusExt};
20+
use std::process::{self, Command, ExitStatus};
21+
use std::sync::atomic::{AtomicU32, Ordering};
22+
23+
use libc::c_int;
24+
25+
/// This stunt allocator allows us to spot heap allocations in the child.
26+
struct PidChecking<A> {
27+
parent: A,
28+
require_pid: AtomicU32,
29+
}
30+
31+
#[global_allocator]
32+
static ALLOCATOR: PidChecking<std::alloc::System> = PidChecking {
33+
parent: std::alloc::System,
34+
require_pid: AtomicU32::new(0),
35+
};
36+
37+
impl<A> PidChecking<A> {
38+
fn engage(&self) {
39+
let parent_pid = process::id();
40+
eprintln!("engaging allocator trap, parent pid={}", parent_pid);
41+
self.require_pid.store(parent_pid, Ordering::Release);
42+
}
43+
fn check(&self) {
44+
let require_pid = self.require_pid.load(Ordering::Acquire);
45+
if require_pid != 0 {
46+
let actual_pid = process::id();
47+
if require_pid != actual_pid {
48+
unsafe {
49+
libc::raise(libc::SIGTRAP);
50+
}
51+
}
52+
}
53+
}
54+
}
55+
56+
unsafe impl<A:GlobalAlloc> GlobalAlloc for PidChecking<A> {
57+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
58+
self.check();
59+
self.parent.alloc(layout)
60+
}
61+
62+
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
63+
self.check();
64+
self.parent.dealloc(ptr, layout)
65+
}
66+
67+
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
68+
self.check();
69+
self.parent.alloc_zeroed(layout)
70+
}
71+
72+
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
73+
self.check();
74+
self.parent.realloc(ptr, layout, new_size)
75+
}
76+
}
77+
78+
fn expect_aborted(status: ExitStatus) {
79+
dbg!(status);
80+
let signal = status.signal().expect("expected child process to die of signal");
81+
assert!(signal == libc::SIGABRT || signal == libc::SIGILL);
82+
}
83+
84+
fn main() {
85+
ALLOCATOR.engage();
86+
87+
fn run(do_panic: &dyn Fn()) -> ExitStatus {
88+
let child = unsafe { libc::fork() };
89+
assert!(child >= 0);
90+
if child == 0 {
91+
panic::always_abort();
92+
do_panic();
93+
process::exit(0);
94+
}
95+
let mut status: c_int = 0;
96+
let got = unsafe { libc::waitpid(child, &mut status, 0) };
97+
assert_eq!(got, child);
98+
let status = ExitStatus::from_raw(status.into());
99+
status
100+
}
101+
102+
fn one(do_panic: &dyn Fn()) {
103+
let status = run(do_panic);
104+
expect_aborted(status);
105+
}
106+
107+
one(&|| panic!());
108+
one(&|| panic!("some message"));
109+
one(&|| panic!("message with argument: {}", 42));
110+
111+
#[derive(Debug)]
112+
struct Wotsit { }
113+
one(&|| panic_any(Wotsit { }));
114+
115+
let mut c = Command::new("echo");
116+
unsafe {
117+
c.pre_exec(|| panic!("{}", "crash now!"));
118+
}
119+
let st = c.status().expect("failed to get command status");
120+
expect_aborted(st);
121+
122+
struct DisplayWithHeap;
123+
impl fmt::Display for DisplayWithHeap {
124+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
125+
let s = vec![0; 100];
126+
let s = std::hint::black_box(s);
127+
write!(f, "{:?}", s)
128+
}
129+
}
130+
131+
// Some panics in the stdlib that we want not to allocate, as
132+
// otherwise these facilities become impossible to use in the
133+
// child after fork, which is really quite awkward.
134+
135+
one(&|| { None::<DisplayWithHeap>.unwrap(); });
136+
one(&|| { None::<DisplayWithHeap>.expect("unwrapped a none"); });
137+
one(&|| { std::str::from_utf8(b"\xff").unwrap(); });
138+
one(&|| {
139+
let x = [0, 1, 2, 3];
140+
let y = x[std::hint::black_box(4)];
141+
let _z = std::hint::black_box(y);
142+
});
143+
144+
// Finally, check that our stunt allocator can actually catch an allocation after fork.
145+
// ie, that our test is effective.
146+
147+
let status = run(&|| panic!("allocating to display... {}", DisplayWithHeap));
148+
dbg!(status);
149+
assert_eq!(status.signal(), Some(libc::SIGTRAP));
150+
}

0 commit comments

Comments
 (0)