Files
james.maesandClaude Fable 5.2 3ba676106f build(coverage): gate renderer and bridge coverage at baseline; seeded mutation campaigns (P13)
Coverage gates:
- renderer and bridges/renderer gain JaCoCo (prepare-agent, report, check at
  verify) with a bundle rule at the measured baseline, rounded down: renderer
  94% instructions and 91% lines, bridge 85% instructions and 82% lines.
- New code is held to 100% line coverage from its first commit: packages
  com.kingsrook.qbits.renderer.scene2 and below, AssetInspector, ZplRaster,
  and the new foundation classes the label layout design names. The rules
  match nothing until those exist, so later packages do not edit these poms.
- The foundation's 85% instruction and 90% class rule is unchanged.

Fuzz scaffolding:
- MutationCampaign in each module's test tree: a seeded, bounded mutation
  harness with no clock and no unseeded randomness; a finding replays from
  its seed and iteration.
- One campaign per module in the normal test phase: what the version 1 scene
  parser accepts is a version 1 scene (checked by a second parser), and what
  the host printer gate accepts holds only the renderer's command set.
- docs/security/fuzzing.md: targets, properties, limits, seeds and replay.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 17:28:42 -06:00

71 lines
4.1 KiB
Markdown

# Parser fuzzing
## Scope and properties
K22-TEST-25 applies: this repository parses untrusted input at two boundaries. Templates are written by users,
and the isolated renderer is treated as compromisable, so its output is untrusted to the host.
| Target | Module | Input | Campaign |
|---|---|---|---|
| Version 1 scene parser, `ZplRenderer.validate` | `renderer` | A `qqq-zpl-scene/1` source | `MutationCampaignTest.theVersionOneSceneParserAcceptsOnlyVersionOneScenes` |
| Host printer gate, `ZplOutputPolicy.validate` | `bridges/renderer` | ZPL bytes returned by the worker | `MutationCampaignTest.theHostPrinterGateAcceptsOnlyTheRendererCommandSet` |
The harness is `MutationCampaign`, one copy in each module's test tree (the modules share no test code). It
mutates valid inputs from the version 1 compatibility corpus (`renderer/src/test/resources/compat-v1`): bit
flips, byte and range edits, truncation, duplication and tokens of the format spliced in. Each campaign checks
unsafe acceptance, not only crashes:
- **Scene parser.** An accepted input, read again by a second and differently configured JSON parser, is a
version 1 scene of known element types within the element limit. Any exception is a refusal, because the
worker turns every exception into its opaque error answer. A stack overflow or a failed acceptance check is a
finding.
- **Printer gate.** An accepted input contains only the commands the renderer may emit and no tilde command.
A refusal is the gate's `IllegalArgumentException`, or the strict decoder's `CharacterCodingException` for
field text that is not UTF-8. Any other failure, or a failed acceptance check, is a finding.
Each campaign also asserts that it reached both acceptance and refusal, so a campaign that only ever refuses
cannot pass.
This is seeded generated-input fuzzing, not coverage-guided instrumentation. Do not claim OSS-Fuzz or
ClusterFuzzLite enrolment.
## Running and replaying
The campaigns run in the normal test phase, so every `./mvnw verify`, and therefore every CI build of a pull
request or a push, runs them. No network, clock or unseeded randomness is used: a seed and an iteration
number determine the input.
```sh
# What CI runs: seeds 20261111 and 7919, 20000 iterations each, per target.
./mvnw -B -pl renderer,bridges/renderer -am verify
# Replay a finding, or run a longer campaign before a MINOR or MAJOR release (both seeds at 100000).
./mvnw -B -pl renderer -am test -Dtest=MutationCampaignTest -Dsurefire.failIfNoSpecifiedTests=false -Dfuzz.seed=20261111 -Dfuzz.iterations=100000
./mvnw -B -pl bridges/renderer -am test -Dtest=MutationCampaignTest -Dsurefire.failIfNoSpecifiedTests=false -Dfuzz.seed=20261111 -Dfuzz.iterations=100000
```
A failure lists each finding as `Finding[seed, iteration, failure]`. `MutationCampaign.input(seed, iteration)`
returns that exact input against the same checkout. Reduce it to a named regression test beside the parser's
other tests, fix the parser, and rerun both seeds. Report a suspected vulnerability privately rather than in a
public issue.
Limits: inputs are at most 1 MiB, a campaign is at most 100000 iterations, and the corpus entries used are
under 20 KiB. The harness writes no files, launches no process and reads no credentials. Generated ZPL is never
sent to a printer and generated scenes are only validated, not rendered.
## Adding a target
A new parser of untrusted input adds one test method to the `MutationCampaignTest` of its module, with a
corpus of valid inputs, a dictionary of the format's tokens, the refusal it declares and an independent check
of what it accepts, and adds a row to the table above. The version 2 scene parser and `ZplRaster` are the next
two targets.
## Review record
- **Added:** 2026-11-11, with the two campaigns above.
- The first run of the printer gate campaign showed the `CharacterCodingException` refusal, which the property
had not declared. It is a refusal, not a defect; the property now names it.
- Not yet done: a weekly long campaign and the `docs/security/assurance.json` record that K22-TEST-25 also asks
for. The long campaign is the manual command above until a scheduled job exists.
- Reassess the targets when a parser boundary changes.