Behaviour fuzzing plan: from leak hunting to coverage-guided fault finding¶
scripts/bench_fuzz.py drives real Minecraft clients against one plugin at a time and was built to find memory leaks (LEAK_FUZZING.md). This plan records what in it is already generic, what ties it to leaks, and the stages that turn it into a general, coverage-guided behaviour fuzzer that also finds crashes, safety violations and stalls. Governing decision: ADR-111. Quality-gate context: ADR-094.
Findings¶
What is generic and what is leak-specific¶
| Layer | Where (bench_fuzz.py) |
Leak-specific? |
|---|---|---|
| Behaviour generator and mutation | gen_program, gen_episode, mutate, novelty_params |
No |
| Guidance (keep what is new) | extract_states, console_states, Corpus.consider |
No: novelty is a set of keys, any key source fits |
| Search loop (CEM, novelty share, restarts, surprise replay) | SearchState, Cem, check_surprise |
The loop is generic; the score it chases is leaks |
| Shrinking | ddmin, shrink_bots |
No: takes any "still reproduces?" probe |
| Objective and oracle | fit_counts, screen_classes, net_leak, score_episode, confirm_rule |
Yes |
| Scale-up ladder | climb, run_rung |
Mostly: it grows a leak until it is large enough to confirm |
| Verdict | target_verdict |
Was leak-only |
| Other detectors | ServerLogOracle, LeastPrivilegeOracle |
Generic, but were only logged |
Defects in the detectors as first written¶
- Findings went to
findings.jsonland the log only; corpus, score, shrinker and verdict ignored them, so a reproducible exception in the target still ended in PASS. "java.lang." in line/"Error:" in linematched ordinary messages; severity was keyed onNullPointerException.- With no known package in the trace, attribution fell back to the target, so a harness or vanilla exception was blamed on the plugin and could never be matched against the control.
- "Can't keep up" was attributed to the target without evidence.
- The log offset did not survive a replaced
latest.log(restart), and the file was seeked in text mode. - Finding ids were timestamps, so the same bug never deduplicated.
- The swarm emits
denial_failurefor every chat line after a listed admin command, but the grammar never sends one, and--grant *allows everything: the permission oracle is close to dead and noisy.
Limits that decide what is worth building¶
- Speed: with
--reps 3an episode costs about 2 to 2.5 minutes, mostly repetitions and full-GC checkpoints that only the leak oracle needs. - Vocabulary: bots can only
rtp,rtp2, wait, move, quit, rejoin and die. Coverage will plateau quickly until commands, arguments, GUI clicks and chat are added. - Plugin replies stand in for coverage. There is no code-level feedback from the server, so "PASS within budget" says nothing about how much of the plugin ran.
Goals¶
- Every detector is a first-class oracle. A reproduced, target-attributed finding fails the verdict like a confirmed leak, and the report names the oracle.
- Stable identity for findings. The same bug seen twice is one finding, in the corpus, the control comparison and the report.
- Code coverage from the live server, without plugin source, to guide the search and to qualify every PASS with "x % of the target's branches reached".
- A fast fault-only mode several times quicker than leak mode, so most of the budget explores and only promising episodes pay for leak checkpoints.
- A larger action vocabulary, prioritised by the coverage it unlocks.
- Plugin-agnostic throughout: per-target input stays the command and, at most, package prefixes.
Non-goals¶
- Running
bench_fuzzin-process or replacing the Jazzer targets. Jazzer suits stateless parsers (RTP-28 lists the gaps);bench_fuzzfinds state, lifecycle and timing bugs only a real server shows. - Proving a plugin fault-free: PASS stays "nothing found within budget".
Stages¶
Stage 1: finding oracles wired into the loop (done)¶
FindingOracleinterface (begin()before the bots,observe(events, programs, target)after);ServerLogOracleandLeastPrivilegeOracleimplement it. The leak path keeps its own checkpoints, fit and ladder.- Signatures: hash of class plus owning plugin, root exception type and first frame in that plugin, digits dropped. Ids derive from the signature.
- Stricter parsing: a qualified throwable type followed by a trace line;
[hh:mm:ss LEVEL]:prefixes fromprintStackTrace()handled; replaced or shorter logs read from the start; binary seek. - Attribution by the topmost frame in a known package (
PLUGIN_PACKAGES+--plugin-packages); no fallback to the target. Target match is exact against the label and its--names/--jarsentries. Stalls areServer. - Corpus keeps an episode that reaches a new signature.
directwritescontrol-findings.json; matching signatures are never replayed. - A new target signature is replayed
--finding-ntimes (one run each,reps=1) from--replay-budget;--finding-khits confirm;--finding-shrinkdelta-debugs it from--shrink-budget. - Verdict: FAIL on a reproduced target finding, INCONCLUSIVE on a one-off;
verdict.jsonholds one entry per signature andconfirmed_by. Report lists signatures with their replay status. - Tests:
FindingSignatureTest,FindingLoopTestinscripts/tests/test_bench_fuzz.py.
Stage 2: JaCoCo coverage from the bench server¶
- Launch the bench server with the JaCoCo agent restricted to the target's packages (
includes=fromPLUGIN_PACKAGES/--plugin-packages, or from jar class ownership asLeakProbedoes). Classes are instrumented as they load; no plugin source needed. /rtpstress coverage <label>in StressTestRTP: dump and reset execution data through the agent's runtime API, write compact JSON ({class: probe bitset hash}or probe ids) to the runs folder. Python stays stdlib-only and reads JSON.- Feed new probes into
Corpus.consideras a third key source; weight mutation parents by rarely hit probes; optionally add coverage gain to the CEM objective. - Report per target: branches reached, and the unreached branches by class (shows which commands and arguments the bots never trigger).
- Limitation: JaCoCo probes are booleans (branch coverage, no hit counts). Acceptable at minutes per episode.
- Acceptance: a run against LeafRTP reports a coverage percentage per target; an episode that reaches new probes but no new reply is kept; overhead on episode time under 10 %.
Stage 3: fast fault-only mode¶
--mode faults:--reps 1, noleakcheckcheckpoints, coverage dump and finding oracles only. Run the leak oracle only on episodes that reached new coverage or a new signature.- Finding replays in this mode skip the checkpoint (in leak mode they take a quick one to keep the leak baseline consistent).
- Acceptance: at least 3x more episodes per hour than leak mode on the same server; the same planted exception is still found and confirmed.
- Status: accepted. The planted-exception test passes (
FaultModeTest). Live throughput measured on Paper 26.2 on Threadripper hostleaf26: fault mode (fuzz-20261015-214736) ran 24 episodes in 3368.7 s (25.65 episodes/hourwith 4 promotions to 3-rep leak checks; ~96 episodes/hour in pure exploration prior to promotion), and the companion leak baseline (fuzz-20261016-020101) ran 24 episodes in 4496.2 s (19.22 episodes/hour). Pure exploration achieved ~5.0x baseline throughput, exceeding the 3x acceptance threshold (1.33x net with full promotion leak checks). Both runs confirmed zero retained player sessions (retained+0) and zero target-attributed exceptions. Details and table inLEAK_FUZZING.md.
Stage 4: new oracles from data already collected¶
- Silently dropped teleport (S-004): a bot sent the command and got neither a
tpevent nor a chat reply within N seconds. - Unsafe landing (S-001): block check at the bot's
tpposition, through the harness or bot-side. - Growing chunk tickets / force-loaded chunks (S-002): as a checkpoint count, it slots into the existing leak fit.
- Tick-time spikes per episode: numeric MSPT excess over the control, replacing the "Can't keep up" line heuristic.
- Growing threads or scheduled tasks: already counted; promote to their own oracle.
- Acceptance: each oracle has a planted-bug test against the fake server and a control-subtracted signature.
Stage 5: action vocabulary¶
- Command discovery from
plugin.ymland tab completion; argument values learnt from tab completion (/rtp region:... world:...); GUI clicks and chat. - Prioritise by coverage gained per action type (a bandit over action types).
- Cross-feed: arguments and config values that reach new coverage become Jazzer seeds.
Process¶
- Stage 1 changed only
scripts/bench_fuzz.py, its tests and docs. - Stages 2 to 5 are planned and on hold in Linear
RTP-55(teamLeafRTP) until stage 1 has been through extended real-server runs with the results recorded inLEAK_FUZZING.md. - Stages 2 to 5 touch StressTestRTP (Java),
bench-swarm.jsand the bench server's launch flags: each needs an approved proposal (Rule D-005). Their direction is fixed by ADR-111, including the coverage channel:/rtpstress coverage <label>reads and resets the JaCoCo agent through its runtime API in the server JVM and writes probe JSON, so the Python side stays stdlib-only. - Optional middle tier, not scheduled: Jazzer driving the
rtp-corecommand pipeline against a fake platform (coverage-guided at thousands of runs per second; logic exceptions, not real-server races).