diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000..b512ea29 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,68 @@ +# Automatically generated by fuzz/generate-files.sh +name: Fuzz + +on: + push: + branches: + - master + - 'test-ci/**' + pull_request: + +jobs: + fuzz: + if: ${{ !github.event.act }} + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + fuzz_target: [ +roundtrip_miniscript_str, +roundtrip_miniscript_script, +parse_descriptor, +roundtrip_semantic, +parse_descriptor_secret, +roundtrip_descriptor, +roundtrip_concrete, +compile_descriptor, + ] + steps: + - name: Install test dependencies + run: sudo apt-get update -y && sudo apt-get install -y binutils-dev libunwind8-dev libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc libiberty-dev + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + id: cache-fuzz + with: + path: | + ~/.cargo/bin + fuzz/target + target + key: cache-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.58 + override: true + profile: minimal + - name: fuzz + run: | + if [[ "${{ matrix.fuzz_target }}" =~ ^bitcoin ]]; then + export RUSTFLAGS='--cfg=hashes_fuzz --cfg=secp256k1_fuzz' + fi + echo "Using RUSTFLAGS $RUSTFLAGS" + cd fuzz && ./fuzz.sh "${{ matrix.fuzz_target }}" + - run: echo "${{ matrix.fuzz_target }}" >executed_${{ matrix.fuzz_target }} + - uses: actions/upload-artifact@v2 + with: + name: executed_${{ matrix.fuzz_target }} + path: executed_${{ matrix.fuzz_target }} + + verify-execution: + if: ${{ !github.event.act }} + needs: fuzz + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + - name: Display structure of downloaded files + run: ls -R + - run: find executed_* -type f -exec cat {} + | sort > executed + - run: source ./fuzz/fuzz-util.sh && listTargetNames | sort | diff - executed diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d5358fa2..2f418fca 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,7 +8,7 @@ name: Continuous integration jobs: lint_fuzz_stable: - name: Lint + Fuzz + name: Lint runs-on: ubuntu-latest steps: - name: Checkout Crate @@ -23,7 +23,6 @@ jobs: override: true - name: Running fuzzer env: - DO_FUZZ: true DO_LINT: true run: ./contrib/test.sh diff --git a/Cargo.toml b/Cargo.toml index 97f797f8..2e90ed3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,3 +58,6 @@ required-features = ["compiler"] [[example]] name = "psbt_sign_finalize" required-features = ["base64"] + +[workspace] +members = ["fuzz"] diff --git a/contrib/test.sh b/contrib/test.sh index 7cab463c..3af30079 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -19,17 +19,6 @@ then cargo fmt -- --check fi -# Fuzz if told to -if [ "$DO_FUZZ" = true ] -then - cd fuzz - cargo test --verbose - ./travis-fuzz.sh - - # Exit out of the fuzzer, do not run other tests. - exit 0 -fi - # Test bitcoind integration tests if told to (this only works with the stable toolchain) if [ "$DO_BITCOIND_TESTS" = true ]; then cd bitcoind-tests diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 63ea0218..75c28e3e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "descriptor-fuzz" +edition = "2018" version = "0.0.1" -authors = ["Automatically generated"] +authors = ["Generated by fuzz/generate-files.sh"] publish = false [package.metadata] @@ -12,38 +13,34 @@ honggfuzz = { version = "0.5.55", default-features = false } regex = { version = "1.4"} elements-miniscript = { path = "..", features = ["compiler"] } -# Prevent this from interfering with workspaces -[workspace] -members = ["."] - [[bin]] -name = "roundtrip_descriptor" -path = "fuzz_targets/roundtrip_descriptor.rs" +name = "roundtrip_miniscript_str" +path = "fuzz_targets/roundtrip_miniscript_str.rs" [[bin]] name = "roundtrip_miniscript_script" path = "fuzz_targets/roundtrip_miniscript_script.rs" [[bin]] -name = "roundtrip_miniscript_str" -path = "fuzz_targets/roundtrip_miniscript_str.rs" - -[[bin]] -name = "roundtrip_concrete" -path = "fuzz_targets/roundtrip_concrete.rs" +name = "parse_descriptor" +path = "fuzz_targets/parse_descriptor.rs" [[bin]] name = "roundtrip_semantic" path = "fuzz_targets/roundtrip_semantic.rs" [[bin]] -name = "compile_descriptor" -path = "fuzz_targets/compile_descriptor.rs" +name = "parse_descriptor_secret" +path = "fuzz_targets/parse_descriptor_secret.rs" [[bin]] -name = "parse_descriptor" -path = "fuzz_targets/parse_descriptor.rs" +name = "roundtrip_descriptor" +path = "fuzz_targets/roundtrip_descriptor.rs" [[bin]] -name = "parse_descriptor_secret" -path = "fuzz_targets/parse_descriptor_secret.rs" +name = "roundtrip_concrete" +path = "fuzz_targets/roundtrip_concrete.rs" + +[[bin]] +name = "compile_descriptor" +path = "fuzz_targets/compile_descriptor.rs" diff --git a/fuzz/README.md b/fuzz/README.md index f636c9d8..17db41d1 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -1,39 +1,115 @@ -# Fuzz Tests +# Fuzzing -Repository for fuzz testing Miniscript. +`bitcoin` and `bitcoin_hashes` have fuzzing harnesses setup for use with +honggfuzz. -## How to reproduce crashes? +To run the fuzz-tests as in CI -- briefly fuzzing every target -- simply +run -Travis should output a offending hex("048531e80700ae6400670000af5168" in the example) -which you can use as shown. Copy and paste the following code lines into file reporting crashes and -replace the hex with the offending hex. -Refer to file [roundtrip_concrete.rs](./fuzz_targets/roundtrip_concrete.rs) for an example. + ./fuzz.sh + +in this directory. + +To build honggfuzz, you must have libunwind on your system, as well as +libopcodes and libbfd from binutils **2.38** on your system. The most +recently-released binutils 2.39 has changed their API in a breaking way. + +On Nix, you can obtain these libraries by running + + nix-shell -p libopcodes_2_38 -p libunwind + +and then run fuzz.sh as above. + +# Fuzzing with weak cryptography + +You may wish to replace the hashing and signing code with broken crypto, +which will be faster and enable the fuzzer to do otherwise impossible +things such as forging signatures or finding preimages to hashes. + +Doing so may result in spurious bug reports since the broken crypto does +not respect the encoding or algebraic invariants upheld by the real crypto. We +would like to improve this but it's a nontrivial problem -- though not +beyond the abilities of a motivated student with a few months of time. +Please let us know if you are interested in taking this on! + +Meanwhile, to use the broken crypto, simply compile (and run the fuzzing +scripts) with + + RUSTFLAGS="--cfg=hashes_fuzz --cfg=secp256k1_fuzz" + +which will replace the hashing library with broken hashes, and the +secp256k1 library with broken cryptography. + +Needless to say, NEVER COMPILE REAL CODE WITH THESE FLAGS because if a +fuzzer can break your crypto, so can anybody. + +# Long-term fuzzing + +To see the full list of targets, the most straightforward way is to run + + source ./fuzz-util.sh + listTargetNames + +To run each of them for an hour, run + + ./cycle.sh + +To run a single fuzztest indefinitely, run + + HFUZZ_BUILD_ARGS='--features honggfuzz_fuzz' cargo hfuzz run + +This script uses the `chrt` utility to try to reduce the priority of the +jobs. If you would like to run for longer, the most straightforward way +is to edit `cycle.sh` before starting. To run the fuzz-tests in parallel, +you will need to implement a custom harness. + +# Adding fuzz tests + +All fuzz tests can be found in the `fuzz_target/` directory. Adding a new +one is as simple as copying an existing one and editing the `do_test` +function to do what you want. + +If your test clearly belongs to a specific crate, please put it in that +crate's directory. Otherwise you can put it directly in `fuzz_target/`. + +If you need to add dependencies, edit the file `generate-files.sh` to add +it to the generated `Cargo.toml`. + +Once you've added a fuzztest, regenerate the `Cargo.toml` and CI job by +running + + ./generate-files.sh + +Then to test your fuzztest, run + + ./fuzz.sh + +If it is working, you will see a rapid stream of data for many seconds +(you can hit Ctrl+C to stop it early). If not, you should quickly see +an error. + +# Reproducing Failures + +If a fuzztest fails, it will exit with a summary which looks something like ``` -#[cfg(test)] -mod tests { - fn extend_vec_from_hex(hex: &str, out: &mut Vec) { - let mut b = 0; - for (idx, c) in hex.as_bytes().iter().enumerate() { - b <<= 4; - match *c { - b'A'...b'F' => b |= c - b'A' + 10, - b'a'...b'f' => b |= c - b'a' + 10, - b'0'...b'9' => b |= c - b'0', - _ => panic!("Bad hex"), - } - if (idx & 1) == 1 { - out.push(b); - b = 0; - } - } - } - - #[test] - fn duplicate_crash() { - let mut a = Vec::new(); - extend_vec_from_hex("048531e80700ae6400670000af5168", &mut a); - super::do_test(&a); - } -} +... + fuzzTarget : hfuzz_target/x86_64-unknown-linux-gnu/release/hashes_sha256 +CRASH: +DESCRIPTION: +ORIG_FNAME: 00000000000000000000000000000000.00000000.honggfuzz.cov +FUZZ_FNAME: hfuzz_workspace/hashes_sha256/SIGABRT.PC.7ffff7c8abc7.STACK.18826d9b64.CODE.-6.ADDR.0.INSTR.mov____%eax,%ebp.fuzz +... +===================================================================== +fff400610004 ``` + +The final line is a hex-encoded version of the input that caused the crash. You +can test this directly by editing the `duplicate_crash` test to copy/paste the +hex output into the call to `Vec::::from_hex`. Then run the test with + + cargo test + +Note that if you set your `RUSTFLAGS` while fuzzing (see above) you must make +sure they are set the same way when running `cargo test`. + diff --git a/fuzz/cycle.sh b/fuzz/cycle.sh new file mode 100755 index 00000000..0b59827a --- /dev/null +++ b/fuzz/cycle.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Continuosly cycle over fuzz targets running each for 1 hour. +# It uses chrt SCHED_IDLE so that other process takes priority. +# +# For hfuzz options see https://github.com/google/honggfuzz/blob/master/docs/USAGE.md + +set -e +REPO_DIR=$(git rev-parse --show-toplevel) +# shellcheck source=./fuzz-util.sh +source "$REPO_DIR/fuzz/fuzz-util.sh" + +while : +do + for targetFile in $(listTargetFiles); do + targetName=$(targetFileToName "$targetFile") + echo "Fuzzing target $targetName ($targetFile)" + + # fuzz for one hour + HFUZZ_RUN_ARGS='--run_time 3600' chrt -i 0 cargo hfuzz run "$targetName" + # minimize the corpus + HFUZZ_RUN_ARGS="-i hfuzz_workspace/$targetName/input/ -P -M" chrt -i 0 cargo hfuzz run "$targetName" + done +done + diff --git a/fuzz/fuzz-util.sh b/fuzz/fuzz-util.sh new file mode 100755 index 00000000..804e0da9 --- /dev/null +++ b/fuzz/fuzz-util.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +REPO_DIR=$(git rev-parse --show-toplevel) + +listTargetFiles() { + pushd "$REPO_DIR/fuzz" > /dev/null || exit 1 + find fuzz_targets/ -type f -name "*.rs" + popd > /dev/null || exit 1 +} + +targetFileToName() { + echo "$1" \ + | sed 's/^fuzz_targets\///' \ + | sed 's/\.rs$//' \ + | sed 's/\//_/g' +} + +targetFileToHFuzzInputArg() { + baseName=$(basename "$1") + dirName="${baseName%.*}" + if [ -d "hfuzz_input/$dirName" ]; then + echo "HFUZZ_INPUT_ARGS=\"-f hfuzz_input/$FILE/input\"" + fi +} + +listTargetNames() { + for target in $(listTargetFiles); do + targetFileToName "$target" + done +} + +# Utility function to avoid CI failures on Windows +checkWindowsFiles() { + incorrectFilenames=$(find . -type f -name "*,*" -o -name "*:*" -o -name "*<*" -o -name "*>*" -o -name "*|*" -o -name "*\?*" -o -name "*\**" -o -name "*\"*" | wc -l) + if [ "$incorrectFilenames" -gt 0 ]; then + echo "Bailing early because there is a Windows-incompatible filename in the tree." + exit 2 + fi +} + +# Checks whether a fuzz case output some report, and dumps it in hex +checkReport() { + reportFile="hfuzz_workspace/$1/HONGGFUZZ.REPORT.TXT" + if [ -f "$reportFile" ]; then + cat "$reportFile" + for CASE in "hfuzz_workspace/$1/SIG"*; do + xxd -p -c10000 < "$CASE" + done + exit 1 + fi +} diff --git a/fuzz/fuzz.sh b/fuzz/fuzz.sh new file mode 100755 index 00000000..5fc65ae6 --- /dev/null +++ b/fuzz/fuzz.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -ex + +REPO_DIR=$(git rev-parse --show-toplevel) + +# shellcheck source=./fuzz-util.sh +source "$REPO_DIR/fuzz/fuzz-util.sh" + +# Check that input files are correct Windows file names +checkWindowsFiles + +if [ "$1" == "" ]; then + targetFiles="$(listTargetFiles)" +else + targetFiles=fuzz_targets/"$1".rs +fi + +cargo --version +rustc --version + +# Testing +cargo install --force honggfuzz --no-default-features +for targetFile in $targetFiles; do + targetName=$(targetFileToName "$targetFile") + echo "Fuzzing target $targetName ($targetFile)" + if [ -d "hfuzz_input/$targetName" ]; then + HFUZZ_INPUT_ARGS="-f hfuzz_input/$targetName/input\"" + else + HFUZZ_INPUT_ARGS="" + fi + HFUZZ_RUN_ARGS="--run_time 30 --exit_upon_crash -v $HFUZZ_INPUT_ARGS" cargo hfuzz run "$targetName" + + checkReport "$targetName" +done diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh new file mode 100755 index 00000000..4787693a --- /dev/null +++ b/fuzz/generate-files.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -e + +REPO_DIR=$(git rev-parse --show-toplevel) + +# shellcheck source=./fuzz-util.sh +source "$REPO_DIR/fuzz/fuzz-util.sh" + +# 1. Generate fuzz/Cargo.toml +cat > "$REPO_DIR/fuzz/Cargo.toml" <> "$REPO_DIR/fuzz/Cargo.toml" < "$REPO_DIR/.github/workflows/fuzz.yml" <executed_\${{ matrix.fuzz_target }} + - uses: actions/upload-artifact@v2 + with: + name: executed_\${{ matrix.fuzz_target }} + path: executed_\${{ matrix.fuzz_target }} + + verify-execution: + if: \${{ !github.event.act }} + needs: fuzz + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + - name: Display structure of downloaded files + run: ls -R + - run: find executed_* -type f -exec cat {} + | sort > executed + - run: source ./fuzz/fuzz-util.sh && listTargetNames | sort | diff - executed +EOF + diff --git a/fuzz/travis-fuzz.sh b/fuzz/travis-fuzz.sh deleted file mode 100755 index db1b9eb3..00000000 --- a/fuzz/travis-fuzz.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e -cargo install --force honggfuzz --no-default-features -for TARGET in fuzz_targets/*; do - FILENAME=$(basename $TARGET) - FILE="${FILENAME%.*}" - if [ -d hfuzz_input/$FILE ]; then - HFUZZ_INPUT_ARGS="-f hfuzz_input/$FILE/input" - fi - HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" HFUZZ_RUN_ARGS="-N1000000 --exit_upon_crash -v $HFUZZ_INPUT_ARGS" cargo hfuzz run $FILE - - if [ -f hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT ]; then - cat hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT - for CASE in hfuzz_workspace/$FILE/SIG*; do - cat $CASE | xxd -p - done - exit 1 - fi -done