Fix thread teardown panic when weakref callback fires during cleanup (#7965)

This commit is contained in:
James Clarke
2026-05-25 05:55:32 +01:00
committed by GitHub
parent bc3d00e879
commit a5775e0c07
4 changed files with 66 additions and 34 deletions

2
.gitignore vendored
View File

@@ -27,4 +27,4 @@ Lib/site-packages/*
Lib/test/data/*
!Lib/test/data/README
cpython/
.claude/scheduled_tasks.lock

View File

@@ -530,10 +530,16 @@ pub(crate) mod _thread {
// Increment thread count when thread actually starts executing
vm.state.thread_count.fetch_add(1);
match func.invoke(args, vm) {
Ok(_obj) => {}
Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {}
Err(exc) => {
// Inner scope: drop `func` (and its Python refs) before the thread
// slot is torn down below. Otherwise the parameter `func` would drop
// at end-of-function, after cleanup_current_thread_frames has cleared
// CURRENT_THREAD_SLOT, and a weakref callback fired during that drop
// would panic in push_thread_frame.
{
let func = func;
if let Err(exc) = func.invoke(args, vm)
&& !exc.fast_isinstance(vm.ctx.exceptions.system_exit)
{
vm.run_unraisable(
exc,
Some("Exception ignored in thread started by".to_owned()),
@@ -1663,11 +1669,18 @@ pub(crate) mod _thread {
// Increment thread count when thread actually starts executing
vm_state.thread_count.fetch_add(1);
// Run the function
match func.invoke((), vm) {
Ok(_) => {}
Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {}
Err(exc) => {
// Inner scope: drop `func` (and its Python refs) before the
// outer scopeguard::defer tears down the thread slot. As a
// `move` closure capture, `func` would otherwise drop after
// all locals (including the scopeguard `_guard`), and a
// weakref callback fired during that drop would panic in
// push_thread_frame.
{
let func = func;
// Run the function
if let Err(exc) = func.invoke((), vm)
&& !exc.fast_isinstance(vm.ctx.exceptions.system_exit)
{
vm.run_unraisable(
exc,
Some("Exception ignored in thread started by".to_owned()),

View File

@@ -1,6 +1,7 @@
import multiprocessing
import os
import threading
import time
def import_in_thread(module_name):
@@ -62,6 +63,48 @@ def start_fork_process_after_thread():
assert process.exitcode == 0, process.exitcode
def thread_join_ordering():
output = []
def thread_function(name):
output.append((name, 0))
time.sleep(2.0)
output.append((name, 1))
output.append((0, 0))
x = threading.Thread(target=thread_function, args=(1,))
output.append((0, 1))
x.start()
output.append((0, 2))
x.join()
output.append((0, 3))
assert len(output) == 6, output
# CPython has [(1, 0), (0, 2)] for the middle 2, but we have [(0, 2), (1, 0)]
# TODO: maybe fix this, if it turns out to be a problem?
# assert output == [(0, 0), (0, 1), (1, 0), (0, 2), (1, 1), (0, 3)]
def thread_exit_without_join():
# Regression for https://github.com/RustPython/RustPython/issues/7813:
# a thread started without ``.join()`` must exit cleanly even when the
# captured target callable drops during teardown (which can fire
# weakref callbacks that re-enter the VM).
output = []
def runner():
output.append("runner done")
threading.Thread(target=runner).start()
time.sleep(1)
output.append("main done")
assert "runner done" in output, output
assert "main done" in output, output
thread_join_ordering()
thread_exit_without_join()
import_in_thread("functools")
import_in_thread("tempfile")
import_in_thread("multiprocessing.connection")

View File

@@ -1,24 +0,0 @@
import threading
import time
output = []
def thread_function(name):
output.append((name, 0))
time.sleep(2.0)
output.append((name, 1))
output.append((0, 0))
x = threading.Thread(target=thread_function, args=(1,))
output.append((0, 1))
x.start()
output.append((0, 2))
x.join()
output.append((0, 3))
assert len(output) == 6, output
# CPython has [(1, 0), (0, 2)] for the middle 2, but we have [(0, 2), (1, 0)]
# TODO: maybe fix this, if it turns out to be a problem?
# assert output == [(0, 0), (0, 1), (1, 0), (0, 2), (1, 1), (0, 3)]