Repository navigation
Conversation
ucdr_deserialize_rstring() advanced by the length read off the wire without checking it against the buffer, and returned a pointer whose terminating NUL it never checked. Refuse a zero length, one past the end of the buffer, or one whose last byte is not NUL, setting ub->error. Bound the count in ucdr_deserialize_sequence_rstring() and leave *number at 0 on failure. Well-formed input decodes exactly as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
queriable_data_handler() replied with sequence_number 1 and the server's own GID every time, never reading the request's rmw attachment. rmw_zenoh clients match a reply to its pending request by that sequence number, so a long-lived client gets its first call answered and every later reply dropped as unmatched; `ros2 service call` makes a fresh client per call (always sequence 1) and so hides it. Add picoros_reply_attachment_from_request(), a static-inline helper in picoros.h, and build each reply's attachment in a stack-local copy of srv->attachment: a well-formed request attachment (exactly sizeof(rmw_attachment_t) bytes) has its sequence number and GID echoed; anything else keeps today's values. srv->attachment itself is never written, so one client's GID cannot leak into a later reply. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sub_data_handler(), queriable_data_handler() and the service reply handler copied every incoming payload into a z_malloc() buffer without checking it, so heap exhaustion on a small target became a write through NULL inside the zenoh receive task. Drop the sample or reply (or leave the request unanswered, so the caller times out) and log it instead. picoros_service_client_init() now returns PICOROS_ERROR when its key buffer cannot be allocated. z_malloc(0) may return NULL, so an empty service request is not treated as a failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four CDR strings whose wire length lies (zero, past the buffer end, near UINT32_MAX, no terminating NUL) and a string sequence whose count exceeds the caller's capacity. All five fail against the previous decoder and pass with the bounds check; the round-trip tests are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Three robustness fixes found while running Pico-ROS nodes against
rmw_zenoh_cpp(Lyrical) on ESP32 boards. Each is its own commit, and the string-bounds fix comes with tests.1. Bound string and string-sequence decoding (
picoserdes.c)ucdr_deserialize_rstring()advanced the iterator by the length read off the wire without checking it against the buffer. It also returned a pointer whose terminating NUL it never checked. A malformed or hostile peer could therefore make every later read, and anystrcmp()on the returned string, run past the received buffer. A length nearUINT32_MAXwraps the iterator backwards on 32-bit targets.The decoder now refuses a zero length, a length beyond the remaining buffer, or a string whose last byte isn't NUL. In each case it sets
ub->errorand doesn't move the iterator.ucdr_deserialize_sequence_rstring()also bounds its count againstmax_numberand leaves*numberat 0 on failure. Well-formed input decodes exactly as before.2. Echo the request's rmw identity in service replies (
picoros.c,picoros.h)queriable_data_handler()replied withsequence_number = 1and the server's own GID every time; it never read the request's attachment. rmw_zenoh clients match a reply to its pending request by that sequence number. rclpy, for example, drops a reply with no matching pending request. As a result, a long-lived client gets its first call answered and every later reply dropped, and the call times out.ros2 service callcreates a fresh client for each call, which always starts at sequence 1, so it passes and hides the bug.To reproduce: call a Pico-ROS service twice from one rclpy client. The second call times out.
The fix is a small static-inline helper,
picoros_reply_attachment_from_request(). The handler builds each reply's attachment in a stack-local copy ofsrv->attachment:sizeof(rmw_attachment_t)bytes), its sequence number and GID are echoed into the reply.srv->attachmentitself is never written, so one client's GID can't leak into later replies.timeis still the server's own clock.z_query_reply()sends synchronously, so pointingz_bytes_from_static_buf()at the stack copy is safe.3. Check
z_mallocin the receive paths and client init (picoros.c)The subscriber, queryable and service-reply handlers copied each incoming payload into an unchecked
z_malloc()buffer. On a small target, heap exhaustion became a write through NULL inside the zenoh receive task. Now:picoros_service_client_init()returnsPICOROS_ERRORif it can't allocate its key buffer.z_malloc(0)may legitimately return NULL, so an empty service request is not treated as a failure.Testing
test_examples_types_serdespasses. It has a new "Malformed String Tests" block with four lying-length strings and one over-capacity string sequence. All five cases fail against the current decoder and pass with fix 1, and the existing round-trip tests are unchanged.atomic.crequires GCC or C11. Sopicoros.cwas compile-checked on its own with the project's own flags and-Wall -Wextra. The patch adds no new warnings.b739fc7. There they are built into Zephyr firmware for ESP32 boards, with host-side unit checks for the reply-attachment helper. The multi-call scenario in fix 2 has not yet been exercised on hardware against a livermw_zenohd.Happy to split this into three PRs if you'd prefer.
🤖 Generated with Claude Code