Skip to content

Commit ea9ec3c

Browse files
docs: add security-researcher severity analysis (§7) + fig7
New section and figure on what raises severity: (a) attacker-reachability drives it — consensus_divergence (lift x1.55), resource_exhaustion (x1.39), integer overflow (x1.28) are over-represented in Critical/High, while race_condition has lift ~0 (needs local timing). (b) The central paradox: the severe classes are patched silently 91-98% of the time (consensus_divergence 162/174 = 93% unrated), so the rated column understates the severe population ~10x. Reporting bias: geth = 41% of rated-high because it publishes advisories, not because it has more severe bugs; fix size doesn't separate severity. scripts/make_figures.py adds fig7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e558d4f commit ea9ec3c

3 files changed

Lines changed: 81 additions & 6 deletions

File tree

docs/analysis.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ security fixes across the eleven production Ethereum clients.*
77
> **Summary.** The corpus is dominated by *silently patched* fixes (≈94% ship
88
> with no advisory), its vulnerability profile is **availability- and
99
> consensus-centric** rather than the memory-corruption profile of generic C/C++
10-
> datasets, its fixes are **localized** (43% single-file), and it spans **six
10+
> datasets, its fixes are **localized** (43% single-file), its **severe classes are the ones most often patched silently**, and it spans **six
1111
> languages implementing one protocol** — a diversity axis absent from prior
1212
> vulnerability datasets. We interpret each finding against the
1313
> vulnerability-dataset literature (CVEfixes, BigVul, Devign, CrossVul,
@@ -112,7 +112,49 @@ across implementations. It is the diversity dimension DiverseVul and CrossVul
112112
argue reduces overfitting, obtained here **within a single well-specified
113113
domain**.
114114

115-
## 7. Data quality and coverage
115+
## 7. A security-researcher reading — what raises severity
116+
117+
Only 143 rows carry a rated severity and 66 are Critical/High, so this is a small,
118+
biased sample (see the reporting-bias caveat below) — but the signal is sharp.
119+
120+
![Figure 7](figures/fig7_severity_drivers.png)
121+
122+
**(a) Attacker-reachability is the severity driver.** Among Critical/High fixes,
123+
38% are triggered by `malformed_input` and the rest by `malicious_tx` /
124+
`malicious_p2p_message` — externally reachable, adversary-controlled paths. By
125+
root cause, three classes are *over-represented* in the high-severity slice:
126+
**consensus_divergence (lift ×1.55)**, **resource_exhaustion / DoS (×1.39)**, and
127+
**integer_overflow (×1.28)**. Strikingly, **`race_condition` has lift ≈ 0** — it
128+
is 10% of all fixes but essentially never rated Critical/High, because it
129+
typically needs local timing rather than a remote trigger. Severity here tracks
130+
*reachability × blast-radius* (chain split, node crash, fund-affecting
131+
arithmetic), not code-level bug class alone.
132+
133+
**(b) The severe classes are the ones most often patched silently — the central
134+
paradox.** Every root cause, including the severe ones, is shipped *silently*
135+
91–98% of the time: **consensus_divergence is 93% unrated, integer_overflow 96%,
136+
resource_exhaustion 94%.** Concretely, of **174 `consensus_divergence` fixes only
137+
12 are rated — 162 (93%) carry no severity at all**, despite consensus divergence
138+
being the single highest-severity-lift class. The rated-severity column therefore
139+
*understates* the severe population by roughly an order of magnitude.
140+
141+
**Reporting bias, not a severity map.** Geth accounts for **41% (27/66)** of all
142+
Critical/High rows — not because Geth has more severe bugs, but because it
143+
publishes GitHub Security Advisories while most clients patch silently. So the
144+
rated slice is a **publication artifact**: a client's presence in it measures its
145+
disclosure policy, not its security posture. And fix size does **not** separate
146+
severity (median 51 LOC high-severity vs 45 overall) — you cannot spot a critical
147+
bug by diff size.
148+
149+
*Takeaways for a researcher.* (i) Prioritize by **attacker-reachability ×
150+
subsystem** (p2p, rpc, crypto, consensus state-transition) rather than by whether
151+
a CVE exists. (ii) The **unrated `consensus_divergence` / `resource_exhaustion`
152+
rows are a hunting ground** for under-triaged severe bugs — the corpus surfaces
153+
exactly the silent, high-impact fixes that CVE-anchored datasets miss. (iii)
154+
Because one spec is implemented eleven ways, a severe fix in one client is a lead
155+
to look for its **silent analogue in the others** (§6).
156+
157+
## 8. Data quality and coverage
116158

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

@@ -137,7 +179,7 @@ The low bars — `severity` (6.4%) and `silent_fix_prob` (40%) — are structura
137179
not defects: unrated severity *is* the silent-fix signal (§2), and full-commit
138180
LLM classification was deliberately bounded (§8).
139181

140-
## 8. Implications for use
182+
## 9. Implications for use
141183

142184
1. **Selection under a <1% base rate.** Security fixes are a fraction of a
143185
percent of commits — VulFixMiner's "needle in a haystack." The pipeline
115 KB
Loading

scripts/make_figures.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env python3
22
"""make_figures.py — regenerate docs/figures/*.png from data/ethereum_vulns.parquet.
3-
4-
Run from the repo root: uv run --with matplotlib python scripts/make_figures.py
5-
Used by docs/analysis.md."""
3+
Run from repo root: uv run --with matplotlib python scripts/make_figures.py"""
4+
import pandas as pd, numpy as np, json
5+
import matplotlib as mpl; mpl.use("Agg")
66
import matplotlib.pyplot as plt
77
from matplotlib.ticker import PercentFormatter
88

@@ -130,3 +130,36 @@ def loc(s):
130130
print("figures written to",FG)
131131
import os
132132
for f in sorted(os.listdir(FG)): print(" ",f, round(os.path.getsize(FG+"/"+f)/1024),"KB")
133+
134+
# ---- FIG 7: what raises severity (security-researcher view) -----------------
135+
sev=d.severity.str.lower()
136+
hi=d[sev.isin(['critical','high'])]
137+
causes=[c for c in d.root_cause.value_counts().head(8).index if c!='other']
138+
lift=[]; silent=[]
139+
for rc in causes:
140+
p_all=(d.root_cause==rc).mean(); p_hi=(hi.root_cause==rc).mean()
141+
lift.append(p_hi/p_all if p_all else 0)
142+
sub=d[d.root_cause==rc]
143+
silent.append(100*(~sub.severity.str.lower().isin(['critical','high','medium','low'])).mean())
144+
order=np.argsort(lift)
145+
causes=[causes[i] for i in order]; lift=[lift[i] for i in order]; silent=[silent[i] for i in order]
146+
fig,(a1,a2)=plt.subplots(1,2,figsize=(11.5,4.3))
147+
y=np.arange(len(causes))
148+
cols=[RED if l>1.15 else (GRAY if l<0.85 else BLUE) for l in lift]
149+
a1.hlines(y,1,lift,color=cols,lw=2,zorder=1)
150+
a1.scatter(lift,y,color=cols,s=60,zorder=2)
151+
a1.axvline(1,color="#888",lw=1,ls="--")
152+
a1.set_yticks(y); a1.set_yticklabels([c.replace('_',' ') for c in causes])
153+
a1.set_xlabel("severity lift P(cause | Crit+High) / P(cause | all)")
154+
a1.set_title("(a) What raises severity",loc="left",fontsize=12,color=INK,fontweight="bold")
155+
a1.text(1.02,len(causes)-0.5,"over-\nrepresented →",color=RED,fontsize=8,va="top")
156+
a1.grid(axis="y",visible=False)
157+
a2.barh(y,silent,color=[ORANGE if s>85 else TEAL for s in silent],height=0.7)
158+
a2.set_yticks(y); a2.set_yticklabels([c.replace('_',' ') for c in causes])
159+
a2.set_xlabel("% shipped silently (no rated severity)")
160+
a2.xaxis.set_major_formatter(PercentFormatter()); a2.set_xlim(0,100)
161+
for yi,s in zip(y,silent): a2.text(s-3,yi,f"{s:.0f}%",va="center",ha="right",color="white",fontsize=8.5,fontweight="bold")
162+
a2.set_title("(b) …yet the severe classes are patched silently",loc="left",fontsize=12,color=INK,fontweight="bold")
163+
a2.grid(axis="y",visible=False)
164+
plt.tight_layout(); plt.savefig(f"{FG}/fig7_severity_drivers.png",bbox_inches="tight"); plt.close()
165+
print("fig7 written")

0 commit comments

Comments
 (0)