Repository navigation
fix: bound parser stall on oversized-length garbage prefixes - #173
Conversation
Streaming stalled indefinitely against USB-Serial (Microchip CDC) when
the boot-time byte stream happened to varint-encode a large length. The
parser accepted any declared length up to 1 MB, then took the "wait for
more data" path because the buffer was nowhere close to that size. The
buffer grew unbounded, zero MessageReceived events fired, and the
desktop Live Graph stayed empty despite the device LED blinking.
The v0.19.6 leading-garbage fix handled *unparseable* bytes (1-byte
advance on parse failure) but not this case — the bytes parsed fine as
a length prefix; they just declared an impossibly large frame.
Two changes:
* Cap MaxMessageSizeBytes at 4 KB. Real DaqifiOutMessage frames are
well under 1 KB at 32 channels; initial-status frames that carry
channel metadata are a few KB at most. 4 KB is generous for any
legitimate frame and tight enough to reject the large-prefix
garbage pattern immediately.
* In the partial-frame wait path, add two escape gates:
- Gap gate: declared length > MaxPartialFrameGapBytes (1 KB)
beyond what's currently buffered is treated as garbage. Real
partials leave gaps in hundreds of bytes, not kilobytes.
- Resync gate: if we've already advanced in this call but not
yet produced a message, keep advancing one byte at a time
instead of stopping on the next plausible prefix. Without this,
a bogus 3-byte prefix can reduce to a 2-byte bogus continuation
after one advance and re-stall on every call.
Added regression test `RecoversFromOversizedPrefix` that prepends a
varint for 32768 and asserts the parser resyncs to the valid frame
that follows. Before the fix: 0 messages, 0 bytes consumed — exactly
the desktop symptom. After the fix: the valid frame is recovered.
All 820 existing Core tests still pass. Live repro against
/dev/cu.usbmodem101 (32-channel Nyquist1) still streams cleanly at
both 1 Hz and 100 Hz.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review Summary by QodoFix parser stall on oversized-length garbage prefixes with resync gates
WalkthroughsDescription• Cap MaxMessageSizeBytes from 1 MB to 4 KB to reject oversized garbage prefixes immediately • Add gap and resync gates to prevent indefinite stalls on plausible-but-bogus length prefixes • Add regression test RecoversFromOversizedPrefix validating parser recovery from garbage Diagramflowchart LR
A["Boot-time garbage<br/>on serial port"] -->|"varint encodes<br/>large length"| B["Parser receives<br/>plausible prefix"]
B -->|"Old: 1 MB cap"| C["Wait indefinitely<br/>for data"]
C -->|"Buffer grows<br/>unbounded"| D["Stall: 0 messages<br/>parsed"]
B -->|"New: 4 KB cap +<br/>gap/resync gates"| E["Reject as garbage<br/>& resync"]
E -->|"Advance 1 byte<br/>at a time"| F["Recover valid frame<br/>behind prefix"]
File Changes1. src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs
|
Code Review by Qodo
1.
|
Two fixes in response to qodo-code-review on #173: 1. Resync gate could discard real frame bytes (Bug, Correctness). The previous partial-frame wait path advanced `currentIndex` whenever any bytes had already been skipped in this call and no message had been produced yet. `StreamMessageConsumer` and `SdCardFileParser` both trim `consumedBytes` from their buffers regardless of whether messages were parsed, so this could permanently discard the first byte(s) of a legitimate frame that happened to arrive split across reads behind leading garbage. Replaced the resync gate with a protobuf field-tag plausibility check. When at least one body byte is available in the partial, peek at it: real DaqifiOutMessage frames always start with a valid field tag (wire_type in {0,1,2,5}, field_number >= 1). A byte that fails those checks isn't a real frame start, so it's treated as garbage. Multi-byte tags (continuation bit set) can't be fully validated from a single byte and are allowed through — the caller waits for more data rather than risk discarding a real frame. This preserves recovery from the oversized-prefix stall (the original motivation) without eating into real partial frames. 2. Gap gate used the wrong baseline (Bug, Correctness). `messageLength - remainingData.Length` counts the varint prefix bytes against the "missing payload" figure. The correct calculation is `messageLength - (remainingData.Length - prefixBytes)`, i.e. declared payload minus available payload. Small difference in magnitude (1–5 bytes) but inconsistent with the intent of the heuristic. Also added a regression test `PreservesPartialFrameAfterGarbage` that asserts the parser consumes only the garbage when followed by an incomplete real frame — leaving the partial for the caller to complete on the next read. All 823 tests pass (10 parser tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed both Qodo findings in 31168cf: 1. Resync gate drops partials (Bug): Fixed. Removed the 2. Gap gate miscomputes bytes (Bug): Fixed. Gap is now All 823 tests pass (10 parser tests). |
Summary
MaxMessageSizeBytesat 4 KB (down from 1 MB) so a garbage varint encoding tens of KB no longer parks the parser waiting for data that will never arrive.RecoversFromOversizedPrefixasserts a varint for 32768 is rejected and the parser recovers a valid frame sitting right behind it.Context
v0.19.6 (#169) fixed the unparseable bytes case (advance 1 byte per parse failure, no retry cap). This addresses the other failure mode: bytes that varint-encode a plausible large length. Under the old 1 MB cap, the parser accepted the declared length and took the "wait for more data" branch. Buffer grew unbounded, zero
MessageReceivedevents fired, Live Graph stayed empty despite the device LED blinking.Diagnosed against a physical Nyquist1 on
/dev/cu.usbmodem101driven bydaqifi-core-example-app --mimic-desktop. Core at HEAD streams cleanly at 1 Hz and 100 Hz with 16 analog + 16 digital channels both before and after this fix, so no regression on the happy path.Test plan
dotnet test src/Daqifi.Core.Tests/Daqifi.Core.Tests.csproj— 820 passed (both net8.0 and net9.0)ProtobufMessageParser_ParseMessages_RecoversFromOversizedPrefixfails onmain, passes with this change🤖 Generated with Claude Code