Skip to content

Commit 752a8a5

Browse files
committed
test(rmw): exercise the reachable unwind path, not an FFI panic
A panic out of an `extern "C"` callback aborts rather than unwinding, so the previous test killed the test binary (rc=101) instead of asserting anything -- and took the other tests' results with it. The reachable unwind is Rust-side, before the boundary: `invoke_user_callback!`'s debug assertion. Raise it directly and keep asserting what matters, that the dispatch registration is released so a later set() cannot block forever.
1 parent e87a722 commit 752a8a5

1 file changed

Lines changed: 40 additions & 14 deletions

File tree

crates/rmw-zenoh-rs/src/exec_callback.rs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@
6262
//! thread must not wait — waiting for itself is the deadlock. A `set` on an
6363
//! unrelated thread must wait, because it is the one about to free the pointer.
6464
//!
65+
//! ## What this still does not survive
66+
//!
67+
//! Two threads *both* dispatching this entity's callback *and* both re-entering
68+
//! `set` from inside it will wait on each other. Each is in a live dispatch that
69+
//! may still touch its `user_data` after `set` returns, so neither wait can
70+
//! safely be skipped.
71+
//!
72+
//! Stated rather than hidden, because it is a real residual — but it is not a
73+
//! regression against the reference. `rmw_zenoh_cpp` cannot survive re-entrant
74+
//! `set` at all: `set_callback` takes `event_mutex_` (and replays its backlog
75+
//! under it), so a callback dispatched from `trigger_callback` that calls
76+
//! `set_callback` re-locks a non-recursive `std::mutex` on the thread that
77+
//! already holds it. That deadlocks with one thread. This deadlocks only with
78+
//! two threads mutually re-entering, and the single-threaded case — the one
79+
//! rclcpp actually exercises when an executor detaches from inside a
80+
//! notification — is the case this type makes work.
81+
//!
6582
//! # Enforcement
6683
//!
6784
//! The mutex is a [`hiroz::reentrancy::TrackedMutex`], so its guards are counted
@@ -238,7 +255,6 @@ impl ExecCallback {
238255
/// in a single call, which is what lets an executor that attaches after
239256
/// messages have already arrived — the common startup race — see them.
240257
///
241-
/// [`notify_one`]: Self::notify_one
242258
/// # Blocking
243259
///
244260
/// This returns only once every dispatch that captured the *outgoing*
@@ -248,6 +264,8 @@ impl ExecCallback {
248264
/// `rmw_zenoh_cpp` has the same property, where the wait is on
249265
/// `event_mutex_` instead. A callback re-entering on its own thread never
250266
/// waits.
267+
///
268+
/// [`notify_one`]: Self::notify_one
251269
pub fn set(&self, callback: Option<ExecCallbackFn>, user_data: *mut crate::c_void) {
252270
// Collect under the lock, registering the replay — if there is one —
253271
// before releasing it, for the same reason `notify_one` does: a
@@ -615,25 +633,33 @@ mod tests {
615633
assert_eq!(calls, 1);
616634
}
617635

618-
/// A panicking callback must not leave its registration behind: the wait in
619-
/// `set` would then never be satisfiable, turning the use-after-free into a
620-
/// permanent hang. Covers the `DispatchToken` unwind path.
636+
/// An unwind through a live dispatch must not leave its registration
637+
/// behind: the wait in `set` would then never be satisfiable, turning the
638+
/// use-after-free into a permanent hang. Covers `DispatchToken`'s `Drop`.
639+
///
640+
/// The unwind is raised directly rather than from a callback, because a
641+
/// panic *out of a callback* is not the reachable case: the callbacks are
642+
/// `extern "C"`, and Rust aborts rather than unwinding across that
643+
/// boundary — a panicking executor callback kills the process before any
644+
/// `Drop` runs. What can unwind here is Rust code on this side of the
645+
/// boundary, most notably `invoke_user_callback!`'s own debug assertion,
646+
/// which fires *before* the call.
621647
///
622-
/// The panic is caught, so the run prints one backtrace-ish line from the
623-
/// default hook. That noise is expected.
648+
/// The panic is caught, so the run prints one line from the default hook.
649+
/// That noise is expected.
624650
#[test]
625-
fn a_panicking_callback_does_not_wedge_later_sets() {
626-
unsafe extern "C" fn panics(_user_data: *const std::ffi::c_void, _count: usize) {
627-
panic!("executor callback exploded");
628-
}
629-
651+
fn an_unwind_through_a_dispatch_releases_its_registration() {
630652
let exec = Executor::new("client", 0);
631-
exec.slot.set(Some(panics), exec.user_data());
632653

633-
let unwound = std::panic::catch_unwind(AssertUnwindSafe(|| exec.slot.notify_one()));
654+
let unwound = std::panic::catch_unwind(AssertUnwindSafe(|| {
655+
let _token = exec.slot.enter_dispatch();
656+
panic!("something unwound mid-dispatch");
657+
}));
634658
assert!(unwound.is_err(), "the panic must still propagate");
635659

636-
assert_completes("set() after a callback panicked", move || {
660+
// If the registration leaked, this blocks forever: the wait is looking
661+
// for a dispatch on another thread that has already gone.
662+
assert_completes("set() after an unwind mid-dispatch", move || {
637663
exec.slot.set(None, std::ptr::null_mut());
638664
});
639665
}

0 commit comments

Comments
 (0)