Generated `__annotate__` uses `COMPARE_OP` without the bool-conversion flag, feeding a non-bool to `POP_JUMP_IF_FALSE`
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 77.2k
- Fork
- 36k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
Crash report
What happened?
The compiler-generated __annotate__ function (PEP 649) begins with a guard if .format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError. That comparison is emitted as a COMPARE_OP without the "convert result to bool" flag, immediately followed by POP_JUMP_IF_FALSE — which requires a boolean operand. When __annotate__ is called with a format whose > comparison returns a non-bool, the raw value reaches the jump:
python: Python/generated_cases.c.h: _PyEval_EvalFrameDefault:
Assertion `PyStackRef_BoolCheck(cond)' failed.
Minimal reproducer (aborts on a debug build):
class NonBool:
def __gt__(self, other):
return None # any non-bool result
def f(a: int): # any annotated function/class/module has __annotate__
...
f.__annotate__(NonBool())
It reproduces via a module __annotate__, a function __annotate__, and a class __annotate_func__ alike (the codegen is shared). Original find was wsgiref.types.__annotate__(obj) where obj.__gt__ returns a non-bool.
Root cause
Python/codegen.c, codegen_setup_annotations_scope():
// if .format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
...
ADDOP_I(c, loc, COMPARE_OP, (Py_GT << 5) | compare_masks[Py_GT]); // no bool-cast bit (0x10)
NEW_JUMP_TARGET_LABEL(c, body);
ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, body);
The COMPARE_OP oparg omits bit 0x10, which the eval loop checks to convert the comparison result to a bool (if (oparg & 16) in _COMPARE_OP, Python/bytecodes.c). POP_JUMP_IF_FALSE asserts its operand is a bool. Normal comparisons emit COMPARE_OP without the bit and rely on the later TO_BOOL-folding optimizer pass to set it — see the comment in codegen_addcompare():
// ... The fifth-lowest bit indicates whether the result should be converted to
// bool and is set later):
ADDOP_I(c, loc, COMPARE_OP, (cmp << 5) | compare_masks[cmp]);
But this guard emits COMPARE_OP directly followed by POP_JUMP_IF_FALSE with no intervening TO_BOOL, so the optimizer has nothing to fold and the bit is never set.
Disassembling the same > comparison two ways makes the difference concrete:
if x > 2: -> COMPARE_OP 148 (bool(>)) # bit 0x10 SET
<generated __annotate__> -> COMPARE_OP 132 (>) # bit 0x10 CLEAR
With a normal integer format (annotationlib.Format.VALUE, ...), int > int returns a real bool, so the assertion holds and the bug is latent. A format whose rich comparison returns a non-bool sends the raw object to the jump.
Behavior by build
- Debug build:
assert(PyStackRef_BoolCheck(cond))fails → abort. - Release build: no assert, but
POP_JUMP_IF_FALSEtreats the non-Py_Falsevalue as truthy →__annotate__spuriously raisesNotImplementedError; and it consumes the operand without a decref (the jump assumes an immortal bool), leaking a heap comparison result.
More generally, the compiler is supposed to guarantee that every POP_JUMP_IF_FALSE/POP_JUMP_IF_TRUE operand is a bool; this codegen path breaks that guarantee for arbitrary format arguments.
Proposed fix
Set the bool-conversion bit on the guard's COMPARE_OP, matching what a normal if a > b: ends up with:
- ADDOP_I(c, loc, COMPARE_OP, (Py_GT << 5) | compare_masks[Py_GT]);
+ ADDOP_I(c, loc, COMPARE_OP, (Py_GT << 5) | compare_masks[Py_GT] | 16);
(Py_GT << 5) | compare_masks[Py_GT] | 16 == 148, identical to the oparg the optimizer produces for if a > b:. (Emitting a TO_BOOL after the COMPARE_OP would also work; maintainers may prefer a named constant over the literal 16.)
Found by the fusil fuzzer (@vstinner's fuzzer, revived by @devdanzin);
investigated and drafted by Claude Code (Opus 4.8).
CPython versions tested on:
CPython main branch, 3.16, 3.15, 3.14
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:bcf98ddbc40, Jul 4 2026, 15:04:52) [Clang 21.1.8 (6ubuntu1)]
Linked PRs
- gh-153360
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Đọc codegen_setup_annotations_scope trong Python/codegen.c và phần xử lý COMPARE_OP trong Python/bytecodes.c. Tái hiện ví dụ NonBool trên bản build debug, sau đó bổ sung kiểm thử hồi quy cho các helper chú thích của hàm, module và class. Hoàn tất khi reproducer không còn abort hoặc phát sinh exception ngoài dự kiến.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- c, python
- Lĩnh vực
- compilers
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Đình trệ
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 25/100