Skip to content

Give the strong count the bits an unused weak field held - #8574

Merged
youknowone merged 3 commits into
RustPython:mainfrom
youknowone:fix-32bit-refcount-ceiling
Aug 22, 2026
Merged

Give the strong count the bits an unused weak field held#8574
youknowone merged 3 commits into
RustPython:mainfrom
youknowone:fix-32bit-refcount-ceiling

Conversation

@youknowone

@youknowone youknowone commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes #8469.

The first two commits are @JMLX42's, cherry-picked with authorship intact from
https://github.com/JMLX42/RustPython/tree/fix-32bit-refcount-ceiling β€” they reported the bug and
wrote the fix.

The bug

RefCount packs its state into one usize and split the non-flag bits evenly between a strong and
a weak count, leaving a 32-bit target 15 bits of strong count. Every object holds a strong
reference to its type, so 32 767 live instances of one type overflowed that type object's count and
called refcount_overflow() β€” std::process::abort(), which surfaces on wasm as
RuntimeError: unreachable with no Python-level error and no message.

The weak half was never read and never written: WEAK_COUNT appeared only in its own definition
and in RefCount::new, and weak references are counted by walking the object's WeakRefList
(Py::weak_count). Giving the strong count every bit the three flags leave takes 32-bit targets
from 32 767 to 536 870 911 and 64-bit targets to 2^61 - 1. The word stays usize, so no object
header grows and no target needs a 64-bit atomic.

The second commit widens State::strong to usize: at 61 bits a u32 return truncates above
2^32, where inc would read a wrapped 0 and increment again, and dec would read a wrapped 1 and
report a live object collectable.

The third commit adds a test through inc/dec. inc writes its overflow check separately from
inc_by's, and it is the path the reported aborts came through β€” Context::intern_str β†’
str_type.to_owned() β†’ inc.

Verification

Built wasm32-wasip1 (wasm-release, --features freeze-stdlib,stdlib,stdio,importlib,host_env)
before and after, and ran the reproducer from #8469 under wasmer 7.3.0:

holder = [marker] * N before after
32 765 OK OK
32 766 RuntimeError: unreachable, exit 45 OK
32 767 RuntimeError: unreachable, exit 45 OK
1 000 000 β€” OK

That matches the bisection in the issue exactly. Lib/test/test_list.py trapped on its third test
before and now runs all 68 to completion; test_int, test_dict and test_set pass, as do both
snippets the wasm-wasi CI job runs.

  • cargo test -p rustpython-common --target wasm32-wasip2 under wasmtime: 4 passed. The ceiling
    tests are no-ops on a 64-bit host, where 31 bits already covered them, so a 32-bit target is
    where they mean anything.
  • cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi: pass
  • cargo test in crates/capi: 102 passed
  • cargo run --release -- -m test test_gc test_weakref test_list test_dict test_set test_types test_sys: 1 238 run, SUCCESS
  • cargo clippy -p rustpython-common --all-targets -- -Dwarnings: clean
  • cargo check --target wasm32-wasip1, with and without threading: clean

πŸ€– Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved reference counting to support significantly larger strong-reference counts.
    • Corrected reference increment and decrement behavior, including overflow handling.
    • Ensured newly initialized reference-counted values start in a clean state without an implicit weak reference.

JMLX42 and others added 3 commits August 22, 2026 15:30
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 RustPython#8469

Assisted-by: Claude Code:claude-opus-5
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
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
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9af9e3fc-3e66-4db9-b1a4-68b2435ca981

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between dfbc9b6 and c8cd574.

πŸ“’ Files selected for processing (1)
  • crates/common/src/refcount.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


πŸ“ Walkthrough

Walkthrough

The refcount state now uses all non-flag bits for the strong count. Initialization no longer adds an implicit weak reference. Strong-count operations use native usize values, with tests for larger counts and clean initial state.

Changes

Reference-count state expansion

Layer / File(s) Summary
Expand packed state layout
crates/common/src/refcount.rs
The packed state removes the weak-count field. The documented layouts assign all non-flag bits to the strong count.
Update strong-count lifecycle
crates/common/src/refcount.rs
Construction, access, increment, and overflow checks use native usize strong counts. Tests cover counts above the former 16-bit limit, increment/decrement operations, and the initial raw state.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: βšͺ Minimal Β· up to c8cd5

This localized change expands the available strong-reference count on 32-bit targets without increasing object size, and the supplied test and build results cover the affected wasm and runtime paths. No actionable merge-blocking risk remains beyond normal checks and review.

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly describes the main change: allocating the previously unused weak-field bits to the strong count.
Linked Issues check βœ… Passed The changes address issue #8469 by removing the strong-count ceiling while preserving packed flags and atomic state updates.
Out of Scope Changes check βœ… Passed The changes remain in scope and include only refcount logic updates and tests related to issue #8469.
Docstring Coverage βœ… Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 1 files.
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

@youknowone
youknowone merged commit 344bf8f into RustPython:main Aug 22, 2026
28 checks passed
@youknowone
youknowone deleted the fix-32bit-refcount-ceiling branch August 22, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reference-count ceiling is 32 767 on 32-bit targets: refcount_overflow aborts the process

2 participants