From dc7b91276ce75142eb26508a63b49e9a160cd38e Mon Sep 17 00:00:00 2001 From: Arthur Chan Date: Mon, 9 Nov 2026 04:32:26 +0000 Subject: [PATCH] OSS-Fuzz: Add new fuzzer/corpus/dict/script targets text IO Signed-off-by: Arthur Chan --- Makefile | 5 +++- corp-textio/bom_only | Bin 0 -> 9 bytes corp-textio/empty_input | Bin 0 -> 5 bytes corp-textio/latin1_read | Bin 0 -> 16 bytes corp-textio/surrogateesc | Bin 0 -> 17 bytes corp-textio/utf16_odd | Bin 0 -> 10 bytes corp-textio/utf16be_read | Bin 0 -> 29 bytes corp-textio/utf16bom_read | Bin 0 -> 31 bytes corp-textio/utf16le_read | Bin 0 -> 29 bytes corp-textio/utf32be_iter | Bin 0 -> 53 bytes corp-textio/utf32bom_read | Bin 0 -> 57 bytes corp-textio/utf32le_readn | Bin 0 -> 53 bytes corp-textio/utf8_crlf_iter | Bin 0 -> 235 bytes corp-textio/utf8_read | Bin 0 -> 230 bytes corp-textio/utf8_readlines | Bin 0 -> 230 bytes corp-textio/utf8_readn | Bin 0 -> 230 bytes corp-textio/utf8_truncated | Bin 0 -> 13 bytes fuzz_targets.txt | 1 + fuzzer-textio.dict | 29 +++++++++++++++++++ textio.py | 57 +++++++++++++++++++++++++++++++++++++ 20 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 corp-textio/bom_only create mode 100644 corp-textio/empty_input create mode 100644 corp-textio/latin1_read create mode 100644 corp-textio/surrogateesc create mode 100644 corp-textio/utf16_odd create mode 100644 corp-textio/utf16be_read create mode 100644 corp-textio/utf16bom_read create mode 100644 corp-textio/utf16le_read create mode 100644 corp-textio/utf32be_iter create mode 100644 corp-textio/utf32bom_read create mode 100644 corp-textio/utf32le_readn create mode 100644 corp-textio/utf8_crlf_iter create mode 100644 corp-textio/utf8_read create mode 100644 corp-textio/utf8_readlines create mode 100644 corp-textio/utf8_readn create mode 100644 corp-textio/utf8_truncated create mode 100644 fuzzer-textio.dict create mode 100644 textio.py 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 0000000000000000000000000000000000000000..9850b79696781b5b9a55ac04c26dbe064f1f7c43 GIT binary patch literal 9 QcmZQ%U|`_*|L^}l017Mw1ONa4 literal 0 HcmV?d00001 diff --git a/corp-textio/empty_input b/corp-textio/empty_input new file mode 100644 index 0000000000000000000000000000000000000000..5320c681eeb2973868792dec4d7ccc7c1f1517ff GIT binary patch literal 5 McmZQzU|`??000dD2><{9 literal 0 HcmV?d00001 diff --git a/corp-textio/latin1_read b/corp-textio/latin1_read new file mode 100644 index 0000000000000000000000000000000000000000..f027404a513d275491ed17a174b4067ffc4adff7 GIT binary patch literal 16 XcmZQ)U|?`aPE32LkeB$rER_oYA+!Y^ literal 0 HcmV?d00001 diff --git a/corp-textio/surrogateesc b/corp-textio/surrogateesc new file mode 100644 index 0000000000000000000000000000000000000000..63214b00ac3c899ff3d06cf54951a4b7d86447b7 GIT binary patch literal 17 YcmZQzW@O|jN-S6SuaHz(l3L6K03m+_$N&HU literal 0 HcmV?d00001 diff --git a/corp-textio/utf16_odd b/corp-textio/utf16_odd new file mode 100644 index 0000000000000000000000000000000000000000..254078adaddd722dffa62b953c3eb240ffe6ae68 GIT binary patch literal 10 RcmZQ%U|`_LV8~?P0ssS%0ObGx literal 0 HcmV?d00001 diff --git a/corp-textio/utf16be_read b/corp-textio/utf16be_read new file mode 100644 index 0000000000000000000000000000000000000000..f4ed0675598a0c7a32bef5d390453bad8c0c50a5 GIT binary patch literal 29 hcmZQ(Vq|h)$Y6NMkOPGI3=Xtr1^`r21-AeI literal 0 HcmV?d00001 diff --git a/corp-textio/utf32be_iter b/corp-textio/utf32be_iter new file mode 100644 index 0000000000000000000000000000000000000000..55c206ca37a4b71bd40fe134123c6e107f592cc7 GIT binary patch literal 53 ocmZQ$U}P3xU|`4q;+H_21I773S^LR!Az!FV9m*%db$#D$Ok@R>&_)Em9}}O6Me2RVt+9r|aLR!Az!FV9m*%db$#D$Ok@R>&_)Em9}}O6Me2RVt+9r|WSI GMa2NJhfrPs literal 0 HcmV?d00001 diff --git a/corp-textio/utf8_readlines b/corp-textio/utf8_readlines new file mode 100644 index 0000000000000000000000000000000000000000..3453bcf8036ba6a723a8dcd8c2b0fecd28802e6f GIT binary patch literal 230 zcmZQzU}RJX$w*ZwEX_>LR!Az!FV9m*%db$#D$Ok@R>&_)Em9}}O6Me2RVt+9r|WSI GMa2Ngeo%M- literal 0 HcmV?d00001 diff --git a/corp-textio/utf8_readn b/corp-textio/utf8_readn new file mode 100644 index 0000000000000000000000000000000000000000..8f5983c1bac46e09bfd0d00ed314bfe4a10c5308 GIT binary patch literal 230 zcmZQzU|LR!Az!FV9m*%db$#D$Ok@R>&_)Em9}}O6Me2RVt+9r|WSI GMa2NV15j%K literal 0 HcmV?d00001 diff --git a/corp-textio/utf8_truncated b/corp-textio/utf8_truncated new file mode 100644 index 0000000000000000000000000000000000000000..11991c47aa8187e53a9a0044bd76f07191570402 GIT binary patch literal 13 UcmZQzU|`_Lc$t%vuTXv%01{;b`v3p{ literal 0 HcmV?d00001 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