2026 · Solo · in progress
Emulation-Based Fault Injection Harness
A campaign harness that glitches bare-metal firmware in emulation to measure which countermeasures actually stop a secure-boot bypass — across two instruction sets, with every result cross-checked against a second emulator.
- exhaustive search:
- 95.9M fault pairs
- cross-backend agreement:
- 100% outside CRASH
- architectures:
- Cortex-M3 + RV32I
Problem
A voltage or clock glitch can make a CPU skip an instruction. One skipped branch turns a rejected firmware image into an accepted one. The countermeasures against this — duplicated checks, control-flow integrity, redundant computation — are usually defended by argument. I wanted them defended by measurement.
Constraints
- Emulation, no glitch rig: instruction skip stands in for the physical effect, and that abstraction gets stated, not hidden.
- Campaigns must be exhaustive and reproducible. A harness that returns different answers on different runs is worse than no harness, because its output still looks authoritative.
- The double-fault space is ~96M runs, so throughput is a design constraint, not an optimization.
Approach
Two targets — a secure-boot verifier and a motor safety supervisor — built
across a {base, hardened} × {-O0, -O2, -Os} matrix and run under Unicorn.
Faults trigger on instruction count rather than PC: counts are totally
ordered, PCs are ambiguous inside a loop. A 16-rung snapshot ladder turns an
O(T) replay per fault into O(T/16). The firmware never judges itself — it
reports only what it did, and the harness, which knows what input it
supplied, owns the verdict.
Why it's technically hard
Running the campaigns is the easy part. The hard part is that the harness
lies to you silently, and usually in the direction of a more exciting
result. Six such bugs surfaced: writable flash leaking state between runs,
fork() inheriting emulator state, and twice a telemetry field that a fault
could corrupt into looking like a genuine bypass. That last one had produced
a tidy, plausible conclusion — unoptimized hardened builds are the weakest —
which was entirely an artifact. The real answer is the opposite: they are
the strongest, fully closed. Retracting it meant re-running every number in
the writeup. Every "exploitable" result is now checked against the
firmware's own verdict field before it counts.
Result
Hardening closes the rollback and bad-magic vectors completely on both
instruction sets. The two surviving -O2 bypasses trace to one specific
weakness: redundant checks that only verify agreement with each other,
which a forged image satisfies for free. An exhaustive 95.9M-pair
double-fault search found 1,635 bypasses whose apparent variety collapsed
into a single instruction-count drift effect. Cross-validating against QEMU
on the identical binary gave 100% agreement on every outcome class carrying
a security conclusion; the one divergence localized a real fidelity gap —
Unicorn does not model Cortex-M exception entry, so the firmware's
HardFault handler is dead code under it. Ported to RV32I, the same
countermeasures measure roughly 3× weaker, and break entirely at three
faults where Cortex-M holds.
← instruction index in the golden trace → · rows are skip width k = 1…4
| golden trace | 3,890 instructions · 15,560 experiments |
|---|---|
| OK | 14,604 |
| CRASH | 456 |
| HANG | 400 |
| SDC | 78 |
| SEC_BYPASS | 22 |
| SAFETY_VIOLATION | 0 |
| same build on RV32I | 23 exploitable |
What I'd do next
Hardware validation. Simulation establishes necessary conditions for exploitability, never sufficient ones — and the cross-check already found one place where a CPU emulator and a full machine model disagree, so a real part will find more. The open engineering thread is the safety oracle: an independent watchdog closes 42.2% of violations, and the other 57.8% is firmware that never stops responding. That half needs a mitigation which does not depend on liveness at all — a hardware trip watching gate-driver current, not a software state variable.