Skip to content

GC-manage immortal iterator wrappers that hold managed children - #693

Merged
youknowone merged 1 commit into
mainfrom
issue171
Jul 21, 2026
Merged

GC-manage immortal iterator wrappers that hold managed children#693
youknowone merged 1 commit into
mainfrom
issue171

Conversation

@youknowone

@youknowone youknowone commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Problem

The precise collector roots interpreter frames through walk_pyframe_roots +
walk_raw_immortal_roots, which recursively forwards the inline PyObjectRef
children of immortal (allocate) #[pyre_class] wrappers. JIT frames are
rooted through jitframe_trace (the gcmap), which has no immortal walk. So
for an iterator / sequence wrapper allocated immortal, a managed child reachable
solely through that wrapper (e.g. an enumerate's inner iterator, a deque
iterator's backing deque) was scanned on the interpreter path but dropped on a
moving collection while the wrapper sat in a jitframe slot
— a use-after-free
under the JIT.

Fix

Convert every #[pyre_class] iterator / sequence wrapper whose managed children
are held solely through the wrapper from immortal (allocate) to GC-managed
(allocate_stable). Managed wrappers are scanned by the marker via their tid's
ptr_offsets, so their children are forwarded on both paths.

  • pyre-object: seq / list-reverse / tuple / set iterators, reversed,
    map / filter / zip, count / repeat / compress / starmap /
    accumulate / zip_longest / cycle / chain, the SRE scanner and match
    result
    , range, the long-range iterator, takewhile / dropwhile /
    filterfalse / pairwise, and the callable-sentinel iterator.
  • pyre-interpreter: struct.Struct and its unpack iterator, the two deque
    iterators, and the tokenizer iterator.

The wrappers that were previously registered only through the immortal-root
offset loop (range, long-range, takewhile, dropwhile, filterfalse,
pairwise, _CallableIterator, struct/unpack-iter, deque iterators, tokenizer
iterator) are moved into register_pyre_class at the tail of the GC type-id
chain (tids 118..129), with matching subclass-range hierarchy + alias census
entries; the offset loop is removed. Most drop the vestigial type_id = N for
an auto-assigned tail tid; range keeps an explicit type_id = 118 pinned
to its tail slot, because RANGE_DESCR_GROUP (added by #683) bakes
W_RANGE_GC_TYPE_ID at compile time to virtualize range GET_ITER/FOR_ITER.
The SRE match result and scanner were already registered through
register_pyre_class, so converting them needs no census change.

Write barriers are added to the managed-field setters / next paths that store a
child after construction: the long-range index, the count value, the list-iter /
list-reverse / tuple / set seq, the callable, the struct format, and the
pairwise prev.

W_IntRangeIterator stays immortal — it holds only scalar counters.

Completeness audit

An adversarial audit (four independent lenses — missed conversion, missing
barrier, census consistency, descr-group tid / Rust-heap leak — each finding
verified by a skeptic) swept every #[pyre_class] struct for the same latent
class. Results:

  • W_SRE_Match — confirmed missed conversion (sibling of the converted
    scanner, holds w_string/w_buffer reachable solely through the match).
    Folded into this PR.
  • CoroutineWrapper — audited and refuted: its async drive opcodes
    (GET_AWAITABLE, SEND) permanently abort the JIT tracer, so it never lands
    in a jitframe slot; the immortal-with-registered-offsets gap is inert. Left
    immortal (correct).
  • W_TokenizerIter Rust-heap leak (low) — the now-collectable tokenizer
    iterator owns Rust heap (source String, token Vecs) with no destructor, so
    its buffers leak on collection. Pre-existing in nature (as an immortal it was
    never freed either) and a separate mechanism (destructor plumbing); deferred
    to a follow-up
    , not a UAF.
  • Barrier, census, and descr-tid lenses: no findings.

Validation

  • Boot: census (pytype_to_tid ↔ subclass-range aliases) + subclass-range
    hierarchy + GC-root completeness oracle all green.
  • gcroot battery (converted_smoke.py: every converted type under 200×
    gc.collect()): JIT output identical to NO_JIT, and identical under a
    forced 16 KB nursery.
  • W_SRE_Match (sre_match_gc.py: str + bytes match whose subject is solely
    reachable through the match, gc.collect() between): JIT == NO_JIT == tiny
    nursery.
  • jit: heal nested for-range degenerate loop; virtualize range GET_ITER/FOR_ITER #683 interaction (range_virt_gc.py + a big-int range tid check holding
    the range live across gc.collect()): correct under JIT, GC-stress, and tiny
    nursery — confirming the real managed tid equals the descr-baked
    W_RANGE_GC_TYPE_ID.
  • Perf: neutral-to-faster on the iterator microbenchmarks.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4ee87f0-be42-4021-885b-b85777c4e32b

📥 Commits

Reviewing files that changed from the base of the PR and between 9da9b5f and fc75e32.

📒 Files selected for processing (13)
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/lib.rs
  • pyre/pyre-interpreter/src/module/_collections/mod.rs
  • pyre/pyre-interpreter/src/module/_tokenize/mod.rs
  • pyre/pyre-interpreter/src/module/struct/mod.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/functional.rs
  • pyre/pyre-object/src/interp_itertools.rs
  • pyre/pyre-object/src/interp_sre.rs
  • pyre/pyre-object/src/iterobject.rs
  • pyre/pyre-object/src/operation.rs
  • pyre/pyre-object/src/pyobject.rs
  • pyre/pyre-object/src/setobject.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue171

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.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit fc75e32).
Updated: 2026-07-21T04:56:47.970Z

