Skip to content

Deadlock when capturing a backtrace from allocator during panic with test output capturing enabled #130187

Open
@Nemo157

Description

@Nemo157

I tried this code (minimized from a larger testcase):

use std::alloc::{GlobalAlloc, Layout};
use std::cell::Cell;
use std::backtrace::Backtrace;
use std::thread_local;

thread_local! {
    static CAN_ALLOCATE: Cell<bool> = const { Cell::new(true) };
}

#[derive(Debug)]
pub struct NoAllocate(std::alloc::System);

unsafe impl GlobalAlloc for NoAllocate {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        if !CAN_ALLOCATE.replace(true) {
            let _ =  Backtrace::force_capture();
        }
        self.0.alloc(layout)
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        self.0.dealloc(ptr, layout);
    }
}

#[global_allocator]
static GLOBAL: NoAllocate = NoAllocate(std::alloc::System);

#[test]
#[should_panic]
fn main() {
    CAN_ALLOCATE.set(false);
    panic!();
}

Compiled with rustc --test main.rs.

I expected to see this happen: the test successfully panics and exits.

Instead, this happened: the test deadlocks.

Relevant section of backtrace:

...
#4  std::sync::mutex::Mutex::lock<()> () at std/src/sync/mutex.rs:317
#5  std::sys::backtrace::lock () at std/src/sys/backtrace.rs:18
#6  std::backtrace::Backtrace::create () at std/src/backtrace.rs:326
#7  0x0000555555585b25 in std::backtrace::Backtrace::force_capture () at std/src/backtrace.rs:312
#8  0x000055555556ab52 in <main::NoAllocate as core::alloc::global::GlobalAlloc>::alloc ()
#9  0x000055555556ac45 in __rust_alloc ()
...
#15 alloc::vec::Vec::reserve<u8, alloc::alloc::Global> () at alloc/src/vec/mod.rs:973
...
#22 0x00005555555879a3 in std::io::Write::write_fmt<alloc::vec::Vec<u8, alloc::alloc::Global>> () at std/src/io/mod.rs:1823
#23 0x000055555558aeb7 in std::panicking::default_hook::{closure#1} () at std/src/panicking.rs:256

This is specifically related to output capturing from the test runner, running the same code as a non-test binary or with --nocapture works perfectly.

Meta

This worked in 1.80.1 and nightly-2024-07-13, it started failing in 1.81.0 and nightly-2024-07-14.

The deadlock was introduced by #127397 (cc @jyn514).

The lock is first taken when starting to print from the default panic hook:

let mut lock = backtrace::lock();

The first print to the output then happens:

let _ = writeln!(err, "thread '{name}' panicked at {location}:\n{msg}");

For captured output this requires then reallocating the Vec storing the capture, so it calls into the allocator, hitting the Backtrace::force_capture because this is the first allocation in the test. This attempts to re-entrantly acquire the lock a second time, deadlocking:

let _lock = lock();

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-backtraceArea: BacktracesA-libtestArea: `#[test]` / the `test` libraryC-bugCategory: This is a bug.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions