What happened?
Implementing __neg__ to return a non-integer leaves float_richcompare holding an arbitrary object after PyNumber_Negative, yet it still feeds that value into _PyLong_Lshift, triggering the debug assertion PyLong_Check(a) and risking type confusion on release builds during mixed float-int comparisons.
Proof of Concept:
class EvilInt(int): def __neg__(self): return "" victim = EvilInt(-(1 << 50)) probe = -float(1 << 50) - 0.5 probe < victim
Affected Versions:
Details| Python Version | Status | Exit Code |
|---|---|---|
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 13:30:44) |
ASSERT | -6 |
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 13:31:21) [Clang 18.1.3 (1ubuntu1)] |
ASSERT | -6 |
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 13:32:07) [Clang 18.1.3 (1ubuntu1)] |
ASSERT | -6 |
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 13:33:03) [Clang 18.1.3 (1ubuntu1)] |
ASSERT | -6 |
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 13:33:58) [Clang 18.1.3 (1ubuntu1)] |
ASSERT | -6 |
Python 3.14.0+ (heads/3.14:2e216728038, Oct 28 2025, 13:34:48) [Clang 18.1.3 (1ubuntu1)] |
ASSERT | -6 |
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 13:35:40) [Clang 18.1.3 (1ubuntu1)] |
ASSERT | -6 |
Vulnerable Code:
Detailsstatic PyObject* float_richcompare(PyObject *v, PyObject *w, int op) { /* ... */ double fracpart; double intpart; PyObject *result = NULL; PyObject *vv = NULL; PyObject *ww = w; if (wsign < 0) { // Trigger the __neg__ on the int object whose return value set to ww ww = PyNumber_Negative(w); if (ww == NULL) goto Error; } else Py_INCREF(ww); fracpart = modf(i, &intpart); vv = PyLong_FromDouble(intpart); if (vv == NULL) goto Error; if (fracpart != 0.0) { /* Shift left, and or a 1 bit into vv * to represent the lost fraction. */ PyObject *temp; // Type Confusion: ww has been casted to PyLong with only assertion check (doesn't work on release version) temp = _PyLong_Lshift(ww, 1); Py_SETREF(ww, temp); /* ... */ r = PyObject_RichCompareBool(vv, ww, op); PyObject * _PyLong_Lshift(PyObject *a, int64_t shiftby) { return long_lshift_int64(_PyLong_CAST(a), shiftby); }
Sanitizer Output:
python: Objects/longobject.c:5173: PyObject *_PyLong_Lshift(PyObject *, size_t): Assertion `PyLong_Check(a)' failed.
Aborted (core dumped)
Linked PRs
- gh-143006: add type check in float_richcompare() #143053
- gh-143006: Fix and optimize mixed comparison of float and int #143084
- [3.14] gh-143006: Fix and optimize mixed comparison of float and int (GH-143084) #143623
- [3.13] gh-143006: Fix and optimize mixed comparison of float and int (GH-143084) #143624