diff --git a/Modules/_xxtestfuzz/dictionaries/fuzz_zlib.dict b/Modules/_xxtestfuzz/dictionaries/fuzz_zlib.dict new file mode 100644 index 00000000000000..25f6d24d32a709 --- /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 ea6f982eefc9da..a3cd5dc5ee3b0d 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 00000000000000..7d1895d0524b00 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/gzip_stream differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_alone b/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_alone new file mode 100644 index 00000000000000..4f5e612eed7779 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_alone differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_empty b/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_empty new file mode 100644 index 00000000000000..fa2149e8751217 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/lzma_empty differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/raw_deflate b/Modules/_xxtestfuzz/fuzz_zlib_corpus/raw_deflate new file mode 100644 index 00000000000000..a212167346ce1a Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/raw_deflate differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/xz_stream b/Modules/_xxtestfuzz/fuzz_zlib_corpus/xz_stream new file mode 100644 index 00000000000000..b6f73840a13734 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/xz_stream differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_empty b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_empty new file mode 100644 index 00000000000000..7488025fb2f432 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_empty differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level1 b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level1 new file mode 100644 index 00000000000000..e651b35e2f90af Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level1 differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level9 b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level9 new file mode 100644 index 00000000000000..eb45d410f891be Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_level9 differ diff --git a/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_small b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_small new file mode 100644 index 00000000000000..bc5e451ebaac70 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_zlib_corpus/zlib_small differ diff --git a/Modules/_xxtestfuzz/fuzzer.c b/Modules/_xxtestfuzz/fuzzer.c index 8c2d0e93cd72d4..6e12ef6e898c2c 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; }