From 7c90e20a2c2db0a1941df16fcbfeac3bd7a35b96 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 19 Dec 2025 07:09:11 +0300 Subject: [PATCH 1/9] gh-141778: add missing validation in ast.literal_eval() for non-string input This also changes parsing of the private `__text_signature__` attribute by inspect.signature(). Now we accept here only types, valid for ast.Constant(). --- Lib/ast.py | 13 +++++++++++-- Lib/inspect.py | 3 ++- Lib/test/test_ast/test_ast.py | 4 ++++ Lib/test/test_inspect/test_inspect.py | 4 +++- .../2025-12-19-07-09-02.gh-issue-141778.VdSWcy.rst | 2 ++ 5 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-19-07-09-02.gh-issue-141778.VdSWcy.rst diff --git a/Lib/ast.py b/Lib/ast.py index d9743ba7ab40b12..4badca2eabc76c0 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -59,17 +59,26 @@ def literal_eval(node_or_string): """ if isinstance(node_or_string, str): node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval').body + return _convert_literal(node_or_string, True) elif isinstance(node_or_string, Expression): node_or_string = node_or_string.body return _convert_literal(node_or_string) -def _convert_literal(node): +_type_None = type(None) +_type_Ellipsis = type(...) + + +def _convert_literal(node, omit_validation=False): """ Used by `literal_eval` to convert an AST node into a value. """ if isinstance(node, Constant): - return node.value + if omit_validation: + return node.value + if type(value := node.value) in (str, bytes, int, float, complex, + bool, _type_None, _type_Ellipsis): + return value if isinstance(node, Dict) and len(node.keys) == len(node.values): return dict(zip( map(_convert_literal, node.keys), diff --git a/Lib/inspect.py b/Lib/inspect.py index 07c4e28f0d9952f..93c05a12c07bb5f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2216,7 +2216,8 @@ def wrap_value(s): except NameError: raise ValueError - if isinstance(value, (str, int, float, bytes, bool, type(None))): + if type(value) in (str, int, float, bytes, bool, complex, + type(None), type(...)): return ast.Constant(value) raise ValueError diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index d2b76b46dbe2eba..7bbdfc63eb424cc 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -1890,6 +1890,10 @@ def test_literal_eval(self): self.assertRaises(ValueError, ast.literal_eval, '++6') self.assertRaises(ValueError, ast.literal_eval, '+True') self.assertRaises(ValueError, ast.literal_eval, '2+3') + # gh-141778: reject values of invalid types + node = ast.Expression(body=ast.Constant(object())) + ast.fix_missing_locations(node) + self.assertRaises(ValueError, ast.literal_eval, node) def test_literal_eval_str_int_limit(self): with support.adjust_int_max_str_digits(4000): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 075e1802bebc3e5..97ee587b74d5cb9 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6286,7 +6286,9 @@ def test_threading_module_has_signatures(self): def test_thread_module_has_signatures(self): import _thread no_signature = {'RLock'} - self._test_module_has_signatures(_thread, no_signature) + unsupported_signature = {'interrupt_main'} + self._test_module_has_signatures(_thread, no_signature, + unsupported_signature) def test_time_module_has_signatures(self): no_signature = { diff --git a/Misc/NEWS.d/next/Library/2025-12-19-07-09-02.gh-issue-141778.VdSWcy.rst b/Misc/NEWS.d/next/Library/2025-12-19-07-09-02.gh-issue-141778.VdSWcy.rst new file mode 100644 index 000000000000000..77257f65619a06a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-19-07-09-02.gh-issue-141778.VdSWcy.rst @@ -0,0 +1,2 @@ +Validate value types of :class:`ast.Constant` nodes in the +:func:`ast.literal_eval`. Patch by Sergey B Kirpichev. From e3c4a80229bcdd8547bedd42498948ae37be9fd1 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sun, 24 May 2026 11:56:16 +0300 Subject: [PATCH 2/9] revert unrelated change --- Lib/inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 8a51ce3180fcee2..af6aa3eb37a53bb 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2208,7 +2208,7 @@ def wrap_value(s): raise ValueError if isinstance(value, (str, int, float, bytes, bool, type(None), - type(...), sentinel)): + sentinel)): return ast.Constant(value) raise ValueError From 53e962015ef54bbd755890f347779fac41b7e32a Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sun, 24 May 2026 14:08:22 +0300 Subject: [PATCH 3/9] simplify check for permitted types --- Lib/ast.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index e662063bcc1337a..2df029ec005a79d 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -65,8 +65,8 @@ def literal_eval(node_or_string): return _convert_literal(node_or_string) -_type_None = type(None) -_type_Ellipsis = type(...) +_permitted_literal_types = (str, bytes, int, float, complex, + bool, type(None), type(...)) def _convert_literal(node, omit_validation=False): @@ -76,8 +76,7 @@ def _convert_literal(node, omit_validation=False): if isinstance(node, Constant): if omit_validation: return node.value - if type(value := node.value) in (str, bytes, int, float, complex, - bool, _type_None, _type_Ellipsis): + if type(value := node.value) in _permitted_literal_types: return value if isinstance(node, Dict) and len(node.keys) == len(node.values): return dict(zip( From fc46e56857ec24fd7ecb81bfabf4f288ed0eee83 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sun, 24 May 2026 14:08:46 +0300 Subject: [PATCH 4/9] don't misuse ast.Constant() in the inspect module --- Lib/inspect.py | 28 ++++++++++----------------- Lib/test/test_inspect/test_inspect.py | 8 +++----- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index af6aa3eb37a53bb..499383c60896786 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2209,7 +2209,7 @@ def wrap_value(s): if isinstance(value, (str, int, float, bytes, bool, type(None), sentinel)): - return ast.Constant(value) + return ast.parse(s) raise ValueError class RewriteSymbolics(ast.NodeTransformer): @@ -2230,28 +2230,20 @@ def visit_Name(self, node): raise ValueError() return wrap_value(node.id) - def visit_BinOp(self, node): - # Support constant folding of a couple simple binary operations - # commonly used to define default values in text signatures - left = self.visit(node.left) - right = self.visit(node.right) - if not isinstance(left, ast.Constant) or not isinstance(right, ast.Constant): - raise ValueError - if isinstance(node.op, ast.Add): - return ast.Constant(left.value + right.value) - elif isinstance(node.op, ast.Sub): - return ast.Constant(left.value - right.value) - elif isinstance(node.op, ast.BitOr): - return ast.Constant(left.value | right.value) - raise ValueError - def p(name_node, default_node, default=empty): name = parse_name(name_node) if default_node and default_node is not _empty: try: default_node = RewriteSymbolics().visit(default_node) - default = ast.literal_eval(default_node) - except ValueError: + default_source = ast.unparse(default_node) + try: + default = eval(default_source, module_dict) + except NameError: + try: + default = eval(default_source, sys_module_dict) + except NameError: + raise ValueError + except ValueError as exc: raise ValueError("{!r} builtin has invalid signature".format(obj)) from None parameters.append(Parameter(name, kind, default=default, annotation=empty)) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index a0c4b0030a0fd4b..33e1b3b5b2cbd26 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6287,9 +6287,9 @@ def test_operator_module_has_signatures(self): self._test_module_has_signatures(operator) def test_os_module_has_signatures(self): - unsupported_signature = {'chmod', 'utime'} + unsupported_signature = {'utime'} unsupported_signature |= {name for name in - ['get_terminal_size', 'link', 'register_at_fork', 'startfile'] + ['get_terminal_size', 'register_at_fork', 'startfile'] if hasattr(os, name)} self._test_module_has_signatures(os, unsupported_signature=unsupported_signature) @@ -6339,9 +6339,7 @@ def test_threading_module_has_signatures(self): def test_thread_module_has_signatures(self): import _thread no_signature = {'RLock'} - unsupported_signature = {'interrupt_main'} - self._test_module_has_signatures(_thread, no_signature, - unsupported_signature) + self._test_module_has_signatures(_thread, no_signature) def test_time_module_has_signatures(self): no_signature = { From fdefe31c781157708b119a6fe4d96877e9a18762 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sun, 24 May 2026 16:34:07 +0300 Subject: [PATCH 5/9] w/a for pdb --- Lib/inspect.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 499383c60896786..db99f1d53582f37 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2209,7 +2209,7 @@ def wrap_value(s): if isinstance(value, (str, int, float, bytes, bool, type(None), sentinel)): - return ast.parse(s) + return ast.parse(s, mode='eval').body raise ValueError class RewriteSymbolics(ast.NodeTransformer): @@ -2237,12 +2237,15 @@ def p(name_node, default_node, default=empty): default_node = RewriteSymbolics().visit(default_node) default_source = ast.unparse(default_node) try: - default = eval(default_source, module_dict) - except NameError: + default = ast.literal_eval(default_source) + except ValueError: try: - default = eval(default_source, sys_module_dict) + default = eval(default_source, module_dict) except NameError: - raise ValueError + try: + default = eval(default_source, sys_module_dict) + except NameError: + raise ValueError except ValueError as exc: raise ValueError("{!r} builtin has invalid signature".format(obj)) from None parameters.append(Parameter(name, kind, default=default, annotation=empty)) From 7c09f9f5e44e4517f47a61990f00979ae28af890 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 18 Aug 2026 08:41:46 +0300 Subject: [PATCH 6/9] simplify Lib/inspect.py --- Lib/inspect.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 4ca390775066687..e3ff105d248021d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2215,9 +2215,11 @@ def wrap_value(s): except NameError: raise ValueError - if isinstance(value, (str, int, float, bytes, bool, type(None), - sentinel)): - return ast.parse(s, mode='eval').body + if isinstance(value, (str, int, float, bytes, bool, type(None))): + return ast.Constant(value) + elif isinstance(value, sentinel): + return ast.Call(func=ast.Name(id='sentinel'), + args=[ast.Constant(value.__name__)]) raise ValueError class RewriteSymbolics(ast.NodeTransformer): @@ -2238,23 +2240,28 @@ def visit_Name(self, node): raise ValueError() return wrap_value(node.id) + def visit_BinOp(self, node): + # Support constant folding of a couple simple binary operations + # commonly used to define default values in text signatures + left = self.visit(node.left) + right = self.visit(node.right) + if not isinstance(left, ast.Constant) or not isinstance(right, ast.Constant): + raise ValueError + if isinstance(node.op, ast.Add): + return ast.Constant(left.value + right.value) + elif isinstance(node.op, ast.Sub): + return ast.Constant(left.value - right.value) + elif isinstance(node.op, ast.BitOr): + return ast.Constant(left.value | right.value) + raise ValueError + def p(name_node, default_node, default=empty): name = parse_name(name_node) if default_node and default_node is not _empty: try: default_node = RewriteSymbolics().visit(default_node) - default_source = ast.unparse(default_node) - try: - default = ast.literal_eval(default_source) - except ValueError: - try: - default = eval(default_source, module_dict) - except NameError: - try: - default = eval(default_source, sys_module_dict) - except NameError: - raise ValueError - except ValueError as exc: + default = ast.literal_eval(default_node) + except ValueError: raise ValueError("{!r} builtin has invalid signature".format(obj)) from None parameters.append(Parameter(name, kind, default=default, annotation=empty)) From 592738591f96363cd4a518cca9e4f0833676f794 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 18 Aug 2026 08:50:04 +0300 Subject: [PATCH 7/9] Support sentinel and ast.Compare in ast.literal_eval() --- Lib/ast.py | 34 ++++++++++++++++++++++++++++++++-- Lib/test/test_ast/test_ast.py | 9 +++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 2df029ec005a79d..9aa3f9f16951341 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -91,9 +91,16 @@ def _convert_literal(node, omit_validation=False): return set(map(_convert_literal, node.elts)) if ( isinstance(node, Call) and isinstance(node.func, Name) - and node.func.id == 'set' and node.args == node.keywords == [] ): - return set() + if node.func.id == 'set' and node.args == node.keywords == []: + return set() + elif ( + node.func.id == 'sentinel' and len(node.args) == 1 + and node.keywords == [] + and isinstance(arg := node.args[0], Constant) + and isinstance(name := arg.value, str) + ): + return sentinel(name) if ( isinstance(node, UnaryOp) and isinstance(node.op, (UAdd, USub)) @@ -116,6 +123,29 @@ def _convert_literal(node, omit_validation=False): return left + right else: return left - right + if (isinstance(node, Compare) + and isinstance(node.left, Constant) + and type(left := _convert_literal(node.left)) in (int, float, str) + and len(node.ops) == 1 + and isinstance(op := node.ops[0], (Eq, NotEq, Lt, LtE, Gt, GtE)) + and len(node.comparators) == 1 + and isinstance(node.comparators[0], Constant) + and type(right := node.comparators[0].value) in (int, float, str) + and (type(left) == type(right) or str not in [type(left), type(right)]) + ): + if isinstance(op, Eq): + return left == right + elif isinstance(op, NotEq): + return left != right + elif isinstance(op, Lt): + return left < right + elif isinstance(op, LtE): + return left <= right + elif isinstance(op, Gt): + return left > right + else: + return left >= right + msg = "malformed node or string" if lno := getattr(node, 'lineno', None): msg += f' on line {lno}' diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 0867561da469a35..766e46224c63d92 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -2032,6 +2032,9 @@ def test_literal_eval(self): self.assertEqual(ast.literal_eval('{1, 2, 3}'), {1, 2, 3}) self.assertEqual(ast.literal_eval('b"hi"'), b"hi") self.assertEqual(ast.literal_eval('set()'), set()) + val = ast.literal_eval('sentinel("xyz")') + self.assertTrue(isinstance(val, sentinel)) + self.assertEqual(val.__name__, "xyz") self.assertRaises(ValueError, ast.literal_eval, 'foo()') self.assertEqual(ast.literal_eval('6'), 6) self.assertEqual(ast.literal_eval('+6'), 6) @@ -2040,6 +2043,12 @@ def test_literal_eval(self): self.assertEqual(ast.literal_eval('+3.25'), 3.25) self.assertEqual(ast.literal_eval('-3.25'), -3.25) self.assertEqual(repr(ast.literal_eval('-0.0')), '-0.0') + self.assertEqual(ast.literal_eval('1 == 2'), False) + self.assertEqual(ast.literal_eval('1 != 2'), True) + self.assertEqual(ast.literal_eval('1 < 2'), True) + self.assertEqual(ast.literal_eval('1 <= 2'), True) + self.assertEqual(ast.literal_eval('1 >= 2'), False) + self.assertEqual(ast.literal_eval('1 > 2'), False) self.assertRaises(ValueError, ast.literal_eval, '++6') self.assertRaises(ValueError, ast.literal_eval, '+True') self.assertRaises(ValueError, ast.literal_eval, '2+3') From 37df249181877be10ab6c741b7a56f58f9504ca6 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 18 Aug 2026 08:50:59 +0300 Subject: [PATCH 8/9] mark _thread.interrupt_main signature unsupported previously IntEnum was rendered as 2. I think we should rather preserve repr for int's subclass, like pure-Python methods do. see also #61005 --- Lib/test/test_inspect/test_inspect.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 2e73f20b8b91d4d..66b52c32c7c5cf8 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6375,7 +6375,9 @@ def test_threading_module_has_signatures(self): def test_thread_module_has_signatures(self): import _thread no_signature = {'RLock'} - self._test_module_has_signatures(_thread, no_signature) + unsupported_signature = {'interrupt_main'} + self._test_module_has_signatures(_thread, no_signature, + unsupported_signature=unsupported_signature) def test_time_module_has_signatures(self): no_signature = { From 457aac1d67f399204507b4974776d7565a185ebd Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 18 Aug 2026 09:29:37 +0300 Subject: [PATCH 9/9] revert Compare support --- Lib/ast.py | 23 ----------------------- Lib/test/test_ast/test_ast.py | 6 ------ Lib/test/test_inspect/test_inspect.py | 4 ++-- 3 files changed, 2 insertions(+), 31 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 9aa3f9f16951341..42f4dc04804d06d 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -123,29 +123,6 @@ def _convert_literal(node, omit_validation=False): return left + right else: return left - right - if (isinstance(node, Compare) - and isinstance(node.left, Constant) - and type(left := _convert_literal(node.left)) in (int, float, str) - and len(node.ops) == 1 - and isinstance(op := node.ops[0], (Eq, NotEq, Lt, LtE, Gt, GtE)) - and len(node.comparators) == 1 - and isinstance(node.comparators[0], Constant) - and type(right := node.comparators[0].value) in (int, float, str) - and (type(left) == type(right) or str not in [type(left), type(right)]) - ): - if isinstance(op, Eq): - return left == right - elif isinstance(op, NotEq): - return left != right - elif isinstance(op, Lt): - return left < right - elif isinstance(op, LtE): - return left <= right - elif isinstance(op, Gt): - return left > right - else: - return left >= right - msg = "malformed node or string" if lno := getattr(node, 'lineno', None): msg += f' on line {lno}' diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 766e46224c63d92..f14ac29d15b864a 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -2043,12 +2043,6 @@ def test_literal_eval(self): self.assertEqual(ast.literal_eval('+3.25'), 3.25) self.assertEqual(ast.literal_eval('-3.25'), -3.25) self.assertEqual(repr(ast.literal_eval('-0.0')), '-0.0') - self.assertEqual(ast.literal_eval('1 == 2'), False) - self.assertEqual(ast.literal_eval('1 != 2'), True) - self.assertEqual(ast.literal_eval('1 < 2'), True) - self.assertEqual(ast.literal_eval('1 <= 2'), True) - self.assertEqual(ast.literal_eval('1 >= 2'), False) - self.assertEqual(ast.literal_eval('1 > 2'), False) self.assertRaises(ValueError, ast.literal_eval, '++6') self.assertRaises(ValueError, ast.literal_eval, '+True') self.assertRaises(ValueError, ast.literal_eval, '2+3') diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 66b52c32c7c5cf8..3ebaf0128d6bc9e 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6304,9 +6304,9 @@ def test_operator_module_has_signatures(self): self._test_module_has_signatures(operator) def test_os_module_has_signatures(self): - unsupported_signature = {'utime'} + unsupported_signature = {'chmod', 'utime'} unsupported_signature |= {name for name in - ['get_terminal_size', 'register_at_fork', 'startfile'] + ['get_terminal_size', 'link', 'register_at_fork', 'startfile'] if hasattr(os, name)} self._test_module_has_signatures(os, unsupported_signature=unsupported_signature)