Skip to content

Commit 3343af0

Browse files
committed
gh-159672: Constrain literal conversion during keyword replay
1 parent fd259d2 commit 3343af0

6 files changed

Lines changed: 107 additions & 4 deletions

File tree

‎Include/internal/pycore_unicodeobject.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,11 @@ PyAPI_FUNC(PyObject*) _PyUnicode_DecodeUnicodeEscapeInternal2(
337337
int *first_invalid_escape_char, /* on return, if not -1, contain the first
338338
invalid escaped char (<= 0xff) or invalid
339339
octal escape (> 0xff) in string. */
340-
const char **first_invalid_escape_ptr); /* on return, if not NULL, may
340+
const char **first_invalid_escape_ptr, /* on return, if not NULL, may
341341
point to the first invalid escaped
342342
char in string.
343343
May be NULL if errors is not NULL. */
344+
bool allow_callbacks); /* Allow named escapes and registered error handlers. */
344345

345346
/* --- Raw-Unicode-Escape Codecs ---------------------------------------------- */
346347

‎Lib/test/test_traceback.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,75 @@ def test_keyword_replay_respects_feature_version(self):
19431943
ast.parse(source, feature_version=(3, 9))
19441944
self.assertIsNone(caught.exception._metadata)
19451945

1946+
@cpython_only
1947+
def test_keyword_trials_reject_invalid_literals(self):
1948+
for literal in ("b'é'", r"'\xzz'", r"'\U00110000'"):
1949+
for mode in ("exec", "eval", "single"):
1950+
source = "npt " + literal
1951+
with self.subTest(source=source, mode=mode):
1952+
with self.assertRaises(SyntaxError) as caught:
1953+
compile(source, "<test>", mode)
1954+
self.assertIsNone(caught.exception._metadata)
1955+
1956+
@cpython_only
1957+
@requires_subprocess()
1958+
def test_keyword_trials_do_not_call_literal_hooks(self):
1959+
assert_python_ok('-c', textwrap.dedent(r'''
1960+
import codecs, sys, _pylong
1961+
calls = []
1962+
1963+
def forbidden(*args):
1964+
calls.append(args)
1965+
raise AssertionError("literal callback")
1966+
1967+
class WatchImports:
1968+
def find_spec(self, fullname, path=None, target=None):
1969+
if fullname == "unicodedata":
1970+
forbidden(fullname)
1971+
1972+
sys.meta_path.insert(0, WatchImports())
1973+
codecs.register_error("strict", forbidden)
1974+
_pylong.int_from_string = forbidden
1975+
sys.set_int_max_str_digits(0)
1976+
literals = [(r"'\N{SNOWMAN}'", False), (r"'\xzz'", False),
1977+
(r"'\U00110000'", False), (r"f'\xzz'", False),
1978+
(r"t'\xzz'", False), ("1" * 6001, False),
1979+
(r"'\x61\u0062\U00000063'", True),
1980+
(r"r'\N{SNOWMAN}'", True), (r"'\\N{SNOWMAN}'", True),
1981+
(r"f'\x61{1}'", True), (r"t'\x61{1}'", True)]
1982+
for literal, expected in literals:
1983+
try:
1984+
compile("impor math\nx = " + literal, "<test>", "exec")
1985+
except SyntaxError as error:
1986+
assert (error._metadata is not None) == expected, literal
1987+
else:
1988+
raise AssertionError(literal)
1989+
assert calls == [], calls
1990+
# A cached Unicode name table must not change the trial policy.
1991+
sys.meta_path.pop(0)
1992+
compile(r"x = '\N{SNOWMAN}'", "<test>", "exec")
1993+
try:
1994+
compile("impor math\nx = " + literals[0][0], "<test>", "exec")
1995+
except SyntaxError as error:
1996+
assert error._metadata is None
1997+
# The normal decoder must still honor registered error handlers.
1998+
try:
1999+
br"\xzz".decode("unicode_escape")
2000+
except AssertionError:
2001+
pass
2002+
assert len(calls) == 1, calls
2003+
'''))
2004+
2005+
@cpython_only
2006+
def test_keyword_trials_numeric_limit(self):
2007+
for digits, expected in ((640, True), (641, False)):
2008+
with self.subTest(digits=digits):
2009+
source = "x = " + "1" * digits
2010+
compile(source, "<test>", "exec")
2011+
with self.assertRaises(SyntaxError) as caught:
2012+
compile("impor math\n" + source, "<test>", "exec")
2013+
self.assertEqual(caught.exception._metadata is not None, expected)
2014+
19462015
@cpython_only
19472016
def test_keyword_trials_do_not_call_normalizer(self):
19482017
with unittest.mock.patch("unicodedata.normalize") as normalize:

