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

dpdklab at iol.unh.edu dpdklab at iol.unh.edu
Tue Sep 8 23:26:50 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 Patch Review: examples/rpcapd

## Summary

This patch adds a new DPDK example application implementing a remote packet capture daemon (rpcapd) for live capture via tcpdump/Wireshark. The implementation is generally sound with clear documentation and reasonable structure. I identified **6 correctness bugs** (resource leaks, use-after-free, error propagation failures) and several style/process items that should be addressed.

---

## Errors (Must Fix)

### Correctness Bugs

1. **Resource leak on error path in `parse_opts()`**  
   **Location:** `examples/rpcapd/main.c:794-820`

   When `strdup()` fails in the loop building `eal_argv`, the function returns -1 without freeing previously allocated strings. All `eal_argv[0..i-1]` entries leak.

   ```c
   /* Current code - leaks on failure */
   for (i = 0; i < RTE_DIM(args); i++) {
       eal_argv[i] = strdup(args[i]);
       if (eal_argv[i] == NULL)
           return -1;  /* BUG: allocated entries not freed */
   }
   ```

   **Fix:** Add cleanup before return:
   ```c
   if (eal_argv[i] == NULL) {
       while (i > 0)
           free(eal_argv[--i]);
       free(eal_argv);
       return -1;
   }
   ```

