Actions
Bug #8797
open
VJ
CT
dag: Infinite loop in DAG record processing when rlen < dag_record_size
Bug #8797:
dag: Infinite loop in DAG record processing when rlen < dag_record_size
Affected Versions:
Effort:
Difficulty:
Label:
Description
## Summary
The Endace DAG capture source walks ERF records by advancing its read cursor by each record's self-reported `rlen` field, but never validates that `rlen` is at least the size of an ERF header. A record with `rlen == 0` (or any value below `dag_record_size`) causes `ProcessErfDagRecords()` to advance the cursor by zero bytes and re-evaluate the same record forever, pinning the capture thread at 100 % CPU and halting all further packet ingestion on that stream. The ERF header is written by trusted Endace hardware or, in soft-DAG `DAG_REVERSE_MODE`, by a co-located peer daemon with DAG-device write access — it is not attacker-controlled from the network — so the impact is a local denial of service / detection bypass rather than a remotely triggerable flaw.
## Affected Piece of Code
- **File:** `src/source-erf-dag.c`
- **Function / Location:** `ProcessErfDagRecords()` ~L380–446
- **Subsystem:** capture — Capture sources (af-packet, af-xdp, dpdk, netmap, nfq, pcap, etc.) and packet queue
```c
394 while (((top - ewtn->btm) >= dag_record_size) &&
395 ((processed + dag_record_size) < BYTES_PER_LOOP)) {
396
397 /* Make sure we have at least one packet in the packet pool,
398 * to prevent us from alloc'ing packets at line rate. */
399 PacketPoolWait();
400
401 prec = (char *)ewtn->btm;
402 dr = (dag_record_t*)prec;
403 rlen = SCNtohs(dr->rlen);
404 hdr_type = dr->type;
405
406 /* If we don't have enough data to finish processing this ERF
407 * record return and maybe next time we will.
408 */
409 if ((top - ewtn->btm) < rlen)
410 SCReturnInt(TM_ECODE_OK);
411
412 ewtn->btm += rlen;
413 processed += rlen;
414
415 /* Only support ethernet at this time. */
416 switch (hdr_type & 0x7f) {
417 case ERF_TYPE_PAD:
418 case ERF_TYPE_META:
419 /* Skip. */
420 continue;
```
## The Bug
`ProcessErfDagRecords()` iterates over the `[btm, top)` span of the DAG ring buffer that `dag_advance_stream()` exposed to the capture thread. On each iteration it reads the 16-byte ERF header at the current cursor, extracts the record length with `rlen = SCNtohs(dr->rlen)`, and advances with `ewtn->btm += rlen; processed += rlen;`. The only sanity guard is the partial-record check at L409, `if ((top - ewtn->btm) < rlen) return TM_ECODE_OK;`, whose purpose is to bail out when the tail of the buffer holds an incomplete record. That guard does **not** reject `rlen == 0`, nor any `rlen < dag_record_size`.
With `rlen == 0`:
- The partial-record guard evaluates `(top - btm) < 0`, which is false because the loop condition already established `(top - btm) >= dag_record_size > 0`.
- `ewtn->btm += 0` and `processed += 0` leave both the cursor and the byte budget unchanged.
- The `while` condition at L394, `((top - ewtn->btm) >= dag_record_size) && ((processed + dag_record_size) < BYTES_PER_LOOP)`, therefore remains permanently true (`processed` never grows toward the 4 MiB `BYTES_PER_LOOP` cap).
What happens inside the body depends on `dr->type`:
- For `ERF_TYPE_PAD` (0x30) or `ERF_TYPE_META` (0x1B) the switch at L416 hits the "Skip" arm and executes `continue`, immediately re-entering the loop on the same bytes.
- For `ERF_TYPE_ETH` / `ERF_TYPE_DSM_COLOR_ETH` / `ERF_TYPE_COLOR_ETH` / `ERF_TYPE_COLOR_HASH_ETH`, control falls through to `ProcessErfDagRecord()` (L453–524). There, `rlen == 0` makes `wlen` underflow but the function takes the "Incomplete frame captured." path at L481–484 and returns `TM_ECODE_OK` without consuming a packet — the outer loop spins again, additionally emitting one `SCLogInfo` line per iteration and flooding the log.
Either way the capture thread busy-loops forever. `PacketPoolWait()` at L399 does not throttle the spin because no packet is ever dequeued from the pool on this code path, so it returns immediately each iteration.
**Call chain to the vulnerable loop:**
`main()` → `RunModeDispatch()` (`src/runmodes.c`, `RUNMODE_DAG`) → `RunModeIdsErfDagSingle/AutoFp/Workers()` (`src/runmode-erf-dag.c:67–123`) registers a capture thread using the `"ReceiveErfDag"` module → `TmThreadsSlotPktAcqLoop()` (`src/tm-threads.c`) invokes the module's `PktAcqLoop` callback `ReceiveErfDagLoop()` (`src/source-erf-dag.c:310`) → `dag_advance_stream()` returns the `[btm, top)` span over the DAG ring buffer → `ProcessErfDagRecords()` (L380–446, the vulnerable loop) → optionally `ProcessErfDagRecord()` (L453–524).
**Trigger condition:** the 16-byte ERF header at `ewtn->btm` has `dr->rlen == 0x0000` (network byte order). The cleanest spin is with `dr->type` set to `ERF_TYPE_PAD` or `ERF_TYPE_META`, which keeps the loop entirely inside `ProcessErfDagRecords()`.
**Attack surface / trust boundary:** the bytes in the DAG stream buffer are *not* derived from on-the-wire packet payload; they are written by the Endace card's firmware/driver, or — when an odd-numbered stream is opened and `ReceiveErfDagThreadInit()` puts the stream into soft-DAG `DAG_REVERSE_MODE` (L242–249) — by an external co-located daemon that transmits into the stream via the DAG TX API. Such a peer already has write access to the DAG device. Consequently a remote network attacker cannot set `rlen`; the assumption that `rlen` is always sane is simply not enforced by code, and a buggy or malicious local component can wedge Suricata. The sibling ERF *file* reader already enforces this lower bound (`src/source-erf-file.c:171–176` rejects `rlen < sizeof(DagRecord)`), confirming that the missing check here is the defect rather than an upstream invariant.
**Vulnerability class:** infinite-loop (CWE-835).
## Reproduction Results
**Required configuration:** Suricata built with `--enable-dag` (`HAVE_DAG`) and started with `suricata --dag dagX:N` (`RUNMODE_DAG`). The trigger is not a network packet but a 16-byte ERF record placed in the DAG stream ring buffer that `dag_advance_stream()` exposes to `ReceiveErfDagLoop()`.
**Crafted ERF record** (offsets per `dag_record_t` / `DagRecord` layout in `src/source-erf-file.c:46–54` — `ts:8`, `type:1`, `flags:1`, `rlen:2 BE`, `lctr:2 BE`, `wlen:2 BE`):
```
00 00 00 00 00 00 00 00 30 00 00 00 00 00 00 00
└────────── ts ─────────┘ type flags rlen lctr wlen
(type 0x30 = ERF_TYPE_PAD, rlen = 0x0000)
```
**Steps:**
1. Configure a soft-DAG device with an odd-numbered stream so `ReceiveErfDagThreadInit()` puts it in `DAG_REVERSE_MODE` (L242–249), and run a peer daemon that transmits into that stream via the DAG TX API.
2. From the peer daemon, write the 16-byte record above into the stream so it appears at the read cursor returned by `dag_advance_stream()`.
3. Suricata's capture thread enters `ProcessErfDagRecords()`; on the first iteration it reads `rlen = 0`, advances `btm` by 0, matches `ERF_TYPE_PAD` → `continue`. The thread now busy-loops indefinitely; `PacketPoolWait()` at L399 returns immediately each iteration because no packet is ever dequeued on this path.
**Status:** *analytical only.* A live trigger was not constructed because exercising this path requires Endace DAG hardware or the proprietary `libdag` soft-DAG runtime; the code cannot be reached with a pcap file or a network packet. The analysis is corroborated by the fact that the equivalent ERF-file reader (`src/source-erf-file.c:171–176`) already carries the exact lower-bound check that is missing here.
## Severity
**LOW** — Denial of service. The `ReceiveErfDag` capture thread enters a tight infinite loop, pinning one CPU core at 100 % and permanently halting packet ingestion on that DAG stream, which amounts to a silent detection bypass for all subsequent traffic on the interface. If `dr->type` is an ETH type rather than PAD/META, every iteration additionally emits an `SCLogInfo("Incomplete frame captured.")` line, causing unbounded log/disk growth. There is no memory corruption or information disclosure. Exploitation requires control of a locally-trusted component — the Endace card firmware/driver, or a soft-DAG peer daemon that already has write access to the DAG device — and is not reachable from arbitrary network packets.
## Suggested Fix
Add a lower-bound check on `rlen` immediately after it is read, mirroring `src/source-erf-file.c:173`, so that a corrupt or short record terminates the stream instead of stalling it:
```diff
--- a/src/source-erf-dag.c
+++ b/src/source-erf-dag.c
@@ -403,6 +403,12 @@ ProcessErfDagRecords(ErfDagThreadVars *ewtn, uint8_t *top, uint32_t *pkts_read)
rlen = SCNtohs(dr->rlen);
hdr_type = dr->type;
+ if (rlen < dag_record_size) {
+ SCLogError("Bogus ERF record length %d (< dag_record_size) on stream: %d, DAG: %s",
+ rlen, ewtn->dagstream, ewtn->dagname);
+ SCReturnInt(TM_ECODE_FAILED);
+ }
+
/* If we don't have enough data to finish processing this ERF
* record return and maybe next time we will.
*/
```
Returning `TM_ECODE_FAILED` causes `ReceiveErfDagLoop()` (L359–363) to log the error, close the DAG stream and exit the thread, which is the existing failure-handling path for unrecoverable stream errors.
VJ Updated by Victor Julien about 10 hours ago
@sfd are you able to have a look at this?
SD Updated by Stephen Donnelly about 5 hours ago
Victor Julien wrote in #note-1:
@sfd are you able to have a look at this?
Can confirm, bug appears valid. Agree severity is Minor as this only affects live capture; reading from files is already safe.
This should be fixed; the recommended patch looks fine. Where should this be fixed: main, or main-8.0.x ? Branch main seems to have issues with StatsRegisterCounter etc in source-erf-dag.c
Shall I create a PR or will you handle it?
Actions