Skip to content

Differential/property tests for the hand-written OTLP wire encoder — a bad length prefix fails silently at the collector #201

Description

@plusky

Subtask of #169 (worth doing later, ranked 6).

otel.rs's wire module hand-rolls protobuf varints and length prefixes. A
wrong length prefix does not throw — the collector silently drops the record,
and the audit-adjacent export goes dark with nothing in the stream to say so.

That failure mode is the argument: everything else in the audit path fails loudly
or not at all, and this one can fail quietly at the far end of a network hop.

Suggested direction

prost and proptest as dev-dependencies; round-trip-decode what wire emits
and assert it matches what was encoded. An afternoon, no CI infrastructure — the
tests run in the existing suite.

Assessed and rejected: full fuzzing

Not worth it here. The byte-level parse surfaces are operator-supplied (config)
or Bugzilla-supplied (responses), and the one attacker-facing parser is small and
heavily tested. The gap is in what we emit, not what we accept.


Corrected direction (2026-09-02)

proptest is not being added. The encoder's input space is narrow and varint
widths are enumerable, so a targeted table is exhaustive where it matters and
deterministic; proptest would pull in rand, rusty-fork, wait-timeout and
unarray for less. prost alone, as a dev-dependency — no protoc, no
.proto, no build.rs
, because the messages are hand-transcribed derives,
which is also what makes them an independent oracle rather than generated code
sharing the encoder's assumptions.

The gap is not where this issue says. The existing hand-written extractor
already decodes the emitted bytes, and it catches all three failure modes named
above (bad length prefix, dropped continuation byte, swapped field number). What
it misses:

  • Wire types. fields() normalizes a decoded varint to to_le_bytes(),
    byte-identical to a fixed64 payload — so a fixed64 field emitted as a
    varint passes. A real collector rejects it.
  • Anything unexpected. only() takes the first match by field number and
    nothing asserts the buffer holds no more, so a duplicated field, an undeclared
    field, or a non-canonical varint encoding are all invisible.

Mutation matrix, reproduced independently: length prefix, continuation byte and
field-number mutations are caught by both old and new; fixed64→varint,
duplicated field, undeclared field and non-canonical varint are caught only by
the new tests
.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-assistedAuthored or substantially written with an AI coding agentrustPull requests that update rust code

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions