Skip to content

Commit ac184a9

Browse files
committed
fix(rmw): borrow guard conditions immutably, tighten the graph-event assertion
**Soundness.** `rmw_trigger_guard_condition` took `&mut GuardConditionImpl` while `rmw_wait` holds `&GuardConditionImpl` to the same object. Moving `triggered` behind an `AtomicBool` defined the data race but said nothing about the aliasing: handing out a `&mut` to an object another thread holds a `&` to is undefined behaviour whatever the field types are. All mutation already goes through atomics in `GuardConditionState`, so `trigger` and `reset` now take `&self`, the FFI entry point borrows via `borrow_data`, and the wait-set accessor takes a shared reference. **Test strength.** `reentrant_graph_event.rs` asserted the re-entrant query saw `>= 1` publisher -- satisfiable by `local_pub`, which exists for the whole scenario. A regression invoking the callback *before* inserting the remote entity would still return 1 and pass, so the assertion did not depend on the ordering it claimed to verify. It now requires both. **Lock scope.** `EventsManager::set_callback` fires the backlog callback while the caller's outer `Mutex<EventsManager>` guard is live -- the "outside the lock" it releases is only the inner `event_mutex`. The docs now say so, and `set_shared_callback` is the safe entry point for `&Mutex<EventsManager>` holders, mirroring `update_shared_event_status`. No caller needed migrating: `RmEventHandle::set_callback` already collects under the guard and fires after releasing, and the remaining call sites own their manager outright.
1 parent da94398 commit ac184a9

4 files changed

Lines changed: 73 additions & 9 deletions

File tree

crates/hiroz-tests/tests/reentrant_graph_event.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,18 @@ fn graph_change_callback_querying_the_graph_does_not_deadlock() {
292292
"the graph-change callback never ran for the remote publisher — \
293293
the scenario proved nothing"
294294
);
295+
// Two, not one. `local_pub` is on this node for the whole scenario, so
296+
// `>= 1` was satisfiable without the remote publisher ever being in the
297+
// graph — which is precisely the regression this is meant to catch: a
298+
// callback invoked *before* the entity is inserted would still observe
299+
// the local publisher and pass. Requiring both makes the assertion
300+
// actually depend on the ordering it claims to verify.
295301
assert!(
296-
counted.load(Ordering::SeqCst) >= 1,
297-
"the re-entrant graph query returned {} publishers; it should see at \
298-
least the remote one",
302+
counted.load(Ordering::SeqCst) >= 2,
303+
"the re-entrant graph query saw {} publisher(s) on the topic; it must \
304+
see both the local one and the remote one whose appearance triggered \
305+
this callback. Seeing exactly 1 means the callback ran before the \
306+
remote entity was inserted into the graph",
299307
counted.load(Ordering::SeqCst)
300308
);
301309
});

crates/hiroz/src/event.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,25 @@ impl EventsManager {
6969
}
7070
}
7171

