Skip to content

gh-152330: Make perf_trampoline thread safe.#152331

Open
YukaSadaoka wants to merge 7 commits into
python:mainfrom
YukaSadaoka:trampoline
Open

gh-152330: Make perf_trampoline thread safe.#152331
YukaSadaoka wants to merge 7 commits into
python:mainfrom
YukaSadaoka:trampoline

Conversation

@YukaSadaoka

Copy link
Copy Markdown

Summary of Changes

Fixes multiple critical data races, post-fork deadlocks, memory leaks, and edge-case exception handling issues in perf_trampoline.c under Free Threading and Python 3.14.

  • Cold-Path Mutex (perf_trampoline_mutex): Introduced a dedicated mutex to guard global state mutations and JIT arena allocations (compile_trampoline(), initialization, and teardown). Hot-path executions remain lock-free.
  • Atomic State Management: Converted reads and writes for perf_status, extra_code_index, and persist_after_fork to atomic operations.
  • Safe Teardown Fallback: Updated py_trampoline_evaluator to load extra_code_index atomically. If deactivation has started (returning -1), evaluating threads cleanly bypass the JIT trampoline and fall back to _PyEval_EvalFrameDefault instead of hitting assertion failures.
  • Non-Blocking I/O: Decoupled map file writing (write_state writing to /tmp/perf-PID.map) from the critical section, ensuring slow disk I/O does not hold the compiler lock.
  • Deadlock Prevention: Added a post-fork child handler to force-reset perf_trampoline_mutex to an unlocked state, preventing deadlocks if a thread forks while another thread holds the lock.
  • Graceful Child Teardown: If trampoline re-initialization fails post-fork in a child process, the exception is cleared (PyErr_Clear()) and the trampoline is disabled gracefully rather than leaking unhandled exceptions or crashing.
  • Watcher ID Sentinel Fix: Fixed a sentinel collision in pycore_ceval_state.h by initializing the global watcher ID sentinel to -1 instead of 0. This allows 0 to be correctly recognized as a valid active watcher ID.

Fixes gh-152330

@bedevere-app

bedevere-app Bot commented Jun 26, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@python-cla-bot

python-cla-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app

bedevere-app Bot commented Jun 26, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@bedevere-app

bedevere-app Bot commented Jun 26, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@YukaSadaoka YukaSadaoka changed the title [Free Threading] Make perf_trampoline thread safe. gh-152330: [Free Threading] Make perf_trampoline thread safe. Jun 26, 2026
@bedevere-app

bedevere-app Bot commented Jun 26, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@StanFromIreland StanFromIreland changed the title gh-152330: [Free Threading] Make perf_trampoline thread safe. gh-152330: Make perf_trampoline thread safe. Jun 27, 2026
@bedevere-app

bedevere-app Bot commented Jun 28, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@kumaraditya303
kumaraditya303 requested a review from pablogsal July 24, 2026 11:44
Comment thread Python/perf_trampoline.c
{
free_code_arenas();
perf_trampoline_clear_code_watcher();
// We do NOT clear code_watcher_id here. PyCode_ClearWatcher is not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This watcher remains registered, but _PyPerfTrampoline_Init() adds another one. A destroy event then decrements trampoline_refcount twice and can free active arenas. We need to reuse or safely clear it.

Comment thread Python/perf_trampoline.c
// Call write_state outside the global lock to avoid holding it
// during JIT I/O. The default implementation
// (PyUnstable_WritePerfMapEntry) is internally thread-safe.
trampoline_api.write_state(trampoline_api.state, new_trampoline,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can race with _PyPerfTrampoline_Fini in free-threaded builds: another thread can run free_state between the unlock and this call, and perf_map_jit_write_entry does not re-check perf_map != NULL under its own lock, so we can end up writing to a closed jitdump file. Do we need to re-check the state in the jit callback, or keep write_state under the lock for that backend?

Comment thread Python/perf_trampoline.c
#endif
return -1;
}
code_watcher_id = PyCode_AddWatcher(perf_trampoline_code_watcher);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now that perf_trampoline_reset_state never calls PyCode_ClearWatcher, every re-activation registers a second copy of perf_trampoline_code_watcher (nothing outside the fork handler ever clears one). Each code destruction then decrements trampoline_refcount once per registered copy, so we can free the arenas while trampolines are still live, and after 8 activations PyCode_AddWatcher fails and the trampoline cannot be activated again in the process. I know part of this pre-dates the PR, but we should reuse code_watcher_id when it is still >= 0 instead of registering a new one.

Comment thread Python/perf_trampoline.c
return 0;
}
PyThreadState *tstate = _PyThreadState_GET();
if (code_watcher_id == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now that code_watcher_id is initialized to -1 in _PyEval_RUNTIME_PERF_INIT, this check should be removed: with the new reset behavior it can throw away a valid watcher id 0 that is still registered.

Comment thread Python/perf_trampoline.c
if (ret != 0 || f == NULL) {
new_trampoline = compile_trampoline();
if (new_trampoline != NULL) {
_PyCode_SetExtra((PyObject *)co, index,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

_PyCode_SetExtra() can fail here. We then increment the refcount and execute with an exception set. Please only publish and count the trampoline on success.

Comment thread Python/perf_trampoline.c
// Initialize to -1 since 0 is a valid watcher ID
code_watcher_id = -1;
}
if (!activate) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: PEP 7 , this block (and the fork-restart block and the default_eval fallback) is still using 2-space indentation.

Comment thread Python/perf_trampoline.c
PyMutex_Lock(&perf_trampoline_mutex);
#endif
if (ATOMIC_LOAD_STATUS() == PERF_STATUS_OK) {
ret = _PyCode_GetExtra((PyObject *)co, index, (void **)&f);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

index was loaded before taking the lock, so it can be stale here if another thread did a Fini/Init cycle in between: we would set the extra at the old index and bump the new trampoline_refcount for a trampoline the watcher will never see, so the arenas never get freed. Can we reload extra_code_index after acquiring the mutex? Same applies to PyUnstable_PerfTrampoline_CompileCode.

@@ -0,0 +1 @@
Make perf_trampoline thread safe by fixing critical data races, post-fork deadlocks, memory leaks, and edge-case exception handling issues.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: this is a change in Python/perf_trampoline.c, so the NEWS entry should go in Core and Builtins, not Library.

@bedevere-app

bedevere-app Bot commented Jul 24, 2026

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make perf_trampoline thread safe.

2 participants