cap oversized field width/precision in str.format inference#3131
cap oversized field width/precision in str.format inference#3131nabhan06 wants to merge 2 commits into
Conversation
b9b1065 to
939735c
Compare
DanielNoord
left a comment
There was a problem hiding this comment.
@Pierre-Sassoulas I guess an AI provider has figured out that cap oversized ... is a nice way to create PRs.
Do we really carre about this? There don't seem to be actual user reports and with this regex matching we're making this all very complex.
Signed-off-by: sayed nabhan <nabhan@bugqore.com>
|
Fair on the regex, that was the ugly part. I dropped it and just scan the format spec for digit runs over the cap, since width and precision are the only multi-digit numbers a spec can contain. Same behaviour, a lot less to read now. On whether it's worth it: this is the exact same eager-materialization issue as #3107 (str/bytes multiply and shift), just reached through the format mini-language. "{:>2000000000}".format("x") pushes RSS to a couple of GB during inference of otherwise untrusted source, same as "x" * 10 ** 10 did before that fix. No user report, agreed, I hit it while poking at the format path after #3107. If you'd rather not carry the extra surface for a case nobody's reported, I'm fine closing it, your call. |
|
I guess users expect astroid/pylint to be slow and do not troubleshoot exactly why all the time. I know those are AI contributions, but it's reasonable to add safeguard like this imo. |
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3131 +/- ##
=======================================
Coverage 93.61% 93.61%
=======================================
Files 92 92
Lines 11381 11390 +9
=======================================
+ Hits 10654 10663 +9
Misses 727 727
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
But we're making |
|
Maybe we anticipate too much, maybe it's similar to #2228 (Who does |
|
pylint-dev/pylint#8748 feels like somebody fuzzing the code. I understand we don't want to crash, but if |
|
On the CPython angle: "{:>2000000000}".format("x") doesn't actually crash CPython, it just builds the ~2GB string and returns it fine. So there's nothing to crash loudly on here, the code is valid, it's only astroid that blows up because it materializes that string eagerly while analyzing a line that at runtime might sit behind a branch that never runs (or come from generated/untrusted input). That's the same shape as "x" * 10 ** 10 in #3107 and the [0]*(2**32+1) splat in #2228, and in both of those astroid already went with Uninferable instead of crashing, so this just keeps that behavior consistent rather than inventing a new rule for the format path. On cost: codspeed shows no change, and the check only looks at the digit runs inside a format spec, so normal formatting isn't touched. If you'd rather a simpler shape than the Formatter subclass I'm happy to try, though the subclass is what lets it catch the width passed as an argument ("{:>{}}".format("x", 2000000000)), which a plain template scan would miss. And as I said earlier, if you'd rather not carry it at all it's an easy close, no hard feelings. |
|
I don't have a strong opinion because I don't know where to draw the line honestly. User complaints would have been a nice signal, but we do have fuzzing report based on generated never seen in the Real World ( ™️ ) code that is hardly distinguishable from actual user signal. And I'm not sure users are always watching htop while astroid runs. We probably need to evaluate impacts on standard case before optimizing for corner cases. Right now the benchmark is going to catch major regression (>5% slowdown). I guess complexity analysis at review time is the only thing protecting us from small regression included by adding handling code we don't need for corner case that never happen. And determining the difference between corner case and actual use case is about taste right now. Maybe we should be looking into open source code (number of occurence in the primer?). What do you think @jacobtylerwalls @cdce8p ? |
I'm not sure how many people actually do this, but it is possible to run Pylint on every save (or change!) with editors like VS Code. That means you can end up checking code that is in a weird intermediate state or has a (temporary) copy/paste error that needs to be undone. That kind of code is unlikely to show up in a primer run, and users may not be inclined to report an issue that is not easily reproducible from the command line. I don't know if this particular case needs to be fixed, but I just wanted to highlight one scenario where the added robustness can help. |
|
On where to draw the line: the cap sits at 1e8, which is orders of magnitude past any width or precision real code actually uses (a few hundred at most), so nothing in the primer would trip it. That means no false positives on legitimate formatting; the only behavior change is on specs that would balloon into hundreds of MB or more. So the cost of keeping it is basically nil even if the trigger is rare. That said, it's your call on whether it's worth the extra surface, and I'm good either way. |
|
@nabhan06 I meant when to decide if a code construct that can explode the cost of inference should be considered too niche to warrant a special case that make the normal inference slower. Evaluating if it's going to explode always have a cost even if small. (i.e. x and y in 'if the inference for "{:>2000000000}".format("x") explode and is found in x percent of the code, we can accept astroid to be y % slower all the time') |
|
Fair, let me put actual numbers on the x and y. y first: the guard doesn't touch general inference at all. It lives entirely inside x: effectively zero in real code. The cap is 1e8 and nothing legitimate comes near a width or precision that large. If the pure-Python swap is the part you would rather not pay for the common case, I can keep the C |
Type of Changes
Description
_infer_str_format_callevaluates the template eagerly withformat_template.format(*args), so a tiny analyzed line like"{:>2000000000}".format("x")makes astroid build the padded multi-gigabyte string during inference (I saw RSS jump to ~3 GB before it settles). A width or precision handed in through a nested{}field, e.g."{:>{}}".format("x", 2000000000), does the same. This is the same eager-materialization problem that**,<<, and list/str multiply already guard against, just reached through the format mini-language.The fix routes the call through a small
string.Formattersubclass and checks the width/precision informat_field, which runs after nested fields are resolved, so it covers both literal and argument-supplied sizes. Oversized cases now infer asUninferableand normal formatting is unchanged.Refs #3107