Skip to content

review: act on the Codex and CodeRabbit findings on #887 - #911

Merged
youknowone merged 3 commits into
mainfrom
fib_recursive
Jul 31, 2026
Merged

youknowone merged 3 commits into
mainfrom
fib_recursive

Conversation

@youknowone

@youknowone youknowone commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #887, acting on the automated review findings that landed on it after the merge.

Removed

  • CriticalCodeGuard in the bound_reached compile path (pyre-jit/src/eval.rs). It spanned the whole trace-compile-and-dispatch region, so report_error = 0 covered the walk's residual calls into user code — stack.c:111 returns that flag in place of the overflow verdict, turning a RecursionError into an unreported native overflow. Upstream marks only regions that run no user code (compile.py:976, pyjitpl.py:3305, resume.py:1318). Measured on the fixture it was added for, synth/wasm_ca_trampoline_decline: with the guard suppressed the overflow no longer occurs at all, so the region is inert.
  • The type_id == 9 backtrace capture in remember_young_pointer.

GC correctness

  • w_tuple_new_array_backed published an uninitialized tuple. try_gc_alloc_stable_raw returns uninitialized payload and tuple_object_custom_trace reads ob_header.w_class and wrappeditems, so a collection between pin_root and the items-block allocation read garbage. The header now goes in first with a null items block — the state the trace hook returns early on — and the block is stored after.
  • W_TextIOWrapper published its reference fields with one barrier at the end of attach_buffer, but every w_str_new in between allocates and set_newline / getattr_str / set_encoder_decoder run Python. Barrier after each reference store instead.
  • OwnerRootGuard was auto-Send. Its index names a slot in the acquiring thread's OWNER_ROOTS; a PhantomData<*mut ()> pins it to that thread at compile time.

Error handling

  • attach_stdio_codec dropped both the codec lookup and the encoder/decoder construction errors, leaving a stream that reports itself unreadable however readable its buffer is. init_stream_codecs and init_importlib_bootstrap now propagate; a bootstrap failure still wins as the more fundamental one.
  • allocate_stdio still cannot propagate — it runs inside sys module creation — but no longer discards the result blind: it finishes the two fields the early return skipped instead of forcing STATE_OK over a half-applied init.
  • faulthandler.enable restores FAULTHANDLER_FD when enable_fatal_handlers fails; a failed re-enable was redirecting the handlers already installed.

faulthandler file ownership

enable resolved file=None to a hard-coded fd 2 and dropped the file object. get_fileno_and_file (handler.py:35-49) resolves the current sys.stderr, asks it for fileno(), and flushes it ignoring an ordinary flush error; enable then parks it as fatal_error_w_file (:145) and disable clears it (:150), because the descriptor the installed handler writes to belongs to that object. pyre has no Handler instance, so the owner is a process-global slot walked as a GC root alongside the other interpreter globals — not a module attribute, which would show up in dir(faulthandler) where upstream has none.

Performance

acquire_owner_root scanned OWNER_ROOTS from 0 for a free slot, so rooting the frames of a recursion depth deep cost O(depth²). Released interior slots now go on a free stack; a released tail still shrinks the walked range.

Documentation

Recorded why fbw_inline_recursion_count matching w_code alone is the element-wise greenkey compare of pyjitpl.py:1396-1401: MIFrame.setup (pyjitpl.py:74-80) assigns greenkey once, so a frame's greenkey stays its entry greens, and every InlineFrame is pushed at a callee entry with the (w_code, 0) identity reconstruct_inline_recipe also stamps. Codex had flagged this as a mismatch; it is not one.

Verification

ALL PASSED: dynasm 345/345
ALL PASSED: wasm   341/341
FAILED:     cranelift 1 failed, 344 passed

cargo test: pyre-interpreter 432 passed, majit-gc 215 passed.

The one cranelift failure, synth/mutate_then_raise_caught, panics in collector.rs (minor_custom_trace_target, invalid type_id). It reproduces 3/3 on a rebuild of main with none of these changes applied, and passes on the Linux CI runner — it is a pre-existing macOS-only failure, not introduced here.

Summary by CodeRabbit

  • Bug Fixes

    • Improved garbage collection reliability when managing interpreter and stream references.
    • Fixed faulthandler file ownership and signal-handler behavior, including safer file descriptor validation.
    • Improved tuple initialization safety during garbage collection.
    • Prevented stream codec setup errors from being silently ignored.
  • Reliability

    • Interpreter startup now reports failures when standard stream codecs cannot be initialized.
    • Improved reuse and cleanup of internal root slots for more consistent runtime behavior.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR updates GC root-slot reuse and object publication, adds faulthandler file ownership tracking, propagates stdio codec errors, exposes root-walker wiring, removes a JIT guard, and expands JIT recursion documentation.

