Skip to content

Commit 186cdb8

Browse files
authored
fix: tighten python fuzzing examples (#19)
1 parent 4b48e77 commit 186cdb8

4 files changed

Lines changed: 88 additions & 28 deletions

File tree

README.md

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -198,23 +198,23 @@ same seed or fuzzer input.
198198
### Integration with Atheris
199199

200200
Use the `PickleMutator` class for structure-aware fuzzing:
201+
Pass generated pickle bytes to the parser you actually want to fuzz. Do not call
202+
`pickle.loads()` on generated data outside a sandbox.
201203

202204
```python
203205
import atheris
206+
import sys
204207
from pickle_fuzzer.fuzzer import PickleMutator
205-
import pickle
206-
207-
mutator = PickleMutator(protocol=3)
208208

209209
@atheris.instrument_func
210210
def test_one_input(data: bytes):
211211
# Generate a valid pickle from fuzzer input within the requested budget
212-
pickle_bytes = mutator.mutate(data, max_size=10000)
213-
214-
try:
215-
pickle.loads(pickle_bytes)
216-
except Exception:
217-
pass # Expected - looking for crashes
212+
input_mutator = PickleMutator(protocol=3)
213+
pickle_bytes = input_mutator.mutate(data, max_size=10000)
214+
215+
# Call the parser you actually want to fuzz with pickle_bytes here.
216+
# For example: target.parse_pickle(pickle_bytes)
217+
...
218218

219219
atheris.Setup(sys.argv, test_one_input)
220220
atheris.Fuzz()
@@ -275,6 +275,9 @@ For detailed fuzzing documentation, see [fuzz/README.md](fuzz/README.md).
275275

276276
### Fuzzing Custom Pickle Parsers
277277

278+
Do not point these harnesses at `pickle.loads()` unless the unpickling step runs
279+
inside a sandbox you control.
280+
278281
```python
279282
#!/usr/bin/env python3
280283
import atheris
@@ -283,9 +286,9 @@ from pickle_fuzzer.fuzzer import fuzz_pickle_parser
283286

284287
# Your custom pickle parser
285288
def my_pickle_parser(data: bytes):
286-
# Your parsing logic here
287-
import pickle
288-
return pickle.loads(data)
289+
# Replace this with the parser entrypoint you actually want to fuzz.
290+
# Example: return my_project.parse_pickle(data)
291+
...
289292

290293
if __name__ == "__main__":
291294
# Use structure-aware generation
@@ -300,21 +303,31 @@ if __name__ == "__main__":
300303

301304
```python
302305
import atheris
306+
import io
303307
import pickle
308+
import sys
304309
from pickle_fuzzer.fuzzer import PickleMutator
305310

306-
class CustomUnpickler(pickle.Unpickler):
307-
def find_class(self, module, name):
308-
# Custom class resolution logic
309-
return super().find_class(module, name)
311+
_ALLOWED_GLOBALS = {
312+
# Add only the globals your target intentionally supports.
313+
}
310314

311-
mutator = PickleMutator(protocol=3)
315+
316+
class RestrictedUnpickler(pickle.Unpickler):
317+
def find_class(self, module, name):
318+
try:
319+
return _ALLOWED_GLOBALS[(module, name)]
320+
except KeyError as exc:
321+
raise pickle.UnpicklingError(
322+
f"global '{module}.{name}' is forbidden in this harness"
323+
) from exc
312324

313325
@atheris.instrument_func
314326
def test_custom_unpickler(data: bytes):
315-
pickle_bytes = mutator.mutate(data, max_size=10000)
327+
input_mutator = PickleMutator(protocol=3)
328+
pickle_bytes = input_mutator.mutate(data, max_size=10000)
316329
try:
317-
CustomUnpickler(io.BytesIO(pickle_bytes)).load()
330+
RestrictedUnpickler(io.BytesIO(pickle_bytes)).load()
318331
except Exception:
319332
pass
320333

python/examples/harness.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@
2222

2323
with atheris.instrument_imports():
2424
import sys
25-
import pickle
2625
# import your fuzz target
2726

2827

2928
def TestOneInput(data: bytes) -> None:
30-
"""Test Python's pickle.loads() with generated data."""
29+
"""Generate structured pickle data and pass it to your parser."""
3130
if not data:
3231
return
3332
proto = data[0] % 6
@@ -37,6 +36,8 @@ def TestOneInput(data: bytes) -> None:
3736
try:
3837
# call your fuzz target with pickle_bytes
3938
# e.g. target.parse(pickle_bytes)
39+
_ = pickle_bytes
40+
...
4041
except Exception:
4142
...
4243

python/pickle_fuzzer/fuzzer.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,14 @@ class PickleMutator:
3535
```python
3636
import atheris
3737
from pickle_fuzzer.fuzzer import PickleMutator
38-
39-
mutator = PickleMutator(protocol=4)
40-
38+
4139
@atheris.instrument_func
4240
def fuzz_target(data):
43-
pickle_bytes = mutator.mutate(data, max_size=10000)
41+
input_mutator = PickleMutator(protocol=4)
42+
pickle_bytes = input_mutator.mutate(data, max_size=10000)
4443
# test your pickle parser with pickle_bytes
4544
...
46-
45+
4746
atheris.Setup(sys.argv, fuzz_target)
4847
atheris.Fuzz()
4948
```
@@ -124,8 +123,7 @@ def test_one_input(data: bytes) -> None:
124123
# Test the parser
125124
parser_func(pickle_bytes)
126125
except Exception:
127-
print(f'Failed to parse pickle: {data}')
128-
pass # Expected - we're looking for crashes/hangs
126+
pass # Parser exceptions are expected; we're looking for crashes/hangs.
129127

130128
atheris.Setup(sys.argv, test_one_input)
131129
atheris.Fuzz()

python/tests/test_fuzzer.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
# SPDX-License-Identifier: Apache-2.0
1616

1717
import pickletools
18+
import py_compile
19+
from pathlib import Path
1820

21+
import pickle_fuzzer.fuzzer as fuzz_module
1922
from pickle_fuzzer.fuzzer import PickleMutator
2023

2124

@@ -40,6 +43,9 @@ def test_pickle_mutator_fallback_stays_valid(monkeypatch):
4043
mutator = PickleMutator(protocol=4, seed=123)
4144

4245
class FailingGenerator:
46+
def reset(self):
47+
return None
48+
4349
def generate_from_bytes(self, *args, **kwargs):
4450
raise RuntimeError("boom")
4551

@@ -49,3 +55,45 @@ def generate_from_bytes(self, *args, **kwargs):
4955

5056
assert len(data) <= 32
5157
assert_whole_pickle_consumed(data)
58+
59+
60+
def test_pickle_mutator_reuse_matches_fresh_instance():
61+
reused_mutator = PickleMutator(protocol=4, seed=123)
62+
fresh_mutator = PickleMutator(protocol=4, seed=123)
63+
64+
first = reused_mutator.mutate(b"alpha", max_size=256)
65+
second = reused_mutator.mutate(b"beta", max_size=256)
66+
fresh_second = fresh_mutator.mutate(b"beta", max_size=256)
67+
68+
assert second == fresh_second
69+
assert second != first
70+
assert_whole_pickle_consumed(second)
71+
72+
73+
def test_fuzz_pickle_parser_swallows_parser_exceptions_without_logging(
74+
monkeypatch, capsys
75+
):
76+
captured = {}
77+
78+
monkeypatch.setattr(fuzz_module.atheris, "instrument_func", lambda func: func)
79+
monkeypatch.setattr(
80+
fuzz_module.atheris,
81+
"Setup",
82+
lambda _argv, func: captured.setdefault("callback", func),
83+
)
84+
monkeypatch.setattr(fuzz_module.atheris, "Fuzz", lambda: None)
85+
86+
def failing_parser(_data: bytes) -> None:
87+
raise ValueError("boom")
88+
89+
fuzz_module.fuzz_pickle_parser(failing_parser, protocol=3, use_structure_aware=False)
90+
captured["callback"](b"input")
91+
92+
output = capsys.readouterr()
93+
assert output.out == ""
94+
assert output.err == ""
95+
96+
97+
def test_example_harness_compiles():
98+
harness_path = Path(__file__).resolve().parents[1] / "examples" / "harness.py"
99+
py_compile.compile(str(harness_path), doraise=True)

0 commit comments

Comments
 (0)