Skip to content

Commit 8356e9c

Browse files
authored
[3.14] gh-158585: Fix _PyBytes_Resize() if hash value is already computed (#159585)
* [3.15] gh-158585: Fix _PyBytes_Resize() if hash value is already computed (#159555) * 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(). (cherry picked from commit 6e7738b) * gh-158585: Fix refleak in _testcapi.bytes_resize() (#158806) (cherry picked from commit 587b7a5) (cherry picked from commit 251278d) * Adjust code for 3.14
1 parent 742798d commit 8356e9c

5 files changed

Lines changed: 251 additions & 83 deletions

File tree

‎Lib/test/test_capi/test_bytes.py‎

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

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

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

271292
self.assertRaises(SystemError, resize, b'abc', -1, False)
272293
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
@@ -168,28 +168,75 @@ def test_write_macro(self):
168168

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

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

236+
# Test PyUnicode_Resize(NULL, length)
237+
self.assertRaises(SystemError, resize_null, 0)
238+
self.assertRaises(SystemError, resize_null, 123)
239+
193240
def test_append(self):
194241
"""Test PyUnicode_Append()"""
195242
append = _testlimitedcapi.unicode_append

‎Modules/_testcapi/bytes.c‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44

55
/* Test _PyBytes_Resize() */
66
static PyObject *
7-
bytes_resize(PyObject *Py_UNUSED(module), PyObject *args)
7+
bytes_resize(PyObject *Py_UNUSED(module), PyObject *args, PyObject *kwargs)
88
{
9+
static char *kwlist[] = {"obj", "newsize", "new", "compute_hash", NULL};
910
PyObject *obj;
1011
Py_ssize_t newsize;
1112
int new;
13+
int compute_hash = 0;
1214

13-
if (!PyArg_ParseTuple(args, "Onp", &obj, &newsize, &new))
15+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
16+
"Onp|p", kwlist,
17+
&obj, &newsize, &new, &compute_hash)) {
1418
return NULL;
19+
}
1520

1621
NULLABLE(obj);
1722
if (new) {
@@ -27,13 +32,25 @@ bytes_resize(PyObject *Py_UNUSED(module), PyObject *args)
2732
else {
2833
Py_XINCREF(obj);
2934
}
35+
36+
if (compute_hash) {
37+
if (PyObject_Hash(obj) == -1) {
38+
Py_DECREF(obj);
39+
return NULL;
40+
}
41+
}
42+
43+
PyObject *old_obj = obj;
3044
if (_PyBytes_Resize(&obj, newsize) < 0) {
3145
assert(obj == NULL);
46+
return NULL;
3247
}
3348
else {
3449
assert(obj != NULL);
3550
}
36-
return obj;
51+
52+
Py_ssize_t refcnt = Py_REFCNT(obj);
53+
return Py_BuildValue("Nnp", obj, refcnt, obj != old_obj);
3754
}
3855

3956

@@ -52,7 +69,7 @@ bytes_join(PyObject *Py_UNUSED(module), PyObject *args)
5269

5370

5471
static PyMethodDef test_methods[] = {
55-
{"bytes_resize", bytes_resize, METH_VARARGS},
72+
{"bytes_resize", _PyCFunction_CAST(bytes_resize), METH_VARARGS | METH_KEYWORDS},
5673
{"bytes_join", bytes_join, METH_VARARGS},
5774
{NULL},
5875
};

‎Modules/_testlimitedcapi/unicode.c‎

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,24 +181,59 @@ unicode_resize(PyObject *self, PyObject *args)
181181
PyObject *obj, *copy;
182182
Py_ssize_t length;
183183
int result;
184+
int new = 1;
185+
int compute_hash = 0;
184186

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

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

204239
/* Test PyUnicode_Append() */
@@ -1880,6 +1915,7 @@ static PyMethodDef TestMethods[] = {
18801915
{"test_widechar", test_widechar, METH_NOARGS},
18811916
{"unicode_writechar", unicode_writechar, METH_VARARGS},
18821917
{"unicode_resize", unicode_resize, METH_VARARGS},
1918+
{"unicode_resize_null", unicode_resize_null, METH_VARARGS},
18831919
{"unicode_append", unicode_append, METH_VARARGS},
18841920
{"unicode_appendanddel", unicode_appendanddel, METH_VARARGS},
18851921
{"unicode_fromstringandsize",unicode_fromstringandsize, METH_VARARGS},

0 commit comments

Comments
 (0)