alice-analytics 0.5.0

High-performance telemetry & probabilistic statistical estimation — HyperLogLog++, DDSketch, Count-Min Sketch, local differential privacy, streaming anomaly detection
Documentation
name: Fuzz

# Runs each fuzz target for a bounded time: telemetry values chosen by an
# attacker (NaN / ±Inf / subnormal / extreme counts) must not panic or exhaust
# memory. cargo-fuzz needs a nightly toolchain.

on:
  push:
    # ci/**: the same checks on a branch before it is fast-forwarded to main
    branches: [main, 'ci/**']
    paths:
      - 'src/**'
      - 'fuzz/**'
      - '.github/workflows/fuzz.yml'
  pull_request:
    branches: [main]
    paths:
      - 'src/**'
      - 'fuzz/**'
      - '.github/workflows/fuzz.yml'
  schedule:
    # every day 03:00 UTC
    - cron: '0 3 * * *'
  workflow_dispatch:
    inputs:
      duration_seconds:
        description: 'Per-target fuzz duration (seconds)'
        required: false
        default: '60'
        type: string

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always

jobs:
  fuzz:
    name: Fuzz — ${{ matrix.target }}
    runs-on: ubuntu-latest
    # build + a 60 s run normally take under 10 min
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        target:
          - fuzz_event_parse
          - fuzz_metric_aggregate
          - fuzz_sketch_insert
    steps:
      - uses: actions/checkout@v5

      - name: Install nightly toolchain
        uses: dtolnay/rust-toolchain@nightly

      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: fuzz

      - name: Install cargo-fuzz
        # built from source: the prebuilt cargo-fuzz binary targets musl by
        # default, which the sanitizer build cannot use
        run: cargo install cargo-fuzz --locked

      # the build blocks: a target that does not compile must not pass
      - name: Build fuzz target
        run: |
          cd fuzz
          cargo +nightly fuzz build "${{ matrix.target }}"

      - name: Set fuzz duration
        id: dur
        run: |
          DURATION="${{ github.event.inputs.duration_seconds }}"
          if [ -z "$DURATION" ]; then
            DURATION=60
          fi
          echo "seconds=$DURATION" >> "$GITHUB_OUTPUT"

      # the run is informational: a crash is uploaded for triage, not blocking
      # A crash fails the job (it is a defect, not informational). Each
      # target must also execute at least one input: a run that never executed
      # anything is not a pass. fuzz/regressions/<target> (committed crash
      # inputs) is replayed as an extra corpus, so a fixed crash stays fixed.
      # The summary records runs and libFuzzer's coverage
      - name: Run fuzz target (time-boxed)
        shell: bash
        env:
          CARGO_TERM_COLOR: never
        run: |
          cd fuzz
          T="${{ matrix.target }}"
          extra=()
          if [ -d "regressions/$T" ]; then extra=("regressions/$T"); fi
          mkdir -p "corpus/$T"
          set +e
          cargo +nightly fuzz run "$T" "corpus/$T" "${extra[@]}" -- \
            -max_total_time=${{ steps.dur.outputs.seconds }} 2>&1 | tee fuzz-run.log
          rc=${PIPESTATUS[0]}
          set -e
          runs=$(grep -oE "Done [0-9]+ runs" fuzz-run.log | tail -1 | grep -oE "[0-9]+" || true)
          cov=$(grep -oE "DONE +cov: [0-9]+" fuzz-run.log | tail -1 | grep -oE "[0-9]+$" || true)
          status=ok
          if [ "$rc" -ne 0 ]; then status="crashed (exit $rc)"; fi
          # the row is written before failing, so a crash still shows up here
          echo "| $T | ${runs:-0} runs | cov ${cov:-?} | $status |" >> "$GITHUB_STEP_SUMMARY"
          if [ "$rc" -ne 0 ]; then exit "$rc"; fi
          if [ "${runs:-0}" -le 0 ]; then
            echo "::error::fuzz target $T executed 0 inputs"
            exit 1
          fi

      - name: Upload crash artifacts
        if: failure()
        uses: actions/upload-artifact@v6
        with:
          name: fuzz-crash-${{ matrix.target }}
          path: fuzz/artifacts/${{ matrix.target }}
          if-no-files-found: ignore
          retention-days: 14