A fuzzer for Stratum V2 roles, built along the lines of fuzzamoto.
How far each protocol message is taken is tracked in FEATURES.md. How the
whole thing works, from a program to a filed finding, is in
docs/HowStratamotoWorks.md.
Test cases are not byte strings. They are programs in a typed intermediate
representation, where the types record how one message depends on another: a SetupConnection
finalized for one subprotocol cannot be sent as another, and what needs a session can only run
inside the block that runs once the server agreed to the setup. A mutator can rewrite anything
it likes and still not fabricate a relationship the protocol does not allow, which is what
separates this from feeding random bytes at a decoder.
The scope today is the SetupConnection message flow: every component handles that one flow
completely before the next message is added.
Programs run against the real roles from sv2-apps
over real sockets. A deterministic runtime for simulated roles is being sketched in
stratamoto-dst, and is not wired in yet.
| crate | what it is |
|---|---|
stratamoto-dst |
a seeded executor, clock, network and filesystem, in progress and not yet used |
stratamoto-ir |
the programs: typed variables, operations, builder, compiler, generators, mutators, minimizers |
stratamoto |
the harness: the transport, the runner and the oracles |
stratamoto-targets |
the real roles: sv2-apps' pool, against Bitcoin Core |
stratamoto-scenarios |
one binary per scenario |
stratamoto-nyx-sys |
the Nyx agent a scenario talks to the snapshotting VM through, vendored from fuzzamoto |
stratamoto-libafl |
the fuzzer: LibAFL clients driving Nyx VMs, mutating programs |
stratamoto-cli |
generating, printing and compiling programs by hand, and building a Nyx share directory |
A run goes: a generator builds a program through the builder, which rejects anything ill-typed; the compiler lowers it to actions; the runner carries those out against a deployment; the oracles judge what came back.
A campaign goes the way fuzzamoto's does: a scenario binary boots inside a Nyx VM, brings the roles up and takes a snapshot; the fuzzer mutates programs and runs each from that snapshot, with coverage from the pool's AFL instrumentation; findings are filed by the oracle that made them.
cargo build
cargo testThe programs and the harness need nothing but a Rust toolchain (built with 1.98, edition 2024). Anything touching a real role additionally needs Bitcoin Core; see running against real roles.
cargo build
target/debug/stratamoto generate 7 1 | target/debug/stratamoto print # read it
target/debug/stratamoto generate 7 1 | target/debug/pool_setup_connectionA scenario binary takes a serialized program on stdin and exits non-zero when an oracle finds a violation.
Fuzzing needs bare metal Linux on x86_64 with KVM, and the VMware backdoor enabled in KVM:
sudo modprobe -r kvm-intel kvm # or kvm-amd
sudo modprobe kvm enable_vmware_backdoor=y && sudo modprobe kvm-intelThen, with a pool binary instrumented by cargo-afl (an uninstrumented one runs too, with no coverage to guide the fuzzer):
export STRATAMOTO_POOL=/path/to/instrumented/pool_sv2
cargo build --release -p stratamoto-scenarios --features nyx # the scenario, for the VM
cargo build --release -p stratamoto-libafl # the fuzzer, builds QEMU-Nyx
cargo build --release -p stratamoto-cli
target/release/stratamoto init --sharedir /tmp/share \
--scenario target/release/pool_setup_connection --pool $STRATAMOTO_POOL \
--template-provider /path/to/sv2-apps/integration-tests/template-provider \
--nyx-dir target/release
mkdir -p /tmp/in
target/release/stratamoto-libafl --input /tmp/in --output /tmp/out --share /tmp/share --cores 0-7STRATAMOTO_POOL is read when the Nyx agent is built, so that the shared coverage map is the
size the pool's instrumentation expects. Findings land under /tmp/out/cpu_*/crashes, filed by
cause, as bare programs a scenario binary replays. See stratamoto-libafl.
Dockerfile.libafl carries all of the above but the checkout: the
toolchain, cargo-afl, what QEMU-Nyx needs to build, the pool built with AFL instrumentation,
and Bitcoin Core where the launcher looks for it. The host still has to be bare metal with KVM
and the VMware backdoor enabled, as above.
docker build -f Dockerfile.libafl -t stratamoto-libafl .
docker run --privileged --shm-size=4g -it -v $PWD:/stratamoto stratamoto-libafl bash--privileged is what lets Nyx use KVM, and the fuzzer's clients talk over shared memory, so
/dev/shm wants to be larger than Docker's default. Inside the container:
just -f /ci/libafl.justfile run # build, create the share directory, fuzz on core 0
just -f /ci/libafl.justfile cores=0-7 runThe recipes are the commands above and can be run by hand instead. The
container builds into target/docker, apart from anything the host built, and QEMU-Nyx is
built there once, on the first build.
The pool comes from sv2-apps at the revision this workspace pins. Another one is a build argument away:
docker build --build-arg PR_NUMBER=1234 -f Dockerfile.libafl -t stratamoto-libafl .
docker build --build-arg SV2_APPS_COMMIT=abc123 -f Dockerfile.libafl -t stratamoto-libafl .
docker build --build-arg OWNER=someone --build-arg SV2_APPS_COMMIT=abc123 -f Dockerfile.libafl -t stratamoto-libafl .stratamoto-targets starts sv2-apps' pool binary as its own process and feeds it from a real
Bitcoin Core node, started with sv2-apps' own launcher. The pool carries bitcoin-core-sv2 and
talks to the node over its IPC socket, with nothing in between. Templates therefore come from a
node, not from us, and the pool is the real thing when it is asked to set a connection up.
The pool binary is built from sv2-apps at the revision this workspace pins, and named by
STRATAMOTO_POOL:
cargo install --git https://github.com/stratum-mining/sv2-apps.git \
--rev ab8f30f1784ea20c2de1b2726c47e7eea10f4556 pool_sv2 --root ./sv2
export STRATAMOTO_POOL=$PWD/sv2/bin/pool_sv2That launcher looks for Bitcoin Core in a template-provider directory beside the working
directory and downloads it when it is missing. If you already have a copy, point at it and
nothing is downloaded:
export STRATAMOTO_TEMPLATE_PROVIDER_CACHE=/path/to/sv2-apps/integration-tests/template-providerWithout it, a checkout of sv2-apps beside this one is found automatically.
The node runs in regtest.
The suite has one ignored test, a_second_frame_in_the_teardown_window_can_livelock_the_pool.
On a rejected SetupConnection the pool answers and then sleeps one second before closing the
connection, deliberately, so the error reaches the client. A second frame arriving in that
window races the teardown, and on an unlucky interleaving a pool worker spins at close to a full
core: measured, the busiest thread burns 198 of 200 jiffies over two seconds that should be
idle, while an idle pool sits at zero. The pegged worker starves the runtime and fresh
connections fail their handshake.
It is a livelock rather than a crash, and a race that fires about half the time, so the test repeats the trigger. It is ignored because it asserts a bug is present: when sv2-apps fixes it, the test fails, and that is the signal to update the record.
| variable | what it does |
|---|---|
STRATAMOTO_INPUT |
read a scenario's program from a file instead of stdin |
STRATAMOTO_POOL |
the pool binary the pool target runs |
STRATAMOTO_POOL_LOG |
write the pool's log to this file, and keep it after the run |
STRATAMOTO_DUMP_IR_CONTEXT |
where a locally run scenario writes the program context it dumps for the fuzzer |
STRATAMOTO_TEMPLATE_PROVIDER_CACHE |
where Bitcoin Core already lives |
RUST_LOG |
filters logging from the harness and the real roles alike, through tracing |