Skip to content

Commit a51e8b1

Browse files
committed
rust/ffi: add safe thread storage wrapper
Add a typed ThreadStorage<T> wrapper around the thread storage bindings. Ticket: OISF#8445
1 parent 02c14c9 commit a51e8b1

3 files changed

Lines changed: 225 additions & 12 deletions

File tree

doc/userguide/devguide/extending/threads.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,49 @@ registered. Registered callbacks are kept for the Suricata process lifetime.
3838
``ThreadVars`` carries a lifetime tied to the callback invocation, so the
3939
borrow checker prevents it from being stored beyond the call. Rust callbacks
4040
must not panic, as they are invoked across an FFI boundary.
41+
42+
Thread Storage
43+
==============
44+
45+
``thread::ThreadStorage<T>`` provides typed, per-thread storage backed by
46+
Suricata's thread storage API. Each registered slot holds an independent value
47+
of type ``T`` for every thread.
48+
49+
Register a slot once during initialization with
50+
``ThreadStorage::<T>::register``. Registration must happen before Suricata
51+
finalizes its storage registration, which is the case during plugin
52+
initialization.
53+
54+
.. code-block:: rust
55+
56+
use suricata_ffi::thread::{self, ThreadStorage, ThreadVars};
57+
58+
#[derive(Default)]
59+
struct ThreadState {
60+
flows: u64,
61+
}
62+
63+
fn register(storage: ThreadStorage<ThreadState>) -> Result<(), &'static str> {
64+
thread::register_init_callback(move |tv| on_thread_init(storage, tv))
65+
}
66+
67+
Values are owned by Suricata's thread storage and are dropped automatically when
68+
the thread's storage is freed.
69+
70+
Access the value for a thread through the ``ThreadVars`` wrapper. ``get`` takes
71+
``&ThreadVars`` and returns ``Option<&T>``. ``get_mut`` takes ``&mut
72+
ThreadVars`` and returns ``Option<&mut T>``. ``get_or_insert_with`` also takes
73+
``&mut ThreadVars`` and returns ``Result<&mut T, _>``, inserting a value
74+
produced by the closure if the slot is empty:
75+
76+
.. code-block:: rust
77+
78+
fn on_thread_init(storage: ThreadStorage<ThreadState>, tv: &mut ThreadVars) {
79+
let _ = storage.get_or_insert_with(tv, ThreadState::default);
80+
}
81+
82+
fn on_flow_init(storage: ThreadStorage<ThreadState>, tv: &mut ThreadVars) {
83+
if let Some(state) = storage.get_mut(tv) {
84+
state.flows += 1;
85+
}
86+
}

examples/plugins/rust/src/mod.rs

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,37 @@ use std::ptr::null_mut;
33
use suricata_ffi::eve::{self, SCJsonBuilder};
44
use suricata_ffi::flow;
55
use suricata_ffi::jsonbuilder::JsonBuilder;
6-
use suricata_ffi::thread::{self, ThreadVars};
7-
use suricata_ffi::{SCLogError, SCLogNotice};
6+
use suricata_ffi::thread::{self, ThreadStorage, ThreadVars};
7+
use suricata_ffi::{SCLogError, SCLogNotice, SCLogWarning};
88
use suricata_sys::sys::{self, Flow, Packet, SCEveRegisterCallback, SCPlugin};
99

10+
/// Per-thread state stored in Suricata thread storage.
11+
#[derive(Default)]
12+
struct ThreadState {
13+
flows: u64,
14+
}
15+
1016
unsafe extern "C" fn init() {
1117
suricata_ffi::plugin::init();
1218
SCLogNotice!("Initializing rust example plugin");
1319

20+
// Register per-thread storage once, then hand the (copyable) handle to the
21+
// callbacks that use it.
22+
let thread_storage = match ThreadStorage::<ThreadState>::register("rust-example-thread") {
23+
Ok(storage) => storage,
24+
Err(err) => {
25+
SCLogError!("Failed to register rust example thread storage: {}", err);
26+
return;
27+
}
28+
};
29+
1430
if let Err(err) = register_eve_callbacks() {
1531
SCLogError!("Failed to register rust example EVE callbacks: {}", err);
1632
}
17-
if let Err(err) = register_flow_callbacks() {
33+
if let Err(err) = register_flow_callbacks(thread_storage) {
1834
SCLogError!("Failed to register rust example flow callbacks: {}", err);
1935
}
20-
if let Err(err) = register_thread_callbacks() {
36+
if let Err(err) = register_thread_callbacks(thread_storage) {
2137
SCLogError!("Failed to register rust example thread callbacks: {}", err);
2238
}
2339
}
@@ -29,15 +45,15 @@ fn register_eve_callbacks() -> Result<(), &'static str> {
2945
eve::register_callback(log_eve_wrapped)
3046
}
3147

32-
fn register_flow_callbacks() -> Result<(), &'static str> {
33-
flow::register_init_callback(log_flow_init)?;
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))?;
3450
flow::register_update_callback(log_flow_update)?;
3551
flow::register_finish_callback(log_flow_finish)?;
3652
Ok(())
3753
}
3854

39-
pub fn register_thread_callbacks() -> Result<(), &'static str> {
40-
thread::register_init_callback(on_thread_init)
55+
fn register_thread_callbacks(storage: ThreadStorage<ThreadState>) -> Result<(), &'static str> {
56+
thread::register_init_callback(move |tv| on_thread_init(storage, tv))
4157
}
4258

4359
unsafe extern "C" fn log_eve_raw(
@@ -66,15 +82,39 @@ fn log_eve_wrapped(
6682
Ok(())
6783
}
6884

69-
fn on_thread_init(tv: &mut ThreadVars) {
85+
fn on_thread_init(storage: ThreadStorage<ThreadState>, tv: &mut ThreadVars) {
86+
// Initialize the per-thread storage for this thread.
87+
if let Err(err) = storage.get_or_insert_with(tv, ThreadState::default) {
88+
SCLogError!("failed to initialize rust example thread storage: {}", err);
89+
}
7090
SCLogNotice!(
7191
"rust example thread init callback: thread={:p}",
7292
tv.as_ptr()
7393
);
7494
}
7595

76-
fn log_flow_init(_tv: &mut ThreadVars, _f: *mut Flow, _p: *const Packet) {
77-
SCLogNotice!("rust example flow init callback: flow={:p}", _f);
96+
fn log_flow_init(
97+
storage: ThreadStorage<ThreadState>,
98+
tv: &mut ThreadVars,
99+
f: *mut Flow,
100+
_p: *const Packet,
101+
) {
102+
// Count flows seen by this thread using the per-thread storage.
103+
let flows = match storage.get_mut(tv) {
104+
Some(state) => {
105+
state.flows += 1;
106+
state.flows
107+
}
108+
None => {
109+
SCLogWarning!("rust example thread storage was not initialized");
110+
0
111+
}
112+
};
113+
SCLogNotice!(
114+
"rust example flow init callback: flow={:p}, thread_flows={}",
115+
f,
116+
flows
117+
);
78118
}
79119

80120
fn log_flow_update(_tv: &mut ThreadVars, _f: *mut Flow, _p: *mut Packet) {

rust/ffi/src/thread.rs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@
1515
* 02110-1301, USA.
1616
*/
1717

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

21-
use suricata_sys::sys::{self, SCThreadRegisterInitCallback};
22+
use suricata_sys::sys::{
23+
self, SCThreadGetStorageById, SCThreadRegisterInitCallback, SCThreadSetStorageById,
24+
SCThreadStorageId, SCThreadStorageRegister,
25+
};
2226

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

50177
/// Register a thread initialization callback.

0 commit comments

Comments
 (0)