Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
111 changes: 111 additions & 0 deletions test_crates/forced_unwind/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#![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::SeqCst);
}
}

unsafe extern "C" fn exception_cleanup(_code: UnwindReasonCode, _exception: *mut UnwindException) {
EXCEPTION_FREED.store(true, Ordering::SeqCst);
}

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, &STOP_ARG as *const u8 as *mut c_void,

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.

Suggested change
arg, &STOP_ARG as *const u8 as *mut c_void,
arg, &raw const STOP_ARG as *mut c_void,

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

"stop argument not passed through"
);
assert_eq!(
exception,
EXCEPTION.load(Ordering::SeqCst),

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.

Why SeqCst?

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.

no real reason, just started out with SeqCst without thinking about it much. Switched to 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::SeqCst),
"reached end of stack but the cleanup never ran"
);
// Unwinder must not have freed our exception.
assert!(
!EXCEPTION_FREED.load(Ordering::SeqCst),
"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::leak(Box::new(MaybeUninit::<UnwindException>::zeroed())).as_mut_ptr();

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.

Suggested change
Box::leak(Box::new(MaybeUninit::<UnwindException>::zeroed())).as_mut_ptr();
Box::into_raw(Box::new(MaybeUninit::<UnwindException>::zeroed()));

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

unsafe {
(*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::SeqCst);

let arg = &STOP_ARG as *const u8 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