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
16 changes: 10 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -548,12 +548,16 @@ jobs:
# every filter is vacuous.
- name: Verification-filter guardrail
run: tools/check_verification_filters.py
# REQ-GUARD-GATE-EVIDENCE-002 (h). fuzz-nightly names its targets in a
# hand-written matrix `include` (each carries its own extra_args, #361),
# so the declared set and the run set are maintained separately and
# nothing compared them. A target dropped from either side leaves the
# nightly green having fuzzed a strict subset — no error, no warning, no
# smaller number in the log. Cheap: two file reads, no build.
# REQ-GUARD-GATE-EVIDENCE-002 (h) + REQ-GUARD-FUZZ-SMOKE-001 (#406). Two
# workflows run the fuzz harnesses from lists maintained apart from
# fuzz/Cargo.toml's `[[bin]]` declarations: fuzz-nightly's matrix `include`
# (each target carries its own extra_args, #361) and — the one that
# actually gates merges — THIS file's `fuzz-smoke` job, which hand-writes
# one `cargo fuzz run <target>` step per harness. The original tool audited
# only the ADVISORY nightly, so a target added to fuzz/Cargo.toml but not
# to fuzz-smoke left the REQUIRED context green having fuzzed a strict
# subset (#406). The tool now asserts the declaration against BOTH lists;
# `--ci-workflow` defaults to this file. Cheap: three file reads, no build.
- name: Fuzz-target guardrail self-test
run: tools/check_fuzz_targets.py --self-test
- name: Fuzz-target guardrail
Expand Down
69 changes: 69 additions & 0 deletions artifacts/requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5024,3 +5024,72 @@ artifacts:
status: proposed
release: v0.36.0
tags: [process, guardrail, ci, tooling, human-scoped]

- id: REQ-GUARD-FUZZ-SMOKE-001
type: requirement
title: The fuzz-target guardrail shall audit the REQUIRED context, not only the advisory one
description: >
REQ-GUARD-GATE-EVIDENCE-002 (h) added `tools/check_fuzz_targets.py` to stop
the fuzz target list drifting from its `[[bin]]` declaration in
`fuzz/Cargo.toml`. It audited the wrong one of two lists (#406).

There are two run-lists, maintained apart from the declaration:

* `fuzz-nightly.yml`'s `matrix.include` — one entry per target. This is
the list the original tool compared (`--workflow` default). It is
ADVISORY: the job name is templated (`Fuzz ${{ matrix.target }}`),
which by `check_required_contexts.py`'s own rules can NEVER be a
required context.

* `ci.yml`'s `fuzz-smoke` job — the same three targets hand-written as
one `cargo +nightly fuzz run … <target>` step each. THIS is the list
that gates: `Fuzz smoke (60s/target)` is in
`.github/required-contexts.txt`. Nothing compared it to anything.

So the covered list was advisory and the uncovered list blocked merges.
Add a `[[bin]]` plus a nightly matrix entry but forget the `fuzz-smoke`
step, and the tool reported `All 4 declared fuzz targets are run` while the
REQUIRED gate fuzzed 3 of 4, green, forever. This is -002's own defect
shape one level up: the shortfall in the blocking gate rendered as the
ideal reading of a non-blocking one.

OBLIGATION. The declared `[[bin]]` set shall be asserted equal — both
directions — to the target set RUN by the required `fuzz-smoke` job, not
only to the advisory nightly matrix. Extending the nightly's `- target:`
regex to `ci.yml` does not work: the fuzz-smoke targets sit as the
positional of a `cargo … fuzz run` command inside `- run:` steps, so a
SECOND extractor is required. That extractor drops the `+toolchain` token,
value-taking flags with their argument (so the real `--target <triple>` is
never read as a harness), and the libfuzzer args after `--`; a step that
does not resolve to exactly one target positional is a broken scan
(exit 2), never a silently dropped leg. It is scoped to the `fuzz-smoke:`
job block and skips full-line comments — because `fuzz run` also appears in
prose, and a whole-file scan matched this file's own guard-step comment and
read the required gate as a broken scan.

ORACLE (executed, non-vacuous). `tools/check_fuzz_targets.py --self-test`
carries an 18-row decision table; the fuzz-smoke rows include the #406
acceptance criterion itself — a `[[bin]]` added to `fuzz/Cargo.toml` but
not to the `fuzz-smoke` job must exit 1. That row's exit (1) differs from
the base row's (0) on the same tool with a distinct input, which is the
discrimination the advisory-only tool lacked. A REAL-TREE row runs the
check against the committed `ci.yml` and asserts exit 0, so a `fuzz run`
string landing in a comment (the regression an earlier whole-file scan
shipped) fails the self-test rather than silently reading the gate as
broken. Demonstrated live against the real `ci.yml`: appending a fourth
`[[bin]]` to the real `fuzz/Cargo.toml` makes the fuzz-smoke check exit 1
(`fuzz_new_harness … NOT run by the fuzz-smoke job`), while the unmodified
tree exits 0.

PROVEN vs ASSUMED, stated precisely. PROVEN: the required gate now fails
when its executed target set diverges from the declaration, in either
direction, and fails closed on an unparsable step. NOT CLAIMED: that a
declared, listed target is actually FUZZED. Set equality proves
DECLARATION parity, not execution — a `fuzz-smoke` step carrying
`continue-on-error: true` or `if: false` still counts as "run" here. That
is a distinct obligation (execution evidence, not list parity) and is left
to a successor; this requirement closes the list-parity hole #406 names and
no more.
status: implemented
release: v0.36.0
tags: [process, guardrail, ci, tooling, fuzzing]
70 changes: 70 additions & 0 deletions artifacts/verification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4268,6 +4268,76 @@ artifacts:
release: v0.36.0
tags: [process, guardrail, ci, tooling, fuzzing]

- id: TEST-GUARD-FUZZ-SMOKE
type: feature
title: The REQUIRED fuzz-smoke target list is proven equal to the fuzz targets declared
description: >
Verifies REQ-GUARD-FUZZ-SMOKE-001. Closes #406.

TEST-GUARD-FUZZ-TARGETS above audits the ADVISORY `fuzz-nightly.yml`
matrix. The list that actually blocks merges is `ci.yml`'s `fuzz-smoke`
job (`Fuzz smoke (60s/target)`, in `.github/required-contexts.txt`), which
hand-writes each harness as a `cargo +nightly fuzz run … <target>` step —
and it was compared against nothing. `tools/check_fuzz_targets.py` now
asserts the `[[bin]]` declaration against BOTH run-lists; `--ci-workflow`
defaults to `ci.yml` and the worst exit wins, so a broken scan is never
masked by a clean sibling.

The fuzz-smoke extractor cannot reuse the nightly's `- target:` regex — the
targets sit as the positional of a `cargo … fuzz run` command inside
`- run:` steps. It drops the `+toolchain` token, value-taking flags with
their argument (so the real `--target <triple>` is not read as a harness),
and the libfuzzer args after `--`. A step not resolving to exactly one
target positional exits 2 (broken scan), not a silent skip.

The extractor is SCOPED to the `fuzz-smoke:` job block and skips full-line
comments — because `fuzz run` also occurs in prose. An earlier whole-file
version matched the guard step's own explanatory comment
(`cargo … fuzz run … <target>`) in this same ci.yml, resolved it to
several tokens, and read the REQUIRED `Rivet validate (artifacts)` context
as a broken scan (exit 2) — the fix's own comment turning the gate it adds
red. The self-test's synthetic fixtures could not see that (they carry no
comment), so the suite carries a REAL-TREE row that runs the check against
the committed ci.yml and asserts exit 0; a poisoning comment fails it
loudly.

