Skip to content

Commit 9c044a2

Browse files
feat(docs): severity-axis figures + security checklist & unit/e2e test plan
- fig11 (subsystem×severity and root_cause×severity heatmaps) and fig10 (3D subsystem×severity bars) answer "by severity, which areas/causes are heavy vs light"; wired into analysis.md §7. - docs/checklist.md: a prioritized security checklist built from the inline fixes — per-subsystem review items (each citing a real fix) plus a unit/e2e test matrix, ordered by impact (consensus/value core first, then availability), closing with cross-client differential/adversarial-devnet tests. - Linked from the audit guide's playbook and the README docs index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4f66ab5 commit 9c044a2

7 files changed

Lines changed: 242 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ docs/ BUILD_REPORT · IMPROVEMENT_LOG · silent_fix_detection · mode
219219
## Documentation
220220

221221
- [`docs/security_report.md`](docs/security_report.md) — 🔎 **Auditing Ethereum clients: where the bugs actually live** — a field guide for client devs, audit firms, and white-hats: where to look, the six recurring bug patterns, the attack surface, and the cross-implementation variant hunting that turns one client's fix into a lead on another's live bug
222+
- [`docs/checklist.md`](docs/checklist.md) — ✅ **Security checklist & test plan** — per-subsystem review items and unit/e2e tests derived from the actual fixes, ordered by impact
222223
- [`docs/analysis.md`](docs/analysis.md)**what the data says** (silent-fix majority, availability-first vuln profile, cross-language diversity), read through the dataset-research literature
223224
- [`docs/limitations.md`](docs/limitations.md)**honest inventory of coverage gaps & caveats** (read before relying on the data)
224225
- [`docs/severity_labeling.md`](docs/severity_labeling.md)**methodology**: LLM severity estimation against the bug-bounty model (decompose → map → calibrate)

docs/analysis.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,19 @@ publish advisories more than which clients are safer, and fix size does not trac
137137
severity (median 51 vs 45 LOC), so neither is a shortcut to finding the severe
138138
bugs.
139139

140+
Adding the severity axis to the breakdowns answers the practical question — *by
141+
severity, which areas and causes are heavy, and which are light.*
142+
143+
![Fixes by subsystem and root cause, split by severity](figures/fig11_severity_heatmap.png)
144+
145+
High-severity fixes cluster where the impact model predicts: the state trie, p2p,
146+
sync, and fork-choice by subsystem; `missing_input_validation`,
147+
`resource_exhaustion`, `integer_overflow`, and `consensus_divergence` by cause.
148+
The state trie is dominated by Medium, p2p carries a relatively high share of High.
149+
(The same data as a 3D bar chart is `figures/fig10_severity_3d.png` — striking,
150+
but the heatmap reads exact counts better.) This view drives the prioritized
151+
[security checklist & test plan](./checklist.md).
152+
140153
## 8. Data quality and coverage
141154

142155
![Figure 6](figures/fig6_coverage.png)

