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>
71 lines
4.1 KiB
Markdown
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.
|