fix(replication): bound RESP client parser against unbounded allocation #3651
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Run AFL++ fuzzing on PRs that touch C++ code. | |
| # | |
| # For each PR, an LLM analyzes the diff and generates: | |
| # 1. Targeted seed files — initial inputs crafted to exercise the changed code paths. | |
| # (A "seed" is a RESP-encoded sequence of Redis commands that the fuzzer starts from | |
| # and mutates; see fuzz/seeds/resp/*.resp for the existing seed corpus.) | |
| # 2. Focus command list — commands the mutator should prefer (~70% of the time), | |
| # so mutations concentrate on the affected code instead of spreading randomly. | |
| # | |
| # The fuzzer then runs for 15 minutes in "smoke" mode (stop on first crash). | |
| # Seeds are generated with OpenAI gpt-4.1 by default. When OPENAI_API_KEY is unavailable | |
| # (e.g. fork PRs), seed generation is skipped and the fuzzer uses the existing seed corpus. | |
| # | |
| # Additionally, if the PR touches memcache-related code (memcache_parser, mc_family, | |
| # fuzz/memcache_mutator.py, or fuzz/seeds/memcache/), a focused memcache fuzzing step | |
| # runs automatically after RESP fuzzing passes, reusing the already-built binary. | |
| name: AFL++ PR Fuzzing | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'src/**/*.cc' | |
| - 'src/**/*.h' | |
| - 'helio/**/*.cc' | |
| - 'helio/**/*.h' | |
| - 'fuzz/**' | |
| - '.github/workflows/fuzz-pr.yml' | |
| - '.github/actions/fuzzing/**' | |
| workflow_dispatch: | |
| inputs: | |
| duration: | |
| description: 'Fuzzing duration in minutes' | |
| required: false | |
| default: '15' | |
| type: string | |
| memcache-duration: | |
| description: 'Memcache fuzzing duration in minutes' | |
| required: false | |
| default: '10' | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| fuzz-pr: | |
| runs-on: CI-LARGE-86 | |
| timeout-minutes: 60 | |
| container: | |
| image: ghcr.io/romange/ubuntu-dev:24-afl | |
| options: --security-opt seccomp=unconfined --sysctl "net.ipv6.conf.all.disable_ipv6=0" | |
| credentials: | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| submodules: true | |
| fetch-depth: 0 | |
| - name: Generate PR diff | |
| id: diff | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| BASE=${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA=${{ github.event.pull_request.head.sha }} | |
| MERGE_BASE=$(git merge-base "$BASE" "$HEAD_SHA") | |
| git diff "$MERGE_BASE".."$HEAD_SHA" > /tmp/pr_diff.txt | |
| else | |
| echo "" > /tmp/pr_diff.txt | |
| fi | |
| DIFF_LINES=$(wc -l < /tmp/pr_diff.txt) | |
| echo "diff_lines=${DIFF_LINES}" >> "$GITHUB_OUTPUT" | |
| echo "::group::PR diff summary" | |
| echo "C++ diff lines: ${DIFF_LINES}" | |
| if [ "$DIFF_LINES" -gt 0 ]; then | |
| echo "Changed files:" | |
| grep '^diff --git' /tmp/pr_diff.txt | sed 's|diff --git a/.* b/| |' || true | |
| else | |
| echo "No C++ file changes in this PR — seed generation will be skipped" | |
| fi | |
| echo "::endgroup::" | |
| - name: Generate targeted seeds | |
| id: seeds | |
| run: | | |
| SEEDS_DIR="${GITHUB_WORKSPACE}/fuzz/seeds/pr_targeted" | |
| mkdir -p "$SEEDS_DIR" | |
| python3 fuzz/generate_targeted_seeds.py \ | |
| --provider openai \ | |
| --output-dir "$SEEDS_DIR" \ | |
| < /tmp/pr_diff.txt | |
| FOCUS="" | |
| if [ -f "$SEEDS_DIR/focus_commands.json" ]; then | |
| FOCUS=$(cat "$SEEDS_DIR/focus_commands.json") | |
| fi | |
| echo "focus_commands=${FOCUS}" >> "$GITHUB_OUTPUT" | |
| echo "seeds_dir=${SEEDS_DIR}" >> "$GITHUB_OUTPUT" | |
| SEED_COUNT=$(ls "$SEEDS_DIR"/*.resp 2>/dev/null | wc -l || echo 0) | |
| echo "::group::Seed generation results" | |
| echo "Seeds generated: ${SEED_COUNT}" | |
| echo "Focus commands: ${FOCUS:-none}" | |
| if [ "$SEED_COUNT" -gt 0 ]; then | |
| ls -la "$SEEDS_DIR"/*.resp | |
| fi | |
| echo "::endgroup::" | |
| # Job summary | |
| { | |
| echo "### Fuzzing Seed Generation" | |
| echo "" | |
| if [ "$SEED_COUNT" -gt 0 ]; then | |
| echo "- **Seeds generated:** ${SEED_COUNT}" | |
| echo "- **Focus commands:** \`${FOCUS}\`" | |
| elif [ "$(wc -l < /tmp/pr_diff.txt)" -eq 0 ]; then | |
| echo "- No C++ changes in PR — using default seed corpus" | |
| elif [ -z "$OPENAI_API_KEY" ]; then | |
| echo "- No API key — using default seed corpus" | |
| else | |
| echo "- LLM did not produce usable seeds — using default seed corpus" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| - name: Run AFL++ PR fuzzing | |
| uses: ./.github/actions/fuzzing | |
| with: | |
| mode: smoke | |
| duration-minutes: ${{ github.event.inputs.duration || '15' }} | |
| run-number: ${{ github.run_number }} | |
| extra-seeds-dir: ${{ steps.seeds.outputs.seeds_dir }} | |
| focus-commands: ${{ steps.seeds.outputs.focus_commands }} | |
| # Reuses the binary built by the RESP step above (build: false). | |
| # Only runs when RESP fuzzing passed (default success() condition) and memcache | |
| # code was actually touched in this PR. | |
| - name: Check if memcache-related files changed | |
| id: memcache-check | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| CHANGED=$(grep -E '^diff --git a/(src/(facade/memcache|server/mc_family)|fuzz/(memcache_mutator|seeds/memcache))' /tmp/pr_diff.txt || true) | |
| if [ -n "$CHANGED" ]; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| echo "Memcache-related files changed — will run memcache fuzzing:" | |
| echo "$CHANGED" | sed 's|diff --git a/.* b/| |' | |
| else | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| echo "No memcache-related files changed — skipping memcache fuzzing" | |
| fi | |
| else | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| echo "Manual trigger — running memcache fuzzing" | |
| fi | |
| - name: Run AFL++ memcache fuzzing | |
| if: success() && steps.memcache-check.outputs.run == 'true' | |
| uses: ./.github/actions/fuzzing | |
| with: | |
| mode: smoke | |
| target: memcache | |
| build: 'false' | |
| duration-minutes: ${{ github.event.inputs['memcache-duration'] || '10' }} | |
| run-number: ${{ github.run_number }} |