Skip to content

Commit ee79bfa

Browse files
committed
Abort on stack overflow instead of re-raising SIGSEGV
We use guard pages that cause the process to abort to protect against undefined behavior in the event of stack overflow. We have a handler that catches segfaults, prints out an error message if the segfault was due to a stack overflow, then unregisters itself and returns to allow the signal to be re-raised and kill the process. This caused some confusion, as it was unexpected that safe code would be able to cause a segfault, while it's easy to overflow the stack in safe code. To avoid this confusion, when we detect a segfault in the guard page, abort instead of the previous behavior of re-raising the SIGSEGV. To test this, we need to adapt the tests for segfault to actually check the exit status. Doing so revealed that the existing test for segfault behavior was actually invalid; LLVM optimizes the explicit null pointer reference down to an illegal instruction, so the program aborts with SIGILL instead of SIGSEGV and the test didn't actually trigger the signal handler at all. Use a C helper function to get a null pointer that LLVM can't optimize away, so we get our segfault instead. This is a [breaking-change] if anyone is relying on the exact signal raised to kill a process on stack overflow. Closes #31273
1 parent 303892e commit ee79bfa

File tree

4 files changed

+74
-20
lines changed

4 files changed

+74
-20
lines changed

src/libstd/sys/unix/stack_overflow.rs

+21-17
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,19 @@ mod imp {
8383
// were originally supposed to do.
8484
//
8585
// This handler currently exists purely to print an informative message
86-
// whenever a thread overflows its stack. When run the handler always
87-
// un-registers itself after running and then returns (to allow the original
88-
// signal to be delivered again). By returning we're ensuring that segfaults
89-
// do indeed look like segfaults.
86+
// whenever a thread overflows its stack. We then abort to exit and
87+
// indicate a crash, but to avoid a misleading SIGSEGV that might lead
88+
// users to believe that unsafe code has accessed an invalid pointer; the
89+
// SIGSEGV encountered when overflowing the stack is expected and
90+
// well-defined.
9091
//
91-
// Returning from this kind of signal handler is technically not defined to
92-
// work when reading the POSIX spec strictly, but in practice it turns out
93-
// many large systems and all implementations allow returning from a signal
94-
// handler to work. For a more detailed explanation see the comments on
95-
// #26458.
92+
// If this is not a stack overflow, the handler un-registers itself and
93+
// then returns (to allow the original signal to be delivered again).
94+
// Returning from this kind of signal handler is technically not defined
95+
// to work when reading the POSIX spec strictly, but in practice it turns
96+
// out many large systems and all implementations allow returning from a
97+
// signal handler to work. For a more detailed explanation see the
98+
// comments on #26458.
9699
unsafe extern fn signal_handler(signum: libc::c_int,
97100
info: *mut libc::siginfo_t,
98101
_data: *mut libc::c_void) {
@@ -102,17 +105,18 @@ mod imp {
102105
let addr = siginfo_si_addr(info);
103106

104107
// If the faulting address is within the guard page, then we print a
105-
// message saying so.
108+
// message saying so and abort.
106109
if guard != 0 && guard - PAGE_SIZE <= addr && addr < guard {
107110
report_overflow();
111+
rtabort!("stack overflow");
112+
} else {
113+
// Unregister ourselves by reverting back to the default behavior.
114+
let mut action: sigaction = mem::zeroed();
115+
action.sa_sigaction = SIG_DFL;
116+
sigaction(signum, &action, ptr::null_mut());
117+
118+
// See comment above for why this function returns.
108119
}
109-
110-
// Unregister ourselves by reverting back to the default behavior.
111-
let mut action: sigaction = mem::zeroed();
112-
action.sa_sigaction = SIG_DFL;
113-
sigaction(signum, &action, ptr::null_mut());
114-
115-
// See comment above for why this function returns.
116120
}
117121

118122
static mut MAIN_ALTSTACK: *mut libc::c_void = ptr::null_mut();

src/rt/rust_test_helpers.c

+5
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ rust_get_test_int() {
158158
return 1;
159159
}
160160

161+
char *
162+
rust_get_null_ptr() {
163+
return 0;
164+
}
165+
161166
/* Debug helpers strictly to verify ABI conformance.
162167
*
163168
* FIXME (#2665): move these into a testcase when the testsuite

src/test/run-pass/out-of-stack.rs

+25-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
// ignore-musl
1313

1414
#![feature(asm)]
15+
#![feature(libc)]
16+
17+
#[cfg(unix)]
18+
extern crate libc;
1519

1620
use std::env;
1721
use std::process::Command;
@@ -35,6 +39,24 @@ fn loud_recurse() {
3539
black_box(()); // don't optimize this into a tail call. please.
3640
}
3741

42+
#[cfg(unix)]
43+
fn check_status(status: std::process::ExitStatus)
44+
{
45+
use libc;
46+
use std::os::unix::process::ExitStatusExt;
47+
48+
assert!(!status.success());
49+
assert!(status.signal() != Some(libc::SIGSEGV)
50+
&& status.signal() != Some(libc::SIGBUS));
51+
}
52+
53+
#[cfg(not(unix))]
54+
fn check_status(status: std::process::ExitStatus)
55+
{
56+
assert!(!status.success());
57+
}
58+
59+
3860
fn main() {
3961
let args: Vec<String> = env::args().collect();
4062
if args.len() > 1 && args[1] == "silent" {
@@ -62,7 +84,9 @@ fn main() {
6284
println!("testing: {}", mode);
6385

6486
let silent = Command::new(&args[0]).arg(mode).output().unwrap();
65-
assert!(!silent.status.success());
87+
88+
check_status(silent.status);
89+
6690
let error = String::from_utf8_lossy(&silent.stderr);
6791
assert!(error.contains("has overflowed its stack"),
6892
"missing overflow message: {}", error);

src/test/run-pass/segfault-no-out-of-stack.rs

+23-2
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,39 @@ extern crate libc;
1515
use std::process::{Command, ExitStatus};
1616
use std::env;
1717

18+
#[link(name = "rust_test_helpers")]
19+
extern {
20+
fn rust_get_null_ptr() -> *mut ::libc::c_char;
21+
}
22+
23+
#[cfg(unix)]
24+
fn check_status(status: std::process::ExitStatus)
25+
{
26+
use libc;
27+
use std::os::unix::process::ExitStatusExt;
28+
29+
assert!(status.signal() == Some(libc::SIGSEGV)
30+
|| status.signal() == Some(libc::SIGBUS));
31+
}
32+
33+
#[cfg(not(unix))]
34+
fn check_status(status: std::process::ExitStatus)
35+
{
36+
assert!(!status.success());
37+
}
38+
1839
fn main() {
1940
let args: Vec<String> = env::args().collect();
2041
if args.len() > 1 && args[1] == "segfault" {
21-
unsafe { *(0 as *mut isize) = 1 }; // trigger a segfault
42+
unsafe { *rust_get_null_ptr() = 1; }; // trigger a segfault
2243
} else {
2344
let segfault = Command::new(&args[0]).arg("segfault").output().unwrap();
2445
let stderr = String::from_utf8_lossy(&segfault.stderr);
2546
let stdout = String::from_utf8_lossy(&segfault.stdout);
2647
println!("stdout: {}", stdout);
2748
println!("stderr: {}", stderr);
2849
println!("status: {}", segfault.status);
29-
assert!(!segfault.status.success());
50+
check_status(segfault.status);
3051
assert!(!stderr.contains("has overflowed its stack"));
3152
}
3253
}

0 commit comments

Comments
 (0)