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
3 changes: 0 additions & 3 deletions .github/actions/moon-run/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,3 @@ runs:
- uses: appthrust/moon-ci-retrospect@v1
if: success() || failure()

- run: echo "${{ steps.raa.outputs.summary }}" >> $GITHUB_STEP_SUMMARY
shell: bash
if: always()
18 changes: 14 additions & 4 deletions .moon/tasks/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,30 @@ fileGroups:
- "tests/**/*"

tasks:


cargo:
command: "cargo"
env: &env
CARGO_TERM_COLOR: "always"
options:
runInCI: false

debug-env:
script: printenv | grep -E '^(CARGO|MOON|PROTOC|PROTO|PATH)'
env: *env

build: &build
command: "cargo build"
inputs:
- "@group(cargo)"
- "@group(sources)"
env: &env
CARGO_TERM_COLOR: "always"
env: *env

build-release:
<<: *build
command: "cargo build --release"
env: *env
options:
runInCI: false

Expand All @@ -51,7 +59,7 @@ tasks:
- "@group(cargo)"
- "@group(sources)"
- "@group(tests)"
- "/clippy.toml"
- "/.clippy.toml"
env: *env

lint-fix:
Expand All @@ -61,7 +69,7 @@ tasks:
runInCI: false

test:
command: "cargo nextest run --no-fail-fast -p solidus-$project"
command: "cargo nextest run --no-fail-fast -p $project"
inputs:
- "@group(cargo)"
- "@group(sources)"
Expand All @@ -83,3 +91,5 @@ tasks:
inputs:
- "@group(cargo)"
env: *env


File renamed without changes.
File renamed without changes.
File renamed without changes.
129 changes: 87 additions & 42 deletions engine/fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,17 @@ Prevention.
│ (cargo fuzz tmin) (cargo fuzz cmin) │
│ │ │ │
│ ▼ ▼ │
│ fuzz/regressions/<target>/ fuzz/corpus/<target>/ │
└──────────────────────┼─────────────────────────────────────────────────┘
│ (Minimized crashes)
┌────────────────────────────────────────────────────────────────────────┐
│ 3. Regression Prevention (cargo test) │
│ │
│ [ engine/build.rs ] ──► Reads fuzz/regressions/ at compile time │
│ │ │
│ ▼ │
│ Generates fuzz_regression_<target>.rs in $OUT_DIR │
│ │ │
│ ▼ │
│ [ cargo test ] ───────► Executes minimized crashes as unit tests │
│ every build, preventing regressions │
└────────────────────────────────────────────────────────────────────────┘
` ``
│ fuzz/regressions/<target>/ fuzz/corpus/<target>/│
└──────────────────────┤───────────────────────────────────┤─────────────┘
│ (Minimized crashes) │ (Compacted)
▼ ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ 3. Persistence │
│ │
│ [ regressions/ ] ──► Committed to main (datatest-stable auto-discovers)│
│ [ corpus/ ] ───────► Synced to orphan `corpus` branch + CI cache │
└──────────────────────────────────────────────────────────────────────────┘
```


## How It Works
Expand Down Expand Up @@ -89,6 +83,48 @@ Once a crash is minimized into `regressions/`, it becomes a permanent test with
- These tests execute on every build, ensuring historical crashes can never silently regress.


## Corpus Management

Corpus inputs are stored with two-tier persistence to avoid repository bloat while maintaining
shared progress across CI runs and local sessions.

### CI: Actions Cache + Orphan Branch

The `fuzz-cache` composite action manages both tiers via `mode: pull` and `mode: push` inputs:

- `actions/cache@v4` provides fast, per-run restoration. Cache keys are isolated by main vs PR
(`fuzz-corpus-main-*` vs `fuzz-corpus-pr-<number>-*`). PR caches fall back to main if no
PR-specific cache exists.
- An orphan `corpus` branch provides durable, shared storage. On `pull`, the action fetches the
branch and seeds the corpus directory via `rsync`. On `push`, it syncs the updated corpus back
with retry logic. Branch operations are main-only (silently skipped for PRs). The branch is
self-initializing: created automatically on first push if it doesn't exist.

Workflow sequence (main-fuzz.yml):
1. `fuzz-cache mode: pull` restores from CI cache, then seeds from `corpus` branch
2. `fuzz-saturate` runs all four engine/target variants
3. `fuzz-cache mode: push` syncs compacted corpus back to `corpus` branch
4. `fuzz-triage` minimizes crashes into `regressions/` (only if crashes found)

### Local: Git Worktree

For local sessions, a git worktree at `//.corpus/` provides access to the orphan branch
without switching branches:

```bash
# One-time setup
git worktree add .corpus corpus

# Typical local fuzzing session
moon run engine-fuzz:fuzz-corpus-pull # seed from CI's latest corpus
moon run engine-fuzz:fuzz-saturate # fuzz locally
moon run engine-fuzz:fuzz-corpus-push # triage, compact, push back
```

The `fuzz-corpus-push` task triages crashes, compacts the corpus, pulls any new CI discoveries
made during the session, and pushes the merged result to the `corpus` branch.


## Moon Task Graph

