diff --git a/.github/workflows/fuzz.yaml b/.github/workflows/fuzz.yaml new file mode 100644 index 000000000..fa9de9832 --- /dev/null +++ b/.github/workflows/fuzz.yaml @@ -0,0 +1,97 @@ +name: "Fuzz" + +# Coverage-guided fuzzing of the Jazzer targets in sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java. +# Fuzzing takes the full maxDuration per target, so it runs after merges to main that touch the fuzzed code +# rather than on pull requests. The corpus Jazzer builds is cached between runs so that each run picks up +# where the last one stopped. + +on: + push: + branches: + - main + paths: + - "sdk/src/**" + - "**/pom.xml" + - ".github/workflows/fuzz.yaml" + workflow_dispatch: + +# a burst of merges queues a single run of the latest commit instead of one run each +concurrency: + group: fuzz + +permissions: + contents: read + +jobs: + fuzz: + name: "Fuzz ${{ matrix.target }}" + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + target: + - fuzzZipRead + - fuzzTDF + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Cache Maven packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Set up JDK + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + java-version: "17" + distribution: "temurin" + server-id: github + - name: Restore fuzzing corpus + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: sdk/.cifuzz-corpus + key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} + restore-keys: fuzz-corpus-${{ matrix.target }}- + - name: Generate sources + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUF_INPUT_HTTPS_USERNAME: opentdf-bot + BUF_INPUT_HTTPS_PASSWORD: ${{ secrets.PERSONAL_ACCESS_TOKEN_OPENTDF }} + run: mvn --batch-mode clean generate-sources -Denforcer.skip + - name: Fuzz + env: + JAZZER_FUZZ: "1" + run: | + # the default non-fips profile needs the sdk-fips-bc jar installed + mvn --batch-mode install -pl sdk-fips-bc -am \ + -Dmaven.antrun.skip \ + -Dmaven.test.skip \ + -Denforcer.skip + mvn --batch-mode test -pl sdk \ + -Dmaven.antrun.skip \ + -Denforcer.skip \ + -Dtest='Fuzzing#${{ matrix.target }}' \ + -Dsurefire.failIfNoSpecifiedTests=false + - name: Save fuzzing corpus + if: always() + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: sdk/.cifuzz-corpus + key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} + - name: Upload findings + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: fuzz-findings-${{ matrix.target }} + # Jazzer writes the input that triggered a finding next to the checked in inputs. Commit it there + # once fixed so that it replays as a regression test + path: | + sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/${{ matrix.target }}/crash-* + sdk/target/surefire-reports/ + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 35853a81b..0b491657d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ sdk/src/main/protogen target/ .vscode/ .DS_Store -sdk/sample.tdf \ No newline at end of file +sdk/sample.tdf +# Jazzer corpus generated by fuzzing runs +.cifuzz-corpus/ diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java b/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java index 72ce61d0b..16870ac56 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java @@ -49,8 +49,11 @@ public void fuzzZipRead(FuzzedDataProvider data) { byte[] fuzzBytes = data.consumeRemainingAsBytes(); try { ZipReaderTest.testReadingZipChannel(new SeekableInMemoryByteChannel(fuzzBytes), false); - } catch (InvalidZipException | IllegalArgumentException | JsonParseException | IOException e) { - // cases which are expected with invalid fuzzed inputs + } catch (InvalidZipException | JsonParseException | IOException e) { + // cases which are expected with invalid fuzzed inputs. JsonParseException comes from + // parsing the .json entries, not the zip. anything else the reader throws on bad + // input, such as an IllegalArgumentException from seeking to a corrupt offset, is a + // bug for the fuzzer to find } } } diff --git a/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-commons-zip64-always b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-commons-zip64-always new file mode 100644 index 000000000..00cceefe7 Binary files /dev/null and b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-commons-zip64-always differ diff --git a/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-empty b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-empty new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-empty differ diff --git a/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-plain-utf8-names b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-plain-utf8-names new file mode 100644 index 000000000..e9247c522 Binary files /dev/null and b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-plain-utf8-names differ diff --git a/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64 b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64 new file mode 100644 index 000000000..981ceb9ff Binary files /dev/null and b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64 differ diff --git a/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-comment-with-decoy-signature b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-comment-with-decoy-signature new file mode 100644 index 000000000..9f532d653 Binary files /dev/null and b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-comment-with-decoy-signature differ diff --git a/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-entry-count-only b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-entry-count-only new file mode 100644 index 000000000..83deb5773 Binary files /dev/null and b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-entry-count-only differ diff --git a/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-trailing-data b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-trailing-data new file mode 100644 index 000000000..958864408 Binary files /dev/null and b/sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-trailing-data differ