Skip to content

Commit 8dcbed3

Browse files
committed
Add fuzzer for pickle module
Fuzzes the CPython _pickle C module (Modules/_pickle.c). Dispatches to four operations: pickle.dumps() with protocols 0–5 over a mix of container types (bytes, str, int list, tuple, set, frozenset, bytearray, dict) built from fuzzed data; pickle.loads() driven through restricted and persistent-load Unpickler subclasses (including a fix_imports/bytes encoding variant); Pickler.dump() with memo clearing between objects; and a dumps/loads roundtrip to exercise both sides together.
1 parent dc49ef7 commit 8dcbed3

3 files changed

Lines changed: 145 additions & 2 deletions

File tree

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
all : 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
1+
all : 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-pickle
22

33
PYTHON_CONFIG_PATH=$(CPYTHON_INSTALL_PATH)/bin/python3-config
44
CXXFLAGS += $(shell $(PYTHON_CONFIG_PATH) --cflags)
5-
LDFLAGS += -rdynamic $(shell $(PYTHON_CONFIG_PATH) --ldflags --embed)
5+
LDFLAGS += -rdynamic $(shell $(PYTHON_CONFIG_PATH) --ldflags --embed) $(CPYTHON_MODLIBS) -Wl,--allow-multiple-definition
66

77
fuzzer-html:
88
clang++ $(CXXFLAGS) $(LIB_FUZZING_ENGINE) -std=c++17 fuzzer.cpp -DPYTHON_HARNESS_PATH="\"html.py\"" -ldl $(LDFLAGS) -o fuzzer-html
@@ -40,3 +40,6 @@ fuzzer-xml:
4040
clang++ $(CXXFLAGS) $(LIB_FUZZING_ENGINE) -std=c++17 fuzzer.cpp -DPYTHON_HARNESS_PATH="\"xml.py\"" -ldl $(LDFLAGS) -o fuzzer-xml
4141
fuzzer-zoneinfo:
4242
clang++ $(CXXFLAGS) $(LIB_FUZZING_ENGINE) -std=c++17 fuzzer.cpp -DPYTHON_HARNESS_PATH="\"zoneinfo.py\"" -ldl $(LDFLAGS) -o fuzzer-zoneinfo
43+
44+
fuzzer-pickle:
45+
clang++ $(CXXFLAGS) $(LIB_FUZZING_ENGINE) -std=c++17 fuzzer.cpp -DPYTHON_HARNESS_PATH="\"pickle.py\"" -ldl $(LDFLAGS) -o fuzzer-pickle

fuzz_targets.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ email email.py
77
html html.py
88
httpclient httpclient.py
99
json json.py
10+
pickle pickle.py
1011
plistlib plist.py
1112
re re.py
1213
tarfile tarfile.py

