-
Notifications
You must be signed in to change notification settings - Fork 13
Guard against out-of-bounds indexing on attacker-controlled attestation data #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -284,13 +284,17 @@ fn process_attestations( | |
| .entry(target.root) | ||
| .or_insert_with(|| std::iter::repeat_n(false, validator_count).collect()); | ||
| // Mark that each validator in this aggregation has voted for the target. | ||
| // Use get_mut to safely skip bits beyond the validator set — a malicious | ||
| // attestation could carry aggregation_bits longer than validator_count. | ||
| for (validator_id, _) in attestation | ||
| .aggregation_bits | ||
| .iter() | ||
| .enumerate() | ||
| .filter(|(_, voted)| *voted) | ||
| { | ||
| votes[validator_id] = true; | ||
| if let Some(vote) = votes.get_mut(validator_id) { | ||
| *vote = true; | ||
| } | ||
| } | ||
|
|
||
| // Check whether the vote count crosses the supermajority threshold | ||
|
|
@@ -416,10 +420,14 @@ fn try_finalize( | |
| let delta = (state.latest_finalized.slot - old_finalized_slot) as usize; | ||
| justified_slots_ops::shift_window(&mut state.justified_slots, delta); | ||
|
|
||
| // Prune justifications whose roots only appear at now-finalized slots | ||
| // Prune justifications whose roots only appear at now-finalized slots. | ||
| // Use .get() instead of direct index — a root may be absent from root_to_slot | ||
| // if it was never in historical_block_hashes (e.g. carried over from a previous | ||
| // finalization window). Missing roots are conservatively retained. | ||
| justifications.retain(|root, _| { | ||
| let slot = root_to_slot[root]; | ||
| slot > state.latest_finalized.slot | ||
| root_to_slot | ||
| .get(root) | ||
|
Comment on lines
421
to
+429
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale justification entries retained forever, not pruned naturally The comment states retained entries "will be pruned naturally once their slot finalizes", but these entries are in the The only other removal path is The practical consequence is that every such stale entry accumulates permanently in Consider replacing the conservation approach with an explicit prune: if the root's slot is known-and-finalized, remove it; only retain truly unknown roots. That would make the comment accurate and eliminate the accumulation concern: justifications.retain(|root, _| {
match root_to_slot.get(root) {
// Root's slot is known and still above the finalization boundary → keep
Some(&slot) => slot > state.latest_finalized.slot,
// Root not in root_to_slot: already finalized or unknown → prune
None => false,
}
});Alternatively, update the comment to accurately describe the behaviour: these entries are permanently retained and will never be pruned by this path. Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/blockchain/state_transition/src/lib.rs
Line: 421-429
Comment:
**Stale justification entries retained forever, not pruned naturally**
The comment states retained entries *"will be pruned naturally once their slot finalizes"*, but these entries are in the `None` branch precisely because their slot is **already at or below** `state.latest_finalized.slot` — their slot has already been finalized. They will never appear in any future `root_to_slot` map (which always starts at `latest_finalized.slot + 1`), so the `retain` predicate will continue to return `true` for them indefinitely.
The only other removal path is `justifications.remove(&target.root)` at line 319, which fires only when a root reaches supermajority justification. For these stale roots that's prevented: `is_slot_justified` returns `true` for finalized-or-earlier slots (`unwrap_or(true)` in `justified_slots_ops`), so `is_valid_vote` rejects new attestations targeting them. They effectively can never be removed.
The practical consequence is that every such stale entry accumulates permanently in `state.justifications_roots` and `state.justifications_validators` across all subsequent blocks. While bounded by `HistoricalRootsLimit` / `JustificationValidators` capacity, a sustained adversarial workload that repeatedly triggers this path could grow state without a cleanup mechanism. The `expect("justifications_roots limit exceeded")` in `serialize_justifications` is then the only backstop, and it terminates the node rather than pruning gracefully.
Consider replacing the conservation approach with an explicit prune: if the root's slot is known-and-finalized, remove it; only retain truly unknown roots. That would make the comment accurate and eliminate the accumulation concern:
```rust
justifications.retain(|root, _| {
match root_to_slot.get(root) {
// Root's slot is known and still above the finalization boundary → keep
Some(&slot) => slot > state.latest_finalized.slot,
// Root not in root_to_slot: already finalized or unknown → prune
None => false,
}
});
```
Alternatively, update the comment to accurately describe the behaviour: these entries are *permanently* retained and will never be pruned by this path.
How can I resolve this? If you propose a fix, please make it concise. |
||
| .is_none_or(|&slot| slot > state.latest_finalized.slot) | ||
|
Comment on lines
+428
to
+430
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
| }); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to check what the spec does in this case