Skip to contentSkip to conversation

net/pcap: Rx timestamp overwrites mbuf buf_addr in secondary process

DPDK/ethdev Bug 2082 [email protected] 1 participant 1 message Oct 27

lore
Share & tools
lore
#0 lore raw

https://bugs.dpdk.org/show_bug.cgi?id=2082

            Bug ID: 2082
           Summary: net/pcap: Rx timestamp overwrites mbuf buf_addr in
                    secondary process
           Product: DPDK
           Version: 26.11
          Hardware: All
                OS: All
            Status: UNCONFIRMED
          Severity: normal
          Priority: Normal
         Component: ethdev
          Assignee: [email protected]
          Reporter: [email protected]

Target Milestone: ---

Created attachment 430
--> https://bugs.dpdk.org/attachment.cgi?id=430&action=edit
AI proposed patch

============================================================================
ANT-2026-80N5ICNL [NOT SECURITY: ordinary bug]

net/pcap: Rx timestamp overwrites mbuf buf_addr in secondary process
Location: drivers/net/pcap/pcap_ethdev.c:423  in eth_pcap_rx()
Commit:   9a45341f18a1ab47af7123e63d942b90c1b79f38

============================================================================
Since v26.07, a DPDK secondary process that receives on a net_pcap port with the Rx timestamp offload enabled crashes on the first packets it receives. In the secondary, eth_pcap_rx() writes the 64-bit timestamp one byte before each mbuf instead of into the timestamp dynamic field. This overwrites the low seven bytes of buf_addr, so the next read or write of that mbuf's packet data goes to timestamp >> 8.

The cause is that timestamp_dynfield_offset and timestamp_rx_dynflag in drivers/net/pcap/pcap_ethdev.c are per-process statics, starting at -1 and 0. They are only filled in by eth_dev_configure(), which runs in the primary. The timestamp_offloading switch, by contrast, is copied into struct pcap_rx_queue in shared dev_private. A secondary therefore sees the primary's true and its own offset of -1.

This is a regression from 32fd14bf07f8 ("net/pcap: support nanosecond timestamp precision"). Before that commit the field was registered in pmd_pcap_probe() in every process. Only v26.07 and 26.11-rc1 are affected; 25.11 and earlier are not.

The attached reproducer runs a primary dpdk-testpmd with --enable-rx-timestamp on an rx_pcap= port and an rxonly secondary. The secondary segfaults on its second Rx burst, writing at the capture's timestamp >> 8 plus the 128-byte headroom, in the rte_memcpy() at line 392. Without the offload, the same pair runs cleanly. A live iface= port fails the same way on ordinary traffic. If a capture timestamp happens to decode to a mapped address, the next frame is silently written there instead of crashing. Single-process applications and Tx-only secondaries are not affected.

A patch is attached. It moves the offset and flag into pmd_internals and the Rx queue, alongside timestamp_offloading.

This is an ordinary regression, not a security issue: it fires on every packet a secondary receives with the offload on,
whatever the packet contains, so the affected configuration has not worked at all since v26.07, and nobody who can send
traffic or supply a capture gets anything beyond what the first ordinary packet already does.

## Details

In a secondary process, eth_pcap_rx() stores the Rx timestamp at (char *)mbuf - 1 instead of in the timestamp dynamic
field. The 8-byte store replaces the low seven bytes of mbuf->buf_addr with the timestamp.

Shown with two dpdk-testpmd processes from the ASan build and a capture file whose 64 records all carry the timestamp
0x414141414100 << 8 ns:

```
primary:    dpdk-testpmd -l 0-1 --no-pci --proc-type=primary --vdev
net_pcap0,rx_pcap=in.pcap \
                -- -i --total-num-mbufs=4096 --enable-rx-timestamp
secondary:  dpdk-testpmd -l 0-1 --no-pci --proc-type=secondary -- -i
--forward-mode=rxonly --no-flush-rx
            testpmd> start
```

(The reproducer adds --file-prefix, --huge-dir and, for ASan, --base-virtaddr to both.)
The secondary dies in its second Rx burst with an ASan SEGV on unknown address 0x414141414180, a WRITE, in the
rte_memcpy() at drivers/net/pcap/pcap_ethdev.c:392. That address is the timestamp shifted right by 8, plus the 128 bytes
of headroom. Without --enable-rx-timestamp the same pair receives the 64 frames and exits normally. The default (gcc,
non-ASan) build behaves the same way: the kernel logs segfault at 414141414180.

The offset and the flag of the dynamic field are static variables, so every process has its own copy, and the copy starts
at -1:

```c
/* drivers/net/pcap/pcap_ethdev.c:63 */
static uint64_t timestamp_rx_dynflag;
static int timestamp_dynfield_offset = -1;
```

