suricata: Update to 8.0.4

Message ID 20260318134024.2600353-1-matthias.fischer@ipfire.org
State Accepted
Commit 7bb5de1cac986dd795096a78232cef20069923c1
Headers
Series suricata: Update to 8.0.4 |

Commit Message

Matthias Fischer 18 Mar 2026, 1:39 p.m. UTC
The contents of ‘suricata-8.0.3-purge-hyperscan-cache.patch’ 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.

Build is running without seen problems.

Excerpt from changelog:

"8.0.4 -- 2026-03-12

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 >,<,!,= (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)"

Signed-off-by: Matthias Fischer <matthias.fischer@ipfire.org>
---
 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
  

Comments

Michael Tremer 18 Mar 2026, 2:58 p.m. UTC | #1
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 <matthias.fischer@ipfire.org> wrote:
> 
> The contents of ‘suricata-8.0.3-purge-hyperscan-cache.patch’ 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.
> 
> Build is running without seen problems.
> 
> Excerpt from changelog:
> 
> "8.0.4 -- 2026-03-12
> 
> 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 >,<,!,= (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)"
> 
> Signed-off-by: Matthias Fischer <matthias.fischer@ipfire.org>
> ---
> 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
> 
> 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 @@
> 
> include Config
> 
> -VER        = 8.0.3
> +VER        = 8.0.4
> 
> THISAPP    = suricata-$(VER)
> DL_FILE    = $(THISAPP).tar.gz
> @@ -40,7 +40,7 @@ objects = $(DL_FILE)
> 
> $(DL_FILE) = $(DL_FROM)/$(DL_FILE)
> 
> -$(DL_FILE)_BLAKE2 = ab87fde815338a7520badd2f4d8c8bfaccc778ecffbb13028fe9d561b1bf0e4ef2a43296b88fffb306df9e28fcd5997fa22c72ac887c40efbea799e0110fcb56
> +$(DL_FILE)_BLAKE2 = a6c1958d82bb8c288c8d551d99851d19a89073397bda38bc90907950d17c35e40eb4845e9a88913bafc5c56bdad8c026e0fb665c494b102861c2b8f210c72d7f
> 
> install : $(TARGET)
> 
> @@ -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
> 
> cd $(DIR_APP) && LDFLAGS="$(LDFLAGS)" ./configure \
> --prefix=/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 <lsismis@oisf.net>
> -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 <lsismis@oisf.net>
> -  *
> -- * MPM pattern matcher that calls the Hyperscan regex matcher.
> -+ * Hyperscan cache helper utilities for MPM cache files.
> -  */
> - 
> - #include "suricata-common.h"
> -commit 2a313ff429eb49be5e4c3b9dadfca127fa64c5fe
> -Author: Lukas Sismis <lsismis@oisf.net>
> -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];
> - 
> -     char hash_file_path_suffix[] = "_v1.hs";
> --    char filename[PATH_MAX];
> -+    char filename[NAME_MAX];
> -     uint64_t r = snprintf(
> -             filename, sizeof(filename), "%020" PRIu64 "%s", hs_db_hash, hash_file_path_suffix);
> -     if (r != (uint64_t)(20 + strlen(hash_file_path_suffix)))
> -commit c282880174875fab6bcc62a2a60c85b58dfb0d32
> -Author: Lukas Sismis <lsismis@oisf.net>
> -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 @@
> - 
> - #ifdef BUILD_HYPERSCAN
> - 
> -+#include "rust.h"
> - #include <hs.h>
> - 
> --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];
> - 
> -     char hash_file_path_suffix[] = "_v1.hs";
> -     char filename[NAME_MAX];
> --    uint64_t r = snprintf(
> --            filename, sizeof(filename), "%020" PRIu64 "%s", hs_db_hash, hash_file_path_suffix);
> --    if (r != (uint64_t)(20 + strlen(hash_file_path_suffix)))
> -+    uint64_t r = snprintf(filename, sizeof(filename), "%s%s", hs_db_hash, hash_file_path_suffix);
> -+    if (r != (uint64_t)(strlen(hs_db_hash) + strlen(hash_file_path_suffix)))
> -         return NULL;
> - 
> -     r = 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 == NULL);
> -     BUG_ON(p->sids == NULL);
> - 
> --    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));
> - }
> - 
> --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 = HSCacheConstructFPath(dirpath, hs_db_hash);
> -     if (hash_file_static == NULL)
> -@@ -161,7 +161,7 @@ freeup:
> -     return ret;
> - }
> - 
> --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 = false;
> -     char *db_stream = NULL;
> -@@ -220,14 +220,26 @@ cleanup:
> -     return ret;
> - }
> - 
> --uint64_t HSHashDb(const PatternDatabase *pd)
> -+int HSHashDb(const PatternDatabase *pd, char *hash, size_t hash_len)
> - {
> --    uint32_t hash[2] = { 0 };
> --    hashword2(&pd->pattern_cnt, 1, &hash[0], &hash[1]);
> -+    SCSha256 *hasher = SCSha256New();
> -+    if (hasher == NULL) {
> -+        SCLogDebug("sha256 hashing failed");
> -+        return -1;
> -+    }
> -+    SCSha256Update(hasher, (const uint8_t *)&pd->pattern_cnt, sizeof(pd->pattern_cnt));
> -     for (uint32_t i = 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 = NULL;
> -+        SCLogDebug("sha256 hashing failed");
> -+        return -1;
> -     }
> --    return ((uint64_t)hash[1] << 32) | hash[0];
> -+
> -+    hasher = NULL;
> -+    return 0;
> - }
> - 
> - void HSSaveCacheIterator(void *data, void *aux)
> -@@ -244,7 +256,11 @@ void HSSaveCacheIterator(void *data, void *aux)
> -         return;
> -     }
> - 
> --    if (HSSaveCache(pd->hs_db, HSHashDb(pd), iter_data->cache_path) == 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)) != 0) {
> -+        return;
> -+    }
> -+    if (HSSaveCache(pd->hs_db, hs_db_hash, iter_data->cache_path) == 0) {
> -         pd->cached = 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;
> - };
> - 
> --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 */
> - 
> -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 = *pd;
> --        uint64_t db_lookup_hash = HSHashDb(pd_cached);
> --        if (HSLoadCache(&pd_cached->hs_db, db_lookup_hash, cache_dir_path) == 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)) != 0) {
> -+            return -1;
> -+        }
> -+        if (HSLoadCache(&pd_cached->hs_db, hs_db_hash, cache_dir_path) == 0) {
> -             pd_cached->ref_cnt = 1;
> -             pd_cached->cached = true;
> -             if (HSScratchAlloc(pd_cached->hs_db) != 0) {
> -commit 3e4fdb2118bfcb8b2644944daded2d8c67420499
> -Author: Lukas Sismis <lsismis@oisf.net>
> -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 = [
> -  "windows-sys 0.52.0",
> - ]
> - 
> -+[[package]]
> -+name = "humantime"
> -+version = "2.3.0"
> -+source = "registry+https://github.com/rust-lang/crates.io-index"
> -+checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
> -+
> - [[package]]
> - name = "indexmap"
> - version = "2.11.4"
> -@@ -1551,6 +1557,7 @@ dependencies = [
> -  "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 = "~1.5.0"
> - base64 = "~0.22.1"
> - bendy = { version = "~0.3.3", default-features = false }
> - asn1-rs = { version = "~0.6.2" }
> -+humantime = "~2.3.0"
> - ldap-parser = { version = "~0.5.0" }
> - hex = "~0.4.3"
> - psl = "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 @@
> - 
> - //! Utility module.
> - 
> -+use std::borrow::Cow;
> - use std::ffi::CStr;
> - use std::os::raw::c_char;
> - 
> -@@ -26,6 +27,8 @@ use nom8::combinator::verify;
> - use nom8::multi::many1_count;
> - use nom8::{AsChar, IResult, Parser};
> - 
> -+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;
> - }
> - 
> -+/// 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 = 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 = match CStr::from_ptr(input).to_str() {
> -+        Ok(s) => s,
> -+        Err(_) => return -1,
> -+    };
> -+
> -+    let trimmed = input_str.trim();
> -+    if trimmed.is_empty() {
> -+        return -1;
> -+    }
> -+
> -+    let normalized = duration_unit_normalize(trimmed);
> -+    match parse_duration(normalized.as_ref()) {
> -+        Ok(duration) => {
> -+            *res = duration.as_secs();
> -+            0
> -+        }
> -+        Err(_) => -1,
> -+    }
> -+}
> -+
> - #[cfg(test)]
> - mod tests {
> - 
> -     use super::*;
> -+    use std::ffi::CString;
> -+    use std::ptr::{null, null_mut};
> - 
> -     #[test]
> -     fn test_parse_domain() {
> -@@ -83,4 +132,73 @@ mod tests {
> -         let buf1: &[u8] = "a(x)y.com".as_bytes();
> -         assert!(parse_domain(buf1).is_err());
> -     }
> -+
> -+    #[test]
> -+    fn test_parse_time_valid() {
> -+        unsafe {
> -+            let mut v: u64 = 0;
> -+
> -+            let s = CString::new("10").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 10);
> -+
> -+            let s = CString::new("0").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 0);
> -+
> -+            let s = CString::new("2H").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 7200);
> -+
> -+            let s = CString::new("1 day").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 86400);
> -+
> -+            let s = CString::new("1w").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 604800);
> -+
> -+            let s = CString::new("1 week").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 604800);
> -+
> -+            let s = CString::new("1y").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 31557600);
> -+
> -+            let s = CString::new("1 year").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
> -+            assert_eq!(v, 31557600);
> -+
> -+            // max
> -+            let s = 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 = 0;
> -+            let s = CString::new("10q").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1);
> -+
> -+            let s = CString::new("abc").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1);
> -+
> -+            let s = CString::new("-300s").unwrap();
> -+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1);
> -+
> -+            let s = 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 = (u64::MAX / 31557600) + 1;
> -+            let s = 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 <lsismis@oisf.net>
> -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"
> - 
> - /** 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;
> - }
> - 
> -+/**
> -+ * \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 = NULL;
> -+
> -+    if (SCConfGet(name, &strval) == 0)
> -+        return 0;
> -+
> -+    if (strval == NULL || strval[0] == '\0')
> -+        return 0;
> -+
> -+    if (SCParseTimeDuration(strval, val) != 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 <lsismis@oisf.net>
> -Date:   Mon Sep 15 11:36:01 2025 +0200
> -
> -    path: signal last use of the file (touch)
> -    
> -    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.
> -    
> -    This is needed to remove stale cache files of the system.
> -    
> -    Access time is not used as it may be, on the system level, disabled.
> -    
> -    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) != 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 == NULL || path[0] == '\0') {
> -+        errno = EINVAL;
> -+        return -1;
> -+    }
> -+#ifndef OS_WIN32
> -+    struct utimbuf ub;
> -+    ub.actime = ub.modtime = time(NULL);
> -+    if (utime(path, &ub) == 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);
> - 
> - #endif /* SURICATA_UTIL_PATH_H */
> -commit 7031c268655aec5c44420902bbda6f7aea8eba33
> -Author: Lukas Sismis <lsismis@oisf.net>
> -Date:   Mon Sep 15 11:39:02 2025 +0200
> -
> -    hs: touch cache files on use to signal activity
> -    
> -    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
> -         }
> - 
> -         ret = 0;
> -+        /* Touch file to update modification time so active caches are retained. */
> -+        if (SCTouchFile(hash_file_static) != 0) {
> -+            SCLogDebug("Failed to update mtime for %s", hash_file_static);
> -+        }
> -         goto freeup;
> -     }
> - 
> -commit 08f5abe5e967bbcfbc0c11a797ef86125afd3db8
> -Author: Lukas Sismis <lsismis@oisf.net>
> -Date:   Sun Dec 28 00:09:29 2025 +0100
> -
> -    detect-engine: make mpm & spm part of MT stub ctx
> -    
> -    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.
> -    
> -    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 = -1;
> -     de_ctx->tenant_id = tenant_id;
> - 
> -+    de_ctx->mpm_matcher = PatternMatchDefaultMatcher();
> -+    de_ctx->spm_matcher = SinglePatternMatchDefaultMatcher();
> -+
> -+    if (mpm_table[de_ctx->mpm_matcher].ConfigInit) {
> -+        de_ctx->mpm_cfg = mpm_table[de_ctx->mpm_matcher].ConfigInit();
> -+        if (de_ctx->mpm_cfg == 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 == DETECT_ENGINE_TYPE_DD_STUB || type == DETECT_ENGINE_TYPE_MT_STUB) {
> -         de_ctx->version = DetectEngineGetVersion();
> -         SCLogDebug("stub %u with version %u", type, de_ctx->version);
> -@@ -2511,23 +2525,8 @@ static DetectEngineCtx *DetectEngineCtxInitReal(
> -     }
> -     de_ctx->failure_fatal = (failure_fatal == 1);
> - 
> --    de_ctx->mpm_matcher = PatternMatchDefaultMatcher();
> --    de_ctx->spm_matcher = 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 = mpm_table[de_ctx->mpm_matcher].ConfigInit();
> --        if (de_ctx->mpm_cfg == 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 = SpmInitGlobalThreadCtx(de_ctx->spm_matcher);
> -     if (de_ctx->spm_global_thread_ctx == NULL) {
> -         SCLogDebug("Unable to alloc SpmGlobalThreadCtx.");
> -commit 15c83be61ac3f47bf198fe24eb908db5a84b7ccd
> -Author: Lukas Sismis <lsismis@oisf.net>
> -Date:   Mon Sep 15 11:24:23 2025 +0200
> -
> -    hs: prune stale MPM cache files
> -    
> -    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.
> -    
> -    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).
> -    
> -    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`
> - 
> -+.. _hyperscan-cache-configuration:
> -+
> - Hyperscan caching
> - ~~~~~~~~~~~~~~~~~
> - 
> -@@ -104,6 +106,24 @@ To enable this function, in `suricata.yaml` configure:
> -     sgh-mpm-caching-path: /var/lib/suricata/cache/hs
> - 
> - 
> -+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
> -+  <hyperscan-cache-configuration>` for more information.
> - 
> - 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:
> - 
> -     ret = 0;
> - 
> --    if (mpm_table[de_ctx->mpm_matcher].CacheRuleset != NULL) {
> --        mpm_table[de_ctx->mpm_matcher].CacheRuleset(de_ctx->mpm_cfg);
> --    }
> --
> -  end:
> -     gettimeofday(&de_ctx->last_reload, NULL);
> -     if (SCRunmodeGet() == 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;
> - }
> - 
> -+void DetectEngineMpmCacheService(uint32_t op_flags)
> -+{
> -+    DetectEngineCtx *de_ctx = 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 != NULL) {
> -+        de_ctx->mpm_cfg->cache_stats = mpm_table[de_ctx->mpm_matcher].CacheStatsInit();
> -+        if (de_ctx->mpm_cfg->cache_stats == NULL) {
> -+            goto error;
> -+        }
> -+    }
> -+
> -+    if (op_flags & DETECT_ENGINE_MPM_CACHE_OP_SAVE) {
> -+        if (mpm_table[de_ctx->mpm_matcher].CacheRuleset != 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 != NULL) {
> -+            mpm_table[de_ctx->mpm_matcher].CachePrune(de_ctx->mpm_cfg);
> -+        }
> -+    }
> -+
> -+    if (mpm_table[de_ctx->mpm_matcher].CacheStatsPrint != NULL) {
> -+        mpm_table[de_ctx->mpm_matcher].CacheStatsPrint(de_ctx->mpm_cfg->cache_stats);
> -+    }
> -+
> -+    if (mpm_table[de_ctx->mpm_matcher].CacheStatsDeinit != NULL) {
> -+        mpm_table[de_ctx->mpm_matcher].CacheStatsDeinit(de_ctx->mpm_cfg->cache_stats);
> -+        de_ctx->mpm_cfg->cache_stats = 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 == 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) != 1) {
> -+                    de_ctx->mpm_cfg->cache_max_age_seconds = 7ULL * 24ULL * 60ULL * 60ULL;
> -+                }
> -+            }
> -+        }
> -     }
> - 
> -     if (type == DETECT_ENGINE_TYPE_DD_STUB || type == DETECT_ENGINE_TYPE_MT_STUB) {
> -@@ -4885,6 +4936,8 @@ int DetectEngineReload(const SCInstance *suri)
> - 
> -     SCLogDebug("old_de_ctx should have been freed");
> - 
> -+    DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE | DETECT_ENGINE_MPM_CACHE_OP_PRUNE);
> -+
> -     SCLogNotice("rule reload complete");
> - 
> - #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;
> - 
> - /** Remember to add the options in SignatureIsIPOnly() at detect.c otherwise it wont be part of a signature group */
> - 
> -+#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;
> -     }
> - 
> -+    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;
> -     }
> - 
> -+    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;
> -     }
> - 
> -+    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;
> -     }
> - 
> -+    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;
> -     }
> - 
> -+    DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE | DETECT_ENGINE_MPM_CACHE_OP_PRUNE);
> -     SCLogNotice("reload-tenants complete");
> - 
> -     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;
> -     }
> - 
> -+    DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_PRUNE);
> -+
> -     /* walk free list, freeing the removed de_ctx */
> -     DetectEnginePruneFreeList();
> - 
> -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);
> -     }
> - }
> - 
> -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 <hs.h>
> - 
> --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"
> - 
> --    char hash_file_path_suffix[] = "_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 = snprintf(filename, sizeof(filename), "%s%s", hs_db_hash, hash_file_path_suffix);
> --    if (r != (uint64_t)(strlen(hs_db_hash) + strlen(hash_file_path_suffix)))
> --        return NULL;
> -+    uint64_t r = snprintf(filename, sizeof(filename), "%s" HS_CACHE_FILE_SUFFIX, db_hash);
> -+    if (r != (uint64_t)(strlen(db_hash) + strlen(HS_CACHE_FILE_SUFFIX)))
> -+        return -1;
> - 
> --    r = PathMerge(hash_file_path, sizeof(hash_file_path), folder_path, filename);
> -+    r = PathMerge(out_path, out_path_size, dir_path, filename);
> -     if (r)
> --        return NULL;
> -+        return -1;
> - 
> --    return hash_file_path;
> -+    return 0;
> - }
> - 
> - static char *HSReadStream(const char *file_path, size_t *buffer_sz)
> -@@ -121,8 +122,11 @@ static void SCHSCachePatternHash(const SCHSPattern *p, SCSha256 *sha256)
> - 
> - int HSLoadCache(hs_database_t **hs_db, const char *hs_db_hash, const char *dirpath)
> - {
> --    const char *hash_file_static = HSCacheConstructFPath(dirpath, hs_db_hash);
> --    if (hash_file_static == NULL)
> -+    char hash_file_static[PATH_MAX];
> -+    int ret = (int)HSCacheConstructFPath(
> -+            dirpath, hs_db_hash, hash_file_static, sizeof(hash_file_static));
> -+
> -+    if (ret != 0)
> -         return -1;
> - 
> -     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
> - 
> -     FILE *db_cache = fopen(hash_file_static, "r");
> -     char *buffer = NULL;
> --    int ret = 0;
> -     if (db_cache) {
> -         size_t buffer_size;
> -         buffer = 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 = false;
> -     char *db_stream = NULL;
> -     size_t db_size;
> --    int ret = -1;
> -+    int ret;
> - 
> -     hs_error_t err = hs_serialize_database(hs_db, &db_stream, &db_size);
> -     if (err != HS_SUCCESS) {
> -         SCLogWarning("Failed to serialize Hyperscan database: %s", HSErrorToStr(err));
> -+        ret = -1;
> -         goto cleanup;
> -     }
> - 
> --    const char *hash_file_static = HSCacheConstructFPath(dstpath, hs_db_hash);
> -+    char hash_file_static[PATH_MAX];
> -+    ret = (int)HSCacheConstructFPath(
> -+            dstpath, hs_db_hash, hash_file_static, sizeof(hash_file_static));
> -+    if (ret != 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 = true;
> -         }
> -+        ret = -1;
> -         goto cleanup;
> -     }
> -     size_t r = 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;
> -     }
> - 
> --    ret = 0;
> - cleanup:
> -     if (db_stream)
> -         SCFree(db_stream);
> -@@ -270,4 +278,187 @@ void HSSaveCacheIterator(void *data, void *aux)
> -     }
> - }
> - 
> -+void HSCacheFilenameUsedIterator(void *data, void *aux)
> -+{
> -+    PatternDatabase *pd = (PatternDatabase *)data;
> -+    struct HsInUseCacheFilesIteratorData *iter_data = (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)) != 0) {
> -+        return;
> -+    }
> -+
> -+    char *fpath = SCCalloc(PATH_MAX, sizeof(char));
> -+    if (fpath == 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 = 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 = strrchr(filename, '_');
> -+    if (underscore == NULL || strcmp(underscore, HS_CACHE_FILE_SUFFIX) != 0) {
> -+        return true;
> -+    }
> -+
> -+    return false;
> -+}
> -+
> -+int SCHSCachePruneEvaluate(MpmConfig *mpm_conf, HashTable *inuse_caches)
> -+{
> -+    if (mpm_conf == NULL || mpm_conf->cache_dir_path == NULL)
> -+        return -1;
> -+    if (mpm_conf->cache_max_age_seconds == 0)
> -+        return 0; // disabled
> -+
> -+    const time_t now = time(NULL);
> -+    if (now == (time_t)-1) {
> -+        return -1;
> -+    } else if (mpm_conf->cache_max_age_seconds >= (uint64_t)now) {
> -+        return 0;
> -+    }
> -+
> -+    DIR *dir = opendir(mpm_conf->cache_dir_path);
> -+    if (dir == NULL) {
> -+        return -1;
> -+    }
> -+
> -+    struct dirent *ent;
> -+    char path[PATH_MAX];
> -+    uint32_t considered = 0, removed = 0;
> -+    const time_t cutoff = now - (time_t)mpm_conf->cache_max_age_seconds;
> -+    while ((ent = readdir(dir)) != NULL) {
> -+        const char *name = ent->d_name;
> -+        size_t namelen = strlen(name);
> -+        if (namelen < 3 || strcmp(name + namelen - 3, ".hs") != 0)
> -+            continue;
> -+
> -+        if (PathMerge(path, ARRAY_SIZE(path), mpm_conf->cache_dir_path, name) != 0)
> -+            continue;
> -+
> -+        struct stat st;
> -+        if (stat(path, &st) != 0 || !S_ISREG(st.st_mode))
> -+            continue;
> -+
> -+        considered++;
> -+
> -+        const bool prune_by_age = HSPruneFileByAge(st.st_mtime, cutoff);
> -+        const bool prune_by_version = HSPruneFileByVersion(name);
> -+        if (!prune_by_age && !prune_by_version)
> -+            continue;
> -+
> -+        void *cache_inuse = HashTableLookup(inuse_caches, path, (uint16_t)strlen(path));
> -+        if (cache_inuse != NULL)
> -+            continue; // in use
> -+
> -+        if (unlink(path) == 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 = mpm_conf->cache_stats;
> -+    if (pd_cache_stats) {
> -+        pd_cache_stats->hs_dbs_cache_pruned_cnt = removed;
> -+        pd_cache_stats->hs_dbs_cache_pruned_considered_cnt = considered;
> -+        pd_cache_stats->hs_dbs_cache_pruned_cutoff = cutoff;
> -+        pd_cache_stats->cache_max_age_seconds = mpm_conf->cache_max_age_seconds;
> -+    }
> -+    return 0;
> -+}
> -+
> -+void *SCHSCacheStatsInit(void)
> -+{
> -+    PatternDatabaseCache *pd_cache_stats = SCCalloc(1, sizeof(PatternDatabaseCache));
> -+    if (pd_cache_stats == NULL) {
> -+        SCLogError("Failed to allocate memory for Hyperscan cache stats");
> -+        return NULL;
> -+    }
> -+    return pd_cache_stats;
> -+}
> -+
> -+void SCHSCacheStatsPrint(void *data)
> -+{
> -+    if (data == NULL) {
> -+        return;
> -+    }
> -+
> -+    PatternDatabaseCache *pd_cache_stats = (PatternDatabaseCache *)data;
> -+
> -+    char time_str[64];
> -+    struct tm tm_s;
> -+    struct tm *tm_info = SCLocalTime(pd_cache_stats->hs_dbs_cache_pruned_cutoff, &tm_s);
> -+    if (tm_info != 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 == NULL) {
> -+        return;
> -+    }
> -+    PatternDatabaseCache *pd_cache_stats = (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;
> - };
> - 
> -+/**
> -+ * \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 */
> - 
> - #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;
> - 
> - 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 = { 0 };
> --    struct HsIteratorData iter_data = { .pd_stats = &pd_stats,
> -+    PatternDatabaseCache *pd_stats = mpm_conf->cache_stats;
> -+    struct HsIteratorData iter_data = { .pd_stats = pd_stats,
> -         .cache_path = 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;
> - }
> - 
> -+static uint32_t FilenameTableHash(HashTable *ht, void *data, uint16_t len)
> -+{
> -+    const char *fname = data;
> -+    uint32_t hash = hashlittle_safe(data, strlen(fname), 0);
> -+    hash %= 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 =
> -+            HashTableInit(INIT_DB_HASH_SIZE, FilenameTableHash, NULL, FilenameTableFree);
> -+    if (inuse_caches == NULL) {
> -+        return -1;
> -+    }
> -+    struct HsInUseCacheFilesIteratorData iter_data = { .tbl = inuse_caches,
> -+        .cache_path = mpm_conf->cache_dir_path };
> -+
> -+    SCMutexLock(&g_db_table_mutex);
> -+    HashTableIterate(g_db_table, HSCacheFilenameUsedIterator, &iter_data);
> -+    SCMutexUnlock(&g_db_table_mutex);
> -+
> -+    int r = 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 = SCHSAddPatternCS;
> -     mpm_table[MPM_HS].AddPatternNocase = SCHSAddPatternCI;
> -     mpm_table[MPM_HS].Prepare = SCHSPreparePatterns;
> -+    mpm_table[MPM_HS].CacheStatsInit = SCHSCacheStatsInit;
> -+    mpm_table[MPM_HS].CacheStatsPrint = SCHSCacheStatsPrint;
> -+    mpm_table[MPM_HS].CacheStatsDeinit = SCHSCacheStatsDeinit;
> -     mpm_table[MPM_HS].CacheRuleset = SCHSCacheRuleset;
> -+    mpm_table[MPM_HS].CachePrune = SCHSCachePrune;
> -     mpm_table[MPM_HS].Search = SCHSSearch;
> -     mpm_table[MPM_HS].PrintCtx = SCHSPrintInfo;
> -     mpm_table[MPM_HS].PrintThreadCtx = 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_ {
> - 
> - typedef struct MpmConfig_ {
> -     const char *cache_dir_path;
> -+    uint64_t cache_max_age_seconds; /* 0 means disabled/no pruning policy */
> -+    void *cache_stats;
> - } MpmConfig;
> - 
> - 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 <lsismis@oisf.net>
> -Date:   Mon Nov 3 19:47:16 2025 +0100
> -
> -    hs: warn about the same cache directory
> -    
> -    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.
> -- 
> 2.53.0
> 
>
  

Patch

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 @@ 
 
 include Config
 
-VER        = 8.0.3
+VER        = 8.0.4
 
 THISAPP    = suricata-$(VER)
 DL_FILE    = $(THISAPP).tar.gz
@@ -40,7 +40,7 @@  objects = $(DL_FILE)
 
 $(DL_FILE) = $(DL_FROM)/$(DL_FILE)
 
-$(DL_FILE)_BLAKE2 = ab87fde815338a7520badd2f4d8c8bfaccc778ecffbb13028fe9d561b1bf0e4ef2a43296b88fffb306df9e28fcd5997fa22c72ac887c40efbea799e0110fcb56
+$(DL_FILE)_BLAKE2 = a6c1958d82bb8c288c8d551d99851d19a89073397bda38bc90907950d17c35e40eb4845e9a88913bafc5c56bdad8c026e0fb665c494b102861c2b8f210c72d7f
 
 install : $(TARGET)
 
@@ -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
 
 	cd $(DIR_APP) && LDFLAGS="$(LDFLAGS)" ./configure \
 		--prefix=/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 <lsismis@oisf.net>
-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 <lsismis@oisf.net>
-  *
-- * MPM pattern matcher that calls the Hyperscan regex matcher.
-+ * Hyperscan cache helper utilities for MPM cache files.
-  */
- 
- #include "suricata-common.h"
-commit 2a313ff429eb49be5e4c3b9dadfca127fa64c5fe
-Author: Lukas Sismis <lsismis@oisf.net>
-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];
- 
-     char hash_file_path_suffix[] = "_v1.hs";
--    char filename[PATH_MAX];
-+    char filename[NAME_MAX];
-     uint64_t r = snprintf(
-             filename, sizeof(filename), "%020" PRIu64 "%s", hs_db_hash, hash_file_path_suffix);
-     if (r != (uint64_t)(20 + strlen(hash_file_path_suffix)))
-commit c282880174875fab6bcc62a2a60c85b58dfb0d32
-Author: Lukas Sismis <lsismis@oisf.net>
-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 @@
- 
- #ifdef BUILD_HYPERSCAN
- 
-+#include "rust.h"
- #include <hs.h>
- 
--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];
- 
-     char hash_file_path_suffix[] = "_v1.hs";
-     char filename[NAME_MAX];
--    uint64_t r = snprintf(
--            filename, sizeof(filename), "%020" PRIu64 "%s", hs_db_hash, hash_file_path_suffix);
--    if (r != (uint64_t)(20 + strlen(hash_file_path_suffix)))
-+    uint64_t r = snprintf(filename, sizeof(filename), "%s%s", hs_db_hash, hash_file_path_suffix);
-+    if (r != (uint64_t)(strlen(hs_db_hash) + strlen(hash_file_path_suffix)))
-         return NULL;
- 
-     r = 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 == NULL);
-     BUG_ON(p->sids == NULL);
- 
--    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));
- }
- 
--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 = HSCacheConstructFPath(dirpath, hs_db_hash);
-     if (hash_file_static == NULL)
-@@ -161,7 +161,7 @@ freeup:
-     return ret;
- }
- 
--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 = false;
-     char *db_stream = NULL;
-@@ -220,14 +220,26 @@ cleanup:
-     return ret;
- }
- 
--uint64_t HSHashDb(const PatternDatabase *pd)
-+int HSHashDb(const PatternDatabase *pd, char *hash, size_t hash_len)
- {
--    uint32_t hash[2] = { 0 };
--    hashword2(&pd->pattern_cnt, 1, &hash[0], &hash[1]);
-+    SCSha256 *hasher = SCSha256New();
-+    if (hasher == NULL) {
-+        SCLogDebug("sha256 hashing failed");
-+        return -1;
-+    }
-+    SCSha256Update(hasher, (const uint8_t *)&pd->pattern_cnt, sizeof(pd->pattern_cnt));
-     for (uint32_t i = 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 = NULL;
-+        SCLogDebug("sha256 hashing failed");
-+        return -1;
-     }
--    return ((uint64_t)hash[1] << 32) | hash[0];
-+
-+    hasher = NULL;
-+    return 0;
- }
- 
- void HSSaveCacheIterator(void *data, void *aux)
-@@ -244,7 +256,11 @@ void HSSaveCacheIterator(void *data, void *aux)
-         return;
-     }
- 
--    if (HSSaveCache(pd->hs_db, HSHashDb(pd), iter_data->cache_path) == 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)) != 0) {
-+        return;
-+    }
-+    if (HSSaveCache(pd->hs_db, hs_db_hash, iter_data->cache_path) == 0) {
-         pd->cached = 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;
- };
- 
--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 */
- 
-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 = *pd;
--        uint64_t db_lookup_hash = HSHashDb(pd_cached);
--        if (HSLoadCache(&pd_cached->hs_db, db_lookup_hash, cache_dir_path) == 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)) != 0) {
-+            return -1;
-+        }
-+        if (HSLoadCache(&pd_cached->hs_db, hs_db_hash, cache_dir_path) == 0) {
-             pd_cached->ref_cnt = 1;
-             pd_cached->cached = true;
-             if (HSScratchAlloc(pd_cached->hs_db) != 0) {
-commit 3e4fdb2118bfcb8b2644944daded2d8c67420499
-Author: Lukas Sismis <lsismis@oisf.net>
-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 = [
-  "windows-sys 0.52.0",
- ]
- 
-+[[package]]
-+name = "humantime"
-+version = "2.3.0"
-+source = "registry+https://github.com/rust-lang/crates.io-index"
-+checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
-+
- [[package]]
- name = "indexmap"
- version = "2.11.4"
-@@ -1551,6 +1557,7 @@ dependencies = [
-  "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 = "~1.5.0"
- base64 = "~0.22.1"
- bendy = { version = "~0.3.3", default-features = false }
- asn1-rs = { version = "~0.6.2" }
-+humantime = "~2.3.0"
- ldap-parser = { version = "~0.5.0" }
- hex = "~0.4.3"
- psl = "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 @@
- 
- //! Utility module.
- 
-+use std::borrow::Cow;
- use std::ffi::CStr;
- use std::os::raw::c_char;
- 
-@@ -26,6 +27,8 @@ use nom8::combinator::verify;
- use nom8::multi::many1_count;
- use nom8::{AsChar, IResult, Parser};
- 
-+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;
- }
- 
-+/// 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 = 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 = match CStr::from_ptr(input).to_str() {
-+        Ok(s) => s,
-+        Err(_) => return -1,
-+    };
-+
-+    let trimmed = input_str.trim();
-+    if trimmed.is_empty() {
-+        return -1;
-+    }
-+
-+    let normalized = duration_unit_normalize(trimmed);
-+    match parse_duration(normalized.as_ref()) {
-+        Ok(duration) => {
-+            *res = duration.as_secs();
-+            0
-+        }
-+        Err(_) => -1,
-+    }
-+}
-+
- #[cfg(test)]
- mod tests {
- 
-     use super::*;
-+    use std::ffi::CString;
-+    use std::ptr::{null, null_mut};
- 
-     #[test]
-     fn test_parse_domain() {
-@@ -83,4 +132,73 @@ mod tests {
-         let buf1: &[u8] = "a(x)y.com".as_bytes();
-         assert!(parse_domain(buf1).is_err());
-     }
-+
-+    #[test]
-+    fn test_parse_time_valid() {
-+        unsafe {
-+            let mut v: u64 = 0;
-+
-+            let s = CString::new("10").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 10);
-+
-+            let s = CString::new("0").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 0);
-+
-+            let s = CString::new("2H").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 7200);
-+
-+            let s = CString::new("1 day").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 86400);
-+
-+            let s = CString::new("1w").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 604800);
-+
-+            let s = CString::new("1 week").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 604800);
-+
-+            let s = CString::new("1y").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 31557600);
-+
-+            let s = CString::new("1 year").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), 0);
-+            assert_eq!(v, 31557600);
-+
-+            // max
-+            let s = 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 = 0;
-+            let s = CString::new("10q").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1);
-+
-+            let s = CString::new("abc").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1);
-+
-+            let s = CString::new("-300s").unwrap();
-+            assert_eq!(SCParseTimeDuration(s.as_ptr(), &mut v), -1);
-+
-+            let s = 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 = (u64::MAX / 31557600) + 1;
-+            let s = 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 <lsismis@oisf.net>
-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"
- 
- /** 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;
- }
- 
-+/**
-+ * \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 = NULL;
-+
-+    if (SCConfGet(name, &strval) == 0)
-+        return 0;
-+
-+    if (strval == NULL || strval[0] == '\0')
-+        return 0;
-+
-+    if (SCParseTimeDuration(strval, val) != 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 <lsismis@oisf.net>
-Date:   Mon Sep 15 11:36:01 2025 +0200
-
-    path: signal last use of the file (touch)
-    
-    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.
-    
-    This is needed to remove stale cache files of the system.
-    
-    Access time is not used as it may be, on the system level, disabled.
-    
-    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) != 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 == NULL || path[0] == '\0') {
-+        errno = EINVAL;
-+        return -1;
-+    }
-+#ifndef OS_WIN32
-+    struct utimbuf ub;
-+    ub.actime = ub.modtime = time(NULL);
-+    if (utime(path, &ub) == 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);
- 
- #endif /* SURICATA_UTIL_PATH_H */
-commit 7031c268655aec5c44420902bbda6f7aea8eba33
-Author: Lukas Sismis <lsismis@oisf.net>
-Date:   Mon Sep 15 11:39:02 2025 +0200
-
-    hs: touch cache files on use to signal activity
-    
-    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
-         }
- 
-         ret = 0;
-+        /* Touch file to update modification time so active caches are retained. */
-+        if (SCTouchFile(hash_file_static) != 0) {
-+            SCLogDebug("Failed to update mtime for %s", hash_file_static);
-+        }
-         goto freeup;
-     }
- 
-commit 08f5abe5e967bbcfbc0c11a797ef86125afd3db8
-Author: Lukas Sismis <lsismis@oisf.net>
-Date:   Sun Dec 28 00:09:29 2025 +0100
-
-    detect-engine: make mpm & spm part of MT stub ctx
-    
-    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.
-    
-    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 = -1;
-     de_ctx->tenant_id = tenant_id;
- 
-+    de_ctx->mpm_matcher = PatternMatchDefaultMatcher();
-+    de_ctx->spm_matcher = SinglePatternMatchDefaultMatcher();
-+
-+    if (mpm_table[de_ctx->mpm_matcher].ConfigInit) {
-+        de_ctx->mpm_cfg = mpm_table[de_ctx->mpm_matcher].ConfigInit();
-+        if (de_ctx->mpm_cfg == 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 == DETECT_ENGINE_TYPE_DD_STUB || type == DETECT_ENGINE_TYPE_MT_STUB) {
-         de_ctx->version = DetectEngineGetVersion();
-         SCLogDebug("stub %u with version %u", type, de_ctx->version);
-@@ -2511,23 +2525,8 @@ static DetectEngineCtx *DetectEngineCtxInitReal(
-     }
-     de_ctx->failure_fatal = (failure_fatal == 1);
- 
--    de_ctx->mpm_matcher = PatternMatchDefaultMatcher();
--    de_ctx->spm_matcher = 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 = mpm_table[de_ctx->mpm_matcher].ConfigInit();
--        if (de_ctx->mpm_cfg == 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 = SpmInitGlobalThreadCtx(de_ctx->spm_matcher);
-     if (de_ctx->spm_global_thread_ctx == NULL) {
-         SCLogDebug("Unable to alloc SpmGlobalThreadCtx.");
-commit 15c83be61ac3f47bf198fe24eb908db5a84b7ccd
-Author: Lukas Sismis <lsismis@oisf.net>
-Date:   Mon Sep 15 11:24:23 2025 +0200
-
-    hs: prune stale MPM cache files
-    
-    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.
-    
-    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).
-    
-    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`
- 
-+.. _hyperscan-cache-configuration:
-+
- Hyperscan caching
- ~~~~~~~~~~~~~~~~~
- 
-@@ -104,6 +106,24 @@ To enable this function, in `suricata.yaml` configure:
-     sgh-mpm-caching-path: /var/lib/suricata/cache/hs
- 
- 
-+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
-+  <hyperscan-cache-configuration>` for more information.
- 
- 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:
- 
-     ret = 0;
- 
--    if (mpm_table[de_ctx->mpm_matcher].CacheRuleset != NULL) {
--        mpm_table[de_ctx->mpm_matcher].CacheRuleset(de_ctx->mpm_cfg);
--    }
--
-  end:
-     gettimeofday(&de_ctx->last_reload, NULL);
-     if (SCRunmodeGet() == 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;
- }
- 
-+void DetectEngineMpmCacheService(uint32_t op_flags)
-+{
-+    DetectEngineCtx *de_ctx = 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 != NULL) {
-+        de_ctx->mpm_cfg->cache_stats = mpm_table[de_ctx->mpm_matcher].CacheStatsInit();
-+        if (de_ctx->mpm_cfg->cache_stats == NULL) {
-+            goto error;
-+        }
-+    }
-+
-+    if (op_flags & DETECT_ENGINE_MPM_CACHE_OP_SAVE) {
-+        if (mpm_table[de_ctx->mpm_matcher].CacheRuleset != 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 != NULL) {
-+            mpm_table[de_ctx->mpm_matcher].CachePrune(de_ctx->mpm_cfg);
-+        }
-+    }
-+
-+    if (mpm_table[de_ctx->mpm_matcher].CacheStatsPrint != NULL) {
-+        mpm_table[de_ctx->mpm_matcher].CacheStatsPrint(de_ctx->mpm_cfg->cache_stats);
-+    }
-+
-+    if (mpm_table[de_ctx->mpm_matcher].CacheStatsDeinit != NULL) {
-+        mpm_table[de_ctx->mpm_matcher].CacheStatsDeinit(de_ctx->mpm_cfg->cache_stats);
-+        de_ctx->mpm_cfg->cache_stats = 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 == 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) != 1) {
-+                    de_ctx->mpm_cfg->cache_max_age_seconds = 7ULL * 24ULL * 60ULL * 60ULL;
-+                }
-+            }
-+        }
-     }
- 
-     if (type == DETECT_ENGINE_TYPE_DD_STUB || type == DETECT_ENGINE_TYPE_MT_STUB) {
-@@ -4885,6 +4936,8 @@ int DetectEngineReload(const SCInstance *suri)
- 
-     SCLogDebug("old_de_ctx should have been freed");
- 
-+    DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE | DETECT_ENGINE_MPM_CACHE_OP_PRUNE);
-+
-     SCLogNotice("rule reload complete");
- 
- #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;
- 
- /** Remember to add the options in SignatureIsIPOnly() at detect.c otherwise it wont be part of a signature group */
- 
-+#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;
-     }
- 
-+    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;
-     }
- 
-+    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;
-     }
- 
-+    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;
-     }
- 
-+    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;
-     }
- 
-+    DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_SAVE | DETECT_ENGINE_MPM_CACHE_OP_PRUNE);
-     SCLogNotice("reload-tenants complete");
- 
-     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;
-     }
- 
-+    DetectEngineMpmCacheService(DETECT_ENGINE_MPM_CACHE_OP_PRUNE);
-+
-     /* walk free list, freeing the removed de_ctx */
-     DetectEnginePruneFreeList();
- 
-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);
-     }
- }
- 
-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 <hs.h>
- 
--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"
- 
--    char hash_file_path_suffix[] = "_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 = snprintf(filename, sizeof(filename), "%s%s", hs_db_hash, hash_file_path_suffix);
--    if (r != (uint64_t)(strlen(hs_db_hash) + strlen(hash_file_path_suffix)))
--        return NULL;
-+    uint64_t r = snprintf(filename, sizeof(filename), "%s" HS_CACHE_FILE_SUFFIX, db_hash);
-+    if (r != (uint64_t)(strlen(db_hash) + strlen(HS_CACHE_FILE_SUFFIX)))
-+        return -1;
- 
--    r = PathMerge(hash_file_path, sizeof(hash_file_path), folder_path, filename);
-+    r = PathMerge(out_path, out_path_size, dir_path, filename);
-     if (r)
--        return NULL;
-+        return -1;
- 
--    return hash_file_path;
-+    return 0;
- }
- 
- static char *HSReadStream(const char *file_path, size_t *buffer_sz)
-@@ -121,8 +122,11 @@ static void SCHSCachePatternHash(const SCHSPattern *p, SCSha256 *sha256)
- 
- int HSLoadCache(hs_database_t **hs_db, const char *hs_db_hash, const char *dirpath)
- {
--    const char *hash_file_static = HSCacheConstructFPath(dirpath, hs_db_hash);
--    if (hash_file_static == NULL)
-+    char hash_file_static[PATH_MAX];
-+    int ret = (int)HSCacheConstructFPath(
-+            dirpath, hs_db_hash, hash_file_static, sizeof(hash_file_static));
-+
-+    if (ret != 0)
-         return -1;
- 
-     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
- 
-     FILE *db_cache = fopen(hash_file_static, "r");
-     char *buffer = NULL;
--    int ret = 0;
-     if (db_cache) {
-         size_t buffer_size;
-         buffer = 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 = false;
-     char *db_stream = NULL;
-     size_t db_size;
--    int ret = -1;
-+    int ret;
- 
-     hs_error_t err = hs_serialize_database(hs_db, &db_stream, &db_size);
-     if (err != HS_SUCCESS) {
-         SCLogWarning("Failed to serialize Hyperscan database: %s", HSErrorToStr(err));
-+        ret = -1;
-         goto cleanup;
-     }
- 
--    const char *hash_file_static = HSCacheConstructFPath(dstpath, hs_db_hash);
-+    char hash_file_static[PATH_MAX];
-+    ret = (int)HSCacheConstructFPath(
-+            dstpath, hs_db_hash, hash_file_static, sizeof(hash_file_static));
-+    if (ret != 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 = true;
-         }
-+        ret = -1;
-         goto cleanup;
-     }
-     size_t r = 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;
-     }
- 
--    ret = 0;
- cleanup:
-     if (db_stream)
-         SCFree(db_stream);
-@@ -270,4 +278,187 @@ void HSSaveCacheIterator(void *data, void *aux)
-     }
- }
- 
-+void HSCacheFilenameUsedIterator(void *data, void *aux)
-+{
-+    PatternDatabase *pd = (PatternDatabase *)data;
-+    struct HsInUseCacheFilesIteratorData *iter_data = (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)) != 0) {
-+        return;
-+    }
-+
-+    char *fpath = SCCalloc(PATH_MAX, sizeof(char));
-+    if (fpath == 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 = 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 = strrchr(filename, '_');
-+    if (underscore == NULL || strcmp(underscore, HS_CACHE_FILE_SUFFIX) != 0) {
-+        return true;
-+    }
-+
-+    return false;
-+}
-+
-+int SCHSCachePruneEvaluate(MpmConfig *mpm_conf, HashTable *inuse_caches)
-+{
-+    if (mpm_conf == NULL || mpm_conf->cache_dir_path == NULL)
-+        return -1;
-+    if (mpm_conf->cache_max_age_seconds == 0)
-+        return 0; // disabled
-+
-+    const time_t now = time(NULL);
-+    if (now == (time_t)-1) {
-+        return -1;
-+    } else if (mpm_conf->cache_max_age_seconds >= (uint64_t)now) {
-+        return 0;
-+    }
-+
-+    DIR *dir = opendir(mpm_conf->cache_dir_path);
-+    if (dir == NULL) {
-+        return -1;
-+    }
-+
-+    struct dirent *ent;
-+    char path[PATH_MAX];
-+    uint32_t considered = 0, removed = 0;
-+    const time_t cutoff = now - (time_t)mpm_conf->cache_max_age_seconds;
-+    while ((ent = readdir(dir)) != NULL) {
-+        const char *name = ent->d_name;
-+        size_t namelen = strlen(name);
-+        if (namelen < 3 || strcmp(name + namelen - 3, ".hs") != 0)
-+            continue;
-+
-+        if (PathMerge(path, ARRAY_SIZE(path), mpm_conf->cache_dir_path, name) != 0)
-+            continue;
-+
-+        struct stat st;
-+        if (stat(path, &st) != 0 || !S_ISREG(st.st_mode))
-+            continue;
-+
-+        considered++;
-+
-+        const bool prune_by_age = HSPruneFileByAge(st.st_mtime, cutoff);
-+        const bool prune_by_version = HSPruneFileByVersion(name);
-+        if (!prune_by_age && !prune_by_version)
-+            continue;
-+
-+        void *cache_inuse = HashTableLookup(inuse_caches, path, (uint16_t)strlen(path));
-+        if (cache_inuse != NULL)
-+            continue; // in use
-+
-+        if (unlink(path) == 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 = mpm_conf->cache_stats;
-+    if (pd_cache_stats) {
-+        pd_cache_stats->hs_dbs_cache_pruned_cnt = removed;
-+        pd_cache_stats->hs_dbs_cache_pruned_considered_cnt = considered;
-+        pd_cache_stats->hs_dbs_cache_pruned_cutoff = cutoff;
-+        pd_cache_stats->cache_max_age_seconds = mpm_conf->cache_max_age_seconds;
-+    }
-+    return 0;
-+}
-+
-+void *SCHSCacheStatsInit(void)
-+{
-+    PatternDatabaseCache *pd_cache_stats = SCCalloc(1, sizeof(PatternDatabaseCache));
-+    if (pd_cache_stats == NULL) {
-+        SCLogError("Failed to allocate memory for Hyperscan cache stats");
-+        return NULL;
-+    }
-+    return pd_cache_stats;
-+}
-+
-+void SCHSCacheStatsPrint(void *data)
-+{
-+    if (data == NULL) {
-+        return;
-+    }
-+
-+    PatternDatabaseCache *pd_cache_stats = (PatternDatabaseCache *)data;
-+
-+    char time_str[64];
-+    struct tm tm_s;
-+    struct tm *tm_info = SCLocalTime(pd_cache_stats->hs_dbs_cache_pruned_cutoff, &tm_s);
-+    if (tm_info != 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 == NULL) {
-+        return;
-+    }
-+    PatternDatabaseCache *pd_cache_stats = (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;
- };
- 
-+/**
-+ * \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 */
- 
- #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;
- 
- 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 = { 0 };
--    struct HsIteratorData iter_data = { .pd_stats = &pd_stats,
-+    PatternDatabaseCache *pd_stats = mpm_conf->cache_stats;
-+    struct HsIteratorData iter_data = { .pd_stats = pd_stats,
-         .cache_path = 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;
- }
- 
-+static uint32_t FilenameTableHash(HashTable *ht, void *data, uint16_t len)
-+{
-+    const char *fname = data;
-+    uint32_t hash = hashlittle_safe(data, strlen(fname), 0);
-+    hash %= 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 =
-+            HashTableInit(INIT_DB_HASH_SIZE, FilenameTableHash, NULL, FilenameTableFree);
-+    if (inuse_caches == NULL) {
-+        return -1;
-+    }
-+    struct HsInUseCacheFilesIteratorData iter_data = { .tbl = inuse_caches,
-+        .cache_path = mpm_conf->cache_dir_path };
-+
-+    SCMutexLock(&g_db_table_mutex);
-+    HashTableIterate(g_db_table, HSCacheFilenameUsedIterator, &iter_data);
-+    SCMutexUnlock(&g_db_table_mutex);
-+
-+    int r = 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 = SCHSAddPatternCS;
-     mpm_table[MPM_HS].AddPatternNocase = SCHSAddPatternCI;
-     mpm_table[MPM_HS].Prepare = SCHSPreparePatterns;
-+    mpm_table[MPM_HS].CacheStatsInit = SCHSCacheStatsInit;
-+    mpm_table[MPM_HS].CacheStatsPrint = SCHSCacheStatsPrint;
-+    mpm_table[MPM_HS].CacheStatsDeinit = SCHSCacheStatsDeinit;
-     mpm_table[MPM_HS].CacheRuleset = SCHSCacheRuleset;
-+    mpm_table[MPM_HS].CachePrune = SCHSCachePrune;
-     mpm_table[MPM_HS].Search = SCHSSearch;
-     mpm_table[MPM_HS].PrintCtx = SCHSPrintInfo;
-     mpm_table[MPM_HS].PrintThreadCtx = 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_ {
- 
- typedef struct MpmConfig_ {
-     const char *cache_dir_path;
-+    uint64_t cache_max_age_seconds; /* 0 means disabled/no pruning policy */
-+    void *cache_stats;
- } MpmConfig;
- 
- 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 <lsismis@oisf.net>
-Date:   Mon Nov 3 19:47:16 2025 +0100
-
-    hs: warn about the same cache directory
-    
-    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.