]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #3588: Add stateful signature evaluation
authorSteve Chew (stechew) <stechew@cisco.com>
Wed, 19 Oct 2022 16:20:12 +0000 (16:20 +0000)
committerSteve Chew (stechew) <stechew@cisco.com>
Wed, 19 Oct 2022 16:20:12 +0000 (16:20 +0000)
Merge in SNORT/snort3 from ~OSHUMEIK/snort3:stateful_signature_evaluation to master

Squashed commit of the following:

commit 8477617f494ffebae8c95ad6456c7ce3b630b34b
Author: Oleksii Shumeiko <oshumeik@cisco.com>
Date:   Mon Apr 18 19:27:53 2022 +0300

    detection: add stateful signature evaluation

    If an IPS option sets the cursor beyond the current buffer size,
    an evaluation state will be stored on the flow.
    Rule evaluation will resume later, when enough data from the buffer become available.

    Key updates/features:
    * buffers supported: pkt_data, file_data, js_data
    * a rule fired on the current packet doesn't create continuations
    * continuations are droppped on config reload
    * a few peg counters added
    * rule variables are transferred to the continuation
    * rule latency supported

    Continuation tracks stream source for the following buffers:
    pkt_data -- TCP payload data with respect to flow direction
    js_data -- JavaScript text combined within the same HTTP request/response
    file_data -- file's data within the same file (context)

    Now a leaf node can have children, which are flowbit setters moved to the very end.

    If an inspector sends PDU with data prepended from previous PDUs,
    Continuations will be dropped, because data chunks cannot be concatenated.
    Currently, http_inspect http2_inspect can present accumulated data
    in file_data and js_data buffers.

46 files changed:
doc/user/concepts.txt
src/detection/detect_trace.h
src/detection/detection_continuation.h [new file with mode: 0644]
src/detection/detection_engine.cc
src/detection/detection_engine.h
src/detection/detection_module.cc
src/detection/detection_options.cc
src/detection/detection_options.h
src/detection/dev_notes.txt
src/detection/fp_create.cc
src/detection/fp_detect.cc
src/detection/fp_utils.cc
src/detection/fp_utils.h
src/detection/ips_context.h
src/flow/flow.cc
src/flow/flow.h
src/framework/base_api.h
src/framework/cursor.cc
src/framework/cursor.h
src/framework/inspector.h
src/ips_options/ips_file_data.cc
src/latency/rule_latency.cc
src/latency/rule_latency.h
src/main/test/distill_verdict_stubs.h
src/mime/file_mime_process.cc
src/service_inspectors/dce_rpc/dce_smb2_file.cc
src/service_inspectors/dce_rpc/dce_smb_utils.cc
src/service_inspectors/ftp_telnet/ftp_data.cc
src/service_inspectors/http_inspect/http_field.cc
src/service_inspectors/http_inspect/http_field.h
src/service_inspectors/http_inspect/http_inspect.cc
src/service_inspectors/http_inspect/http_js_norm.cc
src/service_inspectors/http_inspect/http_msg_body.cc
src/service_inspectors/http_inspect/http_msg_header.cc
src/service_inspectors/http_inspect/ips_http_buffer.cc
src/service_inspectors/imap/imap.cc
src/service_inspectors/smtp/smtp.cc
src/stream/file/file_session.cc
src/utils/grouped_list.h [new file with mode: 0644]
src/utils/js_normalizer.h
src/utils/js_tokenizer.h
src/utils/js_tokenizer.l
src/utils/stats.cc
src/utils/stats.h
src/utils/test/CMakeLists.txt
src/utils/test/grouped_list_test.cc [new file with mode: 0644]

index df3db3dc2a13524c909191f3cb347f0d9b3cd0f9..358b2540f6edbe2e5afca94cbe7a740bb172c8a8 100644 (file)
@@ -289,3 +289,24 @@ be skipped if possible.  Note that this differs from Snort 2 which provided
 the fast_pattern:only option to designate such cases.  This is one less
 thing for the rule writer to worry about.
 
+===== Stateful Evaluation
+
+When data forms a kind of stream, e.g. contiguous byte flow (like a file
+transferred over the network or byte sequence from TCP session packets),
+the point of interest may be in a signature which spans across packets (its
+parts lies in different data blocks). In this case, the stateful evaluation
+becomes handy.
+
+If rule evaluation starts in a packet and the cursor position is moved beyond
+the current packet boundary, then the evaluation gets paused and will resume
+later when more data become available to finish the process.
+
+Stateful evaluation is supported for the following buffers:
+
+ 1. pkt_data -- as a sequence of TCP session bytes with respect to their
+ direction (client-to-server, server-to-client)
+
+ 2. js_data -- normalized JavaScript text from the same data transfer session
+
+ 3. file_data -- the same file bytes, e.g. flows from different files do not
+ overlap
index 4cf7a651043acdc8333d68ac6e2abf4d259f83c7..09979365e07dcc9dd8f824ea1cc6793afab5ed06 100644 (file)
@@ -48,6 +48,7 @@ enum
     TRACE_PKT_DETECTION,
     TRACE_OPTION_TREE,
     TRACE_TAG,
+    TRACE_CONT,
 };
 
 void clear_trace_cursor_info();
diff --git a/src/detection/detection_continuation.h b/src/detection/detection_continuation.h
new file mode 100644 (file)
index 0000000..24d30c5
--- /dev/null
@@ -0,0 +1,308 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2022-2022 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// detection_continuation.h author Yehor Velykozhon <yvelykoz@cisco.com>
+
+#ifndef DETECTION_CONTINUATION_H
+#define DETECTION_CONTINUATION_H
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "ips_options/extract.h"
+#include "latency/rule_latency.h"
+#include "latency/rule_latency_state.h"
+#include "main/thread_config.h"
+#include "protocols/packet.h"
+#include "trace/trace_api.h"
+#include "utils/grouped_list.h"
+#include "utils/stats.h"
+
+#include "detection_options.h"
+#include "detect_trace.h"
+#include "ips_context.h"
+#include "rule_option_types.h"
+
+class Continuation
+{
+public:
+    template <bool opt_parent>
+    static inline void postpone(const Cursor&,
+        const detection_option_tree_node_t&, const detection_option_eval_data_t&);
+
+    static inline void recall(dot_node_state_t&, const snort::Packet*);
+
+    inline bool is_reloaded() const;
+
+    inline void eval(snort::Packet&);
+
+private:
+    Continuation(int max = 1024) : states_cnt(0), states_cnt_max(max),
+        reload_id(snort::SnortConfig::get_thread_reload_id())
+    { }
+
+    template <bool opt_parent>
+    inline void add(const Cursor&,
+        const detection_option_tree_node_t&, const detection_option_eval_data_t&);
+
+    struct State
+    {
+        State() : data(), root(), selector(nullptr), node(nullptr),
+            waypoint(0), sid(0), packet_number(0), opt_parent(false)
+        {
+            for (uint8_t i = 0; i < NUM_IPS_OPTIONS_VARS; ++i)
+                byte_extract_vars[i] = 0;
+        }
+
+        State(const detection_option_tree_node_t& n, const detection_option_eval_data_t& d,
+            snort::IpsOption* s, unsigned wp, uint64_t id, bool p) : data(d),
+            root(1, nullptr, d.otn, new RuleLatencyState[snort::ThreadConfig::get_instance_max()]()),
+            selector(s), node(const_cast<detection_option_tree_node_t*>(&n)),
+            waypoint(wp), sid(id), packet_number(d.p->context->packet_number), opt_parent(p)
+        {
+            for (uint8_t i = 0; i < NUM_IPS_OPTIONS_VARS; ++i)
+                snort::GetVarValueByIndex(&byte_extract_vars[i], i);
+
+            root.children = &node;
+        }
+
+        ~State()
+        { delete[] root.latency_state; }
+
+        inline bool eval(snort::Packet&);
+
+        detection_option_eval_data_t data;
+        detection_option_tree_root_t root;
+        snort::IpsOption* selector;
+        detection_option_tree_node_t* node;
+        unsigned waypoint;
+        uint64_t sid;
+        uint64_t packet_number;
+        uint32_t byte_extract_vars[NUM_IPS_OPTIONS_VARS];
+        bool opt_parent;
+    };
+
+    using LState = snort::GroupedList<State>;
+
+    LState states;
+    unsigned states_cnt;
+    const unsigned states_cnt_max;
+    const unsigned reload_id;
+};
+
+template <bool opt_parent>
+void Continuation::postpone(const Cursor& cursor,
+    const detection_option_tree_node_t& node, const detection_option_eval_data_t& data)
+{
+    if (!cursor.awaiting_data())
+        return;
+
+    if (!data.p->has_paf_payload())
+        return;
+
+    assert(data.p->flow);
+
+    auto cont = data.p->flow->ips_cont;
+
+    if (!cont)
+    {
+        cont = data.p->flow->ips_cont = new Continuation();
+        snort::pc.cont_flows++;
+    }
+
+    cont->add<opt_parent>(cursor, node, data);
+}
+
+void Continuation::recall(dot_node_state_t& nst,
+     const snort::Packet* p)
+{
+    if (nst.last_check.context_num != nst.conts_num)
+        return;
+
+    auto cnt = LState::erase_group((LState*&)nst.conts);
+    assert(nst.conts == nullptr);
+    assert(cnt);
+
+    debug_logf(detection_trace, TRACE_CONT, nullptr,
+        "The path matched, dropping %u continuation(s)\n", cnt);
+
+    assert(p);
+    assert(p->flow);
+    assert(p->flow->ips_cont);
+
+    auto cont = p->flow->ips_cont;
+
+    assert(cnt <= cont->states_cnt);
+    cont->states_cnt -= cnt;
+    snort::pc.cont_recalls += cnt;
+}
+
+bool Continuation::is_reloaded() const
+{
+    return snort::SnortConfig::get_thread_reload_id() != reload_id;
+}
+
+void Continuation::eval(snort::Packet& p)
+{
+    if (!p.has_paf_payload())
+        return;
+
+    debug_logf(detection_trace, TRACE_CONT, nullptr,
+        "Processing %u continuation(s)\n", states_cnt);
+
+    if (states_cnt > snort::pc.cont_max_num)
+        snort::pc.cont_max_num = states_cnt;
+
+    auto i = states.get_next();
+
+    while (i != &states)
+    {
+        auto st = i;
+        i = i->get_next();
+
+        if ((**st).eval(p))
+        {
+            assert(0 < states_cnt);
+            assert(st != &states);
+            --states_cnt;
+            st->leave_group();
+            delete st;
+        }
+    }
+}
+
+bool Continuation::State::eval(snort::Packet& p)
+{
+    Cursor cursor(&p);
+
+    // Create local object to follow latency workflow
+    RuleLatency::Context rule_latency_ctx(root, &p);
+
+    if (RuleLatency::suspended())
+        return true;
+
+    if (packet_number == p.context->packet_number)
+        return false;
+
+    if (selector)
+        selector->eval(cursor, &p);
+
+    if (sid != cursor.id())
+        return false;
+
+    if (cursor.is_buffer_accumulated())
+    {
+        debug_logf(detection_trace, TRACE_CONT, nullptr,
+            "Continuation dropped due to accumulated data in '%s'\n", cursor.get_name());
+
+        return true;
+    }
+
+    cursor.set_pos(waypoint);
+
+    if (cursor.awaiting_data(true) or cursor.size() == 0)
+    {
+        waypoint = cursor.get_next_pos();
+        debug_logf(detection_trace, TRACE_CONT, data.p,
+            "Continuation postponed, %u bytes to go\n", waypoint);
+        return false;
+    }
+
+    assert(cursor.get_name());
+    debug_logf(detection_trace, TRACE_CONT, data.p,
+        "Cursor reached the position, evaluating sub-tree with "
+        "current buffer '%s'\n", cursor.get_name());
+
+    data.pmd = nullptr;
+    data.p = &p;
+
+    int result = 0;
+    snort::pc.cont_evals++;
+
+    for (uint8_t i = 0; i < NUM_IPS_OPTIONS_VARS; ++i)
+        snort::SetVarValueByIndex(byte_extract_vars[i], i);
+
+    const detection_option_tree_node_t* node = root.children[0];
+
+    if (opt_parent)
+    {
+        for (int i = 0; i < node->num_children; ++i)
+            result += detection_option_node_evaluate(node->children[i], data, cursor);
+    }
+    else
+    {
+        result = detection_option_node_evaluate(node, data, cursor);
+    }
+
+    clear_trace_cursor_info();
+
+    if (result)
+        snort::pc.cont_matches++;
+    else
+        snort::pc.cont_mismatches++;
+
+    return true;
+}
+
+template <bool opt_parent>
+void Continuation::add(const Cursor& cursor,
+    const detection_option_tree_node_t& node, const detection_option_eval_data_t& data)
+{
+    auto selector = data.buf_selector;
+    auto pos = cursor.get_next_pos();
+    auto sid = cursor.id();
+    auto nst = node.state + snort::get_instance_id();
+    assert(nst);
+
+    if (nst->last_check.context_num != nst->conts_num)
+    {
+        nst->conts_num = nst->last_check.context_num;
+        nst->conts = nullptr;
+    }
+
+    if (states_cnt < states_cnt_max)
+    {
+        ++states_cnt;
+        new LState(states, (LState*&)nst->conts, node, data, selector, pos, sid, opt_parent);
+    }
+    else
+    {
+        debug_logf(detection_trace, TRACE_CONT, data.p,
+            "Max size reached (%u), rejecting the oldest continuation\n",
+            states_cnt_max);
+
+        auto st = states.get_next();
+        assert(st != &states);
+
+        if ((**st).packet_number == data.p->context->packet_number)
+            st->leave_group();
+        delete st;
+
+        new LState(states, (LState*&)nst->conts, node, data, selector, pos, sid, opt_parent);
+    }
+
+    snort::pc.cont_creations++;
+
+    assert(cursor.get_name());
+    assert(!selector || selector->get_name());
+    debug_logf(detection_trace, TRACE_CONT, data.p,
+        "Adding a continuation: "
+        "current buffer '%s', buffer selector '%s'; %u more bytes needed\n",
+        cursor.get_name(), selector ? selector->get_name() : "N/A", pos);
+}
+
+#endif
index eeea781642ac77570f75f55db3ec90fd99d67b7c..f8c3f70eb41960e798de127e1ff37c825cfd3ef7 100644 (file)
@@ -105,6 +105,7 @@ DetectionEngine::DetectionEngine()
     context = Analyzer::get_switcher()->interrupt();
 
     context->file_data = DataPointer(nullptr, 0);
