Skip to content

Commit 6e7738b

Browse files
authored
gh-158585: Fix _PyBytes_Resize() if hash value is already computed (#158589)
_PyBytes_Resize() now creates a new bytes object if the hash value was already computed. bytes_resize_inplace() no longer sets the hash value to -1, since this function must not be called if the hash value was already computed. Add tests on PyUnicode_Resize().
1 parent 1b015e6 commit 6e7738b

5 files changed

Lines changed: 177 additions & 53 deletions

File tree

‎Lib/test/test_capi/test_bytes.py‎

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -239,39 +239,60 @@ def test_resize(self):
239239
"""Test _PyBytes_Resize()"""
240240
_resize = _testcapi.bytes_resize
241241

242-
def resize(obj, size, new):
243-
result = _resize(obj, size, new)
244-
if 1 <= len(result):
245-
if new or size != len(obj):
246-
# gh-156995: Make sure that the result is a fresh object.
247-
# Previously, _PyBytes_Resize(&obj, 1) returned a singleton
248-
# if _PyObject_IsUniquelyReferenced() is false.
249-
self.assertEqual(sys.getrefcount(result), 1)
250-
self.assertFalse(sys._is_immortal(result))
251-
else:
242+
def assert_is_fresh_copy(result, refcnt, is_new_obj):
243+
self.assertEqual(refcnt, 1)
244+
self.assertTrue(is_new_obj)
245+
self.assertFalse(sys._is_immortal(result))
246+
247+
def resize(obj, size, new, compute_hash=False):
248+
old_size = len(obj)
249+
result, refcnt, is_new_obj = _resize(obj, size, new,
250+
compute_hash=compute_hash)
251+
252+
if size == old_size:
253+
# Return the same object unchanged
254+
self.assertFalse(is_new_obj)
255+
elif old_size == 0:
256+
assert_is_fresh_copy(result, refcnt, is_new_obj)
257+
elif size == 0:
252258
# check that the result is the empty bytes string singleton
259+
self.assertEqual(result, b'')
253260
self.assertTrue(sys._is_immortal(result))
261+
self.assertTrue(is_new_obj)
262+
elif (not new) or compute_hash:
263+
# gh-156995: Make sure that the result is a fresh object.
264+
# Previously, _PyBytes_Resize(&obj, 1) returned a singleton
265+
# if _PyObject_IsUniquelyReferenced() is false.
266+
assert_is_fresh_copy(result, refcnt, is_new_obj)
267+
else:
268+
# An in-place resize can return the same memory address, or
269+
# not. 'is_new_obj' cannot be tested.
270+
self.assertEqual(refcnt, 1)
271+
self.assertFalse(sys._is_immortal(result))
272+
254273
return result
255274

256275
for new in True, False:
257-
with self.subTest(new=new):
258-
self.assertEqual(resize(b'abc', 0, new), b'')
259-
self.assertEqual(resize(b'abc', 1, new), b'a')
260-
self.assertEqual(resize(b'abc', 2, new), b'ab')
261-
self.assertEqual(resize(b'abc', 3, new), b'abc')
262-
b = resize(b'abc', 4, new)
263-
self.assertEqual(len(b), 4)
264-
self.assertEqual(b[:3], b'abc')
265-
266-
self.assertEqual(resize(b'a', 0, new), b'')
267-
self.assertEqual(resize(b'a', 1, new), b'a')
268-
b = resize(b'a', 2, new)
269-
self.assertEqual(len(b), 2)
270-
self.assertEqual(b[:1], b'a')
271-
272-
self.assertEqual(resize(b'', 0, new), b'')
273-
self.assertEqual(len(resize(b'', 1, new)), 1)
274-
self.assertEqual(len(resize(b'', 2, new)), 2)
276+
for compute_hash in True, False:
277+
with self.subTest(new=new, compute_hash=compute_hash):
278+
self.assertEqual(resize(b'abc', 0, new, compute_hash), b'')
279+
self.assertEqual(resize(b'abc', 1, new, compute_hash), b'a')
280+
self.assertEqual(resize(b'abc', 2, new, compute_hash), b'ab')
281+
self.assertEqual(resize(b'abc', 3, new, compute_hash), b'abc')
282+
283+
b = resize(b'abc', 4, new, compute_hash)
284+
self.assertEqual(len(b), 4)
285+
self.assertEqual(b[:3], b'abc')
286+
287+
self.assertEqual(resize(b'a', 0, new, compute_hash), b'')
288+
self.assertEqual(resize(b'a', 1, new, compute_hash), b'a')
289+
b = resize(b'a', 2, new, compute_hash)
290+
self.assertEqual(len(b), 2)
291+
self.assertEqual(b[:1], b'a')
292+
293+
self.assertEqual(resize(b'', 0, new, compute_hash), b'')
294+
self.assertEqual(len(resize(b'', 1, new, compute_hash)), 1)
295+
self.assertEqual(len(resize(b'', 2, new, compute_hash)), 2)
275296

276297
self.assertRaises(SystemError, resize, b'abc', -1, False)
277298
self.assertRaises(SystemError, resize, bytearray(b'abc'), 3, False)

‎Lib/test/test_capi/test_unicode.py‎

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,28 +170,75 @@ def test_write_macro(self):
170170

171171
def test_resize(self):
172172
"""Test PyUnicode_Resize()"""
173-
resize = _testlimitedcapi.unicode_resize
173+
_resize = _testlimitedcapi.unicode_resize
174+
resize_null = _testlimitedcapi.unicode_resize_null
175+
176+
def resize(s, length, new=True, compute_hash=False):
177+
if s is not NULL and isinstance(s, str):
178+
old_length = len(s)
179+
else:
180+
old_length = 0
181+
result, int_result, refcnt, is_new_obj = _resize(s, length,
182+
new, compute_hash)
183+
self.assertEqual(int_result, 0)
184+
185+
if length == old_length:
186+
# Return the same object unchanged
187+
self.assertFalse(is_new_obj)
188+
elif length == 0:
189+
# Get the empty Unicode string
190+
self.assertEqual(result, '')
191+
self.assertTrue(sys._is_immortal(result))
192+
self.assertTrue(is_new_obj)
193+
elif (not new) or compute_hash:
194+
# Get a fresh copy
195+
self.assertEqual(refcnt, 1)
196+
self.assertTrue(is_new_obj)
197+
self.assertFalse(sys._is_immortal(result))
198+
else:
199+
# In-size replace can return the same address, or not.
200+
# So 'is_new_obj' cannot be tested.
201+
self.assertFalse(sys._is_immortal(result))
202+
203+
return result
174204

175205
strings = [
176206
# all strings have exactly 3 characters
177207
'abc', '\xa1\xa2\xa3', '\u4f60\u597d\u4e16',
178208
'\U0001f600\U0001f601\U0001f602'
179209
]
180-
for s in strings:
181-
self.assertEqual(resize(s, 3), (s, 0))
182-
self.assertEqual(resize(s, 2), (s[:2], 0))
183-
self.assertEqual(resize(s, 4), (s + '\0', 0))
184-
self.assertEqual(resize(s, 10), (s + '\0'*7, 0))
185-
self.assertEqual(resize(s, 0), ('', 0))
186-
self.assertRaises(MemoryError, resize, s, PY_SSIZE_T_MAX)
187-
self.assertRaises(SystemError, resize, s, -1)
188-
self.assertRaises(SystemError, resize, s, PY_SSIZE_T_MIN)
210+
for new in (True, False):
211+
for compute_hash in (True, False):
212+
for s in strings:
213+
with self.subTest(new=new, compute_hash=compute_hash, s=s):
214+
self.assertEqual(resize(s, 3, new, compute_hash),
215+
s)
216+
self.assertEqual(resize(s, 2, new, compute_hash),
217+
s[:2])
218+
self.assertEqual(resize(s, 4, new, compute_hash),
219+
s + '\0')
220+
self.assertEqual(resize(s, 10, new, compute_hash),
221+
s + '\0'*7)
222+
self.assertEqual(resize(s, 0, new, compute_hash),
223+
'')
224+
225+
with self.assertRaises(MemoryError):
226+
resize(s, PY_SSIZE_T_MAX, new, compute_hash)
227+
with self.assertRaises(SystemError):
228+
resize(s, -1, new, compute_hash)
229+
with self.assertRaises(SystemError):
230+
resize(s, PY_SSIZE_T_MIN, new, compute_hash)
231+
189232
self.assertRaises(SystemError, resize, b'abc', 0)
190233
self.assertRaises(SystemError, resize, [], 0)
191234
self.assertRaises(SystemError, resize, NULL, 0)
192235
# TODO: Test PyUnicode_Resize() with non-modifiable and legacy unicode
193236
# and with NULL as the address.
194237

238+
# Test PyUnicode_Resize(NULL, length)
239+
self.assertRaises(SystemError, resize_null, 0)
240+
self.assertRaises(SystemError, resize_null, 123)
241+
195242
def test_append(self):
196243
"""Test PyUnicode_Append()"""
197244
append = _testlimitedcapi.unicode_append

‎Modules/_testcapi/bytes.c‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@
1111

1212
/* Test _PyBytes_Resize() */
1313
static PyObject *
14-
bytes_resize(PyObject *Py_UNUSED(module), PyObject *args)
14+
bytes_resize(PyObject *Py_UNUSED(module), PyObject *args, PyObject *kwargs)
1515
{
16+
static char *kwlist[] = {"obj", "newsize", "new", "compute_hash", NULL};
1617
PyObject *obj;
1718
Py_ssize_t newsize;
1819
int new;
20+
int compute_hash = 0;
1921

20-
if (!PyArg_ParseTuple(args, "Onp", &obj, &newsize, &new))
22+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
23+
"Onp|p", kwlist,
24+
&obj, &newsize, &new, &compute_hash)) {
2125
return NULL;
26+
}
2227

