Repository navigation
fix quadratic time of the XML declaration regex in vXml - #2037
shamsManna wants to merge 4 commits into
Conversation
vXml._parse strips the declared encoding from an XML property's declaration with a regex whose lazy [^>]*? and greedy \s+ both match whitespace, so a long run of whitespace in the declaration is retried from every position and to_xcal() takes quadratic time (200k spaces: 41 s). Match a single whitespace before encoding, as the bytes regex does. The whitespace left in the declaration is valid XML. AI: Claude Fable 5.1 (claude-fable-5-1) via Claude Code. Prompt: find a security bug in icalendar, reproduce it, and write a minimal fix with a regression test and change log entry. Output: (this commit), reviewed by the author.
|
Thanks for the pull request, @shamsManna! A maintainer will review it shortly. |
Mixed activityActivity patterns show a mix of organic and automated signals. Evidence
Last 5 PRs:
This is an automated analysis by AgentScan |
niccokunzmann
left a comment
There was a problem hiding this comment.
Thanks for finding this.. regex is always a bit a hit and miss and we must check them all for quadratic runtime.
A possessive \s++ alone is still quadratic, because the lazy [^>]*? keeps retrying it from every position in the whitespace run. Matching the declaration as alternating possessive runs of whitespace and other characters is linear and removes all whitespace before encoding, like the original pattern. Python 3.10 keeps the single \s.
niccokunzmann
left a comment
There was a problem hiding this comment.
This looks good.. Thanks for the fix.
| r"""^(\ufeff?<\?xml\s[^>]*?)\s+encoding\s*=\s*("[^"]*"|'[^']*')""", | ||
| re.ASCII, | ||
| ) | ||
| # With ``[^>]*?\s+encoding``, the whitespace run before the attribute is retried |
There was a problem hiding this comment.
@maintainers, whats you opinion on including old code in an explanation?
There was a problem hiding this comment.
I think it's not useful. Better to explain what it does now, and avoid history.
stevepiercy
left a comment
There was a problem hiding this comment.
Thanks for bringing this to our attention. I have a couple of questions and a suggestion to remove comments. Would you please take care? Thank you!
| # With ``[^>]*?\s+encoding``, the whitespace run before the attribute is retried | ||
| # from every position that the lazy ``[^>]*?`` reaches, so a long run of whitespace | ||
| # in the declaration takes quadratic time. |
There was a problem hiding this comment.
| # With ``[^>]*?\s+encoding``, the whitespace run before the attribute is retried | |
| # from every position that the lazy ``[^>]*?`` reaches, so a long run of whitespace | |
| # in the declaration takes quadratic time. |
There was a problem hiding this comment.
done, removed it. the comment above the regexes now just says what they do.
| event = Event() | ||
| event.add( | ||
| "XML", | ||
| '<?xml version="1.0"' + whitespace * 200_000 + '?><a xmlns="urn:x">text</a>', |
There was a problem hiding this comment.
How does test the presence of whitespace relative to encoding? Should there be another test for that case?
There was a problem hiding this comment.
it didn't, that test has no encoding in the declaration at all. added test_xml_property_to_xcal_with_long_whitespace_around_the_encoding: 200k runs before version, before encoding and after it, and it checks the encoding still gets stripped (encoding="ISO-8859-1" with café € as the text, so a leftover encoding would garble it).
the run before version is the important one. it's whitespace that isn't followed by encoding, which is what made the old \s+ quadratic even when an encoding is present (40k tabs there took 4.9 s with the old pattern, under 1 ms now). i also added two short declarations with tabs and newlines around encoding to the parametrized encoding test.
| # whitespace and other characters that are never given back, so the match is | ||
| # linear and all whitespace before ``encoding`` is removed. | ||
| _DECLARED_ENCODING = re.compile( | ||
| r"""^(\ufeff?<\?xml\s[^\s>]*+(?:\s++[^\s>]++)*?)\s++encoding\s*+=\s*+""" |
There was a problem hiding this comment.
I think double-quotes need to be escaped. Please check against this test, and let me know what you think.
There was a problem hiding this comment.
they don't need escaping in the source. both patterns are in r"""...""" triple-quoted raw strings, so a single " inside is just a literal quote and doesn't end the string. regex101 asks for \" because your link uses " as the delimiter, which corresponds to r"...".
i ran your test string through the compiled pattern on 3.11+ and it strips the encoding together with all the whitespace before it, giving <?xml version="1.0"\n\t\t\t\n\n?><a xmlns="urn:x">text</a>. a declaration like it is now in the tests.
| # A single ``\s`` before ``encoding`` keeps the match linear. | ||
| # The whitespace that stays in the declaration is valid XML. | ||
| _DECLARED_ENCODING = re.compile( | ||
| r"""^(\ufeff?<\?xml\s[^>]*?)\sencoding\s*=\s*("[^"]*"|'[^']*')""", |
There was a problem hiding this comment.
We should test this one as well.
There was a problem hiding this comment.
agreed. it's now _DECLARED_ENCODING_PY310 at module level, and _DECLARED_ENCODING points to it on 3.10. a declared_encoding fixture runs the declaration tests against both, so the 3.10 regex is tested on every python version and the possessive one on 3.11+ (skipped on 3.10).
Drop the comment about the old pattern and keep the Python 3.10 regex as _DECLARED_ENCODING_PY310, so the declared encoding tests run against it on every Python version as well as against the possessive one on 3.11+. Add a test with long whitespace runs before version, before encoding and after encoding, which checks the encoding is still removed, plus declarations with whitespace and newlines around encoding.
Linked issue
Description
vXml._parsestrips the declared encoding with a regex whose lazy[^>]*?and greedy\s+both match whitespace, so a long whitespace run in the XML declaration is retried from every position: 200k spaces take 41 s into_xcal(), and an untrusted .ics stalls xCal conversion. Fix: a single whitespace before encoding, like the bytes regex. Same problem as #1449, in the XML property from #1958.Checklist