Files in the reviewed diff
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/lib.rs
pyre/pyre-interpreter/src/module/_collections/mod.rs
pyre/pyre-interpreter/src/module/_tokenize/mod.rs
pyre/pyre-interpreter/src/module/struct/mod.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-object/src/functional.rs
pyre/pyre-object/src/interp_itertools.rs
pyre/pyre-object/src/interp_sre.rs
pyre/pyre-object/src/iterobject.rs
pyre/pyre-object/src/operation.rs
pyre/pyre-object/src/pyobject.rs
pyre/pyre-object/src/setobject.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-object/src/functional.rs:22 ↔ pypy/module/__builtin__/functional.py:268enumerate always stores an iterator; PyPy retains an exact-list input for its list fast path.

  • pyre/pyre-object/src/functional.rs:687 ↔ pypy/module/__builtin__/functional.py:445W_Range lacks PyPy’s promote_step state, so it cannot select PyPy’s specialized one-argument/step-one range iterator forms (functional.py:546).

4. Structural adaptations

  • pyre/pyre-object/src/functional.rs:57 ↔ rpython/memory/gctransform/framework.py:803 — converting wrapper allocations to allocate_stable is a Rust GC adaptation of RPython’s translated managed-allocation transform. The same mechanical change covers the changed iterator/scanner/struct/deque constructors.

  • pyre/pyre-jit/src/eval.rs:2637 ↔ rpython/memory/gctransform/framework.py:807 — explicit Rust JIT type registration and the matching alias census (pyre/pyre-object/src/pyobject.rs:978 ↔ rpython/translator/backendopt/normalizecalls.py:302) replace RPython’s translation-time type-id/layout construction.

  • pyre/pyre-interpreter/src/baseobjspace.rs:11299 ↔ rpython/memory/gctransform/framework.py:1423 — added write barriers after pointer-field stores implement barriers that RPython inserts during GC transformation rather than in PyPy source methods.

  • pyre/pyre-object/src/functional.rs:252 ↔ pypy/module/__builtin__/functional.py:838 — Rust’s map.strict field is a CPython 3.14 compatibility addition absent from the local PyPy source.

  • pyre/pyre-interpreter/src/baseobjspace.rs:10786 ↔ pypy/objspace/std/iterobject.py:75 — sequence-iterator exhaustion/error handling deliberately follows CPython 3.14 rather than PyPy’s older behavior.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/dd61a4dc6e5301aa951c47ef7c040a59d0fed979/pyre-object/src/interp_itertools.rs#L208
