diff --git a/Makefile b/Makefile index 01df3e8..228cf9a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: fuzzer-binascii fuzzer-html fuzzer-email fuzzer-httpclient fuzzer-json fuzzer-difflib fuzzer-csv fuzzer-decode fuzzer-ast fuzzer-tarfile fuzzer-tarfile-hypothesis fuzzer-zipfile fuzzer-zipfile-hypothesis fuzzer-re fuzzer-configparser fuzzer-tomllib fuzzer-plistlib fuzzer-xml fuzzer-zoneinfo +all: fuzzer-binascii fuzzer-html fuzzer-email fuzzer-httpclient fuzzer-json fuzzer-difflib fuzzer-csv fuzzer-decode fuzzer-ast fuzzer-tarfile fuzzer-tarfile-hypothesis fuzzer-zipfile fuzzer-zipfile-hypothesis fuzzer-re fuzzer-configparser fuzzer-tomllib fuzzer-plistlib fuzzer-xml fuzzer-zoneinfo fuzzer-textio PYTHON_CONFIG_PATH=$(CPYTHON_INSTALL_PATH)/bin/python3-config CXXFLAGS += $(shell $(PYTHON_CONFIG_PATH) --cflags) @@ -42,3 +42,6 @@ fuzzer-xml: clang++ $(CXXFLAGS) $(LIB_FUZZING_ENGINE) -std=c++17 fuzzer.cpp -DPYTHON_HARNESS_PATH="\"xml.py\"" -ldl $(LDFLAGS) -o fuzzer-xml fuzzer-zoneinfo: clang++ $(CXXFLAGS) $(LIB_FUZZING_ENGINE) -std=c++17 fuzzer.cpp -DPYTHON_HARNESS_PATH="\"zoneinfo.py\"" -ldl $(LDFLAGS) -o fuzzer-zoneinfo + +fuzzer-textio: + clang++ $(CXXFLAGS) $(LIB_FUZZING_ENGINE) -std=c++17 fuzzer.cpp -DPYTHON_HARNESS_PATH="\"textio.py\"" -ldl $(LDFLAGS) -o fuzzer-textio diff --git a/corp-textio/bom_only b/corp-textio/bom_only new file mode 100644 index 0000000..9850b79 Binary files /dev/null and b/corp-textio/bom_only differ diff --git a/corp-textio/empty_input b/corp-textio/empty_input new file mode 100644 index 0000000..5320c68 Binary files /dev/null and b/corp-textio/empty_input differ diff --git a/corp-textio/latin1_read b/corp-textio/latin1_read new file mode 100644 index 0000000..f027404 Binary files /dev/null and b/corp-textio/latin1_read differ diff --git a/corp-textio/surrogateesc b/corp-textio/surrogateesc new file mode 100644 index 0000000..63214b0 Binary files /dev/null and b/corp-textio/surrogateesc differ diff --git a/corp-textio/utf16_odd b/corp-textio/utf16_odd new file mode 100644 index 0000000..254078a Binary files /dev/null and b/corp-textio/utf16_odd differ diff --git a/corp-textio/utf16be_read b/corp-textio/utf16be_read new file mode 100644 index 0000000..f4ed067 Binary files /dev/null and b/corp-textio/utf16be_read differ diff --git a/corp-textio/utf16bom_read b/corp-textio/utf16bom_read new file mode 100644 index 0000000..2b38295 Binary files /dev/null and b/corp-textio/utf16bom_read differ diff --git a/corp-textio/utf16le_read b/corp-textio/utf16le_read new file mode 100644 index 0000000..35dc0ec Binary files /dev/null and b/corp-textio/utf16le_read differ diff --git a/corp-textio/utf32be_iter b/corp-textio/utf32be_iter new file mode 100644 index 0000000..55c206c Binary files /dev/null and b/corp-textio/utf32be_iter differ diff --git a/corp-textio/utf32bom_read b/corp-textio/utf32bom_read new file mode 100644 index 0000000..12cf7b9 Binary files /dev/null and b/corp-textio/utf32bom_read differ diff --git a/corp-textio/utf32le_readn b/corp-textio/utf32le_readn new file mode 100644 index 0000000..d5e7516 Binary files /dev/null and b/corp-textio/utf32le_readn differ diff --git a/corp-textio/utf8_crlf_iter b/corp-textio/utf8_crlf_iter new file mode 100644 index 0000000..bfd2aa4 Binary files /dev/null and b/corp-textio/utf8_crlf_iter differ diff --git a/corp-textio/utf8_read b/corp-textio/utf8_read new file mode 100644 index 0000000..032b182 Binary files /dev/null and b/corp-textio/utf8_read differ diff --git a/corp-textio/utf8_readlines b/corp-textio/utf8_readlines new file mode 100644 index 0000000..3453bcf Binary files /dev/null and b/corp-textio/utf8_readlines differ diff --git a/corp-textio/utf8_readn b/corp-textio/utf8_readn new file mode 100644 index 0000000..8f5983c Binary files /dev/null and b/corp-textio/utf8_readn differ diff --git a/corp-textio/utf8_truncated b/corp-textio/utf8_truncated new file mode 100644 index 0000000..11991c4 Binary files /dev/null and b/corp-textio/utf8_truncated differ diff --git a/fuzz_targets.txt b/fuzz_targets.txt index b016889..2efbc53 100644 --- a/fuzz_targets.txt +++ b/fuzz_targets.txt @@ -12,6 +12,7 @@ plistlib plist.py re re.py tarfile tarfile.py tarfile-hypothesis tarfile_hypothesis.py +textio textio.py tomllib tomllib.py xml xml.py zipfile zipfile.py diff --git a/fuzzer-textio.dict b/fuzzer-textio.dict new file mode 100644 index 0000000..1e9ee25 --- /dev/null +++ b/fuzzer-textio.dict @@ -0,0 +1,29 @@ +# UTF/latin-1 decode tokens: BOMs, partial sequences, newlines, surrogates +"\xef\xbb\xbf" +"\xff\xfe" +"\xfe\xff" +"\xff\xfe\x00\x00" +"\x00\x00\xfe\xff" +"\xc3" +"\xc3\xa9" +"\xe2\x82" +"\xe2\x82\xac" +"\xf0\x9f" +"\xf0\x9f\x98" +"\xd8" +"\xd8\x00" +"\xdc\x8c" +"\x00\xdc" +"\x00\x41" +"\x41\x00" +"\x00\x00\x00\x41" +"\x41\x00\x00\x00" +"\xff" +"\x80" +"\x0d" +"\x0d\x0a" +"\x0a\x0d" +"a\x0a" +"\x00\x0a" +"abc\x0d\x0a" +"\x1a" diff --git a/textio.py b/textio.py new file mode 100644 index 0000000..f709d61 --- /dev/null +++ b/textio.py @@ -0,0 +1,57 @@ +"""Fuzz io.TextIOWrapper over untrusted bytes. + +The incremental decoder, readahead buffer, newline normalisation, error +handlers and teardown paths are all C code (Modules/_io/*.c). The input +header selects a C-implemented encoding, an error handler, a newline +mode and a read pattern; only UnicodeDecodeError (the strict handler on +malformed bytes) is expected -- anything else propagates as a finding. +""" +import io + +MAX_TEXTIO_TEST_SIZE = 0x100000 # 1 MiB + +ENCODINGS = [ + "utf-8", + "utf-16", + "utf-16-le", + "utf-16-be", + "utf-32", + "utf-32-le", + "utf-32-be", + "latin-1", +] +ERRORS = ["strict", "replace", "ignore", "surrogateescape"] +NEWLINES = ["", "\n", "\r", "\r\n"] + + +def FuzzerRunOne(FuzzerInput): + if len(FuzzerInput) < 6 or len(FuzzerInput) > MAX_TEXTIO_TEST_SIZE: + return + encoding = ENCODINGS[FuzzerInput[0] % len(ENCODINGS)] + errors = ERRORS[FuzzerInput[1] % len(ERRORS)] + newline = NEWLINES[FuzzerInput[2] % len(NEWLINES)] + op = FuzzerInput[3] + chunk = 1 + FuzzerInput[4] % 64 + data = FuzzerInput[5:] + + # Only our own constant parameters are used here; a failure would be + # a bug and is allowed to propagate. + wrapper = io.TextIOWrapper( + io.BytesIO(data), encoding=encoding, errors=errors, newline=newline + ) + try: + pattern = op % 4 + if pattern == 0: + wrapper.read() + elif pattern == 1: + while wrapper.readline(): + pass + elif pattern == 2: + while wrapper.read(chunk): + pass + else: + for _ in wrapper: + pass + wrapper.close() + except UnicodeDecodeError: + pass