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
{{ message }}
Repository navigation
gcmodule: GIL drop during collection in debug mode may result in unexpected behavior with multiple threads #112564
By default, garbage collection is performed without dropping the GIL. However, when debug mode is enabled using gc.set_debug, functions PySys_WriteStderr and PySys_FormatStderr are used to print debug information, potentially leading to GIL drop. In a multi-threaded application, this behavior can result in the execution of code in other threads while the garbage collector (GC) is in the process of collection.
gc.DEBUG_STATS seems to be safe, only possibly disrupting GC's debug info output and hypothetically causing issues for callbacks in gc.callbacks if they exist.
gc.DEBUG_COLLECTABLE can cause segmentation fault.
Code causing segmentation fault
importgcimportthreadingfromtimeimportsleepfromweakrefimportrefclassObject:
passRANGE=10000defmain_thread():
for_inrange(RANGE):
gc.collect()
sleep(0)
defside_thread_segmentation_fault():
for_inrange(RANGE):
references= []
for_inrange(20):
_object=Object()
_list= [_object]
_object._list=_listreferences.append(ref(_object))
collected=gc.collect()
ifcollected==0andany(_object() for_objectinreferences):
# GC started in the main thread and has dropped the GIL# somewhere in the middle of `gc_collect_main`forreferenceinreversed(references):
ifreference() isnotNone:
_object=reference()
_list=_object._list_list.remove(_object)
del_object._listsleep(0)
sleep(0)
gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_COLLECTABLE)
t=threading.Thread(target=side_thread_segmentation_fault)
t.start()
main_thread()
t.join()
Can PySys_WriteStderr and PySys_FormatStderr be replaced with functions that don't drop the GIL?
If I run the script manually without redirecting stderr to /dev/null (./python segfault.py), it segfaults 10 out of 10 times. I believe this is due to the faster writing to /dev/null compared to printing to the terminal. @corona10, the same might be true for your machine; if it's fast enough, it could collect all objects within a time less than the switch interval. In that case, increasing the number of objects from 20 to 50 (or perhaps 70, 100, or even more) can help reproduce the bug. On my machine, changing the number of objects from 20 to 50 results in segfault 17 out of 20 times in the bash for loop with redirecting stderr.
I tested this on a local CPython 3.14.6 build and haven't been able to reproduce the crash so far. I ran a 50M-iteration stress test covering the affected keyword-call path with argument corruption checks, and all iterations completed successfully.
I'll keep narrowing down the conditions needed to trigger the failure.
Here is an improved version of the reproduction script that tests various execution parameters. From my testing, modifying sys.setswitchinterval() seems to have the most significant impact on triggering the race condition.
My current setup:
OS: Fedora Linux 44
Kernel: 7.2.7-200.fc44.x86_64 (64-bit)
Processor: Intel Core Ultra 7 155H
I tested this across multiple Python runtimes, including the system Python v3.14.7 and several versions managed via uv (v3.9 through v3.13). Interestingly, Python 3.9 and 3.10 last slightly longer before reproducing the failure compared to newer versions.
Also, the issue doesn't appear on free-threaded builds of Python.
Code
import gc
import sys
import threading
from time import sleep
from weakref import ref
class CircularReferenceObject:
def __init__(self):
self._list = [self]
def remove_circular_reference(self):
self._list.remove(self)
del self._list
def main_thread(*, number_of_collections):
for _ in range(number_of_collections):
gc.collect()
sleep(0)
def side_thread_segmentation_fault(*, number_of_collections, number_of_objects):
for _ in range(number_of_collections):
weak_refs = []
for _ in range(number_of_objects):
_object = CircularReferenceObject()
weak_refs.append(ref(_object))
collected = gc.collect()
if collected == 0 and any(_object() for _object in weak_refs):
# GC started in the main thread and has dropped the GIL
# somewhere in the middle of `gc_collect_main`
for reference in reversed(weak_refs):
if reference() is not None:
_object = reference()
_object.remove_circular_reference()
sleep(0)
sleep(0)
gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_COLLECTABLE)
for number_of_collections in (1_000, 5_000, 10_000, 20_000):
for number_of_objects in (20, 50, 100, 200, 500, 1_000, 2_000):
for switch_interval in (1, 1e-3, 1e-6, 1e-9, 1e-12):
print(
f"\tNumber of objects: {number_of_objects}\n"
f"\tNumber of collections: {number_of_collections}\n"
f"\tSwitch interbal: {switch_interval}\n"
)
sys.setswitchinterval(switch_interval)
t = threading.Thread(
target=side_thread_segmentation_fault,
kwargs={
"number_of_collections": number_of_collections,
"number_of_objects": number_of_objects,
},
)
t.start()
main_thread(number_of_collections=number_of_collections)
t.join()
Activity
corona10 commentedon Dec 3, 2023
From my macOS with the latest commit, there was no issue
olutra commentedon Dec 3, 2023
I saved the code causing segmentation fault from the bug description to the file
segfault.pyand ran it a couple of timesHere,
./pythonis the binary built today from the main branch.The result is:
If I run the script manually without redirecting
stderrto/dev/null(./python segfault.py), it segfaults 10 out of 10 times. I believe this is due to the faster writing to/dev/nullcompared to printing to the terminal. @corona10, the same might be true for your machine; if it's fast enough, it could collect all objects within a time less than the switch interval. In that case, increasing the number of objects from 20 to 50 (or perhaps 70, 100, or even more) can help reproduce the bug. On my machine, changing the number of objects from 20 to 50 results in segfault 17 out of 20 times in the bash for loop with redirectingstderr.BHUVANSH855 commentedon Sep 24, 2026
I tested this on a local CPython 3.14.6 build and haven't been able to reproduce the crash so far. I ran a 50M-iteration stress test covering the affected keyword-call path with argument corruption checks, and all iterations completed successfully.
I'll keep narrowing down the conditions needed to trigger the failure.
olutra commentedon Sep 25, 2026
Here is an improved version of the reproduction script that tests various execution parameters. From my testing, modifying
sys.setswitchinterval()seems to have the most significant impact on triggering the race condition.My current setup:
OS: Fedora Linux 44
Kernel: 7.2.7-200.fc44.x86_64 (64-bit)
Processor: Intel Core Ultra 7 155H
I tested this across multiple Python runtimes, including the system Python v3.14.7 and several versions managed via uv (v3.9 through v3.13). Interestingly, Python 3.9 and 3.10 last slightly longer before reproducing the failure compared to newer versions.
Also, the issue doesn't appear on free-threaded builds of Python.
Code
johng commentedon Oct 25, 2026
I've raised a fix for this and added a reliable reproduction leveraging
Stderrso that we can free the object during the debug loop