https://github.com/python/cpython/commit/0906d2ab93fc9f4c7e0db031c965e7d8a0db7d37
commit: 0906d2ab93fc9f4c7e0db031c965e7d8a0db7d37
branch: main
author: John <[email protected]>
committer: markshannon <[email protected]>
date: 2026-10-01T12:15:51+01:00
summary:
gh-157468: Validate correct builtins used under JIT (GH-157766)
* Expose two possible builtin JIT gaps
* Add trace and runtime guard for builtin dict
* Check _GUARD_BUILTINS_IS_CANONICAL once per frame
* Add check and fix for globals being folded
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-20-24-43.gh-issue-157468.zVIAva.rst
M Include/internal/pycore_optimizer_types.h
M Include/internal/pycore_uop_ids.h
M Include/internal/pycore_uop_metadata.h
M Lib/test/test_capi/test_opt.py
M Objects/dictobject.c
M Python/bytecodes.c
M Python/executor_cases.c.h
M Python/optimizer_bytecodes.c
M Python/optimizer_cases.c.h
M Python/optimizer_symbols.c
diff --git a/Include/internal/pycore_optimizer_types.h
b/Include/internal/pycore_optimizer_types.h
index a722652cc8163a..c1928ec9ebdac3 100644
--- a/Include/internal/pycore_optimizer_types.h
+++ b/Include/internal/pycore_optimizer_types.h
@@ -140,6 +140,7 @@ typedef union {
typedef struct _Py_UOpsAbstractFrame {
bool globals_watched;
+ bool builtins_checked;
// The version number of the globals dicts, once checked. 0 if unchecked.
uint32_t globals_checked_version;
// Max stacklen
diff --git a/Include/internal/pycore_uop_ids.h
b/Include/internal/pycore_uop_ids.h
index 840a32f0e4481f..ba6e0252024a6d 100644
--- a/Include/internal/pycore_uop_ids.h
+++ b/Include/internal/pycore_uop_ids.h
@@ -142,6 +142,7 @@ enum {
_GUARD_BIT_IS_UNSET_POP_5,
_GUARD_BIT_IS_UNSET_POP_6,
_GUARD_BIT_IS_UNSET_POP_7,
+ _GUARD_BUILTINS_IS_CANONICAL,
_GUARD_CALLABLE_BUILTIN_CLASS,
_GUARD_CALLABLE_BUILTIN_FAST,
_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS,
@@ -365,7 +366,7 @@ enum {
_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE,
_YIELD_VALUE,
};
-#define MAX_UOP_ID 653
+#define MAX_UOP_ID 654
#define _BUILD_INTERPOLATION BUILD_INTERPOLATION
#define _BUILD_LIST BUILD_LIST
@@ -750,6 +751,10 @@ enum {
_GUARD_BIT_IS_UNSET_POP_7_r10,
_GUARD_BIT_IS_UNSET_POP_7_r21,
_GUARD_BIT_IS_UNSET_POP_7_r32,
+ _GUARD_BUILTINS_IS_CANONICAL_r00,
+ _GUARD_BUILTINS_IS_CANONICAL_r11,
+ _GUARD_BUILTINS_IS_CANONICAL_r22,
+ _GUARD_BUILTINS_IS_CANONICAL_r33,
_GUARD_CALLABLE_BUILTIN_CLASS_r00,
_GUARD_CALLABLE_BUILTIN_FAST_r00,
_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS_r00,
@@ -1439,7 +1444,7 @@ enum {
_WITH_EXCEPT_START_r33,
_YIELD_VALUE_r11,
};
-#define MAX_UOP_REGS_ID 1650
+#define MAX_UOP_REGS_ID 1655
#ifdef __cplusplus
}
diff --git a/Include/internal/pycore_uop_metadata.h
b/Include/internal/pycore_uop_metadata.h
index 1c440fe9f1e978..177564a7545941 100644
--- a/Include/internal/pycore_uop_metadata.h
+++ b/Include/internal/pycore_uop_metadata.h
@@ -196,6 +196,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = {
[_GUARD_GLOBALS_VERSION] = HAS_DEOPT_FLAG,
[_LOAD_GLOBAL_MODULE] = HAS_DEOPT_FLAG,
[_LOAD_GLOBAL_BUILTINS] = HAS_DEOPT_FLAG,
+ [_GUARD_BUILTINS_IS_CANONICAL] = HAS_DEOPT_FLAG,
[_DELETE_FAST] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ERROR_FLAG |
HAS_ESCAPES_FLAG,
[_MAKE_CELL] = HAS_ARG_FLAG | HAS_FREE_FLAG | HAS_ERROR_FLAG |
HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG,
[_DELETE_DEREF] = HAS_ARG_FLAG | HAS_FREE_FLAG | HAS_ERROR_FLAG |
HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG,
@@ -1894,6 +1895,15 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
{ -1, -1, -1 },
},
},
+ [_GUARD_BUILTINS_IS_CANONICAL] = {
+ .best = { 0, 1, 2, 3 },
+ .entries = {
+ { 0, 0, _GUARD_BUILTINS_IS_CANONICAL_r00 },
+ { 1, 1, _GUARD_BUILTINS_IS_CANONICAL_r11 },
+ { 2, 2, _GUARD_BUILTINS_IS_CANONICAL_r22 },
+ { 3, 3, _GUARD_BUILTINS_IS_CANONICAL_r33 },
+ },
+ },
[_DELETE_FAST] = {
.best = { 0, 0, 0, 0 },
.entries = {
@@ -4315,6 +4325,10 @@ const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = {
[_GUARD_GLOBALS_VERSION_r33] = _GUARD_GLOBALS_VERSION,
[_LOAD_GLOBAL_MODULE_r01] = _LOAD_GLOBAL_MODULE,
[_LOAD_GLOBAL_BUILTINS_r01] = _LOAD_GLOBAL_BUILTINS,
+ [_GUARD_BUILTINS_IS_CANONICAL_r00] = _GUARD_BUILTINS_IS_CANONICAL,
+ [_GUARD_BUILTINS_IS_CANONICAL_r11] = _GUARD_BUILTINS_IS_CANONICAL,
+ [_GUARD_BUILTINS_IS_CANONICAL_r22] = _GUARD_BUILTINS_IS_CANONICAL,
+ [_GUARD_BUILTINS_IS_CANONICAL_r33] = _GUARD_BUILTINS_IS_CANONICAL,
[_DELETE_FAST_r00] = _DELETE_FAST,
[_MAKE_CELL_r00] = _MAKE_CELL,
[_DELETE_DEREF_r00] = _DELETE_DEREF,
@@ -5292,6 +5306,11 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1]
= {
[_GUARD_BIT_IS_UNSET_POP_7_r10] = "_GUARD_BIT_IS_UNSET_POP_7_r10",
[_GUARD_BIT_IS_UNSET_POP_7_r21] = "_GUARD_BIT_IS_UNSET_POP_7_r21",
[_GUARD_BIT_IS_UNSET_POP_7_r32] = "_GUARD_BIT_IS_UNSET_POP_7_r32",
+ [_GUARD_BUILTINS_IS_CANONICAL] = "_GUARD_BUILTINS_IS_CANONICAL",
+ [_GUARD_BUILTINS_IS_CANONICAL_r00] = "_GUARD_BUILTINS_IS_CANONICAL_r00",
+ [_GUARD_BUILTINS_IS_CANONICAL_r11] = "_GUARD_BUILTINS_IS_CANONICAL_r11",
+ [_GUARD_BUILTINS_IS_CANONICAL_r22] = "_GUARD_BUILTINS_IS_CANONICAL_r22",
+ [_GUARD_BUILTINS_IS_CANONICAL_r33] = "_GUARD_BUILTINS_IS_CANONICAL_r33",
[_GUARD_CALLABLE_BUILTIN_CLASS] = "_GUARD_CALLABLE_BUILTIN_CLASS",
[_GUARD_CALLABLE_BUILTIN_CLASS_r00] = "_GUARD_CALLABLE_BUILTIN_CLASS_r00",
[_GUARD_CALLABLE_BUILTIN_FAST] = "_GUARD_CALLABLE_BUILTIN_FAST",
@@ -6488,6 +6507,8 @@ int _PyUop_num_popped(int opcode, int oparg)
return 0;
case _LOAD_GLOBAL_BUILTINS:
return 0;
+ case _GUARD_BUILTINS_IS_CANONICAL:
+ return 0;
case _DELETE_FAST:
return 0;
case _MAKE_CELL:
diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py
index 64b6e82936d2f1..88077aacecbf5b 100644
--- a/Lib/test/test_capi/test_opt.py
+++ b/Lib/test/test_capi/test_opt.py
@@ -1,3 +1,4 @@
+import builtins
import contextlib
import dis
import itertools
@@ -5276,6 +5277,119 @@ def jitted(funcs):
with self.assertRaises(NameError):
jitted([f, f_with_bad_globals])
+ def test_jitted_code_sees_changed_copied_builtins(self):
+ # Trace-time check. The traced function's builtins is a copy of the
+ # canonical dict with the same keys version, so a version check
+ # cannot tell them apart. The optimizer must see that func_builtins
+ # is not interp->builtins and keep _LOAD_GLOBAL_BUILTINS, which reads
+ # the frame's own dict, rather than fold a constant from the
+ # canonical one. No runtime guard is involved.
+
+ def f(n):
+ return [len("hello") for _ in range(n)]
+
+ copied_builtins = vars(builtins).copy()
+ f = types.FunctionType(f.__code__, {"__builtins__": copied_builtins})
+
+ f(TIER2_THRESHOLD)
+ ex = get_first_executor(f)
+ self.assertIsNotNone(ex)
+ # Not folded: the load must still consult the frame's builtins.
+ self.assertIn("_LOAD_GLOBAL_BUILTINS", get_opnames(ex))
+
+ # Replacing an existing value does not change the keys version.
+ copied_builtins["len"] = lambda s: 42
+ self.assertEqual(f(8), [42] * 8)
+
+ def test_jitted_code_sees_changed_copied_globals(self):
+ # Copying a dict must not carry over the keys version of the source.
+ # The optimizer folds a global to a constant guarded only by the
+ # globals keys version plus a watcher on the traced dict. A copy is
+ # not watched, and replacing an existing value does not change the
+ # keys version, so a function whose globals are a copy of the traced
+ # dict would otherwise pass _GUARD_GLOBALS_VERSION and see the stale
+ # constant.
+ def f(n):
+ for _ in range(n):
+ x = COPIED_GLOBAL
+ return x
+
+ for copy in (dict.copy, dict):
+ with self.subTest(copy=copy):
+ original = {"COPIED_GLOBAL": 1}
+ # A fresh code object, so that each subtest traces anew.
+ f_original = types.FunctionType(f.__code__.replace(), original)
+ self.assertEqual(f_original(TIER2_THRESHOLD), 1)
+ ex = get_first_executor(f_original)
+ self.assertIsNotNone(ex)
+ uops = get_opnames(ex)
+ self.assertIn("_GUARD_GLOBALS_VERSION", uops)
+ # The global was folded to a constant.
+ self.assertNotIn("_LOAD_GLOBAL_MODULE", uops)
+
+ copied = copy(original)
+ copied["COPIED_GLOBAL"] = 2
+ # Share the code object, so that the same executor is entered.
+ f_copied = types.FunctionType(f_original.__code__, copied)
+ self.assertEqual(f_copied(TIER2_THRESHOLD), 2)
+ self.assertEqual(f_original(TIER2_THRESHOLD), 1)
+
+ def test_jitted_code_sees_different_builtins(self):
+ # Runtime check. The traced function's builtins IS the canonical
+ # dict, so folding len to a constant is correct at trace time.
+ # A second function sharing the code object then enters the same
+ # executor with other builtins, so only the runtime guard on the
+ # executing frame's builtins can catch it.
+ def f(n):
+ return [len("hello") for _ in range(n)]
+
+ namespace = {"__builtins__": builtins}
+ f_canonical = types.FunctionType(f.__code__, namespace)
+ copied_builtins = vars(builtins).copy()
+ namespace["__builtins__"] = copied_builtins
+ f_copied = types.FunctionType(f.__code__, namespace)
+
+
+ f_canonical(TIER2_THRESHOLD)
+ ex = get_first_executor(f_canonical)
+ self.assertIsNotNone(ex)
+ self.assertIn("_GUARD_BUILTINS_IS_CANONICAL", get_opnames(ex))
+
+ copied_builtins["len"] = lambda s: 42
+ # The executor's owner still sees the canonical len.
+ self.assertEqual(f_canonical(8), [5] * 8)
+ # A different function enters the same executor with other builtins.
+ self.assertEqual(f_copied(8), [42] * 8)
+
+ def test_builtins_guard_emitted_once_per_frame(self):
+ # A frame's builtins cannot change once the frame is pushed, so
+ # repeated builtin loads in one frame share a single guard, just as
+ # they already share a single _GUARD_GLOBALS_VERSION.
+
+ def warmup(n):
+ x = 0
+ for _ in range(n):
+ x += len("ab")
+ return x
+
+ def one_frame(n):
+ x = 0
+ for _ in range(n):
+ x += len("ab") + abs(-1) + ord("c")
+ return x
+
+ # The optimizer context is reused for every compilation, so compile an
+ # unrelated trace first: state that is not reset per frame leaks here.
+ warmup(TIER2_THRESHOLD)
+ self.assertIsNotNone(get_first_executor(warmup))
+
+ _, ex = self._run_with_optimizer(one_frame, TIER2_THRESHOLD)
+ self.assertIsNotNone(ex)
+ uop_names = get_opnames(ex)
+ self.assertNotIn("_LOAD_GLOBAL_BUILTINS", uop_names) # all folded
+ self.assertEqual(uop_names.count("_GUARD_BUILTINS_IS_CANONICAL"), 1)
+ self.assertEqual(uop_names.count("_GUARD_GLOBALS_VERSION"), 1)
+
def test_reference_tracking_across_call_doesnt_crash(self):
def f1():
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-20-24-43.gh-issue-157468.zVIAva.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-20-24-43.gh-issue-157468.zVIAva.rst
new file mode 100644
index 00000000000000..bec475ae711423
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-20-24-43.gh-issue-157468.zVIAva.rst
@@ -0,0 +1,7 @@
+Fix the JIT optimizer folding a builtin name to a constant taken from the
+interpreter's builtins dictionary even when the running function uses a
+different ``__builtins__`` mapping, such as one created with
+``vars(builtins).copy()``. The optimizer now only folds when the function's
+builtins is the interpreter's, and the folded constant is guarded at runtime so
+that another function sharing the same code object but a different builtins
+mapping does not use it.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index 15377ac083c2b2..49eb8481e128d0 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -1041,6 +1041,10 @@ clone_combined_dict_keys(PyDictObject *orig)
memcpy(keys, orig->ma_keys, keys_size);
+ /* The keys version must be unique per keys object: the specializer
+ and the JIT optimizer rely on it to identify a dict's keys. */
+ keys->dk_version = 0;
+
/* After copying key/value pairs, we need to incref all
keys and values and they are about to be co-owned by a
new dict object. */
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 8dfb76ef7884d1..3f329274f06b11 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -2356,6 +2356,10 @@ dummy_func(
STAT_INC(LOAD_GLOBAL, hit);
}
+ tier2 op(_GUARD_BUILTINS_IS_CANONICAL, (--)) {
+ DEOPT_IF(BUILTINS() != tstate->interp->builtins);
+ }
+
macro(LOAD_GLOBAL_MODULE) =
unused/1 + // Skip over the counter
NOP + // For guard insertion in the JIT optimizer
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index b60e9b2b52daa8..400f07b58e7fa5 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -10482,6 +10482,76 @@
break;
}
+ case _GUARD_BUILTINS_IS_CANONICAL_r00: {
+ CHECK_CURRENT_CACHED_VALUES(0);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ if (BUILTINS() != tstate->interp->builtins) {
+ UOP_STAT_INC(uopcode, miss);
+ SET_CURRENT_CACHED_VALUES(0);
+ JUMP_TO_JUMP_TARGET();
+ }
+ SET_CURRENT_CACHED_VALUES(0);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ break;
+ }
+
+ case _GUARD_BUILTINS_IS_CANONICAL_r11: {
+ CHECK_CURRENT_CACHED_VALUES(1);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ _PyStackRef _stack_item_0 = _tos_cache0;
+ if (BUILTINS() != tstate->interp->builtins) {
+ UOP_STAT_INC(uopcode, miss);
+ _tos_cache0 = _stack_item_0;
+ SET_CURRENT_CACHED_VALUES(1);
+ JUMP_TO_JUMP_TARGET();
+ }
+ _tos_cache0 = _stack_item_0;
+ SET_CURRENT_CACHED_VALUES(1);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ break;
+ }
+
+ case _GUARD_BUILTINS_IS_CANONICAL_r22: {
+ CHECK_CURRENT_CACHED_VALUES(2);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ _PyStackRef _stack_item_0 = _tos_cache0;
+ _PyStackRef _stack_item_1 = _tos_cache1;
+ if (BUILTINS() != tstate->interp->builtins) {
+ UOP_STAT_INC(uopcode, miss);
+ _tos_cache1 = _stack_item_1;
+ _tos_cache0 = _stack_item_0;
+ SET_CURRENT_CACHED_VALUES(2);
+ JUMP_TO_JUMP_TARGET();
+ }
+ _tos_cache1 = _stack_item_1;
+ _tos_cache0 = _stack_item_0;
+ SET_CURRENT_CACHED_VALUES(2);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ break;
+ }
+
+ case _GUARD_BUILTINS_IS_CANONICAL_r33: {
+ CHECK_CURRENT_CACHED_VALUES(3);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ _PyStackRef _stack_item_0 = _tos_cache0;
+ _PyStackRef _stack_item_1 = _tos_cache1;
+ _PyStackRef _stack_item_2 = _tos_cache2;
+ if (BUILTINS() != tstate->interp->builtins) {
+ UOP_STAT_INC(uopcode, miss);
+ _tos_cache2 = _stack_item_2;
+ _tos_cache1 = _stack_item_1;
+ _tos_cache0 = _stack_item_0;
+ SET_CURRENT_CACHED_VALUES(3);
+ JUMP_TO_JUMP_TARGET();
+ }
+ _tos_cache2 = _stack_item_2;
+ _tos_cache1 = _stack_item_1;
+ _tos_cache0 = _stack_item_0;
+ SET_CURRENT_CACHED_VALUES(3);
+ ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
+ break;
+ }
+
case _DELETE_FAST_r00: {
CHECK_CURRENT_CACHED_VALUES(0);
ASSERT_WITHIN_STACK_BOUNDS_IGNORING_CACHE(__FILE__, __LINE__);
diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c
index ca4ce4f733ea34..6284885c428832 100644
--- a/Python/optimizer_bytecodes.c
+++ b/Python/optimizer_bytecodes.c
@@ -2531,13 +2531,24 @@ dummy_func(void) {
else if (interp->rare_events.builtin_dict >=
_Py_MAX_ALLOWED_BUILTINS_MODIFICATIONS) {
/* Do nothing */
}
+ else if (ctx->frame->func == NULL ||
+ ctx->frame->func->func_builtins != builtins) {
+ /* Do nothing */
+ }
else {
if (!ctx->builtins_watched) {
PyDict_Watch(BUILTINS_WATCHER_ID, builtins);
ctx->builtins_watched = true;
}
- if (ctx->frame->globals_checked_version != 0 &&
ctx->frame->globals_watched) {
+ if (ctx->frame->globals_checked_version != 0 &&
+ ctx->frame->globals_watched)
+ {
cnst = convert_global_to_const(this_instr, builtins);
+ if (cnst != NULL && !ctx->frame->builtins_checked) {
+ ctx->frame->builtins_checked = true;
+ ADD_OP(_GUARD_BUILTINS_IS_CANONICAL, 0, 0);
+ ADD_OP(this_instr->opcode, 0, (uintptr_t)cnst);
+ }
}
}
if (cnst == NULL) {
diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h
index 57a9539016bbb2..bd3ed9ce99a555 100644
--- a/Python/optimizer_cases.c.h
+++ b/Python/optimizer_cases.c.h
@@ -2301,13 +2301,23 @@
}
else if (interp->rare_events.builtin_dict >=
_Py_MAX_ALLOWED_BUILTINS_MODIFICATIONS) {
}
+ else if (ctx->frame->func == NULL ||
+ ctx->frame->func->func_builtins != builtins) {
+ }
else {
if (!ctx->builtins_watched) {
PyDict_Watch(BUILTINS_WATCHER_ID, builtins);
ctx->builtins_watched = true;
}
- if (ctx->frame->globals_checked_version != 0 &&
ctx->frame->globals_watched) {
+ if (ctx->frame->globals_checked_version != 0 &&
+ ctx->frame->globals_watched)
+ {
cnst = convert_global_to_const(this_instr, builtins);
+ if (cnst != NULL && !ctx->frame->builtins_checked) {
+ ctx->frame->builtins_checked = true;
+ ADD_OP(_GUARD_BUILTINS_IS_CANONICAL, 0, 0);
+ ADD_OP(this_instr->opcode, 0, (uintptr_t)cnst);
+ }
}
}
if (cnst == NULL) {
@@ -2328,6 +2338,10 @@
break;
}
+ case _GUARD_BUILTINS_IS_CANONICAL: {
+ break;
+ }
+
case _DELETE_FAST: {
break;
}
diff --git a/Python/optimizer_symbols.c b/Python/optimizer_symbols.c
index 3147e9c5065edd..eaa4c8dbbdc5de 100644
--- a/Python/optimizer_symbols.c
+++ b/Python/optimizer_symbols.c
@@ -1378,6 +1378,7 @@ _Py_uop_frame_new(
frame->stack_pointer = frame->stack;
frame->globals_checked_version = 0;
frame->globals_watched = false;
+ frame->builtins_checked = false;
frame->func = NULL;
frame->caller = false;
frame->is_c_recursion_checked = false;
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]