Skip to content

fix(rmw_event): validate handles and set an error message on the failure paths - #64

Open
benaliabderrahmane wants to merge 2 commits into
develfrom
fix/event-api-hardening
Open

fix(rmw_event): validate handles and set an error message on the failure paths#64
benaliabderrahmane wants to merge 2 commits into
develfrom
fix/event-api-hardening

Conversation

@benaliabderrahmane

@benaliabderrahmane benaliabderrahmane commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Description

Finishes the event API surface that #61 started. #61 made *_event_init reject
unsupported event types, which is the refusal rclcpp handles cleanly. The
remaining entry points were still bare stubs, and two of the gaps stay reachable:

  • rmw_event_set_callback returned RMW_RET_UNSUPPORTED without setting an
    error message
    . That missing message is the ": error not set" half of the
    original EventsExecutor crash report, and it is what made the failure hard to
    place. rclcpp can no longer reach this function, but a failure code with
    nothing behind it is a dead end for anyone calling it directly.
  • rmw_take_event validated neither its handle nor its implementation
    identifier, unlike the endpoint and node entry points elsewhere in this RMW.

RMW_CHECK_TYPE_IDENTIFIERS_MATCH now guards five entry points that were
missing it: both *_event_init functions, rmw_take_event,
rmw_event_set_callback and rmw_subscription_set_on_new_message_callback; the
service and client callback setters already had it.

One deliberate omission worth a look during review: rmw_event_fini is left
validating nothing at all — a bare (void)event; return RMW_RET_OK;. An earlier
revision of this branch did guard it, and the guards came back out: rcl_event_fini
skips the call entirely when event->impl is NULL, and that is exactly what
rcl_*_event_init leaves behind, since it deallocates impl and nulls it before
returning our RMW_RET_UNSUPPORTED. So no rejected init ever reaches this
function, and neither does a foreign handle — the guards were defending a state
the stack cannot produce, which is CLAUDE.md's "no error handling for impossible
scenarios". There is nothing to release either: rmw_event_t::data aliases the
endpoint's impl struct, owned by rmw_destroy_publisher /
rmw_destroy_subscription. The function carries a comment saying so.

Also adds an ## [Unreleased] / ### Fixed section to CHANGELOG.md covering
the above.

Is this user-facing behavior change?

Yes, in three ways, all narrow:

  • A failing rmw_event_set_callback now leaves an error message, so rcl logs
    something useful instead of error not set. It also now returns
    RMW_RET_INVALID_ARGUMENT for a null handle and
    RMW_RET_INCORRECT_RMW_IMPLEMENTATION for a foreign one, where devel
    returned RMW_RET_UNSUPPORTED for both.
  • rmw_take_event returns RMW_RET_INVALID_ARGUMENT /
    RMW_RET_INCORRECT_RMW_IMPLEMENTATION on misuse that previously returned
    RMW_RET_OK, and both *_event_init functions return
    RMW_RET_INCORRECT_RMW_IMPLEMENTATION for a foreign endpoint where devel
    fell through to the unsupported-type check and returned RMW_RET_UNSUPPORTED.
    Only reachable by passing a null or a foreign handle.
  • rmw_subscription_set_on_new_message_callback returns
    RMW_RET_INCORRECT_RMW_IMPLEMENTATION for a foreign subscription, where
    devel would have cast that subscription's data to
    rmw_uds::UdsSubscription * and locked its callback_mutex.

rmw_event_fini is unchanged: it still returns RMW_RET_OK for every handle.

No event type becomes supported. rmw_event_type_is_supported still returns
false for all of them.

How was this tested?

test/test_rmw_event.cpp grows from 2 tests to 9. Three of the nine fail on
the current devel behavior and pass after this change
:
EventInitRejectsForeignEndpoints, TakeEventRejectsBadArguments,
EventSetCallbackReportsUnsupportedWithAMessage. The remaining six also
pass on devel — including EventFiniIsANoOp, which pins the contract
devel already had.

The two existing tests are broadened to iterate 0 .. RMW_EVENT_INVALID rather
than naming two event types by hand. rmw_event_type_t has no explicit
initializers, so both the ordinals and RMW_EVENT_INVALID shift as upstream adds
event types — no ordinal is hardcoded anywhere.