+    context->file_data_id = 0;
 
     reset();
 }
@@ -299,11 +300,34 @@ DataBuffer& DetectionEngine::get_alt_buffer(Packet* p)
 }
 
 void DetectionEngine::set_file_data(const DataPointer& dp)
-{ Analyzer::get_switcher()->get_context()->file_data = dp; }
+{
+    auto c = Analyzer::get_switcher()->get_context();
+    c->file_data = dp;
+    c->file_data_id = 0;
+    c->file_data_drop_sse = false;
+    c->file_data_no_sse = false;
+}
+
+void DetectionEngine::set_file_data(const DataPointer& dp, uint64_t id, bool is_accum, bool no_flow)
+{
+    auto c = Analyzer::get_switcher()->get_context();
+    c->file_data = dp;
+    c->file_data_id = id;
+    c->file_data_drop_sse = is_accum;
+    c->file_data_no_sse = no_flow;
+}
 
-DataPointer& DetectionEngine::get_file_data(IpsContext* c)
+const DataPointer& DetectionEngine::get_file_data(const IpsContext* c)
 { return c->file_data; }
 
+const DataPointer& DetectionEngine::get_file_data(const IpsContext* c, uint64_t& id, bool& drop_sse, bool& no_sse)
+{
+    id = c->file_data_id;
+    drop_sse = c->file_data_drop_sse;
+    no_sse = c->file_data_no_sse;
+    return c->file_data;
+}
+
 void DetectionEngine::set_data(unsigned id, IpsContextData* p)
 { Analyzer::get_switcher()->get_context()->set_context_data(id, p); }
 
index 5b62a0fb0271f486456dd7acd801a9150fcd6d52..44533aa70e5710471e557f3637fdd28ac05547b8 100644 (file)
@@ -69,7 +69,9 @@ public:
     static Packet* get_encode_packet();
 
     static void set_file_data(const DataPointer& dp);
-    static DataPointer& get_file_data(IpsContext*);
+    static void set_file_data(const DataPointer& dp, uint64_t id, bool is_accum, bool no_flow);
+    static const DataPointer& get_file_data(const IpsContext*);
+    static const DataPointer& get_file_data(const IpsContext*, uint64_t& id, bool& drop_sse, bool& no_sse);
 
     static uint8_t* get_buffer(unsigned& max);
     static struct DataBuffer& get_alt_buffer(Packet*);
@@ -127,6 +129,12 @@ static inline void set_file_data(const uint8_t* p, unsigned n)
     DetectionEngine::set_file_data(dp);
 }
 
+static inline void set_file_data(const uint8_t* p, unsigned n, uint64_t id, bool is_accum = false, bool no_flow = false)
+{
+    DataPointer dp { p, n };
+    DetectionEngine::set_file_data(dp, id, is_accum, no_flow);
+}
+
 static inline void clear_file_data()
 { set_file_data(nullptr, 0); }
 
index 8bf8273e4f469cfb1f82d4dcacfae355f0de66f8..2d5084277d132b682fb2fa4e02aef75f0f77b951 100644 (file)
@@ -49,6 +49,7 @@ static const TraceOption detection_trace_options[] =
     { "pkt_detect",    TRACE_PKT_DETECTION,     "enable packet detection trace logging" },
     { "opt_tree",      TRACE_OPTION_TREE,       "enable tree option trace logging" },
     { "tag",           TRACE_TAG,               "enable tag trace logging" },
+    { "cont",          TRACE_CONT,              "enable rule continuation trace logging" },
     { nullptr, 0, nullptr }
 };
 #endif
index ab2fb627a28bd180bc51ec5d9110ba238569dd62..acc5bed6f6ed3a45e12fbe89244672317e6487b2 100644 (file)
@@ -54,6 +54,7 @@
 #include "utils/util.h"
 #include "utils/util_cstring.h"
 
+#include "detection_continuation.h"
 #include "detection_engine.h"
 #include "detection_module.h"
 #include "detection_util.h"
@@ -348,26 +349,17 @@ void* add_detection_option_tree(SnortConfig* sc, detection_option_tree_node_t* o
 }
 
 int detection_option_node_evaluate(
-    detection_option_tree_node_t* node, detection_option_eval_data_t& eval_data,
+    const detection_option_tree_node_t* node, detection_option_eval_data_t& eval_data,
     const Cursor& orig_cursor)
 {
     assert(node and eval_data.p);
 
+    node_eval_trace(node, orig_cursor, eval_data.p);
+
     auto& state = node->state[get_instance_id()];
     RuleContext profile(state);
 
-    int result = 0;
-    int rval;
-    char tmp_noalert_flag = 0;
-    Cursor cursor = orig_cursor;
-    bool continue_loop = true;
-    bool flowbits_setoperation = false;
-    int loop_count = 0;
-    uint32_t tmp_byte_extract_vars[NUM_IPS_OPTIONS_VARS];
     uint64_t cur_eval_context_num = eval_data.p->context->context_num;
-
-    node_eval_trace(node, cursor, eval_data.p);
-
     auto p = eval_data.p;
 
     // see if evaluated it before ...
@@ -410,6 +402,16 @@ int detection_option_node_evaluate(
             content_last = pmd->last_check + get_instance_id();
     }
 
+    bool continue_loop = true;
+    int loop_count = 0;
+
+    char tmp_noalert_flag = 0;
+    int result = 0;
+    uint32_t tmp_byte_extract_vars[NUM_IPS_OPTIONS_VARS];
+    IpsOption* buf_selector = eval_data.buf_selector;
+    Cursor cursor = orig_cursor;
+    int rval;
+
     // No, haven't evaluated this one before... Check it.
     do
     {
@@ -511,20 +513,20 @@ int detection_option_node_evaluate(
         case RULE_OPTION_TYPE_FLOWBIT:
             if ( node->evaluate )
             {
-                flowbits_setoperation = flowbits_setter(node->option_data);
-
-                if ( flowbits_setoperation )
-                    // set to match so we don't bail early
-                    rval = (int)IpsOption::MATCH;
-
-                else
-                    rval = node->evaluate(node->option_data, cursor, eval_data.p);
+                rval = node->evaluate(node->option_data, cursor, eval_data.p);
+                assert((flowbits_setter(node->option_data) and rval == (int)IpsOption::MATCH)
+                    or !flowbits_setter(node->option_data));
             }
             break;
 
         default:
             if ( node->evaluate )
+            {
+                IpsOption* opt = (IpsOption*)node->option_data;
+                if ( opt->is_buffer_setter() )
+                    buf_selector = opt;
                 rval = node->evaluate(node->option_data, cursor, p);
+            }
             break;
         }
 
@@ -532,6 +534,11 @@ int detection_option_node_evaluate(
         {
             debug_log(detection_trace, TRACE_RULE_EVAL, p, "no match\n");
             state.last_check.result = result;
+            // See if the option has failed due to incomplete input or its failed evaluation
+            if (orig_cursor.awaiting_data())
+                Continuation::postpone<false>(orig_cursor, *node, eval_data);
+            else
+                Continuation::postpone<true>(cursor, *node, eval_data);
             return result;
         }
         else if ( rval == (int)IpsOption::FAILED_BIT )
@@ -644,6 +651,7 @@ int detection_option_node_evaluate(
                             continue;
                     }
 
+                    eval_data.buf_selector = buf_selector;
                     child_state->result = detection_option_node_evaluate(
                         node->children[i], eval_data, cursor);
 
@@ -668,10 +676,14 @@ int detection_option_node_evaluate(
                 // the children so don't need to reeval this content/pcre rule
                 // option at a new offset.
                 // Else, reset the DOE ptr to last eval for offset/depth,
-                // distance/within adjustments for this same content/pcre
-                // rule option
+                // distance/within adjustments for this same content/pcre rule option.
+                // If the node and its sub-tree propagate MATCH back,
+                // then all its continuations are recalled.
                 if ( result == node->num_children )
+                {
                     continue_loop = false;
+                    Continuation::recall(state, p);
+                }
 
                 // Don't need to reset since it's only checked after we've gone
                 // through the loop at least once and the result will have
@@ -706,15 +718,6 @@ int detection_option_node_evaluate(
     }
     while ( continue_loop );
 
-    if ( flowbits_setoperation && result == (int)IpsOption::MATCH )
-    {
-        // Do any setting/clearing/resetting/toggling of flowbits here
-        // given that other rule options matched
-        rval = node->evaluate(node->option_data, cursor, p);
-        if ( rval != (int)IpsOption::MATCH )
-            result = rval;
-    }
-
     if ( eval_data.flowbit_failed )
     {
         // something deeper in the tree failed a flowbit test, we may need to
index 503de9c8c3e2007d30ad1ee521ae8909f3b856c2..556fa2bd78559f13449e3b11c1c6ef4f56105a28 100644 (file)
@@ -40,6 +40,7 @@
 namespace snort
 {
 class HashNode;
+class IpsOption;
 class XHash;
 struct Packet;
 struct SnortConfig;
@@ -61,6 +62,8 @@ struct dot_node_state_t
         char result;
         char flowbit_failed;
     } last_check;
+    void* conts;
+    uint64_t conts_num;
 
     // FIXIT-L perf profiler stuff should be factored of the node state struct
     hr_duration elapsed;
@@ -86,41 +89,64 @@ struct dot_node_state_t
     }
 };
 
-struct detection_option_tree_node_t
+struct detection_option_tree_node_t;
+
+struct detection_option_tree_bud_t
 {
-    eval_func_t evaluate;
+    int relative_children;
+    int num_children;
     detection_option_tree_node_t** children;
+    const struct OptTreeNode* otn;
+
+    detection_option_tree_bud_t()
+        : relative_children(0), num_children(0), children(nullptr), otn(nullptr) {}
+
+    detection_option_tree_bud_t(int num, detection_option_tree_node_t** d_otn, const OptTreeNode* r_otn)
+        : relative_children(0), num_children(num), children(d_otn), otn(r_otn) {}
+};
+
+struct detection_option_tree_node_t : public detection_option_tree_bud_t
+{
+    eval_func_t evaluate;
     void* option_data;
     dot_node_state_t* state;
-    struct OptTreeNode* otn;
     int is_relative;
-    int num_children;
-    int relative_children;
     option_type_t option_type;
 };
 
-struct detection_option_tree_root_t
+struct detection_option_tree_root_t : public detection_option_tree_bud_t
 {
-    int num_children;
-    detection_option_tree_node_t** children;
     RuleLatencyState* latency_state;
 
-    struct OptTreeNode* otn;  // first rule in tree
+    detection_option_tree_root_t()
+        : detection_option_tree_bud_t(), latency_state(nullptr) {}
+
+    detection_option_tree_root_t(int num, detection_option_tree_node_t** d_otn, const OptTreeNode* r_otn,
+        RuleLatencyState* lat)
+        : detection_option_tree_bud_t(num, d_otn, r_otn), latency_state(lat) {}
 };
 
 struct detection_option_eval_data_t
 {
-    detection_option_eval_data_t() = delete;
-
-    detection_option_eval_data_t(snort::Packet* p) :
-        pmd(nullptr), p(p), leaf_reached(0), flowbit_failed(0), flowbit_noalert(0)
-    { }
-
-    void* pmd;
+    const void* pmd;
     snort::Packet* p;
+    snort::IpsOption* buf_selector;
+    const struct OptTreeNode* otn;  // first rule in current processed tree
     char leaf_reached;
     char flowbit_failed;
     char flowbit_noalert;
+
+    detection_option_eval_data_t()
+        : pmd(nullptr), p(nullptr), buf_selector(nullptr), otn(nullptr)
+        , leaf_reached(0), flowbit_failed(0), flowbit_noalert(0) {}
+
+    detection_option_eval_data_t(snort::Packet* packet, const OptTreeNode* otn,
+        const void* match_data = nullptr) : pmd(match_data), p(packet), buf_selector(nullptr)
+        , otn(otn), leaf_reached(0), flowbit_failed(0), flowbit_noalert(0) {}
+
+    detection_option_eval_data_t(const detection_option_eval_data_t& m)
+        : pmd(m.pmd), p(m.p), buf_selector(m.buf_selector), otn(m.otn)
+        , leaf_reached(m.leaf_reached), flowbit_failed(m.flowbit_failed), flowbit_noalert(m.flowbit_noalert) {}
 };
 
 // return existing data or add given and return nullptr
@@ -128,7 +154,7 @@ void* add_detection_option(struct snort::SnortConfig*, option_type_t, void*);
 void* add_detection_option_tree(struct snort::SnortConfig*, detection_option_tree_node_t*);
 
 int detection_option_node_evaluate(
-    detection_option_tree_node_t*, detection_option_eval_data_t&, const class Cursor&);
+    const detection_option_tree_node_t*, detection_option_eval_data_t&, const class Cursor&);
 
 void print_option_tree(detection_option_tree_node_t*, int level);
 void detection_option_tree_update_otn_stats(snort::XHash*);
index aeaba669217d81134760a71832e4f441a59c922e..186f2053c0a6aeb468531720aacd3626ea802baf 100644 (file)
@@ -199,3 +199,67 @@ When packets arrive:
         }
     }
 
