python / python/cpython

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

Abierto
#153,354 2 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

interpreter-core topic-typing type-crash
Lenguaje dominante
Python
Estrellas
77.2k
Forks
35.9k
Métricas de merge de PR
Métricas de PR pendientes

Descripción

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

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Lee codegen_setup_annotations_scope en Python/codegen.c y el manejo de COMPARE_OP en Python/bytecodes.c. Reproduce el ejemplo NonBool en una compilación de depuración y añade después cobertura de regresión para los helpers de anotaciones de funciones, módulos y clases. Hecho cuando el reproductor ya no aborta ni genera excepciones de forma espuria.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
c, python
Área
compilers
Tipo de issue
Error
Dificultad
3/5
Tiempo estimado
1-2 días
Estado de actividad
Estancado
Claridad
Bien especificado
Aptitud para principiantes
25/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.