Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
trashcan
  • Loading branch information
youknowone committed Feb 22, 2026
commit d479d0019609ca9c2c8afb28bd8675f84af308cc
1 change: 0 additions & 1 deletion Lib/test/test_asyncio/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1649,7 +1649,6 @@ async def test(ctx):
# SSLProtocol should be DECREF to 0
self.assertIsNone(ctx())

@unittest.expectedFailure # TODO: RUSTPYTHON; - gc.collect() doesn't release SSLContext properly
def test_shutdown_timeout_handler_leak(self):
loop = self.loop

Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5117,7 +5117,6 @@ def __new__(cls):
cls.lst = [2**i for i in range(10000)]
X.descr

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_remove_subclass(self):
# bpo-46417: when the last subclass of a type is deleted,
# remove_subclass() clears the internal dictionary of subclasses:
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_faulthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ def test_sigsegv(self):
3,
'Segmentation fault')

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '(?m)^Fatal Python error: Segmentation fault\n\n<Cannot show all threads while the GIL is disabled>\nStack\\ \\(most\\ recent\\ call\\ first\\):\n File "<string>", line 9 in __del__\nCurrent thread\'s C stack trace \\(most recent call first\\):\n( Binary file ".+"(, at .*(\\+|-)0x[0-9a-f]+)? \\[0x[0-9a-f]+\\])|(<.+>)' not found in 'exit'
@skip_segfault_on_android
def test_gc(self):
# bpo-44466: Detect if the GC is running
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2105,7 +2105,6 @@ def f():
return 1
self.assertEqual(f.cache_parameters(), {'maxsize': 1000, "typed": True})

@unittest.expectedFailure # TODO: RUSTPYTHON; GC behavior differs from CPython's refcounting
def test_lru_cache_weakrefable(self):
@self.module.lru_cache
def test_function(x):
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2714,7 +2714,6 @@ def __getattribute__(self, attr):

self.assertFalse(test.called)

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_inspect.test_inspect.TestGetattrStatic.test_cache_does_not_cause_classes_to_persist.<locals>.Foo'> is not None
def test_cache_does_not_cause_classes_to_persist(self):
# regression test for gh-118013:
# check that the internal _shadowed_dict cache does not cause
Expand Down
4 changes: 0 additions & 4 deletions Lib/test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -3813,7 +3813,6 @@ def __del__(self):
""".format(iomod=iomod, kwargs=kwargs)
return assert_python_ok("-c", code)

@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_create_at_shutdown_without_encoding(self):
rc, out, err = self._check_create_at_shutdown()
if err:
Expand All @@ -3823,7 +3822,6 @@ def test_create_at_shutdown_without_encoding(self):
else:
self.assertEqual("ok", out.decode().strip())

@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_create_at_shutdown_with_encoding(self):
rc, out, err = self._check_create_at_shutdown(encoding='utf-8',
errors='strict')
Expand Down Expand Up @@ -4812,13 +4810,11 @@ def run():
else:
self.assertFalse(err.strip('.!'))

@unittest.expectedFailure # TODO: RUSTPYTHON; without GC+GIL, finalize_modules clears __main__ globals while daemon threads are still running
@threading_helper.requires_working_threading()
@support.requires_resource('walltime')
def test_daemon_threads_shutdown_stdout_deadlock(self):
self.check_daemon_threads_shutdown_deadlock('stdout')

@unittest.expectedFailure # TODO: RUSTPYTHON; without GC+GIL, finalize_modules clears __main__ globals while daemon threads are still running
@threading_helper.requires_working_threading()
@support.requires_resource('walltime')
def test_daemon_threads_shutdown_stderr_deadlock(self):
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5165,7 +5165,6 @@ def __init__(self, name='MyLogger', level=logging.NOTSET):
h.close()
logging.setLoggerClass(logging.Logger)

@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_logging_at_shutdown(self):
# bpo-20037: Doing text I/O late at interpreter shutdown must not crash
code = textwrap.dedent("""
Expand All @@ -5185,7 +5184,6 @@ def __del__(self):
self.assertIn("exception in __del__", err)
self.assertIn("ValueError: some error", err)