Changes

GC rooting and object publication

Layer / File(s) Summary
Root storage and object publication
majit/majit-gc/src/collector.rs, majit/majit-gc/src/shadow_stack.rs, pyre/pyre-object/src/tupleobject.rs
Owner-root slots reuse interior holes, guards become non-Send, tuple headers publish before item storage, and the special debug backtrace branch is removed.
Interpreter root-walker wiring
pyre/pyre-interpreter/src/eval.rs
The Python frame root area is exposed and the faulthandler root walker is conditionally registered.

Runtime initialization and handler ownership

Layer / File(s) Summary
Faulthandler file resolution and ownership
pyre/pyre-interpreter/src/module/faulthandler/handler.rs
File descriptors are resolved and validated, owned file objects are tracked as GC roots, and ownership is cleared during handler removal.
Stdio codec publication and error propagation
pyre/pyre-interpreter/src/module/_io/textio.rs, pyre/pyre-interpreter/src/module/sys/vm.rs, pyre/pyrex/src/lib.rs
TextIOWrapper publishes stored references, codec attachment becomes fallible, and stream initialization errors propagate through bootstrap.

JIT control-flow documentation

Layer / File(s) Summary
Function-entry path and recursion documentation
pyre/pyre-jit/src/eval.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
Function-entry compilation no longer enters a critical-code guard, and greenkey filtering assumptions are documented.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

I hop through roots where young refs hide,
Keep stream-bound objects safe inside.
Signals hold files, then roots align,
Codec errors now travel the line.
The JIT hops free—what a fine design!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the PR, but it is too generic and does not describe the actual code changes. Rename it to summarize the main change, e.g. fixing GC publication, faulthandler ownership, and codec error propagation.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fib_recursive

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.

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: d4226796ea

ℹ️ 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".

