mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
Give the strong count the bits an unused weak field held (#8574)
* Give the strong count the bits an unused weak field held RefCount packs its state into one usize and splits the non-flag bits evenly between a strong and a weak count, so a 32-bit target left the strong count 15 bits. An object reaching 32 767 references then called refcount_overflow, which aborts the process; on wasm that surfaces as an unreachable trap with no Python-level error. Two ordinary module imports pass that total. The weak half is never read and never written. WEAK_COUNT appears once outside its own definition, in RefCount::new, and nothing in the repository observes those bits: weak references live in the object's WeakRefList, which Py::weak_count walks. The strong count now takes every bit the three flags leave, which is 29 on a 32-bit target and 61 on a 64-bit one. The tests run on wasm32-wasip2, where the old layout traps and the new one passes. A 64-bit host passes either way, since 31 bits already covered them. Fixes #8469 Assisted-by: Claude Code:claude-opus-5 * Return the strong count as usize so a 64-bit target cannot truncate it The count is 61 bits wide on a 64-bit target once it takes the bits the weak field held, so a u32 return truncates above 4 294 967 295 references: inc would read a wrapped 0 and re-increment, and dec would read a wrapped 1 and free a live object. A 32-bit target is unaffected, where 29 bits fit u32. Assisted-by: Claude Code:claude-opus-5 * Cover the refcount ceiling through inc and dec inc_by writes its own overflow check, so a test built on it leaves inc and dec unexercised. The reported aborts came through inc, one reference at a time. Assisted-by: Claude Code:claude-opus-5 --------- Co-authored-by: Jean-Marc Le Roux <jeanmarc@lx.industries>
This commit is contained in:
1 parent
ed62d4a62b
commit
4ea9e84a3a
1 file changed
+68
-16
@@ -1,8 +1,13 @@
|
||||
use crate::atomic::{Ordering, PyAtomic, Radium};
|
||||
|
||||
// State layout (usize):
|
||||
// [1 bit: destructed] [1 bit: published] [1 bit: leaked] [N bits: weak_count] [M bits: strong_count]
|
||||
// 64-bit: N=30, M=31. 32-bit: N=14, M=15.
|
||||
// [1 bit: destructed] [1 bit: published] [1 bit: leaked] [M bits: strong_count]
|
||||
// 64-bit: M=61. 32-bit: M=29.
|
||||
//
|
||||
// Weak references live in the object's `WeakRefList`, not in this word, so the
|
||||
// strong count takes every bit the flags leave. A 32-bit target reaches its
|
||||
// ceiling at 536 870 911 references rather than the 32 767 that half the word
|
||||
// would allow — a number two ordinary module imports pass on `wasm32`.
|
||||
const FLAG_BITS: u32 = 3;
|
||||
const DESTRUCTED: usize = 1 << (usize::BITS - 1);
|
||||
/// Object was published to a lock-free cache; memory reclamation is
|
||||
@@ -10,12 +15,9 @@ const DESTRUCTED: usize = 1 << (usize::BITS - 1);
|
||||
/// freed memory. Sticky once set.
|
||||
const PUBLISHED: usize = 1 << (usize::BITS - 2);
|
||||
const LEAKED: usize = 1 << (usize::BITS - 3);
|
||||
const TOTAL_COUNT_WIDTH: u32 = usize::BITS - FLAG_BITS;
|
||||
const WEAK_WIDTH: u32 = TOTAL_COUNT_WIDTH / 2;
|
||||
const STRONG_WIDTH: u32 = TOTAL_COUNT_WIDTH - WEAK_WIDTH;
|
||||
const STRONG_WIDTH: u32 = usize::BITS - FLAG_BITS;
|
||||
const STRONG: usize = (1 << STRONG_WIDTH) - 1;
|
||||
const COUNT: usize = 1;
|
||||
const WEAK_COUNT: usize = 1 << STRONG_WIDTH;
|
||||
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
@@ -48,8 +50,8 @@ impl State {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strong(self) -> u32 {
|
||||
((self.inner & STRONG) / COUNT) as u32
|
||||
fn strong(self) -> usize {
|
||||
(self.inner & STRONG) / COUNT
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -76,8 +78,8 @@ impl State {
|
||||
/// Reference count using state layout with LEAKED support.
|
||||
///
|
||||
/// State layout (usize):
|
||||
/// 64-bit: [1 bit: destructed] [1 bit: published] [1 bit: leaked] [30 bits: weak_count] [31 bits: strong_count]
|
||||
/// 32-bit: [1 bit: destructed] [1 bit: published] [1 bit: leaked] [14 bits: weak_count] [15 bits: strong_count]
|
||||
/// 64-bit: [1 bit: destructed] [1 bit: published] [1 bit: leaked] [61 bits: strong_count]
|
||||
/// 32-bit: [1 bit: destructed] [1 bit: published] [1 bit: leaked] [29 bits: strong_count]
|
||||
pub struct RefCount {
|
||||
state: PyAtomic<usize>,
|
||||
}
|
||||
@@ -92,23 +94,22 @@ impl RefCount {
|
||||
/// Create a new RefCount with strong count = 1
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
// Initial state: strong=1, weak=1 (implicit weak for strong refs)
|
||||
Self {
|
||||
state: Radium::new(COUNT + WEAK_COUNT),
|
||||
state: Radium::new(COUNT),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current strong count
|
||||
#[inline]
|
||||
pub fn get(&self) -> usize {
|
||||
State::from_raw(self.state.load(Ordering::Relaxed)).strong() as usize
|
||||
State::from_raw(self.state.load(Ordering::Relaxed)).strong()
|
||||
}
|
||||
|
||||
/// Increment strong count
|
||||
#[inline]
|
||||
pub fn inc(&self) {
|
||||
let val = State::from_raw(self.state.fetch_add(COUNT, Ordering::Relaxed));
|
||||
if val.destructed() || (val.strong() as usize) > STRONG - 1 {
|
||||
if val.destructed() || val.strong() > STRONG - 1 {
|
||||
refcount_overflow();
|
||||
}
|
||||
if val.strong() == 0 {
|
||||
@@ -121,7 +122,7 @@ impl RefCount {
|
||||
pub fn inc_by(&self, n: usize) {
|
||||
debug_assert!(n <= STRONG);
|
||||
let val = State::from_raw(self.state.fetch_add(n * COUNT, Ordering::Relaxed));
|
||||
if val.destructed() || (val.strong() as usize) > STRONG - n {
|
||||
if val.destructed() || val.strong() > STRONG - n {
|
||||
refcount_overflow();
|
||||
}
|
||||
}
|
||||
@@ -135,7 +136,7 @@ impl RefCount {
|
||||
if old.destructed() || old.strong() == 0 {
|
||||
return false;
|
||||
}
|
||||
if (old.strong() as usize) >= STRONG {
|
||||
if old.strong() >= STRONG {
|
||||
refcount_overflow();
|
||||
}
|
||||
let new_state = old.add_strong(1);
|
||||
@@ -299,6 +300,57 @@ pub fn flush_deferred_drops() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The strong count reaches far past a 16-bit ceiling on every target.
|
||||
///
|
||||
/// The count shares its word with the flag bits, so its width follows the
|
||||
/// pointer width. A 32-bit target is the one this guards: a second counter
|
||||
/// packed beside the strong count once left it 15 bits, and `wasm32`
|
||||
/// aborted at 32 767 references — a total two ordinary module imports
|
||||
/// pass. The check is a no-op on a 64-bit host, where 31 bits already
|
||||
/// covered this; run the crate's tests against a 32-bit target to exercise
|
||||
/// it.
|
||||
#[test]
|
||||
fn strong_count_reaches_past_a_16_bit_ceiling() {
|
||||
const REFERENCES: usize = 1 << 20;
|
||||
|
||||
let rc = RefCount::new();
|
||||
rc.inc_by(REFERENCES);
|
||||
assert_eq!(rc.get(), REFERENCES + 1);
|
||||
}
|
||||
|
||||
/// `inc` and `dec` reach the same ceiling as `inc_by`.
|
||||
///
|
||||
/// The aborts reported against this layout came one reference at a time
|
||||
/// through `inc`, whose overflow check is written separately from
|
||||
/// `inc_by`'s, and the count has to come back down through `dec` without
|
||||
/// reporting the object collectable before the last reference goes.
|
||||
#[test]
|
||||
fn inc_and_dec_reach_past_a_16_bit_ceiling() {
|
||||
const REFERENCES: usize = 1 << 20;
|
||||
|
||||
let rc = RefCount::new(); // strong = 1
|
||||
for _ in 1..REFERENCES {
|
||||
rc.inc();
|
||||
}
|
||||
assert_eq!(rc.get(), REFERENCES);
|
||||
for _ in 1..REFERENCES {
|
||||
assert!(!rc.dec());
|
||||
}
|
||||
assert_eq!(rc.get(), 1);
|
||||
assert!(rc.dec());
|
||||
}
|
||||
|
||||
/// A fresh count holds exactly one strong reference and no stray bits.
|
||||
///
|
||||
/// `get` masks the flags away, so a spare field left in the word would not
|
||||
/// show up there. Reading the raw state keeps the layout honest.
|
||||
#[test]
|
||||
fn a_new_refcount_holds_one_strong_reference_and_nothing_else() {
|
||||
let rc = RefCount::new();
|
||||
assert_eq!(rc.get(), 1);
|
||||
assert_eq!(rc.state.load(Ordering::Relaxed), COUNT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn published_bit_survives_refcount_traffic() {
|
||||
let rc = RefCount::new(); // strong = 1
|
||||
|
||||
Reference in new issue
Block a user