Skip to content

Commit 7c5f4eb

Browse files
committed
gh-158445: Detect invalid UCS4 characters in debug mode (#158502)
In debug mode, PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND) and PyUnicodeWriter_WriteUCS4() now raise a SystemError if a character is not in range [U+0000; U+10ffff], instead of creating an invalid str object. In release mode, PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND) raises a SystemError if the size is 1. * In debug mode, unicode_char() no longer fails with an assertion error if the character is invalid. Instead, raise SystemError. * Add tests on invalid UCS-4 characters. * Add _testinternalcapi._Py_MAX_UNICODE. * Add unicode_invalid_character() helper function. (cherry picked from commit a14a4a6)
1 parent df1a0fc commit 7c5f4eb

4 files changed

Lines changed: 134 additions & 30 deletions

File tree

‎Doc/c-api/unicode.rst‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,10 @@ APIs:
409409
the UCS1 range, it will be transformed into UCS1
410410
(:c:macro:`PyUnicode_1BYTE_KIND`).
411411
412+
All characters must be in range [U+0000; U+10ffff]. If *kind* is
413+
:c:macro:`PyUnicode_4BYTE_KIND` and the string contains invalid characters,
414+
the behavior is undefined.
415+
412416
.. versionadded:: 3.3
413417
414418
@@ -1868,10 +1872,13 @@ object.
18681872
18691873
.. c:function:: int PyUnicodeWriter_WriteUCS4(PyUnicodeWriter *writer, const Py_UCS4 *str, Py_ssize_t size)
18701874
1871-
Writer the UCS4 string *str* into *writer*.
1875+
Write the UCS4 string *str* into *writer*.
18721876
18731877
*size* is a number of UCS4 characters.
18741878
1879+
All characters must be in range [U+0000; U+10ffff]. If the string contains
1880+
invalid characters, the behavior is undefined.
1881+
18751882
On success, return ``0``.
18761883
On error, set an exception, leave the writer unchanged, and return ``-1``.
18771884

‎Lib/test/test_capi/test_unicode.py‎

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,23 @@
2222
ctypes = None
2323

2424

25+
MAX_UNICODE = _testinternalcapi._Py_MAX_UNICODE
26+
# The first invalid character after MAX_UNICODE
27+
INVALID_CHAR = MAX_UNICODE + 1
28+
# Maximum invalid character which fits into 32-bit Py_UCS4
29+
MAX_INVALID_CHAR = 0xFFFF_FFFF
2530
NULL = None
2631

2732
class Str(str):
2833
pass
2934

3035

36+
def assert_invalid_string(testcase, text):
37+
# Check that a Unicode string contains invalid characters:
38+
# not in range [U+0000; U+10ffff]
39+
testcase.assertRaises(SystemError, list, text)
40+
41+
3142
class CAPITest(unittest.TestCase):
3243

