Project

General

Profile

Bug #8797

Updated by Victor Julien 20 days ago

Reported by Communications Security Establishment (CSE): 

 <pre> 
 ## 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. 
 </pre>

Back