Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/commands/fuzz-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if [ ! -d "fuzz" ]; then
echo "Run Sprint 5.7 setup to initialize fuzzing."
echo ""
echo "Quick setup:"
echo " cargo install cargo-fuzz"
echo " cargo install cargo-fuzz --version 0.13.2 --locked"
echo " cargo +nightly fuzz init"
exit 1
fi
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
fetch-depth: 0 # Full history for baseline comparison

- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@v2
with:
cache: true

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
uses: actions/checkout@v7

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: 'rust'
# Note: CodeQL Rust extractor has known limitations:
Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:
# These do not indicate code issues (verified by cargo check/clippy) and do not impact security coverage.

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
# Analysis uploads results to GitHub Security tab.
# Coverage: ~97% of Rust files successfully extracted (excellent for Rust projects)
# Unparsed files: Test code only (assertions, utilities), no production logic affected
80 changes: 75 additions & 5 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ on:

env:
RUST_BACKTRACE: 1
# cargo-fuzz defaults `--target` to the triple its own binary was built
# for. The prebuilt release binary installed below is a static musl build,
# and the address sanitizer cannot link against static musl libc, so the
# target is named explicitly on every build and run.
FUZZ_TARGET_TRIPLE: x86_64-unknown-linux-gnu
CARGO_TERM_COLOR: always

jobs:
Expand Down Expand Up @@ -64,29 +69,79 @@ jobs:
${{ runner.os }}-fuzz-target-${{ matrix.target }}-
${{ runner.os }}-fuzz-target-

# Install the prebuilt release binary rather than compiling cargo-fuzz.
# `cargo install cargo-fuzz --version 0.13.1` (no --locked) resolved a
# fresh dependency graph on every run and compiled it with the nightly
# toolchain selected above. From 2026-09 that picked up a `rustix` whose
# nightly-only build path uses `rustc_*` attributes current nightly
# rejects ("attributes starting with `rustc` are reserved"), so every
# scheduled run died in this step before fuzzing anything. A prebuilt
# binary cannot be broken by a nightly compiler change. The version is
# pinned so a cargo-fuzz release is adopted deliberately.
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --version 0.13.1
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz@0.13.2

- name: Validate duration input
env:
DURATION_INPUT: ${{ github.event.inputs.duration || '600' }}
run: |
# Zero is rejected too: libFuzzer reads -max_total_time=0 as "no
# limit", so the run would only end at the outer timeout backstop.
case "$DURATION_INPUT" in
''|*[!0-9]*|0|0*)
echo "::error::duration must be a positive whole number of seconds without leading zeros, got: $DURATION_INPUT"
exit 1 ;;
esac

- name: Build fuzz target
run: cargo +nightly fuzz build ${{ matrix.target }}
run: cargo +nightly fuzz build --target "$FUZZ_TARGET_TRIPLE" ${{ matrix.target }}

- name: Run fuzzer
id: fuzz_run
# The dispatch input reaches the script through the environment, not
# by template expansion into the script text, so it cannot inject
# shell. Its format is checked by "Validate duration input" above,
# which (unlike this step) is not continue-on-error.
env:
DURATION_INPUT: ${{ github.event.inputs.duration || '600' }}
run: |
DURATION="${{ github.event.inputs.duration || '600' }}"
DURATION="$DURATION_INPUT"
echo "Running ${{ matrix.target }} for ${DURATION} seconds..."

# libFuzzer refuses to start when the -artifact_prefix directory is
# missing ("The required directory ... does not exist"). Before this
# mkdir the fuzzer exited 1 at once on every run, and the job still
# went green -- see "Fail job if the fuzzer did not run" below.
mkdir -p /tmp/fuzzing-artifacts

# Run fuzzer and capture output
set +e # Don't fail on fuzzer crashes (we want to capture them)
timeout ${DURATION}s cargo +nightly fuzz run ${{ matrix.target }} -- \
# pipefail: without it $? is tee's status, always 0, so the fuzzer's
# own exit code was never observed. The outer timeout is a backstop
# with headroom over -max_total_time, so it cannot cut a run that
# libFuzzer is about to finish on its own.
set -o pipefail
timeout "$((DURATION + 300))s" cargo +nightly fuzz run --target "$FUZZ_TARGET_TRIPLE" ${{ matrix.target }} -- \
-max_total_time=${DURATION} \
-print_final_stats=1 \
-print_corpus_stats=1 \
-artifact_prefix=/tmp/fuzzing-artifacts/ \
-verbosity=1 \
2>&1 | tee /tmp/fuzz_output.txt
EXIT_CODE=$?
set +o pipefail
set -e
echo "fuzzer_exit=${EXIT_CODE}" >> "$GITHUB_OUTPUT"