Comment on lines +272 to +274
let (fd, _w_file) = faulthandler_get_fileno_and_file(
args.get(1).copied().unwrap_or(pyre_object::PY_NULL),
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain each registered signal's file owner

On Unix host builds, when register(file=None) resolves a redirected sys.stderr, the returned owner is discarded here while register_user_signal retains only its descriptor. If sys.stderr is later rebound and the old stream is collected, its descriptor can be closed or reused before the registered signal fires. PyPy stores w_file per signal in Handler.user_w_files and removes it on unregister, so pyre needs an equivalent GC-rooted per-signal owner rather than _w_file.

AGENTS.md reference: AGENTS.md:L184-L192

Useful? React with 👍 / 👎.

Comment on lines +122 to +123
if let Ok(flush) = crate::baseobjspace::getattr_str(resolved, "flush") {
let _ = crate::call::call_function_impl_result(flush, &[]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve asynchronous flush exceptions

When a file's flush attribute lookup or call raises SystemExit, these two ignored results swallow it and allow faulthandler.enable or register to continue. The cited PyPy implementation ignores only ordinary OperationErrors and explicitly re-raises asynchronous exceptions; pyre already represents this distinction with PyErrorKind::SystemExit, so this path should propagate that case.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

Drop the `CriticalCodeGuard` from the `bound_reached` compile path
(`eval.rs`).  It spanned the whole trace-compile-and-dispatch region, so
`report_error = 0` covered the walk's residual calls into user code —
`stack.c:111` returns that flag in place of the overflow verdict, turning
a RecursionError into an unreported native overflow.  Upstream marks only
regions that run no user code (`compile.py:976` `cpu.force` +
`handle_async_forcing`, `pyjitpl.py:3305` `rebuild_state_after_failure`,
`resume.py:1318`).  Measured on the fixture it was added for,
`synth/wasm_ca_trampoline_decline`: with the guard suppressed the
overflow no longer occurs at all, so the region is inert.

Record why `fbw_inline_recursion_count` matching `w_code` alone is the
element-wise greenkey compare of `pyjitpl.py:1396-1401`: `MIFrame.setup`
(`pyjitpl.py:74-80`) assigns `greenkey` once, so a frame's greenkey stays
its entry greens, and every `InlineFrame` is pushed at a callee entry
with the `(w_code, 0)` identity `reconstruct_inline_recipe` also stamps.

Write the tuple header before publishing its root (`tupleobject.rs`).
`try_gc_alloc_stable_raw` returns uninitialized payload and
`tuple_object_custom_trace` reads `ob_header.w_class` and `wrappeditems`,
so a collection between `pin_root` and the items-block allocation read
garbage.  The header now goes in first with a null items block — the
state the trace hook returns early on — and the block is stored after.

Restore `FAULTHANDLER_FD` when `enable_fatal_handlers` fails; a failed
re-enable was redirecting the handlers already installed.

Give `OwnerRootGuard` a `PhantomData<*mut ()>`: its `index` names a slot
in the acquiring thread's `OWNER_ROOTS`, and a bare `usize` left the
guard auto-`Send`.

Delete the `type_id == 9` backtrace capture from
`remember_young_pointer`.

check.py: dynasm 345/345, wasm 341/341.  cranelift 344/345 —
`synth/mutate_then_raise_caught` panics in `collector.rs`
(`minor_custom_trace_target`, invalid type_id) 3/3 both with and without
this commit, and passes on the Linux CI runner.

Assisted-by: Claude
`W_TextIOWrapper` published its reference fields with one write barrier at
the end of `attach_buffer`, but every `w_str_new` in between allocates and
`set_newline` / `getattr_str` / `set_encoder_decoder` run Python, so a
collection could land after a store and before the barrier that would have
told it to trace the wrapper.  Barrier after each reference store instead,
through a `publish_refs` helper, in `attach_buffer` and in
`set_encoder_decoder`.

`attach_stdio_codec` dropped both the codec lookup and the incremental
encoder/decoder construction errors, leaving a stream that reports itself
unreadable however readable its buffer is.  Propagate them:
`init_stream_codecs` and `init_importlib_bootstrap` now return the failure,
with a bootstrap failure still winning as the more fundamental one.

`allocate_stdio` still cannot propagate — it runs inside `sys` module
creation, before there is an interpreter to raise into — but it no longer
discards the result blind: with a null codec the only fallible step
`attach_buffer` reaches is `buffer.seekable()`, so finish the two fields
that early return skipped rather than leaving them at `Default` and
forcing `STATE_OK` over a half-applied init.

`faulthandler.enable` resolved `file=None` to a hard-coded fd 2 and dropped
the file object.  `get_fileno_and_file` (`handler.py:35-49`) resolves the
CURRENT `sys.stderr`, asks it for `fileno()`, and flushes it ignoring an
ordinary flush error; `enable` then parks it as `fatal_error_w_file`
(`handler.py:145`) and `disable` clears it (`:150`), because the descriptor
the installed handler writes to belongs to that object.  pyre has no
Handler instance, so the owner is a process-global slot walked as a GC root
alongside the other interpreter globals — not a module attribute, which
would show up in `dir(faulthandler)` where upstream has none.

`acquire_owner_root` scanned `OWNER_ROOTS` from 0 for a free slot, so
rooting the frames of a recursion `depth` deep cost O(depth²).  Released
interior slots now go on a free stack; a released tail still shrinks the
walked range, and the shrink drops the queued indices it took out of range.

check.py: dynasm 345/345, wasm 341/341.  cranelift 344/345 —
`synth/mutate_then_raise_caught` panics in `collector.rs`
(`minor_custom_trace_target`, invalid type_id) with and without this
commit, and passes on the Linux CI runner.  cargo test: pyre-interpreter
432, majit-gc 215.

Assisted-by: Claude
@github-actions

github-actions Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 27e7baf).
Updated: 2026-07-30T18:23:50.960Z

Files in the reviewed diff
majit/majit-gc/src/collector.rs
majit/majit-gc/src/shadow_stack.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/module/_io/textio.rs
pyre/pyre-interpreter/src/module/faulthandler/handler.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-object/src/tupleobject.rs
pyre/pyrex/src/lib.rs

Codex did not produce a report (exit 1). Last log lines:

file NOT in the list is by definition not introduced by this patch — report
it under section 3 instead, or omit it. Verify every section-1/2 citation
against the list before finalizing the report.

---

Output format requirements (so the report can be parsed mechanically and
posted/triaged automatically). Use these four headings VERBATIM, in this
order, and nothing else at heading level 2:

## 1. Regressions to PyPy parity introduced by this patch
## 2. Other mismatches introduced by this patch
## 3. Pre-existing mismatches (already present before this patch)
## 4. Structural adaptations

Under each heading, list every finding as a bullet. For each finding cite the
concrete `our_file.rs:line ↔ rpython_or_pypy_file.py:line` pair and quote the
divergence concisely. If a section has no findings, still emit the heading
followed by a single line `None.` so all four sections are always present.
Do not modify any files; produce the report only.

Authoritative changed-file list for this patch (git diff upstream/main --name-only):
majit/majit-gc/src/collector.rs
majit/majit-gc/src/shadow_stack.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/module/_io/textio.rs
pyre/pyre-interpreter/src/module/faulthandler/handler.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-object/src/tupleobject.rs
pyre/pyrex/src/lib.rs
warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the bundled bubblewrap in the meantime.
2026-07-30T18:23:48.724771Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:48.817747Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": null,\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401,\n  \"detail\": {\n    \"code\": \"token_expired\",\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\"\n  }\n}")
2026-07-30T18:23:48.837209Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:48.898077Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": null,\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401,\n  \"detail\": {\n    \"code\": \"token_expired\",\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\"\n  }\n}")
2026-07-30T18:23:48.979207Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:48.994866Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.010676Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.255834Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-30T18:23:49.287774Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.302619Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.317289Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.533371Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-30T18:23:49.550574Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.694978Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.847370Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:49.984311Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.128166Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.145242Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.161727Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.369961Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-30T18:23:50.402398Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.416910Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.431517Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.657516Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:23:50.679884Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.

