Skip to content

Commit 31f7c6d

Browse files
author
MarcoFalke
committed
Merge #15295: fuzz: Add test/fuzz/test_runner.py and run it in travis
fa535af fuzz: test_runner: Better error message when built with afl (MarcoFalke) fa7ca8e qa: Add test/fuzz/test_runner.py (MarcoFalke) Pull request description: Can be run with `./test/fuzz/test_runner.py` after building as described in `doc/fuzzing.md` Tree-SHA512: f6a3cd8165ec2de4b363be4fd0a936b4a60829cce923f93fe5d6a046b1bbd64c959cdf790440bf70c0e13b0bb1b956a746a24c6fd92bddeab15b837ed50ffad2
2 parents 642bd7b + fa535af commit 31f7c6d

File tree

8 files changed

+204
-27
lines changed

8 files changed

+204
-27
lines changed

.travis.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ env:
1515
- MAKEJOBS=-j3
1616
- RUN_UNIT_TESTS=true
1717
- RUN_FUNCTIONAL_TESTS=true
18+
- RUN_FUZZ_TESTS=false
1819
- DOCKER_NAME_TAG=ubuntu:18.04
1920
- BOOST_TEST_RANDOM=1$TRAVIS_BUILD_ID
2021
- CCACHE_SIZE=100M
@@ -100,7 +101,7 @@ jobs:
100101
PACKAGES="python3-zmq qtbase5-dev qttools5-dev-tools protobuf-compiler libdbus-1-dev libharfbuzz-dev libprotobuf-dev"
101102
DEP_OPTS="NO_QT=1 NO_UPNP=1 DEBUG=1 ALLOW_HOST_PACKAGES=1"
102103
GOAL="install"
103-
BITCOIN_CONFIG="--enable-zmq --with-gui=qt5 --enable-fuzz --enable-glibc-back-compat --enable-reduce-exports --enable-debug CXXFLAGS=\"-g0 -O2\""
104+
BITCOIN_CONFIG="--enable-zmq --with-gui=qt5 --enable-glibc-back-compat --enable-reduce-exports --enable-debug CXXFLAGS=\"-g0 -O2\""
104105
105106
- stage: test
106107
name: 'x86_64 Linux [GOAL: install] [trusty] [no functional tests, no depends, only system libs]'
@@ -132,6 +133,18 @@ jobs:
132133
GOAL="install"
133134
BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS=-DDEBUG_LOCKORDER --with-sanitizers=address,integer,undefined CC=clang CXX=clang++"
134135
136+
- stage: test
137+
name: 'x86_64 Linux [GOAL: install] [bionic] [no depends, only system libs, sanitizers: fuzzer,address]'
138+
env: >-
139+
HOST=x86_64-unknown-linux-gnu
140+
PACKAGES="clang llvm python3 libssl1.0-dev libevent-dev bsdmainutils libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev"
141+
NO_DEPENDS=1
142+
RUN_UNIT_TESTS=false
143+
RUN_FUNCTIONAL_TESTS=false
144+
RUN_FUZZ_TESTS=true
145+
GOAL="install"
146+
BITCOIN_CONFIG="--disable-wallet --disable-bench --with-utils=no --with-daemon=no --with-libs=no --with-gui=no --enable-fuzz --with-sanitizers=fuzzer,address CC=clang CXX=clang++"
147+
135148
- stage: test
136149
name: 'x86_64 Linux [GOAL: install] [bionic] [no wallet]'
137150
env: >-

.travis/test_04_install.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
export LC_ALL=C.UTF-8
88

99
travis_retry docker pull "$DOCKER_NAME_TAG"
10+
11+
export DIR_FUZZ_IN=${TRAVIS_BUILD_DIR}/qa-assets
12+
git clone https://github.com/bitcoin-core/qa-assets ${DIR_FUZZ_IN}
13+
export DIR_FUZZ_IN=${DIR_FUZZ_IN}/fuzz_seed_corpus/
14+
1015
mkdir -p "${TRAVIS_BUILD_DIR}/sanitizer-output/"
1116
export ASAN_OPTIONS=""
1217
export LSAN_OPTIONS="suppressions=${TRAVIS_BUILD_DIR}/test/sanitizer_suppressions/lsan"

