Can 1 + 1 be equal to 3 in python?

Viewed 477

Before carrying on any further, I am aware that one should never do this. This question is purely for educational purposes; I undertook this exercise as a means of better understanding python's internals, the ctypes, and how they work.

I am aware that it is relatively easy to change the value of integers in python. Actually, there's a whole lot you can do by messing with the internals. From the C API reference,

The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you actually just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behaviour of Python in this case is undefined. :-)

Considering the value of 1 is cached by CPython, it should be relatively easy (or, at least possible) to do this. After a little digging around, I found ctypes was the way to go. However, most of what I try results in a segfault. I got close by changing the value of 2.

import ctypes
def deref(addr, typ):
     return ctypes.cast(addr, ctypes.POINTER(typ))

deref(id(2), ctypes.c_int)[6] = 1

1 + 1 now gives incorrect results (a step in the right direction), but I cannot get it to evaluate to "3":

>>> 1 + 1
1

>>> 1 + 2
1

>>> 1 + 3
[1]    61014 segmentation fault  python3.6

I have tried similar things ending in failure with abarnert's internals module. Is there any way to have 1 + 1 evaluate to 3 in python? Or is "1" so all important that there is no way of making this work without segfaulting my interpreter?

1 Answers

Disclaimer: this answer refers to CPython only; I might have also missed the point of the question...

I was able to (kinda) achieve this by writing a Python extension in C.

In Objects/intobject.c there is an info struct PyInt_Type. Its tp_as_number field is a table of operator functions, the nb_add field of which is the addition operator:

// the function in the same file that nb_add points to
static PyObject *
int_add(PyIntObject *v, PyIntObject *w)
    ...

PyInt_Type is an exposed global variable, and can be retrieved with dlsym in Unix / GetProcAddress in WinAPI:

#include <dlfcn.h>

...

// symbol look-up from the Python extension
void* addr = dlsym(RTLD_DEFAULT, "PyInt_Type");

// pointer to PyInt_Type
PyTypeObject *int_type = addr;

// pointer to int_as_number (PyInt_Type.tp_as_number)
PyNumberMethods *int_funcs = int_type->tp_as_number;

// pointer to int_add (tp_as_number->nb_add)
int_add_orig = int_funcs->nb_add;

// override this with a custom function
int_funcs->nb_add = (binaryfunc)int_add_new;

...

// custom add function
PyObject *int_add_new(PyIntObject *v, PyIntObject *w)
{
    long a = PyInt_AS_LONG(v);
    long b = PyInt_AS_LONG(w);

    // 1 + 1 = 3 special case
    if (a == 1 && b == 1) {
        return PyInt_FromLong(3);
    }

    // for all other cases default to the
    // original add function which was retrieved earlier
    return int_add_orig((PyObject *)v, (PyObject *)w);
}

By preserving all of the original code and internal variables, the new code avoids the previously experienced segfaults:

>>> # load the extension

>>> import [...]

>>> 1 + 1
2

>>> # call the extension function which overloads the add operator

>>> 1 + 1
3

>>> 1 + 0
1

>>> 1 + 2
3

>>> 1 + 3
4
Related