… resolution

`interp_return_log_enabled` was defined twice in `pyre-interpreter/src/eval.rs`
— #874 and #907 each added a copy under the same `cfg`, and neither PR's CI
saw the other's, so `main` does not compile (E0428).  Delete the later
duplicate.

`register` discarded the file `get_fileno_and_file` resolved while the
installed handler kept only its descriptor, so a `register(sig, f)` whose `f`
was later collected left the handler writing through a closed or reused fd.
`handler.py:125` keeps `user_w_files[signum] = w_file` and `:132` pops it on
`unregister`; hold the same per-signal owners in a process-global table walked
by the faulthandler root walker, which now covers both it and the fatal-error
file.

A `fileno()` returning a negative value reached `FAULTHANDLER_FD` and the
handler registration unchecked.  Measured: PyPy accepts it (`enable()`
succeeds), 3.14 raises `RuntimeError: file.fileno() is not a valid file
descriptor` — distinct from the direct-int arm's `ValueError: file is not a
valid file descriptor`.  Follow 3.14.

Resolving the file argument runs a user `fileno()` and `flush()`; `enable` and
`register` ran it before the support gate, so a build that can only answer
`NotImplementedError` executed those side effects first.  Move both inside the
gate — which also puts `register`'s integer coercions ahead of the file
resolution, as its argument parsing does.

Not changed, after measuring: a `SystemExit` raised out of `flush` does NOT
abort `enable`.  `handler.py:44-48` re-raises asynchronous errors
(`error.py:62-65`) and PyPy does propagate it, but 3.14 clears the flush error
unconditionally and swallows it; 3.14 is the behaviour target.

check.py: dynasm 345/345, cranelift 345/345, wasm 341/341 — all three green;
`synth/mutate_then_raise_caught`, which panicked in `collector.rs`
(`minor_custom_trace_target`) 3/3 before, now passes 5/5.  cargo test
pyre-interpreter 438.  faulthandler probes (enable/register file retention
across GC, unregister, negative fileno, missing fileno, flush errors) match
python3.14 line for line.

Assisted-by: Claude
@youknowone

Copy link
Copy Markdown
Collaborator Author

Pushed 27e7baf70e, acting on the two review findings above plus a main breakage found while verifying.

main does not compile

interp_return_log_enabled is defined twice in pyre/pyre-interpreter/src/eval.rs, under the same #[cfg(not(feature = "sandbox"))] — #874 added one copy and #907 the other, and neither PR's CI saw the other's, so origin/main fails with E0428. Deleted the later duplicate here, since this branch cannot build otherwise.

Retain each registered signal's file owner — fixed

register discarded the resolved file while the installed handler kept only its descriptor. handler.py:125 keeps user_w_files[signum] = w_file and :132 pops it on unregister; the same per-signal owners now live in a process-global table walked by the faulthandler root walker, which covers both it and the fatal-error file. Verified: register(sig, f); del f; gc.collect() leaves the fd open, and unregister returns True then False.

Preserve asynchronous flush exceptions — not applied, after measuring

This one is PyPy-only. handler.py:44-48 does re-raise via e.async(space), and PyPy propagates a SystemExit raised out of flush. But pyre's behaviour target is CPython 3.14, and 3.14 clears the flush error unconditionally:

enable(obj) where obj.flush() raises SystemExit(7)
PyPy 3.11 SystemExit propagated: 7
CPython 3.14.5 SystemExit SWALLOWED

I had implemented the propagation before checking, then reverted it — applying it would have moved pyre away from its target. The divergence is now recorded in a comment at the call site.

Also fixed while in there

A fileno() returning a negative value reached FAULTHANDLER_FD and the handler registration unchecked. PyPy accepts it; 3.14 raises RuntimeError: file.fileno() is not a valid file descriptor, distinct from the direct-int arm's ValueError: file is not a valid file descriptor. Follows 3.14 now.

Resolving the file argument runs a user fileno() and flush(), and both enable and register ran it before the support gate — so a build that can only answer NotImplementedError executed those side effects first. Both moved inside the gate, which also puts register's integer coercions ahead of the file resolution, as its argument parsing does.

Verification

ALL PASSED: dynasm    345/345
ALL PASSED: cranelift 345/345
ALL PASSED: wasm      341/341

All three backends green. synth/mutate_then_raise_caught, which panicked in collector.rs (minor_custom_trace_target, invalid type_id) 3/3 on macOS before — including on a rebuild of the base with none of this branch applied — now passes 5/5. cargo test: pyre-interpreter 438.

Every faulthandler probe (file retention across GC for both enable and register, unregister, negative fileno, missing fileno, ordinary and async flush errors, dir() surface) matches python3.14 line for line.

— commented by Claude

@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/27e7baf70e28dd406dae074ea85a0338a338b45b/pyre-interpreter/src/module/faulthandler/handler.rs#L174-L177
P2 Badge Propagate asynchronous flush exceptions

When a supplied file's flush() raises SystemExit, this unconditional discard lets faulthandler.enable() or register() continue and install the handler instead of propagating the exception; pypy/module/faulthandler/handler.py:43-48 explicitly re-raises asynchronous errors. The fresh evidence in this revision is the added comment explicitly confirming that the PyPy behavior is being bypassed in favor of CPython 3.14, despite the repository's strict structural-parity requirement.

AGENTS.md reference: AGENTS.md:L194-L196

ℹ️ 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".

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-interpreter/src/module/faulthandler/handler.rs`:
- Around line 27-36: Update the intra-doc link in the FAULTHANDLER_FILE
documentation to reference the existing walk_faulthandler_roots function instead
of the nonexistent walk_fatal_error_file symbol.
- Around line 146-153: Re-read the normalized pinned object via
gc_roots::shadow_stack_get(file_slot) after pin_root and before calling
getattr_str for "fileno". Use this refreshed value instead of the original
resolved when obtaining and invoking the fileno method, matching the later flush
path.
- Around line 192-238: Add a process-wide faulthandler mutex and acquire its
guard across the complete fd/file update sequences in the enable, register, and
unregister implementations. Hold the guard through handler installation or
removal and the corresponding FAULTHANDLER_FD, FAULTHANDLER_FILE, and per-signum
table updates, including rollback paths, so concurrent calls cannot observe
mismatched ownership.