‎Objects/unicodeobject.c‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6548,7 +6548,8 @@ _PyUnicode_DecodeUnicodeEscapeInternal2(const char *s,
65486548
const char *errors,
65496549
Py_ssize_t *consumed,
65506550
int *first_invalid_escape_char,
6551-
const char **first_invalid_escape_ptr)
6551+
const char **first_invalid_escape_ptr,
6552+
bool allow_callbacks)
65526553
{
65536554
const char *starts = s;
65546555
const char *initial_starts = starts;
@@ -6708,6 +6709,10 @@ _PyUnicode_DecodeUnicodeEscapeInternal2(const char *s,
67086709

67096710
/* \N{name} */
67106711
case 'N':
6712+
if (!allow_callbacks) {
6713+
message = "named Unicode escapes are disabled";
6714+
goto error;
6715+
}
67116716
ucnhash_capi = _PyUnicode_GetNameCAPI();
67126717
if (ucnhash_capi == NULL) {
67136718
PyErr_SetString(
@@ -6767,6 +6772,14 @@ _PyUnicode_DecodeUnicodeEscapeInternal2(const char *s,
67676772
}
67686773
error:;
67696774
Py_ssize_t endinpos = s-starts;
6775+
if (!allow_callbacks) {
6776+
make_decode_exception(&exc, "unicodeescape", starts, end - starts,
6777+
startinpos, endinpos, message);
6778+
if (exc != NULL) {
6779+
PyErr_SetObject(PyExc_UnicodeDecodeError, exc);
6780+
}
6781+
goto onError;
6782+
}
67706783
writer.min_length = end - s + writer.pos;
67716784
if (unicode_decode_call_errorhandler_writer(
67726785
errors, &errorHandler,
@@ -6803,7 +6816,8 @@ _PyUnicode_DecodeUnicodeEscapeStateful(const char *s,
68036816
PyObject *result = _PyUnicode_DecodeUnicodeEscapeInternal2(s, size, errors,
68046817
consumed,
68056818
&first_invalid_escape_char,
6806-
&first_invalid_escape_ptr);
6819+
&first_invalid_escape_ptr,
6820+
true);
68076821
if (result == NULL)
68086822
return NULL;
68096823
if (first_invalid_escape_char != -1) {

‎Parser/pegen.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <Python.h>
22
#include "pycore_ast.h" // _PyAST_Validate(),
3+
#include "pycore_long.h" // _PY_LONG_MAX_STR_DIGITS_THRESHOLD
34
#include "pycore_pystate.h" // _PyThreadState_GET()
45
#include "pycore_parser.h" // _PYPEGEN_NSTATISTICS
56
#include "pycore_pyerrors.h" // PyExc_IncompleteInputError
@@ -1422,6 +1423,15 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
14221423
{
14231424
return NULL;
14241425
}
1426+
// Conservatively bound numeric conversion before any trial. This also
1427+
// excludes the Python helper without changing normal number parsing.
1428+
for (int i = 0; i < p->fill; i++) {
1429+
Token *token = p->tokens[i];
1430+
if (token->type == NUMBER &&
1431+
PyBytes_GET_SIZE(token->bytes) > _PY_LONG_MAX_STR_DIGITS_THRESHOLD) {
1432+
return NULL;
1433+
}
1434+
}
14251435
KeywordTypo candidates[MAX_KEYWORD_TYPO_CANDIDATES];
14261436
int count = collect_keyword_typos(p, error, candidates);
14271437
if (count <= 0) {

‎Parser/pegen_errors.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ _Pypegen_tokenizer_error(Parser *p)
104104
int
105105
_Pypegen_raise_decode_error(Parser *p)
106106
{
107+
if (p->keyword_trial != NULL) {
108+
// A trial can reject a literal without creating a syntax exception.
109+
p->error_indicator = 1;
110+
return -1;
111+
}
107112
assert(PyErr_Occurred());
108113
const char *errtype = NULL;
109114
if (PyErr_ExceptionMatches(PyExc_UnicodeError)) {

‎Parser/string_parser.c‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,11 @@ decode_unicode_with_escapes(Parser *parser, const char *s, size_t len, Token *t)
195195
const char *first_invalid_escape_ptr;
196196
v = _PyUnicode_DecodeUnicodeEscapeInternal2(buf, (Py_ssize_t)len, NULL, NULL,
197197
&first_invalid_escape_char,
198-
&first_invalid_escape_ptr);
198+
&first_invalid_escape_ptr,
199+
parser->keyword_trial == NULL);
200+
if (v == NULL && parser->keyword_trial != NULL) {
201+
parser->error_indicator = 1;
202+
}
199203

200204
// HACK: later we can simply pass the line no, since we don't preserve the tokens
201205
// when we are decoding the string but we preserve the line numbers.

0 commit comments

Comments
 (0)