Repository navigation
Releases
v0.3.0 — on every pull request
The GitHub Action: it builds the pull request and its base, replays the
library against both, and keeps one comment on the pull request with what
this change introduced — why it is a hole, how to close it, and the line,
linked in the pull request's head. The check fails only on what the pull
request introduced. Proofs are withheld from every public channel unless
the base branch's proofs.public names the program, and never for a program
declared on mainnet or nowhere. A push to the default branch builds and
caches, so a new pull request gets its comment in 132 s on a 3-program
workspace and 144 s on a 2-program one; a cold run is 565 s and 637 s.
Every verdict is still taken on local state: there is no mainnet fork.
v0.2.0 — two exploit classes, and it finds the build itself
Two exploit classes now, and the command no longer needs to be told where the
build is.
What's new
A second exploit class. account-substitution-pda (FR-006): a program that
acts on an account of its own type without proving it belongs to the signer in
front of it. The attacker signs legitimately as themselves and hands in the
victim's account of the same type — the signature is real, it just authorises
the wrong account, which is what separates this from missing-signer. Cites
Wormhole, February 2022. On the fixture pair it moves 999,995,000 lamports on
the vulnerable half and is blocked on the fixed one.
The two halves emit byte-identical IDLs apart from the program address: the fix
is a require_keys_eq! inside the handler and shows nowhere in the IDL. A
verdict on this pair cannot have been read from metadata — there is nothing in
it to read.
--artifact is now optional. Without it the working copy is built and the
result cached by a SHA-256 of the sources and the locked dependencies, so a
repeat run with nothing changed skips the build: 59.8s cold against 0.135s warm
on the fixture, same verdict either way.
Sources are followed from the artifact, not searched for. The workspace is
found by walking up from the artifact rather than down from the scan root. A
scan of the vulnerable half used to cite line 41 of the fixed half — a
confident reference to the wrong line, which is worse than none.
Disagreement is now an answer. Where the artifact and the working copy
disagree — the program undeclared, one name at two addresses, or bytes unlike
what these sources build to — the run returns inconclusive with a reason and
exits 2. It never picks a half.
The workspace is read. Anchor.toml is parsed, so deployment is no longer
unknown for every scan; the value decides how much of a proof may be published
(FR-015). Every rule leans one way: anything the parser cannot fully read
resolves to unknown. Being wrong towards local publishes a mainnet exploit;
being wrong towards unknown costs a redaction nobody needed.
Fixed
- Replays no longer exhaust memory. litesvm 1.3.0 never frees an SVM's
native memory, so a process replaying many of them climbed until the
allocator aborted withstd::bad_allocat ~130 MB RSS with gigabytes free.
Two full scans, or a library walk over more than one scenario, hit it. Each
replay now runs in a child process that hands the leak back to the OS on exit. --artifactwas resolved against the scanned root, so
scan fixtures/x --artifact fixtures/x/artifactsdoubled the path.
Also
- Fixture reproducibility is now verified across machines. A cold
ubuntu-latestrunner with no build cache, installing the pinned toolchain
from scratch, produced all four hashes byte for byte. - A local demo runner that warms the build cache before a live showing, and
verifies the warm-up rather than assuming it.
What this release still does not have
BLOCKEDnever means safe. Two scenario classes, two fixture pairs. This tool
proves presence, never absence, and it always will.- No GitHub Action —
apps/actionis still a stub — no fork mode, no PoC
redaction.
Upgrading
Nothing to migrate. --artifact keeps working; it is now optional rather than
required.
v0.1.0 — one command proves an exploit by running it
exploitrun scan replays a library of known Solana exploit classes against an
Anchor program inside an in-process SVM. On the vulnerable half of a fixture pair
it returns EXPLOITED with the attacker's transaction, the account delta that
proves the gain, and the source line that allowed it, and exits 1. On the fixed
half it returns BLOCKED and exits 0.
BLOCKED means this attack did not get through. It never means the program is
safe: this tool proves presence, never absence.
Running it
Linux or macOS — the SVM runtime ships no Windows binary, so on Windows this runs
under WSL. Node 22+, pnpm 9+. Nothing is published to npm yet, so build from the
tag:
git clone --branch v0.1.0 https://github.com/ExploitReplay/exploitrun.git
cd exploitrun
pnpm install
pnpm build
Then point it at a checkout and hand it the built program:
node apps/cli/dist/index.js scan <path> --artifact <dir>
--artifact takes a directory holding one .so and its IDL. --scenarios <ids>
runs a subset instead of the whole library; --json <file> writes the
machine-readable report.
Exit codes
| Code | Meaning |
|---|---|
0 |
nothing got through |
1 |
at least one scenario reached EXPLOITED |
2 |
every scenario was inconclusive |
64 |
the invocation itself was wrong |
What this release does not have, and it has to be said out loud
BLOCKEDnever means safe. One scenario class (missing-signer), one fixture
pair. Zero false positives is measured on that pair, not on a library.- No GitHub Action, no fork mode, no PoC redaction.
apps/actionis a stub. - The CLI needs
--artifactpassed by hand, and locates sources by searching the
path it was given rather than by following the artifact. - Fixture reproducibility is verified on one machine. The workflow that would
verify it across machines has never run.