From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mail02.haj.ipfire.org (localhost [IPv6:::1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4fbX5c6q1Tz3089 for ; Wed, 18 Mar 2026 14:58:44 +0000 (UTC) Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519) (Client CN "mail01.haj.ipfire.org", Issuer "R12" (not verified)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4fbX5W2MDsz2xQW for ; Wed, 18 Mar 2026 14:58:39 +0000 (UTC) Received: from [127.0.0.1] (localhost [127.0.0.1]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail01.ipfire.org (Postfix) with ESMTPSA id 4fbX5V2cKpz12S; Wed, 18 Mar 2026 14:58:38 +0000 (UTC) DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=ipfire.org; s=202003ed25519; t=1773845918; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=mBdgbwEyp/i44cWhunmBS+Rjx/eRZ4sOA6C8k14IaW0=; b=pfp+RpIijosgSPYOTf8aO53EVcBbvQiWoFq0msBCZJjVveZs3dlgXo+QXDHKORcx5XJh/j I5H7ixvapE40eFDA== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=ipfire.org; s=202003rsa; t=1773845918; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=mBdgbwEyp/i44cWhunmBS+Rjx/eRZ4sOA6C8k14IaW0=; b=Y5GV0IBIEbq/++21glk4RGZ9++qk/R3GzpsOzUKxWfnms/0v0B5XzEWK4JPqN5y/I0uhHe kn1Ip4FMIDZ0RQopynGQsdcLf77Yda210D1NcpIimiI/jTNEwXlbQcE1IR60/9qIR6w0z3 yZRTZAJd/RwYXB59lLxAcDhtVTO0t3Z/OcgHjvctDUZu1YM5wl0FDOa99pO/dh4ghUnO0F bv39UVC98X03t92YSBiFykJIpht22uba1v0+7bd4hc9PdFE6IjFTLR4aUBXj5x9Cu+qU0b f3YkajCCGOicCH+grUBNyv6ZsBk3pM0I8i0I7VMqNPclw0n7lUchJZa1y7zd2g== Content-Type: text/plain; charset=utf-8 Precedence: list List-Id: List-Subscribe: , List-Unsubscribe: , List-Post: List-Help: Sender: Mail-Followup-To: Mime-Version: 1.0 Subject: Re: [PATCH] suricata: Update to 8.0.4 From: Michael Tremer In-Reply-To: <20260318134024.2600353-1-matthias.fischer@ipfire.org> Date: Wed, 18 Mar 2026 14:58:37 +0000 Cc: development@lists.ipfire.org Content-Transfer-Encoding: quoted-printable Message-Id: References: <20260318134024.2600353-1-matthias.fischer@ipfire.org> To: Matthias Fischer , Stefan Schantl Hello Matthias, Excellent. I merged this and back ported it into Core Update 201 as = well. @Stefan: Since we have now dropped the cleanup patch, are there any Rust = dependencies that we no longer need and can therefore drop as well? -Michael > On 18 Mar 2026, at 13:39, Matthias Fischer = wrote: >=20 > The contents of =E2=80=98suricata-8.0.3-purge-hyperscan-cache.patch=E2=80= =99 have been integrated in 8.0.4, > and the sources for 'humantime' are now included under = '/rust/vendor/humantime'. > The lfs and the rootfile have been updated. >=20 > Build is running without seen problems. >=20 > Excerpt from changelog: >=20 > "8.0.4 -- 2026-03-12 >=20 > Security #8306: krb5: internal request/response buffering leads to = quadratic complexity (8.0.x backport)(HIGH - CVE 2026-31932) > Security #8297: detect/ssl: null deref with tls.alpn keyword (8.0.x = backport)(HIGH - CVE 2026-31931) > Security #8295: http2: unbounded number of http2 frames per = transaction (8.0.x backport)(CRITICAL - CVE 2026-31935) > Security #8293: smtp/mime: quadratic complexity while looking for url = strings (8.0.x backport)(HIGH - CVE 2026-31934) > Security #8287: krb5: TCP parser never advances past the first record = in a multi-record segment (8.0.x backport) > Bug #8371: dpdk: "auto" in mempool size undercalculates the mempool = size for Rx/Tx descriptors (8.0.x backport) > Bug #8369: ldap: add ldap.rules file (8.0.x backport) > Bug #8367: ndpi: crashing in StorageGetById() (8.0.x backport) > Bug #8362: http2: detection should use a better architecture than the = Vec escaped (8.0.x backport) > Bug #8357: ldap: abandon request incorrectly handled (8.0.x backport) > Bug #8326: hs: harden cache manipulation (8.0.x backport) > Bug #8317: ldap: no invalid_data event in case of invalid request = (8.0.x backport) > Bug #8312: firewall: af-packet IPS mode overwrites firewall mode = (8.0.x backport) > Bug #8309: plugins/ndpi: SIGSEGV in DetectnDPIProtocolPacketMatch = (8.0.x backport) > Bug #8280: build: when documentation tools are install, make dist = attempt to install files to prefix (8.0.x backport) > Bug #8268: Double log rotation with rotation flag/interval (8.0.x = backport) > Bug #8260: lib: examples fail with debug validation as they create = threads after threads are sealed (8.0.x backport) > Bug #8252: dpdk: (x)stats are only accessible before port stop (8.0.x = backport) > Bug #8249: lua: calling metatable garbage collector with nil from a = script leadsd to a null pointer dereference (8.0.x backport) > Bug #8244: hyperscan: coverity warning on stat path check (8.0.x = backport) > Bug #8230: detect/app-layer-event: alert generated for the wrong = packet (8.0.x backport) > Bug #8219: base64: base64_data with relative match after = base64_decode:relative fails (8.0.x backport) > Bug #8207: firewall: loading rules only through yaml fails (8.0.x = backport) > Bug #8167: utils-spm-hs: missing deallocators on hs_compile failure = (8.0.x backport) > Bug #8164: decode/ipv6: set invalid event for wrong ip version (8.0.x = backport) > Bug #7982: detect/tls: zero characters in keywords such as alt name = are mishandled (8.0.x backport) > Optimization #8343: conf: stream.depth is unlimited when absent from = the suricata.yaml > Optimization #8299: stream/tcp: flag 1st seen pkt w stream established = (8.0.x backport) > Feature #8323: hs: add pruning stats details of removal reason (8.0.x = backport) > Feature #8316: firewall: support iprep in firewall mode (8.0.x = backport) > Feature #8235: rules/transform: add gunzip transform (8.0.x backport) > Feature #8233: nfs: log detailed response for versions other than v3 = (8.0.x backport) > Feature #7893: hyperscan: support cache invalidation and removal = (8.0.x backport) > Task #8270: rust: suppress nugatory RUSTSEC-2026-0009 for time crate = (8.0.x backport) > Task #8194: psl: crate should be updated on every release (8.0.x = backport) > Task #8159: build-scopes: add QA or SIMULATION mode (8.0.x backport) > Task #8097: libsuricata: add live example usage of the Suricata = library (8.0.x backport) > Documentation #8331: doc: explain dcerpc.opnum doesn't support = operators >,<,!,=3D (8.0.x backport) > Documentation #8263: doc/userguide: fix within-distance pointer = graphics in payload-keywords doc (8.0.x backport) > Documentation #8240: isdataat: document different semantics between = absolute and relative modes (8.0.x backport) > Documentation #8217: rules/endswith: doc wrong for = offset/distance/within warning (8.0.x backport) > Documentation #8114: doc: remove mention of suricata-7 in latest docs = (8.0.x backport) > Documentation #7932: devguide: add a chapter about Suricata's = exception policies (8.0.x backport)" >=20 > Signed-off-by: Matthias Fischer > --- > config/rootfiles/common/suricata | 2 +- > lfs/suricata | 11 +- > ...suricata-8.0.3-purge-hyperscan-cache.patch | 1341 ----------------- > 3 files changed, 3 insertions(+), 1351 deletions(-) > delete mode 100644 = src/patches/suricata/suricata-8.0.3-purge-hyperscan-cache.patch >=20 > diff --git a/config/rootfiles/common/suricata = b/config/rootfiles/common/suricata > index 518920abd..2d77b74a9 100644 > --- a/config/rootfiles/common/suricata > +++ b/config/rootfiles/common/suricata > @@ -8,7 +8,6 @@ usr/sbin/convert-ids-backend-files > #usr/share/doc/suricata > #usr/share/doc/suricata/AUTHORS > #usr/share/doc/suricata/Basic_Setup.txt > -#usr/share/doc/suricata/GITGUIDE > #usr/share/doc/suricata/INSTALL > #usr/share/doc/suricata/NEWS > #usr/share/doc/suricata/README > @@ -35,6 +34,7 @@ usr/share/suricata > #usr/share/suricata/rules/http2-events.rules > #usr/share/suricata/rules/ipsec-events.rules > #usr/share/suricata/rules/kerberos-events.rules > +#usr/share/suricata/rules/ldap-events.rules > #usr/share/suricata/rules/mdns-events.rules > #usr/share/suricata/rules/modbus-events.rules > #usr/share/suricata/rules/mqtt-events.rules > diff --git a/lfs/suricata b/lfs/suricata > index a20450c31..419257017 100644 > --- a/lfs/suricata > +++ b/lfs/suricata > @@ -24,7 +24,7 @@ >=20 > include Config >=20 > -VER =3D 8.0.3 > +VER =3D 8.0.4 >=20 > THISAPP =3D suricata-$(VER) > DL_FILE =3D $(THISAPP).tar.gz > @@ -40,7 +40,7 @@ objects =3D $(DL_FILE) >=20 > $(DL_FILE) =3D $(DL_FROM)/$(DL_FILE) >=20 > -$(DL_FILE)_BLAKE2 =3D = ab87fde815338a7520badd2f4d8c8bfaccc778ecffbb13028fe9d561b1bf0e4ef2a43296b8= 8fffb306df9e28fcd5997fa22c72ac887c40efbea799e0110fcb56 > +$(DL_FILE)_BLAKE2 =3D = a6c1958d82bb8c288c8d551d99851d19a89073397bda38bc90907950d17c35e40eb4845e9a= 88913bafc5c56bdad8c026e0fb665c494b102861c2b8f210c72d7f >=20 > install : $(TARGET) >=20 > @@ -71,13 +71,6 @@ $(TARGET) : $(patsubst %,$(DIR_DL)/%,$(objects)) > @$(PREBUILD) > @rm -rf $(DIR_APP) && cd $(DIR_SRC) && tar zxf $(DIR_DL)/$(DL_FILE) > cd $(DIR_APP) && patch -Np1 < = $(DIR_SRC)/src/patches/suricata/suricata-8.0.0-disable-sid-2210059.patch > - cd $(DIR_APP) && patch -Np1 < = $(DIR_SRC)/src/patches/suricata/suricata-8.0.3-purge-hyperscan-cache.patch= > - > - # Temporary workaround because the suricata 8.0.3 tarball does not = contain the rust source as trusted vendor > - # for humantime and the module is required since applying the = purge-hyperscan-cache patchfile. > - # > - # So we have to copy our installed rust module into the desired = directory here. > - cd $(DIR_APP) && cp -avf /usr/share/cargo/registry/humantime* = $(DIR_APP)/rust/vendor >=20 > cd $(DIR_APP) && LDFLAGS=3D"$(LDFLAGS)" ./configure \ > --prefix=3D/usr \ > diff --git = a/src/patches/suricata/suricata-8.0.3-purge-hyperscan-cache.patch = b/src/patches/suricata/suricata-8.0.3-purge-hyperscan-cache.patch > deleted file mode 100644 > index 14f36985d..000000000 > --- a/src/patches/suricata/suricata-8.0.3-purge-hyperscan-cache.patch > +++ /dev/null > @@ -1,1341 +0,0 @@ > -commit 47fc78eeae9a365b4d36609154642ca72c9cb9fb > -Author: Lukas Sismis > -Date: Mon Sep 15 11:40:30 2025 +0200 > - > - hs: update the file description > - > -diff --git a/src/util-mpm-hs-cache.c b/src/util-mpm-hs-cache.c > -index 2e58676fa..fd54cf306 100644 > ---- a/src/util-mpm-hs-cache.c > -+++ b/src/util-mpm-hs-cache.c > -@@ -20,7 +20,7 @@ > - * > - * \author Lukas Sismis > - * > -- * MPM pattern matcher that calls the Hyperscan regex matcher. > -+ * Hyperscan cache helper utilities for MPM cache files. > - */ > -=20 > - #include "suricata-common.h" > -commit 2a313ff429eb49be5e4c3b9dadfca127fa64c5fe > -Author: Lukas Sismis > -Date: Thu Oct 30 12:01:33 2025 +0100 > - > - hs: reduce cache filename size to max file limit > - > -diff --git a/src/util-mpm-hs-cache.c b/src/util-mpm-hs-cache.c > -index fd54cf306..1e5001ba0 100644 > ---- a/src/util-mpm-hs-cache.c > -+++ b/src/util-mpm-hs-cache.c > -@@ -41,7 +41,7 @@ static const char *HSCacheConstructFPath(const char = *folder_path, uint64_t hs_db > - static char hash_file_path[PATH_MAX]; > -=20 > - char hash_file_path_suffix[] =3D "_v1.hs"; > -- char filename[PATH_MAX]; > -+ char filename[NAME_MAX]; > - uint64_t r =3D snprintf( > - filename, sizeof(filename), "%020" PRIu64 "%s", = hs_db_hash, hash_file_path_suffix); > - if (r !=3D (uint64_t)(20 + strlen(hash_file_path_suffix))) > -commit c282880174875fab6bcc62a2a60c85b58dfb0d32 > -Author: Lukas Sismis > -Date: Thu Oct 30 12:04:35 2025 +0100 > - > - hs: change hash in the cache name to SHA256 > - > -diff --git a/src/util-mpm-hs-cache.c b/src/util-mpm-hs-cache.c > -index 1e5001ba0..83bbee59c 100644 > ---- a/src/util-mpm-hs-cache.c > -+++ b/src/util-mpm-hs-cache.c > -@@ -34,17 +34,17 @@ > -=20 > - #ifdef BUILD_HYPERSCAN > -=20 > -+#include "rust.h" > - #include > -=20 > --static const char *HSCacheConstructFPath(const char *folder_path, = uint64_t hs_db_hash) > -+static const char *HSCacheConstructFPath(const char *folder_path, = const char *hs_db_hash) > - { > - static char hash_file_path[PATH_MAX]; > -=20 > - char hash_file_path_suffix[] =3D "_v1.hs"; > - char filename[NAME_MAX]; > -- uint64_t r =3D snprintf( > -- filename, sizeof(filename), "%020" PRIu64 "%s", = hs_db_hash, hash_file_path_suffix); > -- if (r !=3D (uint64_t)(20 + strlen(hash_file_path_suffix))) > -+ uint64_t r =3D snprintf(filename, sizeof(filename), "%s%s", = hs_db_hash, hash_file_path_suffix); > -+ if (r !=3D (uint64_t)(strlen(hs_db_hash) + = strlen(hash_file_path_suffix))) > - return NULL; > -=20 > - r =3D PathMerge(hash_file_path, sizeof(hash_file_path), = folder_path, filename); > -@@ -104,22 +104,22 @@ static char *HSReadStream(const char = *file_path, size_t *buffer_sz) > - * Function to hash the searched pattern, only things relevant to = Hyperscan > - * compilation are hashed. > - */ > --static void SCHSCachePatternHash(const SCHSPattern *p, uint32_t *h1, = uint32_t *h2) > -+static void SCHSCachePatternHash(const SCHSPattern *p, SCSha256 = *sha256) > - { > - BUG_ON(p->original_pat =3D=3D NULL); > - BUG_ON(p->sids =3D=3D NULL); > -=20 > -- hashlittle2_safe(&p->len, sizeof(p->len), h1, h2); > -- hashlittle2_safe(&p->flags, sizeof(p->flags), h1, h2); > -- hashlittle2_safe(p->original_pat, p->len, h1, h2); > -- hashlittle2_safe(&p->id, sizeof(p->id), h1, h2); > -- hashlittle2_safe(&p->offset, sizeof(p->offset), h1, h2); > -- hashlittle2_safe(&p->depth, sizeof(p->depth), h1, h2); > -- hashlittle2_safe(&p->sids_size, sizeof(p->sids_size), h1, h2); > -- hashlittle2_safe(p->sids, p->sids_size * sizeof(SigIntId), h1, = h2); > -+ SCSha256Update(sha256, (const uint8_t *)&p->len, = sizeof(p->len)); > -+ SCSha256Update(sha256, (const uint8_t *)&p->flags, = sizeof(p->flags)); > -+ SCSha256Update(sha256, (const uint8_t *)p->original_pat, = p->len); > -+ SCSha256Update(sha256, (const uint8_t *)&p->id, sizeof(p->id)); > -+ SCSha256Update(sha256, (const uint8_t *)&p->offset, = sizeof(p->offset)); > -+ SCSha256Update(sha256, (const uint8_t *)&p->depth, = sizeof(p->depth)); > -+ SCSha256Update(sha256, (const uint8_t *)&p->sids_size, = sizeof(p->sids_size)); > -+ SCSha256Update(sha256, (const uint8_t *)p->sids, p->sids_size * = sizeof(SigIntId)); > - } > -=20 > --int HSLoadCache(hs_database_t **hs_db, uint64_t hs_db_hash, const = char *dirpath) > -+int HSLoadCache(hs_database_t **hs_db, const char *hs_db_hash, const = char *dirpath) > - { > - const char *hash_file_static =3D HSCacheConstructFPath(dirpath, = hs_db_hash); > - if (hash_file_static =3D=3D NULL) > -@@ -161,7 +161,7 @@ freeup: > - return ret; > - } > -=20 > --static int HSSaveCache(hs_database_t *hs_db, uint64_t hs_db_hash, = const char *dstpath) > -+static int HSSaveCache(hs_database_t *hs_db, const char *hs_db_hash, = const char *dstpath) > - { > - static bool notified =3D false; > - char *db_stream =3D NULL; > -@@ -220,14 +220,26 @@ cleanup: > - return ret; > - } > -=20 > --uint64_t HSHashDb(const PatternDatabase *pd) > -+int HSHashDb(const PatternDatabase *pd, char *hash, size_t hash_len) > - { > -- uint32_t hash[2] =3D { 0 }; > -- hashword2(&pd->pattern_cnt, 1, &hash[0], &hash[1]); > -+ SCSha256 *hasher =3D SCSha256New(); > -+ if (hasher =3D=3D NULL) { > -+ SCLogDebug("sha256 hashing failed"); > -+ return -1; > -+ } > -+ SCSha256Update(hasher, (const uint8_t *)&pd->pattern_cnt, = sizeof(pd->pattern_cnt)); > - for (uint32_t i =3D 0; i < pd->pattern_cnt; i++) { > -- SCHSCachePatternHash(pd->parray[i], &hash[0], &hash[1]); > -+ SCHSCachePatternHash(pd->parray[i], hasher); > -+ } > -+ > -+ if (!SCSha256FinalizeToHex(hasher, hash, hash_len)) { > -+ hasher =3D NULL; > -+ SCLogDebug("sha256 hashing failed"); > -+ return -1; > - } > -- return ((uint64_t)hash[1] << 32) | hash[0]; > -+ > -+ hasher =3D NULL; > -+ return 0; > - } > -=20 > - void HSSaveCacheIterator(void *data, void *aux) > -@@ -244,7 +256,11 @@ void HSSaveCacheIterator(void *data, void *aux) > - return; > - } > -=20 > -- if (HSSaveCache(pd->hs_db, HSHashDb(pd), iter_data->cache_path) = =3D=3D 0) { > -+ char hs_db_hash[SC_SHA256_LEN * 2 + 1]; // * 2 for hex +1 for = nul terminator > -+ if (HSHashDb(pd, hs_db_hash, ARRAY_SIZE(hs_db_hash)) !=3D 0) { > -+ return; > -+ } > -+ if (HSSaveCache(pd->hs_db, hs_db_hash, iter_data->cache_path) =3D=3D= 0) { > - pd->cached =3D true; // for rule reloads > - iter_data->pd_stats->hs_dbs_cache_saved_cnt++; > - } > -diff --git a/src/util-mpm-hs-cache.h b/src/util-mpm-hs-cache.h > -index 237762d5a..225c5001a 100644 > ---- a/src/util-mpm-hs-cache.h > -+++ b/src/util-mpm-hs-cache.h > -@@ -35,8 +35,8 @@ struct HsIteratorData { > - const char *cache_path; > - }; > -=20 > --int HSLoadCache(hs_database_t **hs_db, uint64_t hs_db_hash, const = char *dirpath); > --uint64_t HSHashDb(const PatternDatabase *pd); > -+int HSLoadCache(hs_database_t **hs_db, const char *hs_db_hash, const = char *dirpath); > -+int HSHashDb(const PatternDatabase *pd, char *hash, size_t = hash_len); > - void HSSaveCacheIterator(void *data, void *aux); > - #endif /* BUILD_HYPERSCAN */ > -=20 > -diff --git a/src/util-mpm-hs.c b/src/util-mpm-hs.c > -index dde5bf36a..ad7178eb8 100644 > ---- a/src/util-mpm-hs.c > -+++ b/src/util-mpm-hs.c > -@@ -683,8 +683,11 @@ static int PatternDatabaseGetCached( > - return 0; > - } else if (cache_dir_path) { > - pd_cached =3D *pd; > -- uint64_t db_lookup_hash =3D HSHashDb(pd_cached); > -- if (HSLoadCache(&pd_cached->hs_db, db_lookup_hash, = cache_dir_path) =3D=3D 0) { > -+ char hs_db_hash[SC_SHA256_LEN * 2 + 1]; // * 2 for hex +1 = for nul terminator > -+ if (HSHashDb(pd_cached, hs_db_hash, ARRAY_SIZE(hs_db_hash)) = !=3D 0) { > -+ return -1; > -+ } > -+ if (HSLoadCache(&pd_cached->hs_db, hs_db_hash, = cache_dir_path) =3D=3D 0) { > - pd_cached->ref_cnt =3D 1; > - pd_cached->cached =3D true; > - if (HSScratchAlloc(pd_cached->hs_db) !=3D 0) { > -commit 3e4fdb2118bfcb8b2644944daded2d8c67420499 > -Author: Lukas Sismis > -Date: Sat Sep 13 11:23:16 2025 +0200 > - > - misc: time unit parsing function > - > -diff --git a/rust/Cargo.lock.in b/rust/Cargo.lock.in > -index d296a196e..d47cdd197 100644 > ---- a/rust/Cargo.lock.in > -+++ b/rust/Cargo.lock.in > -@@ -688,6 +688,12 @@ dependencies =3D [ > - "windows-sys 0.52.0", > - ] > -=20 > -+[[package]] > -+name =3D "humantime" > -+version =3D "2.3.0" > -+source =3D "registry+https://github.com/rust-lang/crates.io-index" > -+checksum =3D = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" > -+ > - [[package]] > - name =3D "indexmap" > - version =3D "2.11.4" > -@@ -1551,6 +1557,7 @@ dependencies =3D [ > - "flate2", > - "hex", > - "hkdf", > -+ "humantime", > - "ipsec-parser", > - "kerberos-parser", > - "lazy_static", > -diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in > -index 0fedea33f..22e166062 100644 > ---- a/rust/Cargo.toml.in > -+++ b/rust/Cargo.toml.in > -@@ -77,6 +77,7 @@ lazy_static =3D "~1.5.0" > - base64 =3D "~0.22.1" > - bendy =3D { version =3D "~0.3.3", default-features =3D false } > - asn1-rs =3D { version =3D "~0.6.2" } > -+humantime =3D "~2.3.0" > - ldap-parser =3D { version =3D "~0.5.0" } > - hex =3D "~0.4.3" > - psl =3D "2" > -diff --git a/rust/src/util.rs b/rust/src/util.rs > -index 9d45ae26d..2cb2da17c 100644 > ---- a/rust/src/util.rs > -+++ b/rust/src/util.rs > -@@ -17,6 +17,7 @@ > -=20 > - //! Utility module. > -=20 > -+use std::borrow::Cow; > - use std::ffi::CStr; > - use std::os::raw::c_char; > -=20 > -@@ -26,6 +27,8 @@ use nom8::combinator::verify; > - use nom8::multi::many1_count; > - use nom8::{AsChar, IResult, Parser}; > -=20 > -+use humantime::parse_duration; > -+ > - #[no_mangle] > - pub unsafe extern "C" fn SCCheckUtf8(val: *const c_char) -> bool { > - CStr::from_ptr(val).to_str().is_ok() > -@@ -63,10 +66,56 @@ pub unsafe extern "C" fn SCValidateDomain(input: = *const u8, in_len: u32) -> u32 > - return 0; > - } > -=20 > -+/// Add 's' suffix if input is only digits, and convert to lowercase = if needed. > -+fn duration_unit_normalize(input: &str) -> Cow<'_, str> { > -+ if input.bytes().all(|b| b.is_ascii_digit()) { > -+ let mut owned =3D String::with_capacity(input.len() + 1); > -+ owned.push_str(input); > -+ owned.push('s'); > -+ return Cow::Owned(owned); > -+ } > -+ > -+ if input.bytes().any(|b| b.is_ascii_uppercase()) { > -+ Cow::Owned(input.to_ascii_lowercase()) > -+ } else { > -+ Cow::Borrowed(input) > -+ } > -+} > -+ > -+/// Reads a C string from `input`, parses it, and writes the result = to `*res`. > -+/// Returns 0 on success (result written to *res), -1 otherwise. > -+#[no_mangle] > -+pub unsafe extern "C" fn SCParseTimeDuration(input: *const c_char, = res: *mut u64) -> i32 { > -+ if input.is_null() || res.is_null() { > -+ return -1; > -+ } > -+ > -+ let input_str =3D match CStr::from_ptr(input).to_str() { > -+ Ok(s) =3D> s, > -+ Err(_) =3D> return -1, > -+ }; > -+ > -+ let trimmed =3D input_str.trim(); > -+ if trimmed.is_empty() { > -+ return -1; > -+ } > -+ > -+ let normalized =3D duration_unit_normalize(trimmed); > -+ match parse_duration(normalized.as_ref()) { > -+ Ok(duration) =3D> { > -+ *res =3D duration.as_secs(); > -+ 0 > -+ } > -+ Err(_) =3D> -1, > -+ } > -+} > -+ > - #[cfg(test)] > - mod tests { > -=20 > - use super::*; > -+ use std::ffi::CString; > -+ use std::ptr::{null, null_mut}; > -=20 > - #[test] > - fn test_parse_domain() { > -@@ -83,4 +132,73 @@ mod tests { > - let buf1: &[u8] =3D "a(x)y.com".as_bytes(); > - assert!(parse_domain(buf1).is_err()); > - } > -+ > -+ #[test] > -+ fn test_parse_time_valid() { > -+ unsafe { > -+ let mut v: u64 =3D 0; > -+ > -+ let s =3D CString::new("10").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 10); > -+ > -+ let s =3D CString::new("0").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 0); > -+ > -+ let s =3D CString::new("2H").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 7200); > -+ > -+ let s =3D CString::new("1 day").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 86400); > -+ > -+ let s =3D CString::new("1w").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 604800); > -+ > -+ let s =3D CString::new("1 week").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 604800); > -+ > -+ let s =3D CString::new("1y").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 31557600); > -+ > -+ let s =3D CString::new("1 year").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, 31557600); > -+ > -+ // max > -+ let s =3D CString::new("18446744073709551615").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0); > -+ assert_eq!(v, u64::MAX); > -+ } > -+ } > -+ > -+ #[test] > -+ fn test_parse_time_duration_invalid() { > -+ unsafe { > -+ let mut v: u64 =3D 0; > -+ let s =3D CString::new("10q").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1); > -+ > -+ let s =3D CString::new("abc").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1); > -+ > -+ let s =3D CString::new("-300s").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1); > -+ > -+ let s =3D CString::new("1h -600s").unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1); > -+ > -+ assert_eq!(SCParseTimeDuration(null(), &mut v), -1); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), null_mut()), = -1); > -+ > -+ let overflow_years =3D (u64::MAX / 31557600) + 1; > -+ let s =3D CString::new(format!("{}y", = overflow_years)).unwrap(); > -+ assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1); > -+ } > -+ } > - } > -diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs > -index 3dbd2293e..7be2a12b4 100644 > ---- a/rust/sys/src/sys.rs > -+++ b/rust/sys/src/sys.rs > -@@ -701,6 +701,11 @@ extern "C" { > - name: *const ::std::os::raw::c_char, val: *mut f32, > - ) -> ::std::os::raw::c_int; > - } > -+extern "C" { > -+ pub fn SCConfGetTime( > -+ name: *const ::std::os::raw::c_char, val: *mut u64, > -+ ) -> ::std::os::raw::c_int; > -+} > - extern "C" { > - pub fn SCConfSet( > - name: *const ::std::os::raw::c_char, val: *const = ::std::os::raw::c_char, > -commit 85f0382072173c226426d4556a9d959ab0a90c34 > -Author: Lukas Sismis > -Date: Sat Sep 13 23:55:02 2025 +0200 > - > - conf: add time parsing conf function > - > -diff --git a/src/conf.c b/src/conf.c > -index 3be82529d..c81da37b4 100644 > ---- a/src/conf.c > -+++ b/src/conf.c > -@@ -42,6 +42,7 @@ > - #include "util-debug.h" > - #include "util-path.h" > - #include "util-conf.h" > -+#include "rust.h" > -=20 > - /** Maximum size of a complete domain name. */ > - #define NODE_NAME_MAX 1024 > -@@ -647,6 +648,36 @@ int SCConfGetFloat(const char *name, float *val) > - return 1; > - } > -=20 > -+/** > -+ * \brief Retrieve a configuration value as a time duration in = seconds. > -+ * > -+ * The configuration value is expected to be a string with a number > -+ * followed by an optional time-describing unit (e.g. s, seconds, = weeks, years). > -+ * If no unit is specified, seconds are assumed. > -+ * > -+ * \param name Name of configuration parameter to get. > -+ * \param val Pointer to an uint64_t that will be set the > -+ * configuration value in seconds. > -+ * > -+ * \retval 1 will be returned if the name is found and was properly > -+ * converted to a time duration, otherwise 0 will be returned. > -+ */ > -+int SCConfGetTime(const char *name, uint64_t *val) > -+{ > -+ const char *strval =3D NULL; > -+ > -+ if (SCConfGet(name, &strval) =3D=3D 0) > -+ return 0; > -+ > -+ if (strval =3D=3D NULL || strval[0] =3D=3D '\0') > -+ return 0; > -+ > -+ if (SCParseTimeDuration(strval, val) !=3D 0) > -+ return 0; > -+ > -+ return 1; > -+} > -+ > - /** > - * \brief Remove (and SCFree) the provided configuration node. > - */ > -diff --git a/src/conf.h b/src/conf.h > -index 348138998..0f3a881ac 100644 > ---- a/src/conf.h > -+++ b/src/conf.h > -@@ -67,6 +67,7 @@ int SCConfGetInt(const char *name, intmax_t *val); > - int SCConfGetBool(const char *name, int *val); > - int SCConfGetDouble(const char *name, double *val); > - int SCConfGetFloat(const char *name, float *val); > -+int SCConfGetTime(const char *name, uint64_t *val); > - int SCConfSet(const char *name, const char *val); > - int SCConfSetFromString(const char *input, int final); > - int SCConfSetFinal(const char *name, const char *val); > -commit fd3847db728536f6b345c33542f98a72fc058e8b > -Author: Lukas Sismis > -Date: Mon Sep 15 11:36:01 2025 +0200 > - > - path: signal last use of the file (touch) > - =20 > - To have a system-level overview of when was the last time the = file was > - used, update the file modification timestamp to to the current = time. > - =20 > - This is needed to remove stale cache files of the system. > - =20 > - Access time is not used as it may be, on the system level, = disabled. > - =20 > - Ticket: 7830 > - > -diff --git a/src/util-path.c b/src/util-path.c > -index 356c4a772..cde5a67ff 100644 > ---- a/src/util-path.c > -+++ b/src/util-path.c > -@@ -277,3 +277,23 @@ bool SCPathContainsTraversal(const char *path) > - #endif > - return strstr(path, pattern) !=3D NULL; > - } > -+ > -+/** > -+ * \brief Update access and modification time of an existing file to = 'now'. > -+ * \param path The file path to touch > -+ * \retval 0 on success, -1 on failure > -+ */ > -+int SCTouchFile(const char *path) > -+{ > -+ if (path =3D=3D NULL || path[0] =3D=3D '\0') { > -+ errno =3D EINVAL; > -+ return -1; > -+ } > -+#ifndef OS_WIN32 > -+ struct utimbuf ub; > -+ ub.actime =3D ub.modtime =3D time(NULL); > -+ if (utime(path, &ub) =3D=3D 0) > -+ return 0; > -+#endif > -+ return -1; > -+} > -diff --git a/src/util-path.h b/src/util-path.h > -index b2b262490..e835d847d 100644 > ---- a/src/util-path.h > -+++ b/src/util-path.h > -@@ -59,5 +59,6 @@ bool SCIsRegularFile(const struct dirent *const = dir_entry); > - char *SCRealPath(const char *path, char *resolved_path); > - const char *SCBasename(const char *path); > - bool SCPathContainsTraversal(const char *path); > -+int SCTouchFile(const char *path); > -=20 > - #endif /* SURICATA_UTIL_PATH_H */ > -commit 7031c268655aec5c44420902bbda6f7aea8eba33 > -Author: Lukas Sismis > -Date: Mon Sep 15 11:39:02 2025 +0200 > - > - hs: touch cache files on use to signal activity > - =20 > - Ticket: 7830 > - > -diff --git a/src/util-mpm-hs-cache.c b/src/util-mpm-hs-cache.c > -index 83bbee59c..41b308171 100644 > ---- a/src/util-mpm-hs-cache.c > -+++ b/src/util-mpm-hs-cache.c > -@@ -150,6 +150,10 @@ int HSLoadCache(hs_database_t **hs_db, const = char *hs_db_hash, const char *dirpa > - } > -=20 > - ret =3D 0; > -+ /* Touch file to update modification time so active caches = are retained. */ > -+ if (SCTouchFile(hash_file_static) !=3D 0) { > -+ SCLogDebug("Failed to update mtime for %s", = hash_file_static); > -+ } > - goto freeup; > - } > -=20 > -commit 08f5abe5e967bbcfbc0c11a797ef86125afd3db8 > -Author: Lukas Sismis > -Date: Sun Dec 28 00:09:29 2025 +0100 > - > - detect-engine: make mpm & spm part of MT stub ctx > - =20 > - As a intermediary step for Hyperscan (MPM) caching, > - the MPM config initialization should be part of the default > - detect engine context for later dynamic retrieval. > - =20 > - Ticket: 7830 > - > -diff --git a/src/detect-engine.c b/src/detect-engine.c > -index b6d2d4237..12b1683c5 100644 > ---- a/src/detect-engine.c > -+++ b/src/detect-engine.c > -@@ -2495,6 +2495,20 @@ static DetectEngineCtx = *DetectEngineCtxInitReal( > - de_ctx->filemagic_thread_ctx_id =3D -1; > - de_ctx->tenant_id =3D tenant_id; > -=20 > -+ de_ctx->mpm_matcher =3D PatternMatchDefaultMatcher(); > -+ de_ctx->spm_matcher =3D SinglePatternMatchDefaultMatcher(); > -+ > -+ if (mpm_table[de_ctx->mpm_matcher].ConfigInit) { > -+ de_ctx->mpm_cfg =3D = mpm_table[de_ctx->mpm_matcher].ConfigInit(); > -+ if (de_ctx->mpm_cfg =3D=3D NULL) { > -+ goto error; > -+ } > -+ } > -+ if (DetectEngineMpmCachingEnabled() && = mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet) { > -+ mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet( > -+ de_ctx->mpm_cfg, DetectEngineMpmCachingGetPath()); > -+ } > -+ > - if (type =3D=3D DETECT_ENGINE_TYPE_DD_STUB || type =3D=3D = DETECT_ENGINE_TYPE_MT_STUB) { > - de_ctx->version =3D DetectEngineGetVersion(); > - SCLogDebug("stub %u with version %u", type, = de_ctx->version); > -@@ -2511,23 +2525,8 @@ static DetectEngineCtx = *DetectEngineCtxInitReal( > - } > - de_ctx->failure_fatal =3D (failure_fatal =3D=3D 1); > -=20 > -- de_ctx->mpm_matcher =3D PatternMatchDefaultMatcher(); > -- de_ctx->spm_matcher =3D SinglePatternMatchDefaultMatcher(); > -- SCLogConfig("pattern matchers: MPM: %s, SPM: %s", > -- mpm_table[de_ctx->mpm_matcher].name, > -- spm_table[de_ctx->spm_matcher].name); > -- > -- if (mpm_table[de_ctx->mpm_matcher].ConfigInit) { > -- de_ctx->mpm_cfg =3D = mpm_table[de_ctx->mpm_matcher].ConfigInit(); > -- if (de_ctx->mpm_cfg =3D=3D NULL) { > -- goto error; > -- } > -- } > -- if (DetectEngineMpmCachingEnabled() && = mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet) { > -- mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet( > -- de_ctx->mpm_cfg, DetectEngineMpmCachingGetPath()); > -- } > -- > -+ SCLogConfig("pattern matchers: MPM: %s, SPM: %s", = mpm_table[de_ctx->mpm_matcher].name, > -+ spm_table[de_ctx->spm_matcher].name); > - de_ctx->spm_global_thread_ctx =3D = SpmInitGlobalThreadCtx(de_ctx->spm_matcher); > - if (de_ctx->spm_global_thread_ctx =3D=3D NULL) { > - SCLogDebug("Unable to alloc SpmGlobalThreadCtx."); > -commit 15c83be61ac3f47bf198fe24eb908db5a84b7ccd > -Author: Lukas Sismis > -Date: Mon Sep 15 11:24:23 2025 +0200 > - > - hs: prune stale MPM cache files > - =20 > - Hyperscan MPM can cache the compiled contexts to files. > - This however grows as rulesets change and leads to bloating > - the system. This addition prunes the stale cache files based > - on their modified file timestamp. > - =20 > - Part of this work incorporates new model for MPM cache stats > - to split it out from the cache save function and aggregate > - cache-related stats in one place (newly added pruning). > - =20 > - Ticket: 7830 > - > -diff --git a/doc/userguide/performance/hyperscan.rst = b/doc/userguide/performance/hyperscan.rst > -index 065163110..1060d3aef 100644 > ---- a/doc/userguide/performance/hyperscan.rst > -+++ b/doc/userguide/performance/hyperscan.rst > -@@ -83,6 +83,8 @@ if it is present on the system in case of the = "auto" setting. > - If the current suricata installation does not have hyperscan > - support, refer to :ref:`installation` > -=20 > -+.. _hyperscan-cache-configuration: > -+ > - Hyperscan caching > - ~~~~~~~~~~~~~~~~~ > -=20 > -@@ -104,6 +106,24 @@ To enable this function, in `suricata.yaml` = configure: > - sgh-mpm-caching-path: /var/lib/suricata/cache/hs > -=20 > -=20 > -+To avoid cache files growing indefinitely, Suricata supports pruning = of old > -+cache files. Suricata removes cache files older than the specified = age > -+on startup/rule reloads, where age is determined by delta of the = file > -+modification time and the current time. > -+Cache files that are actively being used will have their = modification time > -+updated when loaded, so they won't be deleted. > -+ > -+In `suricata.yaml` configure: > -+ > -+:: > -+ > -+ detect: > -+ sgh-mpm-caching-max-age: 7d > -+ > -+The setting accepts a combination of time units (s,m,h,d,w,y), > -+e.g. `1w 3d 12h` for 1 week, 3 days and 12 hours. Setting the value = to `0` > -+disables pruning. > -+ > - **Note**: > - You might need to create and adjust permissions to the default = caching folder > - path, especially if you are running Suricata as a non-root user. > -diff --git a/doc/userguide/upgrade.rst b/doc/userguide/upgrade.rst > -index ef8d1e369..054e3eb38 100644 > ---- a/doc/userguide/upgrade.rst > -+++ b/doc/userguide/upgrade.rst > -@@ -68,6 +68,10 @@ Other Changes > - from unbounded to 2048. Configuration options, ``max-tx``, > - ``max-points``, and ``max-objects`` have been added for users who > - may need to change these defaults. > -+- Hyperscan caching (`detect.sgh-mpm-caching`), when enabled, prunes > -+ cache files that have not been used in the last 7 days by default. > -+ See :ref:`Hyperscan caching configuration > -+ ` for more information. > -=20 > - Upgrading to 8.0.1 > - ------------------ > -diff --git a/src/detect-engine-loader.c b/src/detect-engine-loader.c > -index ef0e8ef13..a97ebd6d2 100644 > ---- a/src/detect-engine-loader.c > -+++ b/src/detect-engine-loader.c > -@@ -502,10 +502,6 @@ skip_regular_rules: > -=20 > - ret =3D 0; > -=20 > -- if (mpm_table[de_ctx->mpm_matcher].CacheRuleset !=3D NULL) { > -- = mpm_table[de_ctx->mpm_matcher].CacheRuleset(de_ctx->mpm_cfg); > -- } > -- > - end: > - gettimeofday(&de_ctx->last_reload, NULL); > - if (SCRunmodeGet() =3D=3D RUNMODE_ENGINE_ANALYSIS) { > -diff --git a/src/detect-engine.c b/src/detect-engine.c > -index 12b1683c5..28e0bc14a 100644 > ---- a/src/detect-engine.c > -+++ b/src/detect-engine.c > -@@ -2481,6 +2481,49 @@ const char = *DetectEngineMpmCachingGetPath(void) > - return SGH_CACHE_DIR; > - } > -=20 > -+void DetectEngineMpmCacheService(uint32_t op_flags) > -+{ > -+ DetectEngineCtx *de_ctx =3D DetectEngineGetCurrent(); > -+ if (!de_ctx) { > -+ return; > -+ } > -+ > -+ if (!de_ctx->mpm_cfg || !de_ctx->mpm_cfg->cache_dir_path) { > -+ goto error; > -+ } > -+ > -+ if (mpm_table[de_ctx->mpm_matcher].CacheStatsInit !=3D NULL) { > -+ de_ctx->mpm_cfg->cache_stats =3D = mpm_table[de_ctx->mpm_matcher].CacheStatsInit(); > -+ if (de_ctx->mpm_cfg->cache_stats =3D=3D NULL) { > -+ goto error; > -+ } > -+ } > -+ > -+ if (op_flags & DETECT_ENGINE_MPM_CACHE_OP_SAVE) { > -+ if (mpm_table[de_ctx->mpm_matcher].CacheRuleset !=3D NULL) { > -+ = mpm_table[de_ctx->mpm_matcher].CacheRuleset(de_ctx->mpm_cfg); > -+ } > -+ } > -+ > -+ if (op_flags & DETECT_ENGINE_MPM_CACHE_OP_PRUNE) { > -+ if (mpm_table[de_ctx->mpm_matcher].CachePrune !=3D NULL) { > -+ = mpm_table[de_ctx->mpm_matcher].CachePrune(de_ctx->mpm_cfg); > -+ } > -+ } > -+ > -+ if (mpm_table[de_ctx->mpm_matcher].CacheStatsPrint !=3D NULL) { > -+ = mpm_table[de_ctx->mpm_matcher].CacheStatsPrint(de_ctx->mpm_cfg->cache_stat= s); > -+ } > -+ > -+ if (mpm_table[de_ctx->mpm_matcher].CacheStatsDeinit !=3D NULL) { > -+ = mpm_table[de_ctx->mpm_matcher].CacheStatsDeinit(de_ctx->mpm_cfg->cache_sta= ts); > -+ de_ctx->mpm_cfg->cache_stats =3D NULL; > -+ } > -+ > -+error: > -+ DetectEngineDeReference(&de_ctx); > -+} > -+ > - static DetectEngineCtx *DetectEngineCtxInitReal( > - enum DetectEngineType type, const char *prefix, uint32_t = tenant_id) > - { > -@@ -2503,10 +2546,18 @@ static DetectEngineCtx = *DetectEngineCtxInitReal( > - if (de_ctx->mpm_cfg =3D=3D NULL) { > - goto error; > - } > -- } > -- if (DetectEngineMpmCachingEnabled() && = mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet) { > -- mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet( > -- de_ctx->mpm_cfg, DetectEngineMpmCachingGetPath()); > -+ > -+ if (DetectEngineMpmCachingEnabled() && = mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet) { > -+ mpm_table[de_ctx->mpm_matcher].ConfigCacheDirSet( > -+ de_ctx->mpm_cfg, = DetectEngineMpmCachingGetPath()); > -+ > -+ if (mpm_table[de_ctx->mpm_matcher].CachePrune) { > -+ if (SCConfGetTime("detect.sgh-mpm-caching-max-age", > -+ &de_ctx->mpm_cfg->cache_max_age_seconds) = !=3D 1) { > -+ de_ctx->mpm_cfg->cache_max_age_seconds =3D 7ULL = * 24ULL * 60ULL * 60ULL; > -+ } > -+ } > -+ } > - } > -=20 > - if (type =3D=3D DETECT_ENGINE_TYPE_DD_STUB || type =3D=3D = DETECT_ENGINE_TYPE_MT_STUB) { > -@@ -4885,6 +4936,8 @@ int DetectEngineReload(const SCInstance *suri) > -=20 > - SCLogDebug("old_de_ctx should have been freed"); > -=20 > -+ DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE | = DETECT_ENGINE_MPM_CACHE_OP_PRUNE); > -+ > - SCLogNotice("rule reload complete"); > -=20 > - #ifdef HAVE_MALLOC_TRIM > -diff --git a/src/detect-engine.h b/src/detect-engine.h > -index 2c56475f6..2d45d3253 100644 > ---- a/src/detect-engine.h > -+++ b/src/detect-engine.h > -@@ -88,6 +88,7 @@ TmEcode DetectEngineThreadCtxInit(ThreadVars *, = void *, void **); > - TmEcode DetectEngineThreadCtxDeinit(ThreadVars *, void *); > - bool DetectEngineMpmCachingEnabled(void); > - const char *DetectEngineMpmCachingGetPath(void); > -+void DetectEngineMpmCacheService(uint32_t op_flags); > - /* faster as a macro than a inline function on my box -- VJ */ > - #define DetectEngineGetMaxSigId(de_ctx) ((de_ctx)->signum) > - void DetectEngineResetMaxSigId(DetectEngineCtx *); > -diff --git a/src/detect.h b/src/detect.h > -index 62c888e6a..49fbfe3eb 100644 > ---- a/src/detect.h > -+++ b/src/detect.h > -@@ -1750,6 +1750,9 @@ extern SigTableElmt *sigmatch_table; > -=20 > - /** Remember to add the options in SignatureIsIPOnly() at detect.c = otherwise it wont be part of a signature group */ > -=20 > -+#define DETECT_ENGINE_MPM_CACHE_OP_PRUNE BIT_U32(0) > -+#define DETECT_ENGINE_MPM_CACHE_OP_SAVE BIT_U32(1) > -+ > - /* detection api */ > - TmEcode Detect(ThreadVars *tv, Packet *p, void *data); > - uint8_t DetectPreFlow(ThreadVars *tv, DetectEngineThreadCtx = *det_ctx, Packet *p); > -diff --git a/src/runmode-unix-socket.c b/src/runmode-unix-socket.c > -index c2405f057..706a35b7e 100644 > ---- a/src/runmode-unix-socket.c > -+++ b/src/runmode-unix-socket.c > -@@ -967,6 +967,8 @@ TmEcode UnixSocketRegisterTenantHandler(json_t = *cmd, json_t* answer, void *data) > - return TM_ECODE_FAILED; > - } > -=20 > -+ DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE); > -+ > - json_object_set_new(answer, "message", json_string("handler = added")); > - return TM_ECODE_OK; > - } > -@@ -1054,6 +1056,8 @@ TmEcode = UnixSocketUnregisterTenantHandler(json_t *cmd, json_t* answer, void *dat > - return TM_ECODE_FAILED; > - } > -=20 > -+ DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_PRUNE); > -+ > - json_object_set_new(answer, "message", json_string("handler = removed")); > - return TM_ECODE_OK; > - } > -@@ -1126,6 +1130,8 @@ TmEcode UnixSocketRegisterTenant(json_t *cmd, = json_t* answer, void *data) > - return TM_ECODE_FAILED; > - } > -=20 > -+ DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE); > -+ > - json_object_set_new(answer, "message", json_string("adding = tenant succeeded")); > - return TM_ECODE_OK; > - } > -@@ -1193,6 +1199,8 @@ TmEcode UnixSocketReloadTenant(json_t *cmd, = json_t* answer, void *data) > - return TM_ECODE_FAILED; > - } > -=20 > -+ DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE | = DETECT_ENGINE_MPM_CACHE_OP_PRUNE); > -+ > - json_object_set_new(answer, "message", json_string("reloading = tenant succeeded")); > - return TM_ECODE_OK; > - } > -@@ -1226,6 +1234,7 @@ TmEcode UnixSocketReloadTenants(json_t *cmd, = json_t *answer, void *data) > - return TM_ECODE_FAILED; > - } > -=20 > -+ DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE | = DETECT_ENGINE_MPM_CACHE_OP_PRUNE); > - SCLogNotice("reload-tenants complete"); > -=20 > - json_object_set_new(answer, "message", json_string("reloading = tenants succeeded")); > -@@ -1284,6 +1293,8 @@ TmEcode UnixSocketUnregisterTenant(json_t *cmd, = json_t* answer, void *data) > - return TM_ECODE_FAILED; > - } > -=20 > -+ DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_PRUNE); > -+ > - /* walk free list, freeing the removed de_ctx */ > - DetectEnginePruneFreeList(); > -=20 > -diff --git a/src/suricata.c b/src/suricata.c > -index c6f94c3ce..a106c56f7 100644 > ---- a/src/suricata.c > -+++ b/src/suricata.c > -@@ -2688,6 +2688,8 @@ void PostConfLoadedDetectSetup(SCInstance = *suri) > - gettimeofday(&de_ctx->last_reload, NULL); > - DetectEngineAddToMaster(de_ctx); > - DetectEngineBumpVersion(); > -+ DetectEngineMpmCacheService( > -+ DETECT_ENGINE_MPM_CACHE_OP_SAVE | = DETECT_ENGINE_MPM_CACHE_OP_PRUNE); > - } > - } > -=20 > -diff --git a/src/util-mpm-hs-cache.c b/src/util-mpm-hs-cache.c > -index 41b308171..58a2aa6ab 100644 > ---- a/src/util-mpm-hs-cache.c > -+++ b/src/util-mpm-hs-cache.c > -@@ -37,21 +37,22 @@ > - #include "rust.h" > - #include > -=20 > --static const char *HSCacheConstructFPath(const char *folder_path, = const char *hs_db_hash) > --{ > -- static char hash_file_path[PATH_MAX]; > -+#define HS_CACHE_FILE_VERSION "2" > -+#define HS_CACHE_FILE_SUFFIX "_v" HS_CACHE_FILE_VERSION ".hs" > -=20 > -- char hash_file_path_suffix[] =3D "_v1.hs"; > -+static int16_t HSCacheConstructFPath( > -+ const char *dir_path, const char *db_hash, char *out_path, = uint16_t out_path_size) > -+{ > - char filename[NAME_MAX]; > -- uint64_t r =3D snprintf(filename, sizeof(filename), "%s%s", = hs_db_hash, hash_file_path_suffix); > -- if (r !=3D (uint64_t)(strlen(hs_db_hash) + = strlen(hash_file_path_suffix))) > -- return NULL; > -+ uint64_t r =3D snprintf(filename, sizeof(filename), "%s" = HS_CACHE_FILE_SUFFIX, db_hash); > -+ if (r !=3D (uint64_t)(strlen(db_hash) + = strlen(HS_CACHE_FILE_SUFFIX))) > -+ return -1; > -=20 > -- r =3D PathMerge(hash_file_path, sizeof(hash_file_path), = folder_path, filename); > -+ r =3D PathMerge(out_path, out_path_size, dir_path, filename); > - if (r) > -- return NULL; > -+ return -1; > -=20 > -- return hash_file_path; > -+ return 0; > - } > -=20 > - static char *HSReadStream(const char *file_path, size_t *buffer_sz) > -@@ -121,8 +122,11 @@ static void SCHSCachePatternHash(const = SCHSPattern *p, SCSha256 *sha256) > -=20 > - int HSLoadCache(hs_database_t **hs_db, const char *hs_db_hash, const = char *dirpath) > - { > -- const char *hash_file_static =3D HSCacheConstructFPath(dirpath, = hs_db_hash); > -- if (hash_file_static =3D=3D NULL) > -+ char hash_file_static[PATH_MAX]; > -+ int ret =3D (int)HSCacheConstructFPath( > -+ dirpath, hs_db_hash, hash_file_static, = sizeof(hash_file_static)); > -+ > -+ if (ret !=3D 0) > - return -1; > -=20 > - SCLogDebug("Loading the cached HS DB from %s", = hash_file_static); > -@@ -131,7 +135,6 @@ int HSLoadCache(hs_database_t **hs_db, const char = *hs_db_hash, const char *dirpa > -=20 > - FILE *db_cache =3D fopen(hash_file_static, "r"); > - char *buffer =3D NULL; > -- int ret =3D 0; > - if (db_cache) { > - size_t buffer_size; > - buffer =3D HSReadStream(hash_file_static, &buffer_size); > -@@ -170,15 +173,20 @@ static int HSSaveCache(hs_database_t *hs_db, = const char *hs_db_hash, const char > - static bool notified =3D false; > - char *db_stream =3D NULL; > - size_t db_size; > -- int ret =3D -1; > -+ int ret; > -=20 > - hs_error_t err =3D hs_serialize_database(hs_db, &db_stream, = &db_size); > - if (err !=3D HS_SUCCESS) { > - SCLogWarning("Failed to serialize Hyperscan database: %s", = HSErrorToStr(err)); > -+ ret =3D -1; > - goto cleanup; > - } > -=20 > -- const char *hash_file_static =3D HSCacheConstructFPath(dstpath, = hs_db_hash); > -+ char hash_file_static[PATH_MAX]; > -+ ret =3D (int)HSCacheConstructFPath( > -+ dstpath, hs_db_hash, hash_file_static, = sizeof(hash_file_static)); > -+ if (ret !=3D 0) > -+ goto cleanup; > - SCLogDebug("Caching the compiled HS at %s", hash_file_static); > - if (SCPathExists(hash_file_static)) { > - // potentially signs that it might not work as expected as = we got into > -@@ -198,6 +206,7 @@ static int HSSaveCache(hs_database_t *hs_db, = const char *hs_db_hash, const char > - hash_file_static); > - notified =3D true; > - } > -+ ret =3D -1; > - goto cleanup; > - } > - size_t r =3D fwrite(db_stream, sizeof(db_stream[0]), db_size, = db_cache_out); > -@@ -217,7 +226,6 @@ static int HSSaveCache(hs_database_t *hs_db, = const char *hs_db_hash, const char > - goto cleanup; > - } > -=20 > -- ret =3D 0; > - cleanup: > - if (db_stream) > - SCFree(db_stream); > -@@ -270,4 +278,187 @@ void HSSaveCacheIterator(void *data, void *aux) > - } > - } > -=20 > -+void HSCacheFilenameUsedIterator(void *data, void *aux) > -+{ > -+ PatternDatabase *pd =3D (PatternDatabase *)data; > -+ struct HsInUseCacheFilesIteratorData *iter_data =3D (struct = HsInUseCacheFilesIteratorData *)aux; > -+ if (pd->no_cache || !pd->cached) > -+ return; > -+ > -+ char hs_db_hash[SC_SHA256_LEN * 2 + 1]; // * 2 for hex +1 for = nul terminator > -+ if (HSHashDb(pd, hs_db_hash, ARRAY_SIZE(hs_db_hash)) !=3D 0) { > -+ return; > -+ } > -+ > -+ char *fpath =3D SCCalloc(PATH_MAX, sizeof(char)); > -+ if (fpath =3D=3D NULL) { > -+ SCLogWarning("Failed to allocate memory for cache file = path"); > -+ return; > -+ } > -+ if (HSCacheConstructFPath(iter_data->cache_path, hs_db_hash, = fpath, PATH_MAX)) { > -+ SCFree(fpath); > -+ return; > -+ } > -+ > -+ int r =3D HashTableAdd(iter_data->tbl, (void *)fpath, = (uint16_t)strlen(fpath)); > -+ if (r < 0) { > -+ SCLogWarning("Failed to add used cache file path %s to hash = table", fpath); > -+ SCFree(fpath); > -+ } > -+} > -+ > -+/** > -+ * \brief Check if HS cache file is stale by age. > -+ * > -+ * \param mtime File modification time. > -+ * \param cutoff Time cutoff (files older than this will be = removed). > -+ * > -+ * \retval true if file should be pruned, false otherwise. > -+ */ > -+static bool HSPruneFileByAge(time_t mtime, time_t cutoff) > -+{ > -+ return mtime < cutoff; > -+} > -+ > -+/** > -+ * \brief Check if HS cache file is version-compatible. > -+ * > -+ * \param filename Cache file name. > -+ * > -+ * \retval true if file should be pruned, false otherwise. > -+ */ > -+static bool HSPruneFileByVersion(const char *filename) > -+{ > -+ if (strlen(filename) < strlen(HS_CACHE_FILE_SUFFIX)) { > -+ return true; > -+ } > -+ > -+ const char *underscore =3D strrchr(filename, '_'); > -+ if (underscore =3D=3D NULL || strcmp(underscore, = HS_CACHE_FILE_SUFFIX) !=3D 0) { > -+ return true; > -+ } > -+ > -+ return false; > -+} > -+ > -+int SCHSCachePruneEvaluate(MpmConfig *mpm_conf, HashTable = *inuse_caches) > -+{ > -+ if (mpm_conf =3D=3D NULL || mpm_conf->cache_dir_path =3D=3D = NULL) > -+ return -1; > -+ if (mpm_conf->cache_max_age_seconds =3D=3D 0) > -+ return 0; // disabled > -+ > -+ const time_t now =3D time(NULL); > -+ if (now =3D=3D (time_t)-1) { > -+ return -1; > -+ } else if (mpm_conf->cache_max_age_seconds >=3D (uint64_t)now) { > -+ return 0; > -+ } > -+ > -+ DIR *dir =3D opendir(mpm_conf->cache_dir_path); > -+ if (dir =3D=3D NULL) { > -+ return -1; > -+ } > -+ > -+ struct dirent *ent; > -+ char path[PATH_MAX]; > -+ uint32_t considered =3D 0, removed =3D 0; > -+ const time_t cutoff =3D now - = (time_t)mpm_conf->cache_max_age_seconds; > -+ while ((ent =3D readdir(dir)) !=3D NULL) { > -+ const char *name =3D ent->d_name; > -+ size_t namelen =3D strlen(name); > -+ if (namelen < 3 || strcmp(name + namelen - 3, ".hs") !=3D 0) > -+ continue; > -+ > -+ if (PathMerge(path, ARRAY_SIZE(path), = mpm_conf->cache_dir_path, name) !=3D 0) > -+ continue; > -+ > -+ struct stat st; > -+ if (stat(path, &st) !=3D 0 || !S_ISREG(st.st_mode)) > -+ continue; > -+ > -+ considered++; > -+ > -+ const bool prune_by_age =3D HSPruneFileByAge(st.st_mtime, = cutoff); > -+ const bool prune_by_version =3D HSPruneFileByVersion(name); > -+ if (!prune_by_age && !prune_by_version) > -+ continue; > -+ > -+ void *cache_inuse =3D HashTableLookup(inuse_caches, path, = (uint16_t)strlen(path)); > -+ if (cache_inuse !=3D NULL) > -+ continue; // in use > -+ > -+ if (unlink(path) =3D=3D 0) { > -+ removed++; > -+ SCLogDebug("File %s removed because of %s%s%s", path, = prune_by_age ? "age" : "", > -+ prune_by_age && prune_by_version ? " and " : "", > -+ prune_by_version ? "incompatible version" : ""); > -+ } else { > -+ SCLogWarning("Failed to prune \"%s\": %s", path, = strerror(errno)); > -+ } > -+ } > -+ closedir(dir); > -+ > -+ PatternDatabaseCache *pd_cache_stats =3D mpm_conf->cache_stats; > -+ if (pd_cache_stats) { > -+ pd_cache_stats->hs_dbs_cache_pruned_cnt =3D removed; > -+ pd_cache_stats->hs_dbs_cache_pruned_considered_cnt =3D = considered; > -+ pd_cache_stats->hs_dbs_cache_pruned_cutoff =3D cutoff; > -+ pd_cache_stats->cache_max_age_seconds =3D = mpm_conf->cache_max_age_seconds; > -+ } > -+ return 0; > -+} > -+ > -+void *SCHSCacheStatsInit(void) > -+{ > -+ PatternDatabaseCache *pd_cache_stats =3D SCCalloc(1, = sizeof(PatternDatabaseCache)); > -+ if (pd_cache_stats =3D=3D NULL) { > -+ SCLogError("Failed to allocate memory for Hyperscan cache = stats"); > -+ return NULL; > -+ } > -+ return pd_cache_stats; > -+} > -+ > -+void SCHSCacheStatsPrint(void *data) > -+{ > -+ if (data =3D=3D NULL) { > -+ return; > -+ } > -+ > -+ PatternDatabaseCache *pd_cache_stats =3D (PatternDatabaseCache = *)data; > -+ > -+ char time_str[64]; > -+ struct tm tm_s; > -+ struct tm *tm_info =3D = SCLocalTime(pd_cache_stats->hs_dbs_cache_pruned_cutoff, &tm_s); > -+ if (tm_info !=3D NULL) { > -+ strftime(time_str, ARRAY_SIZE(time_str), "%Y-%m-%d = %H:%M:%S", tm_info); > -+ } else { > -+ snprintf(time_str, ARRAY_SIZE(time_str), "%" PRIu64 " = seconds", > -+ pd_cache_stats->cache_max_age_seconds); > -+ } > -+ > -+ if (pd_cache_stats->hs_cacheable_dbs_cnt) { > -+ SCLogInfo("Rule group caching - loaded: %u newly cached: %u = total cacheable: %u", > -+ pd_cache_stats->hs_dbs_cache_loaded_cnt, = pd_cache_stats->hs_dbs_cache_saved_cnt, > -+ pd_cache_stats->hs_cacheable_dbs_cnt); > -+ } > -+ if (pd_cache_stats->hs_dbs_cache_pruned_considered_cnt) { > -+ SCLogInfo("Rule group cache pruning removed %u/%u of HS = caches due to " > -+ "version-incompatibility (not v%s) or " > -+ "age (older than %s)", > -+ pd_cache_stats->hs_dbs_cache_pruned_cnt, > -+ pd_cache_stats->hs_dbs_cache_pruned_considered_cnt, = HS_CACHE_FILE_VERSION, > -+ time_str); > -+ } > -+} > -+ > -+void SCHSCacheStatsDeinit(void *data) > -+{ > -+ if (data =3D=3D NULL) { > -+ return; > -+ } > -+ PatternDatabaseCache *pd_cache_stats =3D (PatternDatabaseCache = *)data; > -+ SCFree(pd_cache_stats); > -+} > -+ > - #endif /* BUILD_HYPERSCAN */ > -diff --git a/src/util-mpm-hs-cache.h b/src/util-mpm-hs-cache.h > -index 225c5001a..24b4eece0 100644 > ---- a/src/util-mpm-hs-cache.h > -+++ b/src/util-mpm-hs-cache.h > -@@ -35,9 +35,24 @@ struct HsIteratorData { > - const char *cache_path; > - }; > -=20 > -+/** > -+ * \brief Data structure to store in-use cache files. > -+ * Used in cache pruning to avoid deleting files that are still in = use. > -+ */ > -+struct HsInUseCacheFilesIteratorData { > -+ HashTable *tbl; // stores file paths of in-use cache files > -+ const char *cache_path; > -+}; > -+ > - int HSLoadCache(hs_database_t **hs_db, const char *hs_db_hash, const = char *dirpath); > - int HSHashDb(const PatternDatabase *pd, char *hash, size_t = hash_len); > - void HSSaveCacheIterator(void *data, void *aux); > -+void HSCacheFilenameUsedIterator(void *data, void *aux); > -+int SCHSCachePruneEvaluate(MpmConfig *mpm_conf, HashTable = *inuse_caches); > -+ > -+void *SCHSCacheStatsInit(void); > -+void SCHSCacheStatsPrint(void *data); > -+void SCHSCacheStatsDeinit(void *data); > - #endif /* BUILD_HYPERSCAN */ > -=20 > - #endif /* SURICATA_UTIL_MPM_HS_CACHE__H */ > -diff --git a/src/util-mpm-hs-core.h b/src/util-mpm-hs-core.h > -index 699dd6956..8392127cf 100644 > ---- a/src/util-mpm-hs-core.h > -+++ b/src/util-mpm-hs-core.h > -@@ -93,6 +93,10 @@ typedef struct PatternDatabaseCache_ { > - uint32_t hs_cacheable_dbs_cnt; > - uint32_t hs_dbs_cache_loaded_cnt; > - uint32_t hs_dbs_cache_saved_cnt; > -+ uint32_t hs_dbs_cache_pruned_cnt; > -+ uint32_t hs_dbs_cache_pruned_considered_cnt; > -+ time_t hs_dbs_cache_pruned_cutoff; > -+ uint64_t cache_max_age_seconds; > - } PatternDatabaseCache; > -=20 > - const char *HSErrorToStr(hs_error_t error_code); > -diff --git a/src/util-mpm-hs.c b/src/util-mpm-hs.c > -index ad7178eb8..df4a66b2e 100644 > ---- a/src/util-mpm-hs.c > -+++ b/src/util-mpm-hs.c > -@@ -835,18 +835,53 @@ static int SCHSCacheRuleset(MpmConfig = *mpm_conf) > - mpm_conf->cache_dir_path); > - return -1; > - } > -- PatternDatabaseCache pd_stats =3D { 0 }; > -- struct HsIteratorData iter_data =3D { .pd_stats =3D &pd_stats, > -+ PatternDatabaseCache *pd_stats =3D mpm_conf->cache_stats; > -+ struct HsIteratorData iter_data =3D { .pd_stats =3D pd_stats, > - .cache_path =3D mpm_conf->cache_dir_path }; > - SCMutexLock(&g_db_table_mutex); > - HashTableIterate(g_db_table, HSSaveCacheIterator, &iter_data); > - SCMutexUnlock(&g_db_table_mutex); > -- SCLogNotice("Rule group caching - loaded: %u newly cached: %u = total cacheable: %u", > -- pd_stats.hs_dbs_cache_loaded_cnt, = pd_stats.hs_dbs_cache_saved_cnt, > -- pd_stats.hs_cacheable_dbs_cnt); > - return 0; > - } > -=20 > -+static uint32_t FilenameTableHash(HashTable *ht, void *data, = uint16_t len) > -+{ > -+ const char *fname =3D data; > -+ uint32_t hash =3D hashlittle_safe(data, strlen(fname), 0); > -+ hash %=3D ht->array_size; > -+ return hash; > -+} > -+ > -+static void FilenameTableFree(void *data) > -+{ > -+ SCFree(data); > -+} > -+ > -+static int SCHSCachePrune(MpmConfig *mpm_conf) > -+{ > -+ if (!mpm_conf || !mpm_conf->cache_dir_path) { > -+ return -1; > -+ } > -+ > -+ SCLogDebug("Pruning the Hyperscan cache folder %s", = mpm_conf->cache_dir_path); > -+ // we need to initialize hash map of in-use cache files > -+ HashTable *inuse_caches =3D > -+ HashTableInit(INIT_DB_HASH_SIZE, FilenameTableHash, = NULL, FilenameTableFree); > -+ if (inuse_caches =3D=3D NULL) { > -+ return -1; > -+ } > -+ struct HsInUseCacheFilesIteratorData iter_data =3D { .tbl =3D = inuse_caches, > -+ .cache_path =3D mpm_conf->cache_dir_path }; > -+ > -+ SCMutexLock(&g_db_table_mutex); > -+ HashTableIterate(g_db_table, HSCacheFilenameUsedIterator, = &iter_data); > -+ SCMutexUnlock(&g_db_table_mutex); > -+ > -+ int r =3D SCHSCachePruneEvaluate(mpm_conf, inuse_caches); > -+ HashTableFree(inuse_caches); > -+ return r; > -+} > -+ > - /** > - * \brief Init the mpm thread context. > - * > -@@ -1178,7 +1213,11 @@ void MpmHSRegister(void) > - mpm_table[MPM_HS].AddPattern =3D SCHSAddPatternCS; > - mpm_table[MPM_HS].AddPatternNocase =3D SCHSAddPatternCI; > - mpm_table[MPM_HS].Prepare =3D SCHSPreparePatterns; > -+ mpm_table[MPM_HS].CacheStatsInit =3D SCHSCacheStatsInit; > -+ mpm_table[MPM_HS].CacheStatsPrint =3D SCHSCacheStatsPrint; > -+ mpm_table[MPM_HS].CacheStatsDeinit =3D SCHSCacheStatsDeinit; > - mpm_table[MPM_HS].CacheRuleset =3D SCHSCacheRuleset; > -+ mpm_table[MPM_HS].CachePrune =3D SCHSCachePrune; > - mpm_table[MPM_HS].Search =3D SCHSSearch; > - mpm_table[MPM_HS].PrintCtx =3D SCHSPrintInfo; > - mpm_table[MPM_HS].PrintThreadCtx =3D SCHSPrintSearchStats; > -diff --git a/src/util-mpm.h b/src/util-mpm.h > -index c2c434152..859ceae12 100644 > ---- a/src/util-mpm.h > -+++ b/src/util-mpm.h > -@@ -90,6 +90,8 @@ typedef struct MpmPattern_ { > -=20 > - typedef struct MpmConfig_ { > - const char *cache_dir_path; > -+ uint64_t cache_max_age_seconds; /* 0 means disabled/no pruning = policy */ > -+ void *cache_stats; > - } MpmConfig; > -=20 > - typedef struct MpmCtx_ { > -@@ -175,7 +177,11 @@ typedef struct MpmTableElmt_ { > - int (*AddPatternNocase)(struct MpmCtx_ *, const uint8_t *, = uint16_t, uint16_t, uint16_t, > - uint32_t, SigIntId, uint8_t); > - int (*Prepare)(MpmConfig *, struct MpmCtx_ *); > -+ void *(*CacheStatsInit)(void); > -+ void (*CacheStatsPrint)(void *data); > -+ void (*CacheStatsDeinit)(void *data); > - int (*CacheRuleset)(MpmConfig *); > -+ int (*CachePrune)(MpmConfig *); > - /** \retval cnt number of patterns that matches: once per = pattern max. */ > - uint32_t (*Search)(const struct MpmCtx_ *, struct MpmThreadCtx_ = *, PrefilterRuleStore *, const uint8_t *, uint32_t); > - void (*PrintCtx)(struct MpmCtx_ *); > -diff --git a/suricata.yaml.in b/suricata.yaml.in > -index a0ab5a066..d7ce7c2cc 100644 > ---- a/suricata.yaml.in > -+++ b/suricata.yaml.in > -@@ -1810,6 +1810,10 @@ detect: > - # Cache files are created in the standard library directory. > - sgh-mpm-caching: yes > - sgh-mpm-caching-path: @e_sghcachedir@ > -+ # Maximum age for cached MPM databases before they are pruned. > -+ # Accepts a combination of time units (s,m,h,d,w,y). > -+ # Omit to use the default, 0 to disable. > -+ # sgh-mpm-caching-max-age: 7d > - # inspection-recursion-limit: 3000 > - # maximum number of times a tx will get logged for rules without = app-layer keywords > - # stream-tx-log-limit: 4 > -commit 56c1552c3e8425ca07ce3b6ba88f2215b984c5fb > -Author: Lukas Sismis > -Date: Mon Nov 3 19:47:16 2025 +0100 > - > - hs: warn about the same cache directory > - =20 > - This is especially relevant for multi-instance simultaneous = setups > - as we might risk read/write races. > - > -diff --git a/doc/userguide/performance/hyperscan.rst = b/doc/userguide/performance/hyperscan.rst > -index 1060d3aef..a64322730 100644 > ---- a/doc/userguide/performance/hyperscan.rst > -+++ b/doc/userguide/performance/hyperscan.rst > -@@ -127,3 +127,7 @@ disables pruning. > - **Note**: > - You might need to create and adjust permissions to the default = caching folder > - path, especially if you are running Suricata as a non-root user. > -+ > -+**Note**: > -+If you're running multiple Suricata instances, use separate cache = folders > -+for each one to avoid read/write conflicts when they run at the same = time. > --=20 > 2.53.0 >=20 >=20