fix(rmw_event): validate handles and set an error message on the failure paths - #64
Open
benaliabderrahmane wants to merge 2 commits into
Open
benaliabderrahmane wants to merge 2 commits into
benaliabderrahmane wants to merge 2 commits into
Conversation
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.
…ure paths rmw_take_event, rmw_event_set_callback, both *_event_init functions and rmw_subscription_set_on_new_message_callback now reject a NULL handle with RMW_RET_INVALID_ARGUMENT and a foreign one with RMW_RET_INCORRECT_RMW_IMPLEMENTATION, and every failure path leaves an error message - the missing message was the ': error not set' half of the original EventsExecutor crash report. rmw_event_fini deliberately validates nothing: rcl_event_fini skips the call when impl is NULL, which is exactly what a rejected init leaves behind, so guards there defended a state the stack cannot produce. Nothing to release either - rmw_event_t::data aliases the endpoint's impl struct. Squashed from: - fix(rmw_event): validate handles and set an error message on the failure paths - fix(rmw_event): drop the unreachable rmw_event_fini guards, check the identifier in set_callback
…n every distro - SetOnNewMessageCallbackRejectsForeignSubscription pins the identifier check this PR adds to rmw_subscription_set_on_new_message_callback; it was the one guarded entry point without a test. - The *RejectsEveryEventType loops were bounded by RMW_EVENT_INVALID, which is the last enumerator on Jazzy but 0 from Kilted on, so they ran a single iteration on three of the four CI distros. Bound them by whichever of it and RMW_EVENT_PUBLICATION_MATCHED is larger. - Drop the claim that rcl reuses the event_info buffer across calls; no upstream layer does. The rule it was propping up stands on its own. - CHANGELOG: state the *_event_init refusal the entry is headlined with (#61), and stop claiming every other entry point already had the identifier check - only the service and client callback setters did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUNQNo26cKRPrVXcaHZnje
benaliabderrahmane
pushed a commit
that referenced
this pull request
Sep 18, 2026
"No background threads" was asserted across DESIGN.md and README.md and is no longer unconditional: none for an executor that waits, one per-context listener thread for an application that registers a listener callback. Each assertion now says what it actually guarantees. DESIGN.md gains "The listener thread: delivery without a wait": why EventsExecutor deadlocks against wait-only delivery, the lazy start, the reordering and lost-wakeup hazards and what closes them (drain_mutex, the per-wait-set delivery fd), the listener_mutex lifetime rule and the restrictions it implies, that a callback may take from its own endpoint because callback_mutex is recursive, which context-wide locks exist, and what the thread deliberately is not. README, test/README and CHANGELOG follow. Squashed from: - docs: document the listener thread; narrow the no-threads guarantee - docs: fix "edge-driven", describe the per-wait-set delivery fd, drop #64's entry - docs: correct the drain-safety claim, and log the two blocker fixes - docs: sweep the claims this stack invalidated but the prose pass missed - docs: a callback may now take from its own endpoint - docs: callback_mutex is recursive; listener_mutex is not the only context lock Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUNQNo26cKRPrVXcaHZnje
benaliabderrahmane
force-pushed
the
fix/event-api-hardening
branch
from
September 18, 2026 12:44
7e91404 to
cb8abd7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Finishes the event API surface that #61 started. #61 made
*_event_initrejectunsupported event types, which is the refusal
rclcpphandles cleanly. Theremaining entry points were still bare stubs, and two of the gaps stay reachable:
rmw_event_set_callbackreturnedRMW_RET_UNSUPPORTEDwithout setting anerror message. That missing message is the
": error not set"half of theoriginal
EventsExecutorcrash report, and it is what made the failure hard toplace.
rclcppcan no longer reach this function, but a failure code withnothing behind it is a dead end for anyone calling it directly.
rmw_take_eventvalidated neither its handle nor its implementationidentifier, unlike the endpoint and node entry points elsewhere in this RMW.
RMW_CHECK_TYPE_IDENTIFIERS_MATCHnow guards five entry points that weremissing it: both
*_event_initfunctions,rmw_take_event,rmw_event_set_callbackandrmw_subscription_set_on_new_message_callback; theservice and client callback setters already had it.
One deliberate omission worth a look during review:
rmw_event_finiis leftvalidating nothing at all — a bare
(void)event; return RMW_RET_OK;. An earlierrevision of this branch did guard it, and the guards came back out:
rcl_event_finiskips the call entirely when
event->implis NULL, and that is exactly whatrcl_*_event_initleaves behind, since it deallocatesimpland nulls it beforereturning our
RMW_RET_UNSUPPORTED. So no rejected init ever reaches thisfunction, 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::dataaliases theendpoint's impl struct, owned by
rmw_destroy_publisher/rmw_destroy_subscription. The function carries a comment saying so.Also adds an
## [Unreleased]/### Fixedsection toCHANGELOG.mdcoveringthe above.
Is this user-facing behavior change?
Yes, in three ways, all narrow:
rmw_event_set_callbacknow leaves an error message, sorcllogssomething useful instead of
error not set. It also now returnsRMW_RET_INVALID_ARGUMENTfor a null handle andRMW_RET_INCORRECT_RMW_IMPLEMENTATIONfor a foreign one, wheredevelreturned
RMW_RET_UNSUPPORTEDfor both.rmw_take_eventreturnsRMW_RET_INVALID_ARGUMENT/RMW_RET_INCORRECT_RMW_IMPLEMENTATIONon misuse that previously returnedRMW_RET_OK, and both*_event_initfunctions returnRMW_RET_INCORRECT_RMW_IMPLEMENTATIONfor a foreign endpoint wheredevelfell 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_callbackreturnsRMW_RET_INCORRECT_RMW_IMPLEMENTATIONfor a foreign subscription, wheredevelwould have cast that subscription'sdatatormw_uds::UdsSubscription *and locked itscallback_mutex.rmw_event_finiis unchanged: it still returnsRMW_RET_OKfor every handle.No event type becomes supported.
rmw_event_type_is_supportedstill returnsfalsefor all of them.How was this tested?
test/test_rmw_event.cppgrows from 2 tests to 9. Three of the nine fail onthe current
develbehavior and pass after this change:EventInitRejectsForeignEndpoints,TakeEventRejectsBadArguments,EventSetCallbackReportsUnsupportedWithAMessage. The remaining six alsopass on
devel— includingEventFiniIsANoOp, which pins the contractdevelalready had.The two existing tests are broadened to iterate
0 .. RMW_EVENT_INVALIDratherthan naming two event types by hand.
rmw_event_type_thas no explicitinitializers, so both the ordinals and
RMW_EVENT_INVALIDshift as upstream addsevent 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.