From d3cc53acd352ebaadd19a9079ac4738771b62c8d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 4 Mar 2026 21:20:55 +0900 Subject: [PATCH 1/6] Add freelists for PyComplex, PyInt, PyBoundMethod, PyRange - PyComplex(100), PyInt(100), PyBoundMethod(20), PyRange(6) - Use try_with instead of with in all freelist push/pop to prevent panic on thread-local access during thread teardown --- crates/vm/src/builtins/complex.rs | 40 ++++++++++++++++++++++++++++++ crates/vm/src/builtins/dict.rs | 39 ++++++++++++++++------------- crates/vm/src/builtins/float.rs | 39 ++++++++++++++++------------- crates/vm/src/builtins/function.rs | 40 ++++++++++++++++++++++++++++++ crates/vm/src/builtins/int.rs | 40 ++++++++++++++++++++++++++++++ crates/vm/src/builtins/list.rs | 39 ++++++++++++++++------------- crates/vm/src/builtins/range.rs | 40 ++++++++++++++++++++++++++++++ crates/vm/src/builtins/slice.rs | 39 ++++++++++++++++------------- 8 files changed, 248 insertions(+), 68 deletions(-) diff --git a/crates/vm/src/builtins/complex.rs b/crates/vm/src/builtins/complex.rs index 84fd0c806c4..f05e5a32faa 100644 --- a/crates/vm/src/builtins/complex.rs +++ b/crates/vm/src/builtins/complex.rs @@ -10,7 +10,9 @@ use crate::{ stdlib::warnings, types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp, Representable}, }; +use core::cell::Cell; use core::num::Wrapping; +use core::ptr::NonNull; use num_complex::Complex64; use num_traits::Zero; use rustpython_common::hash; @@ -24,11 +26,49 @@ pub struct PyComplex { value: Complex64, } +// spell-checker:ignore MAXFREELIST +thread_local! { + static COMPLEX_FREELIST: Cell> = const { Cell::new(crate::object::FreeList::new()) }; +} + impl PyPayload for PyComplex { + const MAX_FREELIST: usize = 100; + const HAS_FREELIST: bool = true; + #[inline] fn class(ctx: &Context) -> &'static Py { ctx.types.complex_type } + + #[inline] + unsafe fn freelist_push(obj: *mut PyObject) -> bool { + COMPLEX_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) + } + + #[inline] + unsafe fn freelist_pop() -> Option> { + COMPLEX_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() + } } impl ToPyObject for Complex64 { diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index 7ba173fe7e4..f2a7e6a5a29 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -77,27 +77,32 @@ impl PyPayload for PyDict { #[inline] unsafe fn freelist_push(obj: *mut PyObject) -> bool { - DICT_FREELIST.with(|fl| { - let mut list = fl.take(); - let stored = if list.len() < Self::MAX_FREELIST { - list.push(obj); - true - } else { - false - }; - fl.set(list); - stored - }) + DICT_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) } #[inline] unsafe fn freelist_pop() -> Option> { - DICT_FREELIST.with(|fl| { - let mut list = fl.take(); - let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); - fl.set(list); - result - }) + DICT_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() } } diff --git a/crates/vm/src/builtins/float.rs b/crates/vm/src/builtins/float.rs index f78b227fc38..e9267a9bf00 100644 --- a/crates/vm/src/builtins/float.rs +++ b/crates/vm/src/builtins/float.rs @@ -49,27 +49,32 @@ impl PyPayload for PyFloat { #[inline] unsafe fn freelist_push(obj: *mut PyObject) -> bool { - FLOAT_FREELIST.with(|fl| { - let mut list = fl.take(); - let stored = if list.len() < Self::MAX_FREELIST { - list.push(obj); - true - } else { - false - }; - fl.set(list); - stored - }) + FLOAT_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) } #[inline] unsafe fn freelist_pop() -> Option> { - FLOAT_FREELIST.with(|fl| { - let mut list = fl.take(); - let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); - fl.set(list); - result - }) + FLOAT_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() } } diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 03663d22e5d..a0cb090c572 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -20,6 +20,8 @@ use crate::{ Callable, Comparable, Constructor, GetAttr, GetDescriptor, PyComparisonOp, Representable, }, }; +use core::cell::Cell; +use core::ptr::NonNull; use core::sync::atomic::{AtomicU32, Ordering::Relaxed}; use itertools::Itertools; #[cfg(feature = "jit")] @@ -1205,11 +1207,49 @@ impl PyBoundMethod { } } +// spell-checker:ignore MAXFREELIST +thread_local! { + static BOUND_METHOD_FREELIST: Cell> = const { Cell::new(crate::object::FreeList::new()) }; +} + impl PyPayload for PyBoundMethod { + const MAX_FREELIST: usize = 20; + const HAS_FREELIST: bool = true; + #[inline] fn class(ctx: &Context) -> &'static Py { ctx.types.bound_method_type } + + #[inline] + unsafe fn freelist_push(obj: *mut PyObject) -> bool { + BOUND_METHOD_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) + } + + #[inline] + unsafe fn freelist_pop() -> Option> { + BOUND_METHOD_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() + } } impl Representable for PyBoundMethod { diff --git a/crates/vm/src/builtins/int.rs b/crates/vm/src/builtins/int.rs index d2d462b8f30..01863615ac1 100644 --- a/crates/vm/src/builtins/int.rs +++ b/crates/vm/src/builtins/int.rs @@ -20,7 +20,9 @@ use crate::{ types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp, Representable}, }; use alloc::fmt; +use core::cell::Cell; use core::ops::{Neg, Not}; +use core::ptr::NonNull; use malachite_bigint::{BigInt, Sign}; use num_integer::Integer; use num_traits::{One, Pow, PrimInt, Signed, ToPrimitive, Zero}; @@ -48,7 +50,15 @@ where } } +// spell-checker:ignore MAXFREELIST +thread_local! { + static INT_FREELIST: Cell> = const { Cell::new(crate::object::FreeList::new()) }; +} + impl PyPayload for PyInt { + const MAX_FREELIST: usize = 100; + const HAS_FREELIST: bool = true; + #[inline] fn class(ctx: &Context) -> &'static Py { ctx.types.int_type @@ -57,6 +67,36 @@ impl PyPayload for PyInt { fn into_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { vm.ctx.new_int(self.value).into() } + + #[inline] + unsafe fn freelist_push(obj: *mut PyObject) -> bool { + INT_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) + } + + #[inline] + unsafe fn freelist_pop() -> Option> { + INT_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() + } } macro_rules! impl_into_pyobject_int { diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index ff3ed64f263..c13dea57169 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -89,27 +89,32 @@ impl PyPayload for PyList { #[inline] unsafe fn freelist_push(obj: *mut PyObject) -> bool { - LIST_FREELIST.with(|fl| { - let mut list = fl.take(); - let stored = if list.len() < Self::MAX_FREELIST { - list.push(obj); - true - } else { - false - }; - fl.set(list); - stored - }) + LIST_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) } #[inline] unsafe fn freelist_pop() -> Option> { - LIST_FREELIST.with(|fl| { - let mut list = fl.take(); - let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); - fl.set(list); - result - }) + LIST_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() } } diff --git a/crates/vm/src/builtins/range.rs b/crates/vm/src/builtins/range.rs index ec1a662ddad..795ec230ba9 100644 --- a/crates/vm/src/builtins/range.rs +++ b/crates/vm/src/builtins/range.rs @@ -15,7 +15,9 @@ use crate::{ Representable, SelfIter, }, }; +use core::cell::Cell; use core::cmp::max; +use core::ptr::NonNull; use crossbeam_utils::atomic::AtomicCell; use malachite_bigint::{BigInt, Sign}; use num_integer::Integer; @@ -67,11 +69,49 @@ pub struct PyRange { pub step: PyIntRef, } +// spell-checker:ignore MAXFREELIST +thread_local! { + static RANGE_FREELIST: Cell> = const { Cell::new(crate::object::FreeList::new()) }; +} + impl PyPayload for PyRange { + const MAX_FREELIST: usize = 6; + const HAS_FREELIST: bool = true; + #[inline] fn class(ctx: &Context) -> &'static Py { ctx.types.range_type } + + #[inline] + unsafe fn freelist_push(obj: *mut PyObject) -> bool { + RANGE_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) + } + + #[inline] + unsafe fn freelist_pop() -> Option> { + RANGE_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() + } } impl PyRange { diff --git a/crates/vm/src/builtins/slice.rs b/crates/vm/src/builtins/slice.rs index c0b68ed9e8a..aeb3337c7d8 100644 --- a/crates/vm/src/builtins/slice.rs +++ b/crates/vm/src/builtins/slice.rs @@ -60,27 +60,32 @@ impl PyPayload for PySlice { #[inline] unsafe fn freelist_push(obj: *mut PyObject) -> bool { - SLICE_FREELIST.with(|fl| { - let mut list = fl.take(); - let stored = if list.len() < Self::MAX_FREELIST { - list.push(obj); - true - } else { - false - }; - fl.set(list); - stored - }) + SLICE_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) } #[inline] unsafe fn freelist_pop() -> Option> { - SLICE_FREELIST.with(|fl| { - let mut list = fl.take(); - let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); - fl.set(list); - result - }) + SLICE_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() } } From 3d4951d2dbc0d1f5646283dce59c9f2b5c5bfba4 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 4 Mar 2026 23:36:23 +0900 Subject: [PATCH 2/6] Use alloc::dealloc in FreeList::Drop to avoid thread-local access panic During thread teardown, Box::from_raw triggers cascading destructors that may access already-destroyed thread-local storage (GC state, other freelists). Use raw dealloc instead to free memory without running destructors. --- crates/vm/src/object/core.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index fce1eaf7e35..7ced1478c34 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -885,8 +885,19 @@ impl Default for FreeList { impl Drop for FreeList { fn drop(&mut self) { + // During thread teardown, we cannot safely run destructors on cached + // objects because their Drop impls may access thread-local storage + // (GC state, other freelists) that is already destroyed. + // Instead, free just the raw allocation. The payload's heap fields + // (BigInt, PyObjectRef, etc.) are leaked, but this is bounded by + // MAX_FREELIST per type per thread. for ptr in self.items.drain(..) { - drop(unsafe { Box::from_raw(ptr as *mut PyInner) }); + unsafe { + alloc::alloc::dealloc( + ptr as *mut u8, + alloc::alloc::Layout::new::>(), + ); + } } } } From 7b25894011afeb70be61dfa39f6a8e0069c94b30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Mar 2026 14:38:25 +0000 Subject: [PATCH 3/6] Auto-format: cargo fmt --all --- crates/vm/src/object/core.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index 7ced1478c34..72ad9898f76 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -893,10 +893,7 @@ impl Drop for FreeList { // MAX_FREELIST per type per thread. for ptr in self.items.drain(..) { unsafe { - alloc::alloc::dealloc( - ptr as *mut u8, - alloc::alloc::Layout::new::>(), - ); + alloc::alloc::dealloc(ptr as *mut u8, alloc::alloc::Layout::new::>()); } } } From b96290910d9756257bc0cb1f357fe6e09209b01f Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 4 Mar 2026 23:59:32 +0900 Subject: [PATCH 4/6] Fix clippy: use core::alloc::Layout instead of alloc::alloc::Layout --- crates/vm/src/object/core.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index 72ad9898f76..32e0f7cfe97 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -893,7 +893,7 @@ impl Drop for FreeList { // MAX_FREELIST per type per thread. for ptr in self.items.drain(..) { unsafe { - alloc::alloc::dealloc(ptr as *mut u8, alloc::alloc::Layout::new::>()); + alloc::alloc::dealloc(ptr as *mut u8, core::alloc::Layout::new::>()); } } } From 7b614aef3c2e101f0e032c8153e2abb806a89e25 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 5 Mar 2026 00:40:37 +0900 Subject: [PATCH 5/6] Address review: PyBoundMethod clear=false, update FreeList doc comment - Set clear=false on PyBoundMethod (tp_clear=NULL in classobject.c) - Update FreeList doc comment to match actual Drop behavior (raw dealloc) --- crates/vm/src/builtins/function.rs | 2 +- crates/vm/src/object/core.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index a0cb090c572..ede0e3dcf1a 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -1084,7 +1084,7 @@ impl Constructor for PyFunction { } } -#[pyclass(module = false, name = "method", traverse)] +#[pyclass(module = false, name = "method", traverse, clear = false)] #[derive(Debug)] pub struct PyBoundMethod { object: PyObjectRef, diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index 32e0f7cfe97..77330b2665e 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -858,11 +858,11 @@ impl PyInner { } } -/// Thread-local freelist storage that properly deallocates cached objects -/// on thread teardown. +/// Thread-local freelist storage for reusing object allocations. /// -/// Wraps a `Vec<*mut PyObject>` and implements `Drop` to convert each -/// raw pointer back into `Box>` for proper deallocation. +/// Wraps a `Vec<*mut PyObject>`. On thread teardown, `Drop` frees raw +/// `PyInner` allocations without running payload destructors to avoid +/// accessing already-destroyed thread-local storage (GC state, other freelists). pub(crate) struct FreeList { items: Vec<*mut PyObject>, _marker: core::marker::PhantomData, From 01b5319e46129fd68dcf291fc9e641cb2f71cf56 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 5 Mar 2026 01:54:48 +0900 Subject: [PATCH 6/6] Remove PyBoundMethod freelist to fix refcount/weakref test failures Non-Option PyObjectRef fields retain references in freelist, causing weakref and refcount assertions to fail in test_unittest, test_multiprocessing, and test_socket. --- crates/vm/src/builtins/function.rs | 42 +----------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index ede0e3dcf1a..03663d22e5d 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -20,8 +20,6 @@ use crate::{ Callable, Comparable, Constructor, GetAttr, GetDescriptor, PyComparisonOp, Representable, }, }; -use core::cell::Cell; -use core::ptr::NonNull; use core::sync::atomic::{AtomicU32, Ordering::Relaxed}; use itertools::Itertools; #[cfg(feature = "jit")] @@ -1084,7 +1082,7 @@ impl Constructor for PyFunction { } } -#[pyclass(module = false, name = "method", traverse, clear = false)] +#[pyclass(module = false, name = "method", traverse)] #[derive(Debug)] pub struct PyBoundMethod { object: PyObjectRef, @@ -1207,49 +1205,11 @@ impl PyBoundMethod { } } -// spell-checker:ignore MAXFREELIST -thread_local! { - static BOUND_METHOD_FREELIST: Cell> = const { Cell::new(crate::object::FreeList::new()) }; -} - impl PyPayload for PyBoundMethod { - const MAX_FREELIST: usize = 20; - const HAS_FREELIST: bool = true; - #[inline] fn class(ctx: &Context) -> &'static Py { ctx.types.bound_method_type } - - #[inline] - unsafe fn freelist_push(obj: *mut PyObject) -> bool { - BOUND_METHOD_FREELIST - .try_with(|fl| { - let mut list = fl.take(); - let stored = if list.len() < Self::MAX_FREELIST { - list.push(obj); - true - } else { - false - }; - fl.set(list); - stored - }) - .unwrap_or(false) - } - - #[inline] - unsafe fn freelist_pop() -> Option> { - BOUND_METHOD_FREELIST - .try_with(|fl| { - let mut list = fl.take(); - let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); - fl.set(list); - result - }) - .ok() - .flatten() - } } impl Representable for PyBoundMethod {