In `@pyre/pyre-object/src/tupleobject.rs`:
- Around line 200-216: Update the null-raw fallback around
try_gc_alloc_stable_raw and alloc_tuple_items_block_gc to root header.w_class
before the allocation call, then reload the rooted class pointer when
constructing W_TupleObject. Ensure the fallback uses the refreshed shadow-stack
value rather than the potentially stale local header.w_class.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a032245b-1e5f-4037-9681-08b1c0f2ee75

📥 Commits

Reviewing files that changed from the base of the PR and between 25b2442 and 27e7baf.

📒 Files selected for processing (10)
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/shadow_stack.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/module/_io/textio.rs
  • pyre/pyre-interpreter/src/module/faulthandler/handler.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/tupleobject.rs
  • pyre/pyrex/src/lib.rs
💤 Files with no reviewable changes (2)
  • majit/majit-gc/src/collector.rs
  • pyre/pyre-jit/src/eval.rs

Comment on lines +27 to +36
/// `handler.py:145` `self.fatal_error_w_file = w_file` / `handler.py:150`
/// `self.fatal_error_w_file = None`: the descriptor the handler writes to
/// belongs to this object, so it has to outlive the installed handlers rather
/// than be collected and have its finalizer close the fd under them. pyre has
/// no Handler instance and the signal callback is a bare `extern "C" fn` that
/// cannot capture one, so the owner is a process-global slot, walked as a GC
/// root by [`walk_fatal_error_file`].
#[cfg(all(unix, feature = "host_env"))]
static FAULTHANDLER_FILE: std::sync::atomic::AtomicPtr<pyre_object::PyObject> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale intra-doc reference: walk_fatal_error_file doesn't exist.