The only place that fills them in is eth_dev_configure(). The port is configured by the primary; a secondary attaches
to it and never runs this code:

```c
/* drivers/net/pcap/pcap_ethdev.c:1028 */
        internals->timestamp_offloading = !!(rxmode->offloads &
RTE_ETH_RX_OFFLOAD_TIMESTAMP);

        if (internals->timestamp_offloading && timestamp_rx_dynflag == 0) {
                int ret =
rte_mbuf_dyn_rx_timestamp_register(&timestamp_dynfield_offset,
                                                            
&timestamp_rx_dynflag);
```

The switch that enables the store is not per process. eth_rx_queue_setup() copies it into the Rx queue
(pcap_q->timestamp_offloading = internals->timestamp_offloading, line 1253), and struct pcap_rx_queue is part of
dev_private, which is shared memory. The secondary branch of pmd_pcap_probe() (lines 2061-2096) attaches to the port
and installs eth_pcap_rx as rx_pkt_burst (line 2088) without touching the two statics. So the Rx function in a
secondary sees the primary's true and its own -1:

```c
/* drivers/net/pcap/pcap_ethdev.c:414 */
                if (pcap_q->timestamp_offloading) {                      /*
shared: set by the primary */
                        …
                        uint64_t ns = (uint64_t)header->ts.tv_sec *
NSEC_PER_SEC
                                +  header->ts.tv_usec;

                        *RTE_MBUF_DYNFIELD(mbuf, timestamp_dynfield_offset,  /*
per process: still -1 */
                                           rte_mbuf_timestamp_t *) = ns;

                        mbuf->ol_flags |= timestamp_rx_dynflag;              /*
per process: still 0 */
```

RTE_MBUF_DYNFIELD(m, offset, type) is (type)((uintptr_t)(m) + (offset)) (lib/mbuf/rte_mbuf_dyn.h:228) and buf_addr
is the first member of struct rte_mbuf. The low byte of ns lands in the last byte of the
struct rte_mempool_objhdr in front of the mbuf (the top byte of its iova). The other seven bytes become bytes 0-6 of
buf_addr; its top byte is zero already, so buf_addr == ns >> 8.

Nothing resets buf_addr when a direct mbuf is freed and allocated again (it is set once, in rte_pktmbuf_init()), so the
damage stays with the mbuf in the shared pool. In the example, burst 1 receives 32 frames: each is copied into a good
mbuf at line 392 and then the store at line 423 sets that mbuf's buf_addr to 0x414141414100. rxonly frees the
32 mbufs, burst 2 gets them back from the lcore cache, and the copy at line 392 now writes the next frame to
0x414141414100 + RTE_PKTMBUF_HEADROOM. An application that looks at the packet does not get that far: with
set verbose 1 in the secondary, testpmd faults on the first burst, reading the Ethernet header at 0x41414141418c.

This is a regression from 32fd14bf07f8 ("net/pcap: support nanosecond timestamp precision"), first released in
v26.07. Before it, pmd_pcap_probe() registered the field ahead of the primary/secondary branch, in every process, so
only v26.07 and 26.11-rc1 are affected; 25.11 and earlier are not.

## Impact

Anyone who receives on a net_pcap port from a secondary process with the Rx timestamp offload on hits this, on the first
packets, every time: since v26.07 that combination does not work. The configuration is a supported one
(Multiprocess aware = Y in doc/guides/nics/features/pcap.ini) but needs two non-default choices: the primary enables RTE_ETH_RX_OFFLOAD_TIMESTAMP on a net_pcap port, and a secondary process calls rte_eth_rx_burst() on it. Every packet
received that way leaves one mbuf of the shared pool with a wrong buf_addr, whatever the packet contains.

* Live interface (iface=, rx_iface=): the timestamp is the kernel's wall-clock time, which today gives a non-canonical
address. The secondary crashes when the application first reads packet data,
or when the driver first reuses an mbuf.
With iface=lo and the testpmd pair above, three UDP datagrams sent to
127.0.0.1 end the secondary with a SEGV in the
same rte_memcpy().
* Capture file (rx_pcap=): the record timestamp selects the address and the following frame supplies the bytes. A
capture with ordinary timestamps crashes the secondary the same way (a 2026
timestamp faults at 0x18e2040019a2cd).
A timestamp that happens to decode to a mapped address gives no crash: the
next frame is silently copied there,
corrupting whatever that memory holds.

A single-process application is not affected (the same capture with the same offload in one testpmd process is received
with the correct timestamp on all 64 packets), nor is a secondary that only transmits.

Signature · 2 lines
You are receiving this mail because:
You are the assignee for the bug.