diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c34d8b3425..2cad565e82a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,11 +82,17 @@ jobs: # Maybe if codecov wasn't broken we wouldn't need to do this... ./codecov --verbose upload-process --disable-search --fail-on-error -f target/codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'tests' cargo clean - - name: Download honggfuzz corpus - uses: actions/download-artifact@v4 - with: - name: hfuzz-corpus - path: fuzz/hfuzz_workspace + - name: Clone fuzzing corpus + run: git clone --depth=1 https://github.com/lightningdevkit/ldk-fuzzing-corpus.git fuzz/ldk-fuzzing-corpus + - name: Symlink corpus into hfuzz_workspace + run: | + set -eu + cd fuzz + for D in ldk-fuzzing-corpus/rust-lightning/*/; do + NAME=$(basename "$D") + mkdir -p "hfuzz_workspace/${NAME}_target" + cp -r "ldk-fuzzing-corpus/rust-lightning/${NAME}" "hfuzz_workspace/${NAME}_target/input" + done - name: Run fuzz coverage generation run: | ./contrib/generate_fuzz_coverage.sh --output-dir `pwd` --output-codecov-json @@ -233,39 +239,59 @@ jobs: - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} - # This is read-only for PRs. It seeds the fuzzer for a more effective run. - # NOTE: The `key` is unique and will always miss, forcing a fallback to - # the `restore-keys` to find the latest global cache from the `main` branch. - - name: Restore persistent fuzz corpus (PR) - if: ${{ github.ref != 'refs/heads/main' }} - uses: actions/cache/restore@v4 - with: - path: fuzz/hfuzz_workspace - key: fuzz-corpus-${{ github.ref }}-${{ github.sha }} - restore-keys: | - fuzz-corpus-refs/heads/main- - # The `restore-keys` performs a prefix search to find the most recent - # cache from a previous `main` run. We then save with a new, unique - # `key` (using the SHA) to ensure the cache is always updated, - # as caches are immutable. - - name: Restore/Save persistent honggfuzz corpus (Main) - if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/cache@v4 - with: - path: fuzz/hfuzz_workspace - key: fuzz-corpus-refs/heads/main-${{ github.sha }} - restore-keys: | - fuzz-corpus-refs/heads/main- + - name: Clone fuzzing corpus + run: git clone --depth=1 https://github.com/lightningdevkit/ldk-fuzzing-corpus.git fuzz/ldk-fuzzing-corpus + - name: Symlink corpus into hfuzz_workspace + run: | + set -eu + cd fuzz + for D in ldk-fuzzing-corpus/rust-lightning/*/; do + NAME=$(basename "$D") + mkdir -p "hfuzz_workspace/${NAME}_target" + ln -sfn "../../ldk-fuzzing-corpus/rust-lightning/${NAME}" \ + "hfuzz_workspace/${NAME}_target/input" + done - name: Run fuzzers run: cd fuzz && ./ci-fuzz.sh && cd .. env: FUZZ_MINIMIZE: ${{ contains(github.event.pull_request.labels.*.name, 'fuzz-minimize') }} - - name: Upload honggfuzz corpus + - name: Stage new corpus entries for upload + if: success() || failure() + run: | + set -eu + WORKSPACE="$(pwd)" + rm -rf "$WORKSPACE/new-corpus" + mkdir -p "$WORKSPACE/new-corpus" + + cd fuzz/ldk-fuzzing-corpus + while IFS= read -r F; do + mkdir -p "$WORKSPACE/new-corpus/$(dirname "$F")" + cp -a "$F" "$WORKSPACE/new-corpus/$F" + done < <(git ls-files --others --exclude-standard rust-lightning/) + cd "$WORKSPACE" + + for D in fuzz/hfuzz_workspace/*_target/; do + [ -d "$D" ] || continue + BASE=$(basename "$D") + NAME="${BASE%_target}" + [ -d "$WORKSPACE/new-corpus/$NAME" ] || continue + for F in "$D"/SIG*; do + FILE="$(basename "$F")" + [ -f "$F" -a ! -f "$WORKSPACE/new-corpus/$NAME/$FILE" ] && + cp "$F" "$WORKSPACE/new-corpus/$NAME/$FILE" + done + done + + NEW=$(find new-corpus -type f 2>/dev/null | wc -l) + echo "Staged $NEW new corpus entries (including any SIG* crashes)" + - name: Upload new corpus entries + if: success() || failure() uses: actions/upload-artifact@v4 with: name: hfuzz-corpus - path: fuzz/hfuzz_workspace + path: new-corpus compression-level: 0 + if-no-files-found: ignore linting: runs-on: ubuntu-latest diff --git a/.github/workflows/push-fuzz-corpus.yml b/.github/workflows/push-fuzz-corpus.yml new file mode 100644 index 00000000000..4551de19cc5 --- /dev/null +++ b/.github/workflows/push-fuzz-corpus.yml @@ -0,0 +1,92 @@ +name: Push fuzz corpus + +# Triggered after the main CI workflow finishes. Because `workflow_run` always +# runs in the *base* repo's context (its workflow file as of `main`, with +# full secrets access) it's safe to handle the corpus push here even for fork +# PRs — none of the PR's modified code or scripts execute in this job. +# +# Caveat: GitHub only fires `workflow_run` for workflow files that live on +# the default branch, so this workflow does nothing until it's merged to +# `master`. +on: + # zizmor flags `workflow_run` as a dangerous trigger because it runs with + # repo secrets in base-branch context. That's exactly why we use it here: + # this workflow never touches any PR-supplied code (no checkout, no script + # execution from the artifact — just cp/git on opaque corpus blobs), so + # the warning is a false positive. + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: ["Continuous Integration Checks"] + types: [completed] + +permissions: + # download-artifact across runs requires `actions: read`. + actions: read + +jobs: + push-corpus: + # Run on either success or fuzzer crash; skip on cancellation. + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + runs-on: ubuntu-latest + steps: + - name: Download fuzz corpus artifact + id: download + # The artifact only exists when the fuzz job got far enough to upload + # it. Don't fail this workflow if the upload was skipped. + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: hfuzz-corpus + path: hfuzz-corpus + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Clone fuzzing corpus + if: steps.download.outcome == 'success' + run: git clone --depth=1 https://github.com/lightningdevkit/ldk-fuzzing-corpus.git + + - name: Copy new corpus entries into the corpus checkout + if: steps.download.outcome == 'success' + run: | + set -eu + if [ -d hfuzz-corpus/rust-lightning ]; then + cp -rn hfuzz-corpus/rust-lightning/. ldk-fuzzing-corpus/rust-lightning/ + fi + + - name: Open PR with new corpus entries + if: steps.download.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.CORPUS_PUSH_TOKEN }} + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + RUN_ID: ${{ github.event.workflow_run.id }} + run: | + set -eu + cd ldk-fuzzing-corpus + if [ -z "$(git status --porcelain)" ]; then + echo "No new corpus entries to contribute." + exit 0 + fi + if [ -z "${GH_TOKEN:-}" ]; then + echo "Found new corpus entries but CORPUS_PUSH_TOKEN is unset; skipping PR." + git status --short + exit 0 + fi + BRANCH="ci/new-corpus-${RUN_ID}" + git config user.email "ldk-ci@users.noreply.github.com" + git config user.name "LDK CI" + git checkout -b "$BRANCH" + git add rust-lightning + git commit \ + -m "Add corpus entries from rust-lightning CI" \ + -m "Source commit: ${SOURCE_SHA}" \ + -m "Run: ${RUN_URL}" + REMOTE=$(git config --get remote.origin.url) + PUSH_URL="https://x-access-token:${GH_TOKEN}@${REMOTE#https://}" + git push "$PUSH_URL" "HEAD:$BRANCH" + gh pr create \ + --title "New corpus entries from rust-lightning CI run ${RUN_ID}" \ + --body "Discovered while running fuzz CI against \`${SOURCE_SHA}\`. Source: ${RUN_URL}" \ + --head "$BRANCH" \ + --base master