ANT-2026-61Y3NTY3 · oisf/suricata
use-after-free high
Severity Claude critical · Security research firm high · Maintainer -
Discovered by Claude Mythos Preview
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
- Operator has a rule using http.uri; to_lowercase; decompress; (or equivalent transform chain)
- Attacker sends an HTTP request to a monitored host with a URI crafted so the tolower_transform allocates a 4096-byte inspection buffer
- decompress_transform calls SCInspectionBufferCheckAndExpand which reallocs the buffer to a new address, freeing the old region
- 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
Triage and disclosure were performed by Trail of Bits.
- Verdict
- true positive
- Severity
- high
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 failurehttps://github.com/OISF/suricata/commit/2b20a436e7249e7eb688f0c9caf26ad9179fc680
Dates from discovery through public reveal.
- 2026-03-24 Reported to tracker
- 2026-04-29 Sent to maintainer
- 2026-05-09 Maintainer acknowledged
- 2026-05-17 Patch released
- 2026-08-17 Publicly revealed
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"
}