Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Macro Py_CLEAR references argument two times. #98724

Open
hochl opened this issue Oct 26, 2022 · 16 comments
Open

Macro Py_CLEAR references argument two times. #98724

hochl opened this issue Oct 26, 2022 · 16 comments
Labels
interpreter-core Interpreter core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@hochl
Copy link

hochl commented Oct 26, 2022

Bug report

The macro Py_CLEAR(op) references the argument op two times. If the macro is called with an expression it will be evaluated two times, for example Py_CLEAR(p++).

Your environment

x86_64

  • CPython versions tested on:
    Python 3.7m
  • Operating system and architecture:
    Debian Stable

I suggest a fix similar to this (old version commented out with #if 0):

#if 0
#define Py_CLEAR(op)                            \
    do {                                        \
        PyObject *_py_tmp = (PyObject *)(op);   \
        if (_py_tmp != NULL) {                  \
            (op) = NULL;                        \
            Py_DECREF(_py_tmp);                 \
        }                                       \
    } while (0)
#else
#define Py_CLEAR(op)                                         \
    do {                                                     \
        PyObject **_py_tmp = (PyObject **)&(op);             \
        if (*_py_tmp != NULL) {                              \
            PyObject *_py_tmp2 = *_py_tmp;                   \
            (*_py_tmp) = NULL;                               \
            Py_DECREF(_py_tmp2);                             \
        }                                                    \
    } while (0)
#endif

I am not sure if this has happened anywhere, but I see a possible problem here. I think the compiler will optimize out the additional temporary variable in most cases.

@hochl hochl added the type-bug An unexpected behavior, bug, or error label Oct 26, 2022
@sobolevn
Copy link
Member

sobolevn commented Oct 26, 2022

Here's an example of an expression in PyClear:

static void
reap_obj(pylist fd2obj[FD_SETSIZE + 1])
{
unsigned int i;
for (i = 0; i < (unsigned int)FD_SETSIZE + 1 && fd2obj[i].sentinel >= 0; i++) {
Py_CLEAR(fd2obj[i].obj);
}
fd2obj[0].sentinel = -1;
}

We also have:

static int
template_clear(TemplateObject *self)
{
    Py_CLEAR(self->literal);
    for (Py_ssize_t i = 0, n = Py_SIZE(self); i < n; i++) {
        Py_CLEAR(self->items[i].literal);
    }
    return 0;
}

And

        for (Py_ssize_t i = 0; i < keys->dk_nentries; i++) {
            Py_CLEAR(values->values[i]);
        }

All cases are element access, but this is the most complex examples I am able to find.

@sobolevn sobolevn added the interpreter-core Interpreter core (Objects, Python, Grammar, and Parser dirs) label Oct 26, 2022
@hochl
Copy link
Author

hochl commented Oct 26, 2022

I am not sure that I made the point totally clear. Here is an example program for clarification, using some dummy type for PyObject.

#include <stdlib.h>

#define _PyObject_CAST(op) ((PyObject*)(op))
#define Py_CLEAR(op)                            \
    do {                                        \
        PyObject *_py_tmp = _PyObject_CAST(op); \
        if (_py_tmp != NULL) {                  \
            (op) = NULL;                        \
            /* Py_DECREF(_py_tmp); */           \
            free((void*)_py_tmp);               \
        }                                       \
    } while (0)

typedef struct {
} PyObject;

int main()
{
    PyObject* obj[16];
    PyObject** p = obj;
    size_t i;

    for (i = 0; i < 16; i++) {
        obj[i] = malloc(sizeof(PyObject));
    }

#if 1
    for (int i = 0; i < 16; i++) {
        Py_CLEAR(*p++);
    }
#else
    for (int i = 0; i < 16; i++, p++) {
        Py_CLEAR(*p);
    }
#endif
}

This code will give a buffer overflow. Changing the last line to for (int i = 0; i < 16; i++, p++) Py_CLEAR(*p); works without problems. The side effect is not triggered by Python library code and most probably not in any sane C project, but it is not unconceivable that this macro shoots someone out there into the foot. I suggest that, if you have the choice, to somehow make all macros evaluate their arguments just once.

@ericvsmith
Copy link
Member

ericvsmith commented Oct 26, 2022

I'm not sure we've ever promised that macros won't evaluate their args more than once. @vstinner ?

@vstinner
Copy link
Member

vstinner commented Oct 31, 2022

"Duplication of side effects" is one catch of macros that PEP 670 tries to avoid: https://peps.python.org/pep-0670/#rationale

Sadly, Py_CLEAR() cannot be converted to a function (as part of PEP 670) since it magically gets a reference to a pointer thanks to magic macro preprocessor. An hypothetical Py_Clear() function would take a pointer to a Python object, so PyObject** type, like: Py_Clear(&variable);.

IMO using &argument in the macro implementation is an acceptable fix to prevent the duplication of side effects.

I think the compiler will optimize out the additional temporary variable in most cases.

I agree with you. Moreover, correctness matters more than performance for Py_CLEAR() API.

cc @serhiy-storchaka @erlend-aasland

@vstinner
Copy link
Member

vstinner commented Oct 31, 2022

@serhiy-storchaka
Copy link
Member

serhiy-storchaka commented Oct 31, 2022

I am against extending ABI without need. Py_CLEAR is a part of API, but not in ABI. Incref/decref is anough, Py_CLEAR is just an API sugar.

Calling Py_Clear(&variable) can prevent some compiler optimizations. The compiler can no longer use a register for variable in a register, and it can no longer assume variable is now NULL, and it cannot guarantee the the value of local variable will not change outside of the local code. I faced a similar situation recently. And the problem was not only that the compiler generated less efficient code, but that it started issuing warnings about correct code that it could no longer fully analyze.

@hochl
Copy link
Author

hochl commented Nov 4, 2022

How about this macro?

#define Py_CLEAR(op)                              \
    do {                                          \
        PyObject **_py_tmp = (PyObject**)&(op);   \
        if (*_py_tmp != NULL) {                   \
            PyObject* _py_tmp2 = *_py_tmp;        \
            *_py_tmp = NULL;                      \
            /* Py_DECREF(_py_tmp2); */            \
            free((void*)_py_tmp2);                \
        }                                         \
    } while (0)

Replace the free line with the commented out line for real Python, this is for my earlier example program.

vstinner added a commit to vstinner/cpython that referenced this issue Nov 4, 2022
Fix the :c:macro:`Py_CLEAR` macro to avoid the duplicate of side
effects. Previously, if the argument had side effects, these side
effects were duplicated.

Add _testcapi.test_py_clear() unit test.
@vstinner
Copy link
Member

vstinner commented Nov 4, 2022

The macro Py_CLEAR(op) references the argument op two times. If the macro is called with an expression it will be evaluated two times, for example Py_CLEAR(p++).

This issue looks an hypothetical bug, but I'm not convinced that it's possible to write an expression which a side effect and which makes sense to set to NULL. For example, in your example, what's the point of writing p++ = NULL; (through the macro).

To be clear, GCC fails to build the following C code:

int main()
{
    void *ptr = 0;
    ptr++ = 0;
    return 0;
}

vstinner added a commit to vstinner/cpython that referenced this issue Nov 4, 2022
Fix the :c:macro:`Py_CLEAR` macro to avoid the duplicate of side
effects. Previously, if the argument had side effects, these side
effects were duplicated.

Add _testcapi.test_py_clear() unit test.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 4, 2022
Fix the Py_CLEAR() macro to avoid the duplicate of side effects.
Previously, if the argument had side effects, these side effects were
duplicated.

Add _testcapi.test_py_clear() unit test.
@vstinner
Copy link
Member

vstinner commented Nov 4, 2022

I created PR #99100 to fix this issue. I'm not convinced that we should fix it, but a full PR might help to make a decision.

In the past, I saw surprising bug like https://bugs.python.org/issue43181 about passing a C++ expression to a C macro. So well, maybe in case of doubt, it's better to fix the issue to be extra safe. Py_CLEAR() pretends to be safer than using directly the Py_DECREF() macro ;-)

@vstinner
Copy link
Member

vstinner commented Nov 4, 2022

Ah wait, I read again #98724 (comment) and now I got the issue :-) I updated the unit test in my PR #99100.

@hochl
Copy link
Author

hochl commented Nov 4, 2022

Perfect! The main problem I have with macros that pretend to be functions is that they might evaluate their arguments several times, and this is totally unexpected. I hope the improved version works for all use-cases :)

