Skip to content

Commit 527e9a2

Browse files
authored
[3.14] gh-157710: Add MemoryError tests to PyUnicodeWriter (#159557)
[3.15] gh-157710: Add MemoryError tests to PyUnicodeWriter (#159548) * gh-157710: Add _PyUnicodeWriter_CanWrite() function (#157712) Make sure that a writer can be modified before writing into it. * Document that PyUnicodeWriter is not thread safe. * Add singletons tests to test_capi.test_unicode. * Check PyUnicode_CheckExact() earlier in _PyUnicode_IsModifiable(). (cherry picked from commit 8542958) * gh-157710: Add MemoryError tests to PyUnicodeWriter (#157853) Add a test changing the buffer kind multiple times. Add assertions to _PyUnicodeWriter_InitWithBuffer(). (cherry picked from commit 6b97452) * No need to export _PyUnicode_IsModifiable() (cherry picked from commit adc1127)
1 parent 3128d07 commit 527e9a2

6 files changed

Lines changed: 120 additions & 19 deletions

File tree

‎Doc/c-api/unicode.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,9 @@ object.
18131813
The instance must be destroyed by :c:func:`PyUnicodeWriter_Finish` on
18141814
success, or :c:func:`PyUnicodeWriter_Discard` on error.
18151815
1816+
The API is **not thread safe**. To share a writer with multiple threads, a
1817+
critical section or a lock is needed.
1818+
18161819
.. c:function:: PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length)
18171820
18181821
Create a Unicode writer instance.

‎Include/internal/pycore_unicodeobject.h‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,41 @@ extern "C" {
99
#endif
1010

1111
#include "pycore_fileutils.h" // _Py_error_handler
12+
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_SSIZE_RELAXED
1213
#include "pycore_ucnhash.h" // _PyUnicode_Name_CAPI
1314

15+
16+
// Maximum code point of Unicode 6.0: 0x10ffff (1,114,111).
17+
// The value must be the same in fileutils.c.
18+
#define _Py_MAX_UNICODE 0x10ffff
19+
20+
21+
static inline Py_hash_t PyUnicode_HASH(PyObject *op)
22+
{
23+
assert(PyUnicode_Check(op));
24+
return FT_ATOMIC_LOAD_SSIZE_RELAXED(_PyASCIIObject_CAST(op)->hash);
25+
}
26+
27+
28+
#ifndef NDEBUG
29+
static inline int
30+
_PyUnicodeWriter_CanWrite(_PyUnicodeWriter *writer)
31+
{
32+
// Code adapted from unicode_modifiable()
33+
assert(!writer->readonly);
34+
PyObject *buffer = writer->buffer;
35+
assert(buffer != NULL);
36+
// Do not use _PyObject_IsUniquelyReferenced(): the caller can have its own
37+
// lock to prevent a writer being used by two theads at the same time.
38+
assert(Py_REFCNT(buffer) == 1);
39+
assert(PyUnicode_HASH(buffer) == -1);
40+
assert(!PyUnicode_CHECK_INTERNED(buffer));
41+
assert(!_Py_IsImmortal(buffer));
42+
return 1;
43+
}
44+
#endif
45+
46+
1447
/* --- Characters Type APIs ----------------------------------------------- */
1548

1649
extern int _PyUnicode_IsXidStart(Py_UCS4 ch);

‎Lib/test/support/__init__.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3361,3 +3361,22 @@ def built_with_c_assertions():
33613361
return Py_DEBUG
33623362
else:
33633363
return bool(_testlimitedcapi._py_getbuiltwithassert())
3364+
3365+
3366+
@contextlib.contextmanager
3367+
def inject_memory_error_cm(start=0, stop=0):
3368+
"""
3369+
Similar to inject_memory_error() but can be used as a context manager.
3370+
3371+
Raise SkipTest if the _testcapi extension module is missing
3372+
"""
3373+
try:
3374+
import _testcapi
3375+
except ImportError:
3376+
raise unittest.SkipTest("_testcapi required")
3377+
3378+
try:
3379+
_testcapi.set_nomemory(start, stop)
3380+
yield
3381+
finally:
3382+
_testcapi.remove_mem_hooks()

‎Lib/test/test_capi/test_unicode.py‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,10 +2088,8 @@ def test_substring_empty(self):
20882088
self.assertEqual(writer.finish(), '')
20892089

20902090
def test_singletons(self):
2091-
for size in (0, 123):
2092-
with self.subTest(size=size):
2093-
writer = self.create_writer(size)
2094-
self.assertIs(writer.finish(), '')
2091+
writer = self.create_writer(5)
2092+
self.assertIs(writer.finish(), '')
20952093

20962094
for ch in range(256):
20972095
with self.subTest(ch=ch):
@@ -2100,6 +2098,39 @@ def test_singletons(self):
21002098
writer.write_substring(ch + 'xxx', 0, 1)
21012099
self.assertIs(writer.finish(), ch)
21022100

2101+
@support.nomemtest
2102+
def test_memory_error(self):
2103+
# Inject MemoryError in PyUnicodeWriter_WriteStr()
2104+
writer = self.create_writer(0)
2105+
writer.write_str("start")
2106+
with self.assertRaises(MemoryError):
2107+
with support.inject_memory_error_cm():
2108+
# Resize the internal str object
2109+
writer.write_str("s" * 1024)
2110+
writer.write_str(" end")
2111+
self.assertEqual(writer.finish(), "start end")
2112+
2113+
# Inject MemoryError in PyUnicodeWriter_Finish()
2114+
writer = self.create_writer(1024)
2115+
writer.write_str("abc")
2116+
with self.assertRaises(MemoryError):
2117+
with support.inject_memory_error_cm():
2118+
# Need to truncate the internal str object
2119+
writer.finish()
2120+
2121+
def test_change_kind(self):
2122+
writer = self.create_writer(0)
2123+
# Create an ASCII buffer
2124+
writer.write_str('ascii ')
2125+
# Change the buffer to UCS1
2126+
writer.write_str('latin1:\xe9 ')
2127+
# Change the buffer to UCS2
2128+
writer.write_str('ucs2:\u20ac ')
2129+
# Change the buffer to UCS4
2130+
writer.write_str('ucs4:\U0010ffff')
2131+
self.assertEqual(writer.finish(),
2132+
'ascii latin1:\xe9 ucs2:\u20ac ucs4:\U0010ffff')
2133+
21032134

21042135
# Test PyUnicodeWriter_Format()
21052136
@unittest.skipIf(ctypes is None, 'need ctypes')

‎Objects/longobject.c‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2144,6 +2144,7 @@ long_to_decimal_string_internal(PyObject *aa,
21442144
Py_DECREF(scratch);
21452145
return -1;
21462146
}
2147+
assert(_PyUnicodeWriter_CanWrite(writer));
21472148
}
21482149
else if (bytes_writer) {
21492150
*bytes_str = _PyBytesWriter_Prepare(bytes_writer, *bytes_str, strlen);
@@ -2313,8 +2314,10 @@ long_format_binary(PyObject *aa, int base, int alternate,
23132314
}
23142315

23152316
if (writer) {
2316-
if (_PyUnicodeWriter_Prepare(writer, sz, 'x') == -1)
2317+
if (_PyUnicodeWriter_Prepare(writer, sz, 'x') == -1) {
23172318
return -1;
2319+
}
2320+
assert(_PyUnicodeWriter_CanWrite(writer));
23182321
}
23192322
else if (bytes_writer) {
23202323
*bytes_str = _PyBytesWriter_Prepare(bytes_writer, *bytes_str, sz);

‎Objects/unicodeobject.c‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,7 @@ NOTE: In the interpreter's initialization phase, some globals are currently
102102
103103
*/
104104

105-
// Maximum code point of Unicode 6.0: 0x10ffff (1,114,111).
106-
// The value must be the same in fileutils.c.
107-
#define MAX_UNICODE 0x10ffff
105+
#define MAX_UNICODE _Py_MAX_UNICODE
108106

109107
#ifdef Py_DEBUG
110108
# define _PyUnicode_CHECK(op) _PyUnicode_CheckConsistency(op, 0)
@@ -166,12 +164,6 @@ static inline void PyUnicode_SET_UTF8_LENGTH(PyObject *op, Py_ssize_t length)
166164
#define _PyUnicode_HASH(op) \
167165
(_PyASCIIObject_CAST(op)->hash)
168166

169-
static inline Py_hash_t PyUnicode_HASH(PyObject *op)
170-
{
171-
assert(_PyUnicode_CHECK(op));
172-
return FT_ATOMIC_LOAD_SSIZE_RELAXED(_PyASCIIObject_CAST(op)->hash);
173-
}
174-
175167
static inline void PyUnicode_SET_HASH(PyObject *op, Py_hash_t hash)
176168
{
177169
FT_ATOMIC_STORE_SSIZE_RELAXED(_PyASCIIObject_CAST(op)->hash, hash);
@@ -1841,18 +1833,21 @@ unicode_is_singleton(PyObject *unicode)
18411833
}
18421834
#endif
18431835

1836+
// If this function is updated, update also _PyUnicodeWriter_CanWrite().
18441837
static int
18451838
unicode_modifiable(PyObject *unicode)
18461839
{
18471840
assert(_PyUnicode_CHECK(unicode));
1841+
if (!PyUnicode_CheckExact(unicode))
1842+
return 0;
1843+
// On Free Threading, this test fails if called from a thread other
1844+
// than the one which created the str object.
18481845
if (!_PyObject_IsUniquelyReferenced(unicode))
18491846
return 0;
18501847
if (PyUnicode_HASH(unicode) != -1)
18511848
return 0;
18521849
if (PyUnicode_CHECK_INTERNED(unicode))
18531850
return 0;
1854-
if (!PyUnicode_CheckExact(unicode))
1855-
return 0;
18561851
#ifdef Py_DEBUG
18571852
/* singleton refcount is greater than 1 */
18581853
assert(!unicode_is_singleton(unicode));
@@ -2156,6 +2151,7 @@ PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *pub_writer,
21562151
if (_PyUnicodeWriter_Prepare(writer, size - num_surrogates, maxchar) < 0) {
21572152
return -1;
21582153
}
2154+
assert(_PyUnicodeWriter_CanWrite(writer));
21592155

21602156
int kind = writer->kind;
21612157
void *data = (Py_UCS1*)writer->data + writer->pos * kind;
@@ -2453,6 +2449,7 @@ PyUnicodeWriter_WriteUCS4(PyUnicodeWriter *pub_writer,
24532449
if (_PyUnicodeWriter_Prepare(writer, size, maxchar) < 0) {
24542450
return -1;
24552451
}
2452+
assert(_PyUnicodeWriter_CanWrite(writer));
24562453

24572454
int kind = writer->kind;
24582455
void *data = (Py_UCS1*)writer->data + writer->pos * kind;
@@ -2739,8 +2736,10 @@ unicode_fromformat_write_str(_PyUnicodeWriter *writer, PyObject *str,
27392736
else
27402737
maxchar = writer->maxchar;
27412738

2742-
if (_PyUnicodeWriter_Prepare(writer, arglen, maxchar) == -1)
2739+
if (_PyUnicodeWriter_Prepare(writer, arglen, maxchar) == -1) {
27432740
return -1;
2741+
}
2742+
assert(_PyUnicodeWriter_CanWrite(writer));
27442743

27452744
fill = Py_MAX(width - length, 0);
27462745
if (fill && !(flags & F_LJUST)) {
@@ -3030,8 +3029,10 @@ unicode_fromformat_arg(_PyUnicodeWriter *writer,
30303029
Py_ssize_t spacepad = Py_MAX(width - precision - sign, 0);
30313030
Py_ssize_t zeropad = Py_MAX(precision - len, 0);
30323031

3033-
if (_PyUnicodeWriter_Prepare(writer, width, 127) == -1)
3032+
if (_PyUnicodeWriter_Prepare(writer, width, 127) == -1) {
30343033
return NULL;
3034+
}
3035+
assert(_PyUnicodeWriter_CanWrite(writer));
30353036

30363037
if (spacepad && !(flags & F_LJUST)) {
30373038
if (PyUnicode_Fill(writer->buffer, writer->pos, spacepad, ' ') == -1)
@@ -5587,6 +5588,7 @@ unicode_decode_utf8_writer(_PyUnicodeWriter *writer,
55875588
if (_PyUnicodeWriter_Prepare(writer, size, 127) < 0) {
55885589
return -1;
55895590
}
5591+
assert(_PyUnicodeWriter_CanWrite(writer));
55905592

55915593
const char *starts = s;
55925594
const char *end = s + size;
@@ -13854,10 +13856,13 @@ void PyUnicodeWriter_Discard(PyUnicodeWriter *writer)
1385413856
static inline void
1385513857
_PyUnicodeWriter_InitWithBuffer(_PyUnicodeWriter *writer, PyObject *buffer)
1385613858
{
13859+
assert(PyUnstable_Object_IsUniquelyReferenced(buffer));
13860+
1385713861
memset(writer, 0, sizeof(*writer));
1385813862
writer->buffer = buffer;
1385913863
_PyUnicodeWriter_Update(writer);
1386013864
writer->min_length = writer->size;
13865+
assert(_PyUnicodeWriter_CanWrite(writer));
1386113866
}
1386213867

1386313868

@@ -13966,6 +13971,7 @@ _PyUnicodeWriter_WriteCharInline(_PyUnicodeWriter *writer, Py_UCS4 ch)
1396613971
assert(ch <= MAX_UNICODE);
1396713972
if (_PyUnicodeWriter_Prepare(writer, 1, ch) < 0)
1396813973
return -1;
13974+
assert(_PyUnicodeWriter_CanWrite(writer));
1396913975
PyUnicode_WRITE(writer->kind, writer->data, writer->pos, ch);
1397013976
writer->pos++;
1397113977
return 0;
@@ -14013,6 +14019,8 @@ _PyUnicodeWriter_WriteStr(_PyUnicodeWriter *writer, PyObject *str)
1401314019
if (_PyUnicodeWriter_PrepareInternal(writer, len, maxchar) == -1)
1401414020
return -1;
1401514021
}
14022+
14023+
assert(_PyUnicodeWriter_CanWrite(writer));
1401614024
_PyUnicode_FastCopyCharacters(writer->buffer, writer->pos,
1401714025
str, 0, len);
1401814026
writer->pos += len;
@@ -14090,6 +14098,7 @@ _PyUnicodeWriter_WriteSubstring(_PyUnicodeWriter *writer, PyObject *str,
1409014098
if (_PyUnicodeWriter_Prepare(writer, len, maxchar) < 0) {
1409114099
return -1;
1409214100
}
14101+
assert(_PyUnicodeWriter_CanWrite(writer));
1409314102

1409414103
_PyUnicode_FastCopyCharacters(writer->buffer, writer->pos,
1409514104
str, start, len);
@@ -14147,8 +14156,10 @@ _PyUnicodeWriter_WriteASCIIString(_PyUnicodeWriter *writer,
1414714156
return 0;
1414814157
}
1414914158

14150-
if (_PyUnicodeWriter_Prepare(writer, len, 127) == -1)
14159+
if (_PyUnicodeWriter_Prepare(writer, len, 127) == -1) {
1415114160
return -1;
14161+
}
14162+
assert(_PyUnicodeWriter_CanWrite(writer));
1415214163

1415314164
switch (writer->kind)
1415414165
{
@@ -14252,6 +14263,7 @@ _PyUnicodeWriter_WriteLatin1String(_PyUnicodeWriter *writer,
1425214263
maxchar = ucs1lib_find_max_char((const Py_UCS1*)str, (const Py_UCS1*)str + len);
1425314264
if (_PyUnicodeWriter_Prepare(writer, len, maxchar) == -1)
1425414265
return -1;
14266+
assert(_PyUnicodeWriter_CanWrite(writer));
1425514267
unicode_write_cstr(writer->buffer, writer->pos, str, len);
1425614268
writer->pos += len;
1425714269
return 0;

0 commit comments

Comments
 (0)