diff --git a/rust/src/utils/flowbits_resolver.rs b/rust/src/utils/flowbits_resolver.rs new file mode 100644 index 0000000000..0fd41e9272 --- /dev/null +++ b/rust/src/utils/flowbits_resolver.rs @@ -0,0 +1,286 @@ +/* Copyright (C) 2026 Open Information Security Foundation +* + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * 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 + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Shivani Bhardwaj + +use petgraph::algo::{is_cyclic_directed, tarjan_scc, toposort}; +use petgraph::graph::{GraphError, NodeIndex}; +use petgraph::stable_graph::StableDiGraph; +use petgraph::visit::EdgeRef; +use petgraph::Direction; +use std::collections::{HashMap, HashSet}; +use std::os::raw::c_void; + +/// Special Graph Node storing flowbit or signature +#[derive(Debug, Copy, Clone)] +struct SCGNode { + iid: u32, + sid: u32, + nidx: NodeIndex, /* Graph's internal node index */ +} + +/// Edge object to determine a genuine cycle breaker +#[derive(Debug, Copy, Clone)] +struct SCGEdge { + cmd: u8, + fb_id: u32, +} + +#[derive(Debug)] +struct FlowbitSidStore { + graph: StableDiGraph, + iid_map: HashMap, +} + +/// Function to create an empty directed Graph +#[no_mangle] +pub unsafe extern "C" fn SCCreateDirectedGraph() -> *mut c_void { + // StableDiGraph is the ideal choice here for there is removal of + // nodes in the line later and this type of graph guarantees to + // not re-use any existing node indices + let fb_sid_store = FlowbitSidStore { + graph: StableDiGraph::new(), + iid_map: HashMap::new(), + }; + + /* Make an opaque pointer for C as nothing is changed there */ + return Box::into_raw(Box::new(fb_sid_store)) as *mut c_void; +} + +/// Drop the directed Graph. Called from C. +#[no_mangle] +pub unsafe extern "C" fn SCFreeDirectedGraph(store: *mut c_void) { + let _ = Box::from_raw(store as *mut FlowbitSidStore); +} + +#[no_mangle] +pub unsafe extern "C" fn SCGetOrCreateNodeGraph(graph: *mut c_void, iid: u32, sid: u32) -> i64 { + let g = &mut *(graph as *mut FlowbitSidStore); + + let Some(node_idx) = get_or_create_node(g, iid, sid) else { + SCLogError!("Error adding node; Graph is at full capacity"); + return -2; + }; + + node_idx.index() as i64 +} + +/// Function to get or create a node and add an appropriate directed +/// edge based on its type +#[no_mangle] +pub unsafe extern "C" fn SCCreateNodeEdgeDirectedGraph( + fss_void: *mut c_void, from: u32, to: u32, cmd: u8, fb_id: u32, +) -> i64 { + let fss = &mut *(fss_void as *mut FlowbitSidStore); + + let from_idx = NodeIndex::from(from); + let to_idx = NodeIndex::from(to); + + match fss + .graph + .try_update_edge(from_idx, to_idx, SCGEdge { cmd, fb_id }) + { + /* edge from a flowbit setter to a flowbit reader */ + Ok(_) => { + SCLogDebug!( + "Created an edge from {:?} -> {:?} for flowbit {:?} with command: {:?}", + from, + to, + fb_id, + cmd + ); + } + Err(GraphError::EdgeIxLimit) => { + SCLogError!("Error adding edge; Graph is at full capacity"); + return -2; + } + Err(GraphError::NodeOutBounds) => { + SCLogError!("Error adding edge; node does not exist"); + return -2; + } + Err(_) => { + SCLogError!("Error adding edge to the Graph"); + return -2; + } + } + + return 0; +} + +fn check_cycle_update_graph(graph: &mut StableDiGraph) -> i8 { + let mut MAX_STACK_SIZE: usize = 100; + + for i in 0..MAX_STACK_SIZE { + /* Check graph for any cycles */ + if !is_cyclic_directed(&*graph) { + SCLogDebug!("no cycles after {} tries", i); + return 0; + } + + SCLogDebug!("Found a cycle in i {}. Checking if it's valid..", i); + + if !try_resolve_one_cycle(graph) { + /* If we can't resolve any cycle, we're stuck */ + SCLogError!("Unable to resolve cycles after {} tries", i); + return -1; + } + } + + SCLogError!( + "Maximum tries ({}) reached while trying to resolve cycles", + MAX_STACK_SIZE + ); + return -1; +} + +fn try_resolve_one_cycle(graph: &mut StableDiGraph) -> bool { + let sccs = tarjan_scc(&*graph); + let mut edge_map: HashMap = HashMap::new(); + + /* Find the first multi-node SCC */ + for scc in sccs { + if scc.len() == 1 { + let self_loop_edges: Vec<_> = graph + .edges(scc[0]) + .filter(|edge| edge.target() == scc[0]) + .map(|edge| edge.id()) + .collect(); + + if let Some(edge) = self_loop_edges.into_iter().next() { + graph.remove_edge(edge); + return true; + } + continue; + } + + SCLogDebug!("Current scc: {:?}", scc); + let scc_set: HashSet<_> = scc.iter().copied().collect(); + let edge_indices: Vec<_> = graph.edge_indices().collect(); + + for edge_idx in edge_indices { + let Some((_src, tgt)) = graph.edge_endpoints(edge_idx) else { + continue; + }; + if !(scc_set.contains(&_src) && scc_set.contains(&tgt)) { + continue; + } + let SCGEdge { cmd, fb_id } = graph[edge_idx]; + let same_bit_setters = graph + .edges_directed(tgt, Direction::Incoming) + .filter(|e| e.weight().fb_id == fb_id) + .count(); + if same_bit_setters > 1 { + graph.remove_edge(edge_idx); + return true; + } + /* store cycle edges to inspect their commands (weights) later */ + edge_map.insert(edge_idx, cmd); + } + /* Find if the cycle causing edges are made up of differing weights */ + let distinct_weights = edge_map.values().copied().collect::>().len(); + debug_validate_bug_on!(distinct_weights == 0); + if distinct_weights > 1 { + /* Find and remove the edge with highest weight (lowest priority) */ + if let Some((cur_e, _)) = edge_map.iter().max_by_key(|(_, &weight)| weight) { + graph.remove_edge(*cur_e); + return true; + } + } else { + /* Valid cycle with same weights -- can't resolve */ + let sids: Vec<_> = scc_set.into_iter().map(|a| graph[a].sid).collect(); + SCLogError!( + "Cyclic dependency found between flowbits from signatures: {:?}", + sids + ); + return false; + } + break; + } + + /* couldn't resolve */ + false +} + +/// Wrapper function to resolve flowbit dependencies +#[no_mangle] +pub unsafe extern "C" fn SCResolveFlowbitDependencies( + fss_void: *mut c_void, sorted_iid_list: *mut u32, sorted_iid_list_len: u32, +) -> i8 { + SCLogDebug!("Attempting to resolve flowbit dependencies"); + let fss = &mut *(fss_void as *mut FlowbitSidStore); + + let r = check_cycle_update_graph(&mut fss.graph); + if r == -1 { + SCLogError!("Couldn't do anything to fix the graph. Retreating.."); + return -1; + } + + debug_validate_bug_on!(fss.graph.node_count() == 0); + + let sorted_iid_list = + std::slice::from_raw_parts_mut(&mut *sorted_iid_list, sorted_iid_list_len as usize); + + /* No need for all the extra work if there's just one node */ + if fss.graph.node_count() == 1 { + debug_validate_bug_on!(sorted_iid_list_len != 1); + sorted_iid_list[0] = fss.graph[NodeIndex::from(0)].iid; + return 0; + } + + /* At this point, it must be a DAG, so perform a topological sort to find + * out the correct order of signatures */ + return toposort_dag(&fss.graph, sorted_iid_list); +} + +fn get_or_create_node(fss: &mut FlowbitSidStore, iid: u32, sid: u32) -> Option { + if let Some(&nidx) = fss.iid_map.get(&iid) { + return Some(nidx); /* O(1) */ + } + let nd = SCGNode { + iid, + sid, + nidx: NodeIndex::from(u32::MAX), + }; + if let Ok(idx) = fss.graph.try_add_node(nd) { + /* O(1) */ + fss.graph[idx].nidx = idx; + fss.iid_map.insert(iid, idx); + SCLogDebug!("Created node: {:?}", fss.graph[idx]); + return Some(idx); + } + + None +} + +/// Produce a topological ordering of the DAG +fn toposort_dag(graph: &StableDiGraph, sorted_iid_list: &mut [u32]) -> i8 { + match toposort(graph, None) { + Ok(order) => { + debug_validate_bug_on!(order.len() != sorted_iid_list.len()); + for (i, idx) in order.into_iter().enumerate() { + SCLogDebug!("[{:?}]: {:?}", i, graph[idx]); + sorted_iid_list[i] = graph[idx].iid; + } + 0 + } + Err(_) => { + /* Unreachable in practice: cycles are resolved before this point */ + SCLogError!("Graph still contains a cycle; cannot produce an order"); + -1 + } + } +} diff --git a/rust/src/utils/mod.rs b/rust/src/utils/mod.rs index 542aa7791f..36aea88ec4 100644 --- a/rust/src/utils/mod.rs +++ b/rust/src/utils/mod.rs @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Open Information Security Foundation +/* Copyright (C) 2024-2026 Open Information Security Foundation * * You can copy, redistribute or modify this Program under the terms of * the GNU General Public License version 2 as published by the Free @@ -17,4 +17,5 @@ pub mod base64; pub mod datalink; +pub mod flowbits_resolver; pub mod ipaddr;