Skip to content

Commit e5f2474

Browse files
committed
rust/ffi: add safe flow storage wrapper
Add a typed FlowStorage<T> wrapper around the flow storage bindings. Update example and docs. Ticket: OISF#8447
1 parent 233465f commit e5f2474

3 files changed

Lines changed: 215 additions & 15 deletions

File tree

doc/userguide/devguide/extending/flow-lifecycle-callbacks.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,45 @@ The Rust wrappers register closures or function items and return
131131
The raw pointers passed into callbacks are only valid for the duration
132132
of the callback invocation and must not be stored. Rust callbacks must
133133
not panic.
134+
135+
Flow Storage
136+
============
137+
138+
``flow::FlowStorage<T>`` provides typed, per-flow storage backed by
139+
Suricata's flow storage API. Each registered slot holds an independent value
140+
of type ``T`` for every flow.
141+
142+
Register a slot once during initialization with
143+
``FlowStorage::<T>::register``. Registration must happen before Suricata
144+
finalizes its storage registration, which is the case during plugin
145+
initialization.
146+
147+
.. code-block:: rust
148+
149+
use suricata_ffi::flow::{self, Flow, FlowStorage};
150+
use suricata_ffi::thread::ThreadVars;
151+
use suricata_sys::sys::Packet;
152+
153+
#[derive(Default)]
154+
struct FlowState {
155+
packets: u64,
156+
}
157+
158+
fn register(storage: FlowStorage<FlowState>) -> Result<(), &'static str> {
159+
flow::register_update_callback(move |tv, f, p| on_flow_update(storage, tv, f, p))
160+
}
161+
162+
Values are owned by Suricata's flow storage and are dropped automatically when
163+
the flow's storage is freed.
164+
165+
.. code-block:: rust
166+
167+
fn on_flow_init(storage: FlowStorage<FlowState>, _tv: &mut ThreadVars, f: &mut Flow, _p: *const Packet) {
168+
let _ = storage.get_or_insert_with(f, FlowState::default);
169+
}
170+
171+
fn on_flow_update(storage: FlowStorage<FlowState>, _tv: &mut ThreadVars, f: &mut Flow, _p: *mut Packet) {
172+
if let Some(state) = storage.get_mut(f) {
173+
state.packets += 1;
174+
}
175+
}

examples/plugins/rust/src/mod.rs

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::ptr::null_mut;
22

33
use suricata_ffi::eve::{self, SCJsonBuilder};
4-
use suricata_ffi::flow::{self, Flow};
4+
use suricata_ffi::flow::{self, Flow, FlowStorage};
55
use suricata_ffi::jsonbuilder::JsonBuilder;
66
use suricata_ffi::thread::{self, ThreadStorage, ThreadVars};
77
use suricata_ffi::{SCLogError, SCLogNotice, SCLogWarning};
@@ -13,6 +13,12 @@ struct ThreadState {
1313
flows: u64,
1414
}
1515

16+
/// Per-flow state stored in Suricata flow storage.
17+
#[derive(Default)]
18+
struct FlowState {
19+
packets: u64,
20+
}
21+
1622
unsafe extern "C" fn init() {
1723
suricata_ffi::plugin::init();
1824
SCLogNotice!("Initializing rust example plugin");
@@ -26,11 +32,18 @@ unsafe extern "C" fn init() {
2632
return;
2733
}
2834
};
35+
let flow_storage = match FlowStorage::<FlowState>::register("rust-example-flow") {
36+
Ok(storage) => storage,
37+
Err(err) => {
38+
SCLogError!("Failed to register rust example flow storage: {}", err);
39+
return;
40+
}
41+
};
2942

3043
if let Err(err) = register_eve_callbacks() {
3144
SCLogError!("Failed to register rust example EVE callbacks: {}", err);
3245
}
33-
if let Err(err) = register_flow_callbacks(thread_storage) {
46+
if let Err(err) = register_flow_callbacks(thread_storage, flow_storage) {
3447
SCLogError!("Failed to register rust example flow callbacks: {}", err);
3548
}
3649
if let Err(err) = register_thread_callbacks(thread_storage) {
@@ -45,10 +58,15 @@ fn register_eve_callbacks() -> Result<(), &'static str> {
4558
eve::register_callback(log_eve_wrapped)
4659
}
4760

48-
fn register_flow_callbacks(storage: ThreadStorage<ThreadState>) -> Result<(), &'static str> {
49-
flow::register_init_callback(move |tv, f, p| log_flow_init(storage, tv, f, p))?;
50-
flow::register_update_callback(log_flow_update)?;
51-
flow::register_finish_callback(log_flow_finish)?;
61+
fn register_flow_callbacks(
62+
thread_storage: ThreadStorage<ThreadState>,
63+
flow_storage: FlowStorage<FlowState>,
64+
) -> Result<(), &'static str> {
65+
flow::register_init_callback(move |tv, f, p| {
66+
log_flow_init(thread_storage, flow_storage, tv, f, p)
67+
})?;
68+
flow::register_update_callback(move |tv, f, p| log_flow_update(flow_storage, tv, f, p))?;
69+
flow::register_finish_callback(move |tv, f| log_flow_finish(flow_storage, tv, f))?;
5270
Ok(())
5371
}
5472

