Skip to content

Commit a5775e0

Browse files
Fix thread teardown panic when weakref callback fires during cleanup (RustPython#7965)
1 parent bc3d00e commit a5775e0

4 files changed

Lines changed: 66 additions & 34 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ Lib/site-packages/*
2727
Lib/test/data/*
2828
!Lib/test/data/README
2929
cpython/
30-
30+
.claude/scheduled_tasks.lock

crates/vm/src/stdlib/_thread.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -530,10 +530,16 @@ pub(crate) mod _thread {
530530
// Increment thread count when thread actually starts executing
531531
vm.state.thread_count.fetch_add(1);
532532

533-
match func.invoke(args, vm) {
534-
Ok(_obj) => {}
535-
Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {}
536-
Err(exc) => {
533+
// Inner scope: drop `func` (and its Python refs) before the thread
534+
// slot is torn down below. Otherwise the parameter `func` would drop
535+
// at end-of-function, after cleanup_current_thread_frames has cleared
536+
// CURRENT_THREAD_SLOT, and a weakref callback fired during that drop
537+
// would panic in push_thread_frame.
538+
{
539+
let func = func;
540+
if let Err(exc) = func.invoke(args, vm)
541+
&& !exc.fast_isinstance(vm.ctx.exceptions.system_exit)
542+
{
537543
vm.run_unraisable(
538544
exc,
539545
Some("Exception ignored in thread started by".to_owned()),
@@ -1663,11 +1669,18 @@ pub(crate) mod _thread {
16631669
// Increment thread count when thread actually starts executing
16641670
vm_state.thread_count.fetch_add(1);
16651671

1666-
// Run the function
1667-
match func.invoke((), vm) {
1668-
Ok(_) => {}
1669-
Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {}
1670-
Err(exc) => {
1672+
// Inner scope: drop `func` (and its Python refs) before the
1673+
// outer scopeguard::defer tears down the thread slot. As a
1674+
// `move` closure capture, `func` would otherwise drop after
1675+
// all locals (including the scopeguard `_guard`), and a
1676+
// weakref callback fired during that drop would panic in
1677+
// push_thread_frame.
1678+
{
1679+
let func = func;
1680+
// Run the function
1681+
if let Err(exc) = func.invoke((), vm)
1682+
&& !exc.fast_isinstance(vm.ctx.exceptions.system_exit)
1683+
{
16711684
vm.run_unraisable(
16721685
exc,
16731686
Some("Exception ignored in thread started by".to_owned()),

extra_tests/snippets/stdlib_threading.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import multiprocessing
22
import os
33
import threading
4+
import time
45

56

67
def import_in_thread(module_name):
@@ -62,6 +63,48 @@ def start_fork_process_after_thread():
6263
assert process.exitcode == 0, process.exitcode
6364

6465

66+
def thread_join_ordering():
67+
output = []
68+
69+
def thread_function(name):
70+
output.append((name, 0))
71+
time.sleep(2.0)
72+
output.append((name, 1))
73+
74+
output.append((0, 0))
75+
x = threading.Thread(target=thread_function, args=(1,))
76+
output.append((0, 1))
77+
x.start()
78+
output.append((0, 2))
79+
x.join()
80+
output.append((0, 3))
81+
82+
assert len(output) == 6, output
83+
# CPython has [(1, 0), (0, 2)] for the middle 2, but we have [(0, 2), (1, 0)]
84+
# TODO: maybe fix this, if it turns out to be a problem?
85+
# assert output == [(0, 0), (0, 1), (1, 0), (0, 2), (1, 1), (0, 3)]
86+
87+
88+
def thread_exit_without_join():
89+
# Regression for https://github.com/RustPython/RustPython/issues/7813:
90+
# a thread started without ``.join()`` must exit cleanly even when the
91+
# captured target callable drops during teardown (which can fire
92+
# weakref callbacks that re-enter the VM).
93+
output = []
94+
95+
def runner():
96+
output.append("runner done")
97+
98+
threading.Thread(target=runner).start()
99+
time.sleep(1)
100+
output.append("main done")
101+
assert "runner done" in output, output
102+
assert "main done" in output, output
103+
104+
105+
thread_join_ordering()
106+
thread_exit_without_join()
107+
65108
import_in_thread("functools")
66109
import_in_thread("tempfile")
67110
import_in_thread("multiprocessing.connection")

extra_tests/snippets/test_threading.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)