You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hello,
I maintain and run a fuzzer called fusil. It is a fuzzer for Python that has been successfully used to find bugs in both the CPython and PyPy interpreters. In a PoC run, it seems able to fuzz RustPython.
I'd like to know whether you'd be interested in a fuzzing campaign targeting RustPython, so I don't spend time and energy on it if it's not a good fit for you.
Hello, Thank you for contacting. Could you try a pilot run to find 5-20 bugs? I'd like to check it will be worth or not.
I believe fuzzer will find a lot of bugs. But if the reports are duplication of CPython unittests, which probably already has the case reported by your tool, that means we are not ready to seriously run fuzzer.
The first run against RustPython 0.5.0, for about 50 minutes on 4 instances, turned up a batch of pure-Python programs that panic or segfault the interpreter where CPython raises a normal exception.
Here's a sample. Each row is a complete reproducer — paste it into rustpython -c "…":
reproducer
crash
CPython does
import pwd; pwd.struct_passwd().pw_name
panic types/structseq.rs:311 — index out of bounds (a struct-sequence built with fewer elements than its named fields; the field getter indexes with no bounds check)
TypeError at construction
import _md5; _md5.md5()
panic class.rs:87 — static type has not been initialized
returns an md5 object
import _csv; _csv.reader([]).__next__()
panic stdlib/src/csv.rs:805 — unwrap() on None (unregistered dialect line terminator)
works
import _typing; _typing._idfunc()
panic stdlib/_typing.rs:43 — index out of bounds (args[0], no arity check)
TypeError
eval(chr(0xd800))
panic stdlib/builtins.rs:557 — PyStr contains surrogates (expect_str on a lone-surrogate string)
SIGSEGV in _sreMatch::as_mapping — an uninitialized Match (via __new__) has garbage regs/string; the mapping-subscript path reads them with no init guard (group()/__repr__ do guard it)
TypeError
That last one is memory-unsafety (a segfault, not a clean panic). Two more that take a few lines rather than one:
The most common panic (48 hits): stdlib/_thread.rs:977RefCell already borrowed.cleanup_thread_local_data holds LOCAL_GUARDS.borrow_mut() across .clear(), which drops the guards; a dropped thread-local value whose __del__ re-enters _thread._local borrows LOCAL_GUARDS again → BorrowMutError. (Full reproducer + root cause in the catalog.)
A segfault class: native stack overflow from unbounded recursion — hashing/comparing a deep or cyclic object recurses on the native stack with no depth guard → SIGSEGV/SIGABRT, where CPython raises RecursionError.
Due to how the fuzzer works, it's expected that many more issues are found in the first hours of fuzzing than on the long run.
If this is useful I'm happy to file these individually or as an umbrella issue, minimize the remaining segfaults, or run more fuzzing, whatever's most helpful.
Found with fusil. Investigation, root causes, fixes, reproducers and draft created with help from Claude Code
Regarding whether these findings are duplications of CPython unittests: 2 of 9 are directly covered by a CPython test; the other 7 exercise inputs/behaviors the suite doesn't — including one where RustPython passes the relevant CPython tests.
test_re.py:3160check_disallow_instantiation(re.Match) asserts it can't be instantiated. RustPython letsre.Match() construct → the assertion fails, and [...] on the instance segfaults.
YES — running test_re catches it.
RUSTPY-0004 csv reader unwrap
reader iterates fine
test_csv.py iterates readers pervasively. In RustPython every reader iteration panics (csv.reader(['a,b']) panics via get_lineterminator).
No test constructs a too-short / no-arg struct-seq and then reads a field. test_structseq tests the evil-__getitem__ case (correct length) and copy/replace, not this.
no
RUSTPY-0005_typing._idfunc() arity OOB
TypeError (wrong arity)
test_typing never calls the internal _typing._idfunc with zero args.
no
RUSTPY-0006eval/compile surrogate source
UnicodeEncodeError
Notest_compile/test_builtin test compiles/evals a lone-surrogate source string (surrogate tests there are for ascii()/repr(), not compile).
no
RUSTPY-0001_thread._local teardown re-entrancy
works
test_threading_local has test_derived_cycle_dealloc (a local subclass with a ref cycle) — adjacent, but not the "a stored value's __del__ re-enters _local while the owning thread runs cleanup" trigger.
no (adjacent only)
RUSTPY-0003 "static type not initialized"
n/a — CPython has no such failure mode
It's a RustPython-internal type-init issue (tracked as #5210), not a CPython behavior; nothing in CPython's suite targets it.
no (RustPython-specific)
RUSTPY-0007a recursion → native stack overflow
RecursionError
test_json/test_recursion (incl. test_highly_nested_objects_encoding/decoding, "doesn't segfault"), test_richcmp, test_isinstance test recursion. But RustPython PASSES these — it guards cyclic json.dumps (raises), deep-nested json (raises), recursive __eq__/hash/compare (RecursionError). The fleet's segfaults come from other, more specific object graphs.
no — RustPython passes the standard recursion tests; the fuzzer found triggers they don't cover. (Known class = #2796.)
Thank you for running this pilot. It found cases where RustPython crashed but CPython did not. Some of them led to fixes in #8358 and #8371, so I think this approach is useful.
My concern is that it may be too early to run a lot of fuzzing while CPython compatibility are still improving. If some parts already behave like CPython in most cases, would it make sense to start fuzzing those parts?
I can restrict the findings to hard crashes and/or panics, or to specific code areas worth fuzzing, but would have to be guided towards which areas make the most sense.
Alternatively, since I totally jumped the gun and already ran a full fuzzing campaign against RustPython (about 20 crashes found), I could just show you what I have found and you pick and choose.
@youknowone, do you think it makes sense to focus fuzzing on a few modules first? If so, are there any modules with good CPython compatibility that would be useful fuzzing targets?
An AI-assisted check of the current CPython test results for RustPython suggested operator, math, random, array, and set, but I’m not sure whether they are suitable.
@devdanzin Thank you for much for trying this! I reviewed the reports and it looks very useful. In general fuzzing is more useful for mature codebase. In that pov, I think threading/multiprocessing/gc is not ready to fuzzy.
I also like the format "finding | CPython does | CPython test that exercises it | covered?", this is very useful.
To leverage AI agents to automate to solve it, creating issue (or subtask) for each bug will be more convenient.
Status on the catalog, from working through it in #8514 and #8518.
One lead turned out to be live: RPYR-0008
The record has it as "LEAD — not yet reproduced", with the monkeypatch left as a TODO. It does reproduce. The attribute new_invalid_state_error consults is asyncio.InvalidStateError — the lookup imports asyncio.exceptions at level 0, which hands back the package — so patching that name is what triggers it:
import_asyncio, asyncioasyncio.InvalidStateError=lambda*args: 42_asyncio.Future(loop=object()).result()
# thread 'main' panicked at crates/stdlib/src/_asyncio.rs:2753:# called `Result::unwrap()` on an `Err` value: [PyObject PyInt { value: 42 }]
CPython is immune because it caches the class in module state at init and raises it with PyErr_SetString, so a later rebinding is never consulted.
Fixed in #8518: the type is looked up the way get_cancelled_error_type looks its own up, and a lookup that does not produce an exception type falls back to RuntimeError, which is what the neighbouring arms of that function already did.
Four records no longer reproduce on main
id
current behaviour
RUSTPY-0001 thread-local RefCell reentrancy
runs to completion — cleanup_thread_local_data takes the vector out of LOCAL_GUARDS before dropping it (_thread.rs:646), so a __del__ that registers a new guard cannot re-borrow
RUSTPY-0009repr(staticmethod(obj))
propagates ValueError: boom; the unwrap is gone
RUSTPY-0011repr(classmethod(obj))
same
RUSTPY-0010binascii.b2a_qp(b'\n')
returns; the scan is guarded with in_idx > 0 (binascii.rs:507)
Each was re-run from the catalog's own repro.py against a build of current main. Nothing to fix; the records can be closed as fixed.
One I could not reproduce: RUSTPY-0007 face 7c
The object::core::PyInner face, reported in the selectors and asyncio_queues vehicles. The report marks it as needing a per-crash-dir gdb pass to separate it from 7a, and the crash dirs are not public, so this is only what could be tried from outside:
7a's guards landed in Fix 33 reproduced fuzzing and static-review defects #8514 (hash-slot dispatch, genericalias.make_parameters). 18 deep-nesting shapes — gc.collect, copy.deepcopy, marshal, json, pickle, repr, hash, ==, and dropping 100k-deep list/tuple/dict/instance chains on the main thread and on a worker — all raise RecursionError or complete normally.
Both named vehicle surfaces, driven hard. selectors: fileno() implementations that lie (a different answer every call), raise, return out-of-range or non-int descriptors, close the descriptor they just handed out, or unregister the object from inside fileno(), with register/modify/unregister/get_key/select interleaved from four threads. asyncio queues: Queue/LifoQueue/PriorityQueue carrying items whose __lt__ mutates the collection, with _asyncio._register_task/_unregister_task/_enter_task/_leave_task/all_tasks/current_task fed non-task objects from three threads. No crash.
10 object-core abuse shapes: resurrection in __del__ (plain and inside a cycle), weakref callbacks that resurrect, a __del__ that calls gc.collect(), __class__ and __bases__ reassignment across layouts, instances of a type that has been deleted, frame.clear() on a suspended frame, slot-wrapper descriptors called on foreign objects. No crash.
If a backtrace or a crash dir for one of the 7c vehicles can be shared, I'll take another pass at it. For what it is worth, my guess is that it is the PyAtomicRefDebug type confusion fixed in #8514 — that one formatted an object header as payload bytes and segfaulted inside the object core, and the way to reach it was _asyncio._enter_task's error message, which fits an asyncio vehicle — but that is a guess, not a diagnosis.
Everything else
#8514 (merged) covers 34 records in 33 commits — RPYR-0013 and RPYR-0014 share one fix. #8518 covers RUSTPY-0019 (contextvars under threads), RUSTPY-0023 (concurrent generator resumption) and RPYR-0008, plus an itertools.tee collector leak that turned up beside them and is not in the catalog. That leaves 7c as the only record without a fix.
Activity
youknowone commentedon Jul 19, 2026
Hello, Thank you for contacting. Could you try a pilot run to find 5-20 bugs? I'd like to check it will be worth or not.
I believe fuzzer will find a lot of bugs. But if the reports are duplication of CPython unittests, which probably already has the case reported by your tool, that means we are not ready to seriously run fuzzer.
devdanzin commentedon Jul 19, 2026
The first run against RustPython 0.5.0, for about 50 minutes on 4 instances, turned up a batch of pure-Python programs that panic or segfault the interpreter where CPython raises a normal exception.
Here's a sample. Each row is a complete reproducer — paste it into
rustpython -c "…":import pwd; pwd.struct_passwd().pw_nametypes/structseq.rs:311—index out of bounds(a struct-sequence built with fewer elements than its named fields; the field getter indexes with no bounds check)TypeErrorat constructionimport _md5; _md5.md5()class.rs:87—static type has not been initializedimport _csv; _csv.reader([]).__next__()stdlib/src/csv.rs:805—unwrap()onNone(unregistered dialect line terminator)import _typing; _typing._idfunc()stdlib/_typing.rs:43—index out of bounds(args[0], no arity check)TypeErroreval(chr(0xd800))stdlib/builtins.rs:557—PyStr contains surrogates(expect_stron a lone-surrogate string)ValueErrorimport re; M=type(re.match('a','a')); M.__new__(M)[0]_sreMatch::as_mapping— an uninitializedMatch(via__new__) has garbageregs/string; the mapping-subscript path reads them with no init guard (group()/__repr__do guard it)TypeErrorThat last one is memory-unsafety (a segfault, not a clean panic). Two more that take a few lines rather than one:
stdlib/_thread.rs:977RefCell already borrowed.cleanup_thread_local_dataholdsLOCAL_GUARDS.borrow_mut()across.clear(), which drops the guards; a dropped thread-local value whose__del__re-enters_thread._localborrowsLOCAL_GUARDSagain →BorrowMutError. (Full reproducer + root cause in the catalog.)RecursionError.Full catalog — one report per crash, each with a root cause and a fix sketch:
https://github.com/devdanzin/rustpython-findings
An example of what one report looks like (the
re.Matchsegfault):https://gist.github.com/devdanzin/6e8f53711ea6b9c311e2c8fa043b17ca
Due to how the fuzzer works, it's expected that many more issues are found in the first hours of fuzzing than on the long run.
If this is useful I'm happy to file these individually or as an umbrella issue, minimize the remaining segfaults, or run more fuzzing, whatever's most helpful.
Found with fusil. Investigation, root causes, fixes, reproducers and draft created with help from Claude Code
devdanzin commentedon Jul 19, 2026
Regarding whether these findings are duplications of CPython unittests: 2 of 9 are directly covered by a CPython test; the other 7 exercise inputs/behaviors the suite doesn't — including one where RustPython passes the relevant CPython tests.
re.Match.__new__segfaultre.Match()→TypeError: cannot create 're.Match' instancestest_re.py:3160check_disallow_instantiation(re.Match)asserts it can't be instantiated. RustPython letsre.Match()construct → the assertion fails, and[...]on the instance segfaults.test_recatches it.test_csv.pyiterates readers pervasively. In RustPython every reader iteration panics (csv.reader(['a,b'])panics viaget_lineterminator).test_csvwould crash immediately.pwd.struct_passwd()→TypeError: missing required argument 'sequence'test_structseqtests the evil-__getitem__case (correct length) and copy/replace, not this._typing._idfunc()arity OOBTypeError(wrong arity)test_typingnever calls the internal_typing._idfuncwith zero args.eval/compilesurrogate sourceUnicodeEncodeErrortest_compile/test_builtintest compiles/evals a lone-surrogate source string (surrogate tests there are forascii()/repr(), notcompile)._thread._localteardown re-entrancytest_threading_localhastest_derived_cycle_dealloc(a local subclass with a ref cycle) — adjacent, but not the "a stored value's__del__re-enters_localwhile the owning thread runs cleanup" trigger.RecursionErrortest_json/test_recursion(incl.test_highly_nested_objects_encoding/decoding, "doesn't segfault"),test_richcmp,test_isinstancetest recursion. But RustPython PASSES these — it guards cyclicjson.dumps(raises), deep-nested json (raises), recursive__eq__/hash/compare (RecursionError). The fleet's segfaults come from other, more specific object graphs.YangSiJun528 commentedon Jul 26, 2026
Thank you for running this pilot. It found cases where RustPython crashed but CPython did not. Some of them led to fixes in #8358 and #8371, so I think this approach is useful.
My concern is that it may be too early to run a lot of fuzzing while CPython compatibility are still improving. If some parts already behave like CPython in most cases, would it make sense to start fuzzing those parts?
devdanzin commentedon Jul 26, 2026
Thanks for the feedback!
I can restrict the findings to hard crashes and/or panics, or to specific code areas worth fuzzing, but would have to be guided towards which areas make the most sense.
Alternatively, since I totally jumped the gun and already ran a full fuzzing campaign against RustPython (about 20 crashes found), I could just show you what I have found and you pick and choose.
Happy to proceed however you want me to.
YangSiJun528 commentedon Jul 28, 2026
@youknowone, do you think it makes sense to focus fuzzing on a few modules first? If so, are there any modules with good CPython compatibility that would be useful fuzzing targets?
An AI-assisted check of the current CPython test results for RustPython suggested
operator,math,random,array, andset, but I’m not sure whether they are suitable.youknowone commentedon Aug 4, 2026
@devdanzin Thank you for much for trying this! I reviewed the reports and it looks very useful. In general fuzzing is more useful for mature codebase. In that pov, I think threading/multiprocessing/gc is not ready to fuzzy.
I also like the format "finding | CPython does | CPython test that exercises it | covered?", this is very useful.
To leverage AI agents to automate to solve it, creating issue (or subtask) for each bug will be more convenient.
youknowone commentedon Aug 14, 2026
Status on the catalog, from working through it in #8514 and #8518.
One lead turned out to be live: RPYR-0008
The record has it as "LEAD — not yet reproduced", with the monkeypatch left as a TODO. It does reproduce. The attribute
new_invalid_state_errorconsults isasyncio.InvalidStateError— the lookup importsasyncio.exceptionsat level 0, which hands back the package — so patching that name is what triggers it:CPython is immune because it caches the class in module state at init and raises it with
PyErr_SetString, so a later rebinding is never consulted.Fixed in #8518: the type is looked up the way
get_cancelled_error_typelooks its own up, and a lookup that does not produce an exception type falls back toRuntimeError, which is what the neighbouring arms of that function already did.Four records no longer reproduce on main
RUSTPY-0001thread-local RefCell reentrancycleanup_thread_local_datatakes the vector out ofLOCAL_GUARDSbefore dropping it (_thread.rs:646), so a__del__that registers a new guard cannot re-borrowRUSTPY-0009repr(staticmethod(obj))ValueError: boom; theunwrapis goneRUSTPY-0011repr(classmethod(obj))RUSTPY-0010binascii.b2a_qp(b'\n')in_idx > 0(binascii.rs:507)Each was re-run from the catalog's own
repro.pyagainst a build of current main. Nothing to fix; the records can be closed as fixed.One I could not reproduce: RUSTPY-0007 face 7c
The
object::core::PyInnerface, reported in theselectorsandasyncio_queuesvehicles. The report marks it as needing a per-crash-dir gdb pass to separate it from 7a, and the crash dirs are not public, so this is only what could be tried from outside:genericalias.make_parameters). 18 deep-nesting shapes —gc.collect,copy.deepcopy,marshal,json,pickle,repr,hash,==, and dropping 100k-deep list/tuple/dict/instance chains on the main thread and on a worker — all raiseRecursionErroror complete normally.selectors:fileno()implementations that lie (a different answer every call), raise, return out-of-range or non-int descriptors, close the descriptor they just handed out, or unregister the object from insidefileno(), withregister/modify/unregister/get_key/selectinterleaved from four threads.asyncioqueues:Queue/LifoQueue/PriorityQueuecarrying items whose__lt__mutates the collection, with_asyncio._register_task/_unregister_task/_enter_task/_leave_task/all_tasks/current_taskfed non-task objects from three threads. No crash.__del__(plain and inside a cycle), weakref callbacks that resurrect, a__del__that callsgc.collect(),__class__and__bases__reassignment across layouts, instances of a type that has been deleted,frame.clear()on a suspended frame, slot-wrapper descriptors called on foreign objects. No crash.If a backtrace or a crash dir for one of the 7c vehicles can be shared, I'll take another pass at it. For what it is worth, my guess is that it is the
PyAtomicRefDebugtype confusion fixed in #8514 — that one formatted an object header as payload bytes and segfaulted inside the object core, and the way to reach it was_asyncio._enter_task's error message, which fits an asyncio vehicle — but that is a guess, not a diagnosis.Everything else
#8514 (merged) covers 34 records in 33 commits —
RPYR-0013andRPYR-0014share one fix. #8518 coversRUSTPY-0019(contextvars under threads),RUSTPY-0023(concurrent generator resumption) andRPYR-0008, plus anitertools.teecollector leak that turned up beside them and is not in the catalog. That leaves 7c as the only record without a fix.— commented by Claude