From dc7f4fa35904eb8420b2e7cc57565f3ae986077d Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 10:17:45 +0200 Subject: [PATCH 01/10] feat(ci): add ClusterFuzzLite fuzzing, SLSA provenance attestation, and GitHub Releases - Add .clusterfuzzlite/ config (project.yaml, Dockerfile, build.sh) with standalone Jazzer fuzz targets for PathNavigator and MatchingUtilities - Add clusterfuzzlite.yml workflow: PR code-change fuzzing (5min) + weekly batch (30min) - Add SLSA provenance attestation via actions/attest-build-provenance@v4.1.0 - Add automatic GitHub Release creation on tag pushes via softprops/action-gh-release@v3.0.0 - Capture Docker image digest for provenance subject binding - All actions pinned to latest SHA commits Scorecard checks targeted: Fuzzing, Signed-Releases --- .clusterfuzzlite/Dockerfile | 8 ++ .clusterfuzzlite/build.sh | 126 ++++++++++++++++++++++++++ .clusterfuzzlite/project.yaml | 1 + .github/workflows/ci.yml | 43 ++++++++- .github/workflows/clusterfuzzlite.yml | 75 +++++++++++++++ 5 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 .clusterfuzzlite/Dockerfile create mode 100644 .clusterfuzzlite/build.sh create mode 100644 .clusterfuzzlite/project.yaml create mode 100644 .github/workflows/clusterfuzzlite.yml diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 000000000..07b494479 --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,8 @@ +FROM gcr.io/oss-fuzz-base/base-builder-jvm + +# Install Maven (project uses Maven wrapper, but ClusterFuzzLite needs mvn on PATH) +RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/* + +COPY . $SRC/project +WORKDIR $SRC/project +COPY .clusterfuzzlite/build.sh $SRC/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 000000000..2bc255c5e --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,126 @@ +#!/bin/bash -eu +# ClusterFuzzLite build script for EDDI +# Compiles the project and packages fuzz targets for Jazzer +# +# The base-builder-jvm image provides: +# $OUT — directory for final fuzzer executables +# $SRC — source directory +# $JAZZER_API_PATH — path to jazzer_agent_deploy.jar + +cd $SRC/project + +# Build project (skip tests — we just need the compiled classes + deps) +mvn clean compile test-compile -DskipTests -B -q + +# Copy dependency JARs into a flat directory +mvn dependency:copy-dependencies \ + -DoutputDirectory=target/deps \ + -DincludeScope=test -B -q + +# ── Stage all runtime artifacts into $OUT ──────────────────────── + +# Project classes and test classes +cp -r target/classes $OUT/classes +cp -r target/test-classes $OUT/test-classes + +# All dependency JARs +mkdir -p $OUT/deps +cp target/deps/*.jar $OUT/deps/ 2>/dev/null || true + +# Build the runtime classpath (relative to $OUT via $this_dir) +# This will be interpolated into each wrapper script +RUNTIME_CP="\$this_dir/classes:\$this_dir/test-classes:\$this_dir/deps/*" + +# ── Fuzz Target: PathNavigatorFuzzer ── +cat > $SRC/PathNavigatorFuzzer.java << 'EOF' +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import ai.labs.eddi.utils.PathNavigator; +import java.util.*; + +public class PathNavigatorFuzzer { + private static final Map SEED_DATA = buildSeedData(); + + private static Map buildSeedData() { + Map root = new HashMap<>(); + root.put("memory", Map.of( + "current", Map.of("output", "hello world", "input", "user said something"), + "last", Map.of("output", "previous response"))); + root.put("properties", Map.of( + "count", 42, "name", "test-agent", "score", 3.14, + "flag", true, "tags", List.of("alpha", "beta", "gamma"))); + root.put("items", List.of( + Map.of("name", "first", "value", 100), + Map.of("name", "second", "value", 200))); + root.put("nested", Map.of("deep", Map.of("deeper", Map.of("deepest", "found-it")))); + return root; + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String path = data.consumeString(500); + try { + PathNavigator.getValue(path, SEED_DATA); + } catch (Exception ignored) { + // Expected — fuzzer explores crash paths + } + } +} +EOF + +# ── Fuzz Target: MatchingUtilitiesFuzzer ── +cat > $SRC/MatchingUtilitiesFuzzer.java << 'EOF' +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import ai.labs.eddi.utils.MatchingUtilities; +import java.util.*; + +public class MatchingUtilitiesFuzzer { + private static final Map DATA = buildData(); + + private static Map buildData() { + Map data = new HashMap<>(); + data.put("memory", Map.of( + "current", Map.of("input", "hello", "output", "world"))); + data.put("properties", Map.of( + "language", "en", "count", 5, "active", true, + "tags", List.of("premium", "beta", "eu"))); + data.put("context", Map.of("channel", "web")); + return data; + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String valuePath = data.consumeString(300); + String equals = data.consumeBoolean() ? data.consumeString(100) : null; + String contains = data.consumeBoolean() ? data.consumeString(100) : null; + try { + MatchingUtilities.executeValuePath(DATA, valuePath, equals, contains); + } catch (Exception ignored) { + // Expected — fuzzer explores crash paths + } + } +} +EOF + +# ── Compile standalone fuzz targets against the project classpath ── +BUILD_CP="target/classes:target/test-classes:target/deps/*" +javac -cp "${BUILD_CP}" \ + -d $OUT \ + $SRC/PathNavigatorFuzzer.java \ + $SRC/MatchingUtilitiesFuzzer.java + +# ── Generate Jazzer wrapper scripts ── +for fuzzer in PathNavigatorFuzzer MatchingUtilitiesFuzzer; do + cat > $OUT/${fuzzer} << WRAPPER +#!/bin/bash +this_dir=\$(dirname "\$0") +LD_LIBRARY_PATH="\${JVM_LD_LIBRARY_PATH:-}":\$this_dir +\$this_dir/jazzer_driver \\ + --agent_path=\$this_dir/jazzer_agent_deploy.jar \\ + --cp=${RUNTIME_CP} \\ + --target_class=${fuzzer} \\ + --jvm_args="-Xmx2048m" \\ + "\$@" +WRAPPER + chmod +x $OUT/${fuzzer} +done + +echo "=== ClusterFuzzLite build complete ===" +echo "Fuzz targets: PathNavigatorFuzzer, MatchingUtilitiesFuzzer" diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 000000000..e6322a24b --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: jvm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51ed76f9a..49ee08aac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -386,8 +386,9 @@ jobs: is-release: ${{ steps.meta.outputs.is-release }} permissions: - contents: read + contents: write # Required for creating GitHub Releases on tag pushes id-token: write # Required for Sigstore cosign keyless signing (OIDC → Fulcio) + attestations: write # Required for SLSA provenance attestation steps: - name: Checkout @@ -486,6 +487,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Push to Docker Hub + id: docker-push run: | PRIMARY_TAG="${{ steps.meta.outputs.primary-tag }}" IS_RELEASE="${{ steps.meta.outputs.is-release }}" @@ -500,7 +502,11 @@ jobs: docker push ${DOCKER_IMAGE}:latest fi + # Capture the pushed image digest for provenance attestation + DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${DOCKER_IMAGE}:${PRIMARY_TAG} | cut -d'@' -f2) + echo "digest=${DIGEST}" >> $GITHUB_OUTPUT echo "### ✅ Pushed to Docker Hub" >> $GITHUB_STEP_SUMMARY + echo "Image digest: \`${DIGEST}\`" >> $GITHUB_STEP_SUMMARY - name: Install cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 @@ -529,6 +535,41 @@ jobs: echo "Keyless OIDC signing via GitHub Actions identity." >> $GITHUB_STEP_SUMMARY echo "Verify: \`cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp '^https://github\\.com/labsai/EDDI/\\.github/workflows/ci\\.yml@refs/(heads/main|tags/.+)$' ${DOCKER_IMAGE}:${PRIMARY_TAG}\`" >> $GITHUB_STEP_SUMMARY + - name: Generate SLSA provenance attestation + if: steps.docker-push.outputs.digest != '' + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: docker.io/${{ env.DOCKER_IMAGE }} + subject-digest: ${{ steps.docker-push.outputs.digest }} + push-to-registry: true + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + with: + generate_release_notes: true + make_latest: true + body: | + ## 🐳 Docker Image + + This release is distributed as a Docker image: + + ```bash + docker pull ${{ env.DOCKER_IMAGE }}:${{ steps.meta.outputs.primary-tag }} + ``` + + ### Verify image signature + + ```bash + cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github\.com/labsai/EDDI/\.github/workflows/ci\.yml@refs/(heads/main|tags/.+)$' \ + ${{ env.DOCKER_IMAGE }}:${{ steps.meta.outputs.primary-tag }} + ``` + + > **Note:** EDDI is distributed exclusively as a Docker image. There are no binary downloads. + > See the [Docker Hub page](https://hub.docker.com/r/labsai/eddi) for all available tags. + # ─── Job 4: Smoke Test ────────────────────────────────────────── smoke-test: name: Smoke Test diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml new file mode 100644 index 000000000..0875e95cd --- /dev/null +++ b/.github/workflows/clusterfuzzlite.yml @@ -0,0 +1,75 @@ +# ClusterFuzzLite — coverage-guided fuzzing for EDDI +# Detected by OpenSSF Scorecard as the "Fuzzing" check +# Fuzz targets: PathNavigator (OGNL replacement), MatchingUtilities (behavior rule conditions) +# +# Modes: +# - PR: code-change fuzzing (5 min) — catches regressions in changed code +# - Weekly: batch fuzzing (30 min) — deep continuous fuzzing for rare crashes + +name: ClusterFuzzLite + +on: + pull_request: + branches: [main] + paths: + - 'src/main/java/**' + - 'src/test/java/**/Fuzz*.java' + - 'src/test/java/**/*Fuzz*.java' + - '.clusterfuzzlite/**' + schedule: + - cron: '0 4 * * 0' # Sunday 4am UTC — weekly deep fuzz + +permissions: {} + +jobs: + # ─── PR mode: fuzz only changed code paths ───────────────────── + pr-fuzzing: + name: Fuzz (PR) + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + permissions: + security-events: write # Upload SARIF findings + + steps: + - name: Build Fuzzers + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@52ecc61cb587ee99c26825a112a21abf19c7448c # v1 + with: + language: jvm + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: address + + - name: Run Fuzzers + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@52ecc61cb587ee99c26825a112a21abf19c7448c # v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 + mode: code-change + + # ─── Weekly batch mode: deep continuous fuzzing ───────────────── + batch-fuzzing: + name: Fuzz (Weekly) + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + + permissions: + security-events: write + + steps: + - name: Build Fuzzers + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@52ecc61cb587ee99c26825a112a21abf19c7448c # v1 + with: + language: jvm + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: address + + - name: Run Fuzzers + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@52ecc61cb587ee99c26825a112a21abf19c7448c # v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 1800 + mode: batch From 46039931b9bce9c7e56028ec5db6a61fcfc00229 Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 10:25:52 +0200 Subject: [PATCH 02/10] chore(docs): add changelog entry for scorecard improvements --- docs/changelog.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 996a1e01d..fe9c00297 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -13,6 +13,46 @@ Each entry follows this format: - **Decision** — Key design decisions and their reasoning - **Files** — Links to modified files +## 🔒 OpenSSF Scorecard: Fuzzing + SLSA Provenance + Signed Releases (2026-04-23) + +**Repo:** EDDI (`chore/scorecard-improvements`) + +**What changed:** Added continuous fuzzing, SLSA supply-chain provenance attestation, and automated GitHub Release creation to satisfy three remaining OpenSSF Scorecard checks. + +### ClusterFuzzLite Fuzzing + +- Created `.clusterfuzzlite/` config directory with `project.yaml`, `Dockerfile`, and `build.sh` +- `build.sh` compiles standalone Jazzer fuzz targets for `PathNavigator` and `MatchingUtilities` using proper `jazzer_driver` + `$this_dir`-relative classpath +- Added `.github/workflows/clusterfuzzlite.yml` with two modes: + - **PR mode:** code-change fuzzing (5 min) on PRs touching `src/` + - **Weekly batch:** deep continuous fuzzing (30 min) on Sunday 4am UTC + +### SLSA Provenance Attestation + +- Captures Docker image digest after push (`docker inspect --format`) +- Generates SLSA build provenance attestation via `actions/attest-build-provenance@v4.1.0` +- Pushes attestation to Docker Hub registry alongside the image + +### GitHub Releases (Signed-Releases) + +- Auto-creates GitHub Release on tag pushes via `softprops/action-gh-release@v3.0.0` +- Release body includes Docker pull instructions and `cosign verify` command +- Documents that EDDI is container-only (no binary downloads) + +### Action Version Pinning + +- `sigstore/cosign-installer@v4.1.1` (SHA `cad07c2e...`) +- `actions/attest-build-provenance@v4.1.0` (SHA `a2bbfa25...`) +- `softprops/action-gh-release@v3.0.0` (SHA `b4309332...`) +- `google/clusterfuzzlite@v1` (SHA `52ecc61c...`) — all 4 references + +### Code Review Findings (fixed) + +- **Critical:** Original `build.sh` used absolute build-time container paths in runtime wrapper scripts — rewrote to use `$this_dir`-relative paths and `jazzer_driver` from the base image +- **Minor:** Added `try/catch` in fuzz targets for expected exceptions to prevent Jazzer misreporting + +--- + ## 🐛 Compose AuthStartupGuard Fix & CI Tag Bypass (2026-04-23) **Repo:** EDDI (`fix/compose-auth-guard`) From b74bac2858b05e28f6094cf1282ddfae525332fe Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 13:05:42 +0200 Subject: [PATCH 03/10] fix(ci): resolve ClusterFuzzLite build failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three root causes identified and fixed: 1. .dockerignore blocked build context: The repo .dockerignore starts with * (exclude-all). Added minimal allowlist entries for .clusterfuzzlite/ and src/main/java/.../utils/ so the CFL Dockerfile can access build.sh and the utility source files. 2. JDK version mismatch: Project requires Java 25 (maven.compiler.release=25) but base-builder-jvm ships Java 21. Eliminated Maven entirely — build.sh now compiles only the 3 needed source files directly with javac. 3. Java 22+ syntax incompatibility: Source files use unnamed variables (catch Exception _) which is a Java 22 feature. build.sh now copies sources to a staging dir and patches them with perl before compiling with -source/-target 21. --- .clusterfuzzlite/Dockerfile | 4 +-- .clusterfuzzlite/build.sh | 56 +++++++++++++++++++++---------------- .dockerignore | 2 ++ 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 07b494479..3c4c532f2 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -1,7 +1,7 @@ FROM gcr.io/oss-fuzz-base/base-builder-jvm -# Install Maven (project uses Maven wrapper, but ClusterFuzzLite needs mvn on PATH) -RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/* +# No Maven or JDK 25 needed — build.sh compiles only the 2 utility +# classes directly with javac (targeting Java 17 to match the runtime JVM). COPY . $SRC/project WORKDIR $SRC/project diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 2bc255c5e..c6540d9cf 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -1,35 +1,38 @@ #!/bin/bash -eu # ClusterFuzzLite build script for EDDI -# Compiles the project and packages fuzz targets for Jazzer +# Compiles ONLY the specific utility classes needed by fuzz targets. +# No Maven, no full project build — avoids JDK version mismatches. # # The base-builder-jvm image provides: # $OUT — directory for final fuzzer executables -# $SRC — source directory +# $SRC — source directory (this is /src) # $JAZZER_API_PATH — path to jazzer_agent_deploy.jar cd $SRC/project -# Build project (skip tests — we just need the compiled classes + deps) -mvn clean compile test-compile -DskipTests -B -q +# ── Copy and patch source files for JDK 21 compatibility ── +# The project targets Java 25 and uses unnamed variables (catch Exception _) +# which is a Java 22+ feature. Patch these for JDK 21 compatibility. +FUZZ_SRC=$SRC/fuzz-src +mkdir -p $FUZZ_SRC/ai/labs/eddi/utils -# Copy dependency JARs into a flat directory -mvn dependency:copy-dependencies \ - -DoutputDirectory=target/deps \ - -DincludeScope=test -B -q - -# ── Stage all runtime artifacts into $OUT ──────────────────────── +for f in PathNavigator.java MatchingUtilities.java RuntimeUtilities.java; do + cp "src/main/java/ai/labs/eddi/utils/$f" "$FUZZ_SRC/ai/labs/eddi/utils/$f" +done -# Project classes and test classes -cp -r target/classes $OUT/classes -cp -r target/test-classes $OUT/test-classes +# Patch unnamed variables: catch (SomeException _) → catch (SomeException ignored) +# This is a Java 22+ feature; CFL runtime uses Java 21. +perl -pi -e 's/(\w+)\s+_\s*\)/\1 ignored)/g' $FUZZ_SRC/ai/labs/eddi/utils/*.java -# All dependency JARs -mkdir -p $OUT/deps -cp target/deps/*.jar $OUT/deps/ 2>/dev/null || true +# Compile utility classes (target Java 21 to match CFL runtime JVM) +mkdir -p $OUT/classes +javac -source 21 -target 21 \ + -d $OUT/classes \ + $FUZZ_SRC/ai/labs/eddi/utils/PathNavigator.java \ + $FUZZ_SRC/ai/labs/eddi/utils/MatchingUtilities.java \ + $FUZZ_SRC/ai/labs/eddi/utils/RuntimeUtilities.java -# Build the runtime classpath (relative to $OUT via $this_dir) -# This will be interpolated into each wrapper script -RUNTIME_CP="\$this_dir/classes:\$this_dir/test-classes:\$this_dir/deps/*" +echo "✅ Utility classes compiled (Java 21 target)" # ── Fuzz Target: PathNavigatorFuzzer ── cat > $SRC/PathNavigatorFuzzer.java << 'EOF' @@ -60,7 +63,7 @@ public class PathNavigatorFuzzer { try { PathNavigator.getValue(path, SEED_DATA); } catch (Exception ignored) { - // Expected — fuzzer explores crash paths + // Expected — fuzzer explores error paths } } } @@ -93,19 +96,24 @@ public class MatchingUtilitiesFuzzer { try { MatchingUtilities.executeValuePath(DATA, valuePath, equals, contains); } catch (Exception ignored) { - // Expected — fuzzer explores crash paths + // Expected — fuzzer explores error paths } } } EOF -# ── Compile standalone fuzz targets against the project classpath ── -BUILD_CP="target/classes:target/test-classes:target/deps/*" -javac -cp "${BUILD_CP}" \ +# ── Compile fuzz targets against the utility classes ── +javac -source 21 -target 21 \ + -cp "$OUT/classes" \ -d $OUT \ $SRC/PathNavigatorFuzzer.java \ $SRC/MatchingUtilitiesFuzzer.java +echo "✅ Fuzz targets compiled" + +# ── Build runtime classpath (relative to $OUT via $this_dir) ── +RUNTIME_CP="\$this_dir/classes:\$this_dir" + # ── Generate Jazzer wrapper scripts ── for fuzzer in PathNavigatorFuzzer MatchingUtilitiesFuzzer; do cat > $OUT/${fuzzer} << WRAPPER diff --git a/.dockerignore b/.dockerignore index c9b9aab2e..eeaf3027f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ * +!.clusterfuzzlite/ +!src/main/java/ai/labs/eddi/utils/ !target/*-runner !target/*-runner.jar !target/lib/* From dddeb690d1db771afc50c9ebc661fb9a2a7f3d69 Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 13:37:12 +0200 Subject: [PATCH 04/10] fix(ci): harden ClusterFuzzLite build and integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClusterFuzzLite: - Vendor utility .java files into .clusterfuzzlite/ to avoid Docker context issues (parent directory un-ignore is fragile in .dockerignore) - Remove src/ exceptions from .dockerignore — zero production impact - Auto-detect JDK version (17 or 21) instead of hardcoding --release - Use --release instead of deprecated -source/-target pair - Add to fuzz target compilation classpath (was missing) - Narrow perl regex to only catch blocks: \bcatch\s*\(\s*(\w+)\s+_\s*\) to prevent false positives on lambda/method unnamed parameters Integration tests: - Verify sendUserInput response (assert 200 + steps) before undo/redo - Extract retryUntilOk() helper into BaseIntegrationIT for reuse - Undo/redo use retry loop with 200ms backoff for Postgres visibility lag - redoAfterUndo now asserts undo success before attempting redo --- .clusterfuzzlite/Dockerfile | 4 +- .clusterfuzzlite/MatchingUtilities.java | 37 +++ .clusterfuzzlite/PathNavigator.java | 235 ++++++++++++++++++ .clusterfuzzlite/RuntimeUtilities.java | 68 +++++ .clusterfuzzlite/build.sh | 39 +-- .dockerignore | 2 +- .../eddi/integration/BaseIntegrationIT.java | 23 ++ .../ConversationServiceComponentIT.java | 35 +-- 8 files changed, 410 insertions(+), 33 deletions(-) create mode 100644 .clusterfuzzlite/MatchingUtilities.java create mode 100644 .clusterfuzzlite/PathNavigator.java create mode 100644 .clusterfuzzlite/RuntimeUtilities.java diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 3c4c532f2..b464040fe 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -1,7 +1,7 @@ FROM gcr.io/oss-fuzz-base/base-builder-jvm -# No Maven or JDK 25 needed — build.sh compiles only the 2 utility -# classes directly with javac (targeting Java 17 to match the runtime JVM). +# No Maven or JDK 25 needed — build.sh compiles only the 3 vendored +# utility classes with javac. JDK version is auto-detected (17 or 21). COPY . $SRC/project WORKDIR $SRC/project diff --git a/.clusterfuzzlite/MatchingUtilities.java b/.clusterfuzzlite/MatchingUtilities.java new file mode 100644 index 000000000..722a43a9a --- /dev/null +++ b/.clusterfuzzlite/MatchingUtilities.java @@ -0,0 +1,37 @@ +package ai.labs.eddi.utils; + +import java.util.List; +import java.util.Map; + +import static ai.labs.eddi.utils.RuntimeUtilities.isNullOrEmpty; + +public class MatchingUtilities { + public static boolean executeValuePath(Map conversationValues, String valuePath, String equals, String contains) { + + boolean success = false; + + Object value = null; + try { + value = PathNavigator.getValue(valuePath, conversationValues); + } catch (Exception _) { + // no value was found, which is an expected case, so silent exception here + } + if (value != null) { + if (!isNullOrEmpty(equals) && equals.equals(value.toString())) { + success = true; + } else if (!isNullOrEmpty(contains)) { + if (value instanceof String s && s.contains(contains)) { + success = true; + } else if (value instanceof List l && l.contains(contains)) { + success = true; + } + } else if (value instanceof Boolean b) { + success = b; + } else if (isNullOrEmpty(equals) && isNullOrEmpty(contains)) { + success = true; + } + } + + return success; + } +} diff --git a/.clusterfuzzlite/PathNavigator.java b/.clusterfuzzlite/PathNavigator.java new file mode 100644 index 000000000..c34fd70ad --- /dev/null +++ b/.clusterfuzzlite/PathNavigator.java @@ -0,0 +1,235 @@ +package ai.labs.eddi.utils; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Safe navigation utility for dot-separated paths through Map/List structures. + * Replaces explicit OGNL calls (Ognl.getValue/Ognl.setValue) to eliminate the + * security surface from arbitrary method invocation. + *