# -print_final_stats=1 prints these only when libFuzzer ran to
# completion (or crashed). Their absence means it never fuzzed.
if grep -q "stat::number_of_executed_units" /tmp/fuzz_output.txt; then
echo "fuzzer_ran=true" >> "$GITHUB_OUTPUT"
else
echo "fuzzer_ran=false" >> "$GITHUB_OUTPUT"
fi

# Parse fuzzing statistics
echo "## Fuzzing Statistics for ${{ matrix.target }}" > /tmp/fuzz_stats.txt
Expand Down Expand Up @@ -155,6 +210,19 @@ jobs:
echo "::error::Fuzzing discovered crashes in ${{ matrix.target }}"
exit 1

# "Run fuzzer" is continue-on-error so that a crash still reaches the
# artifact upload above, which also meant a fuzzer that never started
# passed silently. A clean run must have printed its final stats and
# exited 0; anything else that is not a recorded crash fails here.
- name: Fail job if the fuzzer did not run
if: steps.fuzz_run.outputs.crash_found != 'true' && (steps.fuzz_run.outputs.fuzzer_ran != 'true' || steps.fuzz_run.outputs.fuzzer_exit != '0')
env:
FUZZER_RAN: ${{ steps.fuzz_run.outputs.fuzzer_ran }}
FUZZER_EXIT: ${{ steps.fuzz_run.outputs.fuzzer_exit }}
run: |
echo "::error::${{ matrix.target }} did not complete a fuzzing run (stats printed: ${FUZZER_RAN:-unknown}, exit code: ${FUZZER_EXIT:-unknown})"
exit 1

summary:
name: Fuzzing Summary
runs-on: ubuntu-latest
Expand All @@ -163,11 +231,13 @@ jobs:

steps:
- name: Generate summary
env:
DURATION_INPUT: ${{ github.event.inputs.duration || '600' }}
run: |
echo "## Fuzzing Run Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Date:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
echo "**Duration:** ${{ github.event.inputs.duration || '600' }} seconds per target" >> $GITHUB_STEP_SUMMARY
echo "**Duration:** ${DURATION_INPUT} seconds per target" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All fuzz targets executed. Check individual job outputs for statistics." >> $GITHUB_STEP_SUMMARY
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/gemini-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v2
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v3.2.0
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
Expand All @@ -83,7 +83,7 @@ jobs:

- name: 'Extract command'
id: 'extract_command'
uses: 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3' # ratchet:actions/github-script@v7
uses: 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3' # ratchet:actions/github-script@v9.0.0
env:
EVENT_TYPE: '${{ github.event_name }}.${{ github.event.action }}'
REQUEST: '${{ github.event.comment.body || github.event.review.body || github.event.issue.body }}'
Expand Down Expand Up @@ -183,7 +183,7 @@ jobs:
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v2
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v3.2.0
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gemini-invoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v2
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v3.2.0
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/gemini-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v2
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v3.2.0
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
Expand All @@ -39,7 +39,7 @@ jobs:
permission-pull-requests: 'write'

- name: 'Checkout repository'
uses: 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' # ratchet:actions/checkout@v5
uses: 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' # ratchet:actions/checkout@v7.0.1

- name: 'Run Gemini pull request review'
uses: 'google-github-actions/run-gemini-cli@v0' # ratchet:exclude
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gemini-scheduled-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ jobs:
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v2
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v3.2.0
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gemini-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ jobs:
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v2
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' # ratchet:actions/create-github-app-token@v3.2.0
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
Expand Down
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,73 @@ along with every unmaintained/unsound warning. `cargo audit` and
valid. Nine tests cover it, including a negative control that walks every byte
value and asserts the output contains no illegal code point.

### Fixed (nightly fuzzing)

