Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ large_stack_arrays = "deny"
[profile.release]
lto = "thin"
codegen-units = 1
panic = "abort"
panic = "unwind"
overflow-checks = true
strip = "debuginfo"

Expand Down
2 changes: 2 additions & 0 deletions crates/bemo-ffi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[package]
# Path-based embedders must inherit Bemo's workspace, not the consumer's.
workspace = "../.."
name = "bemo-ffi"
description = "Shared C ABI for Bemo FFM and Native Image bindings"
version.workspace = true
Expand Down
115 changes: 115 additions & 0 deletions crates/bemo-ffi/src/guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! Panic containment at the process-wide C boundary.
//!
//! A panic may leave shared handle state partially updated. Later entries refuse admission;
//! resources are deliberately retained rather than attempting cleanup through damaged state.
//! Calls admitted before the poison is published may still finish. This is not rollback.

use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, Ordering};

// Transport errors -1..-11 and OS errors <= -1000 retain their existing meanings.
const PANIC: i32 = -900;
const POISONED: i32 = -901;

pub(crate) static BOUNDARY: Boundary = Boundary::new();

pub(crate) struct Boundary {
poisoned: AtomicBool,
}

pub(crate) trait Failure: Sized {
fn failure(status: i32) -> Self;
}

impl Failure for i32 {
fn failure(status: i32) -> Self {
status
}
}
impl Failure for i64 {
fn failure(status: i32) -> Self {
i64::from(status)
}
}
impl Failure for u32 {
fn failure(_: i32) -> Self {
0
}
}
impl Failure for u64 {
fn failure(_: i32) -> Self {
0
}
}

impl Boundary {
const fn new() -> Self {
Self {
poisoned: AtomicBool::new(false),
}
}

pub(crate) fn call<T: Failure>(&self, action: impl FnOnce() -> T) -> T {
if self.poisoned.load(Ordering::Acquire) {
return T::failure(POISONED);
}
match catch_unwind(AssertUnwindSafe(action)) {
Ok(result) => result,
Err(_) => {
self.poisoned.store(true, Ordering::Release);
T::failure(PANIC)
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn preserves_values_and_contains_panic_before_poisoning() {
let boundary = Boundary::new();
assert_eq!(boundary.call(|| 42_i32), 42);
assert_eq!(boundary.call(|| 123_u64), 123);
assert_eq!(boundary.call::<i32>(|| panic!("boundary probe")), PANIC);
assert_eq!(boundary.call::<i32>(|| panic!("must never run")), POISONED);
assert_eq!(boundary.call(|| 1_i64), i64::from(POISONED));
assert_eq!(boundary.call(|| 1_u64), 0);
assert_eq!(boundary.call(|| 1_u32), 0);
}

#[test]
fn a_handle_failure_returns_zero_and_poison_is_visible_across_threads() {
let boundary = Boundary::new();
assert_eq!(boundary.call::<u64>(|| panic!("handle probe")), 0);
std::thread::scope(|scope| {
scope
.spawn(|| {
assert_eq!(boundary.call::<i32>(|| panic!("must never run")), POISONED);
})
.join()
.unwrap();
});
}

#[test]
fn unwinding_drops_owned_temporaries_before_returning() {
struct Dropped<'a>(&'a AtomicBool);
impl Drop for Dropped<'_> {
fn drop(&mut self) {
self.0.store(true, Ordering::Relaxed);
}
}
let boundary = Boundary::new();
let dropped = AtomicBool::new(false);
assert_eq!(
boundary.call::<i32>(|| {
let _temporary = Dropped(&dropped);
panic!("cleanup probe");
}),
PANIC
);
assert!(dropped.load(Ordering::Relaxed));
}
}
3 changes: 2 additions & 1 deletion crates/bemo-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ mod tests {
#[test]
fn boundary_matches_core() {
assert_eq!(bemo_abi_version(), bemo::ABI_VERSION);
assert_eq!(bemo_capabilities(), bemo::CAP_TRANSPORT_V3 | bemo::CAP_NATIVE_ALLOC);
assert_eq!(bemo_capabilities(), bemo::CAPABILITIES);
}
}

mod guard;
mod transport;
Loading
Loading