Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions test_crates/forced_unwind/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
9 changes: 9 additions & 0 deletions test_crates/forced_unwind/check.sh
Original file line number Diff line number Diff line change
@@ -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
112 changes: 112 additions & 0 deletions test_crates/forced_unwind/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<UnwindException> = 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::<UnwindException>::zeroed())).cast();
unsafe {
// Made-up class so the exception stays foreign and nothing reads into it.
(*exception).exception_class = u64::from_ne_bytes(*b"TSTFRCEU");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this string?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was experimenting with a catch_unwind here. With MOZ\0RUST the catch would read canary/payload, which we never allocate hence UB.
Dropped catch_unwind anyway since it aborts instead of reaching end of stack, and I'd rather keep this test on that path.
The made-up class just keeps it foreign so nothing reads into it.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still be a meaningful string though, e.g. "TESTRUST"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

(*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
}
1 change: 1 addition & 0 deletions tests/compile_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fn main() {
"catch_std_exception",
"std_catch_exception",
"panic_abort_no_debuginfo",
"forced_unwind",
];

for test in tests {
Expand Down