+ * Supports: - Dot-path navigation: "a.b.c" - Array index access: + * "items[0].name" - Simple arithmetic on final value: "properties.count+1" - + * String concatenation: "properties.first+' '+properties.last" + *

+ * Does NOT support method invocation, static class access, or object + * instantiation. + */ +public class PathNavigator { + + // Matches a path segment with optional array index, e.g. "items[0]" or "name" + private static final Pattern SEGMENT_PATTERN = Pattern.compile("([^.\\[]+)(?:\\[(-?\\d+)])?"); + + // Matches arithmetic/concat at end of path: "path.to.value+1" or + // "path.to.value+otherPath" + private static final Pattern ARITHMETIC_PATTERN = Pattern.compile("^(.+?)([+\\-])(.+)$"); + + /** + * Navigate a dot-separated path through a Map/List structure and return the + * value. + * + * @param path + * dot-separated path, e.g. + * "memory.current.httpCalls.weather[0].temp" + * @param root + * the root Map to navigate + * @return the value at the path, or null if not found + */ + public static Object getValue(String path, Object root) { + if (path == null || path.isEmpty() || root == null) { + return null; + } + + // Try plain path navigation first + Object result = navigatePath(path, root); + if (result != null) { + return result; + } + + // If plain navigation returned null, check for arithmetic/concatenation + Matcher arithmeticMatcher = ARITHMETIC_PATTERN.matcher(path); + if (arithmeticMatcher.matches()) { + String leftPath = arithmeticMatcher.group(1).trim(); + String operator = arithmeticMatcher.group(2); + String rightOperand = arithmeticMatcher.group(3).trim(); + + Object leftValue = navigatePath(leftPath, root); + if (leftValue != null) { + // Try to resolve right operand as a path first, then as a literal + Object rightValue = navigatePath(rightOperand, root); + if (rightValue == null) { + rightValue = parseLiteral(rightOperand); + } + + return applyOperator(leftValue, operator, rightValue); + } + } + + return null; + } + + /** + * Set a value at a dot-separated path in a Map structure. + * + * @param path + * dot-separated path to set the value at + * @param root + * the root Map + * @param value + * the value to set + */ + @SuppressWarnings("unchecked") + public static void setValue(String path, Object root, Object value) { + if (path == null || path.isEmpty() || root == null) { + return; + } + + String[] segments = path.split("\\."); + Object current = root; + + // Navigate to the parent of the target + for (int i = 0; i < segments.length - 1; i++) { + current = resolveSegment(segments[i], current); + if (current == null) { + return; + } + } + + // Set the value on the last segment + String lastSegment = segments[segments.length - 1]; + Matcher matcher = SEGMENT_PATTERN.matcher(lastSegment); + if (matcher.matches()) { + String key = matcher.group(1); + String indexStr = matcher.group(2); + + if (indexStr != null && current instanceof Map parentMap) { + Object list = parentMap.get(key); + if (list instanceof List l) { + try { + int index = Integer.parseInt(indexStr); + if (index >= 0 && index < l.size()) { + ((List) l).set(index, value); + } + } catch (NumberFormatException _) { + // Index exceeds int range — ignore silently + } + } + } else if (current instanceof Map) { + ((Map) current).put(key, value); + } + } + } + + private static Object navigatePath(String path, Object root) { + Object current = root; + String[] segments = path.split("\\."); + + for (String segment : segments) { + if (current == null) { + return null; + } + current = resolveSegment(segment, current); + } + + return current; + } + + private static Object resolveSegment(String segment, Object current) { + Matcher matcher = SEGMENT_PATTERN.matcher(segment); + if (!matcher.matches()) { + return null; + } + + String key = matcher.group(1); + String indexStr = matcher.group(2); + + // Navigate into Map + if (current instanceof Map map) { + current = map.get(key); + } else { + return null; + } + + // Handle array index if present + if (indexStr != null && current instanceof List list) { + try { + int index = Integer.parseInt(indexStr); + if (index >= 0 && index < list.size()) { + current = list.get(index); + } else { + return null; + } + } catch (NumberFormatException _) { + return null; // Index exceeds int range + } + } + + return current; + } + + private static Object applyOperator(Object left, String operator, Object right) { + if (left == null) { + return right; + } + + // both are numbers — do arithmetic + if (left instanceof Number leftNum && right instanceof Number rightNum) { + if (left instanceof Double || left instanceof Float || right instanceof Double || right instanceof Float) { + double result = switch (operator) { + case "+" -> leftNum.doubleValue() + rightNum.doubleValue(); + case "-" -> leftNum.doubleValue() - rightNum.doubleValue(); + default -> leftNum.doubleValue(); + }; + return result; + } else { + long result = switch (operator) { + case "+" -> leftNum.longValue() + rightNum.longValue(); + case "-" -> leftNum.longValue() - rightNum.longValue(); + default -> leftNum.longValue(); + }; + // Return Integer if it fits, otherwise Long + if (result >= Integer.MIN_VALUE && result <= Integer.MAX_VALUE) { + return (int) result; + } + return result; + } + } + + // String concatenation (+ operator only) + if ("+".equals(operator)) { + String leftStr = left.toString(); + String rightStr = right != null ? right.toString() : ""; + return leftStr + rightStr; + } + + return left; + } + + private static Object parseLiteral(String value) { + if (value == null || value.isEmpty()) { + return null; + } + + // String literal: 'some text' + if (value.startsWith("'") && value.endsWith("'") && value.length() >= 2) { + return value.substring(1, value.length() - 1); + } + + // Integer + try { + return Integer.parseInt(value); + } catch (NumberFormatException _) { + } + + // Double + try { + return Double.parseDouble(value); + } catch (NumberFormatException _) { + } + + // Fallback: treat as string + return value; + } +} diff --git a/.clusterfuzzlite/RuntimeUtilities.java b/.clusterfuzzlite/RuntimeUtilities.java new file mode 100644 index 000000000..508e66764 --- /dev/null +++ b/.clusterfuzzlite/RuntimeUtilities.java @@ -0,0 +1,68 @@ +package ai.labs.eddi.utils; + +import java.io.InputStream; +import java.util.Collection; +import java.util.Map; + +/** + * @author ginccc + */ +public class RuntimeUtilities { + public static void checkNotNull(Object object, String name) { + if (object == null) { + String message = "Argument must not be null (%s)"; + message = String.format(message, name); + throw new IllegalArgumentException(message); + } + } + + public static void checkNotEmpty(Object object, String name) { + if (isNullOrEmpty(object)) { + String message = "Argument must not be null nor empty (%s)"; + message = String.format(message, name); + throw new IllegalArgumentException(message); + } + } + + public static void checkCollectionNoNullElements(Collection collection, String name) { + checkNotNull(collection, name); + + for (Object obj : collection) { + if (obj == null) { + String message = "Collection (name=%s) must not contain any null elements!"; + message = String.format(message, name); + throw new IllegalArgumentException(message); + } + } + } + + public static void checkNotNegative(Integer integer, String name) { + checkNotNull(integer, name); + + if (integer < 0) { + String message = "Argument (%s) must be a non-negative integer."; + message = String.format(message, name); + throw new IllegalArgumentException(message); + } + } + + public static boolean isNullOrEmpty(Object obj) { + if (obj == null) { + return true; + } + + if (obj instanceof String) { + return ((String) obj).isEmpty(); + } + + if (obj instanceof Collection c) { + return c.isEmpty(); + } + + return obj instanceof Map m && m.isEmpty(); + } + + public static InputStream getResourceAsStream(String path) { + return Thread.currentThread().getContextClassLoader().getResourceAsStream(path); + } +} diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index c6540d9cf..c92601487 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -1,7 +1,8 @@ #!/bin/bash -eu # ClusterFuzzLite build script for EDDI # Compiles ONLY the specific utility classes needed by fuzz targets. -# No Maven, no full project build — avoids JDK version mismatches. +# Source files are vendored in .clusterfuzzlite/ to avoid needing the +# full project source tree (which is blocked by .dockerignore). # # The base-builder-jvm image provides: # $OUT — directory for final fuzzer executables @@ -10,29 +11,39 @@ cd $SRC/project -# ── Copy and patch source files for JDK 21 compatibility ── +# ── Patch vendored sources for pre-22 JDK compatibility ── # The project targets Java 25 and uses unnamed variables (catch Exception _) -# which is a Java 22+ feature. Patch these for JDK 21 compatibility. +# which is a Java 22+ feature. Patch these for the CFL image's JDK. FUZZ_SRC=$SRC/fuzz-src mkdir -p $FUZZ_SRC/ai/labs/eddi/utils for f in PathNavigator.java MatchingUtilities.java RuntimeUtilities.java; do - cp "src/main/java/ai/labs/eddi/utils/$f" "$FUZZ_SRC/ai/labs/eddi/utils/$f" + cp ".clusterfuzzlite/$f" "$FUZZ_SRC/ai/labs/eddi/utils/$f" done -# Patch unnamed variables: catch (SomeException _) → catch (SomeException ignored) -# This is a Java 22+ feature; CFL runtime uses Java 21. -perl -pi -e 's/(\w+)\s+_\s*\)/\1 ignored)/g' $FUZZ_SRC/ai/labs/eddi/utils/*.java - -# Compile utility classes (target Java 21 to match CFL runtime JVM) +# Patch: catch (SomeException _) → catch (SomeException ignored) +# Only target catch-block patterns to avoid false matches in other contexts. +perl -pi -e 's/\bcatch\s*\(\s*(\w+)\s+_\s*\)/catch ($1 ignored)/g' \ + $FUZZ_SRC/ai/labs/eddi/utils/*.java + +# Detect JDK version (base image may ship 17 or 21) +JAVA_VER=$(javac -version 2>&1 | grep -oP '\d+' | head -1) +if [ "$JAVA_VER" -ge 21 ] 2>/dev/null; then + RELEASE_VER=21 +else + RELEASE_VER=17 +fi +echo "Detected JDK $JAVA_VER — targeting --release $RELEASE_VER" + +# Compile utility classes mkdir -p $OUT/classes -javac -source 21 -target 21 \ +javac --release $RELEASE_VER \ -d $OUT/classes \ $FUZZ_SRC/ai/labs/eddi/utils/PathNavigator.java \ $FUZZ_SRC/ai/labs/eddi/utils/MatchingUtilities.java \ $FUZZ_SRC/ai/labs/eddi/utils/RuntimeUtilities.java -echo "✅ Utility classes compiled (Java 21 target)" +echo "✅ Utility classes compiled (--release $RELEASE_VER)" # ── Fuzz Target: PathNavigatorFuzzer ── cat > $SRC/PathNavigatorFuzzer.java << 'EOF' @@ -102,9 +113,9 @@ public class MatchingUtilitiesFuzzer { } EOF -# ── Compile fuzz targets against the utility classes ── -javac -source 21 -target 21 \ - -cp "$OUT/classes" \ +# ── Compile fuzz targets against the utility classes + Jazzer API ── +javac --release $RELEASE_VER \ + -cp "$OUT/classes:$JAZZER_API_PATH" \ -d $OUT \ $SRC/PathNavigatorFuzzer.java \ $SRC/MatchingUtilitiesFuzzer.java diff --git a/.dockerignore b/.dockerignore index eeaf3027f..3a61dc68d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,6 @@ * !.clusterfuzzlite/ -!src/main/java/ai/labs/eddi/utils/ +!.clusterfuzzlite/** !target/*-runner !target/*-runner.jar !target/lib/* diff --git a/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java b/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java index 09ef034ca..d995d8963 100644 --- a/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java +++ b/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java @@ -258,6 +258,29 @@ protected void waitForConversationReady(String agentId, String conversationId) t } } + /** + * Retries a REST call up to 10 times (200ms apart) until it returns HTTP 200. + * Useful for operations that depend on async state propagation (e.g., undo/redo + * after conversation processing on PostgreSQL). + * + * @param call + * supplier that executes the REST call + * @param description + * human-readable description for the assertion message + */ + protected void retryUntilOk(java.util.function.Supplier call, String description) + throws InterruptedException { + for (int i = 0; i < 10; i++) { + Response response = call.get(); + if (response.statusCode() == 200) { + return; + } + Thread.sleep(200); + } + // Final attempt — will fail with assertion if still not OK + call.get().then().assertThat().statusCode(200); + } + public record ResourceId(String id, int version) { } } diff --git a/src/test/java/ai/labs/eddi/integration/ConversationServiceComponentIT.java b/src/test/java/ai/labs/eddi/integration/ConversationServiceComponentIT.java index 1ab3abf5c..c089af6bf 100644 --- a/src/test/java/ai/labs/eddi/integration/ConversationServiceComponentIT.java +++ b/src/test/java/ai/labs/eddi/integration/ConversationServiceComponentIT.java @@ -75,33 +75,36 @@ void processInput_currentStepOnly() { @Test @DisplayName("should support undo after user input") - void undoUserInput() { + void undoUserInput() throws Exception { ResourceId conversationId = createConversation(agentResourceId.id(), TEST_USER_ID); - // Send input first - sendUserInput(agentResourceId.id(), conversationId.id(), "hello", false, false); + // Send input first and verify it was processed + Response sayResponse = sendUserInput(agentResourceId.id(), conversationId.id(), "hello", false, false); + sayResponse.then().assertThat().statusCode(200) + .body("conversationSteps", hasSize(greaterThanOrEqualTo(2))); - // Undo — path is /{env}/{agentId}/undo/{convId} - Response undoResponse = given().post(String.format("agents/%s/undo", conversationId.id())); - - undoResponse.then().assertThat().statusCode(200); + // Undo — may need a brief wait for the DB write to be visible on Postgres + retryUntilOk(() -> given().post(String.format("agents/%s/undo", conversationId.id())), + "Undo should succeed after user input"); } @Test @DisplayName("should support redo after undo") - void redoAfterUndo() { + void redoAfterUndo() throws Exception { ResourceId conversationId = createConversation(agentResourceId.id(), TEST_USER_ID); - // Send input - sendUserInput(agentResourceId.id(), conversationId.id(), "hello", false, false); - - // Undo — path is /{env}/{agentId}/undo/{convId} - given().post(String.format("agents/%s/undo", conversationId.id())); + // Send input and verify + Response sayResponse = sendUserInput(agentResourceId.id(), conversationId.id(), "hello", false, false); + sayResponse.then().assertThat().statusCode(200) + .body("conversationSteps", hasSize(greaterThanOrEqualTo(2))); - // Redo — path is /{env}/{agentId}/redo/{convId} - Response redoResponse = given().post(String.format("agents/%s/redo", conversationId.id())); + // Undo — retry until available, then assert success + retryUntilOk(() -> given().post(String.format("agents/%s/undo", conversationId.id())), + "Undo should succeed after user input"); - redoResponse.then().assertThat().statusCode(200); + // Redo — retry until available + retryUntilOk(() -> given().post(String.format("agents/%s/redo", conversationId.id())), + "Redo should succeed after undo"); } @Test From 68a13ae7538f5634d7c772b6e86e3a9ac7a3895c Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 14:06:02 +0200 Subject: [PATCH 05/10] fix(ci): add -encoding UTF-8 to javac in CFL build The base-builder-jvm image defaults to US-ASCII locale. The vendored PathNavigator.java contains em-dash characters in comments which cause 'unmappable character' errors. Adding -encoding UTF-8 resolves this. --- .clusterfuzzlite/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index c92601487..3b2b6e37d 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -37,7 +37,7 @@ echo "Detected JDK $JAVA_VER — targeting --release $RELEASE_VER" # Compile utility classes mkdir -p $OUT/classes -javac --release $RELEASE_VER \ +javac --release $RELEASE_VER -encoding UTF-8 \ -d $OUT/classes \ $FUZZ_SRC/ai/labs/eddi/utils/PathNavigator.java \ $FUZZ_SRC/ai/labs/eddi/utils/MatchingUtilities.java \ @@ -114,7 +114,7 @@ public class MatchingUtilitiesFuzzer { EOF # ── Compile fuzz targets against the utility classes + Jazzer API ── -javac --release $RELEASE_VER \ +javac --release $RELEASE_VER -encoding UTF-8 \ -cp "$OUT/classes:$JAZZER_API_PATH" \ -d $OUT \ $SRC/PathNavigatorFuzzer.java \ From 306ba070556c19497b802fad17c67cf0c9e75271 Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 15:17:21 +0200 Subject: [PATCH 06/10] =?UTF-8?q?fix(ci):=20address=20CFL=20review=20findi?= =?UTF-8?q?ngs=20=E2=80=94=20keep=20targets,=20narrow=20catches,=20export?= =?UTF-8?q?=20LD=5FLIBRARY=5FPATH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add keep-unaffected-fuzz-targets: true to PR build step so vendored- copy PRs don't get all targets stripped (fixes 'No fuzz targets found') - Narrow catch blocks: only catch IllegalArgumentException and StackOverflowError — let Jazzer detect NPE, IOOB, ClassCastException - Export LD_LIBRARY_PATH in wrapper scripts so jazzer_driver inherits it --- .clusterfuzzlite/build.sh | 10 +++++----- .github/workflows/clusterfuzzlite.yml | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 3b2b6e37d..947ae74d8 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -73,8 +73,8 @@ public class PathNavigatorFuzzer { String path = data.consumeString(500); try { PathNavigator.getValue(path, SEED_DATA); - } catch (Exception ignored) { - // Expected — fuzzer explores error paths + } catch (IllegalArgumentException | StackOverflowError ignored) { + // Expected: invalid paths and deep recursion are normal fuzz inputs } } } @@ -106,8 +106,8 @@ public class MatchingUtilitiesFuzzer { String contains = data.consumeBoolean() ? data.consumeString(100) : null; try { MatchingUtilities.executeValuePath(DATA, valuePath, equals, contains); - } catch (Exception ignored) { - // Expected — fuzzer explores error paths + } catch (IllegalArgumentException ignored) { + // Expected: invalid value paths are normal fuzz inputs } } } @@ -130,7 +130,7 @@ for fuzzer in PathNavigatorFuzzer MatchingUtilitiesFuzzer; do cat > $OUT/${fuzzer} << WRAPPER #!/bin/bash this_dir=\$(dirname "\$0") -LD_LIBRARY_PATH="\${JVM_LD_LIBRARY_PATH:-}":\$this_dir +export LD_LIBRARY_PATH="\${JVM_LD_LIBRARY_PATH:-}":\$this_dir \$this_dir/jazzer_driver \\ --agent_path=\$this_dir/jazzer_agent_deploy.jar \\ --cp=${RUNTIME_CP} \\ diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml index 0875e95cd..dcce873bf 100644 --- a/.github/workflows/clusterfuzzlite.yml +++ b/.github/workflows/clusterfuzzlite.yml @@ -39,6 +39,9 @@ jobs: language: jvm github-token: ${{ secrets.GITHUB_TOKEN }} sanitizer: address + # Keep all fuzz targets even if the PR only touches vendored copies + # in .clusterfuzzlite/ (not the original src/ files) + keep-unaffected-fuzz-targets: true - name: Run Fuzzers id: run From 8f986eb87d604d352f1050af7a9c8eff38655e1c Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 15:33:14 +0200 Subject: [PATCH 07/10] fix(ci): add debug output and disable bad-build-check for CFL diagnosis The build succeeds (all javac compilations pass) but the post-build validation fails with 'No fuzz targets found'. Adding ls/file/grep diagnostics to understand what looks like after compilation. Temporarily disabling bad-build-check to let the run step proceed. --- .clusterfuzzlite/build.sh | 6 ++++++ .github/workflows/clusterfuzzlite.yml | 1 + 2 files changed, 7 insertions(+) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 947ae74d8..f7cf185bb 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -143,3 +143,9 @@ done echo "=== ClusterFuzzLite build complete ===" echo "Fuzz targets: PathNavigatorFuzzer, MatchingUtilitiesFuzzer" +echo "--- $OUT contents ---" +ls -la $OUT/ +echo "--- file types ---" +file $OUT/PathNavigatorFuzzer $OUT/MatchingUtilitiesFuzzer 2>&1 || true +echo "--- wrapper content check ---" +grep -c jazzer_driver $OUT/PathNavigatorFuzzer || echo "jazzer_driver NOT found in wrapper" diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml index dcce873bf..d17693458 100644 --- a/.github/workflows/clusterfuzzlite.yml +++ b/.github/workflows/clusterfuzzlite.yml @@ -42,6 +42,7 @@ jobs: # Keep all fuzz targets even if the PR only touches vendored copies # in .clusterfuzzlite/ (not the original src/ files) keep-unaffected-fuzz-targets: true + bad-build-check: false - name: Run Fuzzers id: run From 533ae74567b2515b5ae6294e4b515c183e7baf15 Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 15:42:37 +0200 Subject: [PATCH 08/10] fix(ci): add missing language: jvm to CFL run_fuzzers steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: both run_fuzzers steps defaulted to language: c++ which looks for ELF binaries with LLVMFuzzerTestOneInput. JVM fuzz targets are shell wrapper scripts — only detected when language: jvm is set. Also re-enabled bad-build-check and removed debug diagnostics. --- .clusterfuzzlite/build.sh | 6 ------ .github/workflows/clusterfuzzlite.yml | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index f7cf185bb..947ae74d8 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -143,9 +143,3 @@ done echo "=== ClusterFuzzLite build complete ===" echo "Fuzz targets: PathNavigatorFuzzer, MatchingUtilitiesFuzzer" -echo "--- $OUT contents ---" -ls -la $OUT/ -echo "--- file types ---" -file $OUT/PathNavigatorFuzzer $OUT/MatchingUtilitiesFuzzer 2>&1 || true -echo "--- wrapper content check ---" -grep -c jazzer_driver $OUT/PathNavigatorFuzzer || echo "jazzer_driver NOT found in wrapper" diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml index d17693458..b35c2c0c7 100644 --- a/.github/workflows/clusterfuzzlite.yml +++ b/.github/workflows/clusterfuzzlite.yml @@ -42,12 +42,12 @@ jobs: # Keep all fuzz targets even if the PR only touches vendored copies # in .clusterfuzzlite/ (not the original src/ files) keep-unaffected-fuzz-targets: true - bad-build-check: false - name: Run Fuzzers id: run uses: google/clusterfuzzlite/actions/run_fuzzers@52ecc61cb587ee99c26825a112a21abf19c7448c # v1 with: + language: jvm github-token: ${{ secrets.GITHUB_TOKEN }} fuzz-seconds: 300 mode: code-change @@ -74,6 +74,7 @@ jobs: id: run uses: google/clusterfuzzlite/actions/run_fuzzers@52ecc61cb587ee99c26825a112a21abf19c7448c # v1 with: + language: jvm github-token: ${{ secrets.GITHUB_TOKEN }} fuzz-seconds: 1800 mode: batch From e08f53d10f369aac3294d4ae75bb1fd6743cf134 Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 15:51:14 +0200 Subject: [PATCH 09/10] fix(ci): add LLVMFuzzerTestOneInput marker to JVM wrapper scripts The bad-build-check scans wrapper scripts for the string 'LLVMFuzzerTestOneInput' to detect valid fuzz targets. Without it, JVM shell wrappers are invisible to the checker. Adapted from google/oss-fuzz/projects/json-sanitizer/build.sh: - Added '# LLVMFuzzerTestOneInput for fuzzer detection.' comment - Use LD_LIBRARY_PATH as command prefix (not separate export) - Add memory settings based on -runs flag - Re-enabled bad-build-check (was incorrectly disabled) --- .clusterfuzzlite/build.sh | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 947ae74d8..d769eb392 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -126,19 +126,24 @@ echo "✅ Fuzz targets compiled" RUNTIME_CP="\$this_dir/classes:\$this_dir" # ── Generate Jazzer wrapper scripts ── +# The "LLVMFuzzerTestOneInput" comment is required — bad_build_check scans +# wrapper scripts for this string to detect valid fuzz targets. for fuzzer in PathNavigatorFuzzer MatchingUtilitiesFuzzer; do - cat > $OUT/${fuzzer} << WRAPPER -#!/bin/bash -this_dir=\$(dirname "\$0") -export LD_LIBRARY_PATH="\${JVM_LD_LIBRARY_PATH:-}":\$this_dir -\$this_dir/jazzer_driver \\ - --agent_path=\$this_dir/jazzer_agent_deploy.jar \\ - --cp=${RUNTIME_CP} \\ - --target_class=${fuzzer} \\ - --jvm_args="-Xmx2048m" \\ - "\$@" -WRAPPER - chmod +x $OUT/${fuzzer} + echo "#!/bin/bash +# LLVMFuzzerTestOneInput for fuzzer detection. +this_dir=\$(dirname \"\$0\") +if [[ \"\$@\" =~ (^| )-runs=[0-9]+($| ) ]]; then + mem_settings='-Xmx1900m:-Xss900k' +else + mem_settings='-Xmx2048m:-Xss1024k' +fi +LD_LIBRARY_PATH=\"$JVM_LD_LIBRARY_PATH\":\$this_dir \\ +\$this_dir/jazzer_driver --agent_path=\$this_dir/jazzer_agent_deploy.jar \\ +--cp=$RUNTIME_CP \\ +--target_class=$fuzzer \\ +--jvm_args=\"\$mem_settings\" \\ +\$@" > $OUT/$fuzzer + chmod +x $OUT/$fuzzer done echo "=== ClusterFuzzLite build complete ===" From 7d455cf9d71895f005d68c1e5dcfa1aa3b2fc9ac Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Thu, 23 Apr 2026 16:05:25 +0200 Subject: [PATCH 10/10] fix(test): use description parameter in retryUntilOk assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The description parameter was unused — now passed to Hamcrest's describedAs matcher so test failures show which operation was being retried. Also replaced FQN with proper import. --- .../java/ai/labs/eddi/integration/BaseIntegrationIT.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java b/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java index d995d8963..36353b01f 100644 --- a/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java +++ b/src/test/java/ai/labs/eddi/integration/BaseIntegrationIT.java @@ -11,6 +11,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Supplier; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; @@ -268,7 +269,7 @@ protected void waitForConversationReady(String agentId, String conversationId) t * @param description * human-readable description for the assertion message */ - protected void retryUntilOk(java.util.function.Supplier call, String description) + protected void retryUntilOk(Supplier call, String description) throws InterruptedException { for (int i = 0; i < 10; i++) { Response response = call.get(); @@ -278,7 +279,9 @@ protected void retryUntilOk(java.util.function.Supplier call, String d Thread.sleep(200); } // Final attempt — will fail with assertion if still not OK - call.get().then().assertThat().statusCode(200); + call.get().then().assertThat() + .statusCode(describedAs(description + " (expected HTTP 200)", + equalTo(200))); } public record ResourceId(String id, int version) {