Skip to content

Commit e586211

Browse files
authored
GH-104584: Fix ENTER_EXECUTOR (GH-106141)
* Check eval-breaker in ENTER_EXECUTOR. * Make sure that frame->prev_instr is set before entering executor.
1 parent 7f4c812 commit e586211

File tree

8 files changed

+272
-269
lines changed

8 files changed

+272
-269
lines changed

Include/internal/pycore_ceval.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ extern struct _PyInterpreterFrame* _PyEval_GetFrame(void);
152152

153153
extern PyObject* _Py_MakeCoro(PyFunctionObject *func);
154154

155+
/* Handle signals, pending calls, GIL drop request
156+
and asynchronous exception */
155157
extern int _Py_HandlePending(PyThreadState *tstate);
156158

157159

Python/bytecodes.c

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ dummy_func(
141141
ERROR_IF(err, error);
142142
next_instr--;
143143
}
144-
else if (_Py_atomic_load_relaxed_int32(&tstate->interp->ceval.eval_breaker) && oparg < 2) {
145-
goto handle_eval_breaker;
144+
else if (oparg < 2) {
145+
CHECK_EVAL_BREAKER();
146146
}
147147
}
148148

@@ -158,6 +158,9 @@ dummy_func(
158158
next_instr--;
159159
}
160160
else {
161+
if (oparg < 2) {
162+
CHECK_EVAL_BREAKER();
163+
}
161164
_PyFrame_SetStackPointer(frame, stack_pointer);
162165
int err = _Py_call_instrumentation(
163166
tstate, oparg > 0, frame, next_instr-1);
@@ -168,9 +171,6 @@ dummy_func(
168171
next_instr = frame->prev_instr;
169172
DISPATCH();
170173
}
171-
if (_Py_atomic_load_relaxed_int32(&tstate->interp->ceval.eval_breaker) && oparg < 2) {
172-
goto handle_eval_breaker;
173-
}
174174
}
175175
}
176176

@@ -2223,6 +2223,7 @@ dummy_func(
22232223
}
22242224

22252225
inst(JUMP_BACKWARD, (--)) {
2226+
CHECK_EVAL_BREAKER();
22262227
_Py_CODEUNIT *here = next_instr - 1;
22272228
assert(oparg <= INSTR_OFFSET());
22282229
JUMPBY(1-oparg);
@@ -2240,7 +2241,6 @@ dummy_func(
22402241
goto resume_frame;
22412242
}
22422243
#endif /* ENABLE_SPECIALIZATION */
2243-
CHECK_EVAL_BREAKER();
22442244
}
22452245

22462246
pseudo(JUMP) = {
@@ -2254,8 +2254,13 @@ dummy_func(
22542254
};
22552255

22562256
inst(ENTER_EXECUTOR, (--)) {
2257+
CHECK_EVAL_BREAKER();
2258+
22572259
PyCodeObject *code = _PyFrame_GetCode(frame);
22582260
_PyExecutorObject *executor = (_PyExecutorObject *)code->co_executors->executors[oparg&255];
2261+
int original_oparg = executor->vm_data.oparg | (oparg & 0xfffff00);
2262+
JUMPBY(1-original_oparg);
2263+
frame->prev_instr = next_instr - 1;
22592264
Py_INCREF(executor);
22602265
frame = executor->execute(executor, frame, stack_pointer);
22612266
if (frame == NULL) {
@@ -3570,8 +3575,8 @@ dummy_func(
35703575
}
35713576

35723577
inst(INSTRUMENTED_JUMP_BACKWARD, ( -- )) {
3573-
INSTRUMENTED_JUMP(next_instr-1, next_instr+1-oparg, PY_MONITORING_EVENT_JUMP);
35743578
CHECK_EVAL_BREAKER();
3579+
INSTRUMENTED_JUMP(next_instr-1, next_instr+1-oparg, PY_MONITORING_EVENT_JUMP);
35753580
}
35763581

35773582
inst(INSTRUMENTED_POP_JUMP_IF_TRUE, ( -- )) {

Python/ceval.c

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -763,68 +763,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
763763

764764
DISPATCH();
765765

766-
handle_eval_breaker:
767-
768-
/* Do periodic things, like check for signals and async I/0.
769-
* We need to do reasonably frequently, but not too frequently.
770-
* All loops should include a check of the eval breaker.
771-
* We also check on return from any builtin function.
772-
*
773-
* ## More Details ###
774-
*
775-
* The eval loop (this function) normally executes the instructions
776-
* of a code object sequentially. However, the runtime supports a
777-
* number of out-of-band execution scenarios that may pause that
778-
* sequential execution long enough to do that out-of-band work
779-
* in the current thread using the current PyThreadState.
780-
*
781-
* The scenarios include:
782-
*
783-
* - cyclic garbage collection
784-
* - GIL drop requests
785-
* - "async" exceptions
786-
* - "pending calls" (some only in the main thread)
787-
* - signal handling (only in the main thread)
788-
*
789-
* When the need for one of the above is detected, the eval loop
790-
* pauses long enough to handle the detected case. Then, if doing
791-
* so didn't trigger an exception, the eval loop resumes executing
792-
* the sequential instructions.
793-
*
794-
* To make this work, the eval loop periodically checks if any
795-
* of the above needs to happen. The individual checks can be
796-
* expensive if computed each time, so a while back we switched
797-
* to using pre-computed, per-interpreter variables for the checks,
798-
* and later consolidated that to a single "eval breaker" variable
799-
* (now a PyInterpreterState field).
800-
*
801-
* For the longest time, the eval breaker check would happen
802-
* frequently, every 5 or so times through the loop, regardless
803-
* of what instruction ran last or what would run next. Then, in
804-
* early 2021 (gh-18334, commit 4958f5d), we switched to checking
805-
* the eval breaker less frequently, by hard-coding the check to
806-
* specific places in the eval loop (e.g. certain instructions).
807-
* The intent then was to check after returning from calls
808-
* and on the back edges of loops.
809-
*
810-
* In addition to being more efficient, that approach keeps
811-
* the eval loop from running arbitrary code between instructions
812-
* that don't handle that well. (See gh-74174.)
813-
*
814-
* Currently, the eval breaker check happens here at the
815-
* "handle_eval_breaker" label. Some instructions come here
816-
* explicitly (goto) and some indirectly. Notably, the check
817-
* happens on back edges in the control flow graph, which
818-
* pretty much applies to all loops and most calls.
819-
* (See bytecodes.c for exact information.)
820-
*
821-
* One consequence of this approach is that it might not be obvious
822-
* how to force any specific thread to pick up the eval breaker,
823-
* or for any specific thread to not pick it up. Mostly this
824-
* involves judicious uses of locks and careful ordering of code,
825-
* while avoiding code that might trigger the eval breaker
826-
* until so desired.
827-
*/
828766
if (_Py_HandlePending(tstate) != 0) {
829767
goto error;
830768
}
@@ -2796,13 +2734,7 @@ _PyUopExecute(_PyExecutorObject *executor, _PyInterpreterFrame *frame, PyObject
27962734
PyThreadState *tstate = _PyThreadState_GET();
27972735
_PyUOpExecutorObject *self = (_PyUOpExecutorObject *)executor;
27982736

2799-
// Equivalent to CHECK_EVAL_BREAKER()
2800-
_Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY();
2801-
if (_Py_atomic_load_relaxed_int32(&tstate->interp->ceval.eval_breaker)) {
2802-
if (_Py_HandlePending(tstate) != 0) {
2803-
goto error;
2804-
}
2805-
}
2737+
CHECK_EVAL_BREAKER();
28062738

28072739
OBJECT_STAT_INC(optimization_traces_executed);
28082740
_Py_CODEUNIT *ip_offset = (_Py_CODEUNIT *)_PyFrame_GetCode(frame)->co_code_adaptive;

Python/ceval_gil.c

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,8 +1052,65 @@ _PyEval_FiniState(struct _ceval_state *ceval)
10521052
}
10531053
}
10541054

1055-
/* Handle signals, pending calls, GIL drop request
1056-
and asynchronous exception */
1055+
1056+
/* Do periodic things, like check for signals and async I/0.
1057+
* We need to do reasonably frequently, but not too frequently.
1058+
* All loops should include a check of the eval breaker.
1059+
* We also check on return from any builtin function.
1060+
*
1061+
* ## More Details ###
1062+
*
1063+
* The eval loop (this function) normally executes the instructions
1064+
* of a code object sequentially. However, the runtime supports a
1065+
* number of out-of-band execution scenarios that may pause that
1066+
* sequential execution long enough to do that out-of-band work
1067+
* in the current thread using the current PyThreadState.
1068+
*
1069+
* The scenarios include:
1070+
*
1071+
* - cyclic garbage collection
1072+
* - GIL drop requests
1073+
* - "async" exceptions
1074+
* - "pending calls" (some only in the main thread)
1075+
* - signal handling (only in the main thread)
1076+
*
1077+
* When the need for one of the above is detected, the eval loop
1078+
* pauses long enough to handle the detected case. Then, if doing
1079+
* so didn't trigger an exception, the eval loop resumes executing
1080+
* the sequential instructions.
1081+
*
1082+
* To make this work, the eval loop periodically checks if any
1083+
* of the above needs to happen. The individual checks can be
1084+
* expensive if computed each time, so a while back we switched
1085+
* to using pre-computed, per-interpreter variables for the checks,
1086+
* and later consolidated that to a single "eval breaker" variable
1087+
* (now a PyInterpreterState field).
1088+
*
1089+
* For the longest time, the eval breaker check would happen
1090+
* frequently, every 5 or so times through the loop, regardless
1091+
* of what instruction ran last or what would run next. Then, in
1092+
* early 2021 (gh-18334, commit 4958f5d), we switched to checking
1093+
* the eval breaker less frequently, by hard-coding the check to
1094+
* specific places in the eval loop (e.g. certain instructions).
1095+
* The intent then was to check after returning from calls
1096+
* and on the back edges of loops.
1097+
*
1098+
* In addition to being more efficient, that approach keeps
1099+
* the eval loop from running arbitrary code between instructions
1100+
* that don't handle that well. (See gh-74174.)
1101+
*
1102+
* Currently, the eval breaker check happens on back edges in
1103+
* the control flow graph, which pretty much applies to all loops,
1104+
* and most calls.
1105+
* (See bytecodes.c for exact information.)
1106+
*
1107+
* One consequence of this approach is that it might not be obvious
1108+
* how to force any specific thread to pick up the eval breaker,
1109+
* or for any specific thread to not pick it up. Mostly this
1110+
* involves judicious uses of locks and careful ordering of code,
1111+
* while avoiding code that might trigger the eval breaker
1112+
* until so desired.
1113+
*/
10571114
int
10581115
_Py_HandlePending(PyThreadState *tstate)
10591116
{

Python/ceval_macros.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@
117117
#define CHECK_EVAL_BREAKER() \
118118
_Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY(); \
119119
if (_Py_atomic_load_relaxed_int32(&tstate->interp->ceval.eval_breaker)) { \
120-
goto handle_eval_breaker; \
120+
if (_Py_HandlePending(tstate) != 0) { \
121+
goto error; \
122+
} \
121123
}
122124

123125

0 commit comments

Comments
 (0)