P1 Badge Root takewhile before running callbacks

This makes W_TakeWhile GC-managed, but its baseobjspace::next arm still keeps only a raw &mut W_TakeWhile while calling the inner iterator, predicate, and is_true. If that Python code drops the last external reference to the takewhile object and runs gc.collect(), the receiver is not on the shadow stack and can be swept before the later it.stopped = true write; root obj and re-read the fields around those calls, as the compress/starmap branches do. The same pattern needs checking for the adjacent newly managed dropwhile/filterfalse arms.


https://github.com/youknowone/pyre/blob/dd61a4dc6e5301aa951c47ef7c040a59d0fed979/pyre-interpreter/src/baseobjspace.rs#L11299
P1 Badge Root pairwise across inner next calls

The barrier records the new w_prev child, but after this commit W_Pairwise is collectible and this branch still holds only the raw it pointer across next(it.w_iterator). If the inner iterator's __next__ drops the last Python reference to the pairwise object and triggers gc.collect(), the managed receiver can be reclaimed before the subsequent field write/barrier; pin obj and reload it around both inner next calls.


https://github.com/youknowone/pyre/blob/dd61a4dc6e5301aa951c47ef7c040a59d0fed979/pyre-object/src/operation.rs#L49
P1 Badge Root callable iterators while invoking user code

Making _CallableIterator managed means its next implementation can now be swept while it invokes the user callable: baseobjspace::next calls call_function_impl_result(callable, &[]) without pinning obj, so a callable that clears the last Python reference to this iterator and runs gc.collect() leaves only the untraced Rust local before the later re-check/set on obj. Please root the receiver across the callable call and equality check before switching this type to allocate_stable.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Convert the `#[pyre_class]` iterator / sequence wrappers whose managed
children are held solely through the wrapper from immortal (`allocate`)
to GC-managed (`allocate_stable`), so the marker scans them and forwards
their inline `PyObjectRef` children on both the interpreter and JIT paths.
As immortals the marker skipped them and only the interpreter-side
immortal-root walker forwarded the children; over jitframe slots (no
immortal walk) a child reachable solely through the wrapper was dropped on
a moving collection.

pyre-object: the seq / list-reverse / tuple / set iterators, reversed,
map / filter / zip, count / repeat / compress / starmap / accumulate /
zip_longest / cycle / chain, the SRE scanner and match result, range, the
long-range iterator, takewhile / dropwhile / filterfalse / pairwise, and
the callable-sentinel iterator. pyre-interpreter: struct.Struct and its
unpack iterator, the two deque iterators, and the tokenizer iterator.

For the wrappers that were registered only through the immortal-root offset
loop (range, long-range, takewhile, dropwhile, filterfalse, pairwise,
_CallableIterator, struct/unpack-iter, the deque iterators, the tokenizer
iterator): register them through register_pyre_class at the tail of the GC
type-id chain (tids 118..129) and add the matching subclass-range hierarchy
+ alias census entries. The offset loop is removed. Most drop the vestigial
`type_id = N` for an auto-assigned tail tid; range keeps an explicit
`type_id = 118` pinned to its tail slot because RANGE_DESCR_GROUP bakes
`W_RANGE_GC_TYPE_ID` at compile time to virtualize range GET_ITER/FOR_ITER.

The SRE match result and scanner were already registered through
register_pyre_class, so converting them needs no census change.

Add a write barrier to the managed-field setters / next paths that store a
child after construction: the long-range index, the count value, the
list-iter / list-reverse / tuple / set seq, the callable, the struct
format, and the pairwise prev.

W_IntRangeIterator stays immortal — it holds only scalar counters.

Assisted-by: Claude
@youknowone
youknowone merged commit ad78568 into main Jul 21, 2026
29 of 31 checks passed
@youknowone
youknowone deleted the issue171 branch July 21, 2026 07:18
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.

1 participant