Failed notarization WAL write lets a node sign both a finalize vote and an empty vote
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 38/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- go
- Ambito
- distributed-systems, security
Direzione di ricerca
Read simplex/epoch.go around persistNotarization, persistAndBroadcastNotarization, rebroadcastPastFinalizeVotes, and triggerEmptyBlockNotarization. Trace how a failed WAL.Append affects in-memory state and the two signing paths; then inspect the relevant epoch tests for WAL failure injection. Done when a failed append prevents the node from signing both vote types for the same round.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Details
A validator that hits one failed disk write can sign both a finalize vote and an empty vote for the same round. That breaks a rule Simplex safety depends on, and up to f dishonest validators can then form both a finalization and an empty notarization for that round. It needs a transient write-ahead log (WAL) fault on the node plus a relayed finalization for an earlier round. The cause is that persistAndBroadcastNotarization returns the WAL.Append error without halting the epoch or undoing the in-memory notarization.
flowchart TD
n1["peer relaying an earlier finalization"]
n2["rebroadcastPastFinalizeVotes"]
n3["triggerEmptyBlockNotarization"]
n4["round timer from monitorProgress"]
n5["safety of round r (no finalize vote and empty vote together)"]
n6["halt via haltedError after failed append"]
n1 -->|"triggers finalize vote signing"| n2
n2 -->|"signs finalize vote for r"| n5
n4 -->|"fires for r"| n3
n3 -->|"signs empty vote for r"| n5
n6 -->|"not set, node keeps running"| n2
classDef actor stroke:#2563eb,stroke-width:2px
classDef vulnerable_code stroke:#dc2626,stroke-width:2px
classDef existing_control stroke:#16a34a,stroke-width:2px
classDef missing_control stroke:#dc2626,stroke-width:2px,stroke-dasharray:5 5
classDef asset stroke:#9333ea,stroke-width:2px
classDef component stroke:#6b7280,stroke-width:1px
class n1 actor
class n2 vulnerable_code
class n3 vulnerable_code
class n4 component
class n5 asset
class n6 missing_control
- the handlers
maybeCollectNotarizationandhandleNotarizationMessagemark roundras notarized in memory before saving it. They callstoreNotarization, which setsround.notarization, and only afterwards call the persist code. (simplex/epoch.go:1907-1912) - the function
persistNotarizationreturns theWAL.Appenderror beforeincreaseRoundruns. A transient disk fault makes the append fail, so the epoch never leaves roundr. (simplex/epoch.go:1742-1768) - the function
persistAndBroadcastNotarizationpasses the error up without halting or rolling back.e.haltedErroris not set andround.notarizationis not cleared, so memory says roundris notarized while the log has no record of it. (simplex/epoch.go:1771-1775) - the epoch keeps processing messages and time.
HandleMessagestops only oncehaltedErroris set, andAdvanceTimeis never gated. (simplex/epoch.go:136-165) - a peer relays a valid finalization for an earlier, uncommitted round. The finalization has
SeqabovenextSeqToCommit. It goes throughhandleFinalizationMessageandpersistFinalizationintorebroadcastPastFinalizeVotes, which signs a finalize vote for roundrbecause it trusts the in-memorynotarization. (simplex/epoch.go:1418-1461) - the function
triggerEmptyBlockNotarizationsigns and broadcasts an empty vote for roundrwhen the round timer fires. The timer is cancelled only byincreaseRound. The function checks onlye.round > roundand an earlier timeout, never an earlier finalize vote, so it signs. (simplex/epoch.go:2779-2821) - up to
fdishonest validators combine the two signatures into a finalization and an empty notarization for roundr. Once an empty notarization forrexists, a later proposal that skipsris accepted byblockDependencies.
Evidence
- simplex/epoch.go:1771–1775
- simplex/epoch.go:1742–1768
- simplex/epoch.go:1907–1912
- simplex/epoch.go:1418–1461
- simplex/epoch.go:2779–2821
- simplex/epoch.go:136–165
- simplex/epoch.go:2179–2187
Impact
The affected node acts as one extra Byzantine validator for that round. A finalized round can also be skipped, which defeats the safety guarantee of the protocol. The report finds no data disclosure and no outage from this flaw.
Reproduction steps
- The trigger is a local fault the attacker does not control: one
WAL.Appendof a notarization record fails transiently on the victim validator, such as a disk or I/O error. The later appends at:1372and:2805must succeed, so the fault must be transient. After that, any peer can relay a valid finalization for an earlier, uncommitted round, and the report says this needs only low privileges. The finalize vote must be signed before the timer fires, becausemaybeConstructFinalizeVoteMessagerefuses to sign after a timeout. Up tofByzantine validators then collect both signatures.
Recommended fix
(1) After a failed notarization append, persistAndBroadcastNotarization and persistNotarization leave e.rounds[r].notarization set and the epoch running at round r. In-memory state is ahead of the durable log.
Fix criteria: A failed notarization append either halts the epoch, by setting haltedError as the replication paths do and stopping timers from signing, or fully rolls back the in-memory notarization. A test with an injected Append failure shows that no finalize vote is ever signed for that round.
(2) The signing paths do not exclude each other. rebroadcastPastFinalizeVotes trusts the in-memory notarization for the current round, and triggerEmptyBlockNotarization does not check whether a finalize vote was already cast for the round.
Fix criteria: Signing a finalize vote and signing an empty vote for the same round are mutually exclusive, whatever the persistence outcome. A test with any single injected Append failure shows that this node's broadcasts never include both message types for one round.
Status on main
Re-checked against main at 90995e0 on 2026-10-26; still present:
persistNotarization (simplex/epoch.go:1742-1745) returns the writeNotarizationToWal error before increaseRound, and persistAndBroadcastNotarization (epoch.go:1771-1775) propagates it without setting e.haltedError or clearing round.notarization, which storeNotarization already set (epoch.go:1907-1912, 446). HandleMessage (epoch.go:163) only stops once haltedError is set, and AdvanceTime (epoch.go:136-143) is not gated. rebroadcastPastFinalizeVotes (epoch.go:1418-1461) trusts in-memory round.notarization for rounds up to e.round, and triggerEmptyBlockNotarization (epoch.go:2779-2795) checks only e.round>round and timedOut, not a prior finalize vote. Whether this is exploitable depends on the host continuing after HandleMessage returns an error. No PR or issue found.
Severity: MEDIUM
Status: Open
Category: Improper error handling
CWE: CWE-755
Repository: ava-labs/Simplex
Branch: main
Date created: 2026-10-26
Claude Security finding: https://claude.ai/security/findings/find_01JC24yL5oTXowYhuYwx6nDi
- Lingua principale
- Go
- Stelle
- 22
- Fork
- 4
- Merge medio
- 1g 17h
- PR unite (30g)
- 33
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di ava-labs/Simplex
-
low security
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
I maintainer di solito rispondono entro 1 giorno
-
Replicated notarized block dropped when the leader refills the round, stalling the validatorForse già presa @yacovm l’ha presa oggi. Apertamedium security
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
I maintainer di solito rispondono entro 1 giorno
-
Epoch discovery accepts a sealing block on a head count, letting low-stake validators forge epochsForse già presa @yacovm l’ha presa oggi. Apertamedium security
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
I maintainer di solito rispondono entro 1 giorno
-
Replicated empty notarization lets a previous-epoch Telock become the new epoch's chain tipForse già presa @yacovm l’ha presa 1 giorno fa. Apertamedium security
Difficoltà 4/5 3-5 giorni Idoneità per principianti 38/100
ava-labs/Simplex#663 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Builder ignores parent's P-chain height, so a leader's maximal height stalls honest proposalsForse già presa @yacovm l’ha presa 3 giorni fa. Apertamedium security
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di ava-labs/Simplex
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
I maintainer di solito rispondono entro 1 giorno
-
schema-drift
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
stefanhoth/paperless-ngx-cli#61 ·
I maintainer di solito rispondono entro 1 giorno
-
Project submission: CodeAFAperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 72/100
slavakurilyak/awesome-ai-agents#888 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement nav-pilot
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
kind/enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 64/100
I maintainer di solito rispondono entro 1 giorno