From b4cde8405f63c90f39f2cd0f87d7747db7e1c0dd Mon Sep 17 00:00:00 2001 From: Arthur Chan Date: Thu, 5 Nov 2026 03:39:02 +0000 Subject: [PATCH] OSS-Fuzz: Add new fuzzer/corpus/dict targets datetime processing Signed-off-by: Arthur Chan --- .../dictionaries/fuzz_datetime.dict | 33 ++++ .../_xxtestfuzz/fuzz_datetime_corpus/date_0 | Bin 0 -> 12 bytes .../_xxtestfuzz/fuzz_datetime_corpus/date_1 | Bin 0 -> 12 bytes .../_xxtestfuzz/fuzz_datetime_corpus/date_2 | Bin 0 -> 12 bytes .../_xxtestfuzz/fuzz_datetime_corpus/date_3 | Bin 0 -> 12 bytes .../fuzz_datetime_corpus/datetime_0 | 1 + .../fuzz_datetime_corpus/datetime_1 | 1 + .../fuzz_datetime_corpus/datetime_2 | 1 + .../fuzz_datetime_corpus/datetime_3 | 1 + .../fuzz_datetime_corpus/datetime_4 | 1 + .../fuzz_datetime_corpus/datetime_5 | 1 + .../fuzz_datetime_corpus/datetime_6 | 1 + .../fuzz_datetime_corpus/datetime_7 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_0 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_1 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_2 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_3 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_4 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_5 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_6 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_7 | 1 + .../_xxtestfuzz/fuzz_datetime_corpus/time_8 | 1 + Modules/_xxtestfuzz/fuzz_tests.txt | 1 + Modules/_xxtestfuzz/fuzzer.c | 150 ++++++++++++++++++ 24 files changed, 201 insertions(+) create mode 100644 Modules/_xxtestfuzz/dictionaries/fuzz_datetime.dict create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/date_0 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/date_1 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/date_2 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/date_3 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_0 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_1 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_2 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_3 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_4 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_5 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_6 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_7 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_0 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_1 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_2 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_3 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_4 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_5 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_6 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_7 create mode 100644 Modules/_xxtestfuzz/fuzz_datetime_corpus/time_8 diff --git a/Modules/_xxtestfuzz/dictionaries/fuzz_datetime.dict b/Modules/_xxtestfuzz/dictionaries/fuzz_datetime.dict new file mode 100644 index 00000000000000..6824d4dc2884f2 --- /dev/null +++ b/Modules/_xxtestfuzz/dictionaries/fuzz_datetime.dict @@ -0,0 +1,33 @@ +# ISO-8601 tokens for date/time/datetime.fromisoformat +"T" +"Z" +"z" +"+01:00" +"+01:00:30" +"-05:30" +"+0000" +"+01" +"2024-01-15" +"2024-01-15T10:30" +"2024-01-15 10:30:00" +"10:30" +"10:30:00" +"10:30:00.123456" +"10:30:00,5" +".123456" +",123" +"24:00" +"00:00" +"W" +"-W" +"2024-W03-1" +"2024-W03" +"2024-021" +"0001-01-01" +"9999-12-31" +"23:59:59.999999" +"+23:59" +"-23:59" +"hh" +"mm" +"ss" diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_0 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_0 new file mode 100644 index 0000000000000000000000000000000000000000..8ace41233710041fcfd6727f5d6e4ac3de3055c6 GIT binary patch literal 12 TcmZQzH8LdLjViD0t)~D literal 0 HcmV?d00001 diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_3 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_3 new file mode 100644 index 0000000000000000000000000000000000000000..54719f4712541a05726ae3ff0248065055848ea3 GIT binary patch literal 12 TcmZQzH8L.fromisoformat(s); returns new reference or NULL */ +static PyObject* datetime_fromiso_format(PyObject* type, PyObject* s) { + return PyObject_CallMethod(type, "fromisoformat", "O", s); +} + +/* Fuzz date/time/datetime.fromisoformat(x) + arithmetic */ +static int fuzz_datetime(const char* data, size_t size) { + if (size < 2 || size > MAX_DATETIME_TEST_SIZE) { + return 0; + } + /* First byte selects the parser, second drives the timedelta. */ + unsigned char op = (unsigned char) data[0]; + unsigned char amount = (unsigned char) data[1]; + data += 2; + size -= 2; + + PyObject* s = PyUnicode_FromStringAndSize(data, size); + if (s == NULL) { + if (PyErr_ExceptionMatches(PyExc_UnicodeDecodeError)) { + PyErr_Clear(); + } + return 0; + } + + PyObject* parsed = NULL; + switch (op % 3) { + case 0: + parsed = datetime_fromiso_format(date_type, s); + break; + case 1: + parsed = datetime_fromiso_format(time_type, s); + break; + default: + parsed = datetime_fromiso_format(datetime_type, s); + break; + } + if (parsed == NULL) { + /* Invalid ISO strings are the expected failure mode; anything + else is caught by _run_fuzz's last-ditch check. */ + if (PyErr_ExceptionMatches(PyExc_ValueError) || + PyErr_ExceptionMatches(PyExc_TypeError)) + { + PyErr_Clear(); + } + Py_DECREF(s); + return 0; + } + + /* Property: isoformat() output must parse back to an equal object + (fromisoformat is the documented inverse of isoformat). A failure + or mismatch is a genuine logic bug, not a harness fault. */ + PyObject* iso = PyObject_CallMethod(parsed, "isoformat", NULL); + if (iso == NULL) { + PyErr_Clear(); + } else { + PyObject* reparsed = datetime_fromiso_format( + op % 3 == 0 ? date_type : (op % 3 == 1 ? time_type : datetime_type), + iso); + if (reparsed == NULL) { + PyErr_Print(); + abort(); + } + int equal = PyObject_RichCompareBool(parsed, reparsed, Py_EQ); + if (equal < 0) { + PyErr_Clear(); + } else if (equal == 0) { + PyErr_SetString(PyExc_AssertionError, + "isoformat/fromisoformat round-trip mismatch"); + PyErr_Print(); + abort(); + } + Py_DECREF(reparsed); + Py_DECREF(iso); + + /* Exercise C arithmetic and normalisation with a fuzzed timedelta. + (time objects reject +/- with TypeError: expected and cleared.) */ + PyObject* td = PyObject_CallFunction(timedelta_type, "iii", + amount % 30, amount % 60, + amount % 1000000); + if (td == NULL) { + PyErr_Clear(); + } else { + PyObject* shifted = PyNumber_Add(parsed, td); + if (shifted == NULL) { + if (PyErr_ExceptionMatches(PyExc_TypeError) || + PyErr_ExceptionMatches(PyExc_OverflowError)) + { + PyErr_Clear(); + } + } else { + Py_DECREF(shifted); + } + PyObject* shifted_back = PyNumber_Subtract(parsed, td); + if (shifted_back == NULL) { + if (PyErr_ExceptionMatches(PyExc_TypeError) || + PyErr_ExceptionMatches(PyExc_OverflowError)) + { + PyErr_Clear(); + } + } else { + Py_DECREF(shifted_back); + } + Py_DECREF(td); + } + } + + Py_DECREF(parsed); + Py_DECREF(s); + return 0; +} + + /* CPython generates a lot of leak warnings for whatever reason. */ int __lsan_is_turned_off(void) { return 1; } @@ -726,6 +865,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_datetime) + static int DATETIME_INITIALIZED = 0; + if (!DATETIME_INITIALIZED && !init_datetime()) { + PyErr_Print(); + abort(); + } else { + DATETIME_INITIALIZED = 1; + } + + rv |= _run_fuzz(data, size, fuzz_datetime); #endif return rv; }