diff --git a/.github/docker/Dockerfile.cross b/.github/docker/Dockerfile.cross new file mode 100644 index 0000000..f2e5a74 --- /dev/null +++ b/.github/docker/Dockerfile.cross @@ -0,0 +1,53 @@ +FROM ubuntu:24.04 + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc g++ \ + gcc-14-s390x-linux-gnu \ + g++-14-s390x-linux-gnu \ + libc6-dev-s390x-cross \ + libstdc++-14-dev-s390x-cross \ + gcc-14-aarch64-linux-gnu \ + g++-14-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + libstdc++-14-dev-arm64-cross \ + qemu-user-static \ + binfmt-support \ + curl \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +RUN update-alternatives --install /usr/bin/s390x-linux-gnu-gcc s390x-linux-gnu-gcc /usr/bin/s390x-linux-gnu-gcc-14 100 && \ + update-alternatives --install /usr/bin/s390x-linux-gnu-g++ s390x-linux-gnu-g++ /usr/bin/s390x-linux-gnu-g++-14 100 + +RUN update-alternatives --install /usr/bin/aarch64-linux-gnu-gcc aarch64-linux-gnu-gcc /usr/bin/aarch64-linux-gnu-gcc-14 100 && \ + update-alternatives --install /usr/bin/aarch64-linux-gnu-g++ aarch64-linux-gnu-g++ /usr/bin/aarch64-linux-gnu-g++-14 100 + +RUN update-binfmts --enable qemu-s390x || true + +RUN update-binfmts --enable qemu-aarch64 || true + +RUN gcc --version && \ + s390x-linux-gnu-gcc --version && \ + s390x-linux-gnu-g++ --version + +RUN aarch64-linux-gnu-gcc --version && \ + aarch64-linux-gnu-g++ --version + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal +ENV PATH="/root/.cargo/bin:${PATH}" +RUN rustup target add s390x-unknown-linux-gnu + +RUN rustup target add aarch64-unknown-linux-gnu + +ENV CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_LINKER=s390x-linux-gnu-gcc \ + CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_RUNNER="qemu-s390x-static -L /usr/s390x-linux-gnu" \ + CC_s390x_unknown_linux_gnu=s390x-linux-gnu-gcc \ + CXX_s390x_unknown_linux_gnu=s390x-linux-gnu-g++ \ + CXXFLAGS_s390x_unknown_linux_gnu="-std=c++17" + +ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER="qemu-aarch64-static -L /usr/aarch64-linux-gnu" \ + CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \ + CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \ + CXXFLAGS_aarch64_unknown_linux_gnu="-std=c++17" diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml new file mode 100644 index 0000000..5807917 --- /dev/null +++ b/.github/workflows/cron-daily-fuzz.yml @@ -0,0 +1,70 @@ +# Automatically generated by fuzz/generate-files.sh +name: Fuzz +on: + schedule: + # 5am every day UTC, this correlates to: + # - 10pm PDT + # - 6am CET + # - 4pm AEDT + - cron: '00 05 * * *' +permissions: {} + +jobs: + fuzz: + if: ${{ !github.event.act }} + runs-on: ubuntu-24.04 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + # We only get 20 jobs at a time, we probably don't want to go + # over that limit with fuzzing because of the hour run time. + fuzz_target: [ + deserialize_psbt, + ] + 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + id: cache-fuzz + with: + path: | + ~/.cargo/bin + fuzz/target + target + key: cache-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + - uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable + with: + toolchain: '1.74.0' + - 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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: executed_${{ matrix.fuzz_target }} + path: executed_${{ matrix.fuzz_target }} + + verify-execution: + if: ${{ !github.event.act }} + needs: fuzz + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - name: Display structure of downloaded files + run: ls -R + - run: find executed_* -type f -exec cat {} + | sort > executed + - run: cargo fuzz list | sort | diff - executed diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..c7f4776 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,5 @@ +[target.s390x-unknown-linux-gnu] +dockerfile = ".github/docker/Dockerfile.cross" + +[target.aarch64-unknown-linux-gnu] +dockerfile = ".github/docker/Dockerfile.cross" diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..4b091e5 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,230 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitcoin" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "miniscript" +version = "12.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" +dependencies = [ + "bech32", + "bitcoin", +] + +[[package]] +name = "psbt-fuzz" +version = "0.0.1" +dependencies = [ + "arbitrary", + "libfuzzer-sys", + "psbt-v2", +] + +[[package]] +name = "psbt-v2" +version = "0.3.0" +dependencies = [ + "arbitrary", + "bitcoin", + "miniscript", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..034f04f --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "psbt-fuzz" +edition = "2021" +rust-version = "1.74.0" +version = "0.0.1" +authors = ["Generated by fuzz/generate-files.sh"] +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +psbt-v2 = { path = "..", features = ["arbitrary"] } + +arbitrary = { version = "1.0.1" } +libfuzzer-sys = { version = "0.4.0" } + +[lints.rust] +unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)'] } + +[lints.clippy] +redundant_clone = "warn" +use_self = "warn" + +[[bin]] +name = "deserialize_psbt" +path = "fuzz_targets/deserialize_psbt.rs" +test = false +doc = false +bench = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..583f223 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,152 @@ +# Fuzzing + +`rust-psbt` has fuzzing harnesses setup for use with `cargo-fuzz`. + +To run the fuzz-tests as in CI -- briefly fuzzing every target -- simply run + +```bash +./fuzz.sh +``` + +in this directory. + +## 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 + +```bash +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 + +```bash +cargo fuzz list +``` + +To run each of them for an hour, run + +```bash +./cycle.sh +``` +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. + +To run a single fuzztest indefinitely, run + +```bash +cargo +nightly fuzz run "" +``` + +## 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 + +```bash +./generate-files.sh +``` + +Then to test your fuzztest, run + +```bash +./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) that looks something like this: +```text +INFO: Running with entropic power schedule (0xFF, 100). +INFO: Seed: 2953319389 +INFO: Loaded 1 modules (9121 inline 8-bit counters): 9121 [0x104132ea0, 0x104135241), +INFO: Loaded 1 PC tables (9121 PCs): 9121 [0x104135248,0x104158c58), +INFO: 0 files found in /some/path/to/rust-bitcoin/fuzz/corpus/units_arbitrary_weight +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes +INFO: A corpus is not provided, starting from an empty corpus +#2 INITED cov: 42 ft: 42 corp: 1/1b exec/s: 0 rss: 36Mb +#411 NEW cov: 43 ft: 43 corp: 2/9b lim: 8 exec/s: 0 rss: 37Mb L: 8/8 MS: 4 ChangeBinInt-ShuffleBytes-ShuffleBytes-InsertRepeatedBytes- +#1329 NEW cov: 43 ft: 44 corp: 3/26b lim: 17 exec/s: 0 rss: 37Mb L: 17/17 MS: 3 InsertRepeatedBytes-CMP-CopyPart- DE: "\001\000\000\000"- +#1357 REDUCE cov: 43 ft: 44 corp: 3/25b lim: 17 exec/s: 0 rss: 37Mb L: 16/16 MS: 3 CopyPart-CMP-EraseBytes- DE: "\000\000\000\000\000\000\000\000"- +... +``` +If you don't see this, you should quickly see an error. + +## Reproducing Failures + +If a fuzztest fails, it will exit with a summary which looks something like +```text +... +thread '' (3001874) panicked at units/src/weight.rs:103:25: +attempt to multiply with overflow +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +==66478== ERROR: libFuzzer: deadly signal + #0 0x0001049fd3c4 in __sanitizer_print_stack_trace+0x28 (librustc-nightly_rt.asan.dylib:arm64+0x5d3c4) + #1 0x000104078b90 in fuzzer::PrintStackTrace()+0x30 (units_arbitrary_weight:arm64+0x100070b90) + #2 0x00010406d074 in fuzzer::Fuzzer::CrashCallback()+0x54 (units_arbitrary_weight:arm64+0x100065074) + #3 0x000180d26740 in _sigtramp+0x34 (libsystem_platform.dylib:arm64+0x3740) + ... +``` +This will tell you where the test failed and is followed by information about how to reproduce the crash. +It will look something like this: + +```text +... +NOTE: libFuzzer has rudimentary signal handlers. + Combine libFuzzer with AddressSanitizer or similar for better crash reports. +SUMMARY: libFuzzer: deadly signal +MS: 2 ChangeByte-CopyPart-; base unit: 25058c6b0d02cd1d71a030ad61c46b7396ddcdb9 +0x5e,0x5e,0x5e,0x5e,0x5e,0x44,0x0,0x0,0x0,0x0,0x0,0x5d,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x5e,0xa,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0xa5,0x1,0x1,0x1, +^^^^^D\000\000\000\000\000]\001\000\000\000\000\000\000^\012\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\245\001\001\001 +artifact_prefix='/some/path/to/rust-bitcoin/fuzz/artifacts/units_arbitrary_weight/'; Test unit written to /some/path/to/rust-bitcoin/fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 +Base64: Xl5eXl5EAAAAAABdAQAAAAAAAF4KAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBpQEBAQ== +──────────────────────────────────────────────────────────────────────────────── + +Failing input: + + fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 + +Output of `std::fmt::Debug`: + + [94, 94, 94, 94, 94, 68, 0, 0, 0, 0, 0, 93, 1, 0, 0, 0, 0, 0, 0, 94, 10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 165, 1, 1, 1] + +Reproduce with: + + cargo fuzz run units_arbitrary_weight fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 + +Minimize test case with: + + cargo fuzz tmin units_arbitrary_weight fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 + +──────────────────────────────────────────────────────────────────────────────── +``` diff --git a/fuzz/cycle.sh b/fuzz/cycle.sh new file mode 100755 index 0000000..7873853 --- /dev/null +++ b/fuzz/cycle.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Continuously cycle over fuzz targets running each for 1 hour. +# It uses chrt SCHED_IDLE so that other process takes priority. +# +# For cargo-fuzz usage see https://github.com/rust-fuzz/cargo-fuzz?tab=readme-ov-file#usage + +set -euo pipefail + +REPO_DIR=$(git rev-parse --show-toplevel) +# can't find the file because of the ENV var +# shellcheck source=/dev/null +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 + chrt -i 0 cargo +nightly fuzz run "$targetName" -- -max_total_time=3600 + cargo +nightly fuzz cmin "$targetName" + done +done diff --git a/fuzz/fuzz-util.sh b/fuzz/fuzz-util.sh new file mode 100755 index 0000000..5b63de9 --- /dev/null +++ b/fuzz/fuzz-util.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Sort order is affected by locale. See `man sort`. +# > Set LC_ALL=C to get the traditional sort order that uses native byte values. +export LC_ALL=C + +REPO_DIR=$(git rev-parse --show-toplevel) + +listTargetFiles() { + pushd "$REPO_DIR/fuzz" > /dev/null || exit 1 + find fuzz_targets/ -type f -name "*.rs" | sort + popd > /dev/null || exit 1 +} + +targetFileToName() { + echo "$1" \ + | sed 's/^fuzz_targets\///' \ + | sed 's/\.rs$//' \ + | sed 's/\//_/g' \ + | sed 's/^_//g' +} + +# 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 has artifacts, and dumps them in hex +checkReport() { + artifactDir="fuzz/artifacts/$1" + if [ -d "$artifactDir" ] && [ -n "$(ls -A "$artifactDir" 2>/dev/null)" ]; then + echo "Artifacts found for target: $1" + for artifact in "$artifactDir"/*; do + if [ -f "$artifact" ]; then + echo "Artifact: $(basename "$artifact")" + xxd -p -c10000 < "$artifact" + fi + done + exit 1 + fi +} diff --git a/fuzz/fuzz.sh b/fuzz/fuzz.sh new file mode 100755 index 0000000..49641f9 --- /dev/null +++ b/fuzz/fuzz.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# This script is used to briefly fuzz every target when no target is provided. Otherwise, it will briefly fuzz the +# provided target + +set -euox pipefail + +REPO_DIR=$(git rev-parse --show-toplevel) + +# can't find the file because of the ENV var +# shellcheck source=/dev/null +source "$REPO_DIR/fuzz/fuzz-util.sh" + +# Check that input files are correct Windows file names +checkWindowsFiles + +if [ -z "${1:-}" ]; then + targetFiles="$(listTargetFiles)" +else + targetFiles=fuzz_targets/"$1".rs +fi + +cargo --version +rustc --version + +# Testing +cargo install --force cargo-fuzz +for targetFile in $targetFiles; do + targetName=$(targetFileToName "$targetFile") + echo "Fuzzing target $targetName ($targetFile)" + # cargo-fuzz will check for the corpus at fuzz/corpus/ + cargo +nightly fuzz run "$targetName" -- -runs=100000 + checkReport "$targetName" +done + diff --git a/fuzz/fuzz_targets/deserialize_psbt.rs b/fuzz/fuzz_targets/deserialize_psbt.rs new file mode 100644 index 0000000..e5a0781 --- /dev/null +++ b/fuzz/fuzz_targets/deserialize_psbt.rs @@ -0,0 +1,38 @@ +#![cfg_attr(fuzzing, no_main)] +#![cfg_attr(not(fuzzing), allow(unused))] + +use arbitrary::{Arbitrary, Unstructured}; +use libfuzzer_sys::fuzz_target; + +#[cfg(not(fuzzing))] +fn main() {} + +fn do_test(data: &[u8]) { + let mut unstructured = Unstructured::new(data); + + let Ok(bytes_a) = <&[u8]>::arbitrary(&mut unstructured) else { + return; + }; + let Ok(bytes_b) = <&[u8]>::arbitrary(&mut unstructured) else { + return; + }; + + let Ok(psbt_a) = psbt_v2::v0::Psbt::deserialize(bytes_a) else { + return; + }; + + let ser = psbt_v2::v0::Psbt::serialize(&psbt_a); + let deser = psbt_v2::v0::Psbt::deserialize(&ser).unwrap(); + assert_eq!(ser, psbt_v2::v0::Psbt::serialize(&deser)); + + let Ok(mut psbt_b) = psbt_v2::v0::Psbt::deserialize(bytes_b) else { + return; + }; + + let mut psbt_a_clone = psbt_a.clone(); + assert_eq!(psbt_b.combine(psbt_a).is_ok(), psbt_a_clone.combine(psbt_b).is_ok()); +} + +fuzz_target!(|data| { + do_test(data); +}); diff --git a/fuzz/generate_files.sh b/fuzz/generate_files.sh new file mode 100755 index 0000000..16744e0 --- /dev/null +++ b/fuzz/generate_files.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_DIR=$(git rev-parse --show-toplevel) + +# can't find the file because of the ENV var +# shellcheck source=/dev/null +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/cron-daily-fuzz.yml" <executed_\${{ matrix.fuzz_target }} + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: executed_\${{ matrix.fuzz_target }} + path: executed_\${{ matrix.fuzz_target }} + + verify-execution: + if: \${{ !github.event.act }} + needs: fuzz + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - name: Display structure of downloaded files + run: ls -R + - run: find executed_* -type f -exec cat {} + | sort > executed + - run: cargo fuzz list | sort | diff - executed +EOF