docs/checklist.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Ethereum client security checklist & test plan
2+
3+
A working checklist for auditing or hardening an Ethereum client, built by reading
4+
the actual fixes in this corpus and keeping the ones that recur. Items are ordered
5+
by impact: the consensus and value core first (a bug there splits the chain or
6+
moves ETH), then the availability surface (a bug there drops the node). Each item
7+
names a real fix that motivates it and how to test it.
8+
9+
Use it two ways: as a review checklist against an implementation, and as a test
10+
matrix — the *unit* column is what to fuzz or property-test in isolation, the
11+
*e2e* column is what to exercise on a multi-client devnet.
12+
13+
![Where the severe bugs cluster](figures/fig11_severity_heatmap.png)
14+
15+
The heatmap is the quick orientation: `missing_input_validation`,
16+
`resource_exhaustion`, `integer_overflow`, and `consensus_divergence` carry the
17+
most High-severity fixes, and they land hardest in the state trie, p2p, sync,
18+
fork-choice, and the beacon-chain state transition.
19+
20+
---
21+
22+
## Tier 1 — Consensus & value core
23+
24+
A wrong result here is a chain split or forged value. Read this code line by line.
25+
26+
### EVM, opcodes & precompiles
27+
28+
- [ ] **Every opcode handles boundary operands** (zero, max, oversized shift) with
29+
the spec's result and no native exception. *(besu: SHL/SHR/SAR native exception
30+
at key values; geth: RETURNDATA corruption via `datacopy`)*
31+
- [ ] **Gas accounting matches the spec exactly**, with no signed/unsigned or
32+
overflow slip. *(besu: Gas allocation error in CALL — Critical; geth: DoS via
33+
`MulMod`)*
34+
- [ ] **Return-data and memory buffers are bounds-checked before any copy.**
35+
*(geth: RETURNDATA corruption via `datacopy`)*
36+
- [ ] **Precompiles copy, not alias, their inputs/outputs.** *(geth: shallow copy
37+
in the 0x4 precompile)*
38+
39+
| | test |
40+
|---|---|
41+
| **unit** | differential opcode/precompile tests vs the reference spec (EELS) over boundary operands; property-test gas math for overflow; fuzz each opcode's stack inputs |
42+
| **e2e** | official state tests + blockchain tests; execute a block of adversarial opcodes across all clients on a devnet and compare state roots |
43+
44+
### State-transition arithmetic (gas, balance, stake, slots)
45+
46+
- [ ] **All balance / gas / stake / slot arithmetic is overflow- and
47+
underflow-checked**, including empty-collection edge cases. *(lighthouse: Eth1
48+
data underflow; underflow in `verify_transfer`; underflow in shuffle with an
49+
empty list)*
50+
51+
| | test |
52+
|---|---|
53+
| **unit** | property/fuzz tests asserting no under/overflow on extreme and empty inputs |
54+
| **e2e** | process adversarial blocks and states on a devnet; assert no panic and cross-client agreement |
55+
56+
### Fork-choice
57+
58+
- [ ] **Graph traversals are bounded** — no O(n²) `find_head`, no unbounded
59+
recursion or stack growth. *(lighthouse: O(n²) `find_head` and stack overflow in
60+
`filter_block_tree`)*
61+
- [ ] **Zero/null block hashes and missing parents are handled, not assumed.**
62+
*(lighthouse: avoid 0x00 block hashes in `forkchoiceUpdated`)*
63+
- [ ] **Message timing cannot bias the head** (proposer boost, late/early
64+
attestations). *(lighthouse: fork-choice timing attack)*
65+
66+
| | test |
67+
|---|---|
68+
| **unit** | fork-choice tests with crafted block trees, equivocations, zero hashes, deep chains |
69+
| **e2e** | devnet reorg scenarios; proposer-boost edge cases; delayed and duplicated messages |
70+
71+
### State trie & snapshots
72+
73+
- [ ] **Trie/snapshot code returns errors instead of panicking** on malformed or
74+
short nodes. *(geth: stacktrie explicit errors instead of panic; snapshot unlock
75+
before return/panic)*
76+
- [ ] **Concurrent access to layers/snapshots is race-free.** *(geth: race
77+
condition on `diffLayer`)*
78+
79+
| | test |
80+
|---|---|
81+
| **unit** | feed malformed/truncated trie nodes; run the race detector over concurrent layer ops |
82+
| **e2e** | snap-sync from an adversarial peer serving crafted state |
83+
84+
### Crypto & KZG
85+
86+
- [ ] **Curve points, signatures, and proofs are validated** (on-curve, subgroup,
87+
length) before use, and parsing never panics. *(geth: invalid-curve DoS in
88+
secp256k1)*
89+
- [ ] **No `expect`/`unwrap`/assert on attacker-supplied crypto input.**
90+
*(lighthouse: remove `expect` in `kzg_utils`)*
91+
92+
| | test |
93+
|---|---|
94+
| **unit** | fuzz signature/point/proof parsers with invalid and boundary inputs |
95+
| **e2e** | gossip invalid BLS signatures and KZG proofs on a devnet; the node must reject them, not crash |
96+
97+
---
98+
99+
## Tier 2 — Availability surface
100+
101+
A bug here is a remote denial of service. Audit for bounds on anything a peer can
102+
control.
103+
104+
### p2p & discovery
105+
106+
- [ ] **Every length/count field from a peer is bounded** before allocation or
107+
iteration. *(pattern P1: LES `GetProofsV2` DoS; malicious snap/1 request)*
108+
- [ ] **Decode paths never panic** on malformed messages. *(geth: p2p/discover
109+
crash in `Resolve`; les panic)*
110+
111+
| | test |
112+
|---|---|
113+
| **unit** | fuzz every wire-message decoder; assert allocation stays bounded relative to declared sizes |
114+
| **e2e** | flood a node with malformed and oversized gossip/request messages; memory and CPU must stay bounded |
115+
116+
### Sync
117+
118+
- [ ] **Downloader concurrency is correct** — no nil, mutex, or timeout-
119+
resurrection panics under failure. *(geth: timeout resurrection panic; mutex
120+
regression panics; nil panic from wrong variable)*
121+
122+
| | test |
123+
|---|---|
124+
| **unit** | race detector plus fault injection on the downloader queue |
125+
| **e2e** | sync against a peer that stalls, reorders, or withholds responses |
126+
127+
### RPC
128+
129+
- [ ] **Request parameters are validated and result sizes are bounded** — no
130+
unbounded proof or range queries. *(reth: trie-proof edge cases; LES
131+
`GetProofsV2` DoS)*
132+
133+
| | test |
134+
|---|---|
135+
| **unit** | fuzz RPC param decoding; assert response-size limits |
136+
| **e2e** | hammer the RPC with expensive/oversized queries; rate and size limits must hold |
137+
138+
### Transaction pool
139+
140+
- [ ] **Malformed and edge-case transactions are rejected without panic** (bad
141+
signature length, bad sender). *(geth: panic on invalid signature length; panic
142+
on bad tx sender; nil statedb panic in `applyTransaction`)*
143+
144+
| | test |
145+
|---|---|
146+
| **unit** | fuzz tx decoding and validation |
147+
| **e2e** | submit adversarial transaction streams; the pool must stay healthy |
148+
149+
### Beacon-chain queues (attestation / block / slasher)
150+
151+
- [ ] **Per-peer queues are memory-bounded** and cannot be grown without limit.
152+
*(lighthouse: reprocess-queue memory leak; slasher OOM)*
153+
154+
| | test |
155+
|---|---|
156+
| **unit** | queue-bound tests under flood |
157+
| **e2e** | gossip flood of attestations and blocks; memory must stay bounded |
158+
159+
---
160+
161+
## The highest-value tests: cross-client conformance
162+
163+
The bugs unique to this ecosystem live *between* implementations, so the tests
164+
that find them run more than one client at once.
165+
166+
- [ ] **Differential testing.** Feed identical EVM, SSZ, and epoch-processing
167+
inputs to every client and diff the output. Any disagreement is a candidate
168+
chain split.
169+
- [ ] **Adversarial devnet.** Run a mixed-client network with one malicious peer
170+
driving the P1–P6 patterns: malformed gossip, oversized requests, equivocations,
171+
crafted reorgs, boundary-value transactions. Assert liveness, bounded resources,
172+
and a single canonical head.
173+
- [ ] **Spec-divergence fuzzing.** Mutate consensus-critical inputs (opcode
174+
operands, precompile inputs, SSZ containers) and cross-check every client against
175+
the reference spec.
176+
177+
---
178+
179+
*Sources: every checklist item traces to one or more fixes in
180+
`data/ethereum_vulns.parquet` (filter by `label` and `root_cause`); the patterns
181+
P1–P6 and the priority map are in [`security_report.md`](./security_report.md).*

