Project

General

Profile

Actions

Bug #9245

open
MK

SCFlowRunFinishCallbacks() not invoked when FlowWorker evicts a flow locally (CheckWorkQueue), unlike FlowRecycler and the emergency flow-reuse path

Bug #9245: SCFlowRunFinishCallbacks() not invoked when FlowWorker evicts a flow locally (CheckWorkQueue), unlike FlowRecycler and the emergency flow-reuse path

Added by Marc Kührer 4 days ago.

Status:
New
Priority:
Normal
Assignee:
-
Target version:
Affected Versions:
Effort:
low
Difficulty:
Label:
C

Description

Suricata 8.x provides a flow finish-callback API (src/flow-callbacks.h,
SCFlowRegisterFinishCallback / SCFlowRunFinishCallbacks) that third-party plugins
can use to run code when a flow is finished and about to be freed. Per the commit
that introduced it (a6fc37c90, tickets #7319/#7320), this exists specifically so
plugins such as other DPI libraries like nDPI can hook into flow lifecycle events
-- e.g. to release per-flow state they allocated in the init/update callbacks, or
to emit a final per-flow record. There are three places in the engine that free a
flow during normal operation, and only two of them invoke this callback:

1. Recycler() in src/flow-manager.c (the central FlowRecycler thread, handling
flows that time out normally) correctly calls SCFlowRunFinishCallbacks(tv, f)
immediately before FlowClearMemory(f, f->protomap):

static void Recycler(ThreadVars *tv, FlowRecyclerThreadData *ftd, Flow *f)
{
...
SCFlowRunFinishCallbacks(tv, f);
FlowClearMemory(f, f->protomap);
...
}

2. The emergency/memcap-pressure flow-reuse path in src/flow-hash.c likewise
calls SCFlowRunFinishCallbacks(tv, f) immediately before FlowClearMemory.

3. CheckWorkQueue() in src/flow-worker.c — the FlowWorker thread's local-eviction
path, which lets a flow that completes its lifecycle while being processed
inline by a worker thread skip the central FlowRecycler entirely (a
performance optimization) — does NOT call SCFlowRunFinishCallbacks anywhere.
It logs the flow and frees it directly:

static void CheckWorkQueue(ThreadVars *tv, FlowWorkerThreadData *fw, ...)
{
...
if (fw->output_thread_flow != NULL)
(void)OutputFlowLog(tv, fw->output_thread_flow, f);
FlowEndCountersUpdate(tv, &fw->fec, f);
...
FlowClearMemory (f, f->protomap);
FLOWLOCK_UNLOCK(f);
...
}

Because CheckWorkQueue's local-eviction path exists specifically so that most
cleanly-closing flows can bypass the central FlowRecycler for performance reasons,
this is not an edge case: in a typical deployment the majority of flows are
evicted this way. Any plugin that registers a finish callback via
SCFlowRegisterFinishCallback will silently never see the finish event for these
flows, only for the minority that time out via the central FlowRecycler or get
force-reclaimed under memory pressure in flow-hash.c.

This is a correctness/completeness bug, not a stability one -- in both correct
call sites the callback runs before FlowClearMemory, and CheckWorkQueue simply
omits the call rather than invoking it on freed memory, so there is no
use-after-free risk. The practical effect is silent: a plugin such as nDPI that
frees its own per-flow side-state in the finish callback would leak that state
for most flows; a plugin that emits a final per-flow record on finish would
simply never produce one for most flows. Neither failure raises an error, so it
is easy to miss in a low-churn test environment where flows mostly do time out
via the central FlowRecycler, while affecting nearly all flows under realistic,
high-churn production traffic.

Suggested fix: add the missing SCFlowRunFinishCallbacks(tv, f) call in
CheckWorkQueue(), mirroring Recycler() exactly — immediately before
FlowClearMemory(f, f->protomap).

How to reproduce:
- Build a small plugin that calls SCFlowRegisterFinishCallback() in SCPluginInit()
and increments a counter (or logs) on each invocation.
- Run Suricata 8.0.6 or 8.0.7 with the plugin loaded against a pcap/traffic where
TCP flows close gracefully (FIN/ACK) well before any configured flow timeout,
so they are evicted via CheckWorkQueue rather than the central FlowRecycler.
- Observe that the custom finish callback is not invoked for these flows, while
flows that are left to time out (handled by FlowRecycler) do trigger it.

Environment:
- Suricata 8.0.6 and 8.0.7 (verified against the pristine upstream release tags),
any run mode that uses FlowWorker-local flow eviction (default `workers`
run mode)

No data to display

Actions

Also available in: PDF Atom