@serhiy-storchaka
Copy link
Member

serhiy-storchaka commented Nov 5, 2022

I am not sure that it is not just a documentation issue. We can just document that the argument of Py_CLEAR (and the first argument of Py_SETREF) should not have side effect.

We can make it working even for arguments with a side effect, but should it be considered a bug fix or a new feature?

@vstinner
Copy link
Member

vstinner commented Nov 5, 2022

We can make it working even for arguments with a side effect, but should it be considered a bug fix or a new feature?

My PR fix the macro so it behaves correctly with arguments with side effects. For me it's a bugfix and should be backported to stable branches. If you are scared of the new implementation (using a pointer to a pointer), I'm fine with only changing the macro in Python 3.12 for now, and only backport if there is a strong pressure from many users to backport the fix.

@serhiy-storchaka
Copy link
Member

serhiy-storchaka commented Nov 5, 2022

I am fine with the new implementation. I think all modern compilers can get handle it without adding overhead if it is in a macro.

I afraid that users will start to rely on this feature and then their code will work incoretly when compiled with older Python, depending on the bugfix number.

@hochl
Copy link
Author

hochl commented Nov 8, 2022

I think this should be extended to all macros, so that they only reference their arguments once. If that can be done it could be made public that from now on macros are safe to use even with arguments that have side effects.