docs/figures/fig10_severity_3d.png

131 KB
Loading
112 KB
Loading

docs/security_report.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ in the consensus path, which is what turned an off-by-something into a Critical.
183183
present in one and missing in another is a candidate.
184184
5. Rank what you find by the damage one packet or transaction could do.
185185

186+
Working from these patterns as concrete, per-subsystem review items and tests?
187+
See the [security checklist & test plan](./checklist.md).
188+
186189
## Caveats and disclosure
187190

188191
Labels are model- and heuristic-derived (about 0.90 precision), not hand-verified.

scripts/make_figures.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,47 @@ def impact_class(a):
186186
loc="lower right",frameon=False,fontsize=8.5)
187187
plt.tight_layout(); plt.savefig(f"{FG}/fig9_priority_map.png",bbox_inches="tight"); plt.close()
188188
print("fig9 written")
189+
190+
# ---- FIG 10: 3D — subsystem × severity × count -----------------------------
191+
from mpl_toolkits.mplot3d import Axes3D # noqa
192+
se=d.severity_estimated.fillna("")
193+
d['_tier']=se.where(se.isin(['Critical','High','Medium','Low']),'—')
194+
TIERS=['Low','Medium','High','Critical']
195+
TCOL={'Low':TEAL,'Medium':ORANGE,'High':RED,'Critical':'#7b1fa2'}
196+
skip={'other','build-ci','test','cli','metrics-observability','docs'}
197+
subs=[a for a in d.label.value_counts().index if a not in skip][:12][::-1]
198+
fig=plt.figure(figsize=(11,6.6)); ax=fig.add_subplot(111,projection='3d')
199+
for yi,t in enumerate(TIERS):
200+
zs=[int(((d.label==s)&(d._tier==t)).sum()) for s in subs]
201+
xs=np.arange(len(subs))
202+
ax.bar3d(xs, np.full(len(subs),yi), np.zeros(len(subs)), 0.7, 0.55, zs,
203+
color=TCOL[t], shade=True, alpha=0.92)
204+
ax.set_xticks(np.arange(len(subs))+0.35); ax.set_xticklabels(subs,rotation=40,ha='right',fontsize=7.5)
205+
ax.set_yticks(np.arange(len(TIERS))+0.25); ax.set_yticklabels(TIERS,fontsize=8.5)
206+
ax.set_zlabel("fixes",fontsize=9)
207+
ax.set_title("Fixes by subsystem × severity (severity incl. estimated)",fontsize=12.5,color=INK,fontweight="bold")
208+
ax.view_init(elev=22,azim=-58); ax.set_box_aspect((2.0,1.0,0.8))
209+
plt.tight_layout(); plt.savefig(f"{FG}/fig10_severity_3d.png",bbox_inches="tight"); plt.close()
210+
211+
# ---- FIG 11: heatmaps — subsystem×severity and root_cause×severity ---------
212+
def heat(ax,rows,rowlab,title):
213+
M=np.array([[int(((idx==r)&(d._tier==t)).sum()) for t in TIERS] for r in rows])
214+
im=ax.imshow(M,cmap="YlOrRd",aspect="auto")
215+
ax.set_xticks(range(len(TIERS))); ax.set_xticklabels(TIERS,fontsize=9)
216+
ax.set_yticks(range(len(rows))); ax.set_yticklabels(rowlab,fontsize=9)
217+
for i in range(len(rows)):
218+
for j in range(len(TIERS)):
219+
v=M[i,j]
220+
if v: ax.text(j,i,v,ha="center",va="center",fontsize=8,
221+
color="white" if v>M.max()*0.55 else "#333")
222+
ax.set_title(title,loc="left",fontsize=12,color=INK,fontweight="bold")
223+
ax.grid(False)
224+
return im
225+
subs2=[a for a in d.label.value_counts().index if a not in skip][:12]
226+
rcs=[c for c in d.root_cause.value_counts().head(9).index if c!='other'][:8]
227+
fig,(a1,a2)=plt.subplots(1,2,figsize=(12,5.2))
228+
idx=d.label; heat(a1,subs2,subs2,"(a) subsystem × severity")
229+
idx=d.root_cause; im=heat(a2,rcs,[r.replace('_',' ') for r in rcs],"(b) root cause × severity")
230+
fig.colorbar(im,ax=a2,fraction=0.046,pad=0.04,label="fixes")
231+
plt.tight_layout(); plt.savefig(f"{FG}/fig11_severity_heatmap.png",bbox_inches="tight"); plt.close()
232+
print("fig10, fig11 written")

0 commit comments

Comments
 (0)