Skip to content

Commit 3179030

Browse files
authored
[mypyc] Fix handling of invalid codepoint values in librt.strings (#21634)
`isidentifier`, `tolower` and `toupper` would terminate the process if passed a too high Unicode codepoint value. Fix so that all the `is*` functions return false for invalid codepoints and `tolower`/`toupper` functions return them unchanged. I used coding agent assist.
1 parent 1b206eb commit 3179030

4 files changed

Lines changed: 46 additions & 18 deletions

File tree

mypy/typeshed/stubs/librt/librt/strings.pyi

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def read_f64_le(b: bytes, index: i64, /) -> float: ...
4242
def read_f64_be(b: bytes, index: i64, /) -> float: ...
4343

4444
# Codepoint classification helpers operating on i32 codepoints (typically
45-
# obtained via ord(s[i])). Negative inputs return False.
45+
# obtained via ord(s[i])). Out-of-range inputs (negative, or past the maximum
46+
# Unicode code point 0x10FFFF) return False.
4647
def isspace(c: i32, /) -> bool: ...
4748
def isdigit(c: i32, /) -> bool: ...
4849
def isalnum(c: i32, /) -> bool: ...
@@ -53,7 +54,8 @@ def isidentifier(c: i32, /) -> bool: ...
5354
# uppercase / lowercase expands to multiple codepoints (e.g. U+00DF
5455
# uppercases to "SS", U+FB01 to "FI"), returns the input unchanged so
5556
# the signature stays i32 -> i32. Use str.upper() / str.lower() for full
56-
# Unicode case conversion when those cases matter. Negative inputs are
57-
# returned unchanged.
57+
# Unicode case conversion when those cases matter. Out-of-range inputs
58+
# (negative, or past the maximum Unicode code point 0x10FFFF) are returned
59+
# unchanged.
5860
def toupper(c: i32, /) -> i32: ...
5961
def tolower(c: i32, /) -> i32: ...

mypyc/build.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ class ModDesc(NamedTuple):
5454

5555
LIBRT_MODULES = [
5656
ModDesc("librt.internal", ["internal/librt_internal.c"], [], ["internal"]),
57-
ModDesc("librt.strings", ["strings/librt_strings.c"], [], ["strings"]),
57+
ModDesc(
58+
"librt.strings",
59+
["strings/librt_strings.c"],
60+
["strings/librt_strings.h", "strings/librt_strings_common.h"],
61+
["strings"],
62+
),
5863
ModDesc(
5964
"librt.base64",
6065
[

mypyc/lib-rt/strings/librt_strings.h

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ typedef struct {
3131
} StringWriterObject;
3232

3333
// Codepoint classification helpers. Inputs are signed i32 for compatibility
34-
// with mypyc's int32_rprimitive; negative values are non-codepoints and
35-
// return false. Defined `static inline` so they compile statically into
34+
// with mypyc's int32_rprimitive; out-of-range values (negative, or past the
35+
// maximum Unicode code point 0x10FFFF) are non-codepoints and return false.
36+
// Defined `static inline` so they compile statically into
3637
// both the librt.strings module and any mypyc-compiled extension that
3738
// includes this header, avoiding the capsule indirection that would dwarf
3839
// the work of a single Py_UNICODE_IS* macro call.
@@ -58,12 +59,14 @@ static inline bool LibRTStrings_IsAlpha(int32_t c) {
5859
// PyUnicode_IsIdentifier on a 1-character string. Aborts via
5960
// CPyError_OutOfMemory on allocation failure to keep this ERR_NEVER.
6061
static inline bool LibRTStrings_IsIdentifier(int32_t c) {
61-
if (c < 0) return false;
62-
if (c < 128) {
62+
// Unsigned compare: negatives wrap to large values and skip the fast path.
63+
if ((uint32_t)c < 128) {
6364
return (c >= 'a' && c <= 'z')
6465
|| (c >= 'A' && c <= 'Z')
6566
|| c == '_';
6667
}
68+
// Reject negatives and code points past the Unicode maximum.
69+
if ((uint32_t)c > 0x10FFFF) return false;
6770
PyObject *s = PyUnicode_FromOrdinal((int)c);
6871
if (s == NULL) {
6972
CPyError_OutOfMemory();
@@ -101,19 +104,27 @@ static inline int32_t LibRTStrings_ChangeCase_slow(int32_t c, const char *method
101104
// non-ASCII delegates to str.upper on a 1-character string. Returns the
102105
// input unchanged when uppercasing expands to multiple codepoints.
103106
static inline int32_t LibRTStrings_ToUpper(int32_t c) {
104-
if (c < 0) return c;
105-
if (c >= 'a' && c <= 'z') return c - 32;
106-
if (c < 128) return c;
107+
// Unsigned compare: negatives wrap to large values and skip the fast path.
108+
if ((uint32_t)c < 128) {
109+
if (c >= 'a' && c <= 'z') return c - 32;
110+
return c;
111+
}
112+
// Negatives and code points past the Unicode maximum are returned unchanged.
113+
if ((uint32_t)c > 0x10FFFF) return c;
107114
return LibRTStrings_ChangeCase_slow(c, "upper");
108115
}
109116

110117
// Lowercase a codepoint. ASCII fast path is `A..Z -> a..z` (add 32);
111118
// non-ASCII delegates to str.lower on a 1-character string. Returns the
112119
// input unchanged when lowercasing expands to multiple codepoints.
113120
static inline int32_t LibRTStrings_ToLower(int32_t c) {
114-
if (c < 0) return c;
115-
if (c >= 'A' && c <= 'Z') return c + 32;
116-
if (c < 128) return c;
121+
// Unsigned compare: negatives wrap to large values and skip the fast path.
122+
if ((uint32_t)c < 128) {
123+
if (c >= 'A' && c <= 'Z') return c + 32;
124+
return c;
125+
}
126+
// Negatives and code points past the Unicode maximum are returned unchanged.
127+
if ((uint32_t)c > 0x10FFFF) return c;
117128
return LibRTStrings_ChangeCase_slow(c, "lower");
118129
}
119130

mypyc/test-data/run-librt-strings.test

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,8 +1449,9 @@ from testutil import assertRaises
14491449

14501450

14511451
def test_codepoint_classifiers() -> None:
1452-
# Negative values are not codepoints.
1453-
for bad in (i32(-1), i32(-113)):
1452+
# Out-of-range values are not codepoints: negative, just past the maximum
1453+
# valid code point (0x10FFFF), and the largest i32.
1454+
for bad in (i32(-1), i32(-113), i32(0x110000), i32(0x7FFFFFFF)):
14541455
assert not isspace(bad)
14551456
assert not isdigit(bad)
14561457
assert not isalnum(bad)
@@ -1485,6 +1486,10 @@ def test_codepoint_classifiers_via_any() -> None:
14851486
assert f(ord(false_input)) is False
14861487
# Negative values are valid i32, just not codepoints.
14871488
assert f(-1) is False
1489+
# Values within i32 range but past the maximum code point (0x10FFFF)
1490+
# are not codepoints either.
1491+
assert f(0x110000) is False
1492+
assert f(0x7FFFFFFF) is False
14881493
# Inputs outside i32 range raise OverflowError through the wrapper.
14891494
with assertRaises(OverflowError, "codepoint out of i32 range"):
14901495
f(1 << 40)
@@ -1509,8 +1514,9 @@ def _expect(c: str, method: str) -> int:
15091514

15101515

15111516
def test_codepoint_case_conversion() -> None:
1512-
# Negative inputs return unchanged.
1513-
for bad in (i32(-1), i32(-113)):
1517+
# Out-of-range inputs return unchanged: negative, just past the maximum
1518+
# valid code point (0x10FFFF), and the largest i32.
1519+
for bad in (i32(-1), i32(-113), i32(0x110000), i32(0x7FFFFFFF)):
15141520
assert toupper(bad) == bad
15151521
assert tolower(bad) == bad
15161522
# Agree with str.upper / str.lower across the full Unicode range
@@ -1534,6 +1540,10 @@ def test_codepoint_case_conversion_via_any() -> None:
15341540
assert f(in_cp) == out_cp
15351541
# Negative values are valid i32, returned unchanged.
15361542
assert f(-1) == -1
1543+
# Values within i32 range but past the maximum code point (0x10FFFF)
1544+
# are returned unchanged.
1545+
assert f(0x110000) == 0x110000
1546+
assert f(0x7FFFFFFF) == 0x7FFFFFFF
15371547
# Inputs outside i32 range raise OverflowError through the wrapper.
15381548
with assertRaises(OverflowError, "codepoint out of i32 range"):
15391549
f(1 << 40)

0 commit comments

Comments
 (0)