bound oversized format width and precision during inference (#3108)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
diff --git a/ChangeLog b/ChangeLog
index bbc0b7b..490a8e2 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -87,6 +87,14 @@
Refs pylint-dev/pylint#10193
Closes #3007
+* Avoid materializing a multi-gigabyte string while inferring a small literal
+ such as ``"{:>2000000000}".format("x")`` or ``f"{1.5:.2000000000f}"``.
+ ``_infer_str_format_call`` and ``FormattedValue._infer`` now yield
+ ``Uninferable`` when a format spec asks for a width or precision over 1e8,
+ mirroring the sequence/repetition caps in ``astroid.protocols``.
+
+Refs #3108
+
* Fix uncaught ``IndentationError`` when parsing code whose lines end in
``\r``: the slice of source tokenized to compute a class or function
``position`` is split on ``\n`` only and could be misaligned with the AST
diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py
index 2ef32d9..1a04dca 100644
--- a/astroid/brain/brain_builtin_inference.py
+++ b/astroid/brain/brain_builtin_inference.py
@@ -1091,6 +1091,15 @@
keyword_values: dict[str, str] = {k: v.value for k, v in inferred_keyword.items()}
+ import string # pylint: disable=import-outside-toplevel
+
+ try:
+ fields = list(string.Formatter().parse(format_template))
+ except ValueError:
+ return iter([util.Uninferable])
+ if any(spec and util.format_spec_too_large(spec) for _, _, spec, _ in fields):
+ return iter([util.Uninferable])
+
try:
formatted_string = _BoundedFormatter().format(
format_template, *pos_values, **keyword_values
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
index 958bf90..193e028 100644
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -4698,6 +4698,12 @@
value_to_format = value
if isinstance(value, Const):
value_to_format = value.value
+ if isinstance(format_spec.value, str) and util.format_spec_too_large(
+ format_spec.value
+ ):
+ yield util.Uninferable
+ uninferable_already_generated = True
+ continue
try:
formatted = format(value_to_format, format_spec.value)
yield Const(
diff --git a/astroid/util.py b/astroid/util.py
index 4cb4629..e1071e1 100644
--- a/astroid/util.py
+++ b/astroid/util.py
@@ -5,6 +5,7 @@
from __future__ import annotations
+import re
import warnings
from typing import TYPE_CHECKING, Any, Final, Literal
@@ -50,6 +51,29 @@
Uninferable: Final = UninferableBase()
+# Width/precision in a format spec drive how large a formatted string gets.
+# Match the leading width and the optional ``.precision`` from the start of a
+# format spec (PEP 3101 / format mini-language).
+_FORMAT_SPEC_SIZE = re.compile(
+ r"(?:.?[<>=^])?[-+ ]?z?#?0?(?P<width>\d+)?(?:[,_])?(?:\.(?P<precision>\d+))?"
+)
+# Mirrors the sequence/repetition caps in astroid.protocols.
+MAX_FORMATTED_SIZE = 10**8
+
+
+def format_spec_too_large(format_spec: str) -> bool:
+ """Whether a format spec asks for an oversized width or precision.
+
+ Used to avoid materializing a multi-gigabyte string while inferring a tiny
+ literal such as ``"{:>2000000000}".format("x")`` or ``f"{1.5:.2e9f}"``.
+ """
+ # Every group in _FORMAT_SPEC_SIZE is optional, so match is never None.
+ match = _FORMAT_SPEC_SIZE.match(format_spec)
+ return any(
+ size is not None and int(size) > MAX_FORMATTED_SIZE
+ for size in match.group("width", "precision")
+ )
+
class BadOperationMessage:
"""Object which describes a TypeError occurred somewhere in the inference chain.
diff --git a/tests/test_inference.py b/tests/test_inference.py
index 335bf5f..9690183 100644
--- a/tests/test_inference.py
+++ b/tests/test_inference.py
@@ -5378,6 +5378,20 @@
assert inferred[0] is util.Uninferable
+@pytest.mark.parametrize(
+ "code",
+ [
+ "f'{1:>2000000000}'",
+ "f'{0:030000000000}'",
+ "f'{1.5:.2000000000f}'",
+ ],
+)
+def test_fstring_oversized_width_uninferable(code: str) -> None:
+ """A huge width/precision must not materialize a multi-gigabyte string."""
+ node = extract_node(code)
+ assert list(node.infer()) == [util.Uninferable]
+
+
def test_augassign_recursion() -> None:
"""Make sure inference doesn't throw a RecursionError.
@@ -7242,6 +7256,32 @@
@pytest.mark.parametrize(
+ "code",
+ [
+ '"{:>2000000000}".format("x")',
+ '"{:.2000000000f}".format(1.5)',
+ ],
+)
+def test_str_format_oversized_width_uninferable(code: str) -> None:
+ """str.format() with a huge width/precision must stay Uninferable."""
+ node = _extract_single_node(code)
+ assert next(node.infer()) is util.Uninferable
+
+
+@pytest.mark.parametrize(
+ "code",
+ [
+ '"{".format()',
+ '"}{".format()',
+ ],
+)
+def test_str_format_malformed_template_uninferable(code: str) -> None:
+ """A template Formatter().parse() rejects must stay Uninferable."""
+ node = _extract_single_node(code)
+ assert next(node.infer()) is util.Uninferable
+
+
+@pytest.mark.parametrize(
"source, expected",
[
(