Visitar URL original
more algorithm-independent GC infra by youknowone · Pull Request #7194 · RustPython/RustPython · GitHub
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
add overflow guard to inc(), #[must_use] on dec()/safe_inc(), trashca…
…n debug_assert, weakref generic re-check
  • Loading branch information
youknowone committed Feb 22, 2026
commit be29d7c0eb01490ea32f8010444e401c9ea60d72
7 changes: 4 additions & 3 deletions crates/common/src/refcount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl RefCount {
#[inline]
pub fn inc(&self) {
let val = State::from_raw(self.state.fetch_add(COUNT, Ordering::SeqCst));
if val.destructed() {
if val.destructed() || (val.strong() as usize) > STRONG - 1 {
refcount_overflow();
}
if val.strong() == 0 {
Expand Down Expand Up @@ -211,8 +211,9 @@ impl Drop for DeferredDropGuard {
IN_DEFERRED_CONTEXT.with(|in_ctx| {
in_ctx.set(self.was_in_context);
});
// Only flush if we're the outermost context
if !self.was_in_context {
// Only flush if we're the outermost context and not already panicking
// (flushing during unwinding risks double-panic → process abort).
if !self.was_in_context && !std::thread::panicking() {
flush_deferred_drops();
}
}
Expand Down
19 changes: 18 additions & 1 deletion crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ mod trashcan {
#[inline]
pub(super) unsafe fn end() {
let depth = DEALLOC_DEPTH.with(|d| {
let depth = d.get() - 1;
let depth = d.get();
debug_assert!(depth > 0, "trashcan::end called without matching begin");
let depth = depth - 1;
d.set(depth);
depth
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -434,6 +436,21 @@ impl WeakRefList {
// Re-acquire lock for linked list insertion
let _lock = weakref_lock::lock(obj as *const PyObject as usize);

// Re-check: another thread may have inserted a generic ref while we
// were allocating outside the lock. If so, reuse it and drop ours.
if is_generic {
let generic_ptr = self.generic.load(Ordering::Relaxed);
if !generic_ptr.is_null() {
let generic = unsafe { &*generic_ptr };
if generic.0.ref_count.safe_inc() {
// Nullify wr_object so drop_inner won't unlink an
// un-inserted node (which would corrupt the list head).
weak.wr_object.store(ptr::null_mut(), Ordering::Relaxed);
return unsafe { PyRef::from_raw(generic_ptr) };
}
}
}
Comment on lines +441 to +452

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the linked_list module and inspect Pointers::new()
fd "linked_list" -t f -e rs

Repository: RustPython/RustPython

Length of output: 97


🏁 Script executed:

# Read the actual code at the specified location in core.rs
cat -n crates/vm/src/object/core.rs | sed -n '410,450p'

Repository: RustPython/RustPython

Length of output: 2089


🏁 Script executed:

# Get more context on the WeakRefList::add method to understand the full flow
cat -n crates/vm/src/object/core.rs | sed -n '400,460p'

Repository: RustPython/RustPython

Length of output: 3038


🏁 Script executed:

# Read the Pointers struct and new() implementation in linked_list.rs
cat -n crates/common/src/linked_list.rs | head -100

Repository: RustPython/RustPython

Length of output: 4419


🏁 Script executed:

# Search for Pointers::new() definition
rg "impl.*Pointers|fn new\(\)" crates/common/src/linked_list.rs -A 5

Repository: RustPython/RustPython

Length of output: 1033


🏁 Script executed:

# Search for unlink_weakref function in core.rs to verify the corruption path
rg "fn unlink_weakref" crates/vm/src/object/core.rs -A 20

Repository: RustPython/RustPython

Length of output: 770


Dropping the uninserted weak in the re-check path corrupts wrl.head and causes a subsequent use-after-free.

Rust drops locals in reverse declaration order. _lock (line 437) is declared after weak (line 434), so on the return at line 446 the drop sequence is:

  1. _lock dropped → stripe lock released.
  2. weak dropped → PyWeak::drop_inner runs → acquires the stripe lock (succeeds; _lock already gone) → calls unlink_weakref(wrl, node_ptr).

weak was never inserted into the list, so its Pointers fields are in their Pointers::new() initial state (both prev and next are None). unlink_weakref interprets prev == None as "this node is the head" and executes:

wrl.head.store(ptr::null_mut(), Ordering::Relaxed); // ← corrupts the list

This wipes wrl.head to null while the real list nodes are still alive. When obj is eventually deallocated, wrl.clear iterates from head == null and skips every existing weakref. Those weakrefs retain a non-null wr_object pointing to the now-freed obj memory. Their eventual drop_inner then acquires a stripe lock based on a dangling pointer and calls unlink_weakref on already-freed memory → use-after-free.

Fix: before the early return, null out wr_object so drop_inner exits at its first guard check:

🛡️ Proposed fix
         if is_generic {
             let generic_ptr = self.generic.load(Ordering::Relaxed);
             if !generic_ptr.is_null() {
                 let generic = unsafe { &*generic_ptr };
                 if generic.0.ref_count.safe_inc() {
+                    // `weak` was never inserted; nullify wr_object so drop_inner
+                    // returns early and does not call unlink_weakref.
+                    weak.wr_object.store(ptr::null_mut(), Ordering::Relaxed);
                     return unsafe { PyRef::from_raw(generic_ptr) };
                 }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/object/core.rs` around lines 441 - 449, In the re-check path
where you return a resurrected generic (the block using is_generic, generic_ptr,
generic and calling generic.0.ref_count.safe_inc()), clear the weak's wr_object
before the early return so drop of the local weak won't call unlink_weakref on
an uninserted node; i.e., set the weak/inner wr_object to null (use the same
atomic store semantics as other places, e.g., Ordering::Relaxed) on generic.0
(the PyWeak inner) immediately prior to returning PyRef::from_raw(generic_ptr).


// Insert into linked list under stripe lock
let node_ptr = NonNull::from(&*weak);
unsafe {
Expand Down
Loading