The doc comment links to [walk_fatal_error_file], but the function actually defined below (line 79/100) is walk_faulthandler_roots.

✏️ Proposed fix
-/// than be collected and have its finalizer close the fd under them.  pyre has
-/// no Handler instance and the signal callback is a bare `extern "C" fn` that
-/// cannot capture one, so the owner is a process-global slot, walked as a GC
-/// root by [`walk_fatal_error_file`].
+/// than be collected and have its finalizer close the fd under them.  pyre has
+/// no Handler instance and the signal callback is a bare `extern "C" fn` that
+/// cannot capture one, so the owner is a process-global slot, walked as a GC
+/// root by [`walk_faulthandler_roots`].
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// `handler.py:145` `self.fatal_error_w_file = w_file` / `handler.py:150`
/// `self.fatal_error_w_file = None`: the descriptor the handler writes to
/// belongs to this object, so it has to outlive the installed handlers rather
/// than be collected and have its finalizer close the fd under them. pyre has
/// no Handler instance and the signal callback is a bare `extern "C" fn` that
/// cannot capture one, so the owner is a process-global slot, walked as a GC
/// root by [`walk_fatal_error_file`].
#[cfg(all(unix, feature = "host_env"))]
static FAULTHANDLER_FILE: std::sync::atomic::AtomicPtr<pyre_object::PyObject> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
/// `handler.py:145` `self.fatal_error_w_file = w_file` / `handler.py:150`
/// `self.fatal_error_w_file = None`: the descriptor the handler writes to
/// belongs to this object, so it has to outlive the installed handlers rather
/// than be collected and have its finalizer close the fd under them. pyre has
/// no Handler instance and the signal callback is a bare `extern "C" fn` that
/// cannot capture one, so the owner is a process-global slot, walked as a GC
/// root by [`walk_faulthandler_roots`].
#[cfg(all(unix, feature = "host_env"))]
static FAULTHANDLER_FILE: std::sync::atomic::AtomicPtr<pyre_object::PyObject> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/faulthandler/handler.rs` around lines 27 -
36, Update the intra-doc link in the FAULTHANDLER_FILE documentation to
reference the existing walk_faulthandler_roots function instead of the
nonexistent walk_fatal_error_file symbol.

Comment on lines +146 to 153
// `fileno` and `flush` both run Python; keep the file addressable across
// them so the caller receives the relocated pointer, not a stale one.
pyre_object::gc_roots::pin_root(resolved);
let file_slot = pyre_object::gc_roots::shadow_stack_len() - 1;
let method = crate::baseobjspace::getattr_str(resolved, "fileno")?;
let res = crate::call::call_function_impl_result(method, &[])?;
if !unsafe { pyre_object::is_int(res) } {
return Err(crate::PyError::type_error("fileno() returned non-integer"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Stale pointer reused after pin_root — missing re-read via shadow_stack_get.

After pin_root(resolved), resolved is still the un-normalized value the caller passed in; pin_root only fixes up the shadow-stack slot in place (it does not return the normalized address). The established pattern elsewhere in this codebase (call_function_impl_result, FrameLocalsProxy::new) is to re-read the pinned value via shadow_stack_get before using it in a call that can trigger GC. Here, getattr_str(resolved, "fileno") uses the original resolved, not the normalized value — yet a few lines below (line 178), the same function correctly does let resolved = pyre_object::gc_roots::shadow_stack_get(file_slot); before the flush call. This inconsistency inside the same function indicates the first use was missed.

As pin_root's own doc explains, a foreign mutator can have moved the object between the caller obtaining resolved (e.g. via getattr_str(sys, "stderr")) and this pin_root call, leaving resolved a forwarding stub that must be normalized before use.

🐛 Proposed fix
     pyre_object::gc_roots::pin_root(resolved);
     let file_slot = pyre_object::gc_roots::shadow_stack_len() - 1;
-    let method = crate::baseobjspace::getattr_str(resolved, "fileno")?;
+    let resolved = pyre_object::gc_roots::shadow_stack_get(file_slot);
+    let method = crate::baseobjspace::getattr_str(resolved, "fileno")?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// `fileno` and `flush` both run Python; keep the file addressable across
// them so the caller receives the relocated pointer, not a stale one.
pyre_object::gc_roots::pin_root(resolved);
let file_slot = pyre_object::gc_roots::shadow_stack_len() - 1;
let method = crate::baseobjspace::getattr_str(resolved, "fileno")?;
let res = crate::call::call_function_impl_result(method, &[])?;
if !unsafe { pyre_object::is_int(res) } {
return Err(crate::PyError::type_error("fileno() returned non-integer"));
// `fileno` and `flush` both run Python; keep the file addressable across
// them so the caller receives the relocated pointer, not a stale one.
pyre_object::gc_roots::pin_root(resolved);
let file_slot = pyre_object::gc_roots::shadow_stack_len() - 1;
let resolved = pyre_object::gc_roots::shadow_stack_get(file_slot);
let method = crate::baseobjspace::getattr_str(resolved, "fileno")?;
let res = crate::call::call_function_impl_result(method, &[])?;
if !unsafe { pyre_object::is_int(res) } {
return Err(crate::PyError::type_error("fileno() returned non-integer"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/faulthandler/handler.rs` around lines 146 -
153, Re-read the normalized pinned object via
gc_roots::shadow_stack_get(file_slot) after pin_root and before calling
getattr_str for "fileno". Use this refreshed value instead of the original
resolved when obtaining and invoking the fileno method, matching the later flush
path.

