Skip to content

fix(kerykeion): recover receive stream from malformed protobuf payloads#275

Merged
forkwright merged 1 commit into
mainfrom
recover-191-codec
Jul 17, 2026
Merged

fix(kerykeion): recover receive stream from malformed protobuf payloads#275
forkwright merged 1 commit into
mainfrom
recover-191-codec

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Closes #191

What

MeshCodec::decode() treated a malformed FromRadio protobuf 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 with Ok + re-loop. tokio_util::codec::Framed tears down the whole connection on any Decoder::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 no Err-returning path at all: a malformed payload is logged via tracing::warn! (with error + payload_len) and falls through to re-loop, exactly matching the decoder's other corruption-recovery branches.

Dropped the now-unused snafu::ResultExt and ProtobufDecodeSnafu imports from codec.rs (ProtobufDecodeSnafu is still used by mqtt.rs, so only the import in codec.rs was 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 returns Ok(None), never Err.
  • Both share a frame_malformed_payload() helper: a well-framed header wrapping a single 0x00 payload byte, which prost rejects unconditionally as protobuf tag 0 (field number 0 is reserved) — a malformed-payload case that's guaranteed to fail FromRadio::decode independent of the message's oneof variants.
  • Tightened the existing codec_handles_arbitrary_bytes_without_panic proptest to assert result.is_ok() instead of accepting Err as a benign outcome — that leniency is exactly what let this DoS class through the property test in the first place.

Evidence

$ cargo test -p kerykeion codec::
running 16 tests
test codec::tests::decoder_malformed_payload_alone_returns_none ... ok
test codec::tests::decoder_malformed_payload_then_valid_frame_still_decodes ... ok
... (16 passed; 0 failed)

Full gate (vgate kanon gate --stamp):

Summary [6.096s] 773 tests run: 773 passed, 0 skipped
kanon lint: 0 error(s), 0 warning(s), 0 info (209 suppressed)
PASS  cargo fmt
PASS  cargo check (workspace)
PASS  advisory ignore parity
PASS  derive check (deterministic)
PASS  kanon fitness
PASS  dependency audit (cargo deny)
PASS  cargo clippy (workspace)
PASS  cargo nextest (workspace)
PASS  kanon lint
PASS  fleet-names freshness
Gate-Passed: kanon 0.1.10 +stages:fmt,check,clippy,nextest,lint sha:bbfb474b2e665b6f7438fd93bb3550bac233e365

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
forkwright enabled auto-merge (squash) July 17, 2026 14:59
@forkwright
forkwright merged commit 746e8e3 into main Jul 17, 2026
5 checks passed
@forkwright
forkwright deleted the recover-191-codec branch July 17, 2026 15:02
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.

Recover from malformed protobuf payloads instead of erroring out of the decode stream

1 participant