@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_logging_at_shutdown_open(self):
# bpo-26789: FileHandler keeps a reference to the builtin open()
# function to be able to open or reopen the file during Python
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_memoryio.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,6 @@ def test_getbuffer_empty(self):
buf2.release()
memio.write(b'x')

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <memory at 0xbb894d200> is not None
def test_getbuffer_gc_collect(self):
memio = self.ioclass(b"1234567890")
buf = memio.getbuffer()
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_ordered_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,6 @@ def test_dict_update(self):
dict.update(od, [('spam', 1)])
self.assertNotIn('NULL', repr(od))

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_ordered_dict.OrderedDictTests.test_reference_loop.<locals>.A'> is not None
def test_reference_loop(self):
# Issue 25935
OrderedDict = self.OrderedDict
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -3557,7 +3557,6 @@ def test_communicate_repeated_call_after_stdout_close(self):
except subprocess.TimeoutExpired:
pass

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'preexec_fn not supported at interpreter shutdown' not found in b"Exception ignored in: <function AtFinalization.__del__ at 0xa92f93840>\nAttributeError: 'NoneType' object has no attribute 'Popen'\n"
def test_preexec_at_exit(self):
code = f"""if 1:
import atexit
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,6 @@ def func(lock):
def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False):
self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_main_thread_during_shutdown(self):
# bpo-31516: current_thread() should still point to the main thread
# at shutdown
Expand Down Expand Up @@ -1086,7 +1085,6 @@ def checker():
self.assertEqual(threading.getprofile(), old_profile)
self.assertEqual(sys.getprofile(), old_profile)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_locals_at_exit(self):
# bpo-19466: thread locals must not be deleted before destructors
# are called
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,6 @@ def do_test(firstlines, message, charset, lineno):
# Issue #18960: coding spec should have no effect
do_test("x=0\n# coding: GBK\n", "h\xe9 ho", 'utf-8', 5)

@unittest.expectedFailure # TODO: RUSTPYTHON; + b'ZeroDivisionError: division by zero']
def test_print_traceback_at_exit(self):
# Issue #22599: Ensure that it is possible to use the traceback module
# to display an exception at Python exit
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1610,7 +1610,6 @@ def test_issue_8766(self):


class FinalizationTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; - TypeError: 'NoneType' object is not callable
def test_finalization(self):
# Issue #19421: warnings.warn() should not crash
# during Python finalization
Expand Down
77 changes: 77 additions & 0 deletions crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,75 @@ use core::{
#[derive(Debug)]
pub(super) struct Erased;

/// Trashcan mechanism to limit recursive deallocation depth (Py_TRASHCAN).
/// Without this, deeply nested structures (e.g. 200k-deep list) cause stack overflow
/// during deallocation because each level adds a stack frame.
mod trashcan {
use core::cell::Cell;

/// Maximum nesting depth for deallocation before deferring.
/// CPython uses UNWIND_NO_NESTING = 50.
const TRASHCAN_LIMIT: usize = 50;

type DeallocFn = unsafe fn(*mut super::PyObject);
type DeallocQueue = Vec<(*mut super::PyObject, DeallocFn)>;

thread_local! {
static DEALLOC_DEPTH: Cell<usize> = const { Cell::new(0) };
static DEALLOC_QUEUE: Cell<DeallocQueue> = const { Cell::new(Vec::new()) };
}

/// Try to begin deallocation. Returns true if we should proceed,
/// false if the object was deferred (depth exceeded).
#[inline]
pub(super) unsafe fn begin(
obj: *mut super::PyObject,
dealloc: unsafe fn(*mut super::PyObject),
) -> bool {
DEALLOC_DEPTH.with(|d| {
let depth = d.get();
if depth >= TRASHCAN_LIMIT {
// Depth exceeded: defer this deallocation
DEALLOC_QUEUE.with(|q| {
let mut queue = q.take();
queue.push((obj, dealloc));
q.set(queue);
});
false
} else {
d.set(depth + 1);
true
}
})
}

/// End deallocation and process any deferred objects if at outermost level.
#[inline]
pub(super) unsafe fn end() {
let depth = DEALLOC_DEPTH.with(|d| {
let depth = d.get() - 1;
d.set(depth);
depth
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if depth == 0 {
// Process deferred deallocations iteratively
loop {
let next = DEALLOC_QUEUE.with(|q| {
let mut queue = q.take();
let item = queue.pop();
q.set(queue);
item
});
if let Some((obj, dealloc)) = next {
unsafe { dealloc(obj) };
} else {
break;
}
}
}
}
}

/// Default dealloc: handles __del__, weakref clearing, tp_clear, and memory free.
/// Equivalent to subtype_dealloc.
pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
Expand All @@ -89,6 +158,11 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
return; // resurrected by __del__
}

// Trashcan: limit recursive deallocation depth to prevent stack overflow
if !unsafe { trashcan::begin(obj, default_dealloc::<T>) } {
return; // deferred to queue
}

let vtable = obj_ref.0.vtable;

// Untrack from GC BEFORE deallocation.
Expand All @@ -115,6 +189,9 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
// Drop child references - may trigger recursive destruction.
// The object is already deallocated, so circular refs are broken.
drop(edges);

// Trashcan: decrement depth and process deferred objects at outermost level
unsafe { trashcan::end() };
}
pub(super) unsafe fn debug_obj<T: PyPayload + core::fmt::Debug>(
x: &PyObject,
Expand Down