- **The scheduled fuzz workflow had failed every night since at least
2026-09-21 without fuzzing anything.** All five matrix jobs died about 20 s
in, at `cargo install cargo-fuzz --version 0.13.1`. Without `--locked` that
resolved a fresh dependency graph on every run and compiled it with the
nightly toolchain the job selects; from September it picked up a `rustix`
whose nightly-only build path uses `rustc_*` attributes that current nightly
rejects (`attributes starting with rustc are reserved`). Reproduced locally
with nightly 2026-09-23: 0.13.1 unlocked fails exactly so, 0.13.2 `--locked`
builds. CI now installs the prebuilt cargo-fuzz 0.13.2 release binary through
`taiki-e/install-action`, which no compiler change can break; every
maintained manual install line in `docs/` now reads
`cargo install cargo-fuzz --version 0.13.2 --locked`. That binary is a static musl
build, and cargo-fuzz defaults `--target` to its own build triple, where the
address sanitizer cannot link ("sanitizer is incompatible with statically
linked libc"), so the workflow names `x86_64-unknown-linux-gnu` explicitly.
Both the failure and the fix were reproduced locally with the release binary.
- **Even with cargo-fuzz installed, the fuzzers never actually ran, and the job
still went green.** libFuzzer refuses to start when the `-artifact_prefix`
directory is missing, and `/tmp/fuzzing-artifacts/` was never created, so
every target exited 1 immediately. That was invisible because the exit code
was read after `| tee` without `pipefail` (always tee's 0), the step is
`continue-on-error`, and the only failing gate looked for crash files. The
directory is now created, the fuzzer's real exit code and whether it printed
its final stats are step outputs, and a new "Fail job if the fuzzer did not
run" step fails any target that neither completed cleanly nor recorded a
crash. The outer `timeout` gets 300 s of headroom over `-max_total_time` so it
cannot cut a run libFuzzer is about to end itself.
- The `duration` dispatch input reached the fuzz script by template expansion
into the script text. It now arrives through the environment and is
validated as a positive whole number of seconds (zero would mean "no
limit" to libFuzzer) in a step that is not `continue-on-error`.

### Changed (dependency consolidation, 2026-09)

Supersedes Dependabot #9, #12 and #13.

- **`rustls` 0.23.43 -> 0.23.45** (lockfile) resolves RUSTSEC-2026-0285 (TLS 1.3
handshake messages accepted across encryption-level boundaries), which was
failing `cargo deny` and the fuzz-lockfile `cargo audit` on `main`.
- **`rand` 0.9 -> 0.10.** The convenience methods (`random`, `random_range`)
moved from `Rng` to the new `RngExt` trait; eleven `use rand::Rng;` imports
became `use rand::RngExt;`. No call site changed.
- **`criterion` 0.5 -> 0.8** (dev-dependency). `criterion::black_box` is
deprecated in favour of `std::hint::black_box`; the five benchmark files
switch over.
- **`dirs` 6 -> 7**, drop-in. **`x509-parser` 0.16 -> 0.18 in `fuzz/`**, matching
the workspace, which removes a second `asn1-rs`/`der-parser` stack from the
fuzz build.
- Lockfile refresh of both `Cargo.lock` and `fuzz/Cargo.lock` to the newest
semver-compatible releases (68 and 52 packages respectively).
- **Held back: `sysinfo` 0.39 (needs Rust 1.95) and `sqlx` 0.9 (needs Rust
1.94).** Both are normal dependencies reachable from the published crates,
and the workspace MSRV is 1.88. They move when the MSRV does. Verified
afterwards: 0 of 502 resolved workspace packages and 0 of 362 fuzz packages
declare a `rust-version` above 1.88.
- **Container images move from Debian bookworm to trixie** (`rust:1.88-trixie`
builder, `debian:trixie-slim` runtime). trixie renamed the runtime library
package to `libpcap0.8t64`. The user guide's Docker example had the same
bookworm pin and a `rust:1.85` builder below the MSRV; both are corrected.
- **Actions:** `actions-rust-lang/setup-rust-toolchain` v1 -> v2 (benchmarks),
`github/codeql-action` v3 -> v4. The SHA-pinned actions in the Gemini
workflows were already at the latest releases; their `ratchet:` comments
named older versions (checkout v5, create-github-app-token v2, github-script
v7) and now name the versions the SHAs actually are.

### Changed (dependency majors)

Supersedes Dependabot #3 and #4, which proposed the same wave from two
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ ProRT-IP project guidance for Claude Code.

```toml
tokio = "1.35" # Async runtime
pnet = "0.34" # Packets
clap = "4.4" # CLI
sqlx = "0.7" # Async SQL
ratatui = "0.29" # TUI
pnet = "0.35" # Packets
clap = "4.5" # CLI
sqlx = "0.8" # Async SQL (0.9 needs Rust 1.94, above the 1.88 MSRV)
ratatui = "0.30" # TUI
```

**System**: Linux 4.15+, Windows 10+, macOS 11.0+ | Memory 4GB min (16GB rec)
Expand Down
Loading
Loading