diff options
| author | mtg0001 <106211915+mtg0001@users.noreply.github.com> | 2026-08-04 15:31:55 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-04 15:31:55 -0300 |
| commit | b3a5a858e536acf65e6c6761293626628f2227bf (patch) | |
| tree | 881dbe44ac6c0ea28f5a65cbfd9b835ba73ea648 | |
| parent | 173b4649e90f4f5ba49e92569eb063709e647c7d (diff) | |
fuzz: add P2P message parser harness (#142)
* fuzz: add P2P message parser harness
* fuzz: avoid duplicate legacy header includes
* fuzz: seed the corpus with our network magic, not Bitcoin's
The two seed inputs began f5 be b6 d9, which is Bitcoin's message
start. Ours is bf 20 5c fd. CMessageHeader::IsValid rejects on that
field first, so both seeds died at the first check and the smoke run
exercised the reject path and nothing else.
libFuzzer would have to mutate its way onto four exact bytes before
reaching any parser, which does not happen in 128 runs. The job would
have gone green while covering none of what it was added to cover.
---------
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
Co-authored-by: Bitflash-sh <btf01@proton.me>
| -rw-r--r-- | .github/workflows/ci.yml | 3 | ||||
| -rw-r--r-- | Makefile | 7 | ||||
| -rw-r--r-- | src/Makefile | 34 | ||||
| -rw-r--r-- | src/fuzz_net_message.cpp | 32 |
4 files changed, 73 insertions, 3 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 985ba8a..9efd66f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml | |||
| @@ -25,6 +25,9 @@ jobs: | |||
| 25 | - name: Run Linux selftests | 25 | - name: Run Linux selftests |
| 26 | run: make tests | 26 | run: make tests |
| 27 | 27 | ||
| 28 | - name: Run P2P message fuzz smoke | ||
| 29 | run: make fuzz-net-message-smoke | ||
| 30 | |||
| 28 | windows-ucrt64: | 31 | windows-ucrt64: |
| 29 | name: Windows UCRT64 selftests | 32 | name: Windows UCRT64 selftests |
| 30 | runs-on: windows-latest | 33 | runs-on: windows-latest |
| @@ -42,7 +42,7 @@ deps-apt: | |||
| 42 | @if command -v apt-get >/dev/null 2>&1; then \ | 42 | @if command -v apt-get >/dev/null 2>&1; then \ |
| 43 | $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ | 43 | $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ |
| 44 | libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ | 44 | libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ |
| 45 | libboost-system-dev libglfw3-dev libgl-dev python3-pil; \ | 45 | libboost-system-dev libglfw3-dev libgl-dev python3-pil clang; \ |
| 46 | else \ | 46 | else \ |
| 47 | echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ | 47 | echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ |
| 48 | echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl python3-pil"; \ | 48 | echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl python3-pil"; \ |
| @@ -191,6 +191,9 @@ clean: | |||
| 191 | tests: | 191 | tests: |
| 192 | $(MAKE) -C src -f Makefile tests | 192 | $(MAKE) -C src -f Makefile tests |
| 193 | 193 | ||
| 194 | fuzz-net-message-smoke: deps-linux | ||
| 195 | $(MAKE) -C src -f Makefile fuzz-net-message-smoke | ||
| 196 | |||
| 194 | checksums: | 197 | checksums: |
| 195 | ./scripts/make-release-checksums.sh | 198 | ./scripts/make-release-checksums.sh |
| 196 | 199 | ||
| @@ -201,5 +204,5 @@ verify-release: | |||
| 201 | ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) | 204 | ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) |
| 202 | 205 | ||
| 203 | .PHONY: linux windows clean appimage \ | 206 | .PHONY: linux windows clean appimage \ |
| 204 | tests checksums sign-checksums verify-release \ | 207 | tests fuzz-net-message-smoke checksums sign-checksums verify-release \ |
| 205 | deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx | 208 | deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx |
diff --git a/src/Makefile b/src/Makefile index 3577dd6..ac0226e 100644 --- a/src/Makefile +++ b/src/Makefile | |||
| @@ -2,6 +2,7 @@ | |||
| 2 | # Called by: make linux (root Makefile) | 2 | # Called by: make linux (root Makefile) |
| 3 | 3 | ||
| 4 | CXX ?= g++ | 4 | CXX ?= g++ |
| 5 | FUZZ_CXX ?= clang++ | ||
| 5 | 6 | ||
| 6 | SECP_INC ?= /usr/local/include | 7 | SECP_INC ?= /usr/local/include |
| 7 | SECP_LIB ?= /usr/local/lib | 8 | SECP_LIB ?= /usr/local/lib |
| @@ -33,6 +34,9 @@ CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ | |||
| 33 | -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ | 34 | -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ |
| 34 | -I/usr/include/nlohmann \ | 35 | -I/usr/include/nlohmann \ |
| 35 | -I$(IMGUI_DIR) | 36 | -I$(IMGUI_DIR) |
| 37 | FUZZ_CXXFLAGS = $(filter-out -O2,$(CXXFLAGS)) -O1 -g -fno-omit-frame-pointer \ | ||
| 38 | -fsanitize=fuzzer-no-link,address,undefined | ||
| 39 | FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined | ||
| 36 | 40 | ||
| 37 | LIBS = -L$(SECP_LIB) -lsecp256k1 \ | 41 | LIBS = -L$(SECP_LIB) -lsecp256k1 \ |
| 38 | -L$(RANDOMX_DIR)/build -lrandomx \ | 42 | -L$(RANDOMX_DIR)/build -lrandomx \ |
| @@ -117,6 +121,9 @@ NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ | |||
| 117 | NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ | 121 | NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ |
| 118 | obj-node/main_gui.o obj-node/headless.o | 122 | obj-node/main_gui.o obj-node/headless.o |
| 119 | 123 | ||
| 124 | FUZZ_OBJS = $(patsubst obj/%,obj-fuzz/%,$(NODE_OBJS)) \ | ||
| 125 | obj-fuzz/fuzz_net_message.o | ||
| 126 | |||
| 120 | obj-node/%.o: %.cpp $(HEADERS) | 127 | obj-node/%.o: %.cpp $(HEADERS) |
| 121 | @mkdir -p obj-node | 128 | @mkdir -p obj-node |
| 122 | $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -o $@ $< | 129 | $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -o $@ $< |
| @@ -129,8 +136,31 @@ bitflash-node: $(NODE_ONLY_OBJS) | |||
| 129 | $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) | 136 | $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) |
| 130 | @echo "Built: bitflash-node (headless, no libGL needed)" | 137 | @echo "Built: bitflash-node (headless, no libGL needed)" |
| 131 | 138 | ||
| 139 | obj-fuzz/%.o: %.cpp $(HEADERS) | ||
| 140 | @mkdir -p obj-fuzz | ||
| 141 | $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< | ||
| 142 | |||
| 143 | obj-fuzz/sha.o: sha.cpp sha.h | ||
| 144 | @mkdir -p obj-fuzz | ||
| 145 | $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -O1 -o $@ $< | ||
| 146 | |||
| 147 | obj-fuzz/fuzz_net_message.o: fuzz_net_message.cpp $(HEADERS) | ||
| 148 | @mkdir -p obj-fuzz | ||
| 149 | $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< | ||
| 150 | |||
| 151 | fuzz-net-message: $(FUZZ_OBJS) | ||
| 152 | $(FUZZ_CXX) $(filter-out -fsanitize=fuzzer-no-link%,$(FUZZ_CXXFLAGS)) \ | ||
| 153 | $(FUZZ_LDFLAGS) -o $@ $^ $(NODE_LIBS) | ||
| 154 | @echo "Built: fuzz-net-message" | ||
| 155 | |||
| 156 | fuzz-net-message-smoke: fuzz-net-message | ||
| 157 | @mkdir -p ../work/fuzz-corpus/net-message | ||
| 158 | @printf '\277\040\134\375ping\000\000\000\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/ping-header | ||
| 159 | @printf '\277\040\134\375version\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/version-header | ||
| 160 | ./fuzz-net-message -runs=128 ../work/fuzz-corpus/net-message | ||
| 161 | |||
| 132 | clean: | 162 | clean: |
| 133 | rm -rf obj obj-node bitflash bitflash-node test_bip32 | 163 | rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message test_bip32 |
| 134 | 164 | ||
| 135 | test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h | 165 | test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h |
| 136 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto | 166 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto |
| @@ -144,3 +174,5 @@ tests: test_bip32 bitflash-node | |||
| 144 | ./bitflash-node -selftest=wallet-encrypt | 174 | ./bitflash-node -selftest=wallet-encrypt |
| 145 | ./bitflash-node -selftest=net-message | 175 | ./bitflash-node -selftest=net-message |
| 146 | ./bitflash-node -selftest=consensus-limits | 176 | ./bitflash-node -selftest=consensus-limits |
| 177 | |||
| 178 | .PHONY: tests fuzz-net-message-smoke | ||
diff --git a/src/fuzz_net_message.cpp b/src/fuzz_net_message.cpp new file mode 100644 index 0000000..1104d90 --- /dev/null +++ b/src/fuzz_net_message.cpp | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | // | ||
| 4 | // libFuzzer harness for the P2P message envelope parser. It feeds arbitrary | ||
| 5 | // receive-buffer bytes into ProcessMessages() through a dummy CNode, without | ||
| 6 | // opening sockets or starting node threads. | ||
| 7 | |||
| 8 | #include "headers_core.h" | ||
| 9 | |||
| 10 | #include <stdint.h> | ||
| 11 | #include <stddef.h> | ||
| 12 | |||
| 13 | // Stand-ins normally supplied by the GUI/headless entry objects. | ||
| 14 | map<string,string> mapAddressBook; | ||
| 15 | bool gPoolServerRunning = false; | ||
| 16 | void MainFrameRepaint() {} | ||
| 17 | |||
| 18 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) | ||
| 19 | { | ||
| 20 | // Keep this first target focused on parser behavior, not multi-megabyte | ||
| 21 | // allocation pressure. The protocol size guards have their own selftest. | ||
| 22 | if (size > 4096) | ||
| 23 | return 0; | ||
| 24 | |||
| 25 | CNode node(INVALID_SOCKET, CAddress("127.0.0.1")); | ||
| 26 | node.nVersion = 0; // exercise pre-version gating plus the version parser | ||
| 27 | if (size > 0) | ||
| 28 | node.vRecv.write((const char*)data, (int)size); | ||
| 29 | |||
| 30 | ProcessMessages(&node); | ||
| 31 | return 0; | ||
| 32 | } | ||