pickle.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from fuzzeddataprovider import FuzzedDataProvider
2+
import pickle
3+
import io
4+
5+
MAX_CONTAINER_SIZE = 200 # cap on generated container/string sizes to avoid OOM
6+
7+
# Top-level operation constants for FuzzerRunOne dispatch
8+
OP_DUMPS = 0
9+
OP_LOADS = 1
10+
OP_PICKLER = 2
11+
OP_ROUNDTRIP = 3
12+
13+
# Container type constants for build_container
14+
CTYPE_BYTES = 0
15+
CTYPE_STRING = 1
16+
CTYPE_INT_LIST = 2
17+
CTYPE_TUPLE = 3
18+
CTYPE_SET = 4
19+
CTYPE_FROZENSET = 5
20+
CTYPE_BYTEARRAY = 6
21+
CTYPE_DICT = 7
22+
23+
# Unpickler variant constants for op_loads
24+
VARIANT_RESTRICTED = 0
25+
VARIANT_PERSISTENT = 1
26+
VARIANT_RESTRICTED_FIX_IMPORTS = 2
27+
28+
29+
class RestrictedUnpickler(pickle.Unpickler):
30+
def find_class(self, module, name):
31+
raise pickle.UnpicklingError("restricted")
32+
33+
34+
class PersistentUnpickler(pickle.Unpickler):
35+
def persistent_load(self, pid):
36+
return pid
37+
38+
def find_class(self, module, name):
39+
raise pickle.UnpicklingError("restricted")
40+
41+
42+
def build_container(fdp, ctype):
43+
n = fdp.ConsumeIntInRange(0, min(fdp.remaining_bytes(), MAX_CONTAINER_SIZE))
44+
if ctype == CTYPE_BYTES:
45+
return fdp.ConsumeBytes(n)
46+
elif ctype == CTYPE_STRING:
47+
return fdp.ConsumeUnicode(n)
48+
elif ctype == CTYPE_INT_LIST:
49+
return fdp.ConsumeIntList(n, 1)
50+
elif ctype == CTYPE_TUPLE:
51+
return tuple(fdp.ConsumeIntList(n, 1))
52+
elif ctype == CTYPE_SET:
53+
return set(fdp.ConsumeIntList(n, 1))
54+
elif ctype == CTYPE_FROZENSET:
55+
return frozenset(fdp.ConsumeIntList(n, 1))
56+
elif ctype == CTYPE_BYTEARRAY:
57+
return bytearray(fdp.ConsumeBytes(n))
58+
elif ctype == CTYPE_DICT:
59+
d = {}
60+
entries = fdp.ConsumeIntInRange(0, min(n, 64))
61+
for _ in range(entries):
62+
if fdp.remaining_bytes() == 0:
63+
break
64+
kn = fdp.ConsumeIntInRange(1, 20)
65+
key = fdp.ConsumeUnicode(kn)
66+
val = fdp.ConsumeRandomValue()
67+
d[key] = val
68+
return d
69+
return fdp.ConsumeBytes(n)
70+
71+
72+
def op_dumps(fdp):
73+
ctype = fdp.ConsumeIntInRange(CTYPE_BYTES, CTYPE_DICT)
74+
protocol = fdp.ConsumeIntInRange(0, 5)
75+
fix_imports = fdp.ConsumeBool()
76+
obj = build_container(fdp, ctype)
77+
pickle.dumps(obj, protocol=protocol, fix_imports=fix_imports)
78+
79+
80+
def op_loads(fdp):
81+
variant = fdp.ConsumeIntInRange(VARIANT_RESTRICTED, VARIANT_RESTRICTED_FIX_IMPORTS)
82+
data = fdp.ConsumeBytes(fdp.remaining_bytes())
83+
bio = io.BytesIO(data)
84+
if variant == VARIANT_RESTRICTED:
85+
unpickler = RestrictedUnpickler(bio)
86+
elif variant == VARIANT_PERSISTENT:
87+
unpickler = PersistentUnpickler(bio)
88+
else:
89+
unpickler = RestrictedUnpickler(bio, fix_imports=True, encoding="bytes")
90+
unpickler.load()
91+
92+
93+
def op_pickler(fdp):
94+
protocol = fdp.ConsumeIntInRange(0, 5)
95+
n = (
96+
fdp.ConsumeIntInRange(1, min(fdp.remaining_bytes(), MAX_CONTAINER_SIZE))
97+
if fdp.remaining_bytes() > 0
98+
else 0
99+
)
100+
if n == 0:
101+
return
102+
obj1 = fdp.ConsumeIntList(n, 1)
103+
s = fdp.ConsumeUnicode(fdp.ConsumeIntInRange(0, MAX_CONTAINER_SIZE))
104+
bio = io.BytesIO()
105+
p = pickle.Pickler(bio, protocol)
106+
p.dump(obj1)
107+
p.clear_memo()
108+
p.dump(s)
109+
bio.getvalue()
110+
111+
112+
def op_roundtrip(fdp):
113+
ctype = fdp.ConsumeIntInRange(CTYPE_BYTES, CTYPE_DICT)
114+
obj = build_container(fdp, ctype)
115+
dumped = pickle.dumps(obj)
116+
pickle.loads(dumped)
117+
118+
119+
# Fuzzes the _pickle C module (Modules/_pickle.c). Exercises pickle.dumps()
120+
# with protocols 0-5 on various container types (bytes, strings, int lists,
121+
# tuples, sets, frozensets, bytearrays, dicts), pickle.loads() with
122+
# restricted and persistent-load unpickler variants, Pickler.dump() with
123+
# memo clearing, and dumps/loads roundtrips.
124+
def FuzzerRunOne(FuzzerInput):
125+
if len(FuzzerInput) < 1 or len(FuzzerInput) > 0x100000:
126+
return
127+
fdp = FuzzedDataProvider(FuzzerInput)
128+
op = fdp.ConsumeIntInRange(OP_DUMPS, OP_ROUNDTRIP)
129+
try:
130+
if op == OP_DUMPS:
131+
op_dumps(fdp)
132+
elif op == OP_LOADS:
133+
op_loads(fdp)
134+
elif op == OP_PICKLER:
135+
op_pickler(fdp)
136+
else:
137+
op_roundtrip(fdp)
138+
except Exception:
139+
pass

0 commit comments

Comments
 (0)