Skip to content

Commit a94965a

Browse files
committed
gh-153569: add tokenizer validation tools
1 parent a4e0f20 commit a94965a

5 files changed

Lines changed: 639 additions & 9 deletions

File tree

Modules/_xxtestfuzz/fuzz_tests.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ fuzz_struct_unpack
99
fuzz_ast_literal_eval
1010
fuzz_elementtree_parsewhole
1111
fuzz_pycompile
12+
fuzz_tokenizer

Modules/_xxtestfuzz/fuzzer.c

Lines changed: 156 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,22 @@ static int fuzz_ast_literal_eval(const char* data, size_t size) {
441441
#define MAX_ELEMENTTREE_PARSEWHOLE_TEST_SIZE 0x100000
442442
PyObject* xmlparser_type = NULL;
443443
PyObject* bytesio_type = NULL;
444+
445+
static int
446+
init_bytesio(void)
447+
{
448+
if (bytesio_type != NULL) {
449+
return 1;
450+
}
451+
PyObject *io_module = PyImport_ImportModule("_io");
452+
if (io_module == NULL) {
453+
return 0;
454+
}
455+
bytesio_type = PyObject_GetAttrString(io_module, "BytesIO");
456+
Py_DECREF(io_module);
457+
return bytesio_type != NULL;
458+
}
459+
444460
/* Called by LLVMFuzzerTestOneInput for initialization */
445461
static int init_elementtree_parsewhole(void) {
446462
PyObject* elementtree_module = PyImport_ImportModule("_elementtree");
@@ -452,15 +468,7 @@ static int init_elementtree_parsewhole(void) {
452468
if (xmlparser_type == NULL) {
453469
return 0;
454470
}
455-
456-
457-
PyObject* io_module = PyImport_ImportModule("io");
458-
if (io_module == NULL) {
459-
return 0;
460-
}
461-
bytesio_type = PyObject_GetAttrString(io_module, "BytesIO");
462-
Py_DECREF(io_module);
463-
if (bytesio_type == NULL) {
471+
if (!init_bytesio()) {
464472
return 0;
465473
}
466474

@@ -578,6 +586,136 @@ static int fuzz_pycompile(const char* data, size_t size) {
578586
return 0;
579587
}
580588

589+
#define MAX_TOKENIZER_TEST_SIZE 16384
590+
591+
static PyObject *tokenizer_iter_type;
592+
static PyObject *stringio_type;
593+
594+
static int
595+
init_tokenizer(void)
596+
{
597+
PyObject *tokenize_module = PyImport_ImportModule("_tokenize");
598+
if (tokenize_module == NULL) {
599+
return 0;
600+
}
601+
tokenizer_iter_type = PyObject_GetAttrString(
602+
tokenize_module, "TokenizerIter");
603+
Py_DECREF(tokenize_module);
604+
if (tokenizer_iter_type == NULL) {
605+
return 0;
606+
}
607+
608+
PyObject *io_module = PyImport_ImportModule("_io");
609+
if (io_module == NULL) {
610+
return 0;
611+
}
612+
stringio_type = PyObject_GetAttrString(io_module, "StringIO");
613+
Py_DECREF(io_module);
614+
if (stringio_type == NULL) {
615+
return 0;
616+
}
617+
return init_bytesio();
618+
}
619+
620+
static void
621+
clear_expected_tokenizer_error(void)
622+
{
623+
if (PyErr_ExceptionMatches(PyExc_SyntaxError) ||
624+
PyErr_ExceptionMatches(PyExc_UnicodeError) ||
625+
PyErr_ExceptionMatches(PyExc_LookupError) ||
626+
PyErr_ExceptionMatches(PyExc_MemoryError) ||
627+
PyErr_ExceptionMatches(PyExc_Warning)) {
628+
PyErr_Clear();
629+
return;
630+
}
631+
PyErr_Print();
632+
abort();
633+
}
634+
635+
static int
636+
fuzz_tokenizer(const char *data, size_t size)
637+
{
638+
if (size < 1 || size > MAX_TOKENIZER_TEST_SIZE) {
639+
return 0;
640+
}
641+
642+
unsigned char options = (unsigned char)data[0];
643+
data++;
644+
size--;
645+
646+
int bytes_mode = options & 0x01;
647+
PyObject *source;
648+
if (bytes_mode) {
649+
source = PyBytes_FromStringAndSize(data, size);
650+
}
651+
else {
652+
source = PyUnicode_DecodeUTF8(data, size, "replace");
653+
}
654+
if (source == NULL) {
655+
clear_expected_tokenizer_error();
656+
return 0;
657+
}
658+
659+
PyObject *stream_type = bytes_mode ? bytesio_type : stringio_type;
660+
PyObject *stream = PyObject_CallOneArg(stream_type, source);
661+
Py_DECREF(source);
662+
if (stream == NULL) {
663+
clear_expected_tokenizer_error();
664+
return 0;
665+
}
666+
PyObject *readline = PyObject_GetAttrString(stream, "readline");
667+
Py_DECREF(stream);
668+
if (readline == NULL) {
669+
clear_expected_tokenizer_error();
670+
return 0;
671+
}
672+
673+
PyObject *args = PyTuple_Pack(1, readline);
674+
Py_DECREF(readline);
675+
if (args == NULL) {
676+
clear_expected_tokenizer_error();
677+
return 0;
678+
}
679+
PyObject *extra_tokens = options & 0x02 ? Py_True : Py_False;
680+
PyObject *kwargs = bytes_mode
681+
? Py_BuildValue(
682+
"{s:O,s:s}", "extra_tokens", extra_tokens,
683+
"encoding", "utf-8")
684+
: Py_BuildValue("{s:O}", "extra_tokens", extra_tokens);
685+
if (kwargs == NULL) {
686+
Py_DECREF(args);
687+
clear_expected_tokenizer_error();
688+
return 0;
689+
}
690+
691+
PyObject *iterator = PyObject_Call(tokenizer_iter_type, args, kwargs);
692+
Py_DECREF(args);
693+
Py_DECREF(kwargs);
694+
if (iterator == NULL) {
695+
clear_expected_tokenizer_error();
696+
return 0;
697+
}
698+
699+
int terminated = 0;
700+
size_t limit = size * 4 + 32;
701+
for (size_t i = 0; i < limit; i++) {
702+
PyObject *token = PyIter_Next(iterator);
703+
if (token == NULL) {
704+
terminated = 1;
705+
if (PyErr_Occurred()) {
706+
clear_expected_tokenizer_error();
707+
}
708+
break;
709+
}
710+
Py_DECREF(token);
711+
}
712+
Py_DECREF(iterator);
713+
if (!terminated) {
714+
abort();
715+
}
716+
return 0;
717+
}
718+
581719
/* Run fuzzer and abort on failure. */
582720
static int _run_fuzz(const uint8_t *data, size_t size, int(*fuzzer)(const char* , size_t)) {
583721
int rv = fuzzer((const char*) data, size);
@@ -722,6 +860,15 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
722860
#endif
723861
#if !defined(_Py_FUZZ_ONE) || defined(_Py_FUZZ_fuzz_pycompile)
724862
rv |= _run_fuzz(data, size, fuzz_pycompile);
863+
#endif
864+
#if !defined(_Py_FUZZ_ONE) || defined(_Py_FUZZ_fuzz_tokenizer)
865+
static int TOKENIZER_INITIALIZED = 0;
866+
if (!TOKENIZER_INITIALIZED && !init_tokenizer()) {
867+
PyErr_Print();
868+
abort();
869+
}
870+
TOKENIZER_INITIALIZED = 1;
871+
rv |= _run_fuzz(data, size, fuzz_tokenizer);
725872
#endif
726873
return rv;
727874
}

Tools/README

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ scripts A number of useful single-file programs, e.g. run_tests.py
4747
ssl Scripts to generate ssl_data.h from OpenSSL sources, and run
4848
tests against multiple installations of OpenSSL and LibreSSL.
4949

50+
tokenizer Tools for comparing tokenizer behavior between Python builds.
51+
5052
tsan Utilities for building CPython with thread-sanitizer.
5153

5254
unicode Tools for generating unicodedata and codecs from unicode.org

Tools/c-analyzer/cpython/ignored.tsv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,13 +652,16 @@ Modules/_xxtestfuzz/fuzzer.c - xmlparser_type -
652652
Modules/_xxtestfuzz/fuzzer.c - pycompile_scratch -
653653
Modules/_xxtestfuzz/fuzzer.c - start_vals -
654654
Modules/_xxtestfuzz/fuzzer.c - optimize_vals -
655+
Modules/_xxtestfuzz/fuzzer.c - stringio_type -
656+
Modules/_xxtestfuzz/fuzzer.c - tokenizer_iter_type -
655657
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput CSV_READER_INITIALIZED -
656658
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput JSON_LOADS_INITIALIZED -
657659
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput SRE_COMPILE_INITIALIZED -
658660
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput SRE_MATCH_INITIALIZED -
659661
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput STRUCT_UNPACK_INITIALIZED -
660662
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput AST_LITERAL_EVAL_INITIALIZED -
661663
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput ELEMENTTREE_PARSEWHOLE_INITIALIZED -
664+
Modules/_xxtestfuzz/fuzzer.c LLVMFuzzerTestOneInput TOKENIZER_INITIALIZED -
662665

663666
##-----------------------
664667
## the analyzer should have ignored these

0 commit comments

Comments
 (0)