From d1fe14403ea134353bda649ee328e87e3e9e0084 Mon Sep 17 00:00:00 2001 From: Arthur Chan Date: Thu, 5 Nov 2026 03:40:33 +0000 Subject: [PATCH] OSS-Fuzz: Add new fuzzer/corpus/dict targets zlib Signed-off-by: Arthur Chan --- .../_xxtestfuzz/dictionaries/fuzz_zlib.dict | 23 +++ Modules/_xxtestfuzz/fuzz_tests.txt | 1 + .../_xxtestfuzz/fuzz_zlib_corpus/gzip_stream | Bin 0 -> 84 bytes .../_xxtestfuzz/fuzz_zlib_corpus/lzma_alone | Bin 0 -> 85 bytes .../_xxtestfuzz/fuzz_zlib_corpus/lzma_empty | Bin 0 -> 33 bytes .../_xxtestfuzz/fuzz_zlib_corpus/raw_deflate | Bin 0 -> 3 bytes .../_xxtestfuzz/fuzz_zlib_corpus/xz_stream | Bin 0 -> 129 bytes .../_xxtestfuzz/fuzz_zlib_corpus/zlib_empty | Bin 0 -> 9 bytes .../_xxtestfuzz/fuzz_zlib_corpus/zlib_level1 | Bin 0 -> 73 bytes .../_xxtestfuzz/fuzz_zlib_corpus/zlib_level9 | Bin 0 -> 30 bytes .../_xxtestfuzz/fuzz_zlib_corpus/zlib_small | Bin 0 -> 72 bytes Modules/_xxtestfuzz/fuzzer.c | 147 ++++++++++++++++++ 12 files changed, 171 insertions(+) create mode 100644 Modules/_xxtestfuzz/dictionaries/fuzz_zlib.dict create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/gzip_stream create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_alone create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_empty create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/raw_deflate create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/xz_stream create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_empty create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level1 create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level9 create mode 100644 Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_small diff --git a/Modules/_xxtestfuzz/dictionaries/fuzz_zlib.dict b/Modules/_xxtestfuzz/dictionaries/fuzz_zlib.dict new file mode 100644 index 000000000000000..25f6d24d32a709f --- /dev/null +++ b/Modules/_xxtestfuzz/dictionaries/fuzz_zlib.dict @@ -0,0 +1,23 @@ +# zlib/lzma/gzip headers and structure tokens +"\x78\x01" +"\x78\x5e" +"\x78\x9c" +"\x78\xda" +"\x78\xf9" +"\xfd7zXZ" +"\xfd7zXZ\x00" +"\x1f\x8b" +"\x1f\x8b\x08" +"\x5d\x00" +"\x5d\x00\x00" +"\x80" +"\x00\x00\xff\xff" +"\x00\x00" +"\xff\xff" +"\x04\x00" +"\x9c" +"\xda" +"\x00" +"\xff" +"\x02" +"\x01" diff --git a/Modules/_xxtestfuzz/fuzz_tests.txt b/Modules/_xxtestfuzz/fuzz_tests.txt index ea6f982eefc9da1..a3cd5dc5ee3b0d9 100644 --- a/Modules/_xxtestfuzz/fuzz_tests.txt +++ b/Modules/_xxtestfuzz/fuzz_tests.txt @@ -9,3 +9,4 @@ fuzz_struct_unpack fuzz_ast_literal_eval fuzz_elementtree_parsewhole fuzz_pycompile +fuzz_zlib diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/gzip_stream b/Modules/_xxtestfuzz/fuzz_zlib_corpus/gzip_stream new file mode 100644 index 0000000000000000000000000000000000000000..7d1895d0524b0039496deec551d40835838a1ea8 GIT binary patch literal 84 zcmZRQ@8)3G7x+4h=|A^Lk5G?u=XEu_boI`iIUjuDtiR@2&rprC-k!Qa8lJj(8-svS o9=a!WPU?7vYIv+UbJE`@(8E*5^NRFTiG4y07v#)a#F-cv0CvhAt)O4UvU09!`NYN$0kwE^J3giY7&#T neA?odYq)RE=T#hQ_8tni*${repNnhh+)R({`~NS`YG42WN$M*4 literal 0 HcmV?d00001 diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_empty b/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_empty new file mode 100644 index 0000000000000000000000000000000000000000..fa2149e8751217332ea7836bfc59bbf8dffd844b GIT binary patch literal 33 lcmZSnYhD!*#lXPw?AjJL1|X2R@1iKb?Q<<7h{F;Y1pu?m33~tl literal 0 HcmV?d00001 diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/raw_deflate b/Modules/_xxtestfuzz/fuzz_zlib_corpus/raw_deflate new file mode 100644 index 0000000000000000000000000000000000000000..a212167346ce1a4ca3daf596fee1006e8caf8fba GIT binary patch literal 3 KcmZQ%W&i*H3;+cH literal 0 HcmV?d00001 diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/xz_stream b/Modules/_xxtestfuzz/fuzz_zlib_corpus/xz_stream new file mode 100644 index 0000000000000000000000000000000000000000..b6f73840a13734b8b9845b693999841399540f53 GIT binary patch literal 129 zcmV-{0Dk`i{Wp48S^xk9=GL@E0stWa761SMbT8$j-~tu^KwSVT8VI5SvFF|MSjHBN zQ8MeMrk~J5^_Vk2ortm(3C|V_LM9lKwDPo0Z85&Q^Qs7}y~0;Kuvg!Y3JRs1x&QzG js=9+zFhFV300CT=1ONa425?8LDyC+fz44!&6spV-QfvL-(Z4NgeM{ d4UaWvPWt-70I+%rqyPW_ literal 0 HcmV?d00001 diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_small b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_small new file mode 100644 index 0000000000000000000000000000000000000000..bc5e451ebaac700decd91440c84d6f7c1aeb348e GIT binary patch literal 72 zcmZRmn8SV2Bh=&Ed0h=JUA=Q>&Ig}3>#up%GgRZOx2JB9hNrIH#vq`Shwe$8lRDm^ c8XjxTob>kz^zhX2ydphSVxJJh>Y(F&02?J7ZvX%Q literal 0 HcmV?d00001 diff --git a/Modules/_xxtestfuzz/fuzzer.c b/Modules/_xxtestfuzz/fuzzer.c index 8c2d0e93cd72d44..6e12ef6e898c2c9 100644 --- a/Modules/_xxtestfuzz/fuzzer.c +++ b/Modules/_xxtestfuzz/fuzzer.c @@ -595,6 +595,142 @@ static int _run_fuzz(const uint8_t *data, size_t size, int(*fuzzer)(const char* return rv; } +#define MAX_ZLIB_TEST_SIZE 0x100000 + +/* Fuzz zlib and lzma decompression of attacker-controlled bytes. The + module wrappers live in Modules/zlibmodule.c and Modules/_lzmamodule.c + (the zlib/xz libraries themselves are built from source with sanitizer + instrumentation). Streaming decompression with an explicit max_length + bounds the output so decompression bombs cannot OOM the fuzzer. */ +static PyObject* zlib_decompressobj_factory = NULL; +static PyObject* lzma_decompress_factory = NULL; +static PyObject* zlib_error_exc = NULL; +static PyObject* lzma_error_exc = NULL; + +/* Called by LLVMFuzzerTestOneInput for initialization */ +static int init_zlib(void) { + if (PyErr_Occurred()) { + /* A pending exception left over from interpreter startup would + trip asserts inside attribute lookups; report it visibly and + start the lookups with a clean error indicator. */ + PyErr_Print(); + } + PyObject* zlib_module = PyImport_ImportModule("zlib"); + if (zlib_module == NULL) { + return 0; + } + zlib_decompressobj_factory = PyObject_GetAttrString(zlib_module, "decompressobj"); + if (zlib_decompressobj_factory == NULL) { + Py_DECREF(zlib_module); + return 0; + } + zlib_error_exc = PyObject_GetAttrString(zlib_module, "error"); + Py_DECREF(zlib_module); + if (zlib_error_exc == NULL) { + return 0; + } + PyObject* lzma_module = PyImport_ImportModule("lzma"); + if (lzma_module == NULL) { + return 0; + } + lzma_decompress_factory = PyObject_GetAttrString(lzma_module, "LZMADecompressor"); + if (lzma_decompress_factory == NULL) { + Py_DECREF(lzma_module); + return 0; + } + lzma_error_exc = PyObject_GetAttrString(lzma_module, "LZMAError"); + Py_DECREF(lzma_module); + if (lzma_error_exc == NULL) { + return 0; + } + return 1; +} + +/* Fuzz zlib.decompressobj(wbits).decompress / lzma.LZMADecompressor().decompress */ +static int fuzz_zlib(const char* data, size_t size) { + if (size < 2 || size > MAX_ZLIB_TEST_SIZE) { + return 0; + } + unsigned char mode = (unsigned char) data[0]; + data += 1; + size -= 1; + + PyObject* payload = PyBytes_FromStringAndSize(data, (Py_ssize_t)size); + if (payload == NULL) { + return 0; + } + + PyObject* dobj; + if (mode % 4 == 3) { + /* lzma with FORMAT_AUTO (0) */ + dobj = PyObject_CallFunction(lzma_decompress_factory, "(i)", 0); + } else { + /* zlib window bits: 15 = zlib stream, -15 = raw deflate, + 47 = auto-detect zlib or gzip */ + int wbits = 15; + switch (mode % 3) { + case 0: wbits = 15; break; + case 1: wbits = -15; break; + default: wbits = 47; break; + } + dobj = PyObject_CallFunction(zlib_decompressobj_factory, "(i)", wbits); + } + if (dobj == NULL) { + PyErr_Clear(); + Py_DECREF(payload); + return 0; + } + + /* max_length bounds the output per call: decompression bombs end up + in unconsumed_tail instead of memory. */ + PyObject* out = PyObject_CallMethod(dobj, "decompress", "Oi", payload, + 65536); + if (out == NULL) { + /* Expected for attacker bytes: zlib.error / lzma.LZMAError for + corrupt or unsupported streams, EOFError for truncated ones, + ValueError for bad header combinations. */ + if (PyErr_ExceptionMatches(PyExc_EOFError) || + PyErr_ExceptionMatches(PyExc_ValueError) || + PyErr_ExceptionMatches(zlib_error_exc) || + PyErr_ExceptionMatches(lzma_error_exc)) + { + PyErr_Clear(); + } + } else { + Py_DECREF(out); + if (mode % 4 == 3) { + /* LZMADecompressor has no flush(); reading the end-of-stream + flags exercises its EOF detection paths instead. */ + PyObject* eof = PyObject_GetAttrString(dobj, "eof"); + if (eof == NULL) { + PyErr_Clear(); + } else { + Py_DECREF(eof); + } + PyObject* unused = PyObject_GetAttrString(dobj, "unused_data"); + if (unused == NULL) { + PyErr_Clear(); + } else { + Py_DECREF(unused); + } + } else { + /* flush() completes the zlib stream handling paths. */ + PyObject* flushed = PyObject_CallMethod(dobj, "flush", NULL); + if (flushed == NULL) { + if (PyErr_ExceptionMatches(zlib_error_exc)) { + PyErr_Clear(); + } + } else { + Py_DECREF(flushed); + } + } + } + Py_DECREF(dobj); + Py_DECREF(payload); + return 0; +} + + /* CPython generates a lot of leak warnings for whatever reason. */ int __lsan_is_turned_off(void) { return 1; } @@ -726,6 +862,17 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { #endif #if !defined(_Py_FUZZ_ONE) || defined(_Py_FUZZ_fuzz_pycompile) rv |= _run_fuzz(data, size, fuzz_pycompile); +#endif +#if !defined(_Py_FUZZ_ONE) || defined(_Py_FUZZ_fuzz_zlib) + static int ZLIB_INITIALIZED = 0; + if (!ZLIB_INITIALIZED && !init_zlib()) { + PyErr_Print(); + abort(); + } else { + ZLIB_INITIALIZED = 1; + } + + rv |= _run_fuzz(data, size, fuzz_zlib); #endif return rv; }