diff --git a/.github/workflows/ci-failure-issues.yml b/.github/workflows/ci-failure-issues.yml new file mode 100644 index 00000000..d3c8312c --- /dev/null +++ b/.github/workflows/ci-failure-issues.yml @@ -0,0 +1,92 @@ +name: CI Failure Issues + +on: + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: + - Fuzz + types: + - completed + branches: + - master + +permissions: + issues: write + +concurrency: + group: ci-failure-issues-${{ github.event.workflow_run.name }} + cancel-in-progress: false + +jobs: + sync-issue: + if: ${{ contains(fromJson('["failure","success"]'), github.event.workflow_run.conclusion) }} + runs-on: ubuntu-24.04 + + steps: + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const run = context.payload.workflow_run; + const workflow = run.name; + const marker = ``; + const workflowFiles = { + "Fuzz": "cron-daily-fuzz.yml", + }; + + const formatTs = iso => { + const d = new Date(iso); + const pad = n => String(n).padStart(2, "0"); + return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} at ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} UTC`; + }; + const shortSha = sha => (sha || "unknown").slice(0, 7); + const commitUrl = sha => `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${sha}`; + const workflowUrl = workflowFiles[workflow] + ? `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/workflows/${workflowFiles[workflow]}` + : `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions?query=${encodeURIComponent(`workflow:"${workflow}"`)}`; + + function titleFor() { + return `The ${workflow} workflow is failing`; + } + + function bodyForFailure() { + return [ + marker, + "", + `The [${workflow} workflow](${workflowUrl}) started failing on ${formatTs(run.created_at)}: [${workflow} #${run.run_number}](${run.html_url}) - [\`${shortSha(run.head_sha)}\`](${commitUrl(run.head_sha)})`, + ].join("\n"); + } + + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + per_page: 100, + }); + + const existing = issues.find(issue => + !issue.pull_request && issue.body && issue.body.includes(marker) + ); + + if (run.conclusion === "failure" && !existing) { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: titleFor(), + body: bodyForFailure(), + }); + } + + if (run.conclusion === "success" && existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body: `The [${workflow} workflow](${workflowUrl}) successfully ran again on ${formatTs(run.created_at)}: [${workflow} #${run.run_number}](${run.html_url}).`, + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + state: "closed", + }); + } diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml new file mode 100644 index 00000000..42b41f03 --- /dev/null +++ b/.github/workflows/cron-daily-fuzz.yml @@ -0,0 +1,71 @@ +# Automatically generated by fuzz/generate-files.sh +name: Fuzz +on: + schedule: + # 5am every day UTC, this correlates to: + # - 10pm PDT + # - 6am CET + # - 4pm AEDT + - cron: '00 05 * * *' +permissions: {} + +jobs: + fuzz: + if: ${{ !github.event.act }} + runs-on: ubuntu-24.04 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + fuzz_target: [ + compile_parse_tree, + compile_text, + display_parse_tree, + parse_value_rtt, + parse_witness_json_rtt, + reconstruct_value, + ] + steps: + - name: Install test dependencies + run: sudo apt-get update -y && sudo apt-get install -y binutils-dev libunwind8-dev libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc libiberty-dev + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + id: cache-fuzz + with: + path: | + ~/.cargo/bin + fuzz/target + target + key: cache-${{ matrix.fuzz_target }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + - uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable + with: + toolchain: '1.74.0' + - name: fuzz + run: | + echo "Using RUSTFLAGS $RUSTFLAGS" + cd fuzz && ./fuzz.sh "${{ matrix.fuzz_target }}" + - run: echo "${{ matrix.fuzz_target }}" >executed_${{ matrix.fuzz_target }} + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: executed_${{ matrix.fuzz_target }} + path: executed_${{ matrix.fuzz_target }} + + verify-execution: + if: ${{ !github.event.act }} + needs: fuzz + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - run: cargo install --locked --version 0.12.0 cargo-fuzz + - name: Display structure of downloaded files + run: ls -R + - run: find executed_* -type f -exec cat {} + | sort > executed + - run: cargo fuzz list | sort | diff - executed diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 511a033f..afb7c543 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" publish = false edition = "2021" rust-version = "1.79.0" +authors = ["Generated by fuzz/generate-files.sh"] [package.metadata] cargo-fuzz = true @@ -26,15 +27,15 @@ base64 = "0.22.1" unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)'] } [[bin]] -name = "compile_text" -path = "fuzz_targets/compile_text.rs" +name = "compile_parse_tree" +path = "fuzz_targets/compile_parse_tree.rs" test = false doc = false bench = false [[bin]] -name = "compile_parse_tree" -path = "fuzz_targets/compile_parse_tree.rs" +name = "compile_text" +path = "fuzz_targets/compile_text.rs" test = false doc = false bench = false diff --git a/fuzz/fuzz-util.sh b/fuzz/fuzz-util.sh new file mode 100755 index 00000000..d3e5f877 --- /dev/null +++ b/fuzz/fuzz-util.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Sort order is affected by locale. See `man sort`. +# > Set LC_ALL=C to get the traditional sort order that uses native byte values. +export LC_ALL=C + +REPO_DIR=$(git rev-parse --show-toplevel) + +listTargetFiles() { + pushd "$REPO_DIR/fuzz" >/dev/null || exit 1 + find fuzz_targets/ -type f -name "*.rs" | sort + popd >/dev/null || exit 1 +} + +targetFileToName() { + echo "$1" | + sed 's/^fuzz_targets\///' | + sed 's/\.rs$//' | + sed 's/\//_/g' | + sed 's/^_//g' +} + +# Utility function to avoid CI failures on Windows +checkWindowsFiles() { + incorrectFilenames=$(find . -type f -name "*,*" -o -name "*:*" -o -name "*<*" -o -name "*>*" -o -name "*|*" -o -name "*\?*" -o -name "*\**" -o -name "*\"*" | wc -l) + if [ "$incorrectFilenames" -gt 0 ]; then + echo "Bailing early because there is a Windows-incompatible filename in the tree." + exit 2 + fi +} + +# Checks whether a fuzz case has artifacts, and dumps them in hex +checkReport() { + artifactDir="fuzz/artifacts/$1" + if [ -d "$artifactDir" ] && [ -n "$(ls -A "$artifactDir" 2>/dev/null)" ]; then + echo "Artifacts found for target: $1" + for artifact in "$artifactDir"/*; do + if [ -f "$artifact" ]; then + echo "Artifact: $(basename "$artifact")" + xxd -p -c10000 <"$artifact" + fi + done + exit 1 + fi +} diff --git a/fuzz/fuzz.sh b/fuzz/fuzz.sh new file mode 100755 index 00000000..3a4aa602 --- /dev/null +++ b/fuzz/fuzz.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# This script is used to briefly fuzz every target when no target is provided. Otherwise, it will briefly fuzz the +# provided target + +set -euox pipefail + +REPO_DIR=$(git rev-parse --show-toplevel) + +# can't find the file because of the ENV var +# shellcheck source=/dev/null +source "$REPO_DIR/fuzz/fuzz-util.sh" + +target= +max_total_time=100 + +for arg in "$@"; do + case "$arg" in + -max_total_time=*) + max_total_time="${arg#-max_total_time=}" + ;; + -*) + echo "Unknown option: $arg" + exit 2 + ;; + *) + if [ -n "$target" ]; then + echo "Unexpected argument: $arg" + exit 2 + fi + target="$arg" + ;; + esac +done + +case "$max_total_time" in + ''|*[!0-9]*) + echo "-max_total_time must be a non-negative integer number of seconds" + exit 2 + ;; +esac + +# Check that input files are correct Windows file names +checkWindowsFiles + +if [ -z "$target" ]; then + targetFiles="$(listTargetFiles)" +else + targetFiles=fuzz_targets/"$target".rs +fi + +cargo --version +rustc --version + +# Testing +cargo install --force --locked --version 0.12.0 cargo-fuzz +for targetFile in $targetFiles; do + targetName=$(targetFileToName "$targetFile") + echo "Fuzzing target $targetName ($targetFile) for $max_total_time seconds" + # cargo-fuzz will check for the corpus at fuzz/corpus/ + cargo +nightly fuzz run "$targetName" -- -max_total_time="$max_total_time" + checkReport "$targetName" +done diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh new file mode 100755 index 00000000..e1554b2f --- /dev/null +++ b/fuzz/generate-files.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_DIR=$(git rev-parse --show-toplevel) + +# can't find the file because of the ENV var +# shellcheck source=/dev/null +source "$REPO_DIR/fuzz/fuzz-util.sh" + +# 1. Generate fuzz/Cargo.toml +cat > "$REPO_DIR/fuzz/Cargo.toml" <> "$REPO_DIR/fuzz/Cargo.toml" < "$REPO_DIR/.github/workflows/cron-daily-fuzz.yml" <executed_\${{ matrix.fuzz_target }} + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: executed_\${{ matrix.fuzz_target }} + path: executed_\${{ matrix.fuzz_target }} + + verify-execution: + if: \${{ !github.event.act }} + needs: fuzz + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - run: cargo install --locked --version 0.12.0 cargo-fuzz + - name: Display structure of downloaded files + run: ls -R + - run: find executed_* -type f -exec cat {} + | sort > executed + - run: cargo fuzz list | sort | diff - executed +EOF +