gh-98724: Fix Py_CLEAR() macro side effects (#99100) (#100070)

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

Use temporary variables to avoid duplicating side effects of macro
arguments. If available, use _Py_TYPEOF() to avoid type punning.
Otherwise, use memcpy() for the assignment to prevent a
miscompilation with strict aliasing caused by type punning.

Add _Py_TYPEOF() macro: __typeof__() on GCC and clang.

Add test_py_clear() and test_py_setref() unit tests to _testcapi.
This commit is contained in:
Victor Stinner 2022-12-07 15:22:38 +01:00 committed by GitHub
parent 7031275776
commit b11a384dc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 236 additions and 30 deletions

View File

@ -7,8 +7,8 @@
Reference Counting Reference Counting
****************** ******************
The macros in this section are used for managing reference counts of Python The functions and macros in this section are used for managing reference counts
objects. of Python objects.
.. c:function:: Py_ssize_t Py_REFCNT(PyObject *o) .. c:function:: Py_ssize_t Py_REFCNT(PyObject *o)
@ -129,6 +129,11 @@ objects.
It is a good idea to use this macro whenever decrementing the reference It is a good idea to use this macro whenever decrementing the reference
count of an object that might be traversed during garbage collection. count of an object that might be traversed during garbage collection.
.. versionchanged:: 3.12
The macro argument is now only evaluated once. If the argument has side
effects, these are no longer duplicated.
.. c:function:: void Py_IncRef(PyObject *o) .. c:function:: void Py_IncRef(PyObject *o)
Increment the reference count for object *o*. A function version of :c:func:`Py_XINCREF`. Increment the reference count for object *o*. A function version of :c:func:`Py_XINCREF`.
@ -139,3 +144,40 @@ objects.
Decrement the reference count for object *o*. A function version of :c:func:`Py_XDECREF`. Decrement the reference count for object *o*. A function version of :c:func:`Py_XDECREF`.
It can be used for runtime dynamic embedding of Python. It can be used for runtime dynamic embedding of Python.
.. c:macro:: Py_SETREF(dst, src)
Macro safely decrementing the `dst` reference count and setting `dst` to
`src`.
As in case of :c:func:`Py_CLEAR`, "the obvious" code can be deadly::
Py_DECREF(dst);
dst = src;
The safe way is::
Py_SETREF(dst, src);
That arranges to set `dst` to `src` _before_ decrementing reference count of
*dst* old value, so that any code triggered as a side-effect of `dst`
getting torn down no longer believes `dst` points to a valid object.
.. versionadded:: 3.6
.. versionchanged:: 3.12
The macro arguments are now only evaluated once. If an argument has side
effects, these are no longer duplicated.
.. c:macro:: Py_XSETREF(dst, src)
Variant of :c:macro:`Py_SETREF` macro that uses :c:func:`Py_XDECREF` instead
of :c:func:`Py_DECREF`.
.. versionadded:: 3.6
.. versionchanged:: 3.12
The macro arguments are now only evaluated once. If an argument has side
effects, these are no longer duplicated.

View File

@ -851,6 +851,11 @@ Porting to Python 3.12
:class:`bytes` type is accepted for bytes strings. :class:`bytes` type is accepted for bytes strings.
(Contributed by Victor Stinner in :gh:`98393`.) (Contributed by Victor Stinner in :gh:`98393`.)
* The :c:macro:`Py_CLEAR`, :c:macro:`Py_SETREF` and :c:macro:`Py_XSETREF`
macros now only evaluate their arguments once. If an argument has side
effects, these side effects are no longer duplicated.
(Contributed by Victor Stinner in :gh:`98724`.)
Deprecated Deprecated
---------- ----------

View File

@ -305,38 +305,69 @@ _PyObject_GenericSetAttrWithDict(PyObject *, PyObject *,
PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *); PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
/* Safely decref `op` and set `op` to `op2`. /* Safely decref `dst` and set `dst` to `src`.
* *
* As in case of Py_CLEAR "the obvious" code can be deadly: * As in case of Py_CLEAR "the obvious" code can be deadly:
* *
* Py_DECREF(op); * Py_DECREF(dst);
* op = op2; * dst = src;
* *
* The safe way is: * The safe way is:
* *
* Py_SETREF(op, op2); * Py_SETREF(dst, src);
* *
* That arranges to set `op` to `op2` _before_ decref'ing, so that any code * That arranges to set `dst` to `src` _before_ decref'ing, so that any code
* triggered as a side-effect of `op` getting torn down no longer believes * triggered as a side-effect of `dst` getting torn down no longer believes
* `op` points to a valid object. * `dst` points to a valid object.
* *
* Py_XSETREF is a variant of Py_SETREF that uses Py_XDECREF instead of * Temporary variables are used to only evalutate macro arguments once and so
* Py_DECREF. * avoid the duplication of side effects. _Py_TYPEOF() or memcpy() is used to
* avoid a miscompilation caused by type punning. See Py_CLEAR() comment for
* implementation details about type punning.
*
* The memcpy() implementation does not emit a compiler warning if 'src' has
* not the same type than 'src': any pointer type is accepted for 'src'.
*/ */
#ifdef _Py_TYPEOF
#define Py_SETREF(op, op2) \ #define Py_SETREF(dst, src) \
do { \ do { \
PyObject *_py_tmp = _PyObject_CAST(op); \ _Py_TYPEOF(dst)* _tmp_dst_ptr = &(dst); \
(op) = (op2); \ _Py_TYPEOF(dst) _tmp_old_dst = (*_tmp_dst_ptr); \
Py_DECREF(_py_tmp); \ *_tmp_dst_ptr = (src); \
Py_DECREF(_tmp_old_dst); \
} while (0) } while (0)
#else
#define Py_XSETREF(op, op2) \ #define Py_SETREF(dst, src) \
do { \ do { \
PyObject *_py_tmp = _PyObject_CAST(op); \ PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \
(op) = (op2); \ PyObject *_tmp_old_dst = (*_tmp_dst_ptr); \
Py_XDECREF(_py_tmp); \ PyObject *_tmp_src = _PyObject_CAST(src); \
memcpy(_tmp_dst_ptr, &_tmp_src, sizeof(PyObject*)); \
Py_DECREF(_tmp_old_dst); \
} while (0) } while (0)
#endif
/* Py_XSETREF() is a variant of Py_SETREF() that uses Py_XDECREF() instead of
* Py_DECREF().
*/
#ifdef _Py_TYPEOF
#define Py_XSETREF(dst, src) \
do { \
_Py_TYPEOF(dst)* _tmp_dst_ptr = &(dst); \
_Py_TYPEOF(dst) _tmp_old_dst = (*_tmp_dst_ptr); \
*_tmp_dst_ptr = (src); \
Py_XDECREF(_tmp_old_dst); \
} while (0)
#else
#define Py_XSETREF(dst, src) \
do { \
PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \
PyObject *_tmp_old_dst = (*_tmp_dst_ptr); \
PyObject *_tmp_src = _PyObject_CAST(src); \
memcpy(_tmp_dst_ptr, &_tmp_src, sizeof(PyObject*)); \
Py_XDECREF(_tmp_old_dst); \
} while (0)
#endif
PyAPI_DATA(PyTypeObject) _PyNone_Type; PyAPI_DATA(PyTypeObject) _PyNone_Type;

View File

@ -598,15 +598,44 @@ static inline void Py_DECREF(PyObject *op)
* one of those can't cause problems -- but in part that relies on that * one of those can't cause problems -- but in part that relies on that
* Python integers aren't currently weakly referencable. Best practice is * Python integers aren't currently weakly referencable. Best practice is
* to use Py_CLEAR() even if you can't think of a reason for why you need to. * to use Py_CLEAR() even if you can't think of a reason for why you need to.
*
* gh-98724: Use a temporary variable to only evaluate the macro argument once,
* to avoid the duplication of side effects if the argument has side effects.
*
* gh-99701: If the PyObject* type is used with casting arguments to PyObject*,
* the code can be miscompiled with strict aliasing because of type punning.
* With strict aliasing, a compiler considers that two pointers of different
* types cannot read or write the same memory which enables optimization
* opportunities.
*
* If available, use _Py_TYPEOF() to use the 'op' type for temporary variables,
* and so avoid type punning. Otherwise, use memcpy() which causes type erasure
* and so prevents the compiler to reuse an old cached 'op' value after
* Py_CLEAR().
*/ */
#define Py_CLEAR(op) \ #ifdef _Py_TYPEOF
do { \ #define Py_CLEAR(op) \
PyObject *_py_tmp = _PyObject_CAST(op); \ do { \
if (_py_tmp != NULL) { \ _Py_TYPEOF(op)* _tmp_op_ptr = &(op); \
(op) = NULL; \ _Py_TYPEOF(op) _tmp_old_op = (*_tmp_op_ptr); \
Py_DECREF(_py_tmp); \ if (_tmp_old_op != NULL) { \
} \ *_tmp_op_ptr = _Py_NULL; \
Py_DECREF(_tmp_old_op); \
} \
} while (0) } while (0)
#else
#define Py_CLEAR(op) \
do { \
PyObject **_tmp_op_ptr = _Py_CAST(PyObject**, &(op)); \
PyObject *_tmp_old_op = (*_tmp_op_ptr); \
if (_tmp_old_op != NULL) { \
PyObject *_null_ptr = _Py_NULL; \
memcpy(_tmp_op_ptr, &_null_ptr, sizeof(PyObject*)); \
Py_DECREF(_tmp_old_op); \
} \
} while (0)
#endif
/* Function to use in case the object pointer can be NULL: */ /* Function to use in case the object pointer can be NULL: */
static inline void Py_XINCREF(PyObject *op) static inline void Py_XINCREF(PyObject *op)

View File

@ -698,6 +698,15 @@ extern char * _getpty(int *, int, mode_t, int);
# define _Py__has_builtin(x) 0 # define _Py__has_builtin(x) 0
#endif #endif
// _Py_TYPEOF(expr) gets the type of an expression.
//
// Example: _Py_TYPEOF(x) x_copy = (x);
//
// The macro is only defined if GCC or clang compiler is used.
#if defined(__GNUC__) || defined(__clang__)
# define _Py_TYPEOF(expr) __typeof__(expr)
#endif
/* A convenient way for code to know if sanitizers are enabled. */ /* A convenient way for code to know if sanitizers are enabled. */
#if defined(__has_feature) #if defined(__has_feature)

View File

@ -0,0 +1,3 @@
The :c:macro:`Py_CLEAR`, :c:macro:`Py_SETREF` and :c:macro:`Py_XSETREF` macros
now only evaluate their arguments once. If an argument has side effects, these
side effects are no longer duplicated. Patch by Victor Stinner.

View File

@ -2589,6 +2589,91 @@ test_set_type_size(PyObject *self, PyObject *Py_UNUSED(ignored))
} }
// Test Py_CLEAR() macro
static PyObject*
test_py_clear(PyObject *self, PyObject *Py_UNUSED(ignored))
{
// simple case with a variable
PyObject *obj = PyList_New(0);
if (obj == NULL) {
return NULL;
}
Py_CLEAR(obj);
assert(obj == NULL);
// gh-98724: complex case, Py_CLEAR() argument has a side effect
PyObject* array[1];
array[0] = PyList_New(0);
if (array[0] == NULL) {
return NULL;
}
PyObject **p = array;
Py_CLEAR(*p++);
assert(array[0] == NULL);
assert(p == array + 1);
Py_RETURN_NONE;
}
// Test Py_SETREF() and Py_XSETREF() macros, similar to test_py_clear()
static PyObject*
test_py_setref(PyObject *self, PyObject *Py_UNUSED(ignored))
{
// Py_SETREF() simple case with a variable
PyObject *obj = PyList_New(0);
if (obj == NULL) {
return NULL;
}
Py_SETREF(obj, NULL);
assert(obj == NULL);
// Py_XSETREF() simple case with a variable
PyObject *obj2 = PyList_New(0);
if (obj2 == NULL) {
return NULL;
}
Py_XSETREF(obj2, NULL);
assert(obj2 == NULL);
// test Py_XSETREF() when the argument is NULL
Py_XSETREF(obj2, NULL);
assert(obj2 == NULL);
// gh-98724: complex case, Py_SETREF() argument has a side effect
PyObject* array[1];
array[0] = PyList_New(0);
if (array[0] == NULL) {
return NULL;
}
PyObject **p = array;
Py_SETREF(*p++, NULL);
assert(array[0] == NULL);
assert(p == array + 1);
// gh-98724: complex case, Py_XSETREF() argument has a side effect
PyObject* array2[1];
array2[0] = PyList_New(0);
if (array2[0] == NULL) {
return NULL;
}
PyObject **p2 = array2;
Py_XSETREF(*p2++, NULL);
assert(array2[0] == NULL);
assert(p2 == array2 + 1);
// test Py_XSETREF() when the argument is NULL
p2 = array2;
Py_XSETREF(*p2++, NULL);
assert(array2[0] == NULL);
assert(p2 == array2 + 1);
Py_RETURN_NONE;
}
#define TEST_REFCOUNT() \ #define TEST_REFCOUNT() \
do { \ do { \
PyObject *obj = PyList_New(0); \ PyObject *obj = PyList_New(0); \
@ -3252,6 +3337,8 @@ static PyMethodDef TestMethods[] = {
{"pynumber_tobase", pynumber_tobase, METH_VARARGS}, {"pynumber_tobase", pynumber_tobase, METH_VARARGS},
{"without_gc", without_gc, METH_O}, {"without_gc", without_gc, METH_O},
{"test_set_type_size", test_set_type_size, METH_NOARGS}, {"test_set_type_size", test_set_type_size, METH_NOARGS},
{"test_py_clear", test_py_clear, METH_NOARGS},
{"test_py_setref", test_py_setref, METH_NOARGS},
{"test_refcount_macros", test_refcount_macros, METH_NOARGS}, {"test_refcount_macros", test_refcount_macros, METH_NOARGS},
{"test_refcount_funcs", test_refcount_funcs, METH_NOARGS}, {"test_refcount_funcs", test_refcount_funcs, METH_NOARGS},
{"test_py_is_macros", test_py_is_macros, METH_NOARGS}, {"test_py_is_macros", test_py_is_macros, METH_NOARGS},