ANT-2026-61Y3NTY3 · oisf/suricata

use-after-free high

GHSA-qmc9-vqq2-8mv3

Severity Claude critical · Security research firm high · Maintainer -

Discovered by Claude Mythos Preview

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Trail of Bits.

ANT-2026-61Y3NTY3: Use-after-free in mod.rs:547

In Suricata's detection transform pipeline, decompress_transform (decompress.rs:136) calls SCInspectionBufferCheckAndExpand, which reallocs the inspection buffer — freeing the old 4096-byte region. A slice still pointing into that freed region is then passed to gunzip_transform_do (decompress.rs:118), which hands it to flate2's GzDecoder::new. The gzip header parser performs a memcpy from the dangling pointer. Triggering requires a detection rule chaining the to_lowercase and decompress transforms on http.uri, plus HTTP traffic whose URI is large enough to force the realloc.

Target

Project: suricata
Location: mod.rs:547

Technical Details

ASAN: heap-use-after-free, READ of size 10 at 0x7088ec6ff500. The free and the use occur within the same invocation of decompress_transform: line 136 grows the inspection buffer via realloc (invalidating prior pointers), but the input slice handed to the GzDecoder at line 154→118 was derived before the realloc and is not refreshed. This is the classic realloc-invalidates-borrow pattern crossing the Rust/C FFI boundary, where Rust's borrow checker cannot see the C-side realloc.

Crash trace (truncated — full trace in attached crash.log):

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 4180530270
INFO: Loaded 1 modules   (1222905 inline 8-bit counters): 1222905 [0x5a9a5f29bce0, 0x5a9a5f3c65d9), 
INFO: Loaded 1 PC tables (1222905 PCs): 1222905 [0x5a9a5f3c65e0,0x5a9a6066f570), 
/out/fuzz_sigpcap_aware: Running 1 inputs 1 time(s) each.
Running: /tmp/poc
EXIT_CODE:1


=== ASAN Report ===
=================================================================
==27==ERROR: AddressSanitizer: heap-use-after-free on address 0x7088ec6ff500 at pc 0x5a9a5aa77cdb bp 0x7ffe141f4050 sp 0x7ffe141f3810
READ of size 10 at 0x7088ec6ff500 thread T0 (Suricata-Main)
    #0 0x5a9a5aa77cda in __asan_memcpy /src/llvm-project/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:63:3
    #1 0x5a9a5bb128a0 in core::ptr::copy_nonoverlapping::h2cdfeabac3ddf908 /rust/rustup/toolchains/nightly-2025-09-05-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:547:14
    #2 0x5a9a5bb128a0 in core::slice::_$LT$impl$u20$$u5b$T$u5d$$GT$::copy_from_slice::h9b15d3c0cc69a2de /rust/rustup/toolchains/nightly-2025-09-05-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/mod.rs:3881:13
    #3 0x5a9a5bb128a0 in std::io::impls::_$LT$impl$u20$std..io..Read$u20$for$u20$$RF$$u5b$u8$u5d$$GT$::read::h6fe985252d52242f /rust/rustup/toolchains/nightly-2025-09-05-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/impls.rs:310:24
    #4 0x5a9a5be49e57 in flate2::gz::read_into::h2d1c4bf8cbc7ae28 /rust/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.0.35/src/gz/mod.rs:247:13
    #5 0x5a9a5be49e57 in flate2::gz::GzHeaderParser::parse::haf9b32ff2b7e62fb /rust/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.0.35/src/gz/mod.rs:128:35
    #6 0x5a9a5be4d680 in flate2::gz::bufread::GzDecoder$LT$R$GT$::new::hfe0a2d27b1dc5912 /rust/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.0.35/src/gz/bufread.rs:232:41
    #7 0x5a9a5c735053 in suricata::detect::transforms::decompress::gunzip_transform_do::h643c8f1b6e84635d /src/suricata/rust/src/detect/transforms/decompress.rs:118:18
    [... 60 more frames — full trace in crash.log]

Reproduction

  1. Operator has a rule using http.uri; to_lowercase; decompress; (or equivalent transform chain)
  2. Attacker sends an HTTP request to a monitored host with a URI crafted so the tolower_transform allocates a 4096-byte inspection buffer
  3. decompress_transform calls SCInspectionBufferCheckAndExpand which reallocs the buffer to a new address, freeing the old region
  4. gunzip_transform_do passes the stale slice to flate2 GzDecoder, which memcpy-reads 10 bytes from freed heap

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-61Y3NTY3.


Reference: ANT-2026-61Y3NTY3
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Trail of Bits.

Verdict
true positive
Severity
high
UPSTREAM FIX

The change that resolved this finding.

