diff --git a/.github/workflows/cflite.yml b/.github/workflows/cflite.yml index e634d78..96527f6 100644 --- a/.github/workflows/cflite.yml +++ b/.github/workflows/cflite.yml @@ -1,12 +1,12 @@ -# ClusterFuzzLite — continuous fuzzing of browser-bridge's trust boundary: -# the CDP proxy's pure request guards (token extraction, the DNS-rebinding -# Host gate, secret stripping before anything is forwarded to Chromium) and -# the UA pool fed by client-controlled ?session= ids. Runs weekly + on -# demand; a discovered crash fails the job and is uploaded as an artifact. -# Fuzz targets live in ./fuzz, built by .clusterfuzzlite/build.sh (Jazzer.js). -# OpenSSF Scorecard credits the Fuzzing check from the .clusterfuzzlite/ -# config. -name: ClusterFuzzLite +# ClusterFuzzLite COVERAGE REPORT of the fuzz targets in ./fuzz. This action version cannot fuzz +# JavaScript: the OSS-Fuzz builder rejects every sanitizer for JS ("JavaScript projects cannot be +# fuzzed with sanitizers"), the action's config rejects `none`, and `coverage` selects its +# coverage-report runner, not the fuzzer (oss-fuzz infra/cifuzz/config_utils.py forces +# mode=coverage for it). Proven 2026-09-26 on plumbline runs 36204398437 (address) and +# 36204619235 (none). Every run of this workflow builds the targets, replays the corpus for a +# few seconds and uploads a coverage report; the fuzzing itself is fuzz.yml (Jazzer.js). +# The .clusterfuzzlite/ config stays: OpenSSF Scorecard credits the Fuzzing check from it. +name: ClusterFuzzLite coverage on: schedule: @@ -17,15 +17,14 @@ permissions: read-all jobs: Fuzzing: + name: Coverage report (${{ matrix.sanitizer }}) runs-on: ubuntu-latest timeout-minutes: 20 strategy: fail-fast: false matrix: - # JavaScript (Jazzer.js) has no native sanitizer — OSS-Fuzz rejects - # address/memory/undefined for JS ("cannot be fuzzed with sanitizers"), - # and the action's config rejects `none`. `coverage` is the value real - # JS ClusterFuzzLite projects use with this action version. + # `coverage` is the only value this action accepts for JavaScript that builds at all, and it + # means: report, not fuzz (see the header). The fuzzing runs in fuzz.yml. sanitizer: [coverage] steps: - name: Build fuzzers (${{ matrix.sanitizer }}) diff --git a/.github/workflows/fleet-status.yml b/.github/workflows/fleet-status.yml index 20e5448..74ca784 100644 --- a/.github/workflows/fleet-status.yml +++ b/.github/workflows/fleet-status.yml @@ -15,7 +15,7 @@ on: issue_comment: types: [created, edited, deleted] workflow_run: - workflows: [actionlint, build, CodeQL, 'fieldpass ci', hygiene, labels, stealth-score, 'PR triage', Redline] + workflows: [actionlint, build, CodeQL, 'fieldpass ci', hygiene, labels, stealth-score, 'PR triage', Redline, Fuzz] types: [completed] permissions: {} diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..1c0c187 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,80 @@ +# Fuzzing of browser-bridge's trust boundary with Jazzer.js (libFuzzer): every target in ./fuzz runs +# for FUZZ_SECONDS against a corpus that persists between runs through the Actions cache. A +# crash, an uncaught throw or a timeout fails the job and the reproducing input is uploaded as +# an artifact. This workflow is the fuzzer; cflite.yml is only the coverage report, because +# ClusterFuzzLite's action cannot fuzz JavaScript at all: it rejects every sanitizer for JS, +# rejects `none`, and `coverage` selects its report runner (proven 2026-09-26 on plumbline runs +# 36204398437 and 36204619235). Weekly, on demand, and on pull requests that touch the targets +# or the code under them, with a short budget there. +name: Fuzz + +on: + schedule: + - cron: '57 6 * * 1' # weekly, 06:57 UTC, after the coverage report's slot + workflow_dispatch: + inputs: + seconds: + description: 'Seconds per target' + required: false + default: '300' + pull_request: + paths: + - 'fuzz/**' + - 'cdp-proxy.mjs' + - 'ua.mjs' + - 'package.json' + - 'package-lock.json' + - '.github/workflows/fuzz.yml' + +permissions: + contents: read + +concurrency: + group: fuzz-${{ github.ref }} + cancel-in-progress: true + +jobs: + fuzz: + name: Fuzz (Jazzer.js) + # Never from a fork: the schedule is inherited by every fork and means nothing there. + if: github.repository == 'askalf/browser-bridge' + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + - name: Install dependencies + run: npm ci --no-audit --no-fund + # The newest saved corpus, whichever run saved it; the save below writes a fresh key each + # run so the restore-keys prefix always finds the latest. + - name: Restore the corpus + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: fuzz/corpus + key: fuzz-corpus-${{ github.run_id }} + restore-keys: fuzz-corpus- + - name: Fuzz every target + env: + FUZZ_SECONDS: ${{ github.event_name == 'pull_request' && '60' || inputs.seconds || '300' }} + FUZZ_CORPUS_DIR: fuzz/corpus + FUZZ_ARTIFACT_DIR: fuzz/crashes + run: node fuzz/run.mjs + - name: Upload the crashing input + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: fuzz-crashes-${{ github.run_id }} + path: fuzz/crashes + if-no-files-found: ignore + # Only runs on the default branch feed the shared corpus; a pull request's cache is scoped + # to its branch anyway. + - name: Save the corpus + if: always() && github.event_name != 'pull_request' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: fuzz/corpus + key: fuzz-corpus-${{ github.run_id }} diff --git a/.gitignore b/.gitignore index 201129f..61901a9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules/ .DS_Store .env .env.* +# fuzz.yml / fuzz/run.mjs working dirs +fuzz/corpus/ +fuzz/crashes/ diff --git a/fuzz/run.mjs b/fuzz/run.mjs index e41cf75..4932c30 100644 --- a/fuzz/run.mjs +++ b/fuzz/run.mjs @@ -1,27 +1,41 @@ -// `npm run fuzz` — run every Jazzer.js target in ./fuzz for a short burst. -// Continuous fuzzing is done in CI by ClusterFuzzLite (.github/workflows/ -// cflite.yml); this is the fast local repro loop. Targets import the runtime -// .mjs modules directly (no build step). Override the per-target budget with -// FUZZ_SECONDS (default 30). +// `node fuzz/run.mjs` (also `npm run fuzz`) runs every Jazzer.js target in ./fuzz. This is the +// fuzzer CI runs (.github/workflows/fuzz.yml) and the local repro loop. Environment: +// FUZZ_SECONDS per-target budget in seconds (default 30) +// FUZZ_CORPUS_DIR root of per-target corpus dirs, created on demand; libFuzzer reads its +// seeds from