Repository navigation
ci: move the fuzz search from pull requests to a nightly job - #1382
Open
fuad-daoud wants to merge 14 commits into
Open
fuad-daoud wants to merge 14 commits into
fuad-daoud wants to merge 14 commits into
Conversation
Every pull request spent a median 231s on a search that restarted from the seeds each time. Pull requests now run make fuzz-check, which holds the targets to their cap and the quarantine to real targets without searching, and every target's seeds still run in the suite. Fuzzing leaves make gate. The search runs on a schedule or by hand, in a job of its own, with a corpus it restores and saves between nights under a key nothing else reads. Go never trims that directory, so make fuzz-prune drops targets that are gone, entries over 64 KiB, and anything past 1,000 entries a target. A failed search uploads its findings and comments on one standing issue. That job alone may write to issues, only the reporting step holds the token, and the checkout leaves no credential. fuzz.sh reads its targets from the packages ./... names, so a plant under testdata no longer spends the budget or trips the cap, and it searches every target after a failure, so one finding cannot cost the rest of the night. The lint step sets the -trimpath the Makefile exports for every other step, so it can reuse what make vet compiled. Split from 22cd99d, which also shortened minimizing to 1s and the budget of a search run by hand to 5s: those are a change of their own.
go test handles an interrupt and exits, which the search reads as a failed target and carries on from, so Ctrl-C stopped one target of fifteen. Before the search learned to carry on past a failure, set -e stopped it at once. Trap the interrupt so the search exits 130 once go test has gone.
The corpus shares the repository's 10 GiB cache limit with what main saves, at about 1.1 GiB a push, and eviction takes what was used longest ago. Enough pushes in a day lose it, and the search then starts from the seeds, which is what it did before it kept a corpus. That costs depth and nothing else, so it should not fail the run, but it should not pass unnoticed either.
prune decides between keeping a target and rm -rf with printf piped to grep -q. Under pipefail, grep leaving early kills printf, the pipeline reports failure, and a live target reads as gone. The list is small enough today that printf finishes first, but a larger one missed its first line 5 times in 5. A here-string has no writer to kill.
1,000 entries a target was a guess, and five simulated nights refuted it: FuzzCycleDetector reached 908 with its coverage still rising, from 42,394 bits to 48,137, so the cap would have started evicting at random around night six. Its gains fell by about a fifth a night (264, 220, 168, 145, 111), which points to roughly 1,400 entries at that search rate. A search re-runs every entry inside its budget, 9s at 900 entries under load, so the cap cannot rise freely either; 3,000 leaves twice that margin. A target past it now says so in the log.
fuzz.sh takes check, prune or a -fuzztime budget as its one argument. A mistyped mode was handed to go test as a budget, so every target built before rejecting it: chek took 240s to fail. It is now refused at once, with exit 2.
The comment said only the reporting step held the token. setup-go is handed it too, as its token input's default, so that was not so. The job's safety rests on which code it runs: main's, or a writer's dispatch, never a pull request's.
The comment above the fuzz job listed setup-go and the reporting step as the steps handed a token. It left out checkout, whose token input also defaults to github.token, so the list was incomplete. It now names all three, and says that checkout and setup-go get the token as an input's default and the reporting step as GH_TOKEN. The job's safety argument is unchanged: checkout runs before any of the repository's code, and persist-credentials: false keeps the token out of .git/config.
The budget expression refused durations that -fuzztime accepts: a fraction with no integer part (.5s), one with no fraction digits (1.s), and the microsecond unit spelled with either mu sign (5µs, 5μs). The comment above it says the script reads the budget as go test does, so a budget go test takes was refused before any target ran. The expression now accepts a count (100x) or one or more durations, each a number with an optional fraction (5, 5., 5.5 or .5) and a unit of ns, us, µs, μs, ms, s, m or h. It refuses a bare number, a sign, a lone point and a count with a fraction (1.5x). 0 stays refused although go test reads it, as the expression adopted in review does.
go test refuses -fuzztime=0 and every other zero budget (0s, 0x, 0.0s). The previous expression accepted all of them except 0, so each target built only to be refused. The script now refuses every zero budget, as go test does, and no longer claims that go test reads 0: it does not. A leading plus, which both time.ParseDuration and strconv.ParseInt accept, is now accepted too. A value out of range, too large or below a nanosecond, is left to go test to refuse, as the comment now says: telling it apart takes arithmetic, not a pattern.
Only the fuzz search job runs -fuzz, and it saves its corpus under its own key, so nothing the checks and suite jobs save on main holds one. The step that removed $GOCACHE/fuzz before the checks job's save therefore removes nothing: a fresh cache stays without the directory after fuzz-check, bench-smoke and every target's seeds run. Three comments in the workflow's header said otherwise and now say what holds: that the checks job runs go test only with -bench and the search job only with -fuzz, neither of which go help test lists as cacheable; that a run on main only saves; and that the corpus never enters those saves, being bounded by make fuzz-prune in the job that writes it. The comment that the corpus had no cache of its own yet goes with the step.
Two fuzz targets' docs said the gate's search is bounded to seconds. The gate searches nothing now: an ordinary go test runs the seeds, and scripts/fuzz.sh runs the search.
This was referenced Nov 2, 2026
fuad-daoud
marked this pull request as ready for review
November 2, 2026 23:03
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every pull request spends a median 231s on a fuzz search (the 79 runs in #1273) that restarts from the seeds each time, so the search never adds up. This moves it to a nightly job that keeps its corpus.
Stacked on #1373 (#1272), which is stacked on #1311: all three change the same
gate.ymlandMakefilelines. The diff here shows only #1273's changes; once those two merge, this retargets tomain.This carries the nightly search from #1301 (Omar's), as its review on that pull request proposed.
The search leaves pull requests
make fuzz-checkreplaces the search in the gate. It holds the targets to the cap, the quarantine to real targets and the list to at least one searchable target, and searches nothing. Every target's seeds still run ingo test ./.... Fuzzing leavesmake gate.fuzz search, runs on a schedule or by hand and never beside the other two. It has its own concurrency group, since a scheduled run carriesmain's sha and would otherwise sharegate-<sha>withmain's push run. At 1m a target it stays inside the 30-minute job timeout at the target cap: 16 × 1m of search, plus a 6m26 cold build measured on 4 pinned cores (./scripts/fuzz.sh 1xon a freshGOCACHE), is about 22m26, against the search step's 25 and the job's 30. Minimizing adds nothing to that:-fuzztime's deadline cancels it too (go1.27internal/fuzz/fuzz.go: the workers' context is a child of the-fuzztimetimeout), so a found crash cannot lengthen a target's search.$GOCACHE/fuzzalone, under afuzz-corpus-key no other job restores. Go never trims that directory, somake fuzz-prunedrops targets that no longer exist and entries over 64 KiB, and keeps 3,000 entries a target (the cap sits above where a target still builds on its corpus, sinceFuzzCycleDetectorreached 908 entries in five simulated nights with its coverage still rising, and below where re-running every entry, which happens inside the search budget, would leave little time to search). With 3,001 entries for one target on a freshGOCACHE,./scripts/fuzz.sh pruneprintsFuzzDecimal_OrdersAsRationalsDo is past 3000 entries; dropped 1andcorpus: 12000 kB in 3000 entries. A deliberate exception to "the cache never grows" (ci: run the gate as two jobs and drop the Windows and wasm ports #1025, ci: carry main's Go build cache into every run, update actions #755).issues: write, and it runs onlymain's code or a writer's dispatch, never a pull request's. The checkout leaves no credential in.git/configand the search step's environment holds no token, thoughactions/checkoutandsetup-go, each as itstokeninput's default, and the reporting step are handed one.fuzz.shreads its targets from the packages./...names, so a plant undertestdatano longer spends the budget or trips the cap. It also searches every target after a failure, stops at an interrupt, and refuses an argument that is neithercheck,prune, nor a duration or count above zero asgo testreads-fuzztime(a sign allowed), before anything builds.GOFLAGS: -trimpath, as theMakefiledoes for every other step, so it reuses whatmake vetcompiled (ci: move the fuzz search from pull requests to a nightly run #1273's last item). One run each: the step took 219s on the base's run 41489972604 and 53s on this head's run 41537851553.lint, fuzz, benchcheck is nowlint, bench. No ruleset requires either name:gh api repos/dexpace/morphic/rulesets/19179519 --jq '[.rules[].type]'givesdeletion,non_fast_forward,pull_requestandcode_quality, andmain's branch protection lists no required checks.mainno longer drops the fuzz corpus. The step that did is gone, and so are the four comments it made false: only the fuzz search job runs-fuzz, and it saves its corpus under its own key, so nothingmainsaves holds one. On a freshGOCACHE,fuzz.sh check,make bench-smokeandgo test ./... -run '^Fuzz' -count=1each leave no$GOCACHE/fuzz, and a 3s-fuzzrun, the control, does. Twoirtargets' docs that said the gate searches now sayscripts/fuzz.shdoes.-fuzzminimizetime(left at 10s here) is the pull request stacked on this one, for #1290, and the fiveirand threee2etarget moves are #1381.Decisions worth reading
mainand waits for the night, where a 10s search on the pull request might have caught it. ci: cut a pull request's gate from a 9-minute median to about 3.5 #1268 weighs that trade.mainsaves, and eviction takes what was used longest ago. Today the caches already total 11.5 GB (gh api repos/dexpace/morphic/actions/cache/usage), and one push tomainsaves about 1.1 GiB, so roughly nine pushes after a night's save evict its corpus. Of the last 21 days, counted from 02:17 UTC, 7 had nine or more pushes tomain. So on that history about one night in three would restore nothing and search from the seeds: it warns and carries on, but those nights lose the depth this change is for.main's saves may shrink once they hold no fuzz-instrumented builds, which is not measured. ci: measure whether the nightly fuzz corpus survives the cache limit #1383 tracks measuring it after merge and keeping the corpus out of eviction's reach if it holds.Authorship
Omar's commits are carried with him as author:
55f19d31,3cacbf48,b124b961,76556497,3860708c,5849a029,4675381e.55f19d31is the first part of his22cd99d0, split in two so its-fuzzminimizetime=1sand 5s-by-hand part could be its own pull request, the one stacked on this. The split was built on a line at22cd99d0's parent, where the two parts applied in order give22cd99d0's tree exactly.git range-diffagainst his originals shows=fore8b51544,4187fe0f,dc456509and5a11c04e;c6e0265dand967c30d8differ in one context line each, theminimize_timeandFUZZTIMEdefaults, which read10shere; the split part differs where its hunks reapply onto #1272's header. Our commits on top: a budget grammar fix (ace28bc3,ae17963c), the checkout-token comment (bc28ebb2), this change's two.Before and after
flowchart LR subgraph before["Before"] direction TB b1["every pull request"] --> b2["make gate searches 15 targets × 10s<br/>median 231s of each run"] b2 --> b3["each search restarts from the seeds<br/>nothing carries to the next"] end subgraph after["After"] direction TB a1["every pull request"] --> a2["make fuzz-check: caps and quarantine checked,<br/>seeds run in go test, no search"] a3["nightly or by hand"] --> a4["fuzz search job: 1m a target"] a4 --> a5["corpus restored and saved under its own key,<br/>pruned to 3,000 entries a target"] a4 -. "on failure" .-> a6["findings uploaded, comment on #1296"] end before ~~~ afterWhat it brings: pull requests stop paying about four minutes for a search that never builds on itself, and the search runs nightly for a minute a target on a corpus that grows from night to night.
Test plan
GOCACHEleaves nofuzzdirectory afterscripts/fuzz.sh check,make bench-smokeorgo test ./... -run '^Fuzz' -count=1, and keeps one after a 3s-fuzzrun../scripts/fuzz.sh 1xsearches all 15 targets and exits 0, in 6m26 on a cold cache.git grep -nE 'drop-fuzz|drop the fuzz corpus|only with -fuzz and -bench|drops \$GOCACHE/fuzz|dropped before the save|no cache of its own yet' -- .githubandgit grep -nE "gate's (per-target )?search|bounded to seconds"print nothing;actionlintis clean;go test ./internal/archtest ./ir/... ./internal/harnesspasses.push, at 3s a target: run 41354481475, a planted crash, searched all 15 targets past it, uploaded the artifact, commented with the reproducer and saved the corpus after the failure; run 41354927259 restored that corpus and was green; run 41355384470, a broken build, commented that no reproducer was written. Those runs predate the restore warning and the token comment. In 41354481475 the search step took 244s for 15 × 3s, so CI's cold build was about 200s, under the 6m26 measured locally.scheduleandworkflow_dispatchtriggers, and the concurrency group they select, have not run. They need the workflow on the default branch. After merging,gh workflow run gate.ymlexercises them without waiting for the night, and the job's comment on ci: report each failed nightly fuzz search here #1296 with them.make gate: this pull request's CI runs it on this head.Crossings with the other two pull requests
./compilers/openapi/e2e; test: move fuzz targets to packages that link no test tooling #1381 changes only the path, to./compilers/openapi/e2e/fuzz. Whichever lands second keeps this text with that path.irfuzz targets' doc lines: edited here inir/bigval_property_test.goandir/naming_property_test.go; test: move fuzz targets to packages that link no test tooling #1381 writes the same lines inir/fuzz/bigval_test.goandir/fuzz/naming_test.go, so the second merge resolves to the same text.fuzz.sh prunedrops a target that is gone from its old import path, so the first night after test: move fuzz targets to packages that link no test tooling #1381 lands starts those eight from their seeds. Landing test: move fuzz targets to packages that link no test tooling #1381 first, before this saves a corpus, avoids that.Closes #1273
Related: #1272, #1311, #1296, #466, #1290, #1301
Out of scope: nothing on a pull request runs
fuzz.sh pruneor the search, so a defect in either first shows on a night. #1384 tracks afuzz-checkself-test that prunes a fixture cache in$TMPDIRand asserts what it prints.