https://github.com/python/cpython/commit/1405607faf61d76cf00ab88f5d2e81c5fa1754ac
commit: 1405607faf61d76cf00ab88f5d2e81c5fa1754ac
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-11-05T14:28:38+01:00
summary:
gh-161226: Fail if the hash is cached in _PyTuple_Resize() (#161293)
_PyTuple_Resize() now fails with SystemError if the hash value was
already computed.
Add compute_hash optional parameter to _testcapi._tuple_resize().
files:
A Misc/NEWS.d/next/C_API/2026-11-03-01-24-01.gh-issue-161226.MPrJKp.rst
M Doc/c-api/tuple.rst
M Lib/test/test_capi/test_tuple.py
M Modules/_testcapi/tuple.c
M Objects/tupleobject.c
diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst
index e8be4762dc33a1..10929b233d6840 100644
--- a/Doc/c-api/tuple.rst
+++ b/Doc/c-api/tuple.rst
@@ -133,16 +133,36 @@ Tuple Objects
.. c:function:: int _PyTuple_Resize(PyObject **p, Py_ssize_t newsize)
- Can be used to resize a tuple. *newsize* will be the new length of the
tuple.
- Because tuples are *supposed* to be immutable, this should only be used if
there
- is only one reference to the object. Do *not* use this if the tuple may
already
- be known to some other part of the code. The tuple will always grow or
shrink
- at the end. Think of this as destroying the old tuple and creating a new
one,
- only more efficiently. Returns ``0`` on success. Client code should never
- assume that the resulting value of ``*p`` will be the same as before calling
- this function. If the object referenced by ``*p`` is replaced, the original
- ``*p`` is destroyed. On failure, returns ``-1`` and sets ``*p`` to
``NULL``, and
- raises :exc:`MemoryError` or :exc:`SystemError`.
+ Resize a tuple.
+ *newsize* will be the new length of the tuple.
+
+ Because tuples are *supposed* to be immutable, this should only be used if
+ there is only one reference to the object.
+ Do *not* use this if the tuple may already be known to some other part of
+ the code.
+ The tuple hash value must not be computed yet.
+
+ The tuple will always grow or shrink at the end.
+ Think of this as destroying the old tuple and creating a new one, only more
+ efficiently.
+
+ Returns ``0`` on success.
+ Client code should never assume that the resulting value of ``*p`` will be
+ the same as before calling this function.
+ If the object referenced by ``*p`` is replaced, the original ``*p`` is
+ destroyed.
+
+ On failure, returns ``-1`` and sets ``*p`` to ``NULL``, and raises
+ :exc:`MemoryError` or :exc:`SystemError`.
+
+ While tuple objects are usually immutable in Python, this special C API
+ allows mutating a tuple object in-place.
+ The returned tuple object can still be mutated using
+ :c:func:`PyTuple_SetItem`; except if *newsize* is zero in which case it
+ returns the immutable empty tuple.
+
+ .. versionchanged:: next
+ The function now fails if the tuple hash value was already computed.
.. _struct-sequence-objects:
diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py
index fe5d694ea7d831..3f2318f303d57a 100644
--- a/Lib/test/test_capi/test_tuple.py
+++ b/Lib/test/test_capi/test_tuple.py
@@ -1,6 +1,6 @@
-import unittest
import gc
-from sys import getrefcount
+import sys
+import unittest
from test.support import import_helper
_testcapi = import_helper.import_module('_testcapi')
@@ -131,9 +131,9 @@ def check_tuple_from_pair(self, from_pair):
class Temp:
pass
temp = Temp()
- temp_rc = getrefcount(temp)
+ temp_rc = sys.getrefcount(temp)
self.assertEqual(from_pair(temp, temp), (temp, temp))
- self.assertEqual(getrefcount(temp), temp_rc)
+ self.assertEqual(sys.getrefcount(temp), temp_rc)
self._not_tracked(from_pair(1, 2))
self._not_tracked(from_pair(None, None))
@@ -299,30 +299,71 @@ def test_tuple_set_item(self):
def test__tuple_resize(self):
# Test _PyTuple_Resize()
- resize = _testcapi._tuple_resize
+ _resize = _testcapi._tuple_resize
checknull = _testcapi._check_tuple_item_is_NULL
- a = ()
- b = resize(a, 0, False)
- self.assertEqual(len(a), 0)
- self.assertEqual(len(b), 0)
- b = resize(a, 2, False)
- self.assertEqual(len(a), 0)
- self.assertEqual(len(b), 2)
- self.assertTrue(checknull(b, 0))
- self.assertTrue(checknull(b, 1))
-
- a = ([1], [2], [3])
- b = resize(a, 3)
+ def assert_is_fresh_copy(result, refcnt, is_new_obj):
+ self.assertEqual(refcnt, 1)
+ self.assertTrue(is_new_obj)
+ self.assertFalse(sys._is_immortal(result))
+
+ def resize(tup, new_size, new=True, compute_hash=False):
+ if isinstance(tup, tuple):
+ old_size = len(tup)
+ else:
+ old_size = 0
+ result, refcnt, is_new_obj = _resize(tup, new_size, new,
+ compute_hash=compute_hash)
+ if new_size == old_size:
+ # Return the same object unchanged
+ self.assertFalse(is_new_obj)
+ elif old_size == 0:
+ assert_is_fresh_copy(result, refcnt, is_new_obj)
+ elif new_size == 0:
+ # check that the result is the empty bytes string singleton
+ self.assertEqual(result, ())
+ self.assertTrue(sys._is_immortal(result))
+ self.assertTrue(is_new_obj)
+ elif not new:
+ # Make sure that the result is a fresh object.
+ assert_is_fresh_copy(result, refcnt, is_new_obj)
+ else:
+ # An in-place resize can return the same memory address, or
+ # not. 'is_new_obj' cannot be tested.
+ self.assertEqual(refcnt, 1)
+ self.assertFalse(sys._is_immortal(result))
+
+ return result
+
+ # Resizing the empty tuple singleton always create a new tuple
+ for compute_hash in (False, True):
+ with self.subTest(compute_hash=compute_hash):
+ a = ()
+ b = resize(a, 0, False, compute_hash)
+ self.assertEqual(len(a), 0)
+ self.assertEqual(len(b), 0)
+ b = resize(a, 2, False, compute_hash)
+ self.assertEqual(len(a), 0)
+ self.assertEqual(len(b), 2)
+ self.assertTrue(checknull(b, 0))
+ self.assertTrue(checknull(b, 1))
+
+ a = tuple(object() for _ in range(3))
+ b = resize(a, 3, True)
self.assertEqual(b, a)
- b = resize(a, 2)
+ b = resize(a, 2, True)
self.assertEqual(b, a[:2])
- b = resize(a, 5)
+ b = resize(a, 5, True)
self.assertEqual(len(b), 5)
self.assertEqual(b[:3], a)
self.assertTrue(checknull(b, 3))
self.assertTrue(checknull(b, 4))
+ # gh-161226: Disallow resizing a tuple if the hash value is cached
+ a = tuple(object() for _ in range(3))
+ with self.assertRaises(SystemError):
+ resize(a, 3, True, compute_hash=True)
+
a = ()
self.assertRaises(MemoryError, resize, a, PY_SSIZE_T_MAX)
self.assertRaises(SystemError, resize, a, -1)
diff --git
a/Misc/NEWS.d/next/C_API/2026-11-03-01-24-01.gh-issue-161226.MPrJKp.rst
b/Misc/NEWS.d/next/C_API/2026-11-03-01-24-01.gh-issue-161226.MPrJKp.rst
new file mode 100644
index 00000000000000..601db29c66cd9b
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-11-03-01-24-01.gh-issue-161226.MPrJKp.rst
@@ -0,0 +1,2 @@
+:c:func:`_PyTuple_Resize` now fails with :exc:`SystemError` if the hash value
+was already computed. Patch by Victor Stinner.
diff --git a/Modules/_testcapi/tuple.c b/Modules/_testcapi/tuple.c
index 5de1c494c0a8c0..c3a1218d7916e9 100644
--- a/Modules/_testcapi/tuple.c
+++ b/Modules/_testcapi/tuple.c
@@ -66,12 +66,17 @@ tuple_set_item(PyObject *Py_UNUSED(module), PyObject *args)
}
static PyObject *
-_tuple_resize(PyObject *Py_UNUSED(module), PyObject *args)
+_tuple_resize(PyObject *Py_UNUSED(module), PyObject *args, PyObject *kwargs)
{
+ static char *kwlist[] = {"obj", "newsize", "new", "compute_hash", NULL};
PyObject *tup;
Py_ssize_t newsize;
int new = 1;
- if (!PyArg_ParseTuple(args, "On|p", &tup, &newsize, &new)) {
+ int compute_hash = 0;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs,
+ "On|pi", kwlist,
+ &tup, &newsize, &new, &compute_hash)) {
return NULL;
}
if (new) {
@@ -84,12 +89,24 @@ _tuple_resize(PyObject *Py_UNUSED(module), PyObject *args)
NULLABLE(tup);
Py_XINCREF(tup);
}
+
+ if (compute_hash) {
+ if (PyObject_Hash(tup) == -1) {
+ Py_DECREF(tup);
+ return NULL;
+ }
+ }
+
+ PyObject *old_tup = tup;
int r = _PyTuple_Resize(&tup, newsize);
if (r == -1) {
assert(tup == NULL);
return NULL;
}
- return tup;
+ assert(tup != NULL);
+
+ Py_ssize_t refcnt = Py_REFCNT(tup);
+ return Py_BuildValue("Nnp", tup, refcnt, tup != old_tup);
}
static PyObject *
@@ -135,7 +152,7 @@ static PyMethodDef test_methods[] = {
{"tuple_get_size", tuple_get_size, METH_O},
{"tuple_get_item", tuple_get_item, METH_VARARGS},
{"tuple_set_item", tuple_set_item, METH_VARARGS},
- {"_tuple_resize", _tuple_resize, METH_VARARGS},
+ {"_tuple_resize", _PyCFunction_CAST(_tuple_resize), METH_VARARGS |
METH_KEYWORDS},
{"_check_tuple_item_is_NULL", _check_tuple_item_is_NULL, METH_VARARGS},
{"tuple_fromarray", tuple_fromarray, METH_VARARGS},
{NULL},
diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c
index 627147e585c718..68ca2772507295 100644
--- a/Objects/tupleobject.c
+++ b/Objects/tupleobject.c
@@ -1031,8 +1031,11 @@ _PyTuple_Resize(PyObject **pv, Py_ssize_t newsize)
Py_ssize_t oldsize;
v = (PyTupleObject *) *pv;
- if (v == NULL || !Py_IS_TYPE(v, &PyTuple_Type) || newsize < 0 ||
- (Py_SIZE(v) != 0 && !_PyObject_IsUniquelyReferenced(*pv))) {
+ if (v == NULL || !Py_IS_TYPE(v, &PyTuple_Type) || newsize < 0
+ || (Py_SIZE(v) != 0 && !_PyObject_IsUniquelyReferenced(*pv))
+ // gh-161226: Disallow resizing a tuple if the hash value is cached
+ || (Py_SIZE(v) != 0 && v->ob_hash != -1))
+ {
*pv = 0;
Py_XDECREF(v);
PyErr_BadInternalCall();
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]