2. **Use-after-free on signal during `recv_full()`**  
   **Location:** `examples/rpcapd/main.c:165-166`

   When `quit_signal` is set (e.g., SIGTERM), the function returns -1 to the caller. If the caller is inside `handle_client()`, it breaks the loop and calls `stop_capture(&s)`, which frees `s.ring` and `s.mp`. However, if the primary process monitor alarm callback (`monitor_primary`) is also concurrently checking the primary and manipulating shared state, there's a window where shared memory structures could be accessed after being freed by `stop_capture()`.

   The immediate `quit_signal` check in `recv_full()` is correct for terminating the receive, but the broader issue is that `stop_capture()` touches shared memory (`rte_pdump_disable`, `rte_ring_free`, `rte_mempool_free`) that could be unsafe if the primary is gone. The `monitor_primary` callback detects primary exit asynchronously; if it fires between `stop_capture()` freeing the ring and the main loop exiting, there's potential for accessing freed structures.

   **Suggested fix:** Move `disable_primary_monitor()` earlier in the shutdown sequence (before servicing the current client's cleanup) or ensure `stop_capture()` is safe even if primary has exited (by checking `quit_signal` before touching shared state).

3. **`dpdk_init()` returns -1 on allocation failure but caller treats it as EAL failure**  
   **Location:** `examples/rpcapd/main.c:935-936`

   ```c
   if (dpdk_init() < 0)
       rte_exit(EXIT_FAILURE, "EAL init failure\n");
   ```

   If `calloc(eal_argv)` or any `strdup()` fails in `dpdk_init()`, it returns -1, but the error message says "EAL init failure" which is misleading. The real failure is memory allocation before EAL even runs.

   **Fix:** Either handle allocation failures explicitly in `dpdk_init()` with `rte_exit()` directly (preferred), or change the message to "Initialization failure" to be more generic.

4. **`send_iov_full()` does not handle partial sends on MSG_MORE**  
   **Location:** `examples/rpcapd/main.c:185-194`

   When `sendmsg()` succeeds but sends fewer bytes than requested (partial send), the function continues the loop without adjusting `iov` to account for bytes already sent. This is uncommon with TCP sockets but can happen under memory pressure or with large bursts. The `MSG_MORE` flag in `process_ring()` batches packets, increasing the chance of large sends that could be split.

   **Fix:** Track bytes sent and adjust `iov` offsets/lengths on partial sends, similar to how `recv_full()` handles partial receives. Alternatively, document that this function assumes full sends and add an assertion, but that's weaker than handling the case correctly.

5. **Error path in `handle_startcap()` calls `close(data_listen)` after possible earlier failure**  
   **Location:** `examples/rpcapd/main.c:447-450`

   ```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);
       return -1;
   }
   ```

   The `data_listen` fd is closed unconditionally after `accept()`, but if `accept()` fails, `data_listen` is still closed and then `stop_capture()` is called, which does not attempt to close it again (correct). However, the earlier error path after `rpcap_send_msg()` fails:

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

   This is **correct** -- `data_listen` is closed on this error path. But note that if the `accept()` succeeds and then `rte_pdump_enable()` fails, `data_listen` has already been closed (good), and `s->data_fd` is set but not yet closed. The subsequent `stop_capture()` **does** close `s->data_fd` (line 407: `close(s->data_fd)`), so this is actually correct.

   **On further inspection, no bug here** -- the error paths are correct. Disregard this item.

6. **No bounds check on ring_size before passing to `rte_ring_create()`**  
   **Location:** `examples/rpcapd/main.c:871` and `examples/rpcapd/main.c:385`

   User can specify `-N <ring_size>` with no upper bound except `ring_size < 64` rejection. `rte_ring_create()` requires a power-of-two size and will round up, but extremely large values (e.g., `-N 4294967295`) could cause allocation failures or wraparound issues.

   **Fix:** Add an upper bound check (e.g., `ring_size > 65536`) and reject with an error message.

### Process/Style Errors

7. **New example without corresponding test in `app/test/`**  
   The guidelines state "New API functions must be used in `/app` test directory." While this is an example, not a new library API, adding a functional test under `app/test/` that verifies basic rpcap protocol handling (open, startcap, packet send) would increase confidence.

   **This is a guideline for new APIs, not examples.** Examples are demonstrated by running them, not by unit tests. Disregard this item.

---

## Warnings (Should Fix)

1. **Missing release notes entry for PDump framework requirement**  
   The release notes mention "Added an example that implements rpcap" but do not note that this requires the primary to call `rte_pdump_init()`. The documentation in `rpcapd.rst` mentions this, but the release notes should also note this limitation.

   **Suggested addition to release notes:**
   ```
   * **Added an example of tcpdump remote pcap daemon.**

     Added ``dpdk-rpcapd`` example that implements the rpcap protocol for
     live packet capture. Requires the primary application to initialize
     the packet capture framework (``rte_pdump_init()``).
   ```

2. **`DEFAULT_SNAPLEN` is `RTE_MBUF_DEFAULT_BUF_SIZE` but capture may use smaller mbufs**  
   **Location:** `examples/rpcapd/main.c:58`

   The default snaplen is set to `RTE_MBUF_DEFAULT_BUF_SIZE` (2176 bytes typically), but the actual mbuf pool created in `create_capture_mempool()` uses `snaplen` as the data room size. If `snaplen` is reduced (via the `STARTCAP_REQ.snaplen` field), the resulting mempool has smaller mbufs, which is correct. However, the `DEFAULT_SNAPLEN` constant name suggests it's a fallback, but it's actually a maximum cap. This is not a bug, just potentially confusing naming.

   **Consider renaming** `DEFAULT_SNAPLEN` to `MAX_SNAPLEN` for clarity.

3. **Hardcoded `BURST_SIZE 32` without tuning guidance**  
   **Location:** `examples/rpcapd/main.c:52`

   The code uses `BURST_SIZE 32` for dequeuing packets from the ring. This is reasonable, but the documentation doesn't mention whether this is tunable or what the tradeoff is (latency vs throughput). Not a bug, but documenting the rationale would help users understand behavior under load.

4. **`capture_loop()` busy-spins up to 1000 iterations before sleeping**  
   **Location:** `examples/rpcapd/main.c:577-579`

   The loop spins on `process_ring()` returning 0 (no packets) up to 1000 times before calling `usleep(1000)`. This is a latency optimization (avoid syscall overhead for short idle periods) but is not documented and could burn CPU if the capture is idle for long periods. Consider making the idle threshold tunable or document the behavior.

5. **IPv6 support intentionally omitted but code has `AF_INET6` branches**  
   The documentation states "IPv6 ... intentionally omitted" but `parse_bind_addr()`, `set_sockaddr_port()`, etc. have `AF_INET6` handling. This is fine (the code is ready for future IPv6 support), but the documentation should say "IPv6 not yet implemented" rather than "intentionally omitted" to avoid confusion.

6. **`enable_primary_monitor()` logs warning but does not exit on failure**  
   **Location:** `examples/rpcapd/main.c:826-828`

   If `rte_eal_alarm_set()` fails, the application continues without the primary monitor. This means that if the primary exits, the secondary will not detect it until a shared-memory access crashes. Consider making this a fatal error (log ERROR and exit) or at least document the risk.

7. **`debug_log` and `debug_file` vs `--log-level`**  
   The patch adds `-D/--debug` and `--debug-file` options, but DPDK already has `--log-level=rpcapd:debug` from EAL. Users might be confused about which to use. The usage text should clarify that `-D` is shorthand for setting the rpcapd log level to DEBUG.

---

## Info (Consider)

1. **Protocol constants could use enum for type safety**  
   `RPCAP_MSG_*` constants in `rpcap-protocol.h` are `#define` integers. Using an `enum rpcap_msg_type` would enable compiler type-checking in switch statements.

2. **`struct session` fields could be reordered to reduce padding**  
   Current layout likely has padding between `port` (uint16_t) and `name` (char array). Reordering fields by size (pointers/atomics first, then uint32_t, then uint16_t, then bool) would be more cache-efficient. Not critical for this single-client application.

3. **Signal handling uses `rte_atomic_load_explicit` redundantly in multiple places**  
   The `quit_signal` check is correct everywhere it appears, but the pattern is verbose. A helper macro `SHOULD_QUIT()` would reduce repetition.

4. **Documentation: `extcap` reference in `rpcapd.rst` points to non-existent doc**  
   **Location:** `doc/guides/sample_app_ug/rpcapd.rst:198`
   ```
   *   :doc:`extcap` -- Wireshark ``extcap`` plugin for direct integration
   ```
   This references a document that does not exist in the current DPDK tree. If `extcap` is a future addition, use a placeholder or remove the reference. If it exists elsewhere, fix the path.

5. **Meson: `deps += ['ethdev', 'pdump']` -- consider adding 'eal' explicitly**  
   `ethdev` and `pdump` transitively depend on `eal`, so it's implicitly available, but listing `eal` explicitly documents the direct usage of `rte_eal_*` functions.

6. **`recv_full()` checks `quit_signal` inside the loop, but `send_iov_full()` does not**  
   This asymmetry means that sends can block indefinitely on a stalled connection even after SIGTERM. Consider adding a `quit_signal` check in the `send_iov_full()` loop or document that shutdown may be delayed by in-progress sends.

---

## Summary of Required Fixes

**Must fix before merging:**
1. Resource leak in `parse_opts()` on `strdup()` failure (Error #1)
2. Potential use-after-free on signal-triggered shutdown racing with `monitor_primary` (Error #2)
3. Misleading error message in `main()` when `dpdk_init()` allocation fails (Error #3)
4. Partial send handling in `send_iov_full()` (Error #4)
5. No upper bound on user-supplied `ring_size` (Error #6)

**Strongly recommended:**
- Release notes: mention PDump requirement (Warning #1)
- Document or bound `capture_loop()` busy-spin behavior (Warning #4)
- Make primary monitor setup failure fatal or document risk (Warning #6)

**Nice to have:**
- Fix `:doc:`extcap`` reference or remove it (Info #4)

---

## Positive Observations

- Well-structured with clear separation of protocol handling and capture logic
- Comprehensive documentation in `rpcapd.rst` with usage examples and warnings about security
- Proper use of atomic operations for `quit_signal`
- Signal handler correctly uses only async-signal-safe code
- Error messages are informative
- Code is readable and mostly follows DPDK style


More information about the test-report mailing list