Skip to content

Fix atexit unraisable exception message format#7399

Merged
youknowone merged 2 commits intoRustPython:mainfrom
youknowone:atexit
Mar 11, 2026
Merged

Fix atexit unraisable exception message format#7399
youknowone merged 2 commits intoRustPython:mainfrom
youknowone:atexit

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Mar 10, 2026

Match PyErr_FormatUnraisable behavior: use
"Exception ignored in atexit callback {func!r}" as err_msg and pass None as object instead of the callback function.

Summary by CodeRabbit

  • Behavior Changes

    • Exit callbacks now run in last-registered, first-run order (LIFO), preserving expected unwind ordering.
  • Bug Fixes

    • Improved error reports when an exit callback raises, showing a more informative callback representation when possible.
    • Added a safe fallback to a generic error message if rendering the callback for the report fails.

Match PyErr_FormatUnraisable behavior: use
"Exception ignored in atexit callback {func!r}" as err_msg
and pass None as object instead of the callback function.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

Atexit callback storage and control flow were changed: registrations insert LIFO, unregistration searches backward with identity-aware removal, runner iterates stored entries forward and reports exceptions using a rendered func repr (passing None as source). PyGlobalState atexit storage now boxes each callback tuple.

Changes

Cohort / File(s) Summary
Atexit implementation
crates/vm/src/stdlib/atexit.rs
Register now inserts callbacks at front (LIFO). Unregister searches newest→oldest and removes by identity after eq-based match. Runner iterates stored entries forward and calls each; exception handling builds "Exception ignored in atexit callback <func_repr>" and passes None as source to vm.run_unraisable.
Global state type
crates/vm/src/vm/mod.rs
PyGlobalState::atexit_funcs element type changed from Vec<(PyObjectRef, FuncArgs)> to Vec<Box<(PyObjectRef, FuncArgs)>>> (boxed tuples).

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant VM
    participant AtexitList
    participant Callback as "Callback Func"
    Caller->>VM: register(callback, args)
    VM->>AtexitList: insert_front(Box(callback,args))
    Caller->>VM: unregister(callback)
    VM->>AtexitList: search_backwards(&callback) -> identity_remove
    Note over VM,AtexitList: At shutdown
    VM->>AtexitList: take_list()
    AtexitList-->>VM: list (preserving insertion order)
    VM->>Callback: call(args)
    alt exception
        VM->>VM: try repr(callback)
        VM->>VM: vm.run_unraisable(None, "Exception ignored in atexit callback <repr>")
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hop and I register, front of the line,
LIFO for farewells, orderly and fine.
If a callback trips, I'll show its face,
"Exception ignored" with a gentle embrace.
Goodbye, little funcs — I guard your last trace.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title focuses on the unraisable exception message format fix, but the changeset includes substantial additional changes such as LIFO callback ordering, deadlock prevention in unregister, and memory layout modifications (Box wrapping).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@youknowone youknowone marked this pull request as ready for review March 10, 2026 15:46
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm/src/stdlib/atexit.rs`:
- Around line 41-49: The atexit cleanup currently passes Some("Exception ignored
in atexit callback") when func.repr(vm) fails, which diverges from CPython;
change the mapping so func.repr(vm) yields an Option<String> where Ok(repr) ->
Some(format!("Exception ignored in atexit callback {}", repr.as_wtf8())) and
Err(_) -> None, then call vm.run_unraisable(e, msg_option, vm.ctx.none());
update the intermediate variable (previously msg) to be an Option<String> to
match vm.run_unraisable's expected argument and preserve callback identity on
repr failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: fc792fce-8370-4d0b-aa18-21522eff31f9

📥 Commits

Reviewing files that changed from the base of the PR and between d248a04 and 0a2535b.

⛔ Files ignored due to path filters (1)
  • Lib/test/_test_atexit.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/vm/src/stdlib/atexit.rs

Release the lock during equality comparison in unregister so
that __eq__ can safely call atexit.unregister or atexit._clear.
Store callbacks in LIFO order (insert at front) and use
identity-based search after comparison to handle list mutations,
matching atexitmodule.c behavior.

Also pass None as err_msg when func.repr() fails, matching
CPython's PyErr_FormatUnraisable fallback.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/vm/src/stdlib/atexit.rs (1)

40-55: Minor theoretical race window in identity-based search.

The entry_ptr is captured before releasing the lock (line 40), and used for pointer comparison after re-acquiring it (line 49). If the entry is removed and its Box deallocated during vm.bool_eq(), then a new allocation reuses the same address, there's a theoretical false-positive match.

However, this is an acceptable trade-off given:

  1. The probability is extremely low (requires specific allocation patterns)
  2. The alternative (holding the lock during __eq__) can cause deadlock when __eq__ calls atexit functions
  3. CPython's implementation has similar considerations around reentrant __eq__
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/stdlib/atexit.rs` around lines 40 - 55, The pointer-comparison
can yield a false-positive if the boxed entry is deallocated and its address
reused between releasing the lock and re-acquiring it; to fix, add a stable
identity for entries instead of relying on raw pointer addresses: when pushing
into vm.state.atexit_funcs include a unique id (e.g., a monotonically
incrementing u64 or UUID) with each entry, then in the removal logic use that id
for identity-based search rather than entry_ptr (update the producer that
creates entries and the consumer code that searches/removes entries where
vm.bool_eq is used); this avoids the theoretical allocation-reuse race while
keeping the lock-release behavior around vm.bool_eq intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/vm/src/stdlib/atexit.rs`:
- Around line 40-55: The pointer-comparison can yield a false-positive if the
boxed entry is deallocated and its address reused between releasing the lock and
re-acquiring it; to fix, add a stable identity for entries instead of relying on
raw pointer addresses: when pushing into vm.state.atexit_funcs include a unique
id (e.g., a monotonically incrementing u64 or UUID) with each entry, then in the
removal logic use that id for identity-based search rather than entry_ptr
(update the producer that creates entries and the consumer code that
searches/removes entries where vm.bool_eq is used); this avoids the theoretical
allocation-reuse race while keeping the lock-release behavior around vm.bool_eq
intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9b59e07b-9f98-4980-801e-20161fa6f2f9

📥 Commits

Reviewing files that changed from the base of the PR and between 0a2535b and 51628ab.

⛔ Files ignored due to path filters (1)
  • Lib/test/_test_atexit.py is excluded by !Lib/**
📒 Files selected for processing (2)
  • crates/vm/src/stdlib/atexit.rs
  • crates/vm/src/vm/mod.rs

@youknowone youknowone merged commit 85eca21 into RustPython:main Mar 11, 2026
24 of 25 checks passed
@youknowone youknowone deleted the atexit branch March 11, 2026 13:20
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