python / python/cpython

Generated `__annotate__` uses `COMPARE_OP` without the bool-conversion flag, feeding a non-bool to `POP_JUMP_IF_FALSE`

Đang mở
#153,354 2 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

interpreter-core topic-typing type-crash
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_FALSE treats the non-Py_False value as truthy → __annotate__ spuriously raises NotImplementedError; 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

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. 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

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.