Moon manages the parallelization of the full 2x2 engine/target matrix.
Expand All @@ -97,23 +133,27 @@ Moon manages the parallelization of the full 2x2 engine/target matrix.
fuzz
├── fuzz-saturate (parallel)
│ ├── fuzz-structured-saturate
│ │ ├── libfuzz-structured-saturate (fuzz-run.sh ... 86400 180)
│ │ ── libafl-structured-saturate (fuzz-run.sh ... 86400 180 + FUZZ_FEATURES)
│ │ ├── libfuzz-structured-saturate (fuzz-run.sh ... 14400 2)
│ │ ── libafl-structured-saturate (fuzz-run.sh ... 14400 2 + FUZZ_FEATURES)
│ │ └── compact (fuzz-manage.sh compact)
│ └── fuzz-unstructured-saturate
│ ├── libfuzz-unstructured-saturate
── libafl-unstructured-saturate
── libafl-unstructured-saturate
│ └── compact
└── fuzz-triage (sequential, after saturate)
├── triage parse_document_unstructured
└── triage parse_document_structured

fuzz-ci (parallel, all four variants with -runs=0)
├── libfuzz-structured-ci
├── libfuzz-unstructured-ci
├── libafl-structured-ci
└── libafl-unstructured-ci
` ``
fuzz-triage (sequential, after gate signals crashes)
├── triage parse_document_unstructured
└── triage parse_document_structured

fuzz (quick replay, libFuzzer only, -runs=0)
├── libfuzz-structured (fuzz-run.sh ... --replay)
└── libfuzz-unstructured (fuzz-run.sh ... --replay)

fuzz-corpus-pull / fuzz-corpus-push (local worktree sync)
```

### Commands

Expand All @@ -129,7 +169,11 @@ moon run engine-fuzz:fuzz

# Triage only: minimize new crash artifacts into regressions/
moon run engine-fuzz:fuzz-triage
` ``

# Corpus sync (local, requires git worktree at .corpus/)
moon run engine-fuzz:fuzz-corpus-pull
moon run engine-fuzz:fuzz-corpus-push
```


## CI Automation
Expand All @@ -139,23 +183,25 @@ Two GitHub Actions workflows automate fuzzing in CI.
### Trunk Saturation (`main-fuzz.yml`)

Runs weekly (Saturday 2 AM UTC) and on manual dispatch. Discovers all projects with a `fuzz-ci`
task via `moon-q-projects`, then runs `fuzz-saturate` across them in a matrix. If crashes are found,
it triages them automatically and opens a PR with the minimized regression files via the `pr-create`
composite action.
task via `moon-q-projects`, then runs `fuzz-saturate` across them in a matrix. Corpus is pulled
from cache and the orphan branch before fuzzing, and pushed back after. If crashes are found, it
triages them and opens a PR with the minimized regression files via the `pr-create` composite
action.

### PR Saturation (`pr-fuzz.yml`)

Triggered by adding the `fuzz` label to a PR, or via `workflow_dispatch` with a PR number. Runs the
same discover/saturate/triage cycle against the PR branch. Regression files are committed directly
to the PR branch (attributed to the last branch author). Results are reported as a commit status and
sticky PR comment via the `pr-set-status` composite action.
Triggered by adding the `fuzz` label to a PR, or via `workflow_dispatch` with a PR number. Runs
the same discover/saturate/triage cycle against the PR branch. Corpus is seeded from cache only
(falls back to main cache, no branch interaction). Regression files are committed directly to
the PR branch. Results are reported as a commit status and sticky PR comment via the
`pr-set-status` composite action.

Both workflows use these shared composite actions:

- `setup`: Checkout, toolchain installation, Cargo/Moon/pnpm caches
- `moon-q-projects`: Discover fuzz-capable projects with metadata
- `moon-run`: Execute moon tasks with retrospect reporting
- `fuzz-cache`: Per-project corpus caching with main/PR isolation
- `fuzz-cache`: Two-tier corpus persistence (CI cache + orphan branch)
- `git-stage-artifacts`: Download and place matrix artifacts into project directories
- `git-commit-push`: Commit and push with last-author attribution
- `pr-create`: Idempotent PR creation (trunk workflow)
Expand All @@ -165,10 +211,9 @@ Both workflows use these shared composite actions:

## State Management

- `fuzz/corpus/<target>/`: Gitignored. Persisted exclusively via GitHub Actions caching
(`fuzz-cache` action) to avoid repository bloat.
- `fuzz/artifacts/<target>/`: Gitignored. Temporary storage for raw crashes, minimized files, and
log output.
- `fuzz/regressions/<target>/`: Committed. Tiny, minimized crash files that serve as source of
truth for `cargo test`.

| Directory | Tracked | Persistence |
|---|---|---|
| `fuzz/corpus/<target>/` | Gitignored | CI cache (`actions/cache`) + orphan `corpus` branch. Main and PR caches isolated; branch ops main-only. |
| `fuzz/artifacts/<target>/` | Gitignored | Ephemeral. Raw crashes, minimized files, crash summaries, logs. |
| `fuzz/regressions/<target>/` | Committed | Minimized crash files. Source of truth for `cargo test` via `datatest-stable`. |
| `.corpus/` (repo root) | Gitignored | Local git worktree pointing at the orphan `corpus` branch. |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading