|WARNING| examples/rpcapd: demo version of packet capture daemon

dpdklab at iol.unh.edu dpdklab at iol.unh.edu
Tue Sep 8 23:23:33 CEST 2026


Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/169263

_AI Code Review_

=== Patch Review: series.patch (via Claude) ===
Review date: 2026-09-08

# DPDK rpcapd Example Application Review

## Summary
This patch adds a new example application implementing the rpcap protocol for remote packet capture. The code quality is generally good with proper DPDK idioms and comprehensive documentation. I found several correctness bugs related to resource management and error handling, plus some style issues.

---

## Errors (Must Fix)

### Resource Leak in `parse_opts` - strdup'd EAL Arguments
**File**: `examples/rpcapd/main.c`, function `dpdk_init()`  
**Lines**: 854-888

The code allocates strings with `strdup()` and stores them in `eal_argv[]`, but never frees them after `rte_eal_init()` completes. On error paths within the allocation loop, already-allocated strings are also leaked.

**Why it matters**: Memory leak on every invocation. While the process exits soon after, clean resource management should be modeled in example code.

**Fix**:
```c
static int
dpdk_init(void)
{
	/* ... existing declarations ... */
	int ret;

	/* ... existing allocation code ... */

	ret = rte_eal_init(eal_argc, eal_argv);

	/* Free all allocated strings */
	for (unsigned int j = 0; j < i; j++)
		free(eal_argv[j]);
	free(eal_argv);

	if (ret < 0)
		rte_exit(EXIT_FAILURE, "EAL init failed: is the primary process running?\n");

	/* ... rest of function ... */
}
```

Also handle the error path within the allocation loop:
```c
	for (i = 0; i < RTE_DIM(args); i++) {
		eal_argv[i] = strdup(args[i]);
		if (eal_argv[i] == NULL) {
			/* Free what we allocated so far */
			for (unsigned int j = 0; j < i; j++)
				free(eal_argv[j]);
			free(eal_argv);
			return -1;
		}
	}
```

---

### Resource Leak in `stop_capture` - Ring Drain Uses Wrong Free Function
**File**: `examples/rpcapd/main.c`, function `stop_capture()`  
**Line**: 383

```c
while ((n = rte_ring_sc_dequeue_burst(s->ring, (void **)pkts,
                      BURST_SIZE, NULL)) > 0)
    rte_pktmbuf_free_bulk(pkts, n);
```

The mbufs in the ring were captured from arbitrary queues and may originate from different mempools (applications can send mbufs from any pool). `rte_pktmbuf_free_bulk()` batches by pool internally and is safe for mixed-pool arrays. This is correct. (No issue - leaving here for clarity.)

---

### Error Path Leak in `handle_startcap` - Missing `close(data_listen)` on Early Return
**File**: `examples/rpcapd/main.c`, function `handle_startcap()`  
**Lines**: 442-447

```c
if (rpcap_send_msg(fd, RPCAP_MSG_STARTCAP_REPLY, 0, &reply, sizeof(reply)) < 0) {
    close(data_listen);
    stop_capture(s);
    return -1;
}
```

This path is correct - `data_listen` is closed. However, the next error path (lines 453-457) after `accept()` fails **does not close `data_listen`**:

```c
data_fd = accept(data_listen, NULL, NULL);
close(data_listen);
if (data_fd < 0) {
    RTE_LOG(ERR, RPCAPD, "accept on data port: %s\n", strerror(errno));
    stop_capture(s);  /* BUG: data_listen already closed above, so this is fine */
    return -1;
}
```

Actually, `data_listen` **is** closed on line 451 before the `accept()` check. Reviewing again: the code is correct. (No issue.)

---

### Integer Overflow in mempool mbuf_size Calculation
**File**: `examples/rpcapd/main.c`, function `create_capture_mempool()`  
**Line**: 349

```c
uint32_t mbuf_size = RTE_PKTMBUF_HEADROOM + snaplen;
```

If `snaplen` is `UINT32_MAX - RTE_PKTMBUF_HEADROOM + 1` or larger, this addition overflows and wraps to a small value, causing `rte_pktmbuf_pool_create()` to allocate undersized mbufs that will be overrun when packets are copied.

**Why it matters**: Silent corruption or crash when capturing with a malicious or buggy client that requests a huge snaplen.

