Skip to content

Fix malformed-payload handling on reconnect - #58

Draft
jkolo wants to merge 5 commits into
mos9527:v1-compatfrom
jkolo:fix/malformed-payload-on-connect
Draft

Fix malformed-payload handling on reconnect#58
jkolo wants to merge 5 commits into
mos9527:v1-compatfrom
jkolo:fix/malformed-payload-on-connect

Conversation

@jkolo

@jkolo jkolo commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Draft - opening early for visibility, still want to look over edge cases before this is ready.

Fixes malformed-payload handling around reconnect: a notify frame racing the protocol handshake, plus an out-of-bounds read while unpacking truncated frames.

  • Bound-check MDR frame unpacking (OOB read on truncated frames)
  • Don't fail the session on frames that precede the protocol handshake
  • Cover the receive path with framing tests (mdr-framing CTest suite)
  • Mark sent frames as outgoing in the debug log
  • clang-format cleanup on the frame unpacking switch

One note: libmdr/include/mdr/Generated/Command.hpp picked up a hand-added MALFORMED case in the first commit instead of going through codegen - re-ran cmake --build --target codegen locally and it reproduces byte-identical output, so the hand-edit matches what codegen would have produced, but flagging since CONTRIBUTING asks for the generator to be run instead of hand-editing.

jkolo and others added 5 commits August 21, 2026 12:27
MDRUnpackCommand indexed the unescaped frame without checking that it is long
enough to hold a header, so a frame that carries fewer than seven bytes between
its markers - or one whose escape sequences fail to unescape at all, which
Unescape reports as an empty buffer - read past the end of the span. `3E 3C` on
the wire is enough to reach it, and the packet debugger runs the same unpacker
over recorded captures.

Both that and a length field disagreeing with the markers now report MALFORMED
rather than INCOMPLETE: the markers already bound the frame, so waiting for more
bytes cannot repair it, and the receive buffer never drained.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKt3Lk68pt5yr92YV1iTqn
A device that changed state while unattended pushes the matching notify as soon
as the socket opens - PERI_NTFY_PARAM with the paired device list is the one
seen in practice, right after another client released the link. That can arrive
before CONNECT_RET_PROTOCOL_INFO answers, and the guards treated "too early" as
"malformed", killing the session with MDR_RESULT_ERROR_MALFORMED_PAYLOAD. Being
a race against our own handshake, it hit roughly one connection in three.

There is still no table to parse such a frame against, so it is dropped rather
than queued: the ACK has already gone out and init reads back every state we
care about anyway.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKt3Lk68pt5yr92YV1iTqn
mdr_framing_tests drives the real poll path through the mock transport with
synthetic frames: data arriving before the handshake, frames too short to hold a
header, a dangling escape sentry, a frame split across one and five byte reads,
and leftovers preceding a good frame. Every case asserts the session survives
and still accepts the handshake afterwards, which the capture replay cannot
check because it always starts from protocol info.

The mock moves to a shared header so both suites use it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKt3Lk68pt5yr92YV1iTqn
Send dumped its bytes with the same "<<" prefix Receive uses, so a debug log
read as if the device had sent everything.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKt3Lk68pt5yr92YV1iTqn
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