Skip to content

Commit f91b634

Browse files
committed
Auto merge of #110975 - Amanieu:panic_count, r=joshtriplett
Rework handling of recursive panics This PR makes 2 changes to how recursive panics works (a panic while handling a panic). 1. The panic count is no longer used to determine whether to force an immediate abort. This allows code like the following to work without aborting the process immediately: ```rust struct Double; impl Drop for Double { fn drop(&mut self) { // 2 panics are active at once, but this is fine since it is caught. std::panic::catch_unwind(|| panic!("twice")); } } let _d = Double; panic!("once"); ``` Rustc already generates appropriate code so that any exceptions escaping out of a `Drop` called in the unwind path will immediately abort the process. 2. Any panics while the panic hook is executing will force an immediate abort. This is necessary to avoid potential deadlocks like #110771 where a panic happens while holding the backtrace lock. We don't even try to print the panic message in this case since the panic may have been caused by `Display` impls. Fixes #110771
2 parents 82b311b + de607f1 commit f91b634

File tree

9 files changed

+149
-86
lines changed

9 files changed

+149
-86
lines changed

library/std/src/panicking.rs

+67-53
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,18 @@ pub mod panic_count {
298298

299299
pub const ALWAYS_ABORT_FLAG: usize = 1 << (usize::BITS - 1);
300300

301-
// Panic count for the current thread.
302-
thread_local! { static LOCAL_PANIC_COUNT: Cell<usize> = const { Cell::new(0) } }
301+
/// A reason for forcing an immediate abort on panic.
302+
#[derive(Debug)]
303+
pub enum MustAbort {
304+
AlwaysAbort,
305+
PanicInHook,
306+
}
307+
308+
// Panic count for the current thread and whether a panic hook is currently
309+
// being executed..
310+
thread_local! {
311+
static LOCAL_PANIC_COUNT: Cell<(usize, bool)> = const { Cell::new((0, false)) }
312+
}
303313

304314
// Sum of panic counts from all threads. The purpose of this is to have
305315
// a fast path in `count_is_zero` (which is used by `panicking`). In any particular
@@ -328,34 +338,39 @@ pub mod panic_count {
328338
// panicking thread consumes at least 2 bytes of address space.
329339
static GLOBAL_PANIC_COUNT: AtomicUsize = AtomicUsize::new(0);
330340

331-
// Return the state of the ALWAYS_ABORT_FLAG and number of panics.
341+
// Increases the global and local panic count, and returns whether an
342+
// immediate abort is required.
332343
//
333-
// If ALWAYS_ABORT_FLAG is not set, the number is determined on a per-thread
334-
// base (stored in LOCAL_PANIC_COUNT), i.e. it is the amount of recursive calls
335-
// of the calling thread.
336-
// If ALWAYS_ABORT_FLAG is set, the number equals the *global* number of panic
337-
// calls. See above why LOCAL_PANIC_COUNT is not used.
338-
pub fn increase() -> (bool, usize) {
344+
// This also updates thread-local state to keep track of whether a panic
345+
// hook is currently executing.
346+
pub fn increase(run_panic_hook: bool) -> Option<MustAbort> {
339347
let global_count = GLOBAL_PANIC_COUNT.fetch_add(1, Ordering::Relaxed);
340-
let must_abort = global_count & ALWAYS_ABORT_FLAG != 0;
341-
let panics = if must_abort {
342-
global_count & !ALWAYS_ABORT_FLAG
343-
} else {
344-
LOCAL_PANIC_COUNT.with(|c| {
345-
let next = c.get() + 1;
346-
c.set(next);
347-
next
348-
})
349-
};
350-
(must_abort, panics)
348+
if global_count & ALWAYS_ABORT_FLAG != 0 {
349+
return Some(MustAbort::AlwaysAbort);
350+
}
351+
352+
LOCAL_PANIC_COUNT.with(|c| {
353+
let (count, in_panic_hook) = c.get();
354+
if in_panic_hook {
355+
return Some(MustAbort::PanicInHook);
356+
}
357+
c.set((count + 1, run_panic_hook));
358+
None
359+
})
360+
}
361+
362+
pub fn finished_panic_hook() {
363+
LOCAL_PANIC_COUNT.with(|c| {
364+
let (count, _) = c.get();
365+
c.set((count, false));
366+
});
351367
}
352368

353369
pub fn decrease() {
354370
GLOBAL_PANIC_COUNT.fetch_sub(1, Ordering::Relaxed);
355371
LOCAL_PANIC_COUNT.with(|c| {
356-
let next = c.get() - 1;
357-
c.set(next);
358-
next
372+
let (count, _) = c.get();
373+
c.set((count - 1, false));
359374
});
360375
}
361376

@@ -366,7 +381,7 @@ pub mod panic_count {
366381
// Disregards ALWAYS_ABORT_FLAG
367382
#[must_use]
368383
pub fn get_count() -> usize {
369-
LOCAL_PANIC_COUNT.with(|c| c.get())
384+
LOCAL_PANIC_COUNT.with(|c| c.get().0)
370385
}
371386

372387
// Disregards ALWAYS_ABORT_FLAG
@@ -394,7 +409,7 @@ pub mod panic_count {
394409
#[inline(never)]
395410
#[cold]
396411
fn is_zero_slow_path() -> bool {
397-
LOCAL_PANIC_COUNT.with(|c| c.get() == 0)
412+
LOCAL_PANIC_COUNT.with(|c| c.get().0 == 0)
398413
}
399414
}
400415

@@ -655,23 +670,22 @@ fn rust_panic_with_hook(
655670
location: &Location<'_>,
656671
can_unwind: bool,
657672
) -> ! {
658-
let (must_abort, panics) = panic_count::increase();
659-
660-
// If this is the third nested call (e.g., panics == 2, this is 0-indexed),
661-
// the panic hook probably triggered the last panic, otherwise the
662-
// double-panic check would have aborted the process. In this case abort the
663-
// process real quickly as we don't want to try calling it again as it'll
664-
// probably just panic again.
665-
if must_abort || panics > 2 {
666-
if panics > 2 {
667-
// Don't try to print the message in this case
668-
// - perhaps that is causing the recursive panics.
669-
rtprintpanic!("thread panicked while processing panic. aborting.\n");
670-
} else {
671-
// Unfortunately, this does not print a backtrace, because creating
672-
// a `Backtrace` will allocate, which we must to avoid here.
673-
let panicinfo = PanicInfo::internal_constructor(message, location, can_unwind);
674-
rtprintpanic!("{panicinfo}\npanicked after panic::always_abort(), aborting.\n");
673+
let must_abort = panic_count::increase(true);
674+
675+
// Check if we need to abort immediately.
676+
if let Some(must_abort) = must_abort {
677+
match must_abort {
678+
panic_count::MustAbort::PanicInHook => {
679+
// Don't try to print the message in this case
680+
// - perhaps that is causing the recursive panics.
681+
rtprintpanic!("thread panicked while processing panic. aborting.\n");
682+
}
683+
panic_count::MustAbort::AlwaysAbort => {
684+
// Unfortunately, this does not print a backtrace, because creating
685+
// a `Backtrace` will allocate, which we must to avoid here.
686+
let panicinfo = PanicInfo::internal_constructor(message, location, can_unwind);
687+
rtprintpanic!("{panicinfo}\npanicked after panic::always_abort(), aborting.\n");
688+
}
675689
}
676690
crate::sys::abort_internal();
677691
}
@@ -697,16 +711,16 @@ fn rust_panic_with_hook(
697711
};
698712
drop(hook);
699713

700-
if panics > 1 || !can_unwind {
701-
// If a thread panics while it's already unwinding then we
702-
// have limited options. Currently our preference is to
703-
// just abort. In the future we may consider resuming
704-
// unwinding or otherwise exiting the thread cleanly.
705-
if !can_unwind {
706-
rtprintpanic!("thread caused non-unwinding panic. aborting.\n");
707-
} else {
708-
rtprintpanic!("thread panicked while panicking. aborting.\n");
709-
}
714+
// Indicate that we have finished executing the panic hook. After this point
715+
// it is fine if there is a panic while executing destructors, as long as it
716+
// it contained within a `catch_unwind`.
717+
panic_count::finished_panic_hook();
718+
719+
if !can_unwind {
720+
// If a thread panics while running destructors or tries to unwind
721+
// through a nounwind function (e.g. extern "C") then we cannot continue
722+
// unwinding and have to abort immediately.
723+
rtprintpanic!("thread caused non-unwinding panic. aborting.\n");
710724
crate::sys::abort_internal();
711725
}
712726

@@ -716,7 +730,7 @@ fn rust_panic_with_hook(
716730
/// This is the entry point for `resume_unwind`.
717731
/// It just forwards the payload to the panic runtime.
718732
pub fn rust_panic_without_hook(payload: Box<dyn Any + Send>) -> ! {
719-
panic_count::increase();
733+
panic_count::increase(false);
720734

721735
struct RewrapBox(Box<dyn Any + Send>);
722736

src/tools/miri/src/concurrency/thread.rs

+13-6
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,15 @@ pub struct Thread<'mir, 'tcx> {
133133
/// The join status.
134134
join_status: ThreadJoinStatus,
135135

136-
/// The temporary used for storing the argument of
137-
/// the call to `miri_start_panic` (the panic payload) when unwinding.
136+
/// Stack of active panic payloads for the current thread. Used for storing
137+
/// the argument of the call to `miri_start_panic` (the panic payload) when unwinding.
138138
/// This is pointer-sized, and matches the `Payload` type in `src/libpanic_unwind/miri.rs`.
139-
pub(crate) panic_payload: Option<Scalar<Provenance>>,
139+
///
140+
/// In real unwinding, the payload gets passed as an argument to the landing pad,
141+
/// which then forwards it to 'Resume'. However this argument is implicit in MIR,
142+
/// so we have to store it out-of-band. When there are multiple active unwinds,
143+
/// the innermost one is always caught first, so we can store them as a stack.
144+
pub(crate) panic_payloads: Vec<Scalar<Provenance>>,
140145

141146
/// Last OS error location in memory. It is a 32-bit integer.
142147
pub(crate) last_error: Option<MPlaceTy<'tcx, Provenance>>,
@@ -206,7 +211,7 @@ impl<'mir, 'tcx> Thread<'mir, 'tcx> {
206211
stack: Vec::new(),
207212
top_user_relevant_frame: None,
208213
join_status: ThreadJoinStatus::Joinable,
209-
panic_payload: None,
214+
panic_payloads: Vec::new(),
210215
last_error: None,
211216
on_stack_empty,
212217
}
@@ -216,7 +221,7 @@ impl<'mir, 'tcx> Thread<'mir, 'tcx> {
216221
impl VisitTags for Thread<'_, '_> {
217222
fn visit_tags(&self, visit: &mut dyn FnMut(BorTag)) {
218223
let Thread {
219-
panic_payload,
224+
panic_payloads: panic_payload,
220225
last_error,
221226
stack,
222227
top_user_relevant_frame: _,
@@ -226,7 +231,9 @@ impl VisitTags for Thread<'_, '_> {
226231
on_stack_empty: _, // we assume the closure captures no GC-relevant state
227232
} = self;
228233

229-
panic_payload.visit_tags(visit);
234+
for payload in panic_payload {
235+
payload.visit_tags(visit);
236+
}
230237
last_error.visit_tags(visit);
231238
for frame in stack {
232239
frame.visit_tags(visit)

src/tools/miri/src/shims/panic.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
6363
let [payload] = this.check_shim(abi, Abi::Rust, link_name, args)?;
6464
let payload = this.read_scalar(payload)?;
6565
let thread = this.active_thread_mut();
66-
assert!(thread.panic_payload.is_none(), "the panic runtime should avoid double-panics");
67-
thread.panic_payload = Some(payload);
66+
thread.panic_payloads.push(payload);
6867

6968
// Jump to the unwind block to begin unwinding.
7069
this.unwind_to_block(unwind)?;
@@ -146,7 +145,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
146145

147146
// The Thread's `panic_payload` holds what was passed to `miri_start_panic`.
148147
// This is exactly the second argument we need to pass to `catch_fn`.
149-
let payload = this.active_thread_mut().panic_payload.take().unwrap();
148+
let payload = this.active_thread_mut().panic_payloads.pop().unwrap();
150149

151150
// Push the `catch_fn` stackframe.
152151
let f_instance = this.get_ptr_fn(catch_unwind.catch_fn)?.as_instance()?;

src/tools/miri/tests/fail/panic/double_panic.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
//@error-in-other-file: the program aborted
21
//@normalize-stderr-test: "\| +\^+" -> "| ^"
3-
//@normalize-stderr-test: "unsafe \{ libc::abort\(\) \}|crate::intrinsics::abort\(\);" -> "ABORT();"
42
//@normalize-stderr-test: "\n +[0-9]+:[^\n]+" -> "$1"
53
//@normalize-stderr-test: "\n at [^\n]+" -> "$1"
64

@@ -11,6 +9,7 @@ impl Drop for Foo {
119
}
1210
}
1311
fn main() {
12+
//~^ERROR: panic in a function that cannot unwind
1413
let _foo = Foo;
1514
panic!("first");
1615
}

src/tools/miri/tests/fail/panic/double_panic.stderr

+8-21
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,17 @@ thread 'main' panicked at 'first', $DIR/double_panic.rs:LL:CC
22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
33
thread 'main' panicked at 'second', $DIR/double_panic.rs:LL:CC
44
stack backtrace:
5-
thread panicked while panicking. aborting.
6-
error: abnormal termination: the program aborted execution
7-
--> RUSTLIB/std/src/sys/PLATFORM/mod.rs:LL:CC
8-
|
9-
LL | ABORT();
10-
| ^ the program aborted execution
11-
|
12-
= note: inside `std::sys::PLATFORM::abort_internal` at RUSTLIB/std/src/sys/PLATFORM/mod.rs:LL:CC
13-
= note: inside `std::panicking::rust_panic_with_hook` at RUSTLIB/std/src/panicking.rs:LL:CC
14-
= note: inside closure at RUSTLIB/std/src/panicking.rs:LL:CC
15-
= note: inside `std::sys_common::backtrace::__rust_end_short_backtrace::<[closure@std::panicking::begin_panic_handler::{closure#0}], !>` at RUSTLIB/std/src/sys_common/backtrace.rs:LL:CC
16-
= note: inside `std::panicking::begin_panic_handler` at RUSTLIB/std/src/panicking.rs:LL:CC
17-
note: inside `<Foo as std::ops::Drop>::drop`
5+
error: abnormal termination: panic in a function that cannot unwind
186
--> $DIR/double_panic.rs:LL:CC
197
|
20-
LL | panic!("second");
21-
| ^
22-
= note: inside `std::ptr::drop_in_place::<Foo> - shim(Some(Foo))` at RUSTLIB/core/src/ptr/mod.rs:LL:CC
23-
note: inside `main`
24-
--> $DIR/double_panic.rs:LL:CC
8+
LL | / fn main() {
9+
LL | |
10+
LL | | let _foo = Foo;
11+
LL | | panic!("first");
12+
LL | | }
13+
| |_^ panic in a function that cannot unwind
2514
|
26-
LL | }
27-
| ^
28-
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `panic` (in Nightly builds, run with -Z macro-backtrace for more info)
15+
= note: inside `main` at $DIR/double_panic.rs:LL:CC
2916

3017
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
3118

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//@normalize-stderr-test: "\| +\^+" -> "| ^"
2+
//@normalize-stderr-test: "\n +[0-9]+:[^\n]+" -> "$1"
3+
//@normalize-stderr-test: "\n at [^\n]+" -> "$1"
4+
5+
// Checks that nested panics work correctly.
6+
7+
use std::panic::catch_unwind;
8+
9+
fn double() {
10+
struct Double;
11+
12+
impl Drop for Double {
13+
fn drop(&mut self) {
14+
let _ = catch_unwind(|| panic!("twice"));
15+
}
16+
}
17+
18+
let _d = Double;
19+
20+
panic!("once");
21+
}
22+
23+
fn main() {
24+
assert!(catch_unwind(|| double()).is_err());
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
thread 'main' panicked at 'once', $DIR/nested_panic_caught.rs:LL:CC
2+
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
3+
thread 'main' panicked at 'twice', $DIR/nested_panic_caught.rs:LL:CC
4+
stack backtrace:

tests/ui/backtrace.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,17 @@ fn runtest(me: &str) {
104104
"bad output3: {}", s);
105105

106106
// Make sure a stack trace isn't printed too many times
107+
//
108+
// Currently it is printed 3 times ("once", "twice" and "panic in a
109+
// function that cannot unwind") but in the future the last one may be
110+
// removed.
107111
let p = template(me).arg("double-fail")
108112
.env("RUST_BACKTRACE", "1").spawn().unwrap();
109113
let out = p.wait_with_output().unwrap();
110114
assert!(!out.status.success());
111115
let s = str::from_utf8(&out.stderr).unwrap();
112116
let mut i = 0;
113-
for _ in 0..2 {
117+
for _ in 0..3 {
114118
i += s[i + 10..].find("stack backtrace").unwrap() + 10;
115119
}
116120
assert!(s[i + 10..].find("stack backtrace").is_none(),
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// run-pass
2+
// needs-unwind
3+
4+
// Checks that nested panics work correctly.
5+
6+
use std::panic::catch_unwind;
7+
8+
fn double() {
9+
struct Double;
10+
11+
impl Drop for Double {
12+
fn drop(&mut self) {
13+
let _ = catch_unwind(|| panic!("twice"));
14+
}
15+
}
16+
17+
let _d = Double;
18+
19+
panic!("once");
20+
}
21+
22+
fn main() {
23+
assert!(catch_unwind(|| double()).is_err());
24+
}

0 commit comments

Comments
 (0)