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 00000000000000..8ace4123371004 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_0 differ diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_1 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_1 new file mode 100644 index 00000000000000..032c919709146c Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_1 differ diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_2 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_2 new file mode 100644 index 00000000000000..6bcac16374b317 Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_2 differ diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_3 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_3 new file mode 100644 index 00000000000000..54719f4712541a Binary files /dev/null and b/Modules/_xxtestfuzz/fuzz_datetime_corpus/date_3 differ diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_0 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_0 new file mode 100644 index 00000000000000..ef057eeee8b62a --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_0 @@ -0,0 +1 @@ +2024-01-15T10:30 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_1 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_1 new file mode 100644 index 00000000000000..7d23e84d3f3f61 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_1 @@ -0,0 +1 @@ +2024-01-15 10:30:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_2 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_2 new file mode 100644 index 00000000000000..e67caaa770fedf --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_2 @@ -0,0 +1 @@ +2024-01-15T10:30:00.123456 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_3 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_3 new file mode 100644 index 00000000000000..fa07490b71ff50 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_3 @@ -0,0 +1 @@ +2024-01-15T10:30:00+01:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_4 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_4 new file mode 100644 index 00000000000000..3aee0de4035a19 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_4 @@ -0,0 +1 @@ +2024-01-15T10:30:00.123456-05:30 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_5 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_5 new file mode 100644 index 00000000000000..d7091cf475c968 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_5 @@ -0,0 +1 @@ +0001-01-01T00:00:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_6 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_6 new file mode 100644 index 00000000000000..7cae7482480211 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_6 @@ -0,0 +1 @@ +9999-12-31T23:59:59.999999+00:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_7 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_7 new file mode 100644 index 00000000000000..bbd0b36b0c5d01 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/datetime_7 @@ -0,0 +1 @@ +2024-06-01T12:00:00.000001+02:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_0 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_0 new file mode 100644 index 00000000000000..c75d29d33adc3d --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_0 @@ -0,0 +1 @@ +10:30 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_1 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_1 new file mode 100644 index 00000000000000..c6490577325e9a --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_1 @@ -0,0 +1 @@ +10:30:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_2 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_2 new file mode 100644 index 00000000000000..348f88e6e5af95 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_2 @@ -0,0 +1 @@ +10:30:00.123456 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_3 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_3 new file mode 100644 index 00000000000000..5ddcd9eadef3ce --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_3 @@ -0,0 +1 @@ +10:30:00,5 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_4 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_4 new file mode 100644 index 00000000000000..188833d119b8c5 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_4 @@ -0,0 +1 @@ +00:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_5 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_5 new file mode 100644 index 00000000000000..15612f2963c805 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_5 @@ -0,0 +1 @@ +23:59:59.999999 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_6 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_6 new file mode 100644 index 00000000000000..bd8c2a4dfaf752 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_6 @@ -0,0 +1 @@ +10:30+01:00 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_7 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_7 new file mode 100644 index 00000000000000..e492887e1a4532 --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_7 @@ -0,0 +1 @@ +10:30:00-05:30 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_8 b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_8 new file mode 100644 index 00000000000000..dd2c6ce25cf9db --- /dev/null +++ b/Modules/_xxtestfuzz/fuzz_datetime_corpus/time_8 @@ -0,0 +1 @@ +10:30:00.123456+01:00:30 \ No newline at end of file diff --git a/Modules/_xxtestfuzz/fuzz_tests.txt b/Modules/_xxtestfuzz/fuzz_tests.txt index ea6f982eefc9da..86df736548f7c8 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_datetime diff --git a/Modules/_xxtestfuzz/fuzzer.c b/Modules/_xxtestfuzz/fuzzer.c index 8c2d0e93cd72d4..58e86af1aadc99 100644 --- a/Modules/_xxtestfuzz/fuzzer.c +++ b/Modules/_xxtestfuzz/fuzzer.c @@ -595,6 +595,145 @@ static int _run_fuzz(const uint8_t *data, size_t size, int(*fuzzer)(const char* return rv; } +#define MAX_DATETIME_TEST_SIZE 0x10000 + +/* Fuzz the C ISO-8601 fast parsers and date/time arithmetic: date, + time and datetime fromisoformat() (all parsing lives in + Modules/_datetimemodule.c), isoformat() round-tripping as a property + (documented inverse), and timedelta addition/subtraction to drive the + C normalisation and overflow-checking paths. */ +static PyObject* datetime_module = NULL; +static PyObject* date_type = NULL; +static PyObject* time_type = NULL; +static PyObject* datetime_type = NULL; +static PyObject* timedelta_type = NULL; + +/* Called by LLVMFuzzerTestOneInput for initialization */ +static int init_datetime(void) { + datetime_module = PyImport_ImportModule("datetime"); + if (datetime_module == NULL) { + return 0; + } + date_type = PyObject_GetAttrString(datetime_module, "date"); + time_type = PyObject_GetAttrString(datetime_module, "time"); + datetime_type = PyObject_GetAttrString(datetime_module, "datetime"); + timedelta_type = PyObject_GetAttrString(datetime_module, "timedelta"); + return (date_type != NULL && time_type != NULL && + datetime_type != NULL && timedelta_type != NULL); +} + +/* Call .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; }