Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Modules/_xxtestfuzz/dictionaries/fuzz_marshal.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# marshal type codes (with and without the 0x80 REF flag) and structure tokens
"N"
"F"
"T"
"S"
"i"
"I"
"f"
"g"
"x"
"y"
"A"
"s"
"t"
"u"
"r"
"e"
"c"
"("
"["
"{"
"<"
">"
")"
"0"
"C"
"w"
"\x80"
"\x81"
"\x84"
"\x88"
"\x89"
"\x8a"
"\x8d"
"\x8e"
"\x91"
"\xa0"
"\xa1"
"\xa2"
"\xa3"
"\xa4"
"\xa5"
"\xa6"
"\xa7"
"\xa8"
"\xa9"
"\xc0"
"\xc1"
"\xe0"
"\xe9"
"ab"
"aaa"
"\x00\x01"
71 changes: 71 additions & 0 deletions Modules/_xxtestfuzz/dictionaries/fuzz_pickle.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# pickle opcode bytes (protocol 0-5) and typical atoms
"\x80"
"\x80\x02"
"\x80\x04"
"\x80\x05"
"("
")"
"]"
"}"
"."
"t"
"G"
"X"
"U"
"T"
"V"
"S"
"C"
"B"
"L"
"K"
"M"
"J"
"F"
"I"
"o"
"i"
"R"
"P"
"Q"
"N"
"e"
"a"
"s"
"u"
"r"
"p"
"q"
"h"
"j"
"0"
"1"
"g"
"f"
"\x8a"
"\x8b"
"\x8c"
"\x8d"
"\x8e"
"\x8f"
"\x90"
"\x91"
"\x92"
"\x93"
"\x94"
"\x95"
"\x96"
"\x97"
"\x98"
"builtins"
"__main__"
"copyreg"
"_reconstructor"
"collections"
"OrderedDict"
"defaultdict"
"encode"
"loads"
"\x8cbuiltins"
"\x8cdict"
"\x94"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
27 changes: 27 additions & 0 deletions Modules/_xxtestfuzz/fuzz_pickle_corpus/basic_dict_p0.pickle
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(dp0
Valpha
p1
I1
sVbeta
p2
(lp3
F2.5
aVx
p4
ac_codecs
encode
p5
(Vy
p6
Vlatin1
p7
tp8
Rp9
aNaI01
aI00
asVgamma
p10
(I1
I2
tp11
s.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions Modules/_xxtestfuzz/fuzz_pickle_corpus/neg_int_p2.pickle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
€Jë2¤ø.
41 changes: 41 additions & 0 deletions Modules/_xxtestfuzz/fuzz_pickle_corpus/nested_p0.pickle
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
(dp0
Vlist
p1
(lp2
I1
a(lp3
I2
a(lp4
I3
a(dp5
Vdeep
p6
(I4
I5
tp7
saaasVset
p8
c__builtin__
set
p9
((lp10
I1
aI2
aI3
atp11
Rp12
sVfrozenset
p13
c__builtin__
frozenset
p14
((lp15
Va
p16
aVb
p17
aVc
p18
atp19
Rp20
s.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12 changes: 12 additions & 0 deletions Modules/_xxtestfuzz/fuzz_pickle_corpus/proto0_text.pickle
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
(dp0
Vkey
p1
Vvalue
p2
sVn
p3
(lp4
I1
aI2
aI3
as.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions Modules/_xxtestfuzz/fuzz_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ fuzz_struct_unpack
fuzz_ast_literal_eval
fuzz_elementtree_parsewhole
fuzz_pycompile
fuzz_marshal
fuzz_pickle
134 changes: 134 additions & 0 deletions Modules/_xxtestfuzz/fuzzer.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <Python.h>
#include <stdlib.h>
#include <inttypes.h>
#include <marshal.h> /* PyMarshal_ReadObjectFromString for fuzz_marshal */

/* Fuzz PyFloat_FromString as a proxy for float(str). */
static int fuzz_builtin_float(const char* data, size_t size) {
Expand Down Expand Up @@ -595,6 +596,125 @@
return rv;
}

#define MAX_DESERIALISE_TEST_SIZE 0x100000

/* Fuzz marshal.loads() via PyMarshal_ReadObjectFromString. marshal is the
deserialiser behind .pyc files and marshal.dumps()/loads(); unmarshalling
attacker-supplied bytes must stay memory-safe (invalid type tags, negative
or oversized length fields, dangling ref tags, malformed code objects). */
static int fuzz_marshal(const char* data, size_t size) {
if (size > MAX_DESERIALISE_TEST_SIZE) {
return 0;
}
PyObject* obj = PyMarshal_ReadObjectFromString(data, (Py_ssize_t)size);
if (obj == NULL) {
/* Expected for attacker-controlled bytes: truncated streams raise
EOFError, invalid tags raise ValueError, malformed code objects
raise TypeError, deeply nested data raises RecursionError and
oversized length fields raise MemoryError. */
if (PyErr_ExceptionMatches(PyExc_EOFError) ||
PyErr_ExceptionMatches(PyExc_ValueError) ||
PyErr_ExceptionMatches(PyExc_TypeError) ||
PyErr_ExceptionMatches(PyExc_RecursionError) ||
PyErr_ExceptionMatches(PyExc_MemoryError))
{
PyErr_Clear();
}
}
Py_XDECREF(obj);
return 0;
}

/* Fuzz the C unpickler over attacker-controlled bytes: pickle is the classic
"never unpickle untrusted data" surface. The load loop stays in C (we
subclass the C _pickle.Unpickler); find_class is overridden to raise so
mutated payloads cannot import arbitrary modules or execute code inside
the fuzzer process, while every opcode-handling code path (stack/memo
manipulation, string and framing parsing, object reconstruction) remains
covered. Memory-safety bugs still crash the sanitizer. */
static PyObject* pickle_bytesio_type = NULL;
static PyObject* safe_unpickler_type = NULL;

static const char* safe_unpickler_code =
"class _SafeUnpickler(_pickle.Unpickler):\n"
" def find_class(self, module, name):\n"
" raise _pickle.UnpicklingError('find_class is disabled')\n";

/* Called by LLVMFuzzerTestOneInput for initialization */
static int init_pickle(void) {
PyObject* pickle_module = PyImport_ImportModule("_pickle");
if (pickle_module == NULL) {
return 0;
}
PyObject* io_module = PyImport_ImportModule("io");
if (io_module == NULL) {
Py_DECREF(pickle_module);
return 0;
}
pickle_bytesio_type = PyObject_GetAttrString(io_module, "BytesIO");
Py_DECREF(io_module);
if (pickle_bytesio_type == NULL) {
Py_DECREF(pickle_module);
return 0;
}

/* Create the restricted Unpickler subclass once. */
PyObject* globals = PyDict_New();
if (globals == NULL) {
Py_DECREF(pickle_module);
return 0;
}
if (PyDict_SetItemString(globals, "_pickle", pickle_module) < 0) {
Py_DECREF(globals);
Py_DECREF(pickle_module);
return 0;
}
PyObject* result = PyRun_String(safe_unpickler_code, Py_file_input,
globals, globals);
Py_DECREF(pickle_module); /* the new class holds its own references */
if (result != NULL) {
Py_DECREF(result);
safe_unpickler_type = PyDict_GetItemString(globals, "_SafeUnpickler");
Py_XINCREF(safe_unpickler_type);
}
Py_DECREF(globals);
return safe_unpickler_type != NULL;
}

/* Fuzz _pickle.Unpickler.load() over untrusted bytes */
static int fuzz_pickle(const char* data, size_t size) {
if (size > MAX_DESERIALISE_TEST_SIZE) {
return 0;
}
PyObject* input = PyObject_CallFunction(pickle_bytesio_type, "y#",
data, (Py_ssize_t)size);
if (input == NULL) {
PyErr_Print();
abort();
}
PyObject* unpickler = PyObject_CallFunctionObjArgs(safe_unpickler_type,
input, NULL);
Py_DECREF(input);
if (unpickler == NULL) {
PyErr_Print();
abort();
}
PyObject* result = PyObject_CallMethod(unpickler, "load", NULL);
Py_DECREF(unpickler);
if (result == NULL) {
/* Expected for attacker-controlled pickle bytes: invalid streams
raise UnpicklingError/EOFError, and object reconstruction (REDUCE,
SETSTATE, APPEND on built-in containers) can raise arbitrary
exceptions. Python-level failures are cleared here; memory-safety
bugs in the opcode machinery still crash the sanitizer. */
PyErr_Clear();
} else {
Py_DECREF(result);
}
return 0;
}


/* CPython generates a lot of leak warnings for whatever reason. */
int __lsan_is_turned_off(void) { return 1; }

Expand Down Expand Up @@ -726,6 +846,20 @@
#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_marshal)
rv |= _run_fuzz(data, size, fuzz_marshal);
#endif
#if !defined(_Py_FUZZ_ONE) || defined(_Py_FUZZ_fuzz_pickle)
static int PICKLE_INITIALIZED = 0;
if (!PICKLE_INITIALIZED && !init_pickle()) {
PyErr_Print();
abort();
} else {
PICKLE_INITIALIZED = 1;
}

rv |= _run_fuzz(data, size, fuzz_pickle);
#endif
return rv;
}
Loading