Skip to content

Commit 01dc512

Browse files
author
zkan
committed
Add option to enable PCRE2 JIT for regex subsignature matching
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.
1 parent 0c7468e commit 01dc512

16 files changed

Lines changed: 92 additions & 10 deletions

File tree

NEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ ClamAV 1.6.0 includes the following improvements and changes:
1515

1616
### Other improvements
1717

18+
- Added an option to enable the PCRE2 JIT compiler for regex subsignature
19+
matching. When enabled, patterns are JIT-compiled at database load and matched
20+
with native code, significantly reducing scan time for content that triggers
21+
many PCRE subsignatures. The new option is `PCREJit` in `clamd.conf` and
22+
`--pcre-jit` for `clamscan`, and is disabled by default. It requires the
23+
linked PCRE2 library to be built with JIT support; otherwise matching
24+
transparently falls back to the interpreter. Detection results are unchanged.
25+
Note: PCRE2 does not enforce the depth/recursion limit (`PCRERecMatchLimit`)
26+
for JIT matches, though `PCREMatchLimit` is still enforced; leave the option
27+
disabled if you rely on `PCRERecMatchLimit` to bound recursive backtracking.
28+
1829
### Bug fixes
1930

2031
### Acknowledgments

clamd/clamd.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,14 @@ int main(int argc, char **argv)
741741
}
742742
}
743743

744+
if ((opt = optget(opts, "PCREJit"))->active) {
745+
if ((ret = cl_engine_set_num(engine, CL_ENGINE_PCRE_JIT, opt->enabled))) {
746+
logg(LOGG_ERROR, "cli_engine_set_num(PCREJit) failed: %s\n", cl_strerror(ret));
747+
cl_engine_free(engine);
748+
return 1;
749+
}
750+
}
751+
744752
if ((opt = optget(opts, "PCRERecMatchLimit"))->active) {
745753
if ((ret = cl_engine_set_num(engine, CL_ENGINE_PCRE_RECMATCH_LIMIT, opt->numarg))) {
746754
logg(LOGG_ERROR, "cli_engine_set_num(PCRERecMatchLimit) failed: %s\n", cl_strerror(ret));

clamscan/clamscan.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ void help(void)
363363
mprintf(LOGG_INFO, " --pcre-match-limit=#n Maximum calls to the PCRE match function.\n");
364364
mprintf(LOGG_INFO, " --pcre-recmatch-limit=#n Maximum recursive calls to the PCRE match function.\n");
365365
mprintf(LOGG_INFO, " --pcre-max-filesize=#n Maximum size file to perform PCRE subsig matching.\n");
366+
mprintf(LOGG_INFO, " --pcre-jit[=yes/no(*)] JIT-compile PCRE subsignatures for faster matching (requires PCRE2 JIT support; pcre-recmatch-limit is not enforced for JIT matches).\n");
366367
mprintf(LOGG_INFO, " --disable-cache Disable caching and cache checks for hash sums of scanned files.\n");
367368
mprintf(LOGG_INFO, " --hash-hint The file hash so that libclamav does not need to calculate it.\n");
368369
mprintf(LOGG_INFO, " The type of hash must match the '--hash-alg'.\n");

clamscan/manager.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,14 @@ int scanmanager(const struct optstruct *opts)
15101510
}
15111511
}
15121512

