diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 0000000..0cbcb22 --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,7 @@ +FROM ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest AS LITE_BUILDER + +FROM gcr.io/oss-fuzz-base/base-builder:v1 +COPY . $SRC/app-kaspa +COPY ./.clusterfuzzlite/build.sh $SRC/ +COPY --from=LITE_BUILDER /opt/ledger-secure-sdk $SRC/app-kaspa/BOLOS_SDK +WORKDIR $SRC/app-kaspa \ No newline at end of file diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 0000000..80bc5f5 --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash -eu + +# build fuzzers + +pushd fuzzing +cmake -DBOLOS_SDK=../BOLOS_SDK -Bbuild -H. +make -C build +mv ./build/fuzz_tx_parser $OUT +mv ./build/fuzz_txin_parser $OUT +mv ./build/fuzz_txout_parser $OUT +popd \ No newline at end of file diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000..e196c5c --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: c \ No newline at end of file diff --git a/.github/workflows/cflite_cron.yaml b/.github/workflows/cflite_cron.yaml new file mode 100644 index 0000000..9261ce7 --- /dev/null +++ b/.github/workflows/cflite_cron.yaml @@ -0,0 +1,40 @@ +name: ClusterFuzzLite cron tasks +on: + workflow_dispatch: + push: + branches: + - main # Use your actual default branch here. + schedule: + - cron: '0 13 * * 6' # At 01:00 PM, only on Saturday +permissions: read-all +jobs: + Fuzzing: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - mode: batch + sanitizer: address + - mode: batch + sanitizer: memory + - mode: prune + sanitizer: address + - mode: coverage + sanitizer: coverage + steps: + - name: Build Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + language: c # Change this to the language you are fuzzing. + sanitizer: ${{ matrix.sanitizer }} + - name: Run Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 # 5 minutes + mode: ${{ matrix.mode }} + sanitizer: ${{ matrix.sanitizer }} diff --git a/.github/workflows/cflite_pr.yaml b/.github/workflows/cflite_pr.yaml new file mode 100644 index 0000000..f70175e --- /dev/null +++ b/.github/workflows/cflite_pr.yaml @@ -0,0 +1,43 @@ +name: ClusterFuzzLite PR fuzzing +on: + pull_request: + paths: + - '**' +permissions: read-all +jobs: + PR: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined, memory] # Override this with the sanitizers you want. + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: c # Change this to the language you are fuzzing. + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: ${{ matrix.sanitizer }} + # Optional but recommended: used to only run fuzzers that are affected + # by the PR. + # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git + # storage-repo-branch: main # Optional. Defaults to "main" + # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 # 5 minutes + mode: 'code-change' + sanitizer: ${{ matrix.sanitizer }} + output-sarif: true + # Optional but recommended: used to download the corpus produced by + # batch fuzzing. + # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git + # storage-repo-branch: main # Optional. Defaults to "main" + # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". \ No newline at end of file diff --git a/fuzzing/README.md b/fuzzing/README.md index d2d2d8b..6c4b366 100644 --- a/fuzzing/README.md +++ b/fuzzing/README.md @@ -5,14 +5,7 @@ Change `~/ledger/app-kaspa` to wherever you actual `app-kaspa` folder is. ``` -docker run --rm -it -v ~/ledger/app-kaspa:/app ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest bash -apk add build-base libc-dev linux-headers libexecinfo-dev compiler-rt -apk del llvm15 - -mkdir -p /usr/lib/clang/12.0.1/lib/linux/ - -cp /app/fuzzing/llvm-headers/libclang_rt.asan-x86_64.a /usr/lib/clang/12.0.1/lib/linux/libclang_rt.asan-x86_64.a -cp /app/fuzzing/llvm-headers/libclang_rt.fuzzer-x86_64.a /usr/lib/clang/12.0.1/lib/linux/libclang_rt.fuzzer-x86_64.a +docker run --rm -it -v ~/ledger/app-kaspa:/app ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest bash ``` ## Compilation @@ -40,13 +33,15 @@ make -C build ## LLVM Compile -Use this if you want to build the LLVM from scratch. +Use this if you want to build the LLVM from scratch and use it A pre-compiled version of the ones this fuzzing needs is in `llvm-headers`. Update your docker run to: ``` docker run -it -v ~/ledger/app-kaspa:/app -v ~/llvm-project:/tmp/llvm-project ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest bash +apk add build-base libc-dev linux-headers libexecinfo-dev compiler-rt +apk del llvm15 ``` Clone the LLVM project from: https://github.com/llvm/llvm-project diff --git a/fuzzing/llvm-headers/libclang_rt.asan-x86_64.a b/fuzzing/llvm-headers/libclang_rt.asan-x86_64.a deleted file mode 100644 index 0c7633b..0000000 Binary files a/fuzzing/llvm-headers/libclang_rt.asan-x86_64.a and /dev/null differ diff --git a/fuzzing/llvm-headers/libclang_rt.fuzzer-x86_64.a b/fuzzing/llvm-headers/libclang_rt.fuzzer-x86_64.a deleted file mode 100644 index bb109d9..0000000 Binary files a/fuzzing/llvm-headers/libclang_rt.fuzzer-x86_64.a and /dev/null differ diff --git a/src/transaction/deserialize.c b/src/transaction/deserialize.c index f3b7ac6..22e07d4 100644 --- a/src/transaction/deserialize.c +++ b/src/transaction/deserialize.c @@ -33,13 +33,21 @@ parser_status_e transaction_output_deserialize(buffer_t *buf, transaction_output return OUTPUT_VALUE_PARSING_ERROR; } - size_t script_len = (size_t) * (buf->ptr + buf->offset); + if (!buffer_can_read(buf, 1)) { + return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR; + } + + uint8_t script_len = (uint8_t) * (buf->ptr + buf->offset); if (script_len == OP_BLAKE2B) { // P2SH = 0xaa + 0x20 + (script hash) + 0x87 // Total length = 35 // script len is actually the second byte if the first one is 0xaa - script_len = (size_t) * (buf->ptr + buf->offset + 1); + if (!buffer_can_read(buf, 2)) { + return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR; + } + + script_len = (uint8_t) * (buf->ptr + buf->offset + 1); // For P2SH, we expect len to always be 0x20 if (script_len != 0x20) { diff --git a/unit-tests/test_tx_parser.c b/unit-tests/test_tx_parser.c index cc4c0ca..7900f12 100644 --- a/unit-tests/test_tx_parser.c +++ b/unit-tests/test_tx_parser.c @@ -303,6 +303,11 @@ static void test_tx_output_deserialization_fail(void **state) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86 }; + uint8_t invalid_script_missing[] = { + // Value only, no script + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, 0x00 + }; + uint8_t invalid_script_start[] = { // Start is not 0x20 or 0x21 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, 0xa0, @@ -368,6 +373,7 @@ static void test_tx_output_deserialization_fail(void **state) { }; assert_int_equal(run_test_tx_output_serialize(invalid_value, sizeof(invalid_value)), OUTPUT_VALUE_PARSING_ERROR); + assert_int_equal(run_test_tx_output_serialize(invalid_script_missing, sizeof(invalid_script_missing)), OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR); assert_int_equal(run_test_tx_output_serialize(invalid_script_start, sizeof(invalid_script_start)), OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR); assert_int_equal(run_test_tx_output_serialize(invalid_script_end_schnorr, sizeof(invalid_script_end_ecdsa)), OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR); assert_int_equal(run_test_tx_output_serialize(invalid_script_end_ecdsa, sizeof(invalid_script_end_ecdsa)), OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR);