Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Failed notarization WAL write lets a node sign both a finalize vote and an empty vote

オープン
#658 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@yacovm がすでに取り組んでいます。

2026年10月27日 から。

  • #672 @yacovm による — オープン

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
38/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
go

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

medium security

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
  1. the handlers maybeCollectNotarization and handleNotarizationMessage mark round r as notarized in memory before saving it. They call storeNotarization, which sets round.notarization, and only afterwards call the persist code. (simplex/epoch.go:1907-1912)
  2. the function persistNotarization returns the WAL.Append error before increaseRound runs. A transient disk fault makes the append fail, so the epoch never leaves round r. (simplex/epoch.go:1742-1768)
  3. the function persistAndBroadcastNotarization passes the error up without halting or rolling back. e.haltedError is not set and round.notarization is not cleared, so memory says round r is notarized while the log has no record of it. (simplex/epoch.go:1771-1775)
  4. the epoch keeps processing messages and time. HandleMessage stops only once haltedError is set, and AdvanceTime is never gated. (simplex/epoch.go:136-165)
  5. a peer relays a valid finalization for an earlier, uncommitted round. The finalization has Seq above nextSeqToCommit. It goes through handleFinalizationMessage and persistFinalization into rebroadcastPastFinalizeVotes, which signs a finalize vote for round r because it trusts the in-memory notarization. (simplex/epoch.go:1418-1461)
  6. the function triggerEmptyBlockNotarization signs and broadcasts an empty vote for round r when the round timer fires. The timer is cancelled only by increaseRound. The function checks only e.round > round and an earlier timeout, never an earlier finalize vote, so it signs. (simplex/epoch.go:2779-2821)
  7. up to f dishonest validators combine the two signatures into a finalization and an empty notarization for round r. Once an empty notarization for r exists, a later proposal that skips r is accepted by blockDependencies.

Evidence

  1. simplex/epoch.go:1771–1775
  2. simplex/epoch.go:1742–1768
  3. simplex/epoch.go:1907–1912
  4. simplex/epoch.go:1418–1461
  5. simplex/epoch.go:2779–2821
  6. simplex/epoch.go:136–165
  7. 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

  1. The trigger is a local fault the attacker does not control: one WAL.Append of a notarization record fails transiently on the victim validator, such as a disk or I/O error. The later appends at :1372 and :2805 must 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, because maybeConstructFinalizeVoteMessage refuses to sign after a timeout. Up to f Byzantine 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

主要言語
Go
スター
22
フォーク
4
平均マージ
1日 17時間
マージ済み PR(30日)
33

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

ava-labs/Simplex のほかの issue

ava-labs/Simplex の issue をすべて見る

似ている issue

Go の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。