Skip to content

Add option to enable PCRE2 JIT for regex subsignature matching - #1749

Open
j20120307 wants to merge 1 commit into
Cisco-Talos:mainfrom
j20120307:feature/pcre2-jit
Open

Add option to enable PCRE2 JIT for regex subsignature matching#1749
j20120307 wants to merge 1 commit into
Cisco-Talos:mainfrom
j20120307:feature/pcre2-jit

Conversation

@j20120307

Copy link
Copy Markdown

Add option to enable PCRE2 JIT for regex subsignature matching

Summary

cli_pcre_compile() compiles regex subsignatures with pcre2_compile() and
cli_pcre_match() runs them with the interpreted pcre2_match(). The PCRE2
JIT compiler is never enabled, even when the linked PCRE2 library is built
with JIT support.

This PR adds an opt-in option to JIT-compile PCRE subsignatures:

  • PCREJit in clamd.conf
  • --pcre-jit for clamscan
  • a new CL_ENGINE_PCRE_JIT engine setting

It is disabled by default, so existing behavior is unchanged on upgrade.
The option is plumbed exactly like the existing PCREMatchLimit /
PCRERecMatchLimit / PCREMaxFileSize settings (engine field, set/get,
settings copy/apply, option table).

Implementation

  • When enabled, cli_pcre_compile() performs a best-effort
    pcre2_jit_compile(pd->re, PCRE2_JIT_COMPLETE) after the match limits are
    set. pcre2_match() automatically dispatches to the JIT code when a pattern
    has been JIT-compiled, so no change to the normal match call is required.
  • If JIT compilation fails (PCRE2 built without JIT, or a construct the JIT
    does not support), the failure is logged via cli_dbgmsg() and the pattern
    is matched by the interpreter as before.
  • To guarantee identical results to the interpreter when the option is on,
    cli_pcre_match() retries a match with PCRE2_NO_JIT if the JIT matcher
    returns PCRE2_ERROR_JIT_STACKLIMIT. The JIT uses a fixed stack and, unlike
    the interpreter, does not grow on demand; this retry keeps behavior
    equivalent rather than surfacing a JIT-only error.

Motivation

When the Aho-Corasick prefilter promotes a large number of PCRE subsignatures
for full evaluation against a large buffer, the interpreted matcher dominates
scan time. The literal anchors of many filename-style phishing subsignatures
are short and commonly appear in text-heavy input, so a single buffer can
legitimately trigger several hundred PCRE evaluations, each scanning the full
buffer. JIT executes the identical automaton as native code, so this is a pure
latency improvement with no effect on detection.

Benchmark

Measured with two builds of the same revision — option off vs option on —
using an identical signature database and scan configuration (ScanArchive
disabled, cache disabled). The test input was a ~37 MiB archive composed of
many small text files, which the prefilter promotes to roughly several hundred
PCRE subsignatures.

Path Default (off) PCREJit/--pcre-jit on Speedup
clamscan (standalone, includes DB load) ~22.0 s ~10.5 s ~2.1x
clamdscan (resident daemon, scan-only) ~16.6 s ~5.4 s ~3.1x

Phase decomposition (daemon, scan-only) attributes the gain to the PCRE phase:

Phase Off On
Aho-Corasick / Boyer-Moore literal prefilter ~0.9 s ~0.9 s
PCRE full-match phase ~15.8 s ~4.5 s
PCRE subsignatures evaluated identical identical

Detection results were verified identical with the option on and off (EICAR
detected in both; the benign sample reported clean in both).

Testing

  • Ran the libclamav unit test suite (ENABLE_TESTS=ON) on both this branch
    and the unmodified base revision: both report Checks: 1235 with an
    identical set of results, so this change introduces no new test failures. The
    pcre, regex, and matchers suites — which exercise the changed compile
    and match paths — pass. check_matchers.c was updated to build with the JIT
    path enabled so the suite validates that matches are unchanged.
  • Confirmed the option is off by default (scan time and behavior identical to
    the prior release), --pcre-jit=yes engages JIT, and --pcre-jit=no is
    identical to the default.
  • Verified detection is unchanged in both modes with a logical signature
    containing a PCRE subsignature: matching input is detected and non-matching
    input is reported clean, identically with the option on and off. A
    known-malicious test file is detected and benign input reported clean in both
    modes.
  • Confirmed both clamscan and clamdscan/clamd honor the option.

A note on benchmarking

The repository has no in-tree performance/benchmark test harness — the test
suite is correctness-only — so the timing numbers above were measured manually
by timing identical scans with the option off and on. A wall-clock assertion
was intentionally not added, since it would be flaky across CI hardware. The
per-signature --statistics=pcre diagnostic can be used to reproduce the
PCRE-phase timing breakdown.

