Skip to content

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

Merged
merged 4 commits into from
Feb 19, 2025

Conversation

gaogaotiantian
Copy link
Member

@gaogaotiantian gaogaotiantian commented Feb 17, 2025

With PEP 709 + PEP 667, we have a rather special case.

When we have code like

def f():
    lambda: k
    k = 1
    [locals() for k in [0]]
f()

In f, the code object will consider k a free var (so in C it's a CellType) 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

def g()
    lst = []
    for k in [0]:
        lst.append(k)
    return lst

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 the kind data of the variable k before accessing the value - and it is a CO_FAST_FREE - so locals() 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 the kind 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.

@gaogaotiantian gaogaotiantian changed the title Fix a crash when inline comprehension has the same local variable as the outside scope gh-128396: Fix a crash when inline comprehension has the same local variable as the outside scope Feb 17, 2025
@@ -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
Copy link
Member

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)

Copy link
Member Author

@gaogaotiantian gaogaotiantian Feb 17, 2025

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.

Copy link
Member Author

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.

Copy link
Member Author

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']

Copy link
Member

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.

@bswck
Copy link
Contributor

bswck commented Feb 17, 2025

Can't believe that was so trivial 😱

@gaogaotiantian gaogaotiantian added the needs backport to 3.13 bugs and security fixes label Feb 17, 2025
@gaogaotiantian
Copy link
Member Author

The fix might be trivial, but we still have unsolved issues behind the actual cause :)

@gaogaotiantian
Copy link
Member Author

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.

@bswck
Copy link
Contributor

bswck commented Feb 18, 2025

+1, shoutout for such a quick fix!

Copy link
Member

@carljm carljm left a 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.

@gaogaotiantian gaogaotiantian merged commit ccf1732 into python:main Feb 19, 2025
48 checks passed
@miss-islington-app
Copy link

Thanks @gaogaotiantian for the PR 🌮🎉.. I'm working now to backport this PR to: 3.13.
🐍🍒⛏🤖

@gaogaotiantian gaogaotiantian deleted the locals-inline-comp branch February 19, 2025 17:11
@miss-islington-app
Copy link

Sorry, @gaogaotiantian, I could not cleanly backport this to 3.13 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker ccf17323c218a2fdcf7f4845d3eaa74ebddefa44 3.13

@bedevere-app
Copy link

bedevere-app bot commented Feb 19, 2025

GH-130311 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.13 bugs and security fixes label Feb 19, 2025
gaogaotiantian added a commit to gaogaotiantian/cpython that referenced this pull request Feb 19, 2025
… same local variable as the outside scope (pythonGH-130235)

(cherry picked from commit ccf1732)

Co-authored-by: Tian Gao <[email protected]>
@bedevere-bot
Copy link

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot AMD64 FreeBSD14 3.x has failed when building commit ccf1732.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/1232/builds/4715) and take a look at the build logs.
  4. Check if the failure is related to this commit (ccf1732) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/1232/builds/4715

Failed tests:

  • test_interpreters
  • test.test_concurrent_futures.test_interpreter_pool

Summary of the results of the build (if available):

==

Click to see traceback logs
Traceback (most recent call last):
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/threading.py", line 1054, in _bootstrap_inner
    self.run()
    ~~~~~~~~^^
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/threading.py", line 996, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/test/test_interpreters/test_stress.py", line 30, in task
    interp = interpreters.create()
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/test/support/interpreters/__init__.py", line 76, in create
    id = _interpreters.create(reqrefs=True)
interpreters.InterpreterError: interpreter creation failed
k


Traceback (most recent call last):
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/threading.py", line 1054, in _bootstrap_inner
    self.run()
    ~~~~~~~~^^
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/threading.py", line 996, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/test/test_interpreters/test_stress.py", line 47, in run
    interp = interpreters.create()
  File "/home/buildbot/buildarea/3.x.opsec-fbsd14/build/Lib/test/support/interpreters/__init__.py", line 76, in create
    id = _interpreters.create(reqrefs=True)
interpreters.InterpreterError: interpreter creation failed
k


Traceback (most recent call last):
  File "<frozen getpath>", line 358, in <module>
ValueError: embedded null byte
Warning -- Uncaught thread exception: InterpreterError
Exception in thread Thread-145 (task):
RuntimeError: error evaluating path

gaogaotiantian added a commit that referenced this pull request Feb 19, 2025
#130311)

[3.13] gh-128396: Fix a crash when inline comprehension has the same local variable as the outside scope (GH-130235)
(cherry picked from commit ccf1732)
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.

5 participants