@@ -94,13 +112,14 @@ fn on_thread_init(storage: ThreadStorage<ThreadState>, tv: &mut ThreadVars) {
94112
}
95113

96114
fn log_flow_init(
97-
storage: ThreadStorage<ThreadState>,
115+
thread_storage: ThreadStorage<ThreadState>,
116+
flow_storage: FlowStorage<FlowState>,
98117
tv: &mut ThreadVars,
99118
f: &mut Flow,
100119
_p: *const Packet,
101120
) {
102121
// Count flows seen by this thread using the per-thread storage.
103-
let flows = match storage.get_mut(tv) {
122+
let flows = match thread_storage.get_mut(tv) {
104123
Some(state) => {
105124
state.flows += 1;
106125
state.flows
@@ -110,23 +129,48 @@ fn log_flow_init(
110129
0
111130
}
112131
};
132+
// Initialize the per-flow storage for this flow.
133+
if let Err(err) = flow_storage.get_or_insert_with(f, FlowState::default) {
134+
SCLogError!("failed to initialize rust example flow storage: {}", err);
135+
}
113136
SCLogNotice!(
114137
"rust example flow init callback: flow={:p}, thread_flows={}",
115138
f.as_ptr(),
116139
flows
117140
);
118141
}
119142

120-
fn log_flow_update(_tv: &mut ThreadVars, f: &mut Flow, _p: *mut Packet) {
143+
fn log_flow_update(
144+
flow_storage: FlowStorage<FlowState>,
145+
_tv: &mut ThreadVars,
146+
f: &mut Flow,
147+
_p: *mut Packet,
148+
) {
149+
// Count packets seen on this flow using the per-flow storage.
150+
let packets = match flow_storage.get_mut(f) {
151+
Some(state) => {
152+
state.packets += 1;
153+
state.packets
154+
}
155+
None => {
156+
SCLogWarning!("rust example flow storage was not initialized");
157+
0
158+
}
159+
};
121160
SCLogNotice!(
122-
"rust example flow update callback: flow={:p}, packet={:p}",
161+
"rust example flow update callback: flow={:p}, flow_packets={}",
123162
f.as_ptr(),
124-
_p
163+
packets
125164
);
126165
}
127166

128-
fn log_flow_finish(_tv: &mut ThreadVars, f: &mut Flow) {
129-
SCLogNotice!("rust example flow finish callback: flow={:p}", f.as_ptr());
167+
fn log_flow_finish(flow_storage: FlowStorage<FlowState>, _tv: &mut ThreadVars, f: &mut Flow) {
168+
let packets = flow_storage.get(f).map(|state| state.packets).unwrap_or(0);
169+
SCLogNotice!(
170+
"rust example flow finish callback: flow={:p}, flow_packets={}",
171+
f.as_ptr(),
172+
packets
173+
);
130174
}
131175

132176
#[no_mangle]

rust/ffi/src/flow.rs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
* 02110-1301, USA.
1616
*/
1717

18+
use std::ffi::CString;
1819
use std::marker::PhantomData;
1920
use std::os::raw::c_void;
2021

2122
use suricata_sys::sys::{
22-
self, Packet, SCFlowRegisterFinishCallback, SCFlowRegisterInitCallback,
23-
SCFlowRegisterUpdateCallback,
23+
self, Packet, SCFlowGetStorageById, SCFlowRegisterFinishCallback, SCFlowRegisterInitCallback,
24+
SCFlowRegisterUpdateCallback, SCFlowSetStorageById, SCFlowStorageId, SCFlowStorageRegister,
2425
};
2526

2627
use crate::thread::ThreadVars;
@@ -62,6 +63,119 @@ impl<'a> Flow<'a> {
6263
}
6364
}
6465

66+
/// A typed handle to a per-flow storage slot.
67+
///
68+
/// `FlowStorage<T>` wraps the `SCFlowStorageId` returned when registering flow
69+
/// storage with Suricata. Values are stored as a `Box<T>` owned by Suricata's
70+
/// flow storage and are dropped automatically when the flow's storage is freed.
71+
///
72+
/// The handle only holds the storage id, so it is `Copy` and `Send`/`Sync`
73+
/// regardless of `T`, and can be passed by value into the callbacks that need
74+
/// it.
75+
pub struct FlowStorage<T> {
76+
id: SCFlowStorageId,
77+
_marker: PhantomData<fn() -> T>,
78+
}
79+
80+
// Manual `Copy`/`Clone` impls so the handle is copyable regardless of whether
81+
// `T` is; it only holds the storage id.
82+
impl<T> Clone for FlowStorage<T> {
83+
fn clone(&self) -> Self {
84+
*self
85+
}
86+
}
87+
88+
impl<T> Copy for FlowStorage<T> {}
89+
90+
impl<T: Send + 'static> FlowStorage<T> {
91+
/// Register a new flow storage slot for values of type `T`.
92+
///
93+
/// `name` must be unique among registered flow storage. Registration has to
94+
/// happen during initialization, before Suricata finalizes storage
95+
/// registration (`SCStorageFinalize`).
96+
///
97+
/// Returns an error if `name` contains an interior nul byte or if Suricata
98+
/// rejects the registration.
99+
pub fn register(name: &str) -> Result<Self, &'static str> {
100+
let name = CString::new(name).map_err(|_| "flow storage name contains a nul byte")?;
101+
let id = unsafe { SCFlowStorageRegister(name.as_ptr(), Some(Self::free)) };
102+
if id.id < 0 {
103+
return Err("Failed to register flow storage");
104+
}
105+
106+
// Suricata keeps the storage name pointer in its storage mapping for
107+
// the lifetime of the process, so the CString is intentionally leaked.
108+
std::mem::forget(name);
109+
110+
Ok(Self {
111+
id,
112+
_marker: PhantomData,
113+
})
114+
}
115+
116+
/// Return a reference to the value stored for `f`, if any.
117+
pub fn get<'f>(&self, f: &'f Flow<'_>) -> Option<&'f T> {
118+
let ptr = unsafe { SCFlowGetStorageById(f.as_ptr(), self.id) };
119+
if ptr.is_null() {
120+
None
121+
} else {
122+
Some(unsafe { &*(ptr as *const T) })
123+
}
124+
}
125+
126+
/// Return a mutable reference to the value stored for `f`, if any.
127+
///
128+
/// Takes `&mut Flow` so the returned `&mut T` is the only live reference to
129+
/// the stored value for the duration of the borrow.
130+
pub fn get_mut<'f>(&self, f: &'f mut Flow<'_>) -> Option<&'f mut T> {
131+
let ptr = unsafe { SCFlowGetStorageById(f.as_ptr(), self.id) };
132+
if ptr.is_null() {
133+
None
134+
} else {
135+
Some(unsafe { &mut *(ptr as *mut T) })
136+
}
137+
}
138+
139+
/// Return a mutable reference to the value stored for `f`, inserting the
140+
/// value produced by `init` if none is present yet.
141+
///
142+
/// Takes `&mut Flow` so the returned `&mut T` is the only live reference to
143+
/// the stored value for the duration of the borrow.
144+
pub fn get_or_insert_with<'f>(
145+
&self, f: &'f mut Flow<'_>, init: impl FnOnce() -> T,
146+
) -> Result<&'f mut T, &'static str> {
147+
let ptr = unsafe { SCFlowGetStorageById(f.as_ptr(), self.id) };
148+
if !ptr.is_null() {
149+
return Ok(unsafe { &mut *(ptr as *mut T) });
150+
}
151+
152+
// `SCFlowSetStorageById` overwrites the slot without freeing any
153+
// previous value; we only reach here when the slot is empty.
154+
let ptr = Box::into_raw(Box::new(init()));
155+
let rc = unsafe { SCFlowSetStorageById(f.as_mut_ptr(), self.id, ptr.cast()) };
156+
if rc != 0 {
157+
unsafe {
158+
drop(Box::from_raw(ptr));
159+
}
160+
return Err("Failed to set flow storage");
161+
}
162+
163+
Ok(unsafe { &mut *ptr })
164+
}
165+
166+
/// Free callback registered with Suricata flow storage that drops the
167+
/// `Box<T>` backing a stored value.
168+
unsafe extern "C" fn free(ptr: *mut c_void) {
169+
if !ptr.is_null() {
170+
// The drop runs across an FFI boundary, so guard against unwinding
171+
// into C if `T`'s `Drop` panics.
172+
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
173+
drop(Box::from_raw(ptr as *mut T));
174+
}));
175+
}
176+
}
177+
}
178+
65179
/// Register a flow initialization callback.
66180
///
67181
/// The callback is invoked whenever Suricata initializes a flow. It receives:

0 commit comments

Comments
 (0)