3344
@support.cpython_only
@@ -40,14 +51,14 @@ def test_new(self):
4051
self.assertEqual(new(0, maxchar), '')
4152
self.assertEqual(new(5, maxchar), chr(maxchar)*5)
4253
self.assertRaises(MemoryError, new, PY_SSIZE_T_MAX, maxchar)
43-
self.assertEqual(new(0, 0x110000), '')
54+
self.assertEqual(new(0, INVALID_CHAR), '')
4455
self.assertRaises(MemoryError, new, PY_SSIZE_T_MAX//2, 0x4f60)
4556
self.assertRaises(MemoryError, new, PY_SSIZE_T_MAX//2+1, 0x4f60)
4657
self.assertRaises(MemoryError, new, PY_SSIZE_T_MAX//2, 0x1f600)
4758
self.assertRaises(MemoryError, new, PY_SSIZE_T_MAX//2+1, 0x1f600)
4859
self.assertRaises(MemoryError, new, PY_SSIZE_T_MAX//4, 0x1f600)
4960
self.assertRaises(MemoryError, new, PY_SSIZE_T_MAX//4+1, 0x1f600)
50-
self.assertRaises(SystemError, new, 5, 0x110000)
61+
self.assertRaises(SystemError, new, 5, INVALID_CHAR)
5162
self.assertRaises(SystemError, new, -1, 0)
5263
self.assertRaises(SystemError, new, PY_SSIZE_T_MIN, 0)
5364

@@ -84,7 +95,7 @@ def test_fill(self):
8495
s = strings[0]
8596
self.assertRaises(IndexError, fill, s, -1, 0, 0x78)
8697
self.assertRaises(IndexError, fill, s, PY_SSIZE_T_MIN, 0, 0x78)
87-
self.assertRaises(ValueError, fill, s, 0, 0, 0x110000)
98+
self.assertRaises(ValueError, fill, s, 0, 0, INVALID_CHAR)
8899
self.assertRaises(SystemError, fill, b'abc', 0, 0, 0x78)
89100
self.assertRaises(SystemError, fill, [], 0, 0, 0x78)
90101
# CRASHES fill(s, 0, NULL, 0, 0)
@@ -103,7 +114,7 @@ def test_writechar(self):
103114
'\U0001f600\U0001f601\U0001f602'
104115
]
105116
# one character for every kind + out of range code
106-
chars = [0x78, 0xa9, 0x20ac, 0x1f638, 0x110000]
117+
chars = [0x78, 0xa9, 0x20ac, 0x1f638, INVALID_CHAR]
107118
for i, s in enumerate(strings):
108119
for j, c in enumerate(chars):
109120
if j <= i:
@@ -278,7 +289,22 @@ def test_fromkindanddata(self):
278289
self.assertRaises(ValueError, fromkindanddata, 1, NULL, -1)
279290
self.assertRaises(ValueError, fromkindanddata, 1, NULL, PY_SSIZE_T_MIN)
280291
# CRASHES fromkindanddata(1, NULL, 1)
281-
# CRASHES fromkindanddata(4, b'\xff\xff\xff\xff')
292+
293+
# Test invalid UCS-4 string. Create an invalid string in release mode,
294+
# or raise SystemError in debug mode.
295+
for invalid_char in (INVALID_CHAR, MAX_INVALID_CHAR):
296+
with self.subTest(invalid_char=invalid_char):
297+
# Test single character
298+
ucs4_char = invalid_char.to_bytes(4, byteorder=sys.byteorder)
299+
self.assertRaises(SystemError, fromkindanddata, 4, ucs4_char)
300+
301+
# Test multiple characters
302+
s = 'valid'.encode(enc4) + ucs4_char
303+
if support.Py_DEBUG:
304+
self.assertRaises(SystemError, fromkindanddata, 4, s)
305+
else:
306+
result = fromkindanddata(4, s)
307+
assert_invalid_string(self, result)
282308

283309
@support.cpython_only
284310
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
@@ -401,7 +427,7 @@ def check_format(expected, format, *args):
401427
check_format('\U0010ffff',
402428
b'%c', c_int(0x10ffff))
403429
with self.assertRaises(OverflowError):
404-
PyUnicode_FromFormat(b'%c', c_int(0x110000))
430+
PyUnicode_FromFormat(b'%c', c_int(INVALID_CHAR))
405431
# Issue #18183
406432
check_format('\U00010000\U00100000',
407433
b'%c%c', c_int(0x10000), c_int(0x100000))
@@ -988,7 +1014,7 @@ def test_fromordinal(self):
9881014
self.assertEqual(fromordinal(0x20ac), '\u20ac')
9891015
self.assertEqual(fromordinal(0x1f600), '\U0001f600')
9901016

991-
self.assertRaises(ValueError, fromordinal, 0x110000)
1017+
self.assertRaises(ValueError, fromordinal, INVALID_CHAR)
9921018
self.assertRaises(ValueError, fromordinal, -1)
9931019

9941020
@support.cpython_only
@@ -1374,8 +1400,8 @@ def test_findchar(self):
13741400
self.assertEqual(unicode_findchar(str, ord(ch), 0, len(str), -1), i)
13751401

13761402
str = "!>_<!"
1377-
self.assertEqual(unicode_findchar(str, 0x110000, 0, len(str), 1), -1)
1378-
self.assertEqual(unicode_findchar(str, 0x110000, 0, len(str), -1), -1)
1403+
self.assertEqual(unicode_findchar(str, INVALID_CHAR, 0, len(str), 1), -1)
1404+
self.assertEqual(unicode_findchar(str, INVALID_CHAR, 0, len(str), -1), -1)
13791405
# start < end
13801406
self.assertEqual(unicode_findchar(str, ord('!'), 1, len(str)+1, 1), 4)
13811407
self.assertEqual(unicode_findchar(str, ord('!'), 1, PY_SSIZE_T_MAX, 1), 4)
@@ -1796,8 +1822,8 @@ def test_write_char(self):
17961822
writer.write_char(ord('$'))
17971823
writer.write_char(0x20ac)
17981824
writer.write_char(0x10_ffff)
1799-
self.assertRaises(ValueError, writer.write_char, 0x11_0000)
1800-
self.assertRaises(ValueError, writer.write_char, 0xFFFF_FFFF)
1825+
self.assertRaises(ValueError, writer.write_char, INVALID_CHAR)
1826+
self.assertRaises(ValueError, writer.write_char, MAX_INVALID_CHAR)
18011827
self.assertEqual(writer.finish(),
18021828
"\0$\u20AC\U0010FFFF")
18031829

@@ -1961,15 +1987,37 @@ def test_ucs4(self):
19611987
writer.write_ucs4("pair\uD83D\uDC0D".encode(encoding, 'surrogatepass'))
19621988
writer.write_char(ord("-"))
19631989
writer.write_ucs4("null[\0]".encode(encoding), 7)
1964-
invalid = (b'\x00\x00\x11\x00' if sys.byteorder == 'little' else
1965-
b'\x00\x11\x00\x00')
1966-
# CRASHES writer.write_ucs4("invalid".encode(encoding) + invalid)
19671990
writer.write_ucs4(NULL, 0)
19681991
# CRASHES writer.write_ucs4(NULL, 1)
19691992
self.assertEqual(writer.finish(),
19701993
"lone\udc80-pair\ud83d\udc0d-null[\x00]")
19711994

1972-
# invalid size
1995+
# Invalid UCS-4 characters. Create an invalid string in release mode,
1996+
# or raise SystemError in debug mode.
1997+
writer = self.create_writer(0)
1998+
for invalid_char in (INVALID_CHAR, MAX_INVALID_CHAR):
1999+
with self.subTest(invalid_char=invalid_char):
2000+
# Test single character
2001+
ucs4_char = invalid_char.to_bytes(4, byteorder=sys.byteorder)
2002+
if support.Py_DEBUG:
2003+
self.assertRaises(SystemError, writer.write_ucs4, ucs4_char)
2004+
else:
2005+
writer.write_ucs4(ucs4_char)
2006+
2007+
# Test multiple characters
2008+
s = 'valid'.encode(encoding) + ucs4_char
2009+
if support.Py_DEBUG:
2010+
self.assertRaises(SystemError, writer.write_ucs4, s)
2011+
else:
2012+
writer.write_ucs4(s)
2013+
2014+
if support.Py_DEBUG:
2015+
self.assertEqual(writer.finish(), '')
2016+
else:
2017+
result = writer.finish()
2018+
assert_invalid_string(self, result)
2019+
2020+
# Invalid size
19732021
writer = self.create_writer(0)
19742022
with self.assertRaises(ValueError):
19752023
writer.write_ucs4("text".encode(encoding), -1)

‎Modules/_testinternalcapi.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,6 +3500,10 @@ module_exec(PyObject *module)
35003500
}
35013501
PyModule_AddObject(module, "SelfInterruptingContextManager", (PyObject *)&SelfInterruptingContextManager_Type);
35023502

3503+
if (PyModule_AddIntMacro(module, _Py_MAX_UNICODE) < 0) {
3504+
return 1;
3505+
}
3506+
35033507
return 0;
35043508
}
35053509

‎Objects/unicodeobject.c‎

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,16 @@ PyUnicode_CopyCharacters(PyObject *to, Py_ssize_t to_start,
15831583
return how_many;
15841584
}
15851585

1586+
1587+
static void
1588+
unicode_invalid_character(PyObject *exc, Py_UCS4 ch)
1589+
{
1590+
PyErr_Format(exc,
1591+
"character U+%x is not in range [U+0000; U+%x]",
1592+
ch, MAX_UNICODE);
1593+
}
1594+
1595+
15861596
/* Find the maximum code point and count the number of surrogate pairs so a
15871597
correct string length can be computed before converting a string to UCS4.
15881598
This function counts single surrogates as a character and not as a pair.
@@ -1618,9 +1628,7 @@ find_maxchar_surrogates(const wchar_t *begin, const wchar_t *end,
16181628
if (ch > *maxchar) {
16191629
*maxchar = ch;
16201630
if (*maxchar > MAX_UNICODE) {
1621-
PyErr_Format(PyExc_ValueError,
1622-
"character U+%x is not in range [U+0000; U+%x]",
1623-
ch, MAX_UNICODE);
1631+
unicode_invalid_character(PyExc_ValueError, ch);
16241632
return -1;
16251633
}
16261634
}
@@ -1822,17 +1830,15 @@ get_latin1_char(Py_UCS1 ch)
18221830
static PyObject*
18231831
unicode_char(Py_UCS4 ch)
18241832
{
1825-
PyObject *unicode;
1826-
1827-
assert(ch <= MAX_UNICODE);
1828-
18291833
if (ch < 256) {
18301834
return get_latin1_char(ch);
18311835
}
18321836

1833-
unicode = PyUnicode_New(1, ch);
1834-
if (unicode == NULL)
1837+
// Raise SystemError if the character is not in range [U+0000; MAX_UNICODE]
1838+
PyObject *unicode = PyUnicode_New(1, ch);
1839+
if (unicode == NULL) {
18351840
return NULL;
1841+
}
18361842

18371843
assert(PyUnicode_KIND(unicode) != PyUnicode_1BYTE_KIND);
18381844
if (PyUnicode_KIND(unicode) == PyUnicode_2BYTE_KIND) {
@@ -2211,15 +2217,36 @@ static PyObject*
22112217
_PyUnicode_FromUCS4(const Py_UCS4 *u, Py_ssize_t size)
22122218
{
22132219
PyObject *res;
2214-
Py_UCS4 max_char;
22152220

22162221
if (size == 0)
22172222
_Py_RETURN_UNICODE_EMPTY();
22182223
assert(size > 0);
2219-
if (size == 1)
2224+
2225+
if (size == 1) {
2226+
// Raise SystemError if the character is invalid
22202227
return unicode_char(u[0]);
2228+
}
2229+
2230+
#ifdef Py_DEBUG
2231+
// Check for invalid characters in debug mode
2232+
Py_UCS4 max_char = 127;
2233+
for (Py_ssize_t i = 0; i < size; i++) {
2234+
Py_UCS4 ch = u[i];
2235+
if (ch > max_char) {
2236+
if (ch > MAX_UNICODE) {
2237+
unicode_invalid_character(PyExc_SystemError, ch);
2238+
return NULL;
2239+
}
2240+
max_char = ch;
2241+
}
2242+
}
2243+
#else
2244+
// gh-158445: Return MAX_UNICODE even if the string contains invalid
2245+
// characters. Checking for invalid characters would make the function
2246+
// slower whereas it's unlikely in practice.
2247+
Py_UCS4 max_char = ucs4lib_find_max_char(u, u + size);
2248+
#endif
22212249

2222-
max_char = ucs4lib_find_max_char(u, u + size);
22232250
res = PyUnicode_New(size, max_char);
22242251
if (!res)
22252252
return NULL;
@@ -2253,9 +2280,27 @@ PyUnicodeWriter_WriteUCS4(PyUnicodeWriter *pub_writer,
22532280
return 0;
22542281
}
22552282

2256-
Py_UCS4 max_char = ucs4lib_find_max_char(str, str + size);
2283+
#ifdef Py_DEBUG
2284+
// Check for invalid characters in debug mode
2285+
Py_UCS4 maxchar = 127;
2286+
for (Py_ssize_t i = 0; i < size; i++) {
2287+
Py_UCS4 ch = str[i];
2288+
if (ch > maxchar) {
2289+
if (ch > MAX_UNICODE) {
2290+
unicode_invalid_character(PyExc_SystemError, ch);
2291+
return -1;
2292+
}
2293+
maxchar = ch;
2294+
}
2295+
}
2296+
#else
2297+
// gh-158445: Return MAX_UNICODE even if the string contains invalid
2298+
// characters. Checking for invalid characters would make the function
2299+
// slower whereas it's unlikely in practice.
2300+
Py_UCS4 maxchar = ucs4lib_find_max_char(str, str + size);
2301+
#endif
22572302

2258-
if (_PyUnicodeWriter_Prepare(writer, size, max_char) < 0) {
2303+
if (_PyUnicodeWriter_Prepare(writer, size, maxchar) < 0) {
22592304
return -1;
22602305
}
22612306

0 commit comments

Comments
 (0)