fix(matchers): refuse regexes that fail only at the end of a run - #213
Merged
Merged
Conversation
The slow-regex guard timed a pattern only on repeated runs it matches, so a repeat inside a repeat, which blows up when the run is followed by something that makes the match fail, walked through. Measured on 0.7.0: ^(\w+\s?)+$ spent 558 ms on one process name, and ^([\d:]+)+$ in the destination field 2.7 s per packet on a real IPv6 address. A pattern near the budget was refused 20 times in 40, so the form could accept a text that Apply then refused. - The ladder also tries every run followed by a character no probe alphabet contains; all of the above are refused within 20 characters. - re.error, OverflowError and RecursionError from re.compile all become the ordinary "not a valid regular expression" ValueError. The last two escaped every caller: --dry-run exited 1 with a traceback, the expression tester raised and the Control page raised per keystroke. - Accepted patterns are kept (lru_cache keeps nothing that raised), so validation and apply give the same answer; a refusal is judged again. - At apply, a destination, block or target expression that cannot be read leaves the field as it was instead of switching it off, which for a destination or a target meant impairing all traffic. Three tests that pinned the old behaviour are rewritten on purpose; new tests and six mutation entries guard the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-apply-scope # Conflicts: # CHANGELOG.md
|
Warning Review limit reachedNext included review available in 23 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (14)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
1. The slow-regex guard only ever tried runs the pattern matches. A
re:expression is timed on repeated runs (1111,aaaa,1:1:,a.a.) before it is accepted. A repeat inside a repeat only blows up when such a run is followed by something that makes the whole match fail. On a bare run it matches at once, so the whole shape walked through. Measured on 0.7.0 code, all accepted:re:^(\w+\s?)+$StartMenuExperienceHost.exe)re:^([\d:]+)+$re:(a+)+$,re:^(\d+)+$,re:^((a|b|c)+(d|e)*)+$A pattern near the budget,
re:(a|1)*.*.*z, was refused 20 times in 40. The same text could pass when the form checked it and fail when "Apply" compiled it again.2.
reraises more thanre.error.re:a{99999999999}raisesOverflowError, and a few thousand nested groups raiseRecursionError. Every caller ofparse_matchercatches onlyValueError. The results:--dry-runexited 1 with a traceback instead of the CONFIG code;3. An expression that could not be read at apply switched its field off. For the destination and the target process, off means every connection is impaired. A field that could not be read therefore widened the session to all traffic.
The fix
matchers._REGEX_PROBE_TAILS = ("", "!"): every run is also tried followed by a character none of the probe alphabets contains.(a|1)*.*.*zin 40 of 40 runs.re.error,OverflowErrorandRecursionErrorfromre.compileall become the ordinary "not a valid regular expression"ValueError, in the one function every caller passes through._accepted_regexusesfunctools.lru_cache(maxsize=256).lru_cachekeeps nothing for a call that raises, so only acceptances are remembered.apply_targetingkeeps the target in force instead of callingset_target(False).log.filter_skippednow says the field was left as it was, in all three languages.^(a+)+$is harmless is corrected in the code comment and a test docstring;(x+x+)+y;exprtest.MAX_VALUE_CHARSstays 256. With the tail, the shapes that still pass cost at most about 1 ms per search at that length, and cutting it to the 45 characters the ladder covers would refuse long process names the tester exists to try. The comment now gives the measurement.Tests rewritten on purpose
test_matchers.py::test_the_patterns_a_person_would_actually_write_are_still_acceptedlisted^((a|b|c)+(d|e)*)+$as fine. It is the ladder's blind spot, not a good pattern. It moved to the refused list, and the unambiguous^((a|b|c)(d|e)*)+$took its place.test_settings_config_scenario.py::test_apply_settings_bad_expression_disables_dest_targetingand..._blockingpinned "switch it off". They are nowtest_a_bad_destination_at_apply_leaves_the_previous_one_in_placeandtest_a_bad_block_at_apply_leaves_the_previous_one_in_place. Each covers a fresh engine and one that already had a value.test_processes.py::test_apply_targeting_logs_and_disables_on_a_bad_expressionis now..._keeps_the_target_on_a_bad_expression.New guards
test_matchers.py:test_a_pattern_that_fails_only_at_its_end_is_refused_too;test_a_pattern_the_parser_cannot_build_is_a_value_error_on_every_kind;test_an_accepted_pattern_is_not_judged_again.test_nettools_exprtest.py::test_a_regex_re_cannot_build_is_a_verdict_not_an_exception.test_cli_runtime.py::test_exit_code_config_for_bad_input: two new cases, a regexrecannot build and one too slow per packet.tools/mutate.pyand all were caught. The two existing guard mutations keep their anchor lines unchanged.Checks run locally
internal_tools/guards.py --strong --run --lint: 97 test files, plus ruff and mypy on the changed modules. It found two failures, both from the nesting-depth ratchet intest_code_shape.py: the tail loop made_blows_the_budgetthe twelfth function nested four levels deep, against a frozen count of eleven.master(which now includes the fail-open change), the tests of both changes pass (180).The full suite was not run locally. CI runs it on Linux and Windows.
🤖 Generated with Claude Code