fix: saturate Fin sequence arithmetic in proposal-part streaming#195
Open
memosr wants to merge 4 commits into
Open
fix: saturate Fin sequence arithmetic in proposal-part streaming#195memosr wants to merge 4 commits into
memosr wants to merge 4 commits into
Conversation
Add a Networks section to the README documenting Arc's official chain IDs and the correct wallet configuration for Arc Testnet, with a prominent note that the incorrectly-circulated value 1516 is wrong. Fixes circlefin#94
Document that testnet.arcscan.app and docs.arc.network are the canonical Arc Testnet explorer and docs, and that the dead explorer.testnet.arc.network, explorer.arc.io, and docs.arc.io URLs should not be used. Fixes circlefin#81
…laim docs.arc.io is the live canonical docs host (docs.arc.network 301-redirects to it), so it does not belong in the dead-URL list. Keep the note focused on the block explorer: testnet.arcscan.app is canonical; explorer.testnet.arc.network (unreachable) and explorer.arc.io (team-login-gated) are not publicly usable.
A Fin stream message carries an unvalidated u64 `sequence` straight off the wire. `StreamState::insert` computed the stream's expected message count as `msg.sequence as usize + 1`, which a malicious peer could drive to overflow by sending `sequence = u64::MAX`: a panic under debug-assertions and a wrap-to-zero in release builds. Release builds happened to stay safe because the subsequent `buffer.len() == expected_messages` check never matches a wrapped-to-zero target, so the stream is left incomplete and later evicted. But the guarantee rested on wrapping behaviour rather than intent, and the accompanying comment justified the arithmetic with a false invariant: `expected_messages` is assigned from the unbounded `msg.sequence`, not from the bounded `message_count`. Use `saturating_add(1)` so the worst case is `usize::MAX` — an unreachable completion target the stream is evicted for — instead of a panic or a silent wrap. Comment corrected to describe the real bound. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Nice fix — the On the README "Networks" section: can independently corroborate both corrections from our own testnet integration —
Good to see these codified in the README directly rather than left to circulate informally — the |
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
StreamState::insertin the consensus proposal-part streaming layercomputed a stream's expected message count from the Fin message's
sequencefield asmsg.sequence as usize + 1.sequenceis anunvalidated
u64decoded straight off the gossip wire(
crates/types/src/codec/proto.rs—sequence: proto.sequence, nobound-check), and
PartStreamsMap::insertapplies no upper bound on itbefore reaching this arithmetic. A peer on the consensus gossip topic can
therefore send a Fin part for the current height with
sequence = u64::MAXand drive the+ 1to overflow:0.Release builds are incidentally safe today: the later
buffer.len() == expected_messagescompletion check never matches awrapped-to-zero target, so the stream stays
Incompleteand is evictedby the age/limit sweep. No crash, no unbounded memory (already capped by
MAX_MESSAGES_PER_STREAM). The safety rested on wrapping behaviourrather than intent, and the old comment asserted a false invariant — it
claimed the
+ 1"cannot overflow because MAX_MESSAGES_PER_STREAM <<u64::MAX", but
expected_messagesis assigned from the unboundedmsg.sequence, not from the boundedmessage_count.Change
saturating_add(1): worst case isusize::MAX, an unreachablecompletion target the stream is evicted for — never a panic or a silent
wrap-to-zero.
source of
sequence.clippy::arithmetic_side_effectsallow, sincethe arithmetic is checked.
Impact
Eliminates a remotely reachable panic in debug/test builds and removes
release builds' latent reliance on wrap-to-zero. No behavioural change on
valid streams (
sequencevalues are< MAX_MESSAGES_PER_STREAMinpractice). No public API or wire-format change; the touched method is
module-private and
PartStreamsMap::insert's signature is unchanged.Test plan
cargo build -p arc-node-consensus— cleancargo test -p arc-node-consensus streaming— 46 passed, 0 failed(includes property tests for per-stream / total stream limits),
run under debug-assertions
🤖 Generated with Claude Code