Comment on lines +192 to +238
// Resolving the argument runs a user `fileno()` and `flush()`;
// keep those side effects behind the support gate, so a build
// that can only answer NotImplementedError does not run them
// first.
#[cfg(all(unix, feature = "host_env"))]
{
FAULTHANDLER_FD.store(fd, std::sync::atomic::Ordering::Relaxed);
let (fd, w_file) = faulthandler_get_fileno_and_file(
args.first().copied().unwrap_or(pyre_object::PY_NULL),
)?;
// `set_fatal_error_file` allocates its key, so the file has
// to stay addressable from here to the store below.
let _roots = pyre_object::gc_roots::push_roots();
let file_slot = (!w_file.is_null()).then(|| {
pyre_object::gc_roots::pin_root(w_file);
pyre_object::gc_roots::shadow_stack_len() - 1
});
// `pypy_faulthandler_enable(fileno, all_threads)` takes the
// descriptor as an argument, so the handler never observes
// a descriptor the install did not commit to. Split across
// two statements here, the new fd has to be visible before
// the handlers go in — a fatal signal in between would
// otherwise dump to the old one — and has to be rolled back
// when the install fails, or a failed re-enable would
// redirect the handlers already installed.
let previous_fd =
FAULTHANDLER_FD.swap(fd, std::sync::atomic::Ordering::Relaxed);
let ok = rustpython_host_env::faulthandler::enable_fatal_handlers(
faulthandler_signal_handler,
libc::SA_NODEFER | libc::SA_ONSTACK,
);
if ok {
FAULTHANDLER_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
// `handler.py:145` `self.fatal_error_w_file = w_file`.
set_fatal_error_file(file_slot.map_or(
pyre_object::PY_NULL,
pyre_object::gc_roots::shadow_stack_get,
));
return Ok(pyre_object::w_none());
}
FAULTHANDLER_FD.store(previous_fd, std::sync::atomic::Ordering::Relaxed);
return Err(crate::PyError::runtime_error(
"faulthandler.enable: sigaction failed",
));
}
#[cfg(not(all(unix, feature = "host_env")))]
{
let _ = fd;
let _ = args;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any global interpreter lock / serialization primitive guarding
# builtin-function dispatch, which would make concurrent enable()/register()
# calls impossible in practice.
rg -n -C3 --type=rust '\bGIL\b|global_interpreter_lock|InterpLock' pyre majit

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== faulthandler outline =="
ast-grep outline pyre/pyre-interpreter/src/module/faulthandler/handler.rs || true

echo "== relevant faulthandler lines =="
cat -n pyre/pyre-interpreter/src/module/faulthandler/handler.rs | sed -n '1,440p'

echo "== globals definitions =="
rg -n 'FAULTHANDLER_(FD|ENABLED|FILE)|register_user_signal|set_user_signal_file|enable_fatal_handlers' pyre/pyre-interpreter/src/module/faulthandler

Repository: youknowone/pyre

Length of output: 27492


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== global lock / interpreter run guards =="
rg -n --type=rust -C4 '\bInterp(?:Lock|Guard)\b|\bInterpreter(?:Lock|Guard)\b|\bGIL\b|with_lock|lock\(|mutex\(|Mutex|RwLock|SingleThread|single.*thread|freethread|FreeThreaded' pyre/pyre-interpreter src 2>/dev/null | head -n 300

echo "== module dispatch / builtin maker =="
rg -n --type=rust -C5 'fn make_builtin_function_with|pub fn module_ns_store|call_function_impl_result|PyObjectRef' pyre/pyre-interpreter/src | head -n 350

echo "== faulthandler references =="
rg -n -C2 '\bfaulthandler\b|enable_fau|register_user_signal|unregister_user_signal' pyre majit --type=rust --type=py --type=text | head -n 240

Repository: youknowone/pyre

Length of output: 24578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate builtin/function wrappers =="
rg -n --type=rust -C4 'make_builtin_function|make_builtin_function_with|fn call_function_impl_result|struct builtin|BuiltinFunction' pyre/pyre-interpreter/src | head -n 260

echo "== exact builtin symbol references =="
python3 - <<'PY'
from pathlib import Path
for p in Path('pyre/pyre-interpreter/src').rglob('*.rs'):
    s=p.read_text(errors='replace')
    if 'fn make_builtin_function' in s or 'pub fn make_builtin_function' in s or 'call_function_impl_result' in s:
        print(f'--- {p}')
        for i,line in enumerate(s.splitlines(),1):
            if 'make_builtin_function' in line or 'call_function_impl_result' in line:
                print(f'{i}: {line}')
PY

Repository: youknowone/pyre

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gateway.rs make_builtin definitions =="
cat -n pyre/pyre-interpreter/src/gateway.rs | sed -n '1000,1155p'

echo "== call.rs call_function_impl_result definition and thread docs =="
cat -n pyre/pyre-interpreter/src/call.rs | sed -n '1,90p;200,330p;380,410p'

echo "== module storage ownership semantics =="
cat -n pyre/pyre-interpreter/src/gateway.rs | sed -n '1295,1340p'
rg -n -C3 'fn module_ns_store|FAULTHANDLER_REGISTERED|register_module' pyre/pyre-interpreter/src pyre

Repository: youknowone/pyre

Length of output: 50373


Guard the faulthandler fd/file ownership updates with a local mutex.

There is no process-wide interpreter lock serializing these builtin calls, and enable/register update shared globals across separate stores: enable swaps FAULTHANDLER_FD before installing/clearing FAULTHANDLER_FILE; register stores the file through the shared per-signum table only after register_user_signal; unregister can clear an entry while another thread has already installed for that signal. A per-faulthandler mutex around the fd-install and file-statement sequences prevents mismatched ownership across concurrent calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/faulthandler/handler.rs` around lines 192 -
238, Add a process-wide faulthandler mutex and acquire its guard across the
complete fd/file update sequences in the enable, register, and unregister
implementations. Hold the guard through handler installation or removal and the
corresponding FAULTHANDLER_FD, FAULTHANDLER_FILE, and per-signum table updates,
including rollback paths, so concurrent calls cannot observe mismatched
ownership.

Comment on lines 200 to 216
let raw_slot = if raw.is_null() {
None
} else {
unsafe {
std::ptr::write(
raw as *mut W_TupleObject,
W_TupleObject {
ob_header: PyObject {
ob_type: header.ob_type,
w_class: header.w_class,
},
wrappeditems: std::ptr::null_mut(),
},
);
}
crate::gc_roots::pin_root(raw as PyObjectRef);
Some(crate::gc_roots::shadow_stack_len() - 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Root header.w_class on the raw-allocation fallback.

When try_gc_alloc_stable_raw returns null, header.w_class remains an unrooted local across alloc_tuple_items_block_gc, which may move GC objects. Reload that class pointer from a shadow-stack root before constructing the fallback Box<W_TupleObject>; otherwise the fallback can retain a stale GC address.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-object/src/tupleobject.rs` around lines 200 - 216, Update the
null-raw fallback around try_gc_alloc_stable_raw and alloc_tuple_items_block_gc
to root header.w_class before the allocation call, then reload the rooted class
pointer when constructing W_TupleObject. Ensure the fallback uses the refreshed
shadow-stack value rather than the potentially stale local header.w_class.

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