fix(kerykeion): recover receive stream from malformed protobuf payloads#275
Merged
Conversation
Decoder::decode() treated a malformed FromRadio payload inside an otherwise well-framed packet as fatal (`.context(ProtobufDecodeSnafu)?`), while every other corruption class (bad magic, oversized/zero length, partial payload) was recovered with Ok + continue. tokio_util's Framed tears down the whole connection on any Decoder::Error, so one adversary-controlled mesh frame permanently killed reception. Closes #191 Now a malformed payload logs tracing::warn! (error + payload_len) and falls through to re-loop, matching the decoder's existing corruption-recovery pattern; decode() no longer has an Err-returning path at all. Dropped the now-unused snafu::ResultExt and ProtobufDecodeSnafu imports from codec.rs (ProtobufDecodeSnafu is still used by mqtt.rs). Added decoder_malformed_payload_then_valid_frame_still_decodes and decoder_malformed_payload_alone_returns_none, both driven off a shared frame_malformed_payload() helper (a single 0x00 payload byte, which prost rejects unconditionally as protobuf tag 0). Tightened codec_handles_arbitrary_bytes_without_panic to assert is_ok() instead of accepting Err as a benign outcome -- that leniency is what let this class of DoS through the property test in the first place. Gate-Passed: kanon 0.1.10 +stages:fmt,check,clippy,nextest,lint sha:bbfb474b2e665b6f7438fd93bb3550bac233e365
forkwright
enabled auto-merge (squash)
July 17, 2026 14:59
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.
Closes #191
What
MeshCodec::decode()treated a malformedFromRadioprotobuf payload inside an otherwise well-framed packet as fatal (FromRadio::decode(payload.as_ref()).context(ProtobufDecodeSnafu)?), while every other corruption class the decoder can encounter (bad magic, oversized/zero length, partial payload) was recovered withOk+ re-loop.tokio_util::codec::Framedtears down the whole connection on anyDecoder::Error, so one adversary-controlled mesh frame permanently killed the receive stream — the prime OTA DoS site named in the issue.Decoder::decode()now has noErr-returning path at all: a malformed payload is logged viatracing::warn!(witherror+payload_len) and falls through to re-loop, exactly matching the decoder's other corruption-recovery branches.Dropped the now-unused
snafu::ResultExtandProtobufDecodeSnafuimports fromcodec.rs(ProtobufDecodeSnafuis still used bymqtt.rs, so only the import incodec.rswas dead).Tests
decoder_malformed_payload_then_valid_frame_still_decodes— a malformed frame immediately followed by a valid frame in the same buffer; the valid frame still decodes.decoder_malformed_payload_alone_returns_none— a malformed frame alone in the buffer returnsOk(None), neverErr.frame_malformed_payload()helper: a well-framed header wrapping a single0x00payload byte, whichprostrejects unconditionally as protobuf tag 0 (field number 0 is reserved) — a malformed-payload case that's guaranteed to failFromRadio::decodeindependent of the message's oneof variants.codec_handles_arbitrary_bytes_without_panicproptest to assertresult.is_ok()instead of acceptingErras a benign outcome — that leniency is exactly what let this DoS class through the property test in the first place.Evidence
Full gate (
vgate kanon gate --stamp):