diff --git a/rust/src/detect/transforms/decompress.rs b/rust/src/detect/transforms/decompress.rs
index 44c21951d09c..3104a54b0090 100644
--- a/rust/src/detect/transforms/decompress.rs
+++ b/rust/src/detect/transforms/decompress.rs
@@ -20,8 +20,8 @@ use crate::detect::SIGMATCH_OPTIONAL_OPT;
 use flate2::bufread::{GzDecoder, ZlibDecoder};
 use suricata_sys::sys::{
     DetectEngineCtx, DetectEngineThreadCtx, InspectionBuffer, SCDetectHelperTransformRegister,
-    SCDetectSignatureAddTransform, SCInspectionBufferCheckAndExpand, SCInspectionBufferTruncate,
-    SCTransformTableElmt, Signature,
+    SCDetectSignatureAddTransform, SCInspectionBufferCheckAndExpand, SCInspectionBufferInPlace,
+    SCInspectionBufferTruncate, SCTransformTableElmt, Signature,
 };
 
 use std::ffi::CStr;
@@ -132,26 +132,27 @@ unsafe fn decompress_transform(
     if input.is_null() || input_len == 0 {
         return;
     }
-    let input = build_slice!(input, input_len as usize);
+    let tmp;
+    let slice_input = if SCInspectionBufferInPlace(buffer) {
+        // need a temporary buffer as we cannot do the transfom in place
+        // rather copy the input which should be smaller than the decompressed output
+        // (Transform is tried in place when there are multiple chained transforms)
+        // needs to happen before possible realloc in SCInspectionBufferCheckAndExpand
+        tmp = build_slice!(input, input_len as usize).to_vec();
+        &tmp
+    } else {
+        build_slice!(input, input_len as usize)
+    };
+
     let output = SCInspectionBufferCheckAndExpand(buffer, ctx.max_size);
     if output.is_null() {
         // allocation failure
         return;
     }
     let buf = std::slice::from_raw_parts_mut(output, ctx.max_size as usize);
-    let mut tmp = Vec::new();
-    let input = if std::ptr::eq(output, input.as_ptr()) {
-        // need a temporary buffer as we cannot do the transfom in place
-        // rather copy the input which should be smaller than the decompressed output
-        // (Transform is tried in place when there are multiple chaines transforms)
-        tmp.extend_from_slice(input);
-        &tmp
-    } else {
-        input
-    };
 
     //  this succeeds if decompressed data > max_size, but we get nb = max_size
-    if let Some(nb) = decompress_fn(input, buf) {
+    if let Some(nb) = decompress_fn(slice_input, buf) {
         SCInspectionBufferTruncate(buffer, nb);
     } else {
         // decompression failure

https://github.com/OISF/suricata/commit/2b20a436e7249e7eb688f0c9caf26ad9179fc680

TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-24 Reported to tracker
  2. 2026-04-29 Sent to maintainer
  3. 2026-05-09 Maintainer acknowledged
  4. 2026-05-17 Patch released
  5. 2026-08-17 Publicly revealed
PROVENANCE

SHA-3-512 hash:

8cce47a2adebd5006d22356386d064c55cf3edb7c252b5356e40193df6336e47e160560b2597f239706d901ab28486a56a3ad0b905fb45297118f87dcdbb9633

Committed 2026-04-29 00:04 PT

Revealed 2026-08-17 14:12 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-61Y3NTY3",
  "bug_class": "Use-after-free",
  "claude_severity": "critical",
  "commit_sha": null,
  "created_at": "2026-03-24T20:43:50+00:00",
  "description": "In Suricata's detection transform pipeline, decompress_transform (decompress.rs:136) calls SCInspectionBufferCheckAndExpand, which reallocs the inspection buffer — freeing the old 4096-byte region. A slice still pointing into that freed region is then passed to gunzip_transform_do (decompress.rs:118), which hands it to flate2's GzDecoder::new. The gzip header parser performs a memcpy from the dangling pointer. Triggering requires a detection rule chaining the to_lowercase and decompress transforms on http.uri, plus HTTP traffic whose URI is large enough to force the realloc.",
  "discovered_at": null,
  "location": "mod.rs:547",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "suricata",
  "reproduction": [
    "1. Operator has a rule using http.uri; to_lowercase; decompress; (or equivalent transform chain)",
    "2. Attacker sends an HTTP request to a monitored host with a URI crafted so the tolower_transform allocates a 4096-byte inspection buffer",
    "3. decompress_transform calls SCInspectionBufferCheckAndExpand which reallocs the buffer to a new address, freeing the old region",
    "4. gunzip_transform_do passes the stale slice to flate2 GzDecoder, which memcpy-reads 10 bytes from freed heap"
  ],
  "technical_details": "ASAN: heap-use-after-free, READ of size 10 at 0x7088ec6ff500. The free and the use occur within the same invocation of decompress_transform: line 136 grows the inspection buffer via realloc (invalidating prior pointers), but the input slice handed to the GzDecoder at line 154→118 was derived before the realloc and is not refreshed. This is the classic realloc-invalidates-borrow pattern crossing the Rust/C FFI boundary, where Rust's borrow checker cannot see the C-side realloc.",
  "title": "Use-after-free in mod.rs:547",
  "vendor_severity": "high"
}