FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

gh-125038: Crash after genexpr.gi_frame.f_locals manipulations is fixed by efimov-mikhail · Pull Request #125178 · python/cpython · GitHub

Repository navigation

gh-125038: Crash after genexpr.gi_frame.f_locals manipulations is fixed - #125178

Merged
JelleZijlstra merged 11 commits into
python:mainfrom
efimov-mikhail:crash_fix_generators_f_locals
Oct 22, 2024
Merged

JelleZijlstra merged 11 commits into
python:mainfrom
efimov-mikhail:crash_fix_generators_f_locals

Conversation

efimov-mikhail commented Oct 9, 2024 •
edited
Loading

Copy link
Copy Markdown
Member

Some iterator checks are added for _FOR_ITER, _FOR_ITER_TIER_TWO and INSTRUMENTED_FOR_ITER bytecode implementations. TypeError is raised in case of tp_iternext == NULL. Tests on generator modifying through gi_frame.f_locals are added, both to genexpr generators and function generators.

I've added some test cases to emphasize current code behavior and save it explicitly in tests.
IMHO, there is still a room to improvement.

Previous PR becomes incorrect (#125051) after my typo in git commands.
I'm sorry about that.

@JelleZijlstra, Could you please review this again?
Moreover, label "needs backport to 3.13" should be added once again.

…is fixed

Some iterator checks are added for _FOR_ITER,
_FOR_ITER_TIER_TWO and INSTRUMENTED_FOR_ITER bytecode implementations.
TypeError is raised in case of tp_iternext == NULL.
Tests on generator modifying through gi_frame.f_locals are added, both
to genexpr generators and function generators.
…e-125038.ffSLCz.rst

Co-authored-by: Kirill Podoprigora <kirill.bast9@mail.ru>
Eclips4 requested a review from JelleZijlstra October 9, 2024 11:28
Comment thread Python/bytecodes.c Outdated
if (iternext == NULL) {
_PyErr_Format(tstate, PyExc_TypeError,
"'for' requires an object with "
"__iter__ method, got %.100s",

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This error is incorrect as it's looking for __next__, not __iter__. I think we can use the same error as builtins.next():

        PyErr_Format(PyExc_TypeError, 
            "'%.200s' object is not an iterator",
            Py_TYPE(it)->tp_name);

Copy link
Copy Markdown
Member

I think this is wrong approach. Rather than modifying FOR_ITER, etc to handle an incorrect VM state, we should generate code that cannot get into that state, in the bytecode compiler.

Currently, the bytecode compiler assumes that local variables in generator expressions cannot be modified in
This is no longer true, but it remains true that the evaluation stack cannot be modified.

The bytecode for (x for x in ...) starts with:

      0 RETURN_GENERATOR
      2 POP_TOP
      4 RESUME                   0
      6 LOAD_FAST                0 (.0)
>>    8 FOR_ITER                 6 (to 24)

Compare that with the code for for x in ....:

      0 RESUME                   0
      2 LOAD_FAST                0 (...)
      4 GET_ITER
>>    6 FOR_ITER                 4 (to 18)
      10 STORE_FAST               1 (x)

Note the additional GET_ITER as the bytecode compiler doesn't know what ... is.

I think adding the GET_ITER before the FOR_ITER would gain the robustness required in this case, without complicating the iteration code.

Copy link
Copy Markdown
Member Author

So, basically, we want to provide equal results for such code fragments:

  1. Iterable in f_locals['.0']
g = (x for x in range(10))
g.gi_frame.f_locals['.0'] = range(20)
  1. Iterator in f_locals['.0']
g = (x for x in range(10))
g.gi_frame.f_locals['.0'] = iter(range(20))

Have I got your idea correct on high level?

IMHO, this will change be convenient.

Copy link
Copy Markdown
Member

Have I got your idea correct on high level?

Yes, I think those should provide the same results.

Copy link
Copy Markdown
Member

We also want to avoid emitting GET_ITER twice. It is currently emitted in the scope surrounding the generator expression.
We should move it into the generator expression.

In other words, this function:

def f(s):
    return (x for x in s)

currently generates the following bytecode:

  1           RESUME                   0

  2           LOAD_CONST               1 (<code object <genexpr> at 0x7fc1c815e6a0, file "<python-input-0>", line 2>)
              MAKE_FUNCTION
              LOAD_FAST_TEMP           0 (s)
              GET_ITER
              CALL                     0
              RETURN_VALUE

Disassembly of <code object <genexpr> at 0x7fc1c815e6a0, file "<python-input-0>", line 2>:
   2           RETURN_GENERATOR
               POP_TOP
       L1:     RESUME                   0
               LOAD_FAST                0 (.0)
       L2:     FOR_ITER                 6 (to L3)
               ...

I think it should generate:

  1           RESUME                   0

  2           LOAD_CONST               1 (<code object <genexpr> at 0x7fc1c815e6a0, file "<python-input-0>", line 2>)
              MAKE_FUNCTION
              LOAD_FAST_TEMP           0 (s)
              CALL                     0
              RETURN_VALUE

Disassembly of <code object <genexpr> at 0x7fc1c815e6a0, file "<python-input-0>", line 2>:
   2           RETURN_GENERATOR
               POP_TOP
       L1:     RESUME                   0
               LOAD_FAST                0 (.0)
               GET_ITER
       L2:     FOR_ITER                 6 (to L3)
               ...

This is a behavior change, but I think a correct one. If f(2) is called, we get a TypeError, but this seems wrong. The error should occur once the generator expression is iterated over.

Copy link
Copy Markdown
Member Author

We also want to avoid emitting GET_ITER twice. It is currently emitted in the scope surrounding the generator expression. We should move it into the generator expression.

Agree.

This is a behavior change, but I think a correct one. If f(2) is called, we get a TypeError, but this seems wrong. The error should occur once the generator expression is iterated over.

Could you please give some additional information about this behavior change?
Some code snippets, which results will be changed.

Copy link
Copy Markdown
Member

Could you please give some additional information about this behavior change? Some code snippets, which results will be changed.

Currently, this happens:

>>> (x for x in 1)
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    (x for x in 1)
                ^
TypeError: 'int' object is not iterable

With Mark's proposed change, this error would be thrown only when the generator is iterated over.

I think either behavior is fine and I agree with Mark that the proposed change makes for a more elegant implementation, but it is a change in core language behavior that I could see impacting some users (e.g., if you put a try-except around the construction of the genexp), so I wouldn't feel comfortable making that change in a bugfix release.

@markshannon what do you think of applying this PR's approach in the 3.13 branch, and your idea on main only?

Copy link
Copy Markdown
Member Author

With Mark's proposed change, this error would be thrown only when the generator is iterated over.

Understood. Thx for the clarification.

applying this PR's approach in the 3.13 branch, and "GET_ITER changes" on main only?

It looks like a good idea for me, since crash through f_locals modification is already fixed.
And future improvements for GET_ITER bytecode moving could be provided separately.
But it's up to you to decide, of course.

There will be no problem in backports in the case of two separate PRs?

Copy link
Copy Markdown
Member

My proposed fix should have no performance impact (it does no extra work), so I'd definitely prefer that.

If you think that is too intrusive for 3.13, we could leave the GET_ITER in the enclosing scope and add a new CHECK_ITER instruction to be inserted before the FOR_ITER.
CHECK_ITER would raise if the value TOS is not an iterator, otherwise do nothing.

Copy link
Copy Markdown
Member Author

I like the idea of new CHECK_ITER bytecode.

What do you think about this plan:

  1. Add CHECK_ITER bytecode definition, put it in a proper place in generator expressions code, add correct reaction on it in the interpreter in this PR.
  2. Merge this PR to main, provide a backport to 3.13.
  3. Create a new PR.
  4. Move GET_ITER bytecode from the enclosing function to the place of CHECK_ITER bytecode, remove CHECK_ITER, merge PR to main, do not provide any backports.

And what about Python 3.12 and below?
Should we provide any bug fix to those branches?

Copy link
Copy Markdown
Member

That sounds like a good plan.

If the fix is simple enough, then we can backport to 3.12.
I don't think it is worth worrying about 3.11 and earlier.

efimov-mikhail marked this pull request as draft October 15, 2024 08:55
efimov-mikhail marked this pull request as ready for review October 15, 2024 15:16

Copy link
Copy Markdown
Member Author

As @JelleZijlstra points out, GET_ITER is idempotent, so we can drop CHECK_ITER and use GET_ITER instead.

For 3.14, we can then drop the GET_ITER in the caller.

It seems like this solution will be best tradeoff.
I will provide such changes to this PR.

ghost commented Oct 19, 2024 •
edited by ghost
Loading

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

efimov-mikhail force-pushed the crash_fix_generators_f_locals branch from c5d5bf4 to 890b936 Compare October 19, 2024 10:13

Copy link
Copy Markdown
Member

This might introduce a small slowdown in 3.13, but it isn't in the loop, so shouldn't be an issue.

I'm not worried about that though, as we can remove the other GET_ITER and thus the overhead in 3.14.

markshannon left a comment

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Looks good me once you've agreed on a better name for ModifyTest.

Copy link
Copy Markdown
Member Author

It's okay for me to change name from "ModifyTest" to something.
Maybe "ModifyBaseIteratorTest"?
Or "ModifyUnderlyingIterableTest"?

Copy link
Copy Markdown
Member

Either of those names works for me.

sobolevn left a comment

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thank you!

Copy link
Copy Markdown
Member Author

Looks good me once you've agreed on a better name for ModifyTest.

It seems we have agreed. Anything else, or it could be merged?

JelleZijlstra merged commit 079875e into python:main Oct 22, 2024

Copy link
Copy Markdown

Thanks @efimov-mikhail for the PR, and @JelleZijlstra for merging it 🌮🎉.. I'm working now to backport this PR to: 3.13.
🐍🍒⛏🤖

Copy link
Copy Markdown

Sorry, @efimov-mikhail and @JelleZijlstra, I could not cleanly backport this to 3.13 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker 079875e39589eb0628b5883f7ffa387e7476ec06 3.13

JelleZijlstra pushed a commit to JelleZijlstra/cpython that referenced this pull request Oct 22, 2024
…ipulations (pythonGH-125178)

(cherry picked from commit 079875e)

Co-authored-by: Mikhail Efimov <efimov.mikhail@gmail.com>

bedevere-app Bot commented Oct 22, 2024

Copy link
Copy Markdown

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

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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.

6 participants


Back | FazBrowse Home | New Git URL