ci(fuzz): stop the scheduler_solver nightly OOMing on ASan bookkeeping (#361) - #363
ci(fuzz): stop the scheduler_solver nightly OOMing on ASan bookkeeping (#361)#363avrabe wants to merge 1 commit into
Conversation
Rivet verification gate✅ 20/20 passed
Filter: Failed artifacts(none) Updated automatically by |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
#361) `fuzz_scheduler_solver` has ended in `libFuzzer: out-of-memory` on every nightly run the API still returns — 97 of 97, 2026-04-25 through 2026-07-30. Nothing surfaced it because fuzz-nightly is not a required context, so a workflow that has never once passed sat red for three months as a gate nobody reads. It is not a leak. ASan's own accounting at the moment of death (run 30516991027) rules the code under test out: used: 2056Mb; limit: 2048Mb Live Heap: 27.7 MB in 8,340 chunks quarantined: 155.7 MB in 2,104,213 chunks total chunks: 4,345,508 Live plus quarantined *contents* account for 183 MB of 2056 MB. The remaining ~1.87 GB is ASan's per-chunk bookkeeping spread over 4.3M chunks, which is a product of two terms, so the fix addresses both: * `quarantine_size_mb=64` (from 256) bounds the chunk count. The 2.1M quarantined chunks are retained only to catch use-after-free, which is not the bug class this harness hunts — it asserts `solve_milp` never panics. * `malloc_context_size=10` (from 30) bounds the per-chunk cost. Ten frames still names the allocation site, so crash triage is intact. `-max_len=128` is applied to this leg ONLY, via a matrix `include`. The cap is lossless rather than a coverage trade: the harness opens with `.min(8)` / `.min(4)` on the two Vecs while `Arbitrary` grows them to consume whatever buffer it is handed, so bytes past the ~120 the capped domain can encode yield `Task` values that are allocated and discarded unread. Keeping it per-leg is the point. `fuzz_aadl_parse` and `fuzz_codegen_roundtrip` concluded `success` in all 97 runs and parse arbitrarily long source text; a shared `-max_len` on the existing single run line would have shrunk two healthy fuzzers' input space to fix a third's bug. `-rss_limit_mb=4096` is headroom, not the mechanism — the measures above should land well under even the 2048 default. The oracle is two-sided and is the nightly itself: scheduler_solver must flip failure -> success AND the other two legs must stay success. A green run where a parser leg regressed is not a fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dc54b6f to
5199cec
Compare
Oracle satisfied — both sides, full 1h budgetRun 30560598236 —
Both sides of the oracle stated in this PR hold: the OOM leg flipped, and neither parser leg regressed. All three consumed the full 1h budget rather than exiting early, so the green is not a fail-fast artifact. Rebased Grounding note: this is a dispatch result. The scheduled nightly on |
Closes #361.
What
fuzz_scheduler_solvergetsASAN_OPTIONS=malloc_context_size=10:quarantine_size_mb=64and a per-leg
-max_len=128. No Rust changes.The evidence, not the hypothesis
It has ended in
libFuzzer: out-of-memoryon every nightly run the API stillreturns — 97 of 97,
2026-04-25→2026-07-30. There is no passing run in thewindow at all.
It is not a leak, and ASan's own accounting at the moment of death rules the
code under test out (run 30516991027):
Live plus quarantined contents are 183 MB of 2056 MB. The other ~1.87 GB is
ASan's per-chunk bookkeeping across 4.3M chunks — a product of two terms, so both
get addressed:
quarantine_size_mbmalloc_context_sizeUse-after-free is not the bug class this harness hunts; its contract is that
solve_milpnever panics. Ten frames still names the allocation site, so crashtriage survives — deliberately not cut to 2, which saves little and turns a
report into an unactionable address.
-rss_limit_mb=4096is headroom, not the mechanism. If it turns out to beload-bearing, the diagnosis above was wrong.
Why
-max_lenis per-leg — the part worth reviewingThe obvious patch is to append
-max_len=128to the shared run line. That wouldhave been a silent regression: the line is shared across a 3-target matrix, and
across all 97 runs
fuzz_aadl_parseandfuzz_codegen_roundtripconcludedsuccessevery time. Onlyfuzz_scheduler_solverfails.Those two parse arbitrarily long AADL/WIT source text, so a shared cap shrinks two
healthy fuzzers' reachable input space to fix a third's bug. Entropy budget is a
property of the harness, so the flag moved into
matrix.include.For this leg the cap is lossless rather than a coverage trade. The harness opens:
while
Arbitrarygrows thoseVecs to consume whatever buffer it is handed. Everybyte past the ~120 the capped domain can encode yields a
Taskthat is allocatedand then discarded unread. Capping removes allocation churn, not reachable states.
The saved crash artifact is NOT a reproducer
fuzz/artifacts/fuzz_scheduler_solver/oom-c5aafda340317c72f5ba9eb2431f0c189d159c9dis 267 bytes and is simply whichever input happened to be executing when the
process crossed the RSS limit. Replaying it proves nothing. Recording this so
nobody loses a day treating it as the trigger.
Verification
This is the honest part: I cannot verify this locally. No nightly Rust or
cargo-fuzzin this environment, and the failure needs a ~10-minute ASan fuzzingrun to reproduce. The numbers above are read from the CI log, and the
-max_lenargument is read off the harness source; the fix itself is so-far unproven.
The oracle is the nightly, and it is two-sided:
fuzz_scheduler_solvermust flipfailure→success, andfuzz_aadl_parseandfuzz_codegen_roundtripmust staysuccess.A green run where a parser leg regressed is not a fix. YAML was parsed locally to
confirm the matrix expands to the three intended
(target, extra_args)pairs.I will dispatch the workflow on this branch and report the result before merging,
rather than merging on the strength of the reasoning.
No rivet artifact, deliberately
Matching #353 (the CI path-filter PR), which was also workflow-only and carried
none. There are no
REQ-CI-*artifacts in this repo — rivet tracks what spar shalldo, not CI plumbing. Flagging the omission explicitly so it reads as a choice
rather than an oversight.
Meta
Neither this workflow nor
trace-fixtures(#362, 60/60 red) is a requiredcontext, which is the whole reason a job that has never once passed sat red for
three months. A gate nobody reads is not a gate. Making them required is a separate
decision — it needs them green first, which is what these two PRs are for.
🤖 Generated with Claude Code