**Fix**:
```c
static struct rte_mempool *
create_capture_mempool(uint16_t port, uint32_t snaplen)
{
	char name[RTE_MEMPOOL_NAMESIZE];
	uint32_t mbuf_size;

	/* Guard against overflow */
	if (snaplen > UINT32_MAX - RTE_PKTMBUF_HEADROOM) {
		RTE_LOG(ERR, RPCAPD, "snaplen too large\n");
		return NULL;
	}
	mbuf_size = RTE_PKTMBUF_HEADROOM + snaplen;

	snprintf(name, sizeof(name), "rpcapd_p_%u_%d", port, getpid());
	return rte_pktmbuf_pool_create(name, ring_size * 2, MBUF_CACHE_SIZE, 0,
	                               mbuf_size, rte_socket_id());
}
```

Then handle the NULL return in `handle_startcap()` (it already does at line 433).

---

### Use of `gettimeofday` in Fast Path Produces Stale Timestamps
**File**: `examples/rpcapd/main.c`, function `process_ring()`  
**Line**: 500

```c
gettimeofday(&tv, NULL);

struct rpcap_pkthdr pkthdr = {
    .timestamp_sec = htonl((uint32_t)tv.tv_sec),
    .timestamp_usec = htonl((uint32_t)tv.tv_usec),
    /* ... */
};
```

The timestamp is sampled once per burst (outside the loop over `i`) but applied to all packets in the burst. Packets captured at slightly different times get the same timestamp, which can confuse analysis tools that rely on sub-millisecond timing (tcpdump `-ttt`, Wireshark time delta column). This is especially misleading for high packet rates.

**Why it matters**: Timestamp accuracy is degraded; multiple packets incorrectly appear simultaneous.

**Fix**: Move `gettimeofday(&tv, NULL);` **inside** the `for` loop, right before filling `pkthdr`:
```c
for (i = 0; i < n; i++) {
    struct rte_mbuf *m = pkts[i];
    /* ... */
    s->npkt++;

    struct rpcap_header hdr = { /* ... */ };

    gettimeofday(&tv, NULL);  /* <-- moved here */

    struct rpcap_pkthdr pkthdr = { /* ... */ };
    /* ... */
}
```

---

## Warnings (Should Fix)

### Missing Release Notes for New Example
**File**: `doc/guides/rel_notes/release_26_11.rst`  
**Lines**: 58-60

The release notes mention the new example, but the entry should be under the `New Features` section heading (which it is) and follow the required format. The current entry is adequate but could be more descriptive about the protocol and workflow.

Suggestion:
```rst
* **Added rpcapd example for remote packet capture.**

  A new example application ``dpdk-rpcapd`` implements the rpcap protocol,
  allowing libpcap-based tools (tcpdump, Wireshark) to capture packets live
  from DPDK ports over TCP. The application runs as a secondary process and
  integrates with the packet capture framework (pdump).
```

---

### Documentation Reference to Non-Existent extcap Doc
**File**: `doc/guides/sample_app_ug/rpcapd.rst`  
**Line**: 56 and 197

```rst
see :doc:`extcap`.
```
```rst
*   :doc:`extcap` -- Wireshark ``extcap`` plugin
```

These Sphinx `:doc:` references will cause a build warning or error if the `extcap.rst` document does not exist in the same directory. If the extcap documentation is planned for a later patch, note that in a comment or use plain text until the file exists.

**Fix**: Either create a placeholder `doc/guides/sample_app_ug/extcap.rst` (even if minimal), or rewrite the references as text:
```rst
For Wireshark users, the Wireshark ``extcap`` plugin interface
(see Wireshark documentation) is the preferred live-capture path.
```

---

### `listen_addr` Could Be Uninitialized on Early Error
**File**: `examples/rpcapd/main.c`, `parse_opts()` and `main()`