Full suite green locally on jazzy: 163 tests, 0 failures. kilted, rolling and
lyrical are on CI.

Did you use Generative AI?

Additional Information

Independent of the other branches in this series — can merge in any order.

…ure paths

PR #61 made *_event_init reject unsupported event types, which is the
only refusal rclcpp swallows. The remaining event entry points were
still bare stubs, and two of the gaps stay reachable.

rmw_event_set_callback returned RMW_RET_UNSUPPORTED without setting an
error message. That missing message is the ": error not set" half of the
original EventsExecutor crash report ("failed to set the on new message
callback for Event: error not set"), which is what made the failure hard
to place. rclcpp can no longer reach this function now that init rejects
every type, but a failure code with nothing behind it is a dead end for
anyone calling it directly.

rmw_take_event and rmw_event_fini validated neither their handle nor its
implementation identifier, unlike every other entry point here.
rmw_event_fini needs one allowance: rclcpp's EventHandler destructor
runs even when its constructor bailed out on the RMW_RET_UNSUPPORTED
from *_event_init, so it is handed a zero-initialized handle.
Finalizing that is not a caller error, and reporting one would turn
every swallowed UnsupportedEventTypeException into a spurious failure on
the way out.

Also adds the RMW_CHECK_TYPE_IDENTIFIERS_MATCH that both *_event_init
and rmw_subscription_set_on_new_message_callback were missing; the
service and client callback setters already had it.

The tests iterate the whole rmw_event_type_t enum rather than naming
event types. It has no explicit initializers, so both the ordinals and
RMW_EVENT_INVALID shift as upstream adds event types, and this builds
against four distros.

Four of the ten tests fail on the previous behavior and pass after this
change. Full suite green on Jazzy: 164 tests, 0 failures.
benaliabderrahmane added a commit that referenced this pull request Sep 9, 2026
's entry

Four corrections to the docs this PR adds.

"Edge-driven" appeared three times for the listener. The intent was
event-driven rather than timer-driven, which is what the surrounding
sentences argue, but the term means the opposite of what the code does:
listener_watch registers EPOLLIN without EPOLLET and relies on
level-triggering to notice datagrams that arrived before the watch. Now
"event-driven, not timer-driven".

The delivery_fd section described one eventfd per context and claimed
"there is no order in which it is missed". That only held for whichever
wait set woke first - a read drains the whole eventfd counter - so the
section now explains why the fd belongs to the wait set, and the
deregister-before-close rule that comes with it.

The lifetime rule understated itself. listener_mutex is one per context,
so a callback must not destroy, register on, or clear the callback of any
endpoint in the context, nor create or destroy a wait set - not just its
own endpoint - and listener_unwatch blocks on whatever drain is in
flight, not one of "that endpoint".

The CHANGELOG documented #64's event-API changes, and #64 is not an
ancestor of this stack, so merging this without it shipped a changelog
for absent code. That entry moves to #64, which now carries its own
Unreleased section. Replaced here by the fixes this stack actually makes:
the double backlog flush, the setters' error paths, and the std::thread
throw.

test/README.md gains rows for the four new tests and updates the two that
were renamed.

Docs only. Full suite green on Jazzy: 174 tests, 0 failures.
… identifier in set_callback

Two follow-ups on this PR's own stated goal.

rmw_event_fini's allowance for a zero-initialized handle defends a state
the stack cannot produce. rcl_event_fini skips the call entirely when
event->impl is NULL, and that is exactly what rcl_*_event_init leaves
behind: it deallocates impl and nulls it before returning our
RMW_RET_UNSUPPORTED. So no rejected init ever reaches this function, and
neither does a foreign handle. Both guards go, and the function is a
documented no-op again - CLAUDE.md's "no error handling for impossible
scenarios".

rmw_event_set_callback was left as the only event entry point without
RMW_CHECK_TYPE_IDENTIFIERS_MATCH, which is the gap this PR set out to
close. Added.

The two rmw_event_fini tests collapse into one that pins the no-op
contract; EventSetCallbackReportsUnsupportedWithAMessage gains the
foreign-handle case, which fails before this change.

Full suite green on Jazzy: 163 tests, 0 failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant