Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Type confusion in float_richcompare via re-entrant __neg__ #143006

Description

@jackfromeast

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:

Details
static 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13bugs and security fixes3.14bugs and security fixes3.15pre-release feature fixes, bugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or errortype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions