Skip to content

Commit 51628ab

Browse files
committed
Fix atexit unregister deadlock with reentrant __eq__
Release the lock during equality comparison in unregister so that __eq__ can safely call atexit.unregister or atexit._clear. Store callbacks in LIFO order (insert at front) and use identity-based search after comparison to handle list mutations, matching atexitmodule.c behavior. Also pass None as err_msg when func.repr() fails, matching CPython's PyErr_FormatUnraisable fallback.
1 parent 0a2535b commit 51628ab

File tree

3 files changed

+50
-21
lines changed

3 files changed

+50
-21
lines changed

Lib/test/_test_atexit.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ def func():
135135
finally:
136136
atexit.unregister(func)
137137

138-
@unittest.skip("TODO: RUSTPYTHON; Hangs")
139138
def test_eq_unregister_clear(self):
140139
# Issue #112127: callback's __eq__ may call unregister or _clear
141140
class Evil:
@@ -149,7 +148,6 @@ def __eq__(self, other):
149148
atexit.unregister(Evil())
150149
atexit._clear()
151150

152-
@unittest.skip("TODO: RUSTPYTHON; Hangs")
153151
def test_eq_unregister(self):
154152
# Issue #112127: callback's __eq__ may call unregister
155153
def f1():

crates/vm/src/stdlib/atexit.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ mod atexit {
77

88
#[pyfunction]
99
fn register(func: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyObjectRef {
10-
vm.state.atexit_funcs.lock().push((func.clone(), args));
10+
// Callbacks go in LIFO order (insert at front)
11+
vm.state
12+
.atexit_funcs
13+
.lock()
14+
.insert(0, Box::new((func.clone(), args)));
1115
func
1216
}
1317

@@ -18,35 +22,62 @@ mod atexit {
1822

1923
#[pyfunction]
2024
fn unregister(func: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
21-
let mut funcs = vm.state.atexit_funcs.lock();
22-
23-
let mut i = 0;
24-
while i < funcs.len() {
25-
if vm.bool_eq(&funcs[i].0, &func)? {
26-
funcs.remove(i);
27-
} else {
28-
i += 1;
25+
// Iterate backward (oldest to newest in LIFO list).
26+
// Release the lock during comparison so __eq__ can call atexit functions.
27+
let mut i = {
28+
let funcs = vm.state.atexit_funcs.lock();
29+
funcs.len() as isize - 1
30+
};
31+
while i >= 0 {
32+
let (cb, entry_ptr) = {
33+
let funcs = vm.state.atexit_funcs.lock();
34+
if i as usize >= funcs.len() {
35+
i = funcs.len() as isize;
36+
i -= 1;
37+
continue;
38+
}
39+
let entry = &funcs[i as usize];
40+
(entry.0.clone(), &**entry as *const (PyObjectRef, FuncArgs))
41+
};
42+
// Lock released: __eq__ can safely call atexit functions
43+
let eq = vm.bool_eq(&func, &cb)?;
44+
if eq {
45+
// The entry may have moved during __eq__. Search backward by identity.
46+
let mut funcs = vm.state.atexit_funcs.lock();
47+
let mut j = (funcs.len() as isize - 1).min(i);
48+
while j >= 0 {
49+
if core::ptr::eq(&**funcs.get(j as usize).unwrap(), entry_ptr) {
50+
funcs.remove(j as usize);
51+
i = j;
52+
break;
53+
}
54+
j -= 1;
55+
}
2956
}
57+
{
58+
let funcs = vm.state.atexit_funcs.lock();
59+
if i as usize >= funcs.len() {
60+
i = funcs.len() as isize;
61+
}
62+
}
63+
i -= 1;
3064
}
31-
3265
Ok(())
3366
}
3467

3568
#[pyfunction]
3669
pub fn _run_exitfuncs(vm: &VirtualMachine) {
3770
let funcs: Vec<_> = core::mem::take(&mut *vm.state.atexit_funcs.lock());
38-
for (func, args) in funcs.into_iter().rev() {
71+
// Callbacks stored in LIFO order, iterate forward
72+
for entry in funcs.into_iter() {
73+
let (func, args) = *entry;
3974
if let Err(e) = func.call(args, vm) {
4075
let exit = e.fast_isinstance(vm.ctx.exceptions.system_exit);
4176
let msg = func
4277
.repr(vm)
43-
.map(|r| {
44-
format!("Exception ignored in atexit callback {}", r.as_wtf8())
45-
})
46-
.unwrap_or_else(|_| {
47-
"Exception ignored in atexit callback".to_owned()
48-
});
49-
vm.run_unraisable(e, Some(msg), vm.ctx.none());
78+
.ok()
79+
.map(|r| format!("Exception ignored in atexit callback {}", r.as_wtf8()));
80+
vm.run_unraisable(e, msg, vm.ctx.none());
5081
if exit {
5182
break;
5283
}

crates/vm/src/vm/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ pub struct PyGlobalState {
585585
pub stacksize: AtomicCell<usize>,
586586
pub thread_count: AtomicCell<usize>,
587587
pub hash_secret: HashSecret,
588-
pub atexit_funcs: PyMutex<Vec<(PyObjectRef, FuncArgs)>>,
588+
pub atexit_funcs: PyMutex<Vec<Box<(PyObjectRef, FuncArgs)>>>,
589589
pub codec_registry: CodecsRegistry,
590590
pub finalizing: AtomicBool,
591591
pub warnings: WarningsState,

0 commit comments

Comments
 (0)