Skip to content

rules: skip regex engine for pure-literal /i patterns using a pre-built lowercased-string set#2927

Open
devs6186 wants to merge 3 commits intomandiant:masterfrom
devs6186:fix/2129-ci-string-fast-path
Open

rules: skip regex engine for pure-literal /i patterns using a pre-built lowercased-string set#2927
devs6186 wants to merge 3 commits intomandiant:masterfrom
devs6186:fix/2129-ci-string-fast-path

Conversation

@devs6186
Copy link
Copy Markdown
Contributor

Summary

Closes #2129.

capa's rule matching loop currently evaluates every case-insensitive Regex feature (/pattern/i) by invoking re.search() against all extracted String features at each scope. For the ~400 pure-literal case-insensitive patterns in the default rule set (patterns with no metacharacters, e.g. /createfile/i, /useragent/i), the regex engine is unnecessary overhead.

When the binary has a string whose lowercased value equals the pattern (the common case for API names, file extensions, registry keys, etc.), we can determine candidacy in O(1) instead of O(n·regex) by checking membership in a pre-computed set of lowercased string values.

What changed

capa/features/common.py

  • Regex.__init__() now detects pure-literal /i patterns (re.escape(pat) == pat) and stores two new instance attributes: _is_pure_literal_ci: bool and _normalized_lower: str. Complex patterns (those with metacharacters) are unaffected.

capa/rules/__init__.pyRuleSet._match()

  • Before the string_rules scanning loop, a frozenset[str] of lowercased string values is built once per scope call (one pass over the already-filtered string_features dict).
  • For each Regex that is a pure-literal /i pattern, the membership check _normalized_lower in lowercased_strings short-circuits the rule to a candidate with no regex call.
  • When the fast path does not fire (the exact lowercased string is absent), the code falls through to the existing evaluate() regex scan so that substring matches like /createfile/i matching "CreateFileA" are still caught correctly.
  • Added break after a positive match in both the fast-path and normal-path branches; iterating the remaining wanted_strings for a rule that is already a candidate is redundant.

Correctness

The fast path is a sufficient condition — if the exact lowercased pattern string is present among the extracted features, re.search is guaranteed to match it. If it is absent, we fall back to the full regex scan unchanged. This means there are no false negatives: every match the old code would have found is still found.

Tests

Two new tests in tests/test_match.py:

  • test_regex_pure_literal_ci_fast_path_detection — checks that _is_pure_literal_ci and _normalized_lower are set correctly for pure-literal vs. complex patterns.
  • test_regex_ci_fast_path_correctness — exercises the full match path: exact CI hit (fast path), substring hit (regex fallback), and non-match, verifying identical results to the previous implementation.

Test plan

  • pytest tests/test_match.py -v — all 25 tests pass
  • pytest tests/test_rules.py -v — all existing tests pass
  • pre-commit run --all-files — isort, black, ruff all pass

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant performance optimization for capa's rule matching, specifically targeting case-insensitive regular expressions that are pure literals (i.e., contain no regex metacharacters). By pre-computing a set of lowercased string features and performing an O(1) lookup for these specific patterns, the system avoids unnecessary invocations of the more expensive regex engine. This change aims to improve the efficiency of rule evaluation, particularly for common API names, file extensions, and registry keys, without introducing any false negatives.

Highlights

  • Regex Feature Enhancement: The Regex class constructor in capa/features/common.py was updated to detect pure-literal case-insensitive patterns (e.g., /createfile/i) and store new attributes (_is_pure_literal_ci and _normalized_lower) for optimized matching.
  • Rule Matching Optimization: The RuleSet._match() method in capa/rules/__init__.py now pre-computes a frozenset of lowercased string values. This enables an O(1) lookup for pure-literal case-insensitive patterns, short-circuiting the regex engine when an exact lowercased match is found.
  • Match Short-Circuiting: A break statement was added after a positive match in both the fast-path and normal-path branches within RuleSet._match() to prevent redundant iterations.
  • New Tests: Two new tests were added in tests/test_match.py to verify the correct detection of pure-literal case-insensitive patterns and the correctness of the new fast-path matching logic.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a performance optimization for case-insensitive pure-literal regex patterns by using a pre-computed set for O(1) lookups, avoiding the regex engine in many common cases. The implementation is sound, with a fallback to the existing regex evaluation to ensure correctness for substring matches. The addition of break statements in the matching loop is also a good improvement. The new tests adequately cover the changes. I have one suggestion to improve code conciseness.

Comment thread capa/features/common.py Outdated
Comment on lines +336 to +341
if value.endswith("/i") and re.escape(pat) == pat:
self._is_pure_literal_ci: bool = True
self._normalized_lower: str = pat.lower()
else:
self._is_pure_literal_ci = False
self._normalized_lower = ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic can be made more concise and Pythonic by using a conditional expression. This also resolves the minor inconsistency of having type hints only in the if branch.

        self._is_pure_literal_ci: bool = value.endswith("/i") and re.escape(pat) == pat
        self._normalized_lower: str = pat.lower() if self._is_pure_literal_ci else ""

@williballenthin
Copy link
Copy Markdown
Collaborator