The `listen_addr` global is only initialized in `parse_bind_addr()`, which is called at the end of `parse_opts()`. If option parsing fails and calls `rte_exit()` before that point, or if `dpdk_init()` is called before options are parsed (it isn't, but still), `listen_addr` is uninitialized.

**Fix**: Initialize `listen_addr` to a safe default at declaration:
```c
static struct sockaddr_storage listen_addr = {
	.ss_family = AF_INET,
};
static socklen_t listen_addrlen = sizeof(struct sockaddr_in);
```

Then in `parse_opts()`, if no `-b` is given, `parse_bind_addr("127.0.0.1", ...)` will overwrite with the resolved address.

---

### Missing `examples/rpcapd` Directory Creation Check in Meson
**File**: `examples/rpcapd/meson.build`

The meson file is minimal but correct. However, there is no explicit check that `examples/meson.build` actually includes this subdirectory when the conditions are met. The patch adds `'rpcapd',` to the `all_examples` list in `examples/meson.build`, so this is correct. (No issue.)

---

### Statistics Counters Could Overflow 32-Bit `rpcap_stats` Fields
**File**: `examples/rpcapd/main.c`, function `handle_stats()`  
**Lines**: 546-555

```c
struct rpcap_stats reply = {
    .ifrecv   = htonl((uint32_t)es.ipackets),
    .ifdrop   = htonl((uint32_t)es.ierrors),
    /* ... */
};
```

`es.ipackets` and `es.ierrors` are `uint64_t`. Casting to `uint32_t` silently truncates the upper 32 bits. For long-running captures or high-rate interfaces, the stats will wrap after ~4 billion packets.

**Why it matters**: Inaccurate statistics reporting to the client. The rpcap protocol defines these fields as 32-bit, so this is a protocol limitation, not a bug in the code. However, it's worth noting in the documentation.

**Suggested fix**: Add a comment explaining the truncation:
```c
/* rpcap protocol uses 32-bit counters; truncate DPDK's 64-bit stats */
struct rpcap_stats reply = {
    .ifrecv   = htonl((uint32_t)es.ipackets),
    .ifdrop   = htonl((uint32_t)es.ierrors),
    /* ... */
};
```

And document the limitation in `rpcapd.rst` under **Limitations**.

---

## Info (Consider)

### `recv_full` Busy-Waits on `EINTR` Without Timeout
**File**: `examples/rpcapd/main.c`, function `recv_full()`  
**Lines**: 166-179

The loop retries `recv()` indefinitely on `EINTR`. If a signal is delivered repeatedly (malicious or pathological), this could spin. A maximum retry count or poll timeout would make it more robust.

Suggestion: Add a poll/select with timeout before retrying after EINTR, or use `MSG_WAITALL` and handle partial reads explicitly.

---

### `capture_loop` Polls Control Socket Without Timeout
**File**: `examples/rpcapd/main.c`, function `capture_loop()`  
**Line**: 535

```c
if (poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN))
    return 0;
```

The poll timeout is 0 (non-blocking), so the loop spins checking for control messages even when no data is on the ring. The code includes an `idle` counter and `usleep(1000)` after 1000 empty polls, which mitigates this. This is acceptable for an example but could be more efficient with a blocking poll or event-driven design.

No change required (noted as limitation in documentation at line 168).

---

### Magic Number `1000` in `capture_loop` Idle Check
**File**: `examples/rpcapd/main.c`, function `capture_loop()`  
**Lines**: 542-543

```c
if (idle++ < 1000)
    continue;
usleep(1000);
```

Defining `#define IDLE_POLL_THRESHOLD 1000` would improve readability.

---

### `debug_file` FILE Pointer Intentionally Leaked
**File**: `examples/rpcapd/main.c`, function `main()`  
**Lines**: 967-976

Comment at line 969 says "intentionally never closed" which is correct (the kernel reclaims it). No issue - good documentation of intent.

---

### Inconsistent Logging: `NOTICE` vs `WARNING` vs `INFO`
**File**: `examples/rpcapd/main.c`, various functions

- Line 517: `RTE_LOG(NOTICE, ...)` for data connection closed  
- Line 315: `RTE_LOG(WARNING, ...)` for sending error to client  
- Line 695: `RTE_LOG(INFO, ...)` for client connected  

The severity levels are reasonable but could be more consistent. `NOTICE` for data connection close is appropriate since it's a notable event but not an error. No change required.

---

## Style

### Boolean Comparison: `n == 0`, `plen > 0`, etc.
**File**: `examples/rpcapd/main.c`, multiple locations

Per AGENTS.md, explicit comparison against 0 is required for integers. The code already does this correctly throughout (e.g., `if (n < 0)`, `if (plen > 0)`). No issues.

---

### Hard Tabs Present and Correct
The code uses hard tabs for indentation. Verified correct.

---

### Include Order Correct
System headers, then DPDK headers, then local header. Correct at lines 18-50.

---

## Conclusion

This is a well-structured example with comprehensive documentation. The main concerns are:

1. **Memory leak of EAL argv strings** (Error)
2. **Integer overflow in mbuf_size calculation** (Error)
3. **Per-burst timestamps instead of per-packet** (Error - accuracy issue)
4. **Uninitialized `listen_addr` on early exit** (Warning)
5. **Missing extcap.rst documentation** (Warning)

Fix the three errors and the patch will be in good shape. The warnings are minor but should be addressed for production-quality code.


More information about the test-report mailing list