.travis/test_06_script_b.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ if [ "$RUN_FUNCTIONAL_TESTS" = "true" ]; then
1919
DOCKER_EXEC test/functional/test_runner.py --ci --combinedlogslen=4000 --coverage --quiet --failfast
2020
END_FOLD
2121
fi
22+
23+
if [ "$RUN_FUZZ_TESTS" = "true" ]; then
24+
BEGIN_FOLD fuzz-tests
25+
DOCKER_EXEC test/fuzz/test_runner.py -l DEBUG ${DIR_FUZZ_IN}
26+
END_FOLD
27+
fi

Makefile.am

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,11 @@ endif
225225

226226
dist_noinst_SCRIPTS = autogen.sh
227227

228-
EXTRA_DIST = $(DIST_SHARE) test/functional/test_runner.py test/functional $(DIST_CONTRIB) $(DIST_DOCS) $(WINDOWS_PACKAGING) $(OSX_PACKAGING) $(BIN_CHECKS)
228+
EXTRA_DIST = $(DIST_SHARE) $(DIST_CONTRIB) $(DIST_DOCS) $(WINDOWS_PACKAGING) $(OSX_PACKAGING) $(BIN_CHECKS)
229+
230+
EXTRA_DIST += \
231+
test/functional \
232+
test/fuzz
229233

230234
EXTRA_DIST += \
231235
test/util/bitcoin-util-test.py \

doc/fuzzing.md

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@ A special test harness in `src/test/fuzz/` is provided for each fuzz target to
55
provide an easy entry point for fuzzers and the like. In this document we'll
66
describe how to use it with AFL and libFuzzer.
77

8+
## Preparing fuzzing
9+
10+
AFL needs an input directory with examples, and an output directory where it
11+
will place examples that it found. These can be anywhere in the file system,
12+
we'll define environment variables to make it easy to reference them.
13+
14+
libFuzzer will use the input directory as output directory.
15+
16+
Extract the example seeds (or other starting inputs) into the inputs
17+
directory before starting fuzzing.
18+
19+
```
20+
git clone https://github.com/bitcoin-core/qa-assets
21+
export DIR_FUZZ_IN=$PWD/qa-assets/fuzz_seed_corpus
22+
```
23+
24+
Only for AFL:
25+
26+
```
27+
mkdir outputs
28+
export AFLOUT=$PWD/outputs
29+
```
30+
831
## AFL
932

