[{"data":1,"prerenderedAt":960},["ShallowReactive",2],{"page-\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Ffuzzing-a-c-extension-with-atheris-and-asan\u002F":3},{"id":4,"title":5,"body":6,"description":926,"extension":927,"meta":928,"navigation":167,"path":956,"seo":957,"stem":958,"__hash__":959},"content\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Ffuzzing-a-c-extension-with-atheris-and-asan\u002Findex.md","Fuzzing a C Extension with Atheris and ASan",{"type":7,"value":8,"toc":915},"minimark",[9,13,16,21,49,53,201,333,395,538,542,552,562,573,577,580,587,594,677,681,684,691,702,709,766,770,773,783,786,789,792,796,845,849,855,864,873,877,906,911],[10,11,12],"p",{},"Pure Python code cannot corrupt memory. A C extension can, and when it does, the symptoms are rarely a clean crash at the faulty line. A read one byte past a buffer returns whatever happens to be there; a write past the end overwrites a neighbouring object that fails, much later, in unrelated code; a use-after-free works perfectly until the allocator reuses the block. Unit tests pass. Production occasionally segfaults somewhere else.",[10,14,15],{},"Coverage-guided fuzzing with Atheris finds the inputs that reach those paths, and AddressSanitizer makes the errors visible the instant they happen. Together they turn \"occasional segfault in production\" into a report naming the exact line, the size of the overflow, and where the memory was allocated and freed. The setup takes more steps than fuzzing pure Python — the extension must be rebuilt with instrumentation, and the sanitizer runtime must be loaded into an interpreter that was not built with it — but each step is mechanical.",[17,18,20],"h2",{"id":19},"prerequisites","Prerequisites",[22,23,24,37,40],"ul",{},[25,26,27,28,32,33,36],"li",{},"Linux, ",[29,30,31],"code",{},"clang >= 16",", ",[29,34,35],{},"atheris >= 2.3",", Python 3.11 or later.",[25,38,39],{},"An extension built from source with setuptools or scikit-build.",[25,41,42,43,48],{},"The basics from ",[44,45,47],"a",{"href":46},"\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Fwriting-your-first-atheris-fuzz-target\u002F","Writing your first Atheris fuzz target",".",[17,50,52],{"id":51},"solution","Solution",[54,55,60],"pre",{"className":56,"code":57,"language":58,"meta":59,"style":59},"language-bash shiki shiki-themes github-light github-dark","# 1. Build the extension with ASan and libFuzzer coverage instrumentation.\nexport CC=clang CXX=clang++\nexport CFLAGS=\"-fsanitize=address,fuzzer-no-link -g -O1 -fno-omit-frame-pointer\"\nexport CXXFLAGS=\"$CFLAGS\"\nexport LDSHARED=\"clang -shared\"\npip install --no-build-isolation --force-reinstall -e .\n\n# 2. Locate the ASan-enabled libFuzzer runtime shipped with Atheris.\nASAN_LIB=$(python -c \"import atheris; print(atheris.path())\")\u002Fasan_with_fuzzer.so\n","bash","",[29,61,62,71,93,107,126,139,162,169,175],{"__ignoreMap":59},[63,64,67],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"sJ8bj","# 1. Build the extension with ASan and libFuzzer coverage instrumentation.\n",[63,72,74,78,82,85,88,90],{"class":65,"line":73},2,[63,75,77],{"class":76},"szBVR","export",[63,79,81],{"class":80},"sVt8B"," CC",[63,83,84],{"class":76},"=",[63,86,87],{"class":80},"clang CXX",[63,89,84],{"class":76},[63,91,92],{"class":80},"clang++\n",[63,94,96,98,101,103],{"class":65,"line":95},3,[63,97,77],{"class":76},[63,99,100],{"class":80}," CFLAGS",[63,102,84],{"class":76},[63,104,106],{"class":105},"sZZnC","\"-fsanitize=address,fuzzer-no-link -g -O1 -fno-omit-frame-pointer\"\n",[63,108,110,112,115,117,120,123],{"class":65,"line":109},4,[63,111,77],{"class":76},[63,113,114],{"class":80}," CXXFLAGS",[63,116,84],{"class":76},[63,118,119],{"class":105},"\"",[63,121,122],{"class":80},"$CFLAGS",[63,124,125],{"class":105},"\"\n",[63,127,129,131,134,136],{"class":65,"line":128},5,[63,130,77],{"class":76},[63,132,133],{"class":80}," LDSHARED",[63,135,84],{"class":76},[63,137,138],{"class":105},"\"clang -shared\"\n",[63,140,142,146,149,153,156,159],{"class":65,"line":141},6,[63,143,145],{"class":144},"sScJk","pip",[63,147,148],{"class":105}," install",[63,150,152],{"class":151},"sj4cs"," --no-build-isolation",[63,154,155],{"class":151}," --force-reinstall",[63,157,158],{"class":151}," -e",[63,160,161],{"class":105}," .\n",[63,163,165],{"class":65,"line":164},7,[63,166,168],{"emptyLinePlaceholder":167},true,"\n",[63,170,172],{"class":65,"line":171},8,[63,173,174],{"class":69},"# 2. Locate the ASan-enabled libFuzzer runtime shipped with Atheris.\n",[63,176,178,181,183,186,189,192,195,198],{"class":65,"line":177},9,[63,179,180],{"class":80},"ASAN_LIB",[63,182,84],{"class":76},[63,184,185],{"class":80},"$(",[63,187,188],{"class":144},"python",[63,190,191],{"class":151}," -c",[63,193,194],{"class":105}," \"import atheris; print(atheris.path())\"",[63,196,197],{"class":80},")",[63,199,200],{"class":105},"\u002Fasan_with_fuzzer.so\n",[54,202,205],{"className":203,"code":204,"language":188,"meta":59,"style":59},"language-python shiki shiki-themes github-light github-dark","# fuzz_decode.py\nimport sys\nimport atheris\n\nwith atheris.instrument_imports():\n    import fastcodec                      # the C extension under test\n\ndef TestOneInput(data: bytes) -> None:\n    fdp = atheris.FuzzedDataProvider(data)\n    mode = fdp.ConsumeIntInRange(0, 2)\n    payload = fdp.ConsumeBytes(fdp.remaining_bytes())\n    try:\n        if mode == 0:\n            fastcodec.decode(payload)\n        elif mode == 1:\n            fastcodec.decode_stream(payload, chunk=fdp.ConsumeIntInRange(1, 64))\n        else:\n            fastcodec.validate(payload)\n    except fastcodec.DecodeError:\n        pass                              # documented rejection is fine\n\natheris.Setup(sys.argv, TestOneInput)\natheris.Fuzz()\n",[29,206,207,212,217,222,226,231,236,240,245,250,256,262,268,274,280,286,292,298,304,310,316,321,327],{"__ignoreMap":59},[63,208,209],{"class":65,"line":66},[63,210,211],{},"# fuzz_decode.py\n",[63,213,214],{"class":65,"line":73},[63,215,216],{},"import sys\n",[63,218,219],{"class":65,"line":95},[63,220,221],{},"import atheris\n",[63,223,224],{"class":65,"line":109},[63,225,168],{"emptyLinePlaceholder":167},[63,227,228],{"class":65,"line":128},[63,229,230],{},"with atheris.instrument_imports():\n",[63,232,233],{"class":65,"line":141},[63,234,235],{},"    import fastcodec                      # the C extension under test\n",[63,237,238],{"class":65,"line":164},[63,239,168],{"emptyLinePlaceholder":167},[63,241,242],{"class":65,"line":171},[63,243,244],{},"def TestOneInput(data: bytes) -> None:\n",[63,246,247],{"class":65,"line":177},[63,248,249],{},"    fdp = atheris.FuzzedDataProvider(data)\n",[63,251,253],{"class":65,"line":252},10,[63,254,255],{},"    mode = fdp.ConsumeIntInRange(0, 2)\n",[63,257,259],{"class":65,"line":258},11,[63,260,261],{},"    payload = fdp.ConsumeBytes(fdp.remaining_bytes())\n",[63,263,265],{"class":65,"line":264},12,[63,266,267],{},"    try:\n",[63,269,271],{"class":65,"line":270},13,[63,272,273],{},"        if mode == 0:\n",[63,275,277],{"class":65,"line":276},14,[63,278,279],{},"            fastcodec.decode(payload)\n",[63,281,283],{"class":65,"line":282},15,[63,284,285],{},"        elif mode == 1:\n",[63,287,289],{"class":65,"line":288},16,[63,290,291],{},"            fastcodec.decode_stream(payload, chunk=fdp.ConsumeIntInRange(1, 64))\n",[63,293,295],{"class":65,"line":294},17,[63,296,297],{},"        else:\n",[63,299,301],{"class":65,"line":300},18,[63,302,303],{},"            fastcodec.validate(payload)\n",[63,305,307],{"class":65,"line":306},19,[63,308,309],{},"    except fastcodec.DecodeError:\n",[63,311,313],{"class":65,"line":312},20,[63,314,315],{},"        pass                              # documented rejection is fine\n",[63,317,319],{"class":65,"line":318},21,[63,320,168],{"emptyLinePlaceholder":167},[63,322,324],{"class":65,"line":323},22,[63,325,326],{},"atheris.Setup(sys.argv, TestOneInput)\n",[63,328,330],{"class":65,"line":329},23,[63,331,332],{},"atheris.Fuzz()\n",[54,334,336],{"className":56,"code":335,"language":58,"meta":59,"style":59},"# 3. Run with the runtime preloaded and leak detection off.\nLD_PRELOAD=\"$ASAN_LIB\" ASAN_OPTIONS=detect_leaks=0,allocator_may_return_null=1 \\\n  python fuzz_decode.py corpus\u002F -max_total_time=600\n",[29,337,338,343,381],{"__ignoreMap":59},[63,339,340],{"class":65,"line":66},[63,341,342],{"class":69},"# 3. Run with the runtime preloaded and leak detection off.\n",[63,344,345,348,350,352,355,357,360,362,365,367,370,373,375,378],{"class":65,"line":73},[63,346,347],{"class":80},"LD_PRELOAD",[63,349,84],{"class":76},[63,351,119],{"class":105},[63,353,354],{"class":80},"$ASAN_LIB",[63,356,119],{"class":105},[63,358,359],{"class":80}," ASAN_OPTIONS",[63,361,84],{"class":76},[63,363,364],{"class":80},"detect_leaks",[63,366,84],{"class":76},[63,368,369],{"class":105},"0,",[63,371,372],{"class":80},"allocator_may_return_null",[63,374,84],{"class":76},[63,376,377],{"class":105},"1",[63,379,380],{"class":144}," \\\n",[63,382,383,386,389,392],{"class":65,"line":95},[63,384,385],{"class":105},"  python",[63,387,388],{"class":105}," fuzz_decode.py",[63,390,391],{"class":105}," corpus\u002F",[63,393,394],{"class":151}," -max_total_time=600\n",[396,397,400,534],"figure",{"className":398},[399],"diagram",[401,402,409,410,409,414,409,418,409,436,409,444,409,454,409,463,409,469,409,476,409,481,409,486,409,491,409,495,409,498,409,503,409,507,409,510,409,517,409,521,409,529],"svg",{"viewBox":403,"role":404,"ariaLabelledBy":405,"xmlns":408},"0 0 800 246","img",[406,407],"as-t","as-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[411,412,413],"title",{"id":406},"Atheris, the instrumented extension and ASan",[415,416,417],"desc",{"id":407},"Atheris running inside the Python interpreter generates inputs and receives coverage feedback from both instrumented Python code and the C extension, which was compiled with fuzzer-no-link coverage. The extension is also compiled with AddressSanitizer, whose runtime is preloaded into the process, so any out-of-bounds or use-after-free access produces an immediate report.",[419,420,421,422,409],"defs",{},"\n    ",[423,424,431],"marker",{"id":425,"viewBox":426,"refX":427,"refY":428,"markerWidth":429,"markerHeight":429,"orient":430},"as-a","0 0 10 10","9","5","7","auto-start-reverse",[432,433],"path",{"d":434,"fill":435},"M0 0 L10 5 L0 10 z","#81b29a",[437,438],"rect",{"x":439,"y":439,"width":440,"height":441,"rx":442,"fill":443},"0","800","246","14","#fffdf8",[445,446,453],"text",{"x":447,"y":448,"textAnchor":449,"fontSize":450,"fontWeight":451,"fill":452},"400","28","middle","15.5","700","#3d405b","Coverage from both layers, errors caught at the instruction",[437,455],{"x":456,"y":457,"width":458,"height":459,"rx":460,"fill":461,"stroke":462},"26","48","748","178","12","#f4f1de","rgba(61,64,91,0.35)",[445,464,468],{"x":465,"y":466,"fontSize":467,"fill":452},"46","70","11","python process · LD_PRELOAD=asan_with_fuzzer.so",[437,470],{"x":471,"y":472,"width":473,"height":474,"rx":475,"fill":452},"50","90","190","64","10",[445,477,480],{"x":478,"y":479,"textAnchor":449,"fontSize":460,"fontWeight":451,"fill":443},"145","116","Atheris \u002F libFuzzer",[445,482,485],{"x":478,"y":483,"textAnchor":449,"fontSize":484,"fill":443},"136","10.5","mutates inputs",[437,487],{"x":488,"y":472,"width":473,"height":474,"rx":475,"fill":489,"stroke":435,"strokeWidth":490},"310","#e6f0ea","2",[445,492,494],{"x":493,"y":479,"textAnchor":449,"fontSize":460,"fontWeight":451,"fill":452},"405","TestOneInput",[445,496,497],{"x":493,"y":483,"textAnchor":449,"fontSize":484,"fill":452},"instrumented Python",[437,499],{"x":500,"y":472,"width":473,"height":474,"rx":475,"fill":501,"stroke":502,"strokeWidth":490},"566","#fbe9e3","#e07a5f",[445,504,506],{"x":505,"y":479,"textAnchor":449,"fontSize":460,"fontWeight":451,"fill":452},"661","fastcodec.so",[445,508,509],{"x":505,"y":483,"textAnchor":449,"fontSize":484,"fill":452},"ASan + coverage",[65,511],{"x1":512,"y1":513,"x2":514,"y2":513,"stroke":435,"strokeWidth":515,"markerEnd":516},"244","112","306","1.8","url(#as-a)",[65,518],{"x1":519,"y1":513,"x2":520,"y2":513,"stroke":435,"strokeWidth":515,"markerEnd":516},"504","562",[432,522],{"d":523,"fill":524,"stroke":525,"strokeWidth":515,"strokeDashArray":526,"markerEnd":516},"M661 158 C 661 200, 145 200, 145 158","none","#f2cc8f",[527,528],"6","4",[445,530,533],{"x":447,"y":531,"textAnchor":449,"fontSize":484,"fill":532},"200","#8a5a00","edge coverage feeds back into mutation",[535,536,537],"figcaption",{},"Coverage instrumentation guides the fuzzer into the C code's branches; ASan makes every bad memory access in those branches fatal and precise.",[17,539,541],{"id":540},"why-this-works","Why this works",[10,543,544,547,548,551],{},[29,545,546],{},"-fsanitize=fuzzer-no-link"," inserts libFuzzer's coverage callbacks into every branch of the extension without linking a ",[29,549,550],{},"main",", so Atheris receives edge coverage from C as well as from the instrumented Python. That matters because the interesting paths of a decoder — length fields, escape sequences, chunk boundaries — are in C, and without C coverage the fuzzer would be mutating blindly as far as those branches are concerned.",[10,553,554,557,558,561],{},[29,555,556],{},"-fsanitize=address"," adds shadow memory around every allocation and checks every load and store. An access one byte outside a heap buffer, a read from freed memory, a double free — each triggers an immediate abort with a report showing the faulting stack, the allocation stack and, for use-after-free, the free stack. Atheris treats the abort as a crash, writes the input that caused it to ",[29,559,560],{},"crash-\u003Chash>",", and stops.",[10,563,564,565,568,569,572],{},"The preload step exists because the interpreter itself is uninstrumented. The ASan runtime must be initialised before any instrumented code runs, and it must intercept ",[29,566,567],{},"malloc"," and ",[29,570,571],{},"free"," for the whole process; loading it as the first shared library achieves both. Atheris ships a runtime built for exactly this purpose.",[17,574,576],{"id":575},"reading-an-asan-report","Reading an ASan report",[10,578,579],{},"A typical first finding looks like this, trimmed:",[54,581,585],{"className":582,"code":584,"language":445,"meta":59},[583],"language-text","==4121==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000b1\nREAD of size 1 at 0x6020000000b1 thread T0\n    #0 in read_varint fastcodec\u002Fvarint.c:41\n    #1 in decode_field fastcodec\u002Fdecode.c:118\n    #2 in fastcodec_decode fastcodec\u002Fmodule.c:72\n0x6020000000b1 is located 0 bytes after 1-byte region [0x6020000000b0,0x6020000000b1)\nallocated by thread T0 here:\n    #0 in malloc\n    #1 in PyBytes_FromStringAndSize\n",[29,586,584],{"__ignoreMap":59},[10,588,589,590,593],{},"The first line names the error class. The access stack shows where: ",[29,591,592],{},"read_varint"," at line 41 read one byte beyond the input. \"0 bytes after 1-byte region\" says the buffer was one byte long and the read was immediately past it — a varint decoder that did not check for input ending mid-value. The allocation stack confirms the buffer was the Python bytes object passed in. The fix is a bounds check in the loop; the crash input becomes a regression test.",[396,595,597,674],{"className":596},[399],[401,598,409,603,409,606,409,609,409,612,409,615,409,619,409,624,409,629,409,633,409,637,409,641,409,643,409,646,409,649,409,653,409,657,409,660,409,664,409,668,409,671],{"viewBox":599,"role":404,"ariaLabelledBy":600,"xmlns":408},"0 0 800 236",[601,602],"asr-t","asr-d",[411,604,605],{"id":601},"The parts of an ASan report",[415,607,608],{"id":602},"An ASan report is divided into four parts: the error class such as heap-buffer-overflow, the access stack showing where the bad read happened, the region description giving the buffer size and offset of the access, and the allocation stack showing where the buffer came from.",[437,610],{"x":439,"y":439,"width":440,"height":611,"rx":442,"fill":443},"236",[445,613,614],{"x":447,"y":448,"textAnchor":449,"fontSize":450,"fontWeight":451,"fill":452},"Four questions every report answers",[437,616],{"x":456,"y":471,"width":617,"height":618,"rx":467,"fill":501,"stroke":502,"strokeWidth":515},"176","164",[445,620,623],{"x":621,"y":622,"textAnchor":449,"fontSize":460,"fontWeight":451,"fill":452},"114","78","what",[445,625,628],{"x":621,"y":626,"textAnchor":449,"fontSize":484,"fill":627},"108","#8f3d22","heap-buffer-overflow",[445,630,632],{"x":621,"y":631,"textAnchor":449,"fontSize":484,"fill":452},"128","READ of size 1",[437,634],{"x":635,"y":471,"width":617,"height":618,"rx":467,"fill":636,"stroke":525,"strokeWidth":515},"216","#f7f0da",[445,638,640],{"x":639,"y":622,"textAnchor":449,"fontSize":460,"fontWeight":451,"fill":452},"304","where",[445,642,592],{"x":639,"y":626,"textAnchor":449,"fontSize":484,"fill":532},[445,644,645],{"x":639,"y":631,"textAnchor":449,"fontSize":484,"fill":452},"varint.c:41",[437,647],{"x":648,"y":471,"width":617,"height":618,"rx":467,"fill":489,"stroke":435,"strokeWidth":515},"406",[445,650,652],{"x":651,"y":622,"textAnchor":449,"fontSize":460,"fontWeight":451,"fill":452},"494","how far",[445,654,656],{"x":651,"y":626,"textAnchor":449,"fontSize":484,"fill":655},"#2a5f49","0 bytes after",[445,658,659],{"x":651,"y":631,"textAnchor":449,"fontSize":484,"fill":452},"1-byte region",[437,661],{"x":662,"y":471,"width":459,"height":618,"rx":467,"fill":461,"stroke":452,"strokeWidth":663},"596","1.5",[445,665,667],{"x":666,"y":622,"textAnchor":449,"fontSize":460,"fontWeight":451,"fill":452},"685","whose memory",[445,669,670],{"x":666,"y":626,"textAnchor":449,"fontSize":484,"fill":452},"allocated by",[445,672,673],{"x":666,"y":631,"textAnchor":449,"fontSize":484,"fill":452},"PyBytes_From…",[535,675,676],{},"Read the report left to right and the fix is usually obvious: here, a missing end-of-input check in a varint loop.",[17,678,680],{"id":679},"seeding-the-corpus-and-giving-the-fuzzer-a-dictionary","Seeding the corpus and giving the fuzzer a dictionary",[10,682,683],{},"A fuzzer started from an empty corpus spends its first minutes discovering the most basic structure of the format — that a valid message starts with a particular magic byte, that a length prefix must roughly match the payload. For a binary codec that can waste most of a short CI budget. Two cheap inputs shorten that phase dramatically.",[10,685,686,687,690],{},"The first is a seed corpus: a directory of real, valid inputs. Take a few dozen payloads from the codec's own test fixtures, or encode a handful of representative values with the Python-level API, and write each to its own file in ",[29,688,689],{},"corpus\u002F",". libFuzzer starts by running every seed, records the coverage each one reaches, and mutates from there. Valid seeds put the fuzzer past the parser's front door on the first iteration, so mutation time goes into the interesting branches: truncation, oversized lengths, nested structures, unusual tags.",[10,692,693,694,697,698,701],{},"The second is a dictionary: a text file of tokens the format uses, passed with ",[29,695,696],{},"-dict=codec.dict",". Each line is a quoted byte string — magic numbers, tag values, keywords. libFuzzer inserts dictionary entries during mutation, which lets it produce a multi-byte magic value in one step instead of guessing it byte by byte. For text-like formats, the keywords and delimiters belong in the dictionary; for binary formats, the tag bytes and boundary lengths such as ",[29,699,700],{},"\"\\xff\\xff\\xff\\x7f\""," do.",[10,703,704,705,708],{},"After a long run, minimise the corpus with ",[29,706,707],{},"-merge=1"," into a fresh directory. Merging keeps the smallest set of inputs that preserves total coverage, which makes the corpus quick to replay at the start of every future run and small enough to commit or cache as a CI artefact.",[396,710,712,763],{"className":711},[399],[401,713,409,717,409,720,409,723,409,725,409,728,409,733,409,735,409,740,409,746,409,750,409,755,409,758],{"viewBox":599,"role":404,"ariaLabelledBy":714,"xmlns":408},[715,716],"asc-t","asc-d",[411,718,719],{"id":715},"Seeds and a dictionary shorten the warm-up",[415,721,722],{"id":716},"Two curves plot coverage over fuzzing time. Starting from an empty corpus, coverage stays low for a long warm-up before climbing. Starting from valid seeds with a dictionary, coverage jumps immediately and keeps climbing, reaching deeper branches within the same time budget.",[437,724],{"x":439,"y":439,"width":440,"height":611,"rx":442,"fill":443},[445,726,727],{"x":447,"y":448,"textAnchor":449,"fontSize":450,"fontWeight":451,"fill":452},"Coverage over a ten-minute budget",[65,729],{"x1":466,"y1":730,"x2":731,"y2":730,"stroke":452,"strokeWidth":732},"196","760","1.6",[65,734],{"x1":466,"y1":730,"x2":466,"y2":471,"stroke":452,"strokeWidth":732},[445,736,739],{"x":737,"y":738,"textAnchor":449,"fontSize":467,"fill":452},"415","222","time",[445,741,745],{"x":742,"y":743,"textAnchor":449,"fontSize":467,"fill":452,"transform":744},"58","124","rotate(-90 58 124)","edges covered",[432,747],{"d":748,"fill":524,"stroke":502,"strokeWidth":749},"M70 190 C 250 186, 360 170, 760 118","2.6",[445,751,754],{"x":752,"y":753,"fontSize":467,"fill":627},"560","152","empty corpus",[432,756],{"d":757,"fill":524,"stroke":435,"strokeWidth":749},"M70 120 C 140 96, 300 80, 760 58",[445,759,762],{"x":760,"y":761,"fontSize":467,"fill":655},"520","62","seeds + dictionary",[535,764,765],{},"Seeds and a dictionary buy the structural knowledge the fuzzer would otherwise spend most of a short budget discovering.",[17,767,769],{"id":768},"from-crash-file-to-fixed-release","From crash file to fixed release",[10,771,772],{},"A crash is only half a result. The workflow that follows it decides whether the same bug can come back.",[10,774,775,776,779,780,782],{},"First, reproduce outside the fuzzing loop. Passing the crash file as the only argument — ",[29,777,778],{},"python fuzz_decode.py crash-1a2b3c"," with the same ",[29,781,347],{}," — runs just that input and prints the same report. That confirms the crash is deterministic and gives a fast loop for testing the fix.",[10,784,785],{},"Second, write a plain pytest regression test that feeds the exact bytes to the extension. Without ASan, the test may pass even before the fix, because the overflow does not necessarily crash an uninstrumented build. That is why the test belongs in a job that also runs the suite against the sanitizer build — a small matrix entry that builds once with ASan and runs the extension's tests with the runtime preloaded. The regular job still runs the test, which documents the input; the sanitizer job enforces it.",[10,787,788],{},"Third, add the crash input to the seed corpus. Future fuzzing runs then start from it, and mutations of it explore the neighbourhood of the bug, which is where related bugs tend to cluster — a missing bounds check in one field decoder often has siblings in the others.",[10,790,791],{},"Finally, decide whether the bug is a security issue. An out-of-bounds read in a decoder that parses untrusted network input is, and it deserves an advisory and a patched release rather than a quiet fix in the next version.",[17,793,795],{"id":794},"edge-cases-and-failure-modes","Edge cases and failure modes",[22,797,798,809,819,829,835],{},[25,799,800,806,807,48],{},[801,802,803,48],"strong",{},[29,804,805],{},"ASan runtime does not come first in initial library list"," The runtime was not preloaded, or another library was preloaded before it. Put the ASan library first in ",[29,808,347],{},[25,810,811,814,815,818],{},[801,812,813],{},"Flood of leak reports at exit."," CPython does not free everything at shutdown. Set ",[29,816,817],{},"detect_leaks=0"," for fuzzing runs.",[25,820,821,824,825,828],{},[801,822,823],{},"Extension built without coverage."," If only ASan flags were used, the fuzzer finds crashes only by luck. Confirm ",[29,826,827],{},"fuzzer-no-link"," is present in the compile commands printed by the build.",[25,830,831,834],{},[801,832,833],{},"Crashes inside CPython."," A report whose top frames are in the interpreter usually means the extension corrupted an object earlier — a reference count or a buffer it did not own. Look at the allocation and free stacks, not just the access.",[25,836,837,840,841,844],{},[801,838,839],{},"Optimised-away bugs."," High optimisation can remove the faulting read. Build fuzzing binaries at ",[29,842,843],{},"-O1"," with frame pointers, as above.",[17,846,848],{"id":847},"frequently-asked-questions","Frequently Asked Questions",[10,850,851,854],{},[801,852,853],{},"Why do I need AddressSanitizer to fuzz a C extension?","\nWithout it, many memory errors — reading one byte past a buffer, using freed memory — do not crash. They corrupt memory silently and the fuzzer never notices. ASan turns each of those errors into an immediate, precise report at the faulting instruction.",[10,856,857,860,861,863],{},[801,858,859],{},"Why must the ASan runtime be preloaded?","\nThe Python interpreter itself is not built with ASan, so the sanitizer runtime is not loaded when the extension is imported. Preloading the runtime library with ",[29,862,347],{}," makes it available before any instrumented code runs.",[10,865,866,869,870,872],{},[801,867,868],{},"Should I disable ASan leak detection?","\nUsually yes, with ",[29,871,817],{},". CPython intentionally keeps objects alive until exit, so leak detection reports many false positives. Use a dedicated leak-hunting run with suppressions if leaks are the concern.",[17,874,876],{"id":875},"related","Related",[22,878,879,886,892,899],{},[25,880,881,885],{},[44,882,884],{"href":883},"\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002F","Coverage-Guided Fuzzing with Atheris"," — how Atheris works.",[25,887,888,891],{},[44,889,890],{"href":46},"Writing Your First Atheris Fuzz Target"," — target structure and corpora.",[25,893,894,898],{},[44,895,897],{"href":896},"\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Frunning-fuzz-targets-in-ci-with-time-budgets\u002F","Running Fuzz Targets in CI with Time Budgets"," — running this setup on a schedule.",[25,900,901,905],{},[44,902,904],{"href":903},"\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Fstructure-aware-fuzzing-with-atheris-and-protobuf\u002F","Structure-Aware Fuzzing with Atheris and Protobuf"," — valid-by-construction inputs.",[10,907,908,909],{},"← Back to ",[44,910,884],{"href":883},[912,913,914],"style",{},"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}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}",{"title":59,"searchDepth":73,"depth":73,"links":916},[917,918,919,920,921,922,923,924,925],{"id":19,"depth":73,"text":20},{"id":51,"depth":73,"text":52},{"id":540,"depth":73,"text":541},{"id":575,"depth":73,"text":576},{"id":679,"depth":73,"text":680},{"id":768,"depth":73,"text":769},{"id":794,"depth":73,"text":795},{"id":847,"depth":73,"text":848},{"id":875,"depth":73,"text":876},"Find memory errors in Python C extensions: build the extension with AddressSanitizer and libFuzzer coverage, preload the ASan runtime, write an Atheris target, and triage the reports.","md",{"slug":929,"type":930,"breadcrumb":931,"datePublished":932,"dateModified":932,"faq":933,"howto":940},"fuzzing-a-c-extension-with-atheris-and-asan","article","Atheris + ASan","2026-09-18",[934,936,938],{"q":853,"a":935},"Without it, many memory errors — reading one byte past a buffer, using freed memory — do not crash. They corrupt memory silently and the fuzzer never notices. ASan turns each of those errors into an immediate, precise report at the faulting instruction.",{"q":859,"a":937},"The Python interpreter itself is not built with ASan, so the sanitizer runtime is not loaded when the extension is imported. Preloading the runtime library with LD_PRELOAD makes it available before any instrumented code runs.",{"q":868,"a":939},"Usually yes, with detect_leaks=0. CPython intentionally keeps objects alive until exit, so leak detection reports many false positives. Use a dedicated leak-hunting run with suppressions if leaks are the concern.",{"name":941,"description":942,"steps":943},"How to fuzz a C extension with Atheris and ASan","Build the extension with sanitizer and coverage flags, preload the runtime, run an Atheris target, and triage the crash.",[944,947,950,953],{"name":945,"text":946},"Build with instrumentation","Compile the extension with clang and -fsanitize=address,fuzzer-no-link.",{"name":948,"text":949},"Preload the ASan runtime","Set LD_PRELOAD to the ASan-enabled libFuzzer shared library that Atheris ships.",{"name":951,"text":952},"Write the target","Import the extension inside atheris.instrument_imports and feed fuzzer bytes to its entry points.",{"name":954,"text":955},"Triage reports","Read the ASan report, reproduce with the saved crash input, and fix the bounds or lifetime error.","\u002Fproperty-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Ffuzzing-a-c-extension-with-atheris-and-asan",{"title":5,"description":926},"property-based-fuzz-testing-strategies\u002Fcoverage-guided-fuzzing-with-atheris\u002Ffuzzing-a-c-extension-with-atheris-and-asan\u002Findex","DT6Y-HQ2KRImknSFUagOFFZdFuUOJ5MwNeciYmULEDo",1789718769012]