From c4c778035e8645b62dcd9154148d80503f15228b Mon Sep 17 00:00:00 2001 From: Victor Julien Date: Mon, 28 Sep 2026 13:40:40 +0000 Subject: [PATCH] detect: require both halves of a bidirectional rule before matching A transactional ('=>') rule keeps the engines of both directions in a single progress sorted list. While the toserver pass runs it steps over the toclient engines, and the only thing that kept it from declaring a full match was if (direction == 0 && engine->next == NULL) { break; } so it worked only when a toclient engine happened to be the last entry. Engines are sorted by progress across both directions, so a rule whose toserver buffer has the higher progress value ends with a toserver engine: the loop steps over the toclient engine, runs the toserver engine, falls off the end of the list with total_matches set and reports a full match. The toclient condition is then silently ignored, and the rule fires on the request alone. For 'http.host' (p2) and 'http.stat_msg' (p1) the list is http_host/toserver/http2/p1 | http_stat_msg/toclient/http2/p0 | http_stat_msg/toclient/http/p1 | http_host/toserver/http/p2 and a response of "200 OK" still alerts on a content:"ZZZZZZ" in http.stat_msg. Track the skip instead of the list position: remember that an engine of the other direction was stepped over un-inspected and require that to be clear before setting DE_STATE_FLAG_FULL_INSPECT. The decision is now the same for every list layout, and for the layout that already worked (the toclient engine last) it changes nothing beyond 'engine' being NULL rather than non-NULL at the end of the loop: neither sets FULL_INSPECT, and both leave the toserver flags to be stored for the toclient pass, which appends them for both directions (DetectRunStoreStateTx). Ticket: #9087. --- src/detect.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/detect.c b/src/detect.c index 6106d5fdc9..6dbdd86c48 100644 --- a/src/detect.c +++ b/src/detect.c @@ -1316,6 +1316,7 @@ static int DetectRunTxInspectRule(ThreadVars *tv, DetectEngineCtx *de_ctx, uint16_t file_no_match = 0; bool mpm_before_progress = false; // is mpm engine before progress? bool mpm_in_progress = false; // is mpm engine in a buffer we will revisit? + bool other_dir_pending = false; // engine of the other direction left to inspect? TRACE_SID_TXS(s->id, tx, "starting %s", direction ? "toclient" : "toserver"); @@ -1478,10 +1479,12 @@ static int DetectRunTxInspectRule(ThreadVars *tv, DetectEngineCtx *de_ctx, // for transactional rules, the engines on the opposite direction // are ordered by progress on the different side // so we have a two mixed-up lists, and we skip the elements - if (direction == 0 && engine->next == NULL) { - // do not match yet on request only - break; - } + // + // The engine is only inspected in the other direction, so a full + // match must wait for it regardless of where it sits in the list. + // Only reachable while inspecting toserver: in the toclient + // direction the branch above runs the toserver engines as well. + other_dir_pending = true; engine = engine->next; continue; } @@ -1492,7 +1495,7 @@ static int DetectRunTxInspectRule(ThreadVars *tv, DetectEngineCtx *de_ctx, inspect_flags, total_matches, engine); bool full_match = false; - if (engine == NULL && total_matches) { + if (engine == NULL && total_matches && !other_dir_pending) { inspect_flags |= DE_STATE_FLAG_FULL_INSPECT; TRACE_SID_TXS(s->id, tx, "MATCH"); full_match = true;