-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
gh-139540: Fix executor deallocation crash when JIT compilation fails #139952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Note that this wouldn't solve the cold executor issue. It would only prevent an assertion failure in case we branch here (but if we branch here, we're already in a very bad state) |
@picnixz Thanks for the context regarding the "cold executor issue". |
It's a larger and more complex issue. The fact that we have an immortal cold executor seems to make normal executors leak. I don't know if it's because we reset executors in exit sides to a cold executor that then makes that specific executor not garbage-collectable (this is concerning). Your PR would still be fine but I don't know if it won't be eventually rechanged because of the fix we need to do on the other side. |
Sorry but I have the same PR up here https://github.com/python/cpython/pull/137016/files, since July of this year |
The executor object wasn't decremented if Tier 2 code returned with an exception set but not a fatal error. This change moves the Py_DECREF call inside the TIER1_TO_TIER2 macro. This ensures the executor's reference count is always decremented.
Considering this is a duplicate of gh-137016, I'm closing this PR. Sorry! |
For the other leaks, |
if (cold == NULL) { | ||
Py_FatalError("Cannot allocate core JIT code"); | ||
} | ||
_PyObject_GC_TRACK(cold); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect. The executor is immortal for now.
No this was correct, my bad. I didn't read the function correctly. The problem is that we're setting it immortal afterwards and I'm not sure it will be happy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, thanks for the correction.
The intent was to track it for consistency immediately after allocation. My assumption is the GC correctly handles a tracked object that later becomes immortal.
Is that a safe assumption, or should we explicitly _PyObject_GC_UNTRACK
it just before immortalization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That I don't know. The problem here is whether we want an immortal cold executor or not. That's why I wanted to wait for Mark's output here, and didn't write a PR already.
@Fidget-Spinner Do you mind incorporating some observations from this PR in yours? I don't know whether the change is correct here when changing |
Hi @Fidget-Spinner , Thanks for pointing that out and linking your PR. I've taken a close look at both. While our changes overlap in However, this PR primarily addresses a critical reference counting leak that was detectable with ASan. The root cause was that the My PR also includes the same I believe this PR provides a more complete solution for the stability issues in this code path. |
These changes are correct and necessary. The intended lifecycle is:
The other |
Does it fix the leak as in the issue? that is, does it fix the refcounting? I can't check this now, but please check whether the reproducer (without ASAN) is fixed when you run |
#define TIER1_TO_TIER2(EXECUTOR) \ | ||
#define TIER1_TO_TIER2(EXECUTOR) \ | ||
do { \ | ||
OPT_STAT_INC(traces_executed); \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this changed? please revert.
/* Tier-switching macros. */ | ||
|
||
#define TIER1_TO_TIER2(EXECUTOR) \ | ||
#define TIER1_TO_TIER2(EXECUTOR) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this changed? please revert.
tstate->current_executor = (PyObject *)executor; | ||
_PyFrame_SetStackPointer(frame, stack_pointer); | ||
stack_pointer = _PyFrame_GetStackPointer(frame); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are setting the stack pointer and requerying it while doing nothing inbetween.
next_instr = _Py_jit_entry((EXECUTOR), frame, stack_pointer, tstate); \ | ||
frame = tstate->current_frame; \ | ||
stack_pointer = _PyFrame_GetStackPointer(frame); \ | ||
Py_DECREF(EXECUTOR); \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should we decref it here?
assert(tstate->current_executor == NULL); | ||
assert(executor != tstate->interp->cold_executor); | ||
tstate->jit_exit = NULL; | ||
tstate->current_executor = (PyObject *)executor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it just to hold a reference to the executor?
There is a memory leak in make_executor_from_uops (Python/optimizer.c) when JIT compilation fails. The executor object is created via _PyObject_GC_NewVar but _PyObject_GC_TRACK is called after the JIT compilation attempt. If _PyJIT_Compile fails and returns an error, the code calls Py_DECREF(executor) before the object has been tracked by the garbage collector.