72+
/// Install a callback, delivering any backlog immediately.
73+
///
74+
/// **Only for a caller that owns this manager outright.** `&mut self` means
75+
/// the caller holds whatever `Mutex<EventsManager>` wraps it, and the
76+
/// backlog callback below runs while that outer guard is still live — so a
77+
/// callback that re-enters (`RmEventHandle::take_event`, say) deadlocks on
78+
/// it. The "outside the lock" this releases is only the *inner*
79+
/// `event_mutex`.
80+
///
81+
/// Anyone holding an `Arc<Mutex<EventsManager>>` must use
82+
/// [`set_shared_callback`] instead, which is to registration what
83+
/// [`update_shared_event_status`] is to status updates.
7284
pub fn set_callback<F>(&mut self, event_type: ZenohEventType, callback: F)
7385
where
7486
F: Fn(i32) + Send + Sync + 'static,
7587
{
7688
let callback: EventCallback = Arc::new(callback);
7789
let unread_count = self.install_callback(event_type, callback.clone());
78-
// If there were unread events, notify the freshly-registered callback —
79-
// outside the lock.
90+
// Outside the inner `event_mutex` only — see the note above.
8091
if unread_count != 0 {
8192
callback(unread_count);
8293
}
@@ -427,6 +438,40 @@ impl EventWaitData {
427438
/// outer guard alive across the callback, and the callback is user code handed
428439
/// to an rclcpp executor which routinely calls straight back into the same
429440
/// manager (`rmw_take_event` → [`RmEventHandle::take_event`]).
441+
/// Install a callback on a shared manager, delivering any backlog **after**
442+
/// the outer guard is released.
443+
///
444+
/// The registration counterpart of [`update_shared_event_status`], and the
445+
/// entry point every holder of an `Arc<Mutex<EventsManager>>` must use.
446+
/// [`EventsManager::set_callback`] takes `&mut self`, so it can only be called
447+
/// with the outer mutex already held, and it fires the backlog underneath it —
448+
/// a callback that re-enters (`RmEventHandle::take_event`) then self-deadlocks
449+
/// on a non-reentrant `Mutex`. This collects the backlog under the guard, drops
450+
/// it, and only then calls.
451+
pub fn set_shared_callback<F>(
452+
events_mgr: &Mutex<EventsManager>,
453+
event_type: ZenohEventType,
454+
callback: F,
455+
) where
456+
F: Fn(i32) + Send + Sync + 'static,
457+
{
458+
let callback: EventCallback = Arc::new(callback);
459+
460+
// Bound to its own `let` inside a block, for the same reason as
461+
// `update_shared_event_status_with_policy`: as a `match`/`if let` scrutinee
462+
// the guard would outlive the invocation below and reinstate the deadlock.
463+
let unread_count = {
464+
let Ok(mut mgr) = events_mgr.lock() else {
465+
return;
466+
};
467+
mgr.install_callback(event_type, callback.clone())
468+
};
469+
470+
if unread_count != 0 {
471+
callback(unread_count);
472+
}
473+
}
474+
430475
pub fn update_shared_event_status(
431476
events_mgr: &Mutex<EventsManager>,
432477
event_type: ZenohEventType,

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,19 @@ pub struct GuardConditionImpl {
5555
}
5656

5757
impl GuardConditionImpl {
58-
pub(crate) fn trigger(&mut self) -> Result<(), ()> {
58+
// `&self`, not `&mut self`. `rmw_wait` holds shared references to this
59+
// object while scanning the wait set, and `rmw_trigger_guard_condition` can
60+
// fire from a zenoh delivery thread at the same time. Handing out a `&mut`
61+
// to an object another thread holds a `&` to is undefined behaviour in Rust
62+
// whatever the field types are -- moving `triggered` behind an atomic
63+
// defines the *data race* but says nothing about the aliasing. All mutation
64+
// now goes through the atomics in `GuardConditionState`, so shared access is
65+
// sufficient and every borrow can be immutable.
66+
pub(crate) fn trigger(&self) -> Result<(), ()> {
5967
self.state.fire()
6068
}
6169

62-
pub fn reset(&mut self) {
70+
pub fn reset(&self) {
6371
self.state.reset();
6472
}
6573

@@ -138,7 +146,8 @@ pub extern "C" fn rmw_trigger_guard_condition(
138146
return RMW_RET_INVALID_ARGUMENT as _;
139147
}
140148

141-
if let Ok(gc_impl) = (guard_condition as *mut rmw_guard_condition_t).borrow_mut_data() {
149+
// Immutable borrow: see the note on `GuardConditionImpl::trigger`.
150+
if let Ok(gc_impl) = (guard_condition as *mut rmw_guard_condition_t).borrow_data() {
142151
let _ = gc_impl.trigger();
143152
}
144153

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,10 @@ pub extern "C" fn rmw_wait(
388388
unsafe { *gc_array.guard_conditions.add(i) as *mut rmw_guard_condition_impl_t };
389389
if !gc_impl_ptr.is_null() {
390390
unsafe {
391+
// Shared, not exclusive: a delivery thread may be
392+
// inside `trigger` on this same object right now.
391393
let gc_impl =
392-
&mut *(gc_impl_ptr as *mut crate::guard_condition::GuardConditionImpl);
394+
&*(gc_impl_ptr as *const crate::guard_condition::GuardConditionImpl);
393395
if !gc_impl.is_ready() {
394396
// Not ready - set to NULL in place
395397
*gc_array.guard_conditions.add(i) = std::ptr::null_mut();

0 commit comments

Comments
 (0)