Skip to content

fix(kerykeion): bound the wire fields that were trusted to bound themselves - #435

Merged
forkwright merged 2 commits into
mainfrom
fix/229-validate-wire-fields
Aug 21, 2026
Merged

fix(kerykeion): bound the wire fields that were trusted to bound themselves#435
forkwright merged 2 commits into
mainfrom
fix/229-validate-wire-fields

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Summary

Two of #229's seven surviving clauses. Both are the same defect: a value read straight off the radio
was trusted to satisfy a range that only Meshtastic firmware maintains.

Positions

handle_position scaled latitude_i/longitude_i and stored the result unchecked. Those are i32
fields, so their extremes scale to ±214.7 degrees — a hostile or merely broken neighbour could
place a node off the planet, and the value flowed on into the topology and signal paths.

koinon::Coordinates::new already owns this rule for the whole fleet, so it is reused rather than
restated. It also rejects NaN, which a bare range comparison admits.

Hop counts

Both call sites cast wire u32 hop fields to u8 under an #[expect(clippy::cast_possible_truncation)]
whose stated reason was that the values are "bounded by MAX_HOP_LIMIT (7) in Meshtastic firmware."

The cast did not hold that bound. hop_start = 1000 truncates to 232 — outside the very limit the
justification relied on — and 256 truncates to 0, which reads as a valid measurement. The
suppression was justified by a property the code did not enforce.

One shared helper in types.rs now rejects anything past MAX_HOP_LIMIT, next to the constant it
bounds by, and both call sites delegate to it. No cast suppression remains: u8::try_from(..).ok()
says the same thing without one.

It also rejects hop_limit > hop_start rather than saturating to zero as the processor did. That pair
describes no journey, and reporting zero hops for it invents a measurement the packet never carried.

Tests

Every rejection case has an anti-vacuity partner, because a validator that refuses everything would
satisfy the rejections while breaking the protocol:

  • Hop: ordinary pairs still produce counts, including exactly MAX_HOP_LIMIT; then 1000, u32::MAX,
    256 and MAX_HOP_LIMIT + 1 are rejected, with the truncation each would have produced named in the
    assertion message.
  • Position: four out-of-range shapes are discarded and not stored; a position at exactly ±90/±180 is
    kept.

Scope

Five clauses of #229 remain: PSK index handling for index 0 and above 10, ChannelPsk.psk length
validation, trial-decrypt channel attribution accepting any decodable plaintext, unbounded
peer-supplied node/channel accumulation in the handshake, and the unbounded outbound pending queue.

Refs #229

forkwright added 2 commits August 21, 2026 00:47
…selves

Two of #229's surviving clauses, both the same shape: a value read straight off
the radio was trusted to satisfy a range that only Meshtastic firmware
maintains. A neighbour that is hostile or merely wrong is not running that
firmware.

Positions were persisted unchecked. latitude_i is an i32, so its extremes scale
to plus or minus 214.7 degrees -- a node could be placed off the planet, and the
value flowed on into the topology and signal paths. koinon's Coordinates::new
already owns this rule for the whole fleet, so it is reused rather than restated;
it also rejects NaN, which a bare range comparison admits.

Hop counts were cast to u8 under an expect whose reason cited the firmware
bound. The cast did not hold that bound: a hop_start of 1000 truncates to 232,
outside the very limit the justification relied on, and 256 truncates to zero.
Both call sites now share one helper that rejects anything past MAX_HOP_LIMIT.

The helper also rejects a hop_limit above hop_start rather than saturating it to
zero, as the processor did. That pair describes no journey, and reporting zero
hops for it invents a measurement the packet never carried.

Each case has an anti-vacuity partner: ordinary hop fields still produce a
count, and a position exactly at plus or minus 90 and 180 is still kept.

Refs #229
clippy::doc_markdown applies to doc comments on test functions too, not only on
the public API.
@forkwright
forkwright merged commit e7b4f45 into main Aug 21, 2026
11 checks passed
@forkwright
forkwright deleted the fix/229-validate-wire-fields branch August 21, 2026 05:58
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