1513+
if ((opt = optget(opts, "pcre-jit"))->active) {
1514+
if ((ret = cl_engine_set_num(engine, CL_ENGINE_PCRE_JIT, opt->enabled))) {
1515+
logg(LOGG_ERROR, "cli_engine_set_num(CL_ENGINE_PCRE_JIT) failed: %s\n", cl_strerror(ret));
1516+
ret = 2;
1517+
goto done;
1518+
}
1519+
}
1520+
15131521
if ((opt = optget(opts, "pcre-recmatch-limit"))->active) {
15141522
if ((ret = cl_engine_set_num(engine, CL_ENGINE_PCRE_RECMATCH_LIMIT, opt->numarg))) {
15151523
logg(LOGG_ERROR, "cli_engine_set_num(CL_ENGINE_PCRE_RECMATCH_LIMIT) failed: %s\n", cl_strerror(ret));

common/optparser.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,8 @@ const struct clam_option __clam_options[] = {
517517

518518
{"PCREMaxFileSize", "pcre-max-filesize", 0, CLOPT_TYPE_SIZE, MATCH_SIZE, CLI_DEFAULT_PCRE_MAX_FILESIZE, NULL, 0, OPT_CLAMD | OPT_CLAMSCAN, "This option sets the maximum filesize for which PCRE subsigs will be executed.\nFiles exceeding this limit will not have PCRE subsigs executed unless a subsig is encompassed to a smaller buffer.\nNegative values are not allowed.\nSetting this value to zero disables the limit.\nWARNING: setting this limit too high or disabling it may severely impact performance.", "100M"},
519519

520+
{"PCREJit", "pcre-jit", 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 0, NULL, 0, OPT_CLAMD | OPT_CLAMSCAN, "When enabled, PCRE regex subsignatures are JIT-compiled at database load and matched with native code, which can significantly reduce scan time for content that triggers many PCRE subsignatures.\nThis requires the linked PCRE2 library to be built with JIT support; otherwise matching falls back to the interpreter with no functional change.\nDetection results are unchanged when enabled.\nNote: PCRE2 does not enforce the depth/recursion limit (PCRERecMatchLimit) for JIT matches; PCREMatchLimit is still enforced. Leave this disabled if you rely on PCRERecMatchLimit to bound recursive backtracking.", "no"},
521+
520522
/* OnAccess settings */
521523
{"OnAccessMountPath", NULL, 0, CLOPT_TYPE_STRING, NULL, -1, NULL, FLAG_MULTIPLE, OPT_CLAMD, "This option specifies a directory or mount point which should be scanned on access. The mount point specified, or the mount point containing the specified directory will be watched, but only notifications will occur. If any directories are specified, this option will preempt the DDD system. It can also be used multiple times.", "/\n/home/user"},
522524

etc/clamd.conf.sample

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,17 @@ Example
748748
# Default: 100M
749749
#PCREMaxFileSize 400M
750750

751+
# When enabled, PCRE regex subsignatures are JIT-compiled at database load and
752+
# matched with native code. This can significantly reduce scan time for content
753+
# that triggers many PCRE subsignatures. Requires the linked PCRE2 library to be
754+
# built with JIT support; otherwise matching transparently falls back to the
755+
# interpreter. Detection results are unchanged when enabled.
756+
# Note: PCRE2 does not enforce the depth/recursion limit (PCRERecMatchLimit) for
757+
# JIT matches; PCREMatchLimit is still enforced. Leave this disabled if you rely
758+
# on PCRERecMatchLimit to bound recursive backtracking.
759+
# Default: no
760+
#PCREJit yes
761+
751762
# When AlertExceedsMax is set, files exceeding the MaxFileSize, MaxScanSize, or
752763
# MaxRecursion limit will be flagged with the virus name starting with
753764
# "Heuristics.Limits.Exceeded".

libclamav/clamav.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ enum cl_engine_field {
343343
CL_ENGINE_CVDCERTSDIR, /** (char *) */
344344
CL_ENGINE_TMPDIR_RECURSION, /** uint32_t */
345345
CL_ENGINE_FIPS_LIMITS, /** uint32_t */
346+
CL_ENGINE_PCRE_JIT, /** uint32_t */
346347
};
347348

348349
enum bytecode_security {

libclamav/default.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
#define CLI_DEFAULT_PCRE_MATCH_LIMIT 100000
5858
#define CLI_DEFAULT_PCRE_RECMATCH_LIMIT 2000
5959
#define CLI_DEFAULT_PCRE_MAX_FILESIZE (1024 * 1024 * 100) // 100 MB
60+
#define CLI_DEFAULT_PCRE_JIT 0 // PCRE2 JIT disabled by default
6061

6162
/* Maximums */
6263
#define CLI_MAX_MAXRECLEVEL 100

libclamav/matcher-pcre.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ cl_error_t cli_pcre_addpatt(struct cli_matcher *root, const char *virname, const
366366
return CL_SUCCESS;
367367
}
368368

369-
cl_error_t cli_pcre_build(struct cli_matcher *root, long long unsigned match_limit, long long unsigned recmatch_limit, const struct cli_dconf *dconf)
369+
cl_error_t cli_pcre_build(struct cli_matcher *root, long long unsigned match_limit, long long unsigned recmatch_limit, int try_jit, const struct cli_dconf *dconf)
370370
{
371371
unsigned int i;
372372
cl_error_t ret;
@@ -408,11 +408,11 @@ cl_error_t cli_pcre_build(struct cli_matcher *root, long long unsigned match_lim
408408
if (dconf && (dconf->pcre & PCRE_CONF_OPTIONS)) {
409409
/* compile the regex, no options override *wink* */
410410
pm_dbgmsg("cli_pcre_build: Compiling regex: /%s/\n", pm->pdata.expression);
411-
ret = cli_pcre_compile(&(pm->pdata), match_limit, recmatch_limit, 0, 0);
411+
ret = cli_pcre_compile(&(pm->pdata), match_limit, recmatch_limit, 0, 0, try_jit);
412412
} else {
413413
/* compile the regex, options overridden and disabled */
414414
pm_dbgmsg("cli_pcre_build: Compiling regex: /%s/ (without options)\n", pm->pdata.expression);
415-
ret = cli_pcre_compile(&(pm->pdata), match_limit, recmatch_limit, 0, 1);
415+
ret = cli_pcre_compile(&(pm->pdata), match_limit, recmatch_limit, 0, 1, try_jit);
416416
}
417417
if (ret != CL_SUCCESS) {
418418
cli_errmsg("cli_pcre_build: failed to build pcre regex\n");

libclamav/matcher-pcre.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ cl_error_t cli_pcre_addpatt(struct cli_matcher *root, const char *virname, const
6969
void cli_pcre_freemeta(struct cli_matcher *root, struct cli_pcre_meta *pm);
7070
void cli_pcre_freetable(struct cli_matcher *root);
7171

72-
cl_error_t cli_pcre_build(struct cli_matcher *root, long long unsigned match_limit, long long unsigned recmatch_limit, const struct cli_dconf *dconf);
72+
cl_error_t cli_pcre_build(struct cli_matcher *root, long long unsigned match_limit, long long unsigned recmatch_limit, int try_jit, const struct cli_dconf *dconf);
7373
cl_error_t cli_pcre_scanbuf(const unsigned char *buffer, uint32_t length, const char **virname, struct cli_ac_result **res, const struct cli_matcher *root, struct cli_ac_data *mdata, const struct cli_pcre_off *data, cli_ctx *ctx);
7474
cl_error_t cli_pcre_recaloff(struct cli_matcher *root, struct cli_pcre_off *data, struct cli_target_info *info, cli_ctx *ctx);
7575
void cli_pcre_freeoff(struct cli_pcre_off *data);

0 commit comments

Comments
 (0)