+*Stateful Signature Evaluation*
+
+Detection module processes data in block mode. Some features are available to
+extend limits of the block mode: large PDU with accumulated data (rebuilt
+packets), flowbits, and stateful signature evaluation (SSE).
+For data which forms a contiguous flow, a rule evaluation started in one
+packet may end up in subsequent packets.
+
+From the user perspective, rule options are evaluated in left-to-right order,
+doing their calculations, checks, setting cursor position, switching buffers,
+etc. If at some point the cursor position is set beyond the current buffer
+boundary, when the rule's evaluation context is stored on the flow and the
+rule fails for the current packet. Later, when more data arrives, Detection
+module checks if the flow has suspended contexts and resumes their evaluation
+after rule group selection and fast-pattern search are done but before other
+rules evaluated.
+
+The Continuation concept represents a suspended evaluation:
+
+1. IPS options are evaluated in a known order, where they form pairs when
+   the 1st option passes the evaluation context to the next option.
+
+2. The Continuation is merely the context (or a state) with a few things
+   attached. So, it is somewhere between options, rather than in an option.
+
+3. The statement above lets us deal with a single evaluation pass, which is
+   independent of what goes before and after it. (Avoiding multiple children
+   nodes).
+
+4. Evaluation flow goes forward and backwards on this pass. The pass could be
+   attempted several times, depending on presence of retry IPS options.
+
+5. On each attempt, the following could happen:
+
+Forward:
+
+[options="header"]
+|===============================================================================
+| 1st option | 2nd option    | Continuation is ...
+| NO_MATCH   | not evaluated | created and attached to the parent option (if cursor is awaiting data)
+| MATCH      | NO_MATCH      | created and attached to the child option (if original cursor was awaiting data)
+| MATCH      | MATCH         | not created (for this pass, but a subsequent pass can have its own continuation)
+|===============================================================================
+
+At this point, it doesn't matter what happens after in the sub-tree.
+If the 1st and the 2nd options matched, that means a continuation is not
+needed here.
+
+Backward:
+
+[options="header"]
+|===============================================================================
+| option    | children                      | Continuations spawned by the option ...
+| (matched) | no match or partially matched | remain attached to the flow
+| (matched) | all matched                   | are recalled and erased from the flow
+|===============================================================================
+
+So, if some branches in the sub-tree failed to match, then continuations
+created at this node will give another try to these branches (later when more
+data become available). If sub-tree fully matched, then continuations are not
+needed (since the rule has fired) and will be recalled.
+
+Pending continuations from the flow are picked up and updated/evaluated with
+respect to the buffer's source (e.g. flow direction, file context, etc.)
index 16825b8b042f0e3453ffee1c841d36dc7f677acb..128eb8f5dee19930be0b36bf9d7e55ae1f36226d 100644 (file)
@@ -79,7 +79,7 @@ static void print_fp_info(const char*, const OptTreeNode*, const PatternMatchDat
 static OptTreeNode* fixup_tree(
     detection_option_tree_node_t* dot, bool branched, unsigned contents)
 {
-    if ( dot->num_children == 0 )
+    if ( dot->num_children == 0 or dot->option_type == RULE_OPTION_TYPE_LEAF_NODE )
     {
         if ( !branched and contents )
             return (OptTreeNode*)dot->option_data;
@@ -153,28 +153,28 @@ static bool new_sig(int num_children, detection_option_tree_node_t** nodes, OptT
 
 static int otn_create_tree(OptTreeNode* otn, void** existing_tree, Mpse::MpseType mpse_type)
 {
-    detection_option_tree_node_t* node = nullptr, * child;
-    bool need_leaf = false;
-
     if (!existing_tree)
         return -1;
 
     if (!*existing_tree)
         *existing_tree = new_root(otn);
 
-    detection_option_tree_root_t* root = (detection_option_tree_root_t*)*existing_tree;
+    detection_option_tree_root_t* const root = (detection_option_tree_root_t*)*existing_tree;
+    detection_option_tree_bud_t* bud = root;
+    bool need_leaf = false;
 
-    if (!root->children)
+    if (!bud->children)
     {
-        root->num_children++;
-        root->children = (detection_option_tree_node_t**)
-            snort_calloc(root->num_children, sizeof(detection_option_tree_node_t*));
+        bud->num_children++;
+        bud->children = (detection_option_tree_node_t**)
+            snort_calloc(bud->num_children, sizeof(detection_option_tree_node_t*));
         need_leaf = true;
     }
 
     int i = 0;
-    child = root->children[i];
+    detection_option_tree_node_t* child = bud->children[i];
     OptFpList* opt_fp = otn->opt_func;
+    std::list<OptFpList*> fbss;
 
     /* Build out sub-nodes for each option in the OTN fp list */
     while (opt_fp)
@@ -197,61 +197,41 @@ static int otn_create_tree(OptTreeNode* otn, void** existing_tree, Mpse::MpseTyp
             continue;
         }
 
+        // A flowbit setter will be evaluated last.
+        if ( is_flowbit_setter(opt_fp) )
+        {
+            fbss.push_back(opt_fp);
+            opt_fp = opt_fp->next;
+            continue;
+        }
+
         if (!child)
         {
             /* No children at this node */
             child = new_node(opt_fp->type, option_data);
             child->evaluate = opt_fp->OptTestFunc;
 
-            if (!node)
-                root->children[i] = child;
-            else
-                node->children[i] = child;
+            bud->children[i] = child;
 
             child->num_children++;
             child->children = (detection_option_tree_node_t**)
                 snort_calloc(child->num_children, sizeof(detection_option_tree_node_t*));
             child->is_relative = opt_fp->isRelative;
 
-            if (node && child->is_relative)
-                node->relative_children++;
+            if (child->is_relative)
+                bud->relative_children++;
 
             need_leaf = true;
         }
         else
         {
-            bool found_child_match = false;
+            bool found_child_match = child->option_data == option_data;
 
-            if (child->option_data != option_data)
-            {
-                if (!node)
-                {
-                    for (i=1; i<root->num_children; i++)
-                    {
-                        child = root->children[i];
-                        if (child->option_data == option_data)
-                        {
-                            found_child_match = true;
-                            break;
-                        }
-                    }
-                }
-                else
-                {
-                    for (i=1; i<node->num_children; i++)
-                    {
-                        child = node->children[i];
-                        if (child->option_data == option_data)
-                        {
-                            found_child_match = true;
-                            break;
-                        }
-                    }
-                }
-            }
-            else
+            for (i = 1; !found_child_match && i < bud->num_children; i++)
             {
-                found_child_match = true;
+                child = bud->children[i];
+                if (child->option_data == option_data)
+                    found_child_match = true;
             }
 
             if ( !found_child_match )
@@ -265,88 +245,82 @@ static int otn_create_tree(OptTreeNode* otn, void** existing_tree, Mpse::MpseTyp
                     snort_calloc(child->num_children, sizeof(child->children));
                 child->is_relative = opt_fp->isRelative;
 
-                if (!node)
-                {
-                    root->num_children++;
-                    tmp_children = (detection_option_tree_node_t**)
-                        snort_calloc(root->num_children, sizeof(tmp_children));
-                    memcpy(tmp_children, root->children,
-                        sizeof(detection_option_tree_node_t*) * (root->num_children-1));
-
-                    snort_free(root->children);
-                    root->children = tmp_children;
-                    root->children[root->num_children-1] = child;
-                }
-                else
-                {
-                    node->num_children++;
-                    tmp_children = (detection_option_tree_node_t**)
-                        snort_calloc(node->num_children, sizeof(detection_option_tree_node_t*));
-                    memcpy(tmp_children, node->children,
-                        sizeof(detection_option_tree_node_t*) * (node->num_children-1));
-
-                    snort_free(node->children);
-                    node->children = tmp_children;
-                    node->children[node->num_children-1] = child;
-                    if (child->is_relative)
-                        node->relative_children++;
-                }
+                bud->num_children++;
+                tmp_children = (detection_option_tree_node_t**)
+                    snort_calloc(bud->num_children, sizeof(detection_option_tree_node_t*));
+                memcpy(tmp_children, bud->children,
+                       sizeof(detection_option_tree_node_t*) * (bud->num_children-1));
+
+                snort_free(bud->children);
+                bud->children = tmp_children;
+                bud->children[bud->num_children-1] = child;
+
+                if (child->is_relative)
+                    bud->relative_children++;
+
                 need_leaf = true;
             }
         }
-        node = child;
+        bud = child;
         i=0;
-        child = node->children[i];
+        child = bud->children[i];
         opt_fp = opt_fp->next;
     }
 
     // don't add a new leaf node unless we branched higher in the tree or this
     // is a different sig ( eg alert ip ( sid:1; ) vs alert tcp ( sid:2; ) )
     // note: same sig different policy branches at rtn (this is for same policy)
-
-    if ( !need_leaf )
-    {
-        if ( node )
-            need_leaf = new_sig(node->num_children, node->children, otn);
-        else
-            need_leaf = new_sig(root->num_children, root->children, otn);
-    }
-
-    if ( !need_leaf )
+    if ( !need_leaf and !new_sig(bud->num_children, bud->children, otn) )
         return 0;
 
     /* Append a leaf node that has option data of the SigInfo/otn pointer */
     child = new_node(RULE_OPTION_TYPE_LEAF_NODE, otn);
 
-    if (!node)
+    if (bud->children[0])
     {
-        if (root->children[0])
-        {
-            detection_option_tree_node_t** tmp_children;
-            root->num_children++;
-            tmp_children = (detection_option_tree_node_t**)
-                snort_calloc(root->num_children, sizeof(detection_option_tree_node_t*));
-            memcpy(tmp_children, root->children,
-                sizeof(detection_option_tree_node_t*) * (root->num_children-1));
-            snort_free(root->children);
-            root->children = tmp_children;
-        }
-        root->children[root->num_children-1] = child;
+        detection_option_tree_node_t** tmp_children;
+        bud->num_children++;
+        tmp_children = (detection_option_tree_node_t**)
+            snort_calloc(bud->num_children, sizeof(detection_option_tree_node_t*));
+        memcpy(tmp_children, bud->children,
+               sizeof(detection_option_tree_node_t*) * (bud->num_children - 1));
+        snort_free(bud->children);
+        bud->children = tmp_children;
     }
-    else
+    bud->children[bud->num_children - 1] = child;
+
+    // Build after-leaf nodes with flowbit setters.
+    auto fbss_num = fbss.size();
+
+    if (!fbss_num)
+        return 0;
+
+    bud = child;
+    i = 0;
+
+    bud->num_children = fbss_num;
+    bud->children = (detection_option_tree_node_t**)
+        snort_calloc(bud->num_children, sizeof(detection_option_tree_node_t*));
+
+    for (auto fbs : fbss)
     {
-        if (node->children[0])
+        // Only flowbit setters should get here.
+        if (fbs->type == RULE_OPTION_TYPE_LEAF_NODE
+            or is_fast_pattern_only(otn, fbs, mpse_type)
+            or !is_flowbit_setter(fbs) )
         {
-            detection_option_tree_node_t** tmp_children;
-            node->num_children++;
-            tmp_children = (detection_option_tree_node_t**)
-                snort_calloc(node->num_children, sizeof(detection_option_tree_node_t*));
-            memcpy(tmp_children, node->children,
-                sizeof(detection_option_tree_node_t*) * (node->num_children-1));
-            snort_free(node->children);
-            node->children = tmp_children;
+            assert(false);
+            bud->num_children--;
+            continue;
         }
-        node->children[node->num_children-1] = child;
+
+        void* option_data = fbs->ips_opt;
+        child = new_node(fbs->type, option_data);
+        child->evaluate = fbs->OptTestFunc;
+        child->is_relative = fbs->isRelative;
+        bud->children[i++] = child;
+
+        assert(child->is_relative == false);
     }
 
     return 0;
index b3c0e0493c26bd40ac5f59f17fbbefc7d42659df..eb9a5a88a28b5b3181aa3be74beff93cb592e756 100644 (file)
 #include "context_switcher.h"
 #include "detect.h"
 #include "detect_trace.h"
-#include "detection_util.h"
+#include "detection_continuation.h"
 #include "detection_engine.h"
 #include "detection_module.h"
 #include "detection_options.h"
+#include "detection_util.h"
 #include "fp_config.h"
 #include "fp_create.h"
 #include "fp_utils.h"
@@ -341,7 +342,7 @@ static void detection_option_tree_evaluate(detection_option_tree_root_t* root,
 {
     assert(root);
 
-    RuleLatency::Context rule_latency_ctx(root, eval_data.p);
+    RuleLatency::Context rule_latency_ctx(*root, eval_data.p);
 
     if ( RuleLatency::suspended() )
         return;
@@ -361,11 +362,9 @@ static void rule_tree_match(
     IpsContext* context, void* user, void* tree, int index, void* neg_list)
 {
     PMX* pmx = (PMX*)user;
+    Packet* pkt = context->packet;
 
-    detection_option_eval_data_t eval_data(context->packet);
-    eval_data.pmd = pmx->pmd;
-
-    print_pattern(pmx->pmd, eval_data.p);
+    print_pattern(pmx->pmd, pkt);
 
     {
         /* NOTE: The otn will be the first one in the match state. If there are
@@ -381,17 +380,18 @@ static void rule_tree_match(
             PmdLastCheck* last_check =
                 neg_pmx->pmd->last_check + get_instance_id();
 
-            last_check->ts.tv_sec = eval_data.p->pkth->ts.tv_sec;
-            last_check->ts.tv_usec = eval_data.p->pkth->ts.tv_usec;
+            last_check->ts.tv_sec = pkt->pkth->ts.tv_sec;
+            last_check->ts.tv_usec = pkt->pkth->ts.tv_usec;
             last_check->run_num = get_run_num();
             last_check->context_num = context->context_num;
-            last_check->rebuild_flag = (eval_data.p->packet_flags & PKT_REBUILT_STREAM);
+            last_check->rebuild_flag = (pkt->packet_flags & PKT_REBUILT_STREAM);
         }
 
         if ( !tree )
             return;
 
         detection_option_tree_root_t* root = (detection_option_tree_root_t*)tree;
+        detection_option_eval_data_t eval_data(pkt, root->otn, pmx->pmd);
 
         detection_option_tree_evaluate(root, eval_data);
 
@@ -399,47 +399,47 @@ static void rule_tree_match(
             pmqs.qualified_events++;
         else
             pmqs.non_qualified_events++;
-    }
 
-    if (eval_data.flowbit_failed)
-        return;
+        if (eval_data.flowbit_failed)
+            return;
+    }
 
     /* If this is for an IP rule set, evaluate the rules from
      * the inner IP offset as well */
-    if (eval_data.p->packet_flags & PKT_IP_RULE)
+    if (pkt->packet_flags & PKT_IP_RULE)
     {
-        ip::IpApi tmp_api = eval_data.p->ptrs.ip_api;
-        int8_t curr_layer = eval_data.p->num_layers - 1;
+        ip::IpApi tmp_api = pkt->ptrs.ip_api;
+        int8_t curr_layer = pkt->num_layers - 1;
 
-        if (layer::set_inner_ip_api(eval_data.p,
-            eval_data.p->ptrs.ip_api,
+        if (layer::set_inner_ip_api(pkt,
+            pkt->ptrs.ip_api,
             curr_layer) &&
-            (eval_data.p->ptrs.ip_api != tmp_api))
+            (pkt->ptrs.ip_api != tmp_api))
         {
-            const uint8_t* tmp_data = eval_data.p->data;
-            uint16_t tmp_dsize = eval_data.p->dsize;
+            const uint8_t* tmp_data = pkt->data;
+            uint16_t tmp_dsize = pkt->dsize;
 
             /* clear so we don't keep recursing */
-            eval_data.p->packet_flags &= ~PKT_IP_RULE;
-            eval_data.p->packet_flags |= PKT_IP_RULE_2ND;
+            pkt->packet_flags &= ~PKT_IP_RULE;
+            pkt->packet_flags |= PKT_IP_RULE_2ND;
 
             do
             {
-                eval_data.p->data = eval_data.p->ptrs.ip_api.ip_data();
-                eval_data.p->dsize = eval_data.p->ptrs.ip_api.pay_len();
+                pkt->data = pkt->ptrs.ip_api.ip_data();
+                pkt->dsize = pkt->ptrs.ip_api.pay_len();
 
                 /* Recurse, and evaluate with the inner IP */
                 rule_tree_match(context, user, tree, index, nullptr);
             }
-            while (layer::set_inner_ip_api(eval_data.p,
-                eval_data.p->ptrs.ip_api, curr_layer) && (eval_data.p->ptrs.ip_api != tmp_api));
+            while (layer::set_inner_ip_api(pkt,
+                pkt->ptrs.ip_api, curr_layer) && (pkt->ptrs.ip_api != tmp_api));
 
             /*  cleanup restore original data & dsize */
-            eval_data.p->packet_flags &= ~PKT_IP_RULE_2ND;
-            eval_data.p->packet_flags |= PKT_IP_RULE;
+            pkt->packet_flags &= ~PKT_IP_RULE_2ND;
+            pkt->packet_flags |= PKT_IP_RULE;
 
-            eval_data.p->data = tmp_data;
-            eval_data.p->dsize = tmp_dsize;
+            pkt->data = tmp_data;
+            pkt->dsize = tmp_dsize;
         }
     }
 }
@@ -1022,13 +1022,12 @@ static inline void eval_nfp(
             if ( fp->get_debug_print_nc_rules() )
                 LogMessage("NC-testing %u rules\n", port_group->nfp_rule_count);
 
-            detection_option_eval_data_t eval_data(p);
+            detection_option_tree_root_t* root = (detection_option_tree_root_t*)port_group->nfp_tree;
+            detection_option_eval_data_t eval_data(p, root->otn);
 
             debug_log(detection_trace, TRACE_RULE_EVAL, p,
                 "Testing non-content rules\n");
 
-            detection_option_tree_root_t* root = (detection_option_tree_root_t*)port_group->nfp_tree;
-
             detection_option_tree_evaluate(root, eval_data);
 
             if (eval_data.leaf_reached)
@@ -1300,6 +1299,18 @@ void fp_complete(Packet* p, bool search)
     }
     {
         Profile rule_profile(rulePerfStats);
+
+        if (p->flow && p->flow->ips_cont)
+        {
+            if (!p->flow->ips_cont->is_reloaded())
+                p->flow->ips_cont->eval(*p);
+            else
+            {
+                delete p->flow->ips_cont;
+                p->flow->ips_cont = nullptr;
+            }
+        }
+
         stash->process(c);
         print_pkt_info(p, "non-fast-patterns");
         fpEvalPacket(p, FPTask::NON_FP);
index a945239182a59b786d5e3a8952de0bf26f85ee3e..9ec2644eefa048b53c15e369d0c5b7e983d5eece 100644 (file)
@@ -36,6 +36,7 @@
 #include "framework/mpse.h"
 #include "framework/mpse_batch.h"
 #include "hash/ghash.h"
+#include "ips_options/ips_flowbits.h"
 #include "log/messages.h"
 #include "main/snort_config.h"
 #include "parser/parse_conf.h"
@@ -650,6 +651,12 @@ bool is_fast_pattern_only(const OptTreeNode* otn, const OptFpList* ofp, Mpse::Mp
     return false;
 }
 
+bool is_flowbit_setter(const OptFpList* ofp)
+{
+    return ofp->type == RULE_OPTION_TYPE_FLOWBIT
+        and flowbits_setter(ofp->ips_opt);
+}
+
 //--------------------------------------------------------------------------
 // mpse compile threads
 //--------------------------------------------------------------------------
index f2d8700461fff25d9a7c216ede520f94fcd43a8f..2d902a1a596a9921fd7ff88a843e8fce43ba9b1e 100644 (file)
@@ -37,6 +37,7 @@ struct PatternMatchData* get_pmd(OptFpList*, SnortProtocolId, snort::RuleDirecti
 
 bool make_fast_pattern_only(const OptFpList*, const PatternMatchData*);
 bool is_fast_pattern_only(const OptTreeNode*, const OptFpList*, snort::Mpse::MpseType);
+bool is_flowbit_setter(const OptFpList*);
 
 PatternMatcher::Type get_pm_type(const std::string& buf);
 
index 937482e4e6e58ac90ef7323c63186a35c51e4ad3..8d35f89a46272f32a6b795fe4ea2cca41ccc4cf3 100644 (file)
@@ -156,6 +156,9 @@ public:
     SF_EVENTQ* equeue;
 
     DataPointer file_data = DataPointer(nullptr, 0);
+    uint64_t file_data_id = 0;
+    bool file_data_drop_sse = false;
+    bool file_data_no_sse = false;
     DataBuffer alt_data = {};
     unsigned file_pos = 0;
     bool file_type_process = false;
index 3e5342ae501e7219c406121d0c7410fbea348d68..1e27dacb00d7b5c24e0ca4d03fe6adc197fe0e03 100644 (file)
@@ -24,6 +24,7 @@
 #include "flow.h"
 
 #include "detection/context_switcher.h"
+#include "detection/detection_continuation.h"
 #include "detection/detection_engine.h"
 #include "flow/flow_key.h"
 #include "flow/ha.h"
@@ -121,6 +122,9 @@ void Flow::term()
         stash = nullptr;
     }
 
+    delete ips_cont;
+    ips_cont = nullptr;
+
     service = nullptr;
 }
 
@@ -219,6 +223,9 @@ void Flow::reset(bool do_cleanup)
 
     deferred_trust.clear();
 
+    delete ips_cont;
+    ips_cont = nullptr;
+
     constexpr size_t offset = offsetof(Flow, context_chain);
     // FIXIT-L need a struct to zero here to make future proof
     memset((uint8_t*)this+offset, 0, sizeof(Flow)-offset);
index f057c27ee45011d69aa42a99be37f222c5c4abac..578aead7c0960b5cbeb772263dad90b00871d90f 100644 (file)
 #define STREAM_STATE_BLOCK_PENDING     0x0400
 #define STREAM_STATE_RELEASING         0x0800
 
+class Continuation;
 class BitOp;
 class Session;
 
@@ -427,6 +428,7 @@ public:  // FIXIT-M privatize if possible
     Session* session;
     Inspector* ssn_client;
     Inspector* ssn_server;
+    Continuation* ips_cont;
 
     long last_data_seen;
     Layer mpls_client, mpls_server;
index 1345d9de77bad90d93306a0990d451b5d01077fa..9a22b5e7754e249f3f202fac6c493ad8c85b1c69 100644 (file)
@@ -29,7 +29,7 @@
 
 // this is the current version of the base api
 // must be prefixed to subtype version
-#define BASE_API_VERSION 15
+#define BASE_API_VERSION 16
 
 // set options to API_OPTIONS to ensure compatibility
 #ifndef API_OPTIONS
index 9f3e3591c4b951b4705b24322210c139475a0e98..06d47d3d998e236494cd5263f1c2410364792ff5 100644 (file)
@@ -43,9 +43,12 @@ Cursor::Cursor(const Cursor& rhs)
 {
     name = rhs.name;
     buf = rhs.buf;
-    sz = rhs.sz;
-    pos = rhs.pos;
     file_pos = rhs.file_pos;
+    buf_size = rhs.buf_size;
+    current_pos = rhs.current_pos;
+    extensible = rhs.extensible;
+    buf_id = rhs.buf_id;
+    is_accumulated = rhs.is_accumulated;
 
     if (rhs.data)
     {
@@ -107,6 +110,130 @@ void Cursor::reset(Packet* p)
             return;
         }
     }
-    set("pkt_data", p->data, p->get_detect_limit());
+
+    set("pkt_data", p->packet_flags & (PKT_FROM_SERVER | PKT_FROM_CLIENT),
+        p->data, p->get_detect_limit(), true);
 }
 
+//-------------------------------------------------------------------------
+// UNIT TESTS
+//-------------------------------------------------------------------------
+#ifdef UNIT_TEST
+
+#include "catch/snort_catch.h"
+
+TEST_CASE("Boundaries", "[cursor]")
+{
+    const uint8_t buf_1[] = "the first";
+    const uint8_t buf_2[] = "the second";
+    const uint8_t buf_3[] = "the third";
+
+    Cursor cursor;
+
+    SECTION("Stateless buffer")
+    {
+        const int offset = 11;
+        bool r1, r2;
+
+        cursor.set("1", buf_1, sizeof(buf_1), false);
+        r1 = cursor.set_pos(offset);
+        r2 = cursor.awaiting_data();
+
+        CHECK(!r1);
+        CHECK(!r2);
+    }
+
+    SECTION("Ends within 1st PDU")
+    {
+        const int offset = 8;
+        bool r1, r2;
+
+        cursor.set("1", buf_1, sizeof(buf_1), true);
+        r1 = cursor.set_pos(offset);
+        r2 = cursor.awaiting_data();
+
+        CHECK(r1);
+        CHECK(!r2);
+    }
+
+    SECTION("At the very end of 1st PDU")
+    {
+        const int offset = sizeof(buf_1);
+        bool r1, r2;
+
+        cursor.set("1", buf_1, sizeof(buf_1), true);
+        r1 = cursor.set_pos(offset);
+        r2 = cursor.awaiting_data();
+
+        CHECK(r1);
+        CHECK(r2);
+
+        auto rem = cursor.get_next_pos();
+        CHECK(rem == 0);
+
+        cursor.set("2", buf_2, sizeof(buf_2), true);
+        r1 = cursor.set_pos(rem);
+        r2 = cursor.awaiting_data();
+
+        CHECK(r1);
+        CHECK(!r2);
+    }
+
+    SECTION("Ends after 1st PDU")
+    {
+        const int offset = 11;
+        bool r1, r2;
+
+        cursor.set("1", buf_1, sizeof(buf_1), true);
+        r1 = cursor.set_pos(offset);
+        r2 = cursor.awaiting_data();
+
+        CHECK(!r1);
+        CHECK(r2);
+
+        auto rem = cursor.get_next_pos();
+        CHECK(rem == offset - sizeof(buf_1));
+
+        cursor.set("2", buf_2, sizeof(buf_2), true);
+        r1 = cursor.set_pos(rem);
+        r2 = cursor.awaiting_data();
+
+        CHECK(r1);
+        CHECK(!r2);
+    }
+
+    SECTION("Ends after 2nd PDU")
+    {
+        const int offset = 25;
+        bool r1, r2;
+
+        cursor.set("1", buf_1, sizeof(buf_1), true);
+        r1 = cursor.set_pos(offset);
+        r2 = cursor.awaiting_data();
+
+        CHECK(!r1);
+        CHECK(r2);
+
+        auto rem1 = cursor.get_next_pos();
+        CHECK(rem1 == offset - sizeof(buf_1));
+
+        cursor.set("2", buf_2, sizeof(buf_2), true);
+        r1 = cursor.set_pos(rem1);
+        r2 = cursor.awaiting_data();
+
+        CHECK(!r1);
+        CHECK(r2);
+
+        auto rem2 = cursor.get_next_pos();
+        CHECK(rem2 == rem1 - sizeof(buf_2));
+
+        cursor.set("3", buf_3, sizeof(buf_3), true);
+        r1 = cursor.set_pos(rem2);
+        r2 = cursor.awaiting_data();
+
+        CHECK(r1);
+        CHECK(!r2);
+    }
+}
+
+#endif
index 4954f0bd8ba9e573be0077a5c9c1c64a1c7f6b2d..1a38997bae40fd47e3d26bc4cd44b2fdbc366b0c 100644 (file)
@@ -25,6 +25,7 @@
 // Cursor provides a formal way of using buffers when doing detection with
 // IpsOptions.
 
+#include <assert.h>
 #include <cstdint>
 #include <cstring>
 #include <vector>
@@ -82,34 +83,55 @@ public:
 
     void reset(snort::Packet*);
 
-    void set(const char* s, const uint8_t* b, unsigned n)
-    { name = s; buf = b; sz = n; pos = delta = 0; }
+    void set(const char* s, const uint8_t* b, unsigned n, bool ext = false)
+    {
+        name = s; buf = b; buf_size = n; current_pos = delta = 0;
+        extensible = ext and n > 0;
+        buf_id = 0;
+    }
 
-    void set(const char* s, const uint8_t* b, unsigned n, unsigned pos_file)
+    void set(const char* s, const uint8_t* b, unsigned n, unsigned pos_file, bool ext = false)
     {
         file_pos = pos_file;
-        name = s; buf = b; sz = n; pos = delta = 0;
+        name = s; buf = b; buf_size = n; current_pos = delta = 0;
+        extensible = ext and n > 0;
+        buf_id = 0;
+    }
+
+    void set(const char* s, uint64_t id, const uint8_t* b, unsigned n, bool ext = false)
+    {
+        set(s, b, n, ext);
+        buf_id = id;
+    }
+
+    void set(const char* s, uint64_t id, const uint8_t* b, unsigned n, unsigned pos_file, bool ext = false)
+    {
+        set(s, b, n, pos_file, ext);
+        buf_id = id;
     }
 
+    uint64_t id() const
+    { return buf_id; }
+
     const uint8_t* buffer() const
     { return buf; }
 
     unsigned size() const
-    { return sz; }
+    { return buf_size; }
 
     // the NEXT octect after last in buffer
     // (this pointer is out of bounds)
     const uint8_t* endo() const
-    { return buf + sz; }
+    { return buf + buf_size; }
 
     const uint8_t* start() const
-    { return buf + pos; }
+    { return buf + current_pos; }
 
     unsigned length() const
-    { return sz - pos; }
+    { return buf_size - current_pos; }
 
     unsigned get_pos() const
-    { return pos; }
+    { return current_pos; }
 
     unsigned get_delta() const
     { return delta; }
@@ -118,19 +140,15 @@ public:
 
     bool add_pos(unsigned n)
     {
-        if (pos + n > sz)
-            return false;
-        pos += n;
-        return true;
+        current_pos += n;
+        return !(current_pos > buf_size);
     }
 
-    // pos and delta may go 1 byte after end
+    // current_pos and delta may go 1 byte after end
     bool set_pos(unsigned n)
     {
-        if (n > sz)
-            return false;
-        pos = n;
-        return true;
+        current_pos = n;
+        return !(current_pos > buf_size);
     }
 
     bool set_pos_file(unsigned n)
@@ -139,14 +157,25 @@ public:
         return true;
     }
 
+    bool set_accumulation(bool is_accum)
+    {
+        is_accumulated = is_accum;
+        return true;
+    }
+
     unsigned get_file_pos() const
     {
         return file_pos;
     }
 
+    bool is_buffer_accumulated() const
+    {
+        return is_accumulated;
+    }
+
     bool set_delta(unsigned n)
     {
-        if (n > sz)
+        if (n > buf_size)
             return false;
         delta = n;
         return true;
@@ -154,16 +183,31 @@ public:
 
     void set_data(CursorData* cd);
 
+    bool awaiting_data() const
+    { return extensible and current_pos >= buf_size; }
+
+    bool awaiting_data(bool force_ext) const
+    { return force_ext and current_pos >= buf_size; }
+
+    unsigned get_next_pos() const
+    {
+        assert(current_pos >= buf_size);
+        return current_pos - buf_size;
+    }
+
     typedef std::vector<CursorData*> CursorDataVec;
 
 private:
-    const char* name = nullptr;    // rule option name ("pkt_data", "http_uri", etc.)
-    const uint8_t* buf = nullptr;  // start of buffer
-    unsigned sz = 0;               // size of buffer
-    unsigned pos = 0;              // current pos
+    unsigned buf_size = 0;
+    unsigned current_pos = 0;
     unsigned delta = 0;            // loop offset
     unsigned file_pos = 0;         // file pos
+    const uint8_t* buf = nullptr;
+    const char* name = nullptr;    // rule option name ("pkt_data", "http_uri", etc.)
     CursorDataVec* data = nullptr; // data stored on the cursor
+    bool extensible = false;       // if the buffer could have more data in a continuation
+    uint64_t buf_id = 0;           // source buffer ID
+    bool is_accumulated = false;
 };
 
 #endif
index 378993de6447c902598d9b7c4bcc162ab7a93fbc..bcd25be5e73452cac41fca555c9fb03e904573e1 100644 (file)
@@ -57,6 +57,7 @@ struct InspectionBuffer
     };
     const uint8_t* data;
     unsigned len;
+    bool is_accumulated = false;
 };
 
 struct InspectApi;
index 7e2ce3219c61aff86c9e1dc7c357cb7208241d83..57ddaab06187bfe1858b3d06d5edd99fc3dd15ef 100644 (file)
@@ -56,12 +56,17 @@ IpsOption::EvalStatus FileDataOption::eval(Cursor& c, Packet* p)
 {
     RuleProfile profile(fileDataPerfStats);
 
-    DataPointer dp = DetectionEngine::get_file_data(p->context);
+    uint64_t sid;
+    bool drop_sse;
+    bool no_sse;
+    DataPointer dp = DetectionEngine::get_file_data(p->context, sid, drop_sse, no_sse);
 
     if ( !dp.data || !dp.len )
         return NO_MATCH;
-    c.set(s_name, dp.data, dp.len);
+
+    c.set(s_name, sid, dp.data, dp.len, !no_sse);
     c.set_pos_file(p->context->file_pos);
+    c.set_accumulation(drop_sse);
 
     return MATCH;
 }
index 224e27f74667bd33bd14c70f5482138e96866c7f..36c91e6ec926512f7ef193173b31b27c70138e29 100644 (file)
@@ -63,7 +63,7 @@ struct Event
 
     Type type;
     typename SnortClock::duration elapsed;
-    detection_option_tree_root_t* root;
+    const detection_option_tree_root_t& root;
     Packet* packet;
 };
 
@@ -71,10 +71,10 @@ template<typename Clock>
 class RuleTimer : public LatencyTimer<Clock>
 {
 public:
-    RuleTimer(typename Clock::duration d, detection_option_tree_root_t* root, Packet* p) :
+    RuleTimer(typename Clock::duration d, const detection_option_tree_root_t& root, Packet* p) :
         LatencyTimer<Clock>(d), root(root), packet(p) { }
 
-    detection_option_tree_root_t* root;
+    const detection_option_tree_root_t& root;
     Packet* packet;
 };
 
@@ -104,11 +104,11 @@ static inline std::ostream& operator<<(std::ostream& os, const Event& e)
     }
 
     os << clock_usecs(TO_USECS(e.elapsed)) << " usec, ";
-    os << e.root->otn->sigInfo.gid << ":" << e.root->otn->sigInfo.sid << ":"
-        << e.root->otn->sigInfo.rev;
+    os << e.root.otn->sigInfo.gid << ":" << e.root.otn->sigInfo.sid << ":"
+        << e.root.otn->sigInfo.rev;
 
-    if ( e.root->num_children > 1 )
-        os << " (of " << e.root->num_children << ")";
+    if ( e.root.num_children > 1 )
+        os << " (of " << e.root.num_children << ")";
 
     if ( e.packet->has_ip() or e.packet->is_data() )
     {
@@ -142,7 +142,7 @@ struct DefaultRuleInterface
 
     // return true if rule was *reenabled*
     template<typename Duration, typename Time>
-    static bool reenable(detection_option_tree_root_t& root, Duration max_suspend_time,
+    static bool reenable(const detection_option_tree_root_t& root, Duration max_suspend_time,
         Time cur_time)
     {
         auto& state = root.latency_state[get_instance_id()];
@@ -156,7 +156,7 @@ struct DefaultRuleInterface
     }
 
     template<typename Time>
-    static bool timeout_and_suspend(detection_option_tree_root_t& root, unsigned threshold,
+    static bool timeout_and_suspend(const detection_option_tree_root_t& root, unsigned threshold,
         Time time, bool do_suspend)
     {
         auto& state = root.latency_state[get_instance_id()];
@@ -202,7 +202,7 @@ class Impl
 public:
     Impl(const ConfigWrapper&, EventHandler&);
 
-    bool push(detection_option_tree_root_t*, Packet*);
+    bool push(const detection_option_tree_root_t&, Packet*);
     bool pop();
     bool suspended() const;
 
@@ -218,16 +218,16 @@ inline Impl<Clock, RuleTree>::Impl(const ConfigWrapper& cfg, EventHandler& eh) :
 { }
 
 template<typename Clock, typename RuleTree>
-inline bool Impl<Clock, RuleTree>::push(detection_option_tree_root_t* root, Packet* p)
+inline bool Impl<Clock, RuleTree>::push(const detection_option_tree_root_t& root, Packet* p)
 {
-    assert(root and p);
+    assert(p);
 
     // FIXIT-L rule timer is pushed even if rule is not enabled (no visible side-effects)
     timers.emplace_back(config->max_time, root, p);
 
     if ( config->allow_reenable() )
     {
-        if ( RuleTree::reenable(*root, config->max_suspend_time, Clock::now()) )
+        if ( RuleTree::reenable(root, config->max_suspend_time, Clock::now()) )
         {
             Event e { Event::EVENT_ENABLED, config->max_suspend_time, root, p };
             event_handler.handle(e);
@@ -246,7 +246,7 @@ inline bool Impl<Clock, RuleTree>::pop()
 
     bool timed_out = false;
 
-    if ( !RuleTree::is_suspended(*timer.root) )
+    if ( !RuleTree::is_suspended(timer.root) )
     {
         timed_out = timer.timed_out();
 #ifdef REG_TEST
@@ -254,7 +254,7 @@ inline bool Impl<Clock, RuleTree>::pop()
 #endif
         if ( timed_out )
         {
-            auto suspended = RuleTree::timeout_and_suspend(*timer.root, config->suspend_threshold,
+            auto suspended = RuleTree::timeout_and_suspend(timer.root, config->suspend_threshold,
                 Clock::now(), config->suspend);
 
             Event e
@@ -278,7 +278,7 @@ inline bool Impl<Clock, RuleTree>::suspended() const
         return false;
 
     assert(!timers.empty());
-    return RuleTree::is_suspended(*timers.back().root);
+    return RuleTree::is_suspended(timers.back().root);
 }
 
 // -----------------------------------------------------------------------------
@@ -335,7 +335,7 @@ static inline Impl<>& get_impl()
 // rule latency interface
 // -----------------------------------------------------------------------------
 
-void RuleLatency::push(detection_option_tree_root_t* root, Packet* p)
+void RuleLatency::push(const detection_option_tree_root_t& root, Packet* p)
 {
     if ( rule_latency::config->enabled() )
     {
@@ -437,11 +437,11 @@ struct RuleInterfaceSpy
     { is_suspended_called = true; return is_suspended_result; }
 
     template<typename Duration, typename Time>
-    static bool reenable(detection_option_tree_root_t&, Duration, Time)
+    static bool reenable(const detection_option_tree_root_t&, Duration, Time)
     { reenable_called = true; return reenable_result; }
 
     template<typename Time>
-    static bool timeout_and_suspend(detection_option_tree_root_t&, unsigned, Time, bool)
+    static bool timeout_and_suspend(const detection_option_tree_root_t&, unsigned, Time, bool)
     { timeout_and_suspend_called = true; return timeout_and_suspend_result; }
 };
 
@@ -477,7 +477,7 @@ TEST_CASE ( "rule latency impl", "[latency]" )
 
             SECTION( "push rule" )
             {
-                CHECK_FALSE( impl.push(&root, &pkt) );
+                CHECK_FALSE( impl.push(root, &pkt) );
                 CHECK( event_handler.count == 0 );
                 CHECK( RuleInterfaceSpy::reenable_called );
             }
@@ -486,7 +486,7 @@ TEST_CASE ( "rule latency impl", "[latency]" )
             {
                 RuleInterfaceSpy::reenable_result = true;
 
-                CHECK( impl.push(&root, &pkt) );
+                CHECK( impl.push(root, &pkt) );
                 CHECK( event_handler.count == 1 );
                 CHECK( RuleInterfaceSpy::reenable_called );
             }
@@ -498,7 +498,7 @@ TEST_CASE ( "rule latency impl", "[latency]" )
 
             SECTION( "push rule" )
             {
-                CHECK_FALSE( impl.push(&root, &pkt) );
+                CHECK_FALSE( impl.push(root, &pkt) );
                 CHECK( event_handler.count == 0 );
                 CHECK_FALSE( RuleInterfaceSpy::reenable_called );
             }
@@ -509,7 +509,7 @@ TEST_CASE ( "rule latency impl", "[latency]" )
     {
         RuleInterfaceSpy::is_suspended_result = true;
 
-        impl.push(&root, &pkt);
+        impl.push(root, &pkt);
 
         SECTION( "suspending of rules disabled" )
         {
@@ -532,7 +532,7 @@ TEST_CASE ( "rule latency impl", "[latency]" )
     {
         config.config.max_time = 1_ticks;
 
-        impl.push(&root, &pkt);
+        impl.push(root, &pkt);
 
         SECTION( "rule timeout" )
         {
index 464b0e1b05c0f3c69601f1aa9d1fdd6d25ef1662..0aad0b0effb9c9f86b65e20db363d96896c982e0 100644 (file)
@@ -30,7 +30,7 @@ struct Packet;
 class RuleLatency
 {
 public:
-    static void push(detection_option_tree_root_t*, snort::Packet*);
+    static void push(const detection_option_tree_root_t&, snort::Packet*);
     static void pop();
     static bool suspended();
 
@@ -39,7 +39,7 @@ public:
     class Context
     {
     public:
-        Context(detection_option_tree_root_t* root, snort::Packet* p)
+        Context(const detection_option_tree_root_t& root, snort::Packet* p)
         { RuleLatency::push(root, p); }
 
         ~Context()
index 9870ff8ba1d415afa3616d7c905dcd7e2e73ea49..91a7423800589eb7cfb3bf3425dbf51f24b98780 100644 (file)
@@ -168,6 +168,7 @@ void DetectionEngine::idle() { }
 void DetectionEngine::reset() { }
 void DetectionEngine::wait_for_context() { }
 void DetectionEngine::set_file_data(const DataPointer&) { }
+void DetectionEngine::set_file_data(const DataPointer&, uint64_t, bool, bool) { }
 void DetectionEngine::clear_replacement() { }
 void DetectionEngine::disable_all(Packet*) { }
 unsigned get_instance_id() { return 0; }
index 04f9c99b4191808c6e783be81dbd29133dd5bd0d..141b671fc133221368f05483102a6f7634e5bad5 100644 (file)
@@ -582,7 +582,11 @@ const uint8_t* MimeSession::process_mime_data_paf(
                     if ( result != DECODE_SUCCESS )
                         decompress_alert();
 
-                    set_file_data(decomp_buffer, decomp_buf_size);
+                    if (session_base_file_id)
+                        set_file_data(decomp_buffer, decomp_buf_size, get_multiprocessing_file_id());
+                    else
+                        set_file_data(decomp_buffer, decomp_buf_size, file_counter);
+
                     attachment.data = decomp_buffer;
                     attachment.length = decomp_buf_size;
                     attachment.finished = isFileEnd(position);
@@ -885,14 +889,15 @@ void MimeSession::mime_file_process(Packet* p, const uint8_t* data, int data_siz
 {
     Flow* flow = p->flow;
     FileFlows* file_flows = FileFlows::get_file_flows(flow);
-    if(!file_flows)
+
+    if (!file_flows)
         return;
 
     if (continue_inspecting_file)
     {
         if (session_base_file_id)
         {
-            const FileDirection dir = upload? FILE_UPLOAD : FILE_DOWNLOAD;
+            const FileDirection dir = upload ? FILE_UPLOAD : FILE_DOWNLOAD;
             continue_inspecting_file = file_flows->file_process(p, get_file_cache_file_id(), data,
                 data_size, file_offset, dir, get_multiprocessing_file_id(), position);
         }
index fc1f84c72d62ea45bff7089d0a56716923856851..9727a24d4b41c5f1eb5570326568cc51bea51a99 100644 (file)
@@ -235,7 +235,7 @@ bool Dce2Smb2FileTracker::process_data(const uint32_t current_flow_key, const ui
     if (detection_size)
     {
         set_file_data(file_data, (detection_size > UINT16_MAX) ?
-            UINT16_MAX : (uint16_t)detection_size);
+            UINT16_MAX : (uint16_t)detection_size, file_id);
         file_detect();
     }
 
index 6d84ed20ebc6e24dc9098fbd38f6229b956440fe..81c36bc6e3d941f920e618bcf2cd73c4bd68a12d 100644 (file)
@@ -1720,7 +1720,8 @@ void DCE2_SmbProcessFileData(DCE2_SmbSsnData* ssd,
         ((ftracker->ff_file_offset == ftracker->ff_bytes_processed) &&
         ((file_data_depth == 0) || (ftracker->ff_bytes_processed < (uint64_t)file_data_depth))))
     {
-        set_file_data(data_ptr, (data_len > UINT16_MAX) ? UINT16_MAX : (uint16_t)data_len);
+        set_file_data(data_ptr, (data_len > UINT16_MAX) ? UINT16_MAX : (uint16_t)data_len,
+            ftracker->file_key.file_id);
         DCE2_FileDetect();
         set_file_data(nullptr, 0);
     }
index b87c257e3bf28dc68bc6b2fc535004934607393e..68d02254e737353e6a43f5db907e08415e067625 100644 (file)
@@ -62,7 +62,7 @@ static void FTPDataProcess(
 {
     int status;
 
-    set_file_data(p->data, p->dsize);
+    set_file_data(p->data, p->dsize, data_ssn->path_hash);
 
     if (data_ssn->packet_flags & FTPDATA_FLG_REST)
     {
index c52735a85996c83e0481dff230b09a72d440bb7b..36b384896c84315ac3aa6fca35c300b9ebdad5d3 100644 (file)
@@ -74,6 +74,7 @@ void Field::reset()
     strt = nullptr;
     len = STAT_NOT_COMPUTE;
     own_the_buffer = false;
+    was_accumulated = false;
 }
 
 #ifdef REG_TEST
index 5abae6fa83d78410c407c359899392deeecf00b2..afc1dbce605aade4fb6b5407ddd89dfde65a25cd 100644 (file)
@@ -51,6 +51,8 @@ public:
     void set(HttpCommon::StatusCode stat_code);
     void set(int32_t length) { set(static_cast<HttpCommon::StatusCode>(length)); }
     void reset();
+    void set_accumulation(bool is_accum) { was_accumulated = is_accum; }
+    bool is_accumulated() const { return was_accumulated; }
 
 #ifdef REG_TEST
     void print(FILE* output, const char* name) const;
@@ -60,6 +62,8 @@ private:
     const uint8_t* strt = nullptr;
     int32_t len = HttpCommon::STAT_NOT_COMPUTE;
     bool own_the_buffer = false;
+    // FIXIT-M: find better place for the attribute, replace it with actual number of bytes processed
+    bool was_accumulated = false;
 };
 
 struct MimeBufs
index 8d248e2da6c73dea3ecdbdd9658e31f630a85062..677ed8c6e302a8b19c4bf3b18bdfe1ee0dce7cf1 100755 (executable)
@@ -261,6 +261,8 @@ bool HttpInspect::get_buf(unsigned id, Packet* p, InspectionBuffer& b)
 
     b.data = http_buffer.start();
     b.len = http_buffer.length();
+    b.is_accumulated = http_buffer.is_accumulated();
+
     return true;
 }
 
index 10d99a4706ea2f5d0e4951b80cd1448b1d44adf1..702669558114a19e50bf679d87dffb22df739bd9 100644 (file)
@@ -246,6 +246,8 @@ void HttpJsNorm::do_external(const Field& input, Field& output,
             *infractions += INF_JS_CLOSING_TAG;
             events->create_event(EVENT_JS_CLOSING_TAG);
         }
+        if (js_ctx.is_buffer_adjusted())
+            output.set_accumulation(true);
 
         if (ssn->js_built_in_event)
             break;
@@ -391,6 +393,8 @@ void HttpJsNorm::do_inline(const Field& input, Field& output,
             *infractions += INF_JS_OPENING_TAG;
             events->create_event(EVENT_JS_OPENING_TAG);
         }
+        if (js_ctx.is_buffer_adjusted())
+            output.set_accumulation(true);
 
         script_continue = ret == JSTokenizer::SCRIPT_CONTINUE;
     }
index 29bc17852bf6c61433b3348bdfb44a708b2a9d50..d2c46878c67312e11289d6060599a9e5e749201f 100644 (file)
@@ -149,7 +149,7 @@ void HttpMsgBody::analyze()
             msg_text_new.length() : pub_depth_remaining;
         pub_depth_remaining -= publish_length;
     }
-    
+
     if (session_data->mime_state[source_id])
     {
         // FIXIT-M this interface does not convey any indication of end of message body. If the
@@ -167,6 +167,9 @@ void HttpMsgBody::analyze()
             session_data->partial_mime_bufs[source_id] = nullptr;
             last_attachment_complete = session_data->partial_mime_last_complete[source_id];
             session_data->partial_mime_last_complete[source_id] = true;
+
+            if (!mime_bufs->empty())
+                mime_bufs->front().file.set_accumulation(true);
         }
         else
             mime_bufs = new std::list<MimeBufs>;
@@ -210,6 +213,8 @@ void HttpMsgBody::analyze()
                 }
                 else
                     mime_bufs->emplace_back(attach_length, attach_buf, true, STAT_NOT_PRESENT, nullptr, false);
+
+                mime_bufs->back().file.set_accumulation(!last_attachment_complete);
             }
             last_attachment_complete = latest_attachment.finished;
         }
@@ -237,6 +242,8 @@ void HttpMsgBody::analyze()
 
             if (partial_detect_length > 0)
             {
+                detect_data.set_accumulation(true);
+                norm_js_data.set_accumulation(true);
                 const int32_t total_length = partial_detect_length +
                     decompressed_file_body.length();
                 assert(total_length <=
@@ -288,7 +295,9 @@ void HttpMsgBody::analyze()
                 partial_js_detect_length = js_norm_body.length();
             }
 
-            set_file_data(const_cast<uint8_t*>(detect_data.start()), (unsigned)detect_data.length());
+            const uint64_t file_index = get_header(source_id)->get_multi_file_processing_id();
+            set_file_data(const_cast<uint8_t*>(detect_data.start()),
+                (unsigned)detect_data.length(), file_index, detect_data.is_accumulated());
         }
     }
     body_octets += msg_text.length();
@@ -348,7 +357,7 @@ void HttpMsgBody::get_ole_data()
         session_data->fd_state[source_id]->ole_data_reset();
     }
 }
-    
+
 void HttpMsgBody::do_file_decompression(const Field& input, Field& output)
 {
     if (session_data->fd_state[source_id] == nullptr)
@@ -569,10 +578,13 @@ bool HttpMsgBody::run_detection(snort::Packet* p)
     if ((mime_bufs != nullptr) && !mime_bufs->empty())
     {
         auto mb = mime_bufs->cbegin();
-        for (uint32_t count = 1; (count <= params->max_mime_attach) && (mb != mime_bufs->cend());
-            count++, mb++)
+        for (uint32_t count = 0; (count < params->max_mime_attach) && (mb != mime_bufs->cend());
+            ++count, ++mb)
         {
-            set_file_data(mb->file.start(), mb->file.length());
+            const uint64_t idx = get_header(source_id)->get_multi_file_processing_id();
+            set_file_data(mb->file.start(), mb->file.length(), idx,
+                count or mb->file.is_accumulated(),
+                std::next(mb) != mime_bufs->end() or last_attachment_complete);
             if (mb->vba.length() > 0)
                 ole_data.set(mb->vba.length(), mb->vba.start());
             DetectionEngine::detect(p);
@@ -639,7 +651,7 @@ void HttpMsgBody::get_file_info(FileDirection dir, const uint8_t*& filename_buff
         if (filename_length > 0)
             return;
 
-        const Field& path = http_uri->get_norm_path(); 
+        const Field& path = http_uri->get_norm_path();
         if (path.length() > 0)
         {
             int last_slash_index = path.length() - 1;
@@ -747,4 +759,3 @@ void HttpMsgBody::print_body_section(FILE* output, const char* body_type_str)
     HttpMsgSection::print_section_wrapup(output);
 }
 #endif
-
index b3a68f2ce3faf0885a18119c82e7e73ffa1d130c..15bda008ef60a36e321af958eb00864044601947 100755 (executable)
@@ -556,6 +556,9 @@ void HttpMsgHeader::setup_file_processing()
     if (session_data->mime_state[source_id])
         return;
 
+    // Generate the unique file id for multi file processing and set ID for file_data buffer
+    set_multi_file_processing_id(get_transaction_id(), session_data->get_hx_stream_id());
+
     session_data->file_octets[source_id] = 0;
     const int64_t max_file_depth = FileService::get_max_file_depth();
     if (max_file_depth <= 0)
@@ -564,9 +567,6 @@ void HttpMsgHeader::setup_file_processing()
         return;
     }
 
-    // Generate the unique file id for multi file processing
-    set_multi_file_processing_id(get_transaction_id(), session_data->get_hx_stream_id());
-
     session_data->file_depth_remaining[source_id] = max_file_depth;
     FileFlows* file_flows = FileFlows::get_file_flows(flow);
     if (!file_flows)
index a2be675e5cf7d6f8fbf25581f41abf7c3a72929c..989f4ee34203fa94f181604118abc64753e94146 100644 (file)
 #include "protocols/packet.h"
 
 #include "http_common.h"
+#include "http_context_data.h"
 #include "http_enum.h"
 #include "http_inspect.h"
+#include "http_msg_section.h"
 
 using namespace snort;
 using namespace HttpCommon;
@@ -163,7 +165,19 @@ IpsOption::EvalStatus HttpBufferIpsOption::eval(Cursor& c, Packet* p)
     if (http_buffer.length() <= 0)
         return NO_MATCH;
 
-    c.set(key, http_buffer.start(), http_buffer.length());
+    if (idx != BUFFER_PSI_JS_DATA)
+    {
+        c.set(key, http_buffer.start(), http_buffer.length());
+        c.set_accumulation(http_buffer.is_accumulated());
+    }
+    else
+    {
+        HttpMsgSection* section = HttpContextData::get_snapshot(p);
+        uint64_t tid = section ? section->get_transaction_id() : 0;
+        c.set(key, tid, http_buffer.start(), http_buffer.length(), true);
+        c.set_accumulation(http_buffer.is_accumulated());
+    }
+
 
     return MATCH;
 }
index afb6a8055dec0029dc63a1d722da831a53d2dde0..0331258ad7084d2f4a8bb983369b75847c08326f 100644 (file)
@@ -177,7 +177,7 @@ static IMAPData* SetNewIMAPData(IMAP_PROTO_CONF* config, Packet* p)
     imap_ssn = &fd->session;
 
     imapstats.sessions++;
-    imap_ssn->mime_ssn= new ImapMime(p, &(config->decode_conf),&(config->log_config));
+    imap_ssn->mime_ssn= new ImapMime(p, &(config->decode_conf), &(config->log_config));
     imap_ssn->mime_ssn->set_mime_stats(&(imapstats.mime_stats));
 
     if (p->packet_flags & SSNFLAG_MIDSTREAM)
index 92e77602cb8b183c8f0adffd732cd34a66690e84..c118cf8ffa08647efb0e4d89e4ce65c4bc36bf29 100644 (file)
@@ -233,6 +233,7 @@ static SMTPData* SetNewSMTPData(SmtpProtoConf* config, Packet* p)
     p->flow->set_flow_data(fd);
     smtp_ssn = &fd->session;
 
+    smtpstats.sessions++;
     smtp_ssn->mime_ssn = new SmtpMime(p, &(config->decode_conf), &(config->log_config));
     smtp_ssn->mime_ssn->config = config;
     smtp_ssn->mime_ssn->set_mime_stats(&(smtpstats.mime_stats));
index a65f59c6d2579eae7fae6309b08ee124648fa95f..a3651f2fd5050a677ea540c23f81c80f0bc5c4ec 100644 (file)
@@ -86,7 +86,7 @@ int FileSession::process(Packet* p)
         if (file_name)
             file_flows->set_file_name((const uint8_t*)file_name, strlen(file_name));
     }
-    set_file_data(p->data, p->dsize);
+    set_file_data(p->data, p->dsize, c->upload);
 
     return 0;
 }
diff --git a/src/utils/grouped_list.h b/src/utils/grouped_list.h
new file mode 100644 (file)
index 0000000..8305595
--- /dev/null
@@ -0,0 +1,193 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2022-2022 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// grouped_list.h author Cisco
+
+#ifndef GROUPED_LIST_H
+#define GROUPED_LIST_H
+
+// The class below represents a group (double-linked list) of elements,
+// where each element belongs to a sub-group.
+// Sub-groups never intersect,
+// but a sub-group of elements can be deleted altogether from the main group.
+
+#include <assert.h>
+
+namespace snort
+{
+
+template <class T>
+class GroupedList
+{
+public:
+    GroupedList();
+    GroupedList(GroupedList& cont, GroupedList*& group, const T& value);
+    GroupedList(GroupedList& cont, GroupedList*& group, T&& value);
+    template <class... Args>
+    GroupedList(GroupedList& cont, GroupedList*& group, Args&&... args);
+    ~GroupedList();
+
+    inline T& operator *();
+    inline GroupedList* get_next() const;
+    inline void leave_group();
+    inline static unsigned erase_group(GroupedList*& group);
+
+private:
+    inline void init();
+    inline void leave();
+    inline void erase_all();
+
+    GroupedList* prev;       // previous element in a container (list 1)
+    GroupedList* next;       // next element in a container (list 1)
+    GroupedList* mate;       // next element in a group (list 2)
+    GroupedList** grp;       // pointer to this element, a group holder (list 2)
+    T value;                 // value of element
+    const bool holder;       // if the element is a container holder
+};
+
+template <typename T>
+GroupedList<T>::GroupedList()
+    : prev(this), next(this), mate(nullptr), grp(nullptr), holder(true)
+{ }
+
+template <typename T>
+GroupedList<T>::GroupedList(GroupedList& cont, GroupedList*& group, const T& v)
+    : prev(cont.prev), next(&cont), mate(group), grp(&group), value(v), holder(false)
+{
+    init();
+    group = this;
+}
+
+template <typename T>
+GroupedList<T>::GroupedList(GroupedList& cont, GroupedList*& group, T&& v)
+    : prev(cont.prev), next(&cont), mate(group), grp(&group),value(v), holder(false)
+{
+    init();
+    group = this;
+}
+
+template <typename T> template <class... Args>
+GroupedList<T>::GroupedList(GroupedList& cont, GroupedList*& group, Args&&... args)
+    : prev(cont.prev), next(&cont), mate(group), grp(&group),value{std::forward<Args>(args)...}, holder(false)
+{
+    init();
+    group = this;
+}
+
+template <typename T>
+GroupedList<T>::~GroupedList()
+{
+    if (holder)
+        erase_all();
+    else
+        leave();
+}
+
+template <typename T>
+T& GroupedList<T>::operator *()
+{
+    return value;
+}
+
+template <typename T>
+GroupedList<T>* GroupedList<T>::get_next() const
+{
+    return next;
+}
+
+template <typename T>
+inline void GroupedList<T>::leave_group()
+{
+    if (grp)
+        *grp = *grp == this ? mate : nullptr;
+
+    if (mate)
+        mate->grp = grp;
+
+    grp = nullptr;
+    mate = nullptr;
+
+    assert(!holder);
+}
+
+template <typename T>
+inline void GroupedList<T>::init()
+{
+    assert(prev);
+    assert(next);
+    assert(next->holder);
+    assert(!mate or !mate->holder);
+
+    prev->next = this;
+    next->prev = this;
+
+    if (mate)
+        mate->grp = &mate;
+}
+
+template <typename T>
+void GroupedList<T>::leave()
+{
+    assert(!prev or prev->next == this);
+    assert(!next or next->prev == this);
+
+    if (prev)
+        prev->next = next;
+
+    if (next)
+        next->prev = prev;
+}
+
+template <typename T>
+void GroupedList<T>::erase_all()
+{
+    auto it = next;
+
+    while (it != this)
+    {
+        auto el = it;
+        it = it->next;
+
+        assert(!el->holder);
+        delete el;
+    }
+}
+
+template <typename T>
+unsigned GroupedList<T>::erase_group(GroupedList<T>*& group)
+{
+    unsigned cnt = 0;
+    auto it = group;
+    group = nullptr;
+
+    while (it)
+    {
+        auto el = it;
+        it = it->mate;
+        ++cnt;
+
+        assert(!el->holder);
+        delete el;
+    }
+
+    return cnt;
+}
+
+}
+
+#endif
index 5243faaa243f402f3d425e5c4429cc1f3f06d70b..5de32aee832b792c37c12009b04a9044af085ebe 100644 (file)
@@ -71,6 +71,9 @@ public:
     bool is_closing_tag_seen() const
     { return tokenizer.is_closing_tag_seen(); }
 
+    bool is_buffer_adjusted() const
+    { return tokenizer.is_buffer_adjusted(); }
+
 #if defined(CATCH_TEST_BUILD) || defined(BENCHMARK_TEST)
     const char* get_tmp_buf() const
     { return tmp_buf; }
index bd46004d8979742db4d30b02f28210a01ebd41b4..a133074093de3ce42b8247a5afa7a386fdb4b022 100644 (file)
@@ -180,6 +180,7 @@ public:
     bool is_mixed_encoding_seen() const;
     bool is_opening_tag_seen() const;
     bool is_closing_tag_seen() const;
+    bool is_buffer_adjusted() const;
 
 protected:
     [[noreturn]] void LexerError(const char* msg) override
@@ -368,6 +369,7 @@ private:
     const int tmp_cap_size;
 
     bool newline_found = false;
+    bool adjusted_data = false;              // flag for resetting the continuation in case of adjusting js_data
     constexpr static bool insert_semicolon[ASI_GROUP_MAX][ASI_GROUP_MAX]
     {
         {false, false, false, false, false, false, false, false, false, false, false,},
index f75731ae470e9e76ec188c628da8bfe74379b64f..de0bcdd0d32898c0934253afc56735861f30e4ce 100644 (file)
@@ -1828,6 +1828,8 @@ bool JSTokenizer::states_process()
 
 void JSTokenizer::states_adjust()
 {
+    adjusted_data = true;
+
     int outbuf_pos = yyout.rdbuf()->pubseekoff(0, std::ios_base::cur, std::ios_base::out);
     assert(outbuf_pos >= 0);
 
@@ -2045,6 +2047,11 @@ bool JSTokenizer::is_closing_tag_seen() const
     return closing_tag_seen;
 }
 
+bool JSTokenizer::is_buffer_adjusted() const
+{
+    return adjusted_data;
+}
+
 void JSTokenizer::set_block_param(bool f)
 {
     scope_cur().block_param = f;
@@ -3030,6 +3037,7 @@ JSTokenizer::JSRet JSTokenizer::process(size_t& bytes_in, bool external_script)
     unescape_nest_seen = false;
     mixed_encoding_seen = false;
     ext_script = external_script;
+    adjusted_data = false;
 
     auto r = yylex();
 
index be430903e11929cc0ac120ca3f45693f597c91fd..0f3147f965cd8d58bb5c59279bf99ba051b476c3 100644 (file)
@@ -214,6 +214,13 @@ const PegInfo pc_names[] =
     { CountType::SUM, "pcre_match_limit", "total number of times pcre hit the match limit" },
     { CountType::SUM, "pcre_recursion_limit", "total number of times pcre hit the recursion limit" },
     { CountType::SUM, "pcre_error", "total number of times pcre returns error" },
+    { CountType::SUM, "cont_creations", "total number of continuations created" },
+    { CountType::SUM, "cont_recalls", "total number of continuations recalled" },
+    { CountType::SUM, "cont_flows", "total number of flows using continuation" },
+    { CountType::SUM, "cont_evals", "total number of condition-met continuations" },
+    { CountType::SUM, "cont_matches", "total number of continuations matched" },
+    { CountType::SUM, "cont_mismatches", "total number of continuations mismatched" },
+    { CountType::MAX, "cont_max_num", "peak number of simultaneous continuations per flow" },
     { CountType::END, nullptr, nullptr }
 };
 
index 8e548b062e95e3ac69fb84eff69840dfbafebb81..808bfffb42c7a0dc031849217505fb394053c6fb 100644 (file)
@@ -63,6 +63,13 @@ struct PacketCount
     PegCount pcre_match_limit;
     PegCount pcre_recursion_limit;
     PegCount pcre_error;
+    PegCount cont_creations;
+    PegCount cont_recalls;
+    PegCount cont_flows;
+    PegCount cont_evals;
+    PegCount cont_matches;
+    PegCount cont_mismatches;
+    PegCount cont_max_num;
 };
 
 struct ProcessCount
index b9be76201872b8d290a015fe325d586889979b0a..12a694b3b8190a248869d2947dd3737fe129e2aa 100644 (file)
@@ -66,3 +66,7 @@ add_catch_test( streambuf_test
         ../streambuf.cc
 )
 
+add_catch_test( grouped_list_test
+    SOURCES
+        ../grouped_list.h
+)
diff --git a/src/utils/test/grouped_list_test.cc b/src/utils/test/grouped_list_test.cc
new file mode 100644 (file)
index 0000000..9dd7953
--- /dev/null
@@ -0,0 +1,595 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2022-2022 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// grouped_list_test.cc author Cisco
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "catch/catch.hpp"
+
+#include <cstring>
+#include <vector>
+
+#include "utils/grouped_list.h"
+
+using namespace snort;
+using namespace std;
+
+struct Type
+{
+    int id = 0;
+    const char* name = "";
+
+    bool operator ==(const Type& r) const
+    { return id == r.id and !strcmp(name, r.name); }
+};
+
+using Elem = GroupedList<Type>;
+
+template <class... Args>
+static void check_container(const Elem& cont, Args&& ... args)
+{
+    const vector<Type> data{std::forward<Args>(args)...};
+    Elem* it = cont.get_next();
+
+    for (const auto& d : data)
+    {
+        REQUIRE(it != nullptr);
+        CHECK((**it) == d);
+        it = it->get_next();
+    }
+
+    CHECK(it == &cont);
+}
+
+TEST_CASE("Basic", "[Double list]")
+{
+    Elem cont;
+    Elem* group = nullptr;
+
+    SECTION("no data")
+    {
+        check_container(cont);
+    }
+
+    SECTION("1 element")
+    {
+        const Type data = {1, "one"};
+        Elem* el = new Elem(cont, group, data);
+
+        CHECK(el != nullptr);
+
+        check_container(cont, data);
+    }
+
+    SECTION("3 elements")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* el1 = new Elem(cont, group, data1);
+        Elem* el2 = new Elem(cont, group, data2);
+        Elem* el3 = new Elem(cont, group, data3);
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+        CHECK(el3 != nullptr);
+
+        check_container(cont, data1, data2, data3);
+    }
+}
+
+TEST_CASE("Groups", "[Double list]")
+{
+    Elem cont;
+
+    SECTION("3 element group")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* group_a = nullptr;
+
+        Elem* el1 = new Elem(cont, group_a, data1);
+        CHECK(group_a == el1);
+        Elem* el2 = new Elem(cont, group_a, data2);
+        CHECK(group_a == el2);
+        Elem* el3 = new Elem(cont, group_a, data3);
+        CHECK(group_a == el3);
+
+        check_container(cont, data1, data2, data3);
+
+        auto cnt = Elem::erase_group(group_a);
+        CHECK(cnt == 3);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("3 groups")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* group_a = nullptr;
+        Elem* group_b = nullptr;
+        Elem* group_c = nullptr;
+
+        Elem* el1 = new Elem(cont, group_a, data1);
+        CHECK(group_a == el1);
+        Elem* el2 = new Elem(cont, group_b, data2);
+        CHECK(group_b == el2);
+        Elem* el3 = new Elem(cont, group_c, data3);
+        CHECK(group_c == el3);
+
+        check_container(cont, data1, data2, data3);
+
+        auto cnt1 = Elem::erase_group(group_a);
+        CHECK(cnt1 == 1);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, data2, data3);
+
+        auto cnt2 = Elem::erase_group(group_b);
+        CHECK(cnt2 == 1);
+        CHECK(group_b == nullptr);
+
+        check_container(cont, data3);
+
+        auto cnt3 = Elem::erase_group(group_c);
+        CHECK(cnt3 == 1);
+        CHECK(group_c == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("interleaving groups")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* group_a = nullptr;
+        Elem* group_b = nullptr;
+        Elem* group_c = nullptr;
+
+        new Elem(cont, group_a, data1);
+        new Elem(cont, group_b, data2);
+        new Elem(cont, group_c, data3);
+        new Elem(cont, group_a, data1);
+        new Elem(cont, group_b, data2);
+        new Elem(cont, group_c, data3);
+        new Elem(cont, group_a, data1);
+        new Elem(cont, group_b, data2);
+        new Elem(cont, group_c, data3);
+
+        check_container(cont, data1, data2, data3, data1, data2, data3, data1, data2, data3);
+
+        auto cnt1 = Elem::erase_group(group_a);
+        CHECK(cnt1 == 3);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, data2, data3, data2, data3, data2, data3);
+
+        auto cnt2 = Elem::erase_group(group_b);
+        CHECK(cnt2 == 3);
+        CHECK(group_b == nullptr);
+
+        check_container(cont, data3, data3, data3);
+
+        auto cnt3 = Elem::erase_group(group_c);
+        CHECK(cnt3 == 3);
+        CHECK(group_c == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("leaving a group (middle)")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* group_a = nullptr;
+
+        Elem* el1 = new Elem(cont, group_a, data1);
+        CHECK(group_a == el1);
+        Elem* el2 = new Elem(cont, group_a, data2);
+        CHECK(group_a == el2);
+        Elem* el3 = new Elem(cont, group_a, data3);
+        CHECK(group_a == el3);
+
+        check_container(cont, data1, data2, data3);
+
+        el2->leave_group();
+        delete el2;
+        CHECK(group_a == el3);
+
+        check_container(cont, data1, data3);
+
+        auto cnt = Elem::erase_group(group_a);
+        CHECK(cnt == 2);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("leaving a group (begin)")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* group_a = nullptr;
+
+        Elem* el1 = new Elem(cont, group_a, data1);
+        CHECK(group_a == el1);
+        Elem* el2 = new Elem(cont, group_a, data2);
+        CHECK(group_a == el2);
+        Elem* el3 = new Elem(cont, group_a, data3);
+        CHECK(group_a == el3);
+
+        check_container(cont, data1, data2, data3);
+
+        el1->leave_group();
+        delete el1;
+        CHECK(group_a == el3);
+
+        check_container(cont, data2, data3);
+
+        auto cnt = Elem::erase_group(group_a);
+        CHECK(cnt == 2);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("leaving a group (end)")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* group_a = nullptr;
+
+        Elem* el1 = new Elem(cont, group_a, data1);
+        CHECK(group_a == el1);
+        Elem* el2 = new Elem(cont, group_a, data2);
+        CHECK(group_a == el2);
+        Elem* el3 = new Elem(cont, group_a, data3);
+        CHECK(group_a == el3);
+
+        check_container(cont, data1, data2, data3);
+
+        el3->leave_group();
+        delete el3;
+        CHECK(group_a == el2);
+
+        check_container(cont, data1, data2);
+
+        auto cnt = Elem::erase_group(group_a);
+        CHECK(cnt == 2);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("leaving a group (one by one)")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* group_a = nullptr;
+
+        Elem* el1 = new Elem(cont, group_a, data1);
+        CHECK(group_a == el1);
+        Elem* el2 = new Elem(cont, group_a, data2);
+        CHECK(group_a == el2);
+        Elem* el3 = new Elem(cont, group_a, data3);
+        CHECK(group_a == el3);
+
+        check_container(cont, data1, data2, data3);
+
+        el3->leave_group();
+        delete el3;
+        CHECK(group_a == el2);
+
+        el2->leave_group();
+        delete el2;
+        CHECK(group_a == el1);
+
+        el1->leave_group();
+        delete el1;
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("leaving a group (repeated call)")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        const Type data4 = {4, "four"};
+        const Type data5 = {5, "five"};
+        Elem* group_a = nullptr;
+
+        Elem* el1 = new Elem(cont, group_a, data1);
+        Elem* el2 = new Elem(cont, group_a, data2);
+        Elem* el3 = new Elem(cont, group_a, data3);
+        Elem* el4 = new Elem(cont, group_a, data4);
+        Elem* el5 = new Elem(cont, group_a, data5);
+
+        check_container(cont, data1, data2, data3, data4, data5);
+
+        el1->leave_group();
+        el1->leave_group();
+        el1->leave_group();
+
+        el3->leave_group();
+        el3->leave_group();
+        el3->leave_group();
+
+        el5->leave_group();
+        el5->leave_group();
+        el5->leave_group();
+
+        CHECK(group_a == el4);
+        check_container(cont, data1, data2, data3, data4, data5);
+
+        delete el1;
+        delete el2;
+        delete el3;
+        delete el4;
+        delete el5;
+    }
+}
+
+TEST_CASE("Memory management (value by copy)", "[Double list]")
+{
+    Elem cont;
+    Elem* group_a = nullptr;
+    Elem* group_b = nullptr;
+
+    SECTION("delete element")
+    {
+        const Type data = {1, "one"};
+        Elem* el = new Elem(cont, group_a, data);
+
+        CHECK(el != nullptr);
+
+        check_container(cont, data);
+
+        delete el;
+
+        check_container(cont);
+    }
+
+    SECTION("delete group")
+    {
+        const Type data = {1, "one"};
+        Elem* el = new Elem(cont, group_a, data);
+
+        CHECK(el != nullptr);
+
+        check_container(cont, data);
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("delete element, then delete group")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        Elem* el1 = new Elem(cont, group_a, data1);
+        Elem* el2 = new Elem(cont, group_b, data2);
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+
+        delete el2;
+
+        check_container(cont, data1);
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+
+    }
+
+    SECTION("delete group, then delete element")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        Elem* el1 = new Elem(cont, group_a, data1);
+        Elem* el2 = new Elem(cont, group_b, data2);
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, data2);
+
+        delete el2;
+
+        check_container(cont);
+    }
+
+    SECTION("delete elements (remain), then delete group")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* el1 = new Elem(cont, group_a, data1);
+        Elem* el2 = new Elem(cont, group_b, data2);
+        Elem* el3 = new Elem(cont, group_b, data3);
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+        CHECK(el3 != nullptr);
+
+        delete el2;
+
+        check_container(cont, data1, data3);
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, data3);
+
+    }
+
+    SECTION("delete group, then delete elements (remain)")
+    {
+        const Type data1 = {1, "one"};
+        const Type data2 = {2, "two"};
+        const Type data3 = {3, "three"};
+        Elem* el1 = new Elem(cont, group_a, data1);
+        Elem* el2 = new Elem(cont, group_b, data2);
+        Elem* el3 = new Elem(cont, group_b, data3);
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+        CHECK(el3 != nullptr);
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, data2, data3);
+
+        delete el2;
+
+        check_container(cont, data3);
+    }
+}
+
+TEST_CASE("Memory management (value in-place)", "[Double list]")
+{
+    Elem cont;
+    Elem* group_a = nullptr;
+    Elem* group_b = nullptr;
+
+    SECTION("delete element")
+    {
+        Elem* el = new Elem(cont, group_a, 1, "one");
+
+        CHECK(el != nullptr);
+
+        check_container(cont, Type{1, "one"});
+
+        delete el;
+
+        check_container(cont);
+    }
+
+    SECTION("delete group")
+    {
+        Elem* el = new Elem(cont, group_a, 1, "one");
+
+        CHECK(el != nullptr);
+
+        check_container(cont, Type{1, "one"});
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+    }
+
+    SECTION("delete element, then delete group")
+    {
+        Elem* el1 = new Elem(cont, group_a, 1, "one");
+        Elem* el2 = new Elem(cont, group_b, 2, "two");
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+
+        delete el2;
+
+        check_container(cont, Type{1, "one"});
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont);
+
+    }
+
+    SECTION("delete group, then delete element")
+    {
+        Elem* el1 = new Elem(cont, group_a, 1, "one");
+        Elem* el2 = new Elem(cont, group_b, 2, "two");
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, Type{2, "two"});
+
+        delete el2;
+
+        check_container(cont);
+    }
+
+    SECTION("delete elements (remain), then delete group")
+    {
+        Elem* el1 = new Elem(cont, group_a, 1, "one");
+        Elem* el2 = new Elem(cont, group_b, 2, "two");
+        Elem* el3 = new Elem(cont, group_b, 3, "three");
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+        CHECK(el3 != nullptr);
+
+        delete el2;
+
+        check_container(cont, Type{1, "one"}, Type{3, "three"});
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, Type{3, "three"});
+
+    }
+
+    SECTION("delete group, then delete elements (remain)")
+    {
+        Elem* el1 = new Elem(cont, group_a, 1, "one");
+        Elem* el2 = new Elem(cont, group_b, 2, "two");
+        Elem* el3 = new Elem(cont, group_b, 3, "three");
+
+        CHECK(el1 != nullptr);
+        CHECK(el2 != nullptr);
+        CHECK(el3 != nullptr);
+
+        Elem::erase_group(group_a);
+        CHECK(group_a == nullptr);
+
+        check_container(cont, Type{2, "two"}, Type{3, "three"});
+
+        delete el2;
+
+        check_container(cont, Type{3, "three"});
+    }
+}