2328
NULLABLE(obj);
2429
if (new) {
@@ -34,13 +39,24 @@ bytes_resize(PyObject *Py_UNUSED(module), PyObject *args)
3439
else {
3540
Py_XINCREF(obj);
3641
}
42+
43+
if (compute_hash) {
44+
if (PyObject_Hash(obj) == -1) {
45+
return NULL;
46+
}
47+
}
48+
49+
PyObject *old_obj = obj;
3750
if (_PyBytes_Resize(&obj, newsize) < 0) {
3851
assert(obj == NULL);
52+
return NULL;
3953
}
4054
else {
4155
assert(obj != NULL);
4256
}
43-
return obj;
57+
58+
Py_ssize_t refcnt = Py_REFCNT(obj);
59+
return Py_BuildValue("Onp", obj, refcnt, obj != old_obj);
4460
}
4561

4662

@@ -604,7 +620,7 @@ corrupt_bytes(PyObject *Py_UNUSED(module), PyObject *args)
604620

605621

606622
static PyMethodDef test_methods[] = {
607-
{"bytes_resize", bytes_resize, METH_VARARGS},
623+
{"bytes_resize", _PyCFunction_CAST(bytes_resize), METH_VARARGS | METH_KEYWORDS},
608624
{"bytes_join", bytes_join, METH_VARARGS},
609625
{"byteswriter_abc", byteswriter_abc, METH_NOARGS},
610626
{"byteswriter_resize", byteswriter_resize, METH_NOARGS},

‎Modules/_testlimitedcapi/unicode.c‎

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,24 +183,59 @@ unicode_resize(PyObject *self, PyObject *args)
183183
PyObject *obj, *copy;
184184
Py_ssize_t length;
185185
int result;
186+
int new = 1;
187+
int compute_hash = 0;
186188

187-
if (!PyArg_ParseTuple(args, "On", &obj, &length)) {
189+
if (!PyArg_ParseTuple(args, "On|pp", &obj, &length, &new, &compute_hash)) {
188190
return NULL;
189191
}
190192

191193
NULLABLE(obj);
192-
if (!(copy = unicode_copy(obj)) && obj) {
193-
return NULL;
194+
Py_ssize_t old_len = obj ? PyUnicode_GetLength(obj) : 0;
195+
if (obj != NULL && new) {
196+
copy = unicode_copy(obj);
197+
if (!copy) {
198+
return NULL;
199+
}
200+
obj = copy;
194201
}
195-
result = PyUnicode_Resize(&copy, length);
202+
else {
203+
Py_XINCREF(obj);
204+
}
205+
206+
if (compute_hash && obj != NULL) {
207+
if (PyObject_Hash(obj) == -1) {
208+
Py_DECREF(obj);
209+
return NULL;
210+
}
211+
}
212+
213+
PyObject *old_obj = obj;
214+
result = PyUnicode_Resize(&obj, length);
196215
if (result == -1 && PyErr_Occurred()) {
197-
Py_XDECREF(copy);
216+
Py_XDECREF(obj);
198217
return NULL;
199218
}
200-
if (obj && PyUnicode_Check(obj) && length > PyUnicode_GetLength(obj)) {
201-
unicode_fill(copy, PyUnicode_GetLength(obj), length, 0U);
219+
if (obj && PyUnicode_Check(obj) && length > old_len) {
220+
unicode_fill(obj, old_len, length, 0U);
202221
}
203-
return Py_BuildValue("(Ni)", copy, result);
222+
Py_ssize_t refcnt = Py_REFCNT(obj);
223+
return Py_BuildValue("(Ninp)", obj, result, refcnt, obj != old_obj);
224+
}
225+
226+
// Test PyUnicode_Resize(NULL, length)
227+
static PyObject *
228+
unicode_resize_null(PyObject *self, PyObject *args)
229+
{
230+
Py_ssize_t length;
231+
if (!PyArg_ParseTuple(args, "n", &length)) {
232+
return NULL;
233+
}
234+
235+
if (PyUnicode_Resize(NULL, length)) {
236+
return NULL;
237+
}
238+
Py_RETURN_NONE;
204239
}
205240

206241
/* Test PyUnicode_Append() */
@@ -1882,6 +1917,7 @@ static PyMethodDef TestMethods[] = {
18821917
{"test_widechar", test_widechar, METH_NOARGS},
18831918
{"unicode_writechar", unicode_writechar, METH_VARARGS},
18841919
{"unicode_resize", unicode_resize, METH_VARARGS},
1920+
{"unicode_resize_null", unicode_resize_null, METH_VARARGS},
18851921
{"unicode_append", unicode_append, METH_VARARGS},
18861922
{"unicode_appendanddel", unicode_appendanddel, METH_VARARGS},
18871923
{"unicode_fromstringandsize",unicode_fromstringandsize, METH_VARARGS},

‎Objects/bytesobject.c‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3405,7 +3405,6 @@ bytes_resize_inplace(PyObject **pv, Py_ssize_t newsize)
34053405
PyBytesObject *sv = (PyBytesObject *)v;
34063406
Py_SET_SIZE(sv, newsize);
34073407
sv->ob_sval[newsize] = '\0';
3408-
set_ob_shash(sv, -1); /* invalidate cached hash value */
34093408
assert(_PyBytes_IsMutable(*pv));
34103409
return 0;
34113410
}
@@ -3437,7 +3436,9 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize)
34373436

34383437
Py_ssize_t oldsize = PyBytes_GET_SIZE(v);
34393438
if (oldsize == newsize) {
3440-
/* return early if newsize equals to v->ob_size */
3439+
// Leave the object unchanged if the new size is the same as the old
3440+
// size, even if the object is not uniquely referenced or if the hash
3441+
// value was already computed.
34413442
return 0;
34423443
}
34433444

@@ -3458,7 +3459,10 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize)
34583459
return 0;
34593460
}
34603461

3461-
if (!_PyObject_IsUniquelyReferenced(v)) {
3462+
if (!_PyObject_IsUniquelyReferenced(v)
3463+
// Return a copy if the hash value was already computed
3464+
|| get_ob_shash((PyBytesObject *)v) != -1)
3465+
{
34623466
// Allocate and then copy so we don't get a shared immortal
34633467
// one-character singleton!
34643468
result = _PyBytes_FromSize(newsize, 0);

0 commit comments

Comments
 (0)