`tools/check_fuzz_targets.py --self-test` runs an 18-row decision table
before the gate judges anything. The fuzz-smoke rows include:

* the #406 acceptance criterion — a `[[bin]]` added to `fuzz/Cargo.toml`
but not to the fuzz-smoke job must exit 1. Its exit (1) differs from
the base row's (0) under a distinct input, which is the discrimination
the advisory-only tool lacked and the non-vacuity proof for this gate;
* the reverse — fuzz-smoke running a target not declared exits 1;
* a step with no `fuzz run` at all, and a step whose command leaves an
ambiguous positional, each exit 2 (broken scan), so the extractor
cannot pass by reading nothing;
* the `--flag=value` equals form is a single hyphen token, so it is a
flag and does not leak a positional.

The base row only passes because the `--target x86_64-unknown-linux-gnu`
triple is NOT counted as a third harness. Demonstrated live against the
real `ci.yml`: a fourth `[[bin]]` on the real `fuzz/Cargo.toml` makes the
fuzz-smoke check exit 1; the unmodified tree exits 0. Three file reads, no
build.

NOT claimed: that a listed target is actually fuzzed. Set equality is
declaration parity, not execution — a `continue-on-error`/`if: false` leg
still counts as "run". Distinct obligation, left to a successor.
fields:
method: automated-test
steps:
- run: tools/check_fuzz_targets.py --self-test
- run: tools/check_fuzz_targets.py
status: implemented
release: v0.36.0
tags: [process, guardrail, ci, tooling, fuzzing, v0360]
links:
- type: satisfies
target: REQ-GUARD-FUZZ-SMOKE-001
- type: verifies
target: REQ-GUARD-FUZZ-SMOKE-001

- id: TEST-GUARD-VERIFICATION-FILTERS
type: feature
title: A verification step whose test filter selects nothing is proven to fail
Expand Down
Loading
Loading