Compatibility

  • Disabled by default; no behavior change unless explicitly enabled.
  • New CL_ENGINE_PCRE_JIT is appended to the end of the cl_engine_field
    enum, so existing enumerator values are unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a710fc1348

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread libclamav/regex_pcre.c
* is left interpreter-only and matching proceeds unchanged. Disabled by
* default; controlled by the PCREJit / --pcre-jit option. */
if (try_jit) {
int jitrc = pcre2_jit_compile(pd->re, PCRE2_JIT_COMPLETE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve PCRE recursion limits when using JIT

When PCREJit/--pcre-jit is enabled, this compiles every pattern for JIT even though the match context just set PCRERecMatchLimit via pcre2_set_recursion_limit(). PCRE2 documents that the depth/recursion limit is ignored for JIT matches (https://man7.org/linux/man-pages/man3/pcre2api.3.html), so signatures whose recursive backtracking would previously stop with the configured recmatch limit can now continue and match/run until another limit is hit; this changes the advertised detection/resource-bound behavior for deployments that tune PCRERecMatchLimit. Please avoid JIT for matches where the recmatch limit must be enforced, or otherwise force the interpreter for those cases.

Useful? React with 👍 / 👎.

ClamAV compiles regex subsignatures with pcre2_compile() and matches them with
the interpreted pcre2_match(), and never enables the PCRE2 JIT compiler even
when the linked PCRE2 library supports it. For scans where the Aho-Corasick
prefilter promotes many PCRE subsignatures over a large buffer, the interpreted
matcher dominates scan time.

Add a 'PCREJit' clamd.conf option and a '--pcre-jit' clamscan option, exposed
through a new CL_ENGINE_PCRE_JIT engine setting, mirroring how the existing
PCRE limit options are plumbed (engine field, set/get, settings copy/apply).
The option is disabled by default to preserve existing behavior on upgrade.

When enabled, cli_pcre_compile() performs a best-effort pcre2_jit_compile()
after setting the match limits. pcre2_match() automatically dispatches to the
JIT code when a pattern has been JIT-compiled, so no change to the match call
is required for the common path. If JIT compilation fails (PCRE2 built without
JIT, or an unsupported construct), the failure is logged at debug level and
matching falls back to the interpreter.

To guarantee identical results to the interpreter when JIT is enabled,
cli_pcre_match() now retries a match with PCRE2_NO_JIT if the JIT matcher
returns PCRE2_ERROR_JIT_STACKLIMIT (the JIT uses a fixed stack and does not
grow on demand like the interpreter).

On a workload that promotes several hundred PCRE subsignatures over a ~37 MiB
buffer, enabling the option reduced the PCRE phase roughly 3-4x and overall
scan time by about 2x, with no change in detection.
@j20120307
j20120307 force-pushed the feature/pcre2-jit branch from a710fc1 to 01dc512 Compare June 17, 2026 23:29
@j20120307

Copy link
Copy Markdown
Author

Thanks — good catch. Confirmed against the PCRE2 docs: pcre2_set_recursion_limit() is the deprecated alias for pcre2_set_depth_limit(), and per pcre2jit(3) "the PCRE2_ERROR_DEPTHLIMIT error code is never returned when JIT matching is used", so PCRERecMatchLimit is not honored for JIT matches.

Worth noting the overall PCREMatchLimit (pcre2_set_match_limit) is still enforced under JIT, so total match work stays bounded; it's specifically the separate depth/recursion bound that's lost.

Since this is an opt-in feature (disabled by default), I've documented the tradeoff rather than changing the match path: the caveat is now in the PCREJit/--pcre-jit option help, clamd.conf.sample, NEWS.md, and a code comment at the JIT-compile site, advising operators who rely on PCRERecMatchLimit to leave JIT disabled.

Happy to instead force the interpreter when PCRERecMatchLimit is set below the default if you'd prefer the limit always be enforceable.

@juancarlosgd

Copy link
Copy Markdown

Independent verification of this patch, FWIW — evaluating it for a production
security-scanning pipeline (SQS-driven async malware scan, real user-uploaded
Office/PDF files, AWS Lambda arm64).

Setup: applied cleanly against clamav-1.5.3 (no rebase needed — the 4 core
matcher files, regex_pcre.c/h + matcher-pcre.c/h, are byte-identical between
this PR's base and the 1.5.3 tag; only NEWS.md conflicted, dropped from the
vendored patch). Compiled from source (CMake), Rust toolchain via rustup (apt's
cargo/rustc on Debian bookworm is too old for the Cargo.lock v4 format).

Benchmark: real 74.6MB PPTX (not synthetic/random bytes — file structure
matters a lot for PCRE-heavy scans, flat entropy blobs are misleading), same
container, CPU/memory constrained to match our real Lambda allocation
(--memory=4096m --cpus=2.32, i.e. AWS's ~1769MB-per-vCPU ratio):

Config Time
PCREJit no (default) 56.6-57.2s
PCREJit yes 20.6-20.9s

~2.75x, consistent across 3 separate runs (different content each time, to
avoid clamd's hash-based result cache) and across two different CPU/memory
tiers we tested (3072MB/1.74vCPU and 4096MB/2.32vCPU — same speedup regardless
of core count, as expected for a compile-vs-interpret improvement).

Detection verified identical with the option on/off: EICAR detected correctly
in both modes, and the real test file gave the same clean verdict both ways.

Happy to share more detail if useful for review.

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.

2 participants