fix(transport): widen pending_key() to preserve byte_bus_id's full 11 bits - #176
Merged
Merged
Conversation
… bits
rcp::udp::Client::pending_key() and rcp::l2::Client::pending_key() computed
their response-correlation map key by shifting byte_bus_id left 8 bits and
OR-ing in transaction_num, then truncating the result to uint16_t. But
byte_bus_id is an 11-bit wire field (0-2047, avtp::ByteBusId's own comment;
acf.hpp's detail::kByteBusIdMask), not 8 bits, so the truncation silently
dropped its top 3 bits: pending_key(5, 7) and pending_key(261, 7) both
evaluated to 0x0507, even though 5 and 261 are two different, wire-legal
byte_bus_id values.
Under concurrent requests sharing a Client instance (multiple threads
calling Client::request() against different byte_bus_id endpoints, exactly
what REQ-UDP-011's own text describes: "two outstanding requests with
distinct (byte_bus_id, transaction_num) pairs each receive their own
response"), two colliding keys land in the same pending_ map slot. The
second request's map insertion silently overwrites the first's promise
pointer, so:
- the first request's response is delivered to the SECOND request's
promise (misdelivery — the second caller receives the wrong request's
data), and
- the first request's own promise, no longer reachable from pending_,
never gets set — that Client::request() call hangs until ctx's
deadline (or forever with no deadline).
Fix: widen pending_key()'s return type to uint32_t and shift bus_id left by
the same 8 bits, but without truncating the result back down — bus_id
(0-2047, 11 bits) now occupies bits 8-18 of the key and transaction_num
(0-255, 8 bits) occupies bits 0-7, so no two distinct (byte_bus_id,
transaction_num) pairs can ever alias to the same key. Client::pending_'s
map key type is updated to uint32_t to match, in both udp.hpp and l2.hpp.
Also moved pending_key out of each Client class (where it was a private
static method) to a top-level pure function in each file — rcp/udp.hpp's
own encode_annexj_datagram/decode_annexj_datagram and rcp/l2.hpp's own
is_unicast_mac already establish this file's convention for pure,
socket-free helpers: unconditional of any platform guard, so the function
itself (and a test exercising it directly) compiles and runs on every
platform. This also makes the fix independently unit-testable without a
live socket, which rcp/l2.hpp's Client needs (AF_PACKET/CAP_NET_RAW,
Linux-only) but a plain function does not.
Tests added:
- tests/test_udp.cpp: a pure pending_key() unit test proving
pending_key(5, 7) != pending_key(261, 7) and sweeping every
byte_bus_id 256 apart across the full 11-bit range for collisions
(tagged REQ-UDP-011, the existing requirement whose own text this bug
violated); and a new two-thread, real-loopback-socket regression test
("Client::request does not misdeliver or hang for two genuinely
concurrent requests...") that drives a genuine overlap between a
byte_bus_id=5 request (server-side delayed 150ms) and a byte_bus_id=261
request sharing transaction_num 7, and asserts each gets its own
correct response.
- tests/test_l2.cpp: the same pure pending_key() unit test (no REQ tag —
no existing cpp-RCP L2 requirement covers Client-level response
correlation by key the way REQ-UDP-011 does for udp.hpp; a new
REQ-L2-011 entry in .fusa-reqs.json may be warranted but was not added
here per this change's own scope). A live-socket equivalent of the
threaded udp.hpp test was not added to tests/l2_veth_roundtrip.cpp
since that harness needs CAP_NET_RAW/root and only runs in the
dedicated l2-veth CI job, not the privilege-free ctest suite this fix
was verified against.
Verification:
- Full clean rebuild (cmake -DRCP_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug
&& cmake --build build -j): 0 errors, 0 warnings.
- ctest: 100% pass, 58/58 test binaries, including both new pending_key
regression tests.
- Mutation-testing sanity check: reintroduced the original uint16_t
truncation arithmetic (keeping the new call sites/map type so it still
compiled), rebuilt, and confirmed the new tests fail against it —
pending_key(5,7) == pending_key(261,7) (both 0x0507), and the
concurrent-request regression test's byte_bus_id=5 leg times out
(ErrTimeout) exactly matching the "indefinite hang" failure mode this
finding describes. Reverted the mutation and reran the full suite to
confirm 100% pass again.
Closes a HIGH-severity finding from the cpp-RCP v3.0.0 deep audit (batch 1,
transport pending_key collision).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
17 tasks
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.
Bug
rcp::udp::Client::pending_key()andrcp::l2::Client::pending_key()computed their response-correlation map key by shiftingbyte_bus_idleft 8 bits and OR-ing intransaction_num, then truncating the result touint16_t:byte_bus_idis an 11-bit wire field (0–2047;avtp::ByteBusId's own comment,acf.hpp'sdetail::kByteBusIdMask), not 8 bits, so the truncation silently dropped its top 3 bits:pending_key(5, 7)andpending_key(261, 7)both evaluated to0x0507, even though 5 and 261 are two different, wire-legalbyte_bus_idvalues.Under concurrent requests sharing a
Clientinstance (multiple threads callingClient::request()against differentbyte_bus_idendpoints — exactly whatREQ-UDP-011's own text describes: "two outstanding requests with distinct (byte_bus_id, transaction_num) pairs each receive their own response"), two colliding keys land in the samepending_map slot. The second request's insertion silently overwrites the first's promise pointer, so:pending_, never gets set — thatClient::request()call hangs untilctx's deadline (or forever with no deadline).HIGH severity, closes a finding from the cpp-RCP v3.0.0 deep audit (batch 1, transport
pending_keycollision).Fix
Widened
pending_key()'s return type touint32_t, keeping the same left-shift-by-8 but without truncating the result back down —bus_id(0–2047, 11 bits) now occupies bits 8–18 of the key andtransaction_num(0–255, 8 bits) occupies bits 0–7, so no two distinct(byte_bus_id, transaction_num)pairs can ever alias to the same key.Client::pending_'s map key type is updated touint32_tto match, in bothudp.hppandl2.hpp, keeping the two files' fix identical.Also moved
pending_keyout of eachClientclass (where it was a private static method) into a top-level pure function in each file —rcp/udp.hpp's ownencode_annexj_datagram/decode_annexj_datagramandrcp/l2.hpp's ownis_unicast_macalready establish this file's convention for pure, socket-free helpers placed unconditional of any platform guard, so the function itself (and a test exercising it directly) compiles and runs on every platform. This also makes the fix independently unit-testable without a live socket, whichrcp/l2.hpp'sClientneeds (AF_PACKET/CAP_NET_RAW, Linux-only) but a plain function does not.No other code in either file was touched.
Tests
tests/test_udp.cpp:pending_key()unit test provingpending_key(5, 7) != pending_key(261, 7)and sweeping everybyte_bus_id256 apart across the full 11-bit range crossed with severaltransaction_numvalues for collisions (taggedREQ-UDP-011, the existing requirement whose own text this bug violated)."Client::request does not misdeliver or hang for two genuinely concurrent requests...") that drives genuine overlap between abyte_bus_id=5request (server-side delayed 150ms) and abyte_bus_id=261request sharingtransaction_num7, and asserts each gets its own correct response.tests/test_l2.cpp:pending_key()unit test (no REQ tag — no existing cpp-RCP L2 requirement currently covers Client-level response correlation by key the wayREQ-UDP-011does forudp.hpp; a newREQ-L2-011entry in.fusa-reqs.jsonmay be warranted but was deliberately not added here, out of scope for this fix — flagging for follow-up).udp.hpptest was not added totests/l2_veth_roundtrip.cpp, since that harness needsCAP_NET_RAW/root and only runs in the dedicatedl2-vethCI job, not the privilege-freectestsuite this fix was verified against.Verification
Full clean rebuild:
rm -rf build && cmake -S . -B build -DRCP_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug && cmake --build build -j— 0 errors, 0 warnings.Full test suite:
ctest --output-on-failure— 100% pass, 58/58 test binaries, including both newpending_keyregression tests.Mutation-testing sanity check: reintroduced the original
uint16_ttruncation arithmetic (keeping the new call sites/map type so it still compiled), rebuilt, and confirmed the new tests fail against it:pending_key(5,7) == pending_key(261,7)(both0x0507,REQUIREfails as expected)byte_bus_id=5leg times out (ErrTimeout), exactly matching the "indefinite hang" failure mode this finding describes.Reverted the mutation and reran the full suite to confirm 100% pass again.
🤖 Generated with Claude Code