fix(kerykeion): bound the wire fields that were trusted to bound themselves - #435
Merged
Conversation
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.
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.
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_positionscaledlatitude_i/longitude_iand stored the result unchecked. Those arei32fields, 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::newalready owns this rule for the whole fleet, so it is reused rather thanrestated. It also rejects
NaN, which a bare range comparison admits.Hop counts
Both call sites cast wire
u32hop fields tou8under 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 = 1000truncates to 232 — outside the very limit thejustification relied on — and
256truncates to 0, which reads as a valid measurement. Thesuppression was justified by a property the code did not enforce.
One shared helper in
types.rsnow rejects anything pastMAX_HOP_LIMIT, next to the constant itbounds 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_startrather than saturating to zero as the processor did. That pairdescribes 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:
MAX_HOP_LIMIT; then 1000,u32::MAX,256 and
MAX_HOP_LIMIT + 1are rejected, with the truncation each would have produced named in theassertion message.
kept.
Scope
Five clauses of #229 remain: PSK index handling for index 0 and above 10,
ChannelPsk.psklengthvalidation, 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