ava-labs / ava-labs/Simplex

epochDigestCounter grows unbounded under attacker-chosen sequence keys that are never pruned

Open
#563 0 comments 0 reactions 0 assignees View on GitHub
low security
Dominant language
Go
Stars
22
Forks
4
Avg merge
2d 14h
Merged PRs (30d)
34

Description

## Details
The epochDigestCounter records sealing-block attestations in sealingBlockResponses, a map keyed by the sequence number read from the sender's UNVERIFIED block header (bh.Seq). A malicious validator can send a stream of ReplicationResponses, each containing a fabricated quorum round (block with a non-nil SealingBlockInfo, an unknown epoch number, and a digest-consistent fake finalization so it passes VerifyQCConsistentWithBlock) with a distinct, arbitrarily large Seq. Each message allocates a new outer map entry plus inner map (sender -> digest). Cleanup (removeOldEpochs) deletes only keys strictly below the epoch of the last indexed block, so keys chosen near 2^64 are never removed for the process lifetime — the memory is a permanent leak rather than transient buffering, growing linearly and without bound in the number of attacker messages. The counter is unreachable for non-validator senders (validators.Contains(from) gate), so validator privileges are required.

## Evidence
1. [nonvalidator/epochs.go:182–190](https://github.com/ava-labs/Simplex/blob/main/nonvalidator/epochs.go#L182-L190)
collectedSealingBlockInfo creates a new sealingBlockResponses[newEpoch] map entry for every distinct bh.Seq taken from the sender's unverified block header; a validator can send quorum rounds with arbitrary distinct Seq values, each allocating a persistent entry.
2. [nonvalidator/epochs.go:208–213](https://github.com/ava-labs/Simplex/blob/main/nonvalidator/epochs.go#L208-L213)
removeOldEpochs only deletes keys strictly below minEpochToKeep (the epoch of the last indexed block). Keys chosen near MaxUint64 are never pruned for the lifetime of the process.
3. [nonvalidator/non\_validator.go:532–540](https://github.com/ava-labs/Simplex/blob/main/nonvalidator/non_validator.go#L532-L540)
Reachable from handleQrFromUnknownEpoch for any quorum round whose block carries a SealingBlockInfo and an unknown epoch; the only gate is that the sender is in the current validator set.

## Impact
Persistent, unprunable memory growth on the non-validator, eventually degrading or crashing the process; each entry is small so exhaustion requires a large number of messages, and a restart clears the state. No confidentiality or integrity impact.

## Reproduction steps
1. Attacker must be a current validator (membership check against Comm.Validators()). It streams replication responses with fabricated sealing blocks carrying distinct huge sequence numbers; every message permanently allocates map state on the non-validator. Impact accumulates with sustained traffic since per-entry size is small.

## Recommended fix
sealingBlockResponses is keyed by an unvalidated, attacker-chosen sequence number, has no cap on the number of tracked epochs, and its pruning predicate (strictly below the current epoch) never removes keys above the chain's progress. Fix criteria: Bound the number of tracked prospective epochs (e.g., only track sequences within a window above the current tip, or cap entries per sender) and prune keys that cannot correspond to real epochs. Verify a validator streaming distinct huge sequence numbers cannot grow the map beyond a fixed bound.

---
**Severity:** LOW
**Status:** Open
**Category:** Allocation without limits
**CWE:** [CWE-770](https://cwe.mitre.org/data/definitions/770.html)
**Repository:** ava-labs/Simplex
**Branch:** main
**Date created:** 2026-08-21

---

Contributor guide

No contributing guide indexed for this repository

Research direction

Read nonvalidator/epochs.go around collectedSealingBlockInfo and removeOldEpochs, then trace the handleQrFromUnknownEpoch path in nonvalidator/non_validator.go. Reproduce the validator-message sequence described in the issue and inspect sealingBlockResponses growth. Done means distinct attacker-chosen sequence numbers cannot make tracked state exceed a fixed bound and normal epoch cleanup still works.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
distributed-systems, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.