diff --git a/Cargo.lock b/Cargo.lock index 1ea7dc3..251d2d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,14 @@ dependencies = [ "unwinding", ] +[[package]] +name = "forced_unwind" +version = "0.1.0" +dependencies = [ + "libc", + "unwinding", +] + [[package]] name = "gimli" version = "0.34.0" diff --git a/Cargo.toml b/Cargo.toml index e39c204..d08955c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "test_crates/catch_std_exception", "test_crates/std_catch_exception", "test_crates/panic_abort_no_debuginfo", + "test_crates/forced_unwind", ] [dependencies] diff --git a/test_crates/forced_unwind/Cargo.toml b/test_crates/forced_unwind/Cargo.toml new file mode 100644 index 0000000..9028c9a --- /dev/null +++ b/test_crates/forced_unwind/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "forced_unwind" +version = "0.1.0" +edition = "2024" + +[dependencies] +unwinding = { path = "../..", features = ["system-alloc", "personality", "panic-handler"] } +libc = "0.2" diff --git a/test_crates/forced_unwind/check.sh b/test_crates/forced_unwind/check.sh new file mode 100755 index 0000000..a2302c4 --- /dev/null +++ b/test_crates/forced_unwind/check.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -o pipefail +trap "rm -f run.log" EXIT +${CARGO:-cargo} run --release $BUILD_STD 2>&1 | tee run.log +if [ $? -ne 0 ]; then + echo process did not exit successfully + exit 1 +fi +grep -Pz 'forced unwind reached end of stack with cleanups run' run.log diff --git a/test_crates/forced_unwind/src/main.rs b/test_crates/forced_unwind/src/main.rs new file mode 100644 index 0000000..95c3c9e --- /dev/null +++ b/test_crates/forced_unwind/src/main.rs @@ -0,0 +1,112 @@ +#![no_std] +#![no_main] + +extern crate alloc; +extern crate unwinding; + +use alloc::boxed::Box; +use core::ffi::{c_int, c_void}; +use core::mem::MaybeUninit; +use core::ptr; +use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; + +use unwinding::abi::*; +use unwinding::print::*; + +#[link(name = "c")] +unsafe extern "C" {} + +static STOP_ARG: u8 = 0; + +static EXCEPTION: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + +static DROPPED: AtomicBool = AtomicBool::new(false); + +static EXCEPTION_FREED: AtomicBool = AtomicBool::new(false); + +struct RecordOnDrop; + +impl Drop for RecordOnDrop { + fn drop(&mut self) { + DROPPED.store(true, Ordering::Relaxed); + } +} + +unsafe extern "C" fn exception_cleanup(_code: UnwindReasonCode, _exception: *mut UnwindException) { + EXCEPTION_FREED.store(true, Ordering::Relaxed); +} + +unsafe extern "C" fn stop_fn( + version: c_int, + actions: UnwindAction, + _exception_class: u64, + exception: *mut UnwindException, + _unwind_ctx: &mut UnwindContext<'_>, + arg: *mut c_void, +) -> UnwindReasonCode { + assert_eq!(version, 1); + assert_eq!( + arg, &raw const STOP_ARG as *mut c_void, + "stop argument not passed through" + ); + assert_eq!( + exception, + EXCEPTION.load(Ordering::Relaxed), + "exception object not passed through" + ); + assert!( + actions.contains(UnwindAction::FORCE_UNWIND), + "FORCE_UNWIND not set" + ); + assert!( + actions.contains(UnwindAction::CLEANUP_PHASE), + "CLEANUP_PHASE not set" + ); + + if actions.contains(UnwindAction::END_OF_STACK) { + // Unwinder is at end of stack, so `_d` should have been cleaned up. + assert!( + DROPPED.load(Ordering::Relaxed), + "reached end of stack but the cleanup never ran" + ); + // Unwinder must not have freed our exception. + assert!( + !EXCEPTION_FREED.load(Ordering::Relaxed), + "unwinder called the exception cleanup routine during forced unwind" + ); + eprintln!("forced unwind reached end of stack with cleanups run"); + unsafe { libc::exit(0) }; + } + // Not at end of stack yet, keep unwinding. + UnwindReasonCode::NO_REASON +} + +fn foo() { + // Leak the box so it outlives `foo()`. + let exception: *mut UnwindException = + Box::into_raw(Box::new(MaybeUninit::::zeroed())).cast(); + unsafe { + // Made-up class so the exception stays foreign and nothing reads into it. + (*exception).exception_class = u64::from_ne_bytes(*b"TESTRUST"); + (*exception).exception_cleanup = Some(exception_cleanup); + } + EXCEPTION.store(exception, Ordering::Relaxed); + + let arg = &raw const STOP_ARG as *mut c_void; + let code = unsafe { _Unwind_ForcedUnwind(exception, stop_fn, arg) }; + + // The stop function exits at end of stack, so reaching here means the test failed. + panic!("forced unwind returned unexpectedly: {}", code.0); +} + +fn main() { + let _d = RecordOnDrop; + foo(); +} + +// `C-unwind` lets the unwind propagate past `main`, to the end of the stack. +#[unsafe(export_name = "main")] +extern "C-unwind" fn start(_argc: isize, _argv: *const *const u8) -> isize { + main(); + 0 +} diff --git a/tests/compile_tests.rs b/tests/compile_tests.rs index d672a05..6da2598 100644 --- a/tests/compile_tests.rs +++ b/tests/compile_tests.rs @@ -9,6 +9,7 @@ fn main() { "catch_std_exception", "std_catch_exception", "panic_abort_no_debuginfo", + "forced_unwind", ]; for test in tests {