From f3411dad09be84ee43c468784f1205d6b6861b6e Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 7 Oct 2026 08:42:11 -0400 Subject: [PATCH] chore(ci): fuzz Jazzer targets on relevant merges, tighten fuzzZipRead, add zip seeds Add a workflow that runs each @FuzzTest in Fuzzing for its full maxDuration, one matrix job per target, since Jazzer fuzzes one target per run. It does not run on pull requests: fuzzing takes ten minutes per target, which is too slow for the payoff on every change. It runs on pushes to main that touch sdk/src, a pom.xml, or the workflow itself, so merges that cannot change the fuzzed code do not spend runner time. A concurrency group coalesces bursts of merges into a single queued run of the latest commit. It can also be started by hand. The corpus Jazzer generates is cached per target and saved even when the run fails, so each run continues from the last. When a target finds something, the reproducing crash-* input and the surefire reports are uploaded as an artifact. fuzzZipRead swallowed IllegalArgumentException along with the expected InvalidZipException, so a reader that seeks to a corrupt offset instead of rejecting the archive passed silently. Only InvalidZipException, IOException, and JsonParseException (from parsing the .json entries) are now expected; anything else fails the fuzz target. Add seed archives covering structures the existing corpus does not reach: zip64 end of central directory records (full, entry-count-only, with trailing data, with a decoy signature in the comment), UTF-8 entry names, an empty archive, and a commons-compress Zip64Mode.Always archive. Ignore the .cifuzz-corpus directory that Jazzer writes during fuzzing runs. Refs: DSPX-5070 Signed-off-by: Dave Mihalcik --- .github/workflows/fuzz.yaml | 97 ++++++++++++++++++ .gitignore | 4 +- .../java/io/opentdf/platform/sdk/Fuzzing.java | 7 +- .../fuzzZipRead/seed-commons-zip64-always | Bin 0 -> 480 bytes .../sdk/FuzzingInputs/fuzzZipRead/seed-empty | Bin 0 -> 22 bytes .../fuzzZipRead/seed-plain-utf8-names | Bin 0 -> 406 bytes .../sdk/FuzzingInputs/fuzzZipRead/seed-zip64 | Bin 0 -> 554 bytes .../seed-zip64-comment-with-decoy-signature | Bin 0 -> 618 bytes .../fuzzZipRead/seed-zip64-entry-count-only | Bin 0 -> 554 bytes .../fuzzZipRead/seed-zip64-trailing-data | Bin 0 -> 654 bytes 10 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/fuzz.yaml create mode 100644 sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-commons-zip64-always create mode 100644 sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-empty create mode 100644 sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-plain-utf8-names create mode 100644 sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64 create mode 100644 sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-comment-with-decoy-signature create mode 100644 sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-entry-count-only create mode 100644 sdk/src/test/resources/io/opentdf/platform/sdk/FuzzingInputs/fuzzZipRead/seed-zip64-trailing-data 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 0000000000000000000000000000000000000000..00cceefe72d2bcc1848878a0cc438d22932e1e60 GIT binary patch literal 480 zcmWIWW@gc4VBlb2c-G_=+tHPM=sy&2GKer3=oKVZ=Hw@)Ffs@*7%)Hpj80U5N-E^! z=cOy8=H-{BXDF29D-@I#1L=%Zh1|rv%(T?v5`{#NiU1U=PR!HLf?356whE?NFRM5| z4`iVn)Iu0ttpv4D$qL9U$}h=J&d*V@QmV=ni$84O=!BWVfnW@FQV^1*J0vYF86El~a`C=Jxd4r10ZLIglm QfHx}}h{ptkAoUQn0IoA~tpET3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..15cb0ecb3e219d1701294bfdf0fe3f5cb5d208e7 GIT binary patch literal 22 NcmWIWW@Tf*000g10H*)| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e9247c522b8c1ab89e7c664077e2248b31f68be2 GIT binary patch literal 406 zcmWIWW@gc4VBlb2NQ+I2Ef@T?1;_+(8MuM?!~7|ep6pntS5i@uoS#>cnpaXBfTE$J zEBlZEP$39&f@r;h#LArf#FRt@C_^D9KQCP&H7~z3Jwu@+U!kD17)WQND&!{SWu~PT zmnbBHRG=DrVxESU9MCur<_8-KQ>~X(oS#>%1T|dA3dk(VFUe2N&rz~cs>&=-s;v$1 zW@OT3!0joZc}M`si|88BJ%`X`2xP*&imnaa@d#}lU~MQ~3GilR11Vqz!nHv9GKj+f E05e%$YXATM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..981ceb9ffcf6d10d70e0877ed94b1f3fbadf6a7c GIT binary patch literal 554 zcmZ`#u};G<5WUcnhN&w9LYAirXwQd`s=@dHQq-622%kWcc-`_rrJ0s!WT&uXExPTk76!mQIqs?|N zgNt*2i`hq3N-F8RvgV$LNBP*7ch=c!tNEC3ijr@)Qbb2X_}`}2Yei$4#>g^m`EB{E zHG-$j2Sjk%m>@HkflQKZTO`?szmh*pqA>c$AbhS{kRpz+LVd6r>dr6x2>oY3Ki9)@ Q7u5#{QXI$F(PD@IdyyZ9N&o-= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9f532d653d6e1339ab65e663501ea1b04b3bb3d8 GIT binary patch literal 618 zcmWIWW@gc4VBlb2NQ+I2Jvn_x01E>H0|;{hadB>9PL5tlMM+6!US$ASDF;+3P?q68 z6o8Z(=oKVZ=Hw@)Bq~4|3OV_C=?bZN`K9R@3MKgp1*OG6IwMsfH!&|WEw#8rArYh^ zz?+?;qbvK60mwukfYA`^U@ki`PeV%%C=0^;AXR!WQ}nWm^Yf~epq46G0hvYlCHcwu zIZ9SaRhb1!wY34>j7+)=xV;H94++3M4bukmGRTz(FUufl0g1u`fRRB0?g^M@w4sV& zMk0F)W+XoYrjaO~1Q`tSKM26+Sr9HmfHxbP4wMb{DwNHPMn8t~pF(M%K6ViE1S3QM WLQd`s=@dHQq-622%kWcc-`_rrJ0s!WT&uXExPTk76!mQIqs?|N zgNt*2i`hq3N-F8RvgV$LNBP*7ch=c!tNEC3ijr@)Qbb2X_}`}2Yei$4#>g^m`EB{E zHG-$j2Sjk%m>@HkflQKZTO`?szmh*pqA>c$AbhS{kRpz+LVd6r>dr6x2>oY3Ki9)@ R7u5#{QXI$F;bO?IxqEAmff@h+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9588644084519c319d7d8f14437b7a7fdf806842 GIT binary patch literal 654 zcmWIWW@gc4VBlb2NQ+I2Jvn_x01E>H0|;{hadB>9PL5tlMM+6!US$ASDF;+3P?q68 z6o8Z(=oKVZ=Hw@)Bq~4|3OV_C=?bZN`K9R@3MKgp1*OG6IwMsfH!&|WEw#8rArYh^ zz?+?;qbvK60mwukfYA`^U@ki`PeV%%C=0^;AXR!WQ}nWm^Yf~epq46G0hvYlCHcwu zIZ9SaRhb1!wY34>j7+)=xV;H94++3M4bukmGRTz(FUufl0g1u`fRRB0?g^M@w4sV& zMk0F)W+XoYrjaO~1Q`tSKM26+Sr9HmfHxbP4wMb{DwNHPMn8t~pF(M%K6ViE1S3QM TL