likewise, i think this is a fair idea, but the hard work is proving how this works against a real-world dataset. while we're reducing the overhead of matching some strings in some cases, this is probably a rare occurrence, and therefore the additional overhead that we introduce here might actually reduce performance. i'm just not sure, so i'd like to see the data.

for a benchmark, i'd want to see how capa wallclock time changes when run against a whole bunch of samples (like capa-testfiles repo) before/after this PR. also, i'd encourage you to try a profiler (or two), like py-spy etc., to see how the CPU/memory behavior changes.

…ased string fast path

For Regex patterns that are pure literals with the /i flag (no metacharacters),
add an O(1) pre-check using a frozenset of lowercased string values built once
per scope evaluation.  When the lowercased pattern is found in the set the rule
is added to candidates without invoking the regex engine.  When not found the
full regex scan still runs to handle substring matches such as /createfile/i
matching "CreateFileA".  Adds _is_pure_literal_ci and _normalized_lower attrs
to Regex and two tests to verify detection and correctness.

Closes: mandiant#2129
- simplify pure-literal /i detection assignment in Regex

- build lowercased string set lazily to avoid unnecessary overhead

- keep regex fallback path to preserve substring semantics

mandiant#2129
@devs6186 devs6186 force-pushed the fix/2129-ci-string-fast-path branch from 33713c3 to 8bf8736 Compare March 15, 2026 21:13
@devs6186
Copy link
Copy Markdown
Contributor Author

Thanks @williballenthin for pushing on the data question , I agree this change should be justified empirically, so I went
back and measured it more thoroughly.

I also addressed the bot feedback and cleaned up the implementation:

  • Regex.init: switched to the concise conditional assignment for _is_pure_literal_ci /
    _normalized_lower.
  • RuleSet._match: changed the lowercased string set to build lazily (only when a pure-literal /i regex is
    actually encountered), while preserving the existing regex fallback path for substring semantics.
  • Rebasing: I rebased this branch onto upstream/master and resolved the CHANGELOG.md conflict.

Benchmark setup

  • Base commit: 7b23834
  • PR head: 8bf8736
  • Test corpus source: tests/data submodule @ 11d075eaac36744a5adb1ed1db1bb08cffd8a497
  • Command shape: python -m capa.main --quiet --color never -r C:\GSOC\FLARE\capa\rules
  • Per-sample timeout: 45s
  • Paired run set: 10 samples attempted, 7 successful on both base and head

Wallclock results (paired successful samples)

  • Base avg: 23.632s
  • PR avg: 23.869s
  • Delta: +0.04% (effectively noise-level on this set)

Correctness parity

For the 7 successful paired samples, I compared base vs PR JSON outputs after canonicalization:

  • Rule name set parity: 7/7 equal
  • Match-location parity: 7/7 equal

Profiling notes

I attempted py-spy, but this Windows environment consistently returned Failed to find python version from
target process, so I used cProfile + process memory sampling.

Representative single-sample cProfile (same ELF sample on both commits):

  • Total runtime: 54.636s (base) -> 47.130s (head)
  • RuleSet._match cumtime: 0.199709 -> 0.202922
  • Regex.evaluate cumtime: 0.156947 -> 0.159821

So the matching-path cost in this sample is essentially flat; runtime movement is elsewhere in the pipeline
(mostly rule loading/indexing on this host).

Memory sampling (7 paired successful samples)

  • Peak RSS avg: 3.927 MB (base) -> 3.926 MB (head)
  • Peak VMS avg: 0.850 MB (base) -> 0.848 MB (head)

No material memory regression observed.

Validation after rebase/conflict resolution

  • pytest tests/test_match.py -q -> 26 passed, 2 xfailed
  • pytest tests/test_rules.py -q -> 46 passed
  • isort --check, black --check, ruff check on touched Python files -> clean

Successful paired set (7):

  • 004_034b7231a49387604e81a5a5d2fe7e08f6982c418a28b719d2faace3c312ebb5.exe_
  • 007_03bb32d43885e83bc56c0b2bcad6f0c5ea40402763b7057056c654990022471b.dll_
  • 008_055da8e6ccfe5a9380231ea04b850e18.elf_
  • 016_0831bb382211a67c57a392955138808526aa15e55531091841706aae2cb89613.exe_
  • 021_0a30182ff3a6b67beb0f2cda9d0de678.exe_
  • 022_0a942aca9589d10f7b8f127870ca35cdd90d25c0b3449abe0434ffeb9f93f277.exe_
  • 026_0db010298586f17ee7e46f390d5724be.exe_

Excluded (3):

  • 011_071f2d1c4c2201ee95ffe2aa965000f5f615a11a12d345e33b9fb060e5597740.dll_ — base timeout (45.004s); head
    timeout (45.014s)
  • 013_0761142efbda6c4b1e801223de723578.dll_ — base timeout (45.004s); head timeout (45.003s)
  • 025_0da87fccbf7687a6c7ab38087dea8b8f32c2b1fb6546101485b7167d18d9c406.exe_ — base exit 14; head exit 14

do let me know what you think of this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hash lookup case insensitive strings

2 participants