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

GH-59313: Do not allow incomplete tuples, or tuples with NULLs. #117747

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,10 @@ Porting to Python 3.13
platforms, the ``HAVE_STDDEF_H`` macro is only defined on Windows.
(Contributed by Victor Stinner in :gh:`108765`.)

* The :c:func:`PyTuple_SET_ITEM` inline function may not be passed ``NULL``.
This has always been the documented behavior, but was not enforced for
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.python.org/dev/c-api/tuple.html#c.PyTuple_SET_ITEM and https://docs.python.org/dev/c-api/tuple.html#c.PyTuple_SetItem don't say that the second argument must not be NULL. I suggest to modify the doc to be extra explicit about it.

the value until now. :c:var:``Py_None` should be used instead of ``NULL``.

Deprecated
----------

Expand Down
1 change: 1 addition & 0 deletions Include/cpython/tupleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ static inline Py_ssize_t PyTuple_GET_SIZE(PyObject *op) {
static inline void
PyTuple_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) {
PyTupleObject *tuple = _PyTuple_CAST(op);
assert(value != NULL);
vstinner marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You modified PyTuple_SET_ITEM() but not PyTuple_SetItem(), is it on purpose? I suggest to modify both, since PyTuple_SetItem() doesn't call PyTuple_SET_ITEM().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

assert(0 <= index);
assert(index < Py_SIZE(tuple));
tuple->ob_item[index] = value;
Expand Down
3 changes: 3 additions & 0 deletions Include/internal/pycore_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ typedef struct {

PyAPI_FUNC(PyObject *)_PyList_FromArraySteal(PyObject *const *src, Py_ssize_t n);

/* Creates tuple from the list, leaving the list empty */
PyObject *_PyList_AsTupleTakeItems(PyObject *);

#ifdef __cplusplus
}
#endif
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,29 @@ def test_lexicographic_ordering(self):
self.assertLess(a, b)
self.assertLess(b, c)

def test_bug_59313(self):
# Until 3.13, the C-API function PySequence_Tuple
# would create incomplete tuples which were visible
# to the cycle GC
TAG = object()
tuples = []

def referrer_tuples():
return [x for x in gc.get_referrers(TAG)
if isinstance(x, tuple)]

def my_iter():
nonlocal tuples
yield TAG # 'tag' gets stored in the result tuple
tuples += referrer_tuples()
for x in range(10):
tuples += referrer_tuples()
# Prior to 3.13 would raise a SystemError when the tuple needs to be resized
yield x

self.assertEqual(tuple(my_iter()), (TAG, *range(10)))
self.assertEqual(tuples, [])

# Notes on testing hash codes. The primary thing is that Python doesn't
# care about "random" hash codes. To the contrary, we like them to be
# very regular when possible, so that the low-order bits are as evenly
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Make sure that all tuple elements are valid and never NULL. Ensuring that
tuples are always valid reduces the risk of cycle GC bugs and prevents
``gc.get_referrers`` from returning invalid tuples.
4 changes: 2 additions & 2 deletions Modules/_testbuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -333,10 +333,10 @@ pack_from_list(PyObject *obj, PyObject *items, PyObject *format,

offset = NULL;
for (i = 0; i < nitems; i++) {
/* Loop invariant: args[j] are borrowed references or NULL. */
/* Loop invariant: args[j] are borrowed references or None. */
PyTuple_SET_ITEM(args, 0, obj);
for (j = 1; j < 2+nmemb; j++)
PyTuple_SET_ITEM(args, j, NULL);
PyTuple_SET_ITEM(args, j, Py_None);

Py_XDECREF(offset);
offset = PyLong_FromSsize_t(i*itemsize);
Expand Down
10 changes: 5 additions & 5 deletions Modules/itertoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4473,7 +4473,7 @@ zip_longest_next(ziplongestobject *lz)
Py_INCREF(result);
for (i=0 ; i < tuplesize ; i++) {
it = PyTuple_GET_ITEM(lz->ittuple, i);
if (it == NULL) {
if (it == Py_None) {
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
item = Py_NewRef(lz->fillvalue);
} else {
item = PyIter_Next(it);
Expand All @@ -4485,7 +4485,7 @@ zip_longest_next(ziplongestobject *lz)
return NULL;
} else {
item = Py_NewRef(lz->fillvalue);
PyTuple_SET_ITEM(lz->ittuple, i, NULL);
PyTuple_SET_ITEM(lz->ittuple, i, Py_None);
Py_DECREF(it);
}
}
Expand All @@ -4505,7 +4505,7 @@ zip_longest_next(ziplongestobject *lz)
return NULL;
for (i=0 ; i < tuplesize ; i++) {
it = PyTuple_GET_ITEM(lz->ittuple, i);
if (it == NULL) {
if (it == Py_None) {
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
item = Py_NewRef(lz->fillvalue);
} else {
item = PyIter_Next(it);
Expand All @@ -4517,7 +4517,7 @@ zip_longest_next(ziplongestobject *lz)
return NULL;
} else {
item = Py_NewRef(lz->fillvalue);
PyTuple_SET_ITEM(lz->ittuple, i, NULL);
PyTuple_SET_ITEM(lz->ittuple, i, Py_None);
Py_DECREF(it);
}
}
Expand All @@ -4542,7 +4542,7 @@ zip_longest_reduce(ziplongestobject *lz, PyObject *Py_UNUSED(ignored))
return NULL;
for (i=0; i<PyTuple_GET_SIZE(lz->ittuple); i++) {
PyObject *elem = PyTuple_GET_ITEM(lz->ittuple, i);
if (elem == NULL) {
if (elem == Py_None) {
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
elem = PyTuple_New(0);
if (elem == NULL) {
Py_DECREF(args);
Expand Down
52 changes: 12 additions & 40 deletions Objects/abstract.c
Original file line number Diff line number Diff line change
Expand Up @@ -2039,9 +2039,7 @@
PySequence_Tuple(PyObject *v)
{
PyObject *it; /* iter(v) */
Py_ssize_t n; /* guess for result tuple size */

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Address sanitizer

unused variable ‘n’ [-Wunused-variable]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Ubuntu SSL tests with OpenSSL (1.1.1w)

unused variable ‘n’ [-Wunused-variable]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Hypothesis tests on Ubuntu

unused variable ‘n’ [-Wunused-variable]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Ubuntu SSL tests with OpenSSL (3.0.13)

unused variable ‘n’ [-Wunused-variable]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Ubuntu SSL tests with OpenSSL (3.1.5)

unused variable ‘n’ [-Wunused-variable]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Ubuntu SSL tests with OpenSSL (3.2.1)

unused variable ‘n’ [-Wunused-variable]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / build (arm64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / build (arm64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / build and test (x64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows (free-threading) / build and test (x64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows / build (arm64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Windows / build (arm64)

'n': unreferenced local variable [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Ubuntu / build and test

unused variable ‘n’ [-Wunused-variable]

Check warning on line 2042 in Objects/abstract.c

View workflow job for this annotation

GitHub Actions / Ubuntu (free-threading) / build and test

unused variable ‘n’ [-Wunused-variable]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a warning: unused variable ‘n’ [-Wunused-variable]

erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
PyObject *result = NULL;
Py_ssize_t j;

if (v == NULL) {
return null_error();
Expand All @@ -2063,57 +2061,31 @@
if (it == NULL)
return NULL;

/* Guess result size and allocate space. */
n = PyObject_LengthHint(v, 10);
if (n == -1)
goto Fail;
result = PyTuple_New(n);
if (result == NULL)
PyObject *temp = PyList_New(0);
if (temp == NULL)
goto Fail;

/* Fill the tuple. */
for (j = 0; ; ++j) {
/* Fill the temporary list. */
for (;;) {
PyObject *item = PyIter_Next(it);
if (item == NULL) {
if (PyErr_Occurred())
if (PyErr_Occurred()) {
Py_DECREF(temp);
goto Fail;
}
break;
}
if (j >= n) {
size_t newn = (size_t)n;
/* The over-allocation strategy can grow a bit faster
than for lists because unlike lists the
over-allocation isn't permanent -- we reclaim
the excess before the end of this routine.
So, grow by ten and then add 25%.
*/
newn += 10u;
newn += newn >> 2;
if (newn > PY_SSIZE_T_MAX) {
/* Check for overflow */
PyErr_NoMemory();
Py_DECREF(item);
goto Fail;
}
n = (Py_ssize_t)newn;
if (_PyTuple_Resize(&result, n) != 0) {
Py_DECREF(item);
goto Fail;
}
if (_PyList_AppendTakeRef((PyListObject *)temp, item)) {
Py_DECREF(temp);
goto Fail;
}
PyTuple_SET_ITEM(result, j, item);
}

/* Cut tuple back if guess was too large. */
if (j < n &&
_PyTuple_Resize(&result, j) != 0)
goto Fail;

Py_DECREF(it);
PyObject *result = _PyList_AsTupleTakeItems(temp);
Py_DECREF(temp);
return result;

Fail:
Py_XDECREF(result);
Py_DECREF(it);
return NULL;
}
Expand Down
20 changes: 20 additions & 0 deletions Objects/listobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,26 @@ PyList_AsTuple(PyObject *v)
return ret;
}

PyObject *
_PyList_AsTupleTakeItems(PyObject *v)
{
if (v == NULL || !PyList_Check(v)) {
PyErr_BadInternalCall();
return NULL;
}
PyObject *ret;
PyListObject *self = (PyListObject *)v;
if (Py_SIZE(v) == 0) {
return PyTuple_New(0);
}
Py_BEGIN_CRITICAL_SECTION(self);
Py_ssize_t size = Py_SIZE(v);
Py_SET_SIZE(v, 0);
ret = _PyTuple_FromArraySteal(self->ob_item, size);
Py_END_CRITICAL_SECTION();
return ret;
}

PyObject *
_PyList_FromArraySteal(PyObject *const *src, Py_ssize_t n)
{
Expand Down
14 changes: 4 additions & 10 deletions Objects/tupleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,7 @@ static inline int maybe_freelist_push(PyTupleObject *);
static PyTupleObject *
tuple_alloc(Py_ssize_t size)
{
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
#ifdef Py_DEBUG
assert(size != 0); // The empty tuple is statically allocated.
#endif

assert(size > 0);
PyTupleObject *op = maybe_freelist_pop(size);
if (op == NULL) {
/* Check for overflow */
Expand Down Expand Up @@ -78,7 +71,7 @@ PyTuple_New(Py_ssize_t size)
return NULL;
}
for (Py_ssize_t i = 0; i < size; i++) {
op->ob_item[i] = NULL;
op->ob_item[i] = Py_None;
}
_PyObject_GC_TRACK(op);
return (PyObject *) op;
Expand Down Expand Up @@ -205,7 +198,8 @@ tupledealloc(PyTupleObject *op)

Py_ssize_t i = Py_SIZE(op);
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]);
assert(op->ob_item[i] != NULL);
Py_DECREF(op->ob_item[i]);
}
// This will abort on the empty singleton (if there is one).
if (!maybe_freelist_push(op)) {
Expand Down
12 changes: 6 additions & 6 deletions Objects/weakrefobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ clear_weakref_lock_held(PyWeakReference *self, PyObject **callback)
self->wr_prev = NULL;
self->wr_next = NULL;
}
if (callback != NULL) {
if (callback != NULL && self->wr_callback != NULL) {
*callback = self->wr_callback;
self->wr_callback = NULL;
}
Expand Down Expand Up @@ -999,7 +999,7 @@ PyObject_ClearWeakRefs(PyObject *object)
for (int done = 0; !done;) {
LOCK_WEAKREFS(object);
if (*list != NULL && is_basic_ref_or_proxy(*list)) {
PyObject *callback;
PyObject *callback = NULL;
clear_weakref_lock_held(*list, &callback);
assert(callback == NULL);
}
Expand All @@ -1022,7 +1022,7 @@ PyObject_ClearWeakRefs(PyObject *object)

Py_ssize_t num_items = 0;
for (int done = 0; !done;) {
PyObject *callback = NULL;
PyObject *callback = Py_None;
LOCK_WEAKREFS(object);
PyWeakReference *cur = *list;
if (cur != NULL) {
Expand All @@ -1032,18 +1032,18 @@ PyObject_ClearWeakRefs(PyObject *object)
PyTuple_SET_ITEM(tuple, num_items, (PyObject *) cur);
PyTuple_SET_ITEM(tuple, num_items + 1, callback);
num_items += 2;
callback = NULL;
callback = Py_None;
}
}
done = (*list == NULL);
UNLOCK_WEAKREFS(object);

Py_XDECREF(callback);
Py_DECREF(callback);
}

for (Py_ssize_t i = 0; i < num_items; i += 2) {
PyObject *callback = PyTuple_GET_ITEM(tuple, i + 1);
if (callback != NULL) {
if (callback != Py_None) {
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
PyObject *weakref = PyTuple_GET_ITEM(tuple, i);
handle_callback((PyWeakReference *)weakref, callback);
}
Expand Down
Loading