-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
gh-128396: Fix a crash when inline comprehension has the same local variable as the outside scope #130235
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
gh-128396: Fix a crash when inline comprehension has the same local variable as the outside scope #130235
Conversation
@@ -45,8 +45,15 @@ framelocalsproxy_getval(_PyInterpreterFrame *frame, PyCodeObject *co, int i) | |||
if (kind == CO_FAST_FREE || kind & CO_FAST_CELL) { | |||
// The cell was set when the frame was created from | |||
// the function's closure. | |||
assert(PyCell_Check(value)); | |||
cell = value; | |||
// GH-128396: With PEP 709, it's possible to have a fast variable in |
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 possible this will do the wrong thing if the value comes from within the list comprehension, but happens to hold a cell object?
Something like this:
def test_closure_with_inline_comprehension(self):
lambda: k
k = 1
lst = [locals() for k in [types.CellType()]]
self.assertEqual(lst[0]['k'], 0)
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.
Yes, that's possible. Even though we do not recommend using CellType
directly, but that's always a possibility. The good news is that this PR does not make it worse - it does not work properly without this PR either. Also the existing C API PyFrame_GetVar
probably suffers from this as well.
To solve this, we need a reliable way to figure out if we are in an inlined comprehension - I think that's a loophole PEP 709 left us.
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.
The fundamental reason of this issue is the inconsistency between the variable kind that's static in the code object and the actual kind that is dynamic with inline comprehension. PEP 667 is one way to expose that but anything that relies on the kind of a variable in an inline comprehension will expose the issue.
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.
@carljm , when PEP 709 was implemented, did the idea of creating a new iteration variable on the code object with the same name came up in some way? So
def f():
x = 1
[x for x in [0]]
actually compiles to a code object with co_varnames = ['x', 'x']
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.
I think I ruled this out on the assumption that there is too much code (possibly even in CPython) relying on not having two different variable indices with the same varname in a code object, and I didn't think it was necessary. But it's possible I ruled it out too hastily; it would certainly simplify comprehension inlining a lot to do this.
Can't believe that was so trivial 😱 |
The fix might be trivial, but we still have unsolved issues behind the actual cause :) |
It seems to me that we should merge and backport this even if this is not the perfect solution, because it does not make things worse. The example given by @JelleZijlstra does not work before this PR either, and it definitely takes much more effort to make it work properly. Meanwhile, this PR solves a segfault case with minimum impact on other code. We should continue to discuss about the better solution about how PEP 709 should work, but it should not block this PR. All the other CPython/C-extension code will suffer from the same issue as well, it's not a frame proxy specific issue. |
+1, shoutout for such a quick fix! |
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 looks like a reasonable stopgap fix to me. Thanks @gaogaotiantian for the investigation and fix.
I agree we need more consideration of a more thorough fix; unfortunately I am very short on time these days to devote to this area, so I'm not sure how actively I'll be able to contribute to that in the next few months.
Thanks @gaogaotiantian for the PR 🌮🎉.. I'm working now to backport this PR to: 3.13. |
Sorry, @gaogaotiantian, I could not cleanly backport this to
|
GH-130311 is a backport of this pull request to the 3.13 branch. |
… same local variable as the outside scope (pythonGH-130235) (cherry picked from commit ccf1732) Co-authored-by: Tian Gao <[email protected]>
|
With PEP 709 + PEP 667, we have a rather special case.
When we have code like
In
f
, the code object will considerk
a free var (so in C it's aCellType
) because it is used in closure (lambda: k
). However for the list comprehension[locals() for k in [0]]
,k
will be considered as a pure fast variable which is defined by PEP 709.I think this is correct because PEP 709 says that the inlined list comprehension should behave the same as we created a new function. If we created a new function
k
would be a pure fast, not a cell variable.However, in reality, this piece of code does execute in the same frame, and
locals()
tries to read thekind
data of the variablek
before accessing the value - and it is aCO_FAST_FREE
- solocals()
will thus try to get the actual value from the cell that does not exist - which would eventually cause a crash (with--py-debug
, an assertion failure).We can of course do something in the runtime, for example setting some special flag on
k
or secretly change thekind
of the code object during execution, but I feel like that's not necessary.My fix here is simply check if the cell exists, and if not, just get the value, which is how
PyFrame_GetVar
does it. Unless people think this run time behavior should be improved, I think this fix is straightforward and simple.