[{"data":1,"prerenderedAt":1091},["ShallowReactive",2],{"page-\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Frunning-fuzz-targets-in-ci-with-time-budgets\u002F":3},{"id":4,"title":5,"body":6,"description":1057,"extension":1058,"meta":1059,"navigation":681,"path":1087,"seo":1088,"stem":1089,"__hash__":1090},"content\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Frunning-fuzz-targets-in-ci-with-time-budgets\u002Findex.md","Running Fuzz Targets in CI with Time Budgets",{"type":7,"value":8,"toc":1046},"minimark",[9,13,18,34,38,50,79,85,360,375,506,510,521,648,652,655,722,725,728,737,740,744,747,753,763,766,770,773,833,836,847,850,854,898,971,975,981,987,997,1003,1007,1036,1042],[10,11,12],"p",{},"A fuzzer with no budget runs forever, which is why fuzzing so often stops at the experiment stage. The fix is to run two different things: a short, deterministic regression pass on every pull request, and a long discovery run on a schedule. They share a target and a corpus, and nothing else.",[14,15,17],"h2",{"id":16},"prerequisites","Prerequisites",[19,20,21,31],"ul",{},[22,23,24,25,30],"li",{},"A working target as built in ",[26,27,29],"a",{"href":28},"\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Fwriting-your-first-atheris-fuzz-target\u002F","writing your first Atheris fuzz target",".",[22,32,33],{},"A CI system with caching and scheduled jobs.",[14,35,37],{"id":36},"solution","Solution",[10,39,40,44,45,49],{},[41,42,43],"strong",{},"The pull-request run"," replays the corpus and stops. ",[46,47,48],"code",{},"-runs=0"," executes each corpus entry exactly once and exits, which is the cheapest possible proof that no known-bad input crashes:",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-console shiki shiki-themes github-light github-dark","$ python fuzz_decode.py corpus\u002F -runs=0 -print_final_stats=1\nRunning 37 inputs 1 time(s) each.\nDone 37 runs in 0 second(s)\n","console","",[46,58,59,67,73],{"__ignoreMap":56},[60,61,64],"span",{"class":62,"line":63},"line",1,[60,65,66],{},"$ python fuzz_decode.py corpus\u002F -runs=0 -print_final_stats=1\n",[60,68,70],{"class":62,"line":69},2,[60,71,72],{},"Running 37 inputs 1 time(s) each.\n",[60,74,76],{"class":62,"line":75},3,[60,77,78],{},"Done 37 runs in 0 second(s)\n",[10,80,81,84],{},[41,82,83],{},"The scheduled run"," searches. Give it a real budget, and keep whatever it learns:",[51,86,90],{"className":87,"code":88,"language":89,"meta":56,"style":56},"language-yaml shiki shiki-themes github-light github-dark","name: fuzz\non:\n  schedule: [{ cron: \"0 3 * * *\" }]\njobs:\n  discover:\n    runs-on: ubuntu-latest\n    timeout-minutes: 90\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fcache@v4\n        with:\n          path: corpus\n          key: fuzz-corpus-${{ github.run_id }}\n          restore-keys: fuzz-corpus-          # reuse the most recent corpus\n      - run: pip install atheris -e .\n      - run: |\n          python fuzz_decode.py corpus\u002F \\\n            -max_total_time=3600 \\\n            -timeout=10 \\\n            -artifact_prefix=artifacts\u002F \\\n            -print_final_stats=1\n      - if: failure()\n        uses: actions\u002Fupload-artifact@v4\n        with: { name: fuzz-crashes, path: artifacts\u002F }\n","yaml",[46,91,92,106,115,134,142,150,161,172,180,194,206,214,225,236,251,264,277,283,289,295,301,307,320,331],{"__ignoreMap":56},[60,93,94,98,102],{"class":62,"line":63},[60,95,97],{"class":96},"s9eBZ","name",[60,99,101],{"class":100},"sVt8B",": ",[60,103,105],{"class":104},"sZZnC","fuzz\n",[60,107,108,112],{"class":62,"line":69},[60,109,111],{"class":110},"sj4cs","on",[60,113,114],{"class":100},":\n",[60,116,117,120,123,126,128,131],{"class":62,"line":75},[60,118,119],{"class":96},"  schedule",[60,121,122],{"class":100},": [{ ",[60,124,125],{"class":96},"cron",[60,127,101],{"class":100},[60,129,130],{"class":104},"\"0 3 * * *\"",[60,132,133],{"class":100}," }]\n",[60,135,137,140],{"class":62,"line":136},4,[60,138,139],{"class":96},"jobs",[60,141,114],{"class":100},[60,143,145,148],{"class":62,"line":144},5,[60,146,147],{"class":96},"  discover",[60,149,114],{"class":100},[60,151,153,156,158],{"class":62,"line":152},6,[60,154,155],{"class":96},"    runs-on",[60,157,101],{"class":100},[60,159,160],{"class":104},"ubuntu-latest\n",[60,162,164,167,169],{"class":62,"line":163},7,[60,165,166],{"class":96},"    timeout-minutes",[60,168,101],{"class":100},[60,170,171],{"class":110},"90\n",[60,173,175,178],{"class":62,"line":174},8,[60,176,177],{"class":96},"    steps",[60,179,114],{"class":100},[60,181,183,186,189,191],{"class":62,"line":182},9,[60,184,185],{"class":100},"      - ",[60,187,188],{"class":96},"uses",[60,190,101],{"class":100},[60,192,193],{"class":104},"actions\u002Fcheckout@v4\n",[60,195,197,199,201,203],{"class":62,"line":196},10,[60,198,185],{"class":100},[60,200,188],{"class":96},[60,202,101],{"class":100},[60,204,205],{"class":104},"actions\u002Fcache@v4\n",[60,207,209,212],{"class":62,"line":208},11,[60,210,211],{"class":96},"        with",[60,213,114],{"class":100},[60,215,217,220,222],{"class":62,"line":216},12,[60,218,219],{"class":96},"          path",[60,221,101],{"class":100},[60,223,224],{"class":104},"corpus\n",[60,226,228,231,233],{"class":62,"line":227},13,[60,229,230],{"class":96},"          key",[60,232,101],{"class":100},[60,234,235],{"class":104},"fuzz-corpus-${{ github.run_id }}\n",[60,237,239,242,244,247],{"class":62,"line":238},14,[60,240,241],{"class":96},"          restore-keys",[60,243,101],{"class":100},[60,245,246],{"class":104},"fuzz-corpus-",[60,248,250],{"class":249},"sJ8bj","          # reuse the most recent corpus\n",[60,252,254,256,259,261],{"class":62,"line":253},15,[60,255,185],{"class":100},[60,257,258],{"class":96},"run",[60,260,101],{"class":100},[60,262,263],{"class":104},"pip install atheris -e .\n",[60,265,267,269,271,273],{"class":62,"line":266},16,[60,268,185],{"class":100},[60,270,258],{"class":96},[60,272,101],{"class":100},[60,274,276],{"class":275},"szBVR","|\n",[60,278,280],{"class":62,"line":279},17,[60,281,282],{"class":104},"          python fuzz_decode.py corpus\u002F \\\n",[60,284,286],{"class":62,"line":285},18,[60,287,288],{"class":104},"            -max_total_time=3600 \\\n",[60,290,292],{"class":62,"line":291},19,[60,293,294],{"class":104},"            -timeout=10 \\\n",[60,296,298],{"class":62,"line":297},20,[60,299,300],{"class":104},"            -artifact_prefix=artifacts\u002F \\\n",[60,302,304],{"class":62,"line":303},21,[60,305,306],{"class":104},"            -print_final_stats=1\n",[60,308,310,312,315,317],{"class":62,"line":309},22,[60,311,185],{"class":100},[60,313,314],{"class":96},"if",[60,316,101],{"class":100},[60,318,319],{"class":104},"failure()\n",[60,321,323,326,328],{"class":62,"line":322},23,[60,324,325],{"class":96},"        uses",[60,327,101],{"class":100},[60,329,330],{"class":104},"actions\u002Fupload-artifact@v4\n",[60,332,334,336,339,341,343,346,349,352,354,357],{"class":62,"line":333},24,[60,335,211],{"class":96},[60,337,338],{"class":100},": { ",[60,340,97],{"class":96},[60,342,101],{"class":100},[60,344,345],{"class":104},"fuzz-crashes",[60,347,348],{"class":100},", ",[60,350,351],{"class":96},"path",[60,353,101],{"class":100},[60,355,356],{"class":104},"artifacts\u002F",[60,358,359],{"class":100}," }\n",[10,361,362,363,366,367,370,371,374],{},"Three flags carry the weight. ",[46,364,365],{},"-max_total_time"," bounds the run so the job cannot hang. ",[46,368,369],{},"-timeout"," turns a single slow input into a reported finding rather than a stalled job — an input that takes ten seconds in a decoder is a denial-of-service bug. And ",[46,372,373],{},"-artifact_prefix"," puts crash files somewhere the upload step can find them; without it they land in the working directory and are lost with the runner.",[376,377,380,502],"figure",{"className":378},[379],"diagram",[381,382,389,390,389,394,389,398,389,406,389,415,389,424,389,430,389,434,389,438,389,443,389,448,389,451,389,454,389,457,389,461,389,464,389,467,389,470,389,474,389,478,389,482,389,485,389,489,389,492,389,495,389,499],"svg",{"viewBox":383,"role":384,"ariaLabelledBy":385,"xmlns":388},"0 0 760 270","img",[386,387],"fuzzruns-t","fuzzruns-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[391,392,393],"title",{"id":386},"Two fuzzing runs, two purposes",[395,396,397],"desc",{"id":387},"A table comparing the pull-request regression run and the scheduled discovery run across their budget, what each proves, whether they block a merge, and what they produce.",[399,400],"rect",{"x":401,"y":401,"width":402,"height":403,"rx":404,"fill":405},"0","760","270","14","#fffdf8",[407,408,393],"text",{"x":409,"y":410,"textAnchor":411,"fontSize":412,"fontWeight":413,"fill":414},"380","30","middle","15","700","#3d405b",[399,416],{"x":417,"y":418,"width":419,"height":420,"rx":421,"fill":422,"stroke":414,"strokeWidth":423},"24","52","712","40","10","#f2cc8f","1.5",[407,425,429],{"x":426,"y":427,"fontSize":428,"fontWeight":413,"fill":414},"38","76","12","Criterion",[407,431,433],{"x":432,"y":427,"textAnchor":411,"fontSize":428,"fontWeight":413,"fill":414},"390","PR regression",[407,435,437],{"x":436,"y":427,"textAnchor":411,"fontSize":428,"fontWeight":413,"fill":414},"620","Scheduled discovery",[399,439],{"x":417,"y":440,"width":419,"height":420,"fill":441,"stroke":414,"strokeWidth":442},"92","#f4f1de","1",[407,444,447],{"x":426,"y":445,"fontSize":446,"fill":414},"116","11.5","Budget",[407,449,450],{"x":432,"y":445,"textAnchor":411,"fontSize":446,"fill":414},"-runs=0, seconds",[407,452,453],{"x":436,"y":445,"textAnchor":411,"fontSize":446,"fill":414},"-max_total_time, hours",[399,455],{"x":417,"y":456,"width":419,"height":420,"fill":405,"stroke":414,"strokeWidth":442},"132",[407,458,460],{"x":426,"y":459,"fontSize":446,"fill":414},"156","Proves",[407,462,463],{"x":432,"y":459,"textAnchor":411,"fontSize":446,"fill":414},"no known crash",[407,465,466],{"x":436,"y":459,"textAnchor":411,"fontSize":446,"fill":414},"searches for new ones",[399,468],{"x":417,"y":469,"width":419,"height":420,"fill":441,"stroke":414,"strokeWidth":442},"172",[407,471,473],{"x":426,"y":472,"fontSize":446,"fill":414},"196","Blocks a merge",[407,475,477],{"x":432,"y":472,"textAnchor":411,"fontSize":446,"fill":476},"#2a5f49","yes",[407,479,481],{"x":436,"y":472,"textAnchor":411,"fontSize":446,"fill":480},"#8f3d22","no",[399,483],{"x":417,"y":484,"width":419,"height":420,"fill":405,"stroke":414,"strokeWidth":442},"212",[407,486,488],{"x":426,"y":487,"fontSize":446,"fill":414},"236","Produces",[407,490,491],{"x":432,"y":487,"textAnchor":411,"fontSize":446,"fill":414},"a pass or fail",[407,493,494],{"x":436,"y":487,"textAnchor":411,"fontSize":446,"fill":414},"corpus growth and artefacts",[62,496],{"x1":497,"y1":418,"x2":497,"y2":498,"stroke":414,"strokeWidth":442},"274","252",[62,500],{"x1":501,"y1":418,"x2":501,"y2":498,"stroke":414,"strokeWidth":442},"505",[503,504,505],"figcaption",{},"Only the left column belongs on the critical path; conflating the two is why fuzzing gets removed from pipelines.",[14,507,509],{"id":508},"why-this-works","Why this works",[10,511,512,513,515,516,520],{},"libFuzzer's corpus is a set of inputs, each of which reached some coverage the others did not. Replaying it is therefore a coverage-targeted regression suite that was assembled automatically — every entry exists because it exercised something new. Running it with ",[46,514,48],{}," is a deterministic pass over that set, with no mutation and no randomness, which is exactly the property a merge gate needs. The scheduled run does the opposite: it mutates, discovers and grows the corpus, and its output is the ",[517,518,519],"em",{},"next"," regression suite rather than a verdict on this change.",[376,522,524,645],{"className":523},[379],[381,525,389,530,389,533,389,536,389,539,389,541,389,546,389,553,389,559,389,564,389,569,389,574,389,581,389,584,389,588,389,593,389,597,389,601,389,606,389,610,389,613,389,617,389,620,389,623,389,626,389,629,389,632,389,636,389,639,389,642],{"viewBox":526,"role":384,"ariaLabelledBy":527,"xmlns":388},"0 0 760 268",[528,529],"fuzzcadence-t","fuzzcadence-d",[391,531,532],{"id":528},"How the two runs feed each other",[395,534,535],{"id":529},"A timeline showing the cadence: a pull request replays the corpus in seconds, the nightly run searches for an hour and grows the corpus, a crash is converted into a committed regression test, and subsequent pull requests replay the larger corpus.",[399,537],{"x":401,"y":401,"width":402,"height":538,"rx":404,"fill":405},"268",[407,540,532],{"x":409,"y":410,"textAnchor":411,"fontSize":412,"fontWeight":413,"fill":414},[62,542],{"x1":420,"y1":543,"x2":544,"y2":543,"stroke":414,"strokeWidth":545},"150","720","2",[399,547],{"x":548,"y":549,"width":421,"height":550,"rx":551,"fill":552},"35","136","28","3","#81b29a",[399,554],{"x":428,"y":555,"width":556,"height":557,"rx":421,"fill":405,"stroke":552,"strokeWidth":558},"50","209","72","1.8",[407,560,563],{"x":445,"y":561,"textAnchor":411,"fontSize":562,"fontWeight":413,"fill":414},"74","11","per PR",[407,565,568],{"x":445,"y":566,"textAnchor":411,"fontSize":567,"fontWeight":413,"fill":414},"89","12.5","replay corpus",[407,570,573],{"x":445,"y":571,"textAnchor":411,"fontSize":572,"fill":414},"106","10.5","seconds, blocking",[62,575],{"x1":420,"y1":576,"x2":420,"y2":577,"stroke":552,"strokeWidth":578,"strokeDashArray":579},"124","134","1.4",[580,551],"4",[399,582],{"x":583,"y":549,"width":421,"height":550,"rx":551,"fill":422},"262",[399,585],{"x":586,"y":587,"width":556,"height":557,"rx":421,"fill":405,"stroke":422,"strokeWidth":558},"162","176",[407,589,592],{"x":590,"y":591,"textAnchor":411,"fontSize":562,"fontWeight":413,"fill":414},"267","200","nightly",[407,594,596],{"x":590,"y":595,"textAnchor":411,"fontSize":567,"fontWeight":413,"fill":414},"215","search",[407,598,600],{"x":590,"y":599,"textAnchor":411,"fontSize":572,"fill":414},"232","an hour, non-blocking",[62,602],{"x1":590,"y1":603,"x2":590,"y2":604,"stroke":422,"strokeWidth":578,"strokeDashArray":605},"166","174",[580,551],[399,607],{"x":608,"y":549,"width":421,"height":550,"rx":551,"fill":609},"488","#e07a5f",[399,611],{"x":612,"y":555,"width":556,"height":557,"rx":421,"fill":405,"stroke":609,"strokeWidth":558},"389",[407,614,616],{"x":615,"y":561,"textAnchor":411,"fontSize":562,"fontWeight":413,"fill":414},"493","on a crash",[407,618,619],{"x":615,"y":566,"textAnchor":411,"fontSize":567,"fontWeight":413,"fill":414},"commit the input",[407,621,622],{"x":615,"y":571,"textAnchor":411,"fontSize":572,"fill":414},"becomes a pytest case",[62,624],{"x1":615,"y1":576,"x2":615,"y2":577,"stroke":609,"strokeWidth":578,"strokeDashArray":625},[580,551],[399,627],{"x":628,"y":549,"width":421,"height":550,"rx":551,"fill":414},"715",[399,630],{"x":631,"y":587,"width":556,"height":557,"rx":421,"fill":405,"stroke":414,"strokeWidth":558},"539",[407,633,635],{"x":634,"y":591,"textAnchor":411,"fontSize":562,"fontWeight":413,"fill":414},"644","next PR",[407,637,638],{"x":634,"y":595,"textAnchor":411,"fontSize":567,"fontWeight":413,"fill":414},"replay, larger",[407,640,641],{"x":634,"y":599,"textAnchor":411,"fontSize":572,"fill":414},"the finding stays found",[62,643],{"x1":544,"y1":603,"x2":544,"y2":604,"stroke":414,"strokeWidth":578,"strokeDashArray":644},[580,551],[503,646,647],{},"The loop only closes if crashes become committed tests: a corpus in a cache is not a regression suite anyone can rely on.",[14,649,651],{"id":650},"turning-a-crash-into-a-test","Turning a crash into a test",[10,653,654],{},"A crash artefact is bytes. Committing it and adding a five-line pytest case makes the finding permanent, reviewable, and independent of the fuzzing infrastructure:",[51,656,660],{"className":657,"code":658,"language":659,"meta":56,"style":56},"language-python shiki shiki-themes github-light github-dark","import pathlib\nimport pytest\nfrom myapp.wire import decode_frame, FrameError\n\nCRASHES = sorted((pathlib.Path(__file__).parent \u002F \"crashes\").glob(\"*.bin\"))\n\n@pytest.mark.parametrize(\"path\", CRASHES, ids=[p.stem for p in CRASHES])\ndef test_known_crashers_are_rejected_cleanly(path):\n    data = path.read_bytes()\n    # The contract: malformed input raises FrameError and nothing else.\n    with pytest.raises(FrameError):\n        decode_frame(data[0] if data else 0, data[1:])\n","python",[46,661,662,667,672,677,683,688,692,697,702,707,712,717],{"__ignoreMap":56},[60,663,664],{"class":62,"line":63},[60,665,666],{},"import pathlib\n",[60,668,669],{"class":62,"line":69},[60,670,671],{},"import pytest\n",[60,673,674],{"class":62,"line":75},[60,675,676],{},"from myapp.wire import decode_frame, FrameError\n",[60,678,679],{"class":62,"line":136},[60,680,682],{"emptyLinePlaceholder":681},true,"\n",[60,684,685],{"class":62,"line":144},[60,686,687],{},"CRASHES = sorted((pathlib.Path(__file__).parent \u002F \"crashes\").glob(\"*.bin\"))\n",[60,689,690],{"class":62,"line":152},[60,691,682],{"emptyLinePlaceholder":681},[60,693,694],{"class":62,"line":163},[60,695,696],{},"@pytest.mark.parametrize(\"path\", CRASHES, ids=[p.stem for p in CRASHES])\n",[60,698,699],{"class":62,"line":174},[60,700,701],{},"def test_known_crashers_are_rejected_cleanly(path):\n",[60,703,704],{"class":62,"line":182},[60,705,706],{},"    data = path.read_bytes()\n",[60,708,709],{"class":62,"line":196},[60,710,711],{},"    # The contract: malformed input raises FrameError and nothing else.\n",[60,713,714],{"class":62,"line":208},[60,715,716],{},"    with pytest.raises(FrameError):\n",[60,718,719],{"class":62,"line":216},[60,720,721],{},"        decode_frame(data[0] if data else 0, data[1:])\n",[10,723,724],{},"This test runs in the normal suite, needs no Atheris, and states the contract explicitly. It also fails for the right reason if a refactor reintroduces the bug — with the original bytes, not a regenerated approximation.",[10,726,727],{},"Keep the crash directory small by minimising each artefact first. libFuzzer will do it:",[51,729,731],{"className":53,"code":730,"language":55,"meta":56,"style":56},"$ python fuzz_decode.py -minimize_crash=1 -runs=10000 artifacts\u002Fcrash-8f14e45f\n",[46,732,733],{"__ignoreMap":56},[60,734,735],{"class":62,"line":63},[60,736,730],{},[10,738,739],{},"A minimised input is usually a few bytes, which makes the committed fixture readable and the failure obvious.",[14,741,743],{"id":742},"budgeting-across-several-targets","Budgeting across several targets",[10,745,746],{},"Once there are five targets, the scheduled job needs a policy or it spends its whole budget on the first one. Two arrangements work.",[10,748,749,752],{},[41,750,751],{},"Round-robin with equal shares"," is simplest: divide the total budget by the number of targets and run each in turn. It is fair and it under-serves the targets that are still finding things.",[10,754,755,758,759,762],{},[41,756,757],{},"Coverage-weighted"," gives more time to targets whose coverage is still growing, which is where new bugs are. A short probe run reports ",[46,760,761],{},"cov:",", and the scheduler allocates proportionally to the growth since the last run. This is more code than most teams need, but the underlying signal is worth watching manually: a target whose coverage has been flat for a month is either exhausted or blocked, and both deserve investigation rather than more hours.",[10,764,765],{},"Whichever policy, log what each target received and found. A fuzzing job whose output nobody reads is a job that will be deleted in the next pipeline cleanup, and the log line — \"decode: 1h, 0 new crashes, cov 412 → 418\" — is what justifies its cost.",[14,767,769],{"id":768},"reporting-that-keeps-the-programme-alive","Reporting that keeps the programme alive",[10,771,772],{},"A fuzzing job produces no output when it finds nothing, which is most of the time — and a job that is silent for six weeks is a job somebody deletes. A three-line summary per run turns invisible work into visible work.",[51,774,776],{"className":53,"code":775,"language":55,"meta":56,"style":56},"$ python fuzz_decode.py corpus\u002F -max_total_time=3600 -print_final_stats=1 \\\n    | tee run.log\n$ python - \u003C\u003C'EOF' >> summary.md\nimport re, pathlib\nlog = pathlib.Path(\"run.log\").read_text()\ncov = re.findall(r\"cov: (\\d+)\", log)\nexecs = re.search(r\"stat::number_of_executed_units:\\s+(\\d+)\", log)\nprint(f\"- decode: {execs.group(1) if execs else '?'} execs, \"\n      f\"coverage {cov[0] if cov else '?'} → {cov[-1] if cov else '?'}, \"\n      f\"{len(list(pathlib.Path('artifacts').glob('crash-*')))} crash(es)\")\nEOF\n",[46,777,778,783,788,793,798,803,808,813,818,823,828],{"__ignoreMap":56},[60,779,780],{"class":62,"line":63},[60,781,782],{},"$ python fuzz_decode.py corpus\u002F -max_total_time=3600 -print_final_stats=1 \\\n",[60,784,785],{"class":62,"line":69},[60,786,787],{},"    | tee run.log\n",[60,789,790],{"class":62,"line":75},[60,791,792],{},"$ python - \u003C\u003C'EOF' >> summary.md\n",[60,794,795],{"class":62,"line":136},[60,796,797],{},"import re, pathlib\n",[60,799,800],{"class":62,"line":144},[60,801,802],{},"log = pathlib.Path(\"run.log\").read_text()\n",[60,804,805],{"class":62,"line":152},[60,806,807],{},"cov = re.findall(r\"cov: (\\d+)\", log)\n",[60,809,810],{"class":62,"line":163},[60,811,812],{},"execs = re.search(r\"stat::number_of_executed_units:\\s+(\\d+)\", log)\n",[60,814,815],{"class":62,"line":174},[60,816,817],{},"print(f\"- decode: {execs.group(1) if execs else '?'} execs, \"\n",[60,819,820],{"class":62,"line":182},[60,821,822],{},"      f\"coverage {cov[0] if cov else '?'} → {cov[-1] if cov else '?'}, \"\n",[60,824,825],{"class":62,"line":196},[60,826,827],{},"      f\"{len(list(pathlib.Path('artifacts').glob('crash-*')))} crash(es)\")\n",[60,829,830],{"class":62,"line":208},[60,831,832],{},"EOF\n",[10,834,835],{},"Posted to a channel or appended to a dashboard, that line answers the only questions anyone asks: is it still running, is it still learning, and did it find anything. Coverage that grew from 412 to 418 over an hour is a healthy target still exploring; coverage flat for three weeks is a target that has either exhausted its input space or is blocked, and both deserve a look.",[10,837,838,839,842,843,846],{},"Track two further numbers over time. ",[41,840,841],{},"Time to first crash"," on a deliberately broken build validates that the pipeline can still detect failures — a fuzzing job that cannot fail is worse than none. And ",[41,844,845],{},"corpus size"," trending flat while coverage is flat confirms exhaustion rather than a stuck fuzzer.",[10,848,849],{},"Finally, give the crashes an owner and a deadline like any other bug. A crash artefact committed as a regression test and left failing is a red build nobody trusts; a crash triaged, fixed and pinned is the outcome that justifies the whole programme.",[14,851,853],{"id":852},"edge-cases-and-failure-modes","Edge cases and failure modes",[19,855,856,862,868,874,884,892],{},[22,857,858,861],{},[41,859,860],{},"An unbounded run in a pull-request job blocks merges"," and teaches the team to distrust fuzzing; always pass a budget on the critical path.",[22,863,864,867],{},[41,865,866],{},"Cache keys that never rotate accumulate a stale corpus."," Use a run-id key with a prefix restore so the corpus carries forward but can be rebuilt.",[22,869,870,873],{},[41,871,872],{},"Crash artefacts contain real input."," If the fuzzed function processes user data in production, treat artefacts as potentially sensitive before committing them.",[22,875,876,879,880,883],{},[41,877,878],{},"A flaky target produces phantom crashes."," Non-determinism inside ",[46,881,882],{},"TestOneInput"," — a clock, a random seed, a shared cache — makes artefacts unreproducible; seed and isolate exactly as for property tests.",[22,885,886,891],{},[41,887,888,890],{},[46,889,369],{}," interacts with slow CI runners."," A value tuned on a laptop will fire spuriously on a loaded runner; set it well above the worst legitimate input.",[22,893,894,897],{},[41,895,896],{},"Corpus growth is not linear."," A target can stall for hours and then discover a whole subtree; judge productivity over weeks, not runs.\nOne scheduling nuance: run the discovery job at a fixed time and keep its duration constant. Coverage growth is only comparable between runs of equal length, and a job whose budget varies with runner availability produces a trend line nobody can interpret. A fixed hour, a fixed budget and a logged summary make the programme's output a time series rather than a sequence of anecdotes.",[376,899,901,968],{"className":900},[379],[381,902,389,907,389,910,389,913,389,916,389,918,389,921,389,924,389,928,389,933,389,936,389,938,389,941,389,944,389,947,389,949,389,953,389,956,389,959,389,961,389,965],{"viewBox":903,"role":384,"ariaLabelledBy":904,"xmlns":388},"0 0 760 320",[905,906],"fuzzartifacts-t","fuzzartifacts-d",[391,908,909],{"id":905},"What a fuzzing run should leave behind",[395,911,912],{"id":906},"A stack of four artefacts from a scheduled fuzzing run: the merged corpus, any crash inputs, the run log with statistics, and a one-line summary for humans.",[399,914],{"x":401,"y":401,"width":402,"height":915,"rx":404,"fill":405},"320",[407,917,909],{"x":409,"y":410,"textAnchor":411,"fontSize":412,"fontWeight":413,"fill":414},[399,919],{"x":410,"y":920,"width":413,"height":418,"rx":421,"fill":441,"stroke":552,"strokeWidth":558},"56",[399,922],{"x":410,"y":920,"width":923,"height":418,"rx":580,"fill":552},"8",[407,925,927],{"x":418,"y":926,"fontSize":567,"fontWeight":413,"fill":414},"86","the merged corpus",[407,929,932],{"x":930,"y":926,"textAnchor":931,"fontSize":562,"fill":414},"714","end","cached, and the input to the next run",[399,934],{"x":410,"y":935,"width":413,"height":418,"rx":421,"fill":405,"stroke":609,"strokeWidth":558},"120",[399,937],{"x":410,"y":935,"width":923,"height":418,"rx":580,"fill":609},[407,939,940],{"x":418,"y":543,"fontSize":567,"fontWeight":413,"fill":414},"crash inputs",[407,942,943],{"x":930,"y":543,"textAnchor":931,"fontSize":562,"fill":414},"uploaded, then committed as regression fixtures",[399,945],{"x":410,"y":946,"width":413,"height":418,"rx":421,"fill":441,"stroke":422,"strokeWidth":558},"184",[399,948],{"x":410,"y":946,"width":923,"height":418,"rx":580,"fill":422},[407,950,952],{"x":418,"y":951,"fontSize":567,"fontWeight":413,"fill":414},"214","the run log",[407,954,955],{"x":930,"y":951,"textAnchor":931,"fontSize":562,"fill":414},"coverage and execution statistics over time",[399,957],{"x":410,"y":958,"width":413,"height":418,"rx":421,"fill":405,"stroke":414,"strokeWidth":558},"248",[399,960],{"x":410,"y":958,"width":923,"height":418,"rx":580,"fill":414},[407,962,964],{"x":418,"y":963,"fontSize":567,"fontWeight":413,"fill":414},"278","a one-line summary",[407,966,967],{"x":930,"y":963,"textAnchor":931,"fontSize":562,"fill":414},"the only part anyone reads routinely",[503,969,970],{},"The last row is what keeps the first three funded: a silent job is a job that gets deleted.",[14,972,974],{"id":973},"frequently-asked-questions","Frequently Asked Questions",[10,976,977,980],{},[41,978,979],{},"Should fuzzing block a pull request?","\nOnly the regression run should. A short run over the stored corpus proves no known input crashes and finishes in under a minute; discovery runs belong on a schedule where a multi-hour budget is acceptable.",[10,982,983,986],{},[41,984,985],{},"Where should the corpus live?","\nIn CI cache for the working copy and in the repository for a small curated seed set. Caching keeps the accumulated coverage between runs; committing the seeds makes a fresh clone productive immediately.",[10,988,989,992,993,996],{},[41,990,991],{},"What do I do with a crash artefact?","\nCommit it as a regression fixture and add a pytest case that feeds it to the same function. The fuzzer found it once; the test keeps it found.\n",[41,994,995],{},"Can the regression run replace an integration test?","\nNo, and it should not try. Replaying the corpus proves that a set of historically interesting inputs no longer crashes; it says nothing about correct output, because the oracle is only 'does not raise'. Keep the corpus replay as a robustness gate and let the ordinary test suite carry the behavioural assertions.",[10,998,999,1002],{},[41,1000,1001],{},"What if the corpus cache is lost?","\nThe next run rebuilds from the committed seeds, more slowly and with less coverage until it catches up. That is survivable, which is exactly why the seeds belong in the repository — a programme whose entire state lives in a CI cache is one cache eviction away from starting over.",[14,1004,1006],{"id":1005},"related","Related",[19,1008,1009,1016,1022,1029],{},[22,1010,1011,1015],{},[26,1012,1014],{"href":1013},"\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002F","Coverage-guided fuzzing with Atheris"," — the search loop this schedule feeds.",[22,1017,1018,1021],{},[26,1019,1020],{"href":28},"Writing your first Atheris fuzz target"," — the target being budgeted.",[22,1023,1024,1028],{},[26,1025,1027],{"href":1026},"\u002Fadvanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002F","Coverage measurement and enforcement"," — the other coverage signal in the pipeline, and how it is gated.",[22,1030,1031,1035],{},[26,1032,1034],{"href":1033},"\u002Fproperty-based-fuzz-testing-strategies\u002Fhypothesis-framework-fundamentals\u002Freproducing-failures-with-example-and-the-database\u002F","Reproducing Hypothesis failures with @example"," — the same convert-the-finding discipline on the property-testing side.",[10,1037,1038,1039],{},"← Back to ",[26,1040,1041],{"href":1013},"Coverage-Guided Fuzzing with Atheris",[1043,1044,1045],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":56,"searchDepth":69,"depth":69,"links":1047},[1048,1049,1050,1051,1052,1053,1054,1055,1056],{"id":16,"depth":69,"text":17},{"id":36,"depth":69,"text":37},{"id":508,"depth":69,"text":509},{"id":650,"depth":69,"text":651},{"id":742,"depth":69,"text":743},{"id":768,"depth":69,"text":769},{"id":852,"depth":69,"text":853},{"id":973,"depth":69,"text":974},{"id":1005,"depth":69,"text":1006},"Run Atheris in CI without blocking merges: short regression runs against a cached corpus, scheduled discovery runs, crash artefacts, and turning findings into pytest cases.","md",{"slug":1060,"type":1061,"breadcrumb":1062,"datePublished":1063,"dateModified":1063,"faq":1064,"howto":1071},"running-fuzz-targets-in-ci-with-time-budgets","article","Fuzzing in CI","2026-08-01",[1065,1067,1069],{"q":979,"a":1066},"Only the regression run should. A short run over the stored corpus proves no known input crashes and finishes in under a minute; discovery runs belong on a schedule where a multi-hour budget is acceptable.",{"q":985,"a":1068},"In CI cache for the working copy and in the repository for a small curated seed set. Caching keeps the accumulated coverage between runs; committing the seeds makes a fresh clone productive immediately.",{"q":991,"a":1070},"Commit it as a regression fixture and add a pytest case that feeds it to the same function. The fuzzer found it once; the test keeps it found.",{"name":1072,"description":1073,"steps":1074},"How to run Atheris fuzz targets in CI","Bound fuzzing runs, keep the corpus, and convert crashes into permanent tests.",[1075,1078,1081,1084],{"name":1076,"text":1077},"Run a bounded regression pass on every pull request","Replay the stored corpus with a short total-time budget so merges are not blocked.",{"name":1079,"text":1080},"Cache the corpus between runs","Persist the working corpus so accumulated coverage is not lost each build.",{"name":1082,"text":1083},"Schedule a long discovery run","Give the fuzzer hours on a timer, with artefacts uploaded on failure.",{"name":1085,"text":1086},"Convert each crash into a pytest regression","Commit the artefact and assert the documented exception instead of a crash.","\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Frunning-fuzz-targets-in-ci-with-time-budgets",{"title":5,"description":1057},"property-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Frunning-fuzz-targets-in-ci-with-time-budgets\u002Findex","gMzGqS2j-KUutrFqWDKQ7PLM5G_xRAQ7ypPm1ZC8A7w",1785613404473]