1033
### Building AFL
@@ -23,7 +46,7 @@ export AFLPATH=$PWD
2346
To build Bitcoin Core using AFL instrumentation (this assumes that the
2447
`AFLPATH` was set as above):
2548
```
26-
./configure --disable-ccache --disable-shared --enable-tests --enable-fuzz CC=${AFLPATH}/afl-gcc CXX=${AFLPATH}/afl-g++
49+
./configure --disable-ccache --disable-shared --enable-tests --enable-fuzz --disable-wallet --disable-bench --with-utils=no --with-daemon=no --with-libs=no --with-gui=no CC=${AFLPATH}/afl-gcc CXX=${AFLPATH}/afl-g++
2750
export AFL_HARDEN=1
2851
cd src/
2952
make
@@ -39,31 +62,14 @@ binary will be instrumented in such a way that the AFL
3962
features "persistent mode" and "deferred forkserver" can be used. See
4063
https://github.com/mcarpenter/afl/tree/master/llvm_mode for details.
4164

42-
### Preparing fuzzing
43-
44-
AFL needs an input directory with examples, and an output directory where it
45-
will place examples that it found. These can be anywhere in the file system,
46-
we'll define environment variables to make it easy to reference them.
47-
48-
```
49-
mkdir inputs
50-
AFLIN=$PWD/inputs
51-
mkdir outputs
52-
AFLOUT=$PWD/outputs
53-
```
54-
55-
Example inputs are available from:
56-
57-
- https://download.visucore.com/bitcoin/bitcoin_fuzzy_in.tar.xz
58-
- http://strateman.ninja/fuzzing.tar.xz
59-
60-
Extract these (or other starting inputs) into the `inputs` directory before starting fuzzing.
61-
6265
### Fuzzing
6366

6467
To start the actual fuzzing use:
68+
6569
```
66-
$AFLPATH/afl-fuzz -i ${AFLIN} -o ${AFLOUT} -m52 -- test/fuzz/fuzz_target_foo
70+
export FUZZ_TARGET=fuzz_target_foo # Pick a fuzz_target
71+
mkdir ${AFLOUT}/${FUZZ_TARGET}
72+
$AFLPATH/afl-fuzz -i ${DIR_FUZZ_IN}/${FUZZ_TARGET} -o ${AFLOUT}/${FUZZ_TARGET} -m52 -- test/fuzz/${FUZZ_TARGET}
6773
```
6874

6975
You may have to change a few kernel parameters to test optimally - `afl-fuzz`
@@ -74,10 +80,10 @@ will print an error and suggestion if so.
7480
A recent version of `clang`, the address sanitizer and libFuzzer is needed (all
7581
found in the `compiler-rt` runtime libraries package).
7682

77-
To build the `test/test_bitcoin_fuzzy` executable run
83+
To build all fuzz targets with libFuzzer, run
7884

7985
```
80-
./configure --disable-ccache --enable-fuzz --with-sanitizers=fuzzer,address CC=clang CXX=clang++
86+
./configure --disable-ccache --disable-wallet --disable-bench --with-utils=no --with-daemon=no --with-libs=no --with-gui=no --enable-fuzz --with-sanitizers=fuzzer,address CC=clang CXX=clang++
8187
make
8288
```
8389

@@ -86,3 +92,6 @@ interchangeably between libFuzzer and AFL.
8692

8793
See https://llvm.org/docs/LibFuzzer.html#running on how to run the libFuzzer
8894
instrumented executable.
95+
96+
Alternatively run the script in `./test/fuzz/test_runner.py` and provide it
97+
with the `${DIR_FUZZ_IN}` created earlier.

src/Makefile.test.include

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Distributed under the MIT software license, see the accompanying
33
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5-
bin_PROGRAMS += test/test_bitcoin
65

76
FUZZ_TARGETS = \
87
test/fuzz/address_deserialize \
@@ -28,6 +27,8 @@ FUZZ_TARGETS = \
2827

2928
if ENABLE_FUZZ
3029
noinst_PROGRAMS += $(FUZZ_TARGETS:=)
30+
else
31+
bin_PROGRAMS += test/test_bitcoin
3132
endif
3233

3334
TEST_SRCDIR = test

test/config.ini.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
1616
@ENABLE_WALLET_TRUE@ENABLE_WALLET=true
1717
@BUILD_BITCOIN_CLI_TRUE@ENABLE_CLI=true
1818
@BUILD_BITCOIND_TRUE@ENABLE_BITCOIND=true
19+
@ENABLE_FUZZ_TRUE@ENABLE_FUZZ=true
1920
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true

test/fuzz/test_runner.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2019 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Run fuzz test targets.
6+
"""
7+
8+
import argparse
9+
import configparser
10+
import os
11+
import sys
12+
import subprocess
13+
import logging
14+
15+
16+
def main():
17+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
18+
parser.add_argument(
19+
"-l",
20+
"--loglevel",
21+
dest="loglevel",
22+
default="INFO",
23+
help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console.",
24+
)
25+
parser.add_argument(
26+
'--export_coverage',
27+
action='store_true',
28+
help='If true, export coverage information to files in the seed corpus',
29+
)
30+
parser.add_argument(
31+
'seed_dir',
32+
help='The seed corpus to run on (must contain subfolders for each fuzz target).',
33+
)
34+
parser.add_argument(
35+
'target',
36+
nargs='*',
37+
help='The target(s) to run. Default is to run all targets.',
38+
)
39+
40+
args = parser.parse_args()
41+
42+
# Set up logging
43+
logging.basicConfig(
44+
format='%(message)s',
45+
level=int(args.loglevel) if args.loglevel.isdigit() else args.loglevel.upper(),
46+
)
47+
48+
# Read config generated by configure.
49+
config = configparser.ConfigParser()
50+
configfile = os.path.abspath(os.path.dirname(__file__)) + "/../config.ini"
51+
config.read_file(open(configfile, encoding="utf8"))
52+
53+
if not config["components"].getboolean("ENABLE_FUZZ"):
54+
logging.error("Must have fuzz targets built")
55+
sys.exit(1)
56+
57+
# Build list of tests
58+
test_list_all = parse_test_list(makefile=os.path.join(config["environment"]["SRCDIR"], 'src', 'Makefile.test.include'))
59+
60+
if not test_list_all:
61+
logging.error("No fuzz targets found")
62+
sys.exit(1)
63+
64+
logging.info("Fuzz targets found: {}".format(test_list_all))
65+
66+
args.target = args.target or test_list_all # By default run all
67+
test_list_error = list(set(args.target).difference(set(test_list_all)))
68+
if test_list_error:
69+
logging.error("Unknown fuzz targets selected: {}".format(test_list_error))
70+
test_list_selection = list(set(test_list_all).intersection(set(args.target)))
71+
if not test_list_selection:
72+
logging.error("No fuzz targets selected")
73+
logging.info("Fuzz targets selected: {}".format(test_list_selection))
74+
75+
try:
76+
help_output = subprocess.run(
77+
args=[
78+
os.path.join(config["environment"]["BUILDDIR"], 'src', 'test', 'fuzz', test_list_selection[0]),
79+
'-help=1',
80+
],
81+
timeout=1,
82+
check=True,
83+
stderr=subprocess.PIPE,
84+
universal_newlines=True,
85+
).stderr
86+
if "libFuzzer" not in help_output:
87+
logging.error("Must be built with libFuzzer")
88+
sys.exit(1)
89+
except subprocess.TimeoutExpired:
90+
logging.error("subprocess timed out: Currently only libFuzzer is supported")
91+
sys.exit(1)
92+
93+
run_once(
94+
corpus=args.seed_dir,
95+
test_list=test_list_selection,
96+
build_dir=config["environment"]["BUILDDIR"],
97+
export_coverage=args.export_coverage,
98+
)
99+
100+
101+
def run_once(*, corpus, test_list, build_dir, export_coverage):
102+
for t in test_list:
103+
args = [
104+
os.path.join(build_dir, 'src', 'test', 'fuzz', t),
105+
'-runs=1',
106+
os.path.join(corpus, t),
107+
]
108+
logging.debug('Run {} with args {}'.format(t, args))
109+
output = subprocess.run(args, check=True, stderr=subprocess.PIPE, universal_newlines=True).stderr
110+
logging.debug('Output: {}'.format(output))
111+
if not export_coverage:
112+
continue
113+
for l in output.splitlines():
114+
if 'INITED' in l:
115+
with open(os.path.join(corpus, t + '_coverage'), 'w', encoding='utf-8') as cov_file:
116+
cov_file.write(l)
117+
break
118+
119+
120+
def parse_test_list(makefile):
121+
with open(makefile, encoding='utf-8') as makefile_test:
122+
test_list_all = []
123+
read_targets = False
124+
for line in makefile_test.readlines():
125+
line = line.strip().replace('test/fuzz/', '').replace(' \\', '')
126+
if read_targets:
127+
if not line:
128+
break
129+
test_list_all.append(line)
130+
continue
131+
132+
if line == 'FUZZ_TARGETS =':
133+
read_targets = True
134+
return test_list_all
135+
136+
137+
if __name__ == '__main__':
138+
main()

0 commit comments

Comments
 (0)