vstinner added a commit to vstinner/cpython that referenced this issue Nov 8, 2022
Fix the Py_CLEAR(), Py_SETREF() and Py_XSETREF() macrox to avoid the
duplication of side effects. Previously, if the argument had side
effects, these side effects were duplicated.

Add test_py_clear() and test_py_setref() unit tests.
@vstinner
Copy link
Member

vstinner commented Nov 8, 2022

I completed my PR to fix also Py_SETREF() and Py_XSETREF() macros.

vstinner added a commit to vstinner/cpython that referenced this issue Nov 8, 2022
Fix the Py_CLEAR(), Py_SETREF() and Py_XSETREF() macrox to avoid the
duplication of side effects. Previously, if the argument had side
effects, these side effects were duplicated.

Add test_py_clear() and test_py_setref() unit tests.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 8, 2022
Fix the Py_CLEAR(), Py_SETREF() and Py_XSETREF() macrox to avoid the
duplication of side effects. Previously, if the argument had side
effects, these side effects were duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 8, 2022
Fix the Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros to avoid the
duplication of side effects. Previously, if the argument had side
effects, these side effects were duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 8, 2022
Fix the Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros to avoid the
duplication of side effects. Previously, if the argument had side
effects, these side effects were duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 8, 2022
The Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros now only evaluate
their argument once. If the argument has side effects, these side
effects are no longer duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 9, 2022
The Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros now only evaluate
their argument once. If the argument has side effects, these side
effects are no longer duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 9, 2022
The Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros now only evaluate
their argument once. If the argument has side effects, these side
effects are no longer duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
vstinner added a commit that referenced this issue Nov 9, 2022
The Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros now only evaluate
their argument once. If an argument has side effects, these side
effects are no longer duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
vstinner added a commit to vstinner/cpython that referenced this issue Nov 9, 2022
The Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros now only evaluate
their argument once. If an argument has side effects, these side
effects are no longer duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.

(cherry picked from commit c03e05c)
vstinner added a commit that referenced this issue Nov 9, 2022
The Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros now only evaluate
their argument once. If an argument has side effects, these side
effects are no longer duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.

(cherry picked from commit c03e05c)
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Nov 9, 2022
…ythonGH-99288)

The Py_CLEAR(), Py_SETREF() and Py_XSETREF() macros now only evaluate
their argument once. If an argument has side effects, these side
effects are no longer duplicated.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.

(cherry picked from commit 1082890)

Co-authored-by: Victor Stinner <vstinner@python.org>
(cherry picked from commit c03e05c)
vstinner added a commit to python/pythoncapi-compat that referenced this issue Nov 9, 2022
vstinner added a commit to python/pythoncapi-compat that referenced this issue Nov 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core Interpreter core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

5 participants