source/netmap: fix multi-slot packet over-read

A packet that netmap delivers across several ring slots was handed to the
rest of the code as the first slot buffer together with the summed length
of all the slots. When those slot buffers were not next to each other in
memory this read past the first buffer, which could crash the process or
leak bytes from unrelated buffers into inspection, pcap output and IPS or
tap forwarding.

Multi-slot packets are now copied slot by slot into a per-thread buffer so
the caller always gets one addressable frame, and such packets are always
copied into the Packet instead of being referenced zero-copy. Single-slot
packets keep the zero-copy path. The walk over the fragment slots is bounded
by the slots we own and the total length is capped, so a broken or over-long
chain is dropped as one unit and the ring keeps moving forward.

Adds unit tests that build a fake ring and cover single-slot, adjacent,
non-adjacent, ring-wrap, empty-fragment, over-size and never-ending chains.

Ticket: #8777
pull/16497/head
Samaresh Kumar Singh 2 months ago committed by Victor Julien
parent 77e980c783
commit e886d2d030

@ -1175,6 +1175,7 @@ EXTRA_DIST = \
tests/detect-template.c \
tests/detect-transform-pcrexform.c \
tests/detect-ttl.c \
tests/source-netmap.c \
tests/source-pcap.c \
tests/app-layer-htp-file.c \
tests/detect-engine-alert.c \

@ -150,6 +150,10 @@ typedef struct NetmapThreadVars_
int copy_mode;
ChecksumValidationMode checksum_mode;
/* buffer used to join a packet that is split across several netmap slots */
uint8_t *coalesce_buf;
uint32_t coalesce_size;
/* counters */
uint64_t pkts;
uint64_t bytes;
@ -526,6 +530,14 @@ static TmEcode ReceiveNetmapThreadInit(ThreadVars *tv, const void *initdata, voi
ntv->checksum_mode = aconf->in.checksum_mode;
ntv->copy_mode = aconf->in.copy_mode;
/* room for the biggest packet we can handle, used to join split packets */
ntv->coalesce_size = MAX_PAYLOAD_SIZE;
ntv->coalesce_buf = SCMalloc(ntv->coalesce_size);
if (unlikely(ntv->coalesce_buf == NULL)) {
SCLogError("Memory allocation failed");
goto error_ntv;
}
/* enable zero-copy mode for workers runmode */
char const *active_runmode = RunmodeGetActive();
if (strcmp("workers", active_runmode) == 0) {
@ -588,6 +600,7 @@ error_src:
NetmapClose(ntv->ifsrc);
error_ntv:
SCFree(ntv->coalesce_buf);
SCFree(ntv);
error:
@ -663,7 +676,9 @@ static void NetmapReleasePacket(Packet *p)
PacketFreeOrRelease(p);
}
static void NetmapProcessPacket(NetmapThreadVars *ntv, const struct nm_pkthdr *ph)
/* force_copy is set for packets joined in ntv->coalesce_buf. that buffer is
* reused for the next packet, so it cannot be handed out zero-copy. */
static void NetmapProcessPacket(NetmapThreadVars *ntv, const struct nm_pkthdr *ph, bool force_copy)
{
if (ntv->bpf_prog.bf_len) {
struct pcap_pkthdr pkthdr = { {0, 0}, ph->len, ph->len };
@ -684,7 +699,7 @@ static void NetmapProcessPacket(NetmapThreadVars *ntv, const struct nm_pkthdr *p
ntv->pkts++;
ntv->bytes += ph->len;
if (ntv->flags & NETMAP_FLAG_ZERO_COPY) {
if ((ntv->flags & NETMAP_FLAG_ZERO_COPY) && !force_copy) {
if (PacketSetData(p, (uint8_t *)ph->buf, ph->len) == -1) {
TmqhOutputPacketpool(ntv->tv, p);
return;
@ -706,41 +721,72 @@ static void NetmapProcessPacket(NetmapThreadVars *ntv, const struct nm_pkthdr *p
}
/* read the packet at ring->cur into hdr and move the ring past the slots it
* used. a packet can be split over several slots, so walk on until the slot
* that has NS_MOREFRAG cleared. */
static void NetmapReadOnePacket(struct netmap_ring *ring, struct nm_pkthdr *hdr)
* used. slot buffers are not always next to each other, so a packet spread
* over more than one slot is copied into ntv->coalesce_buf and hdr points
* there. returns false when the chain is broken or does not fit, the packet
* is dropped then. */
static bool NetmapReadOnePacket(
struct netmap_ring *ring, NetmapThreadVars *ntv, struct nm_pkthdr *hdr, bool *force_copy)
{
u_int i = ring->cur;
struct netmap_slot *slot = &ring->slot[i];
u_char *oldbuf;
memset(hdr, 0, sizeof(*hdr));
hdr->ts = ring->ts;
hdr->slot = slot;
oldbuf = hdr->buf = (u_char *)NETMAP_BUF(ring, slot->buf_idx);
hdr->buf = (u_char *)NETMAP_BUF(ring, slot->buf_idx);
hdr->len = hdr->caplen = slot->len;
*force_copy = false;
bool dropped = false;
if (slot->flags & NS_MOREFRAG) {
/* a packet cannot use more slots than we currently own */
uint32_t avail = nm_ring_space(ring);
uint32_t total = 0;
uint32_t nfrags = 0;
bool malformed = false;
/* copy the slots into one buffer. keep walking after a problem so the
* whole frame is consumed at once and the ring keeps moving. */
for (;;) {
uint32_t slen = slot->len;
if (!malformed) {
if (slen > ntv->coalesce_size - total) {
/* frame is bigger than we can hold */
malformed = true;
} else {
memcpy(ntv->coalesce_buf + total, (u_char *)NETMAP_BUF(ring, slot->buf_idx),
slen);
total += slen;
}
}
nfrags++;
if (!(slot->flags & NS_MOREFRAG))
break;
if (nfrags >= avail) {
/* the chain never ended inside the slots we own */
malformed = true;
break;
}
i = nm_ring_next(ring, i);
slot = &ring->slot[i];
}
/* loop through the ring slots to get packet data */
while (slot->flags & NS_MOREFRAG) {
/* packet can be fragmented across multiple slots, */
/* so loop until we find the slot with the flag */
/* cleared, signalling the end of the packet data. */
u_char *nbuf;
u_int oldlen = slot->len;
i = nm_ring_next(ring, i);
slot = &ring->slot[i];
hdr->len += slot->len;
nbuf = (u_char *)NETMAP_BUF(ring, slot->buf_idx);
if (oldbuf != NULL && nbuf - oldbuf == ring->nr_buf_size &&
oldlen == ring->nr_buf_size) {
hdr->caplen += slot->len;
oldbuf = nbuf;
if (malformed) {
ntv->drops++;
dropped = true;
} else {
oldbuf = NULL;
hdr->buf = ntv->coalesce_buf;
hdr->len = hdr->caplen = total;
*force_copy = true;
}
}
hdr->ts = ring->ts;
/* release the slots we walked back to the kernel */
ring->head = ring->cur = nm_ring_next(ring, i);
return !dropped;
}
/**
@ -751,13 +797,9 @@ static void NetmapReadOnePacket(struct netmap_ring *ring, struct nm_pkthdr *hdr)
*/
static TmEcode NetmapReadPackets(struct nmport_d *d, int cnt, NetmapThreadVars *ntv)
{
struct nm_pkthdr hdr;
int last_ring = d->last_rx_ring - d->first_rx_ring + 1;
int cur_ring, got = 0, cur_rx_ring = d->cur_rx_ring;
memset(&hdr, 0, sizeof(hdr));
hdr.flags = NM_MORE_PKTS;
if (cnt == 0)
cnt = -1;
@ -771,19 +813,16 @@ static TmEcode NetmapReadPackets(struct nmport_d *d, int cnt, NetmapThreadVars *
/* cycle through the non-empty ring slots to fetch their data */
for (; !nm_ring_empty(ring) && cnt != got; got++) {
if (hdr.buf) { /* from previous round */
NetmapProcessPacket(ntv, &hdr);
}
struct nm_pkthdr hdr;
bool force_copy;
d->cur_rx_ring = cur_rx_ring;
NetmapReadOnePacket(ring, &hdr);
if (NetmapReadOnePacket(ring, ntv, &hdr, &force_copy)) {
NetmapProcessPacket(ntv, &hdr, force_copy);
}
}
}
if (hdr.buf) { /* from previous round */
hdr.flags = 0;
NetmapProcessPacket(ntv, &hdr);
}
return got;
}
@ -899,6 +938,7 @@ static TmEcode ReceiveNetmapThreadDeinit(ThreadVars *tv, void *data)
SCBPFFree(&ntv->bpf_prog);
}
SCFree(ntv->coalesce_buf);
SCFree(ntv);
SCReturnInt(TM_ECODE_OK);
@ -965,6 +1005,10 @@ static TmEcode DecodeNetmapThreadDeinit(ThreadVars *tv, void *data)
SCReturnInt(TM_ECODE_OK);
}
#ifdef UNITTESTS
#include "tests/source-netmap.c"
#endif
/**
* \brief Registration Function for ReceiveNetmap.
*/
@ -977,6 +1021,9 @@ void TmModuleReceiveNetmapRegister(void)
tmm_modules[TMM_RECEIVENETMAP].ThreadDeinit = ReceiveNetmapThreadDeinit;
tmm_modules[TMM_RECEIVENETMAP].cap_flags = SC_CAP_NET_RAW;
tmm_modules[TMM_RECEIVENETMAP].flags = TM_FLAG_RECEIVE_TM;
#ifdef UNITTESTS
tmm_modules[TMM_RECEIVENETMAP].RegisterTests = NetmapRegisterTests;
#endif
}
/**

@ -0,0 +1,292 @@
/* Copyright (C) 2026 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#include "../suricata-common.h"
#include "../util-unittest.h"
/* build a fake rx ring in one block of memory. the ring header and its slots
* come first and the packet buffers follow, so NETMAP_BUF(ring, k) lands on
* buffer number k. num_slots and nr_buf_size are const in the real struct, so
* we write them through a plain pointer as the memory itself is not const. */
static struct netmap_ring *NetmapTestRingNew(uint32_t num_slots, uint32_t buf_size)
{
size_t slots_end =
offsetof(struct netmap_ring, slot) + (size_t)num_slots * sizeof(struct netmap_slot);
size_t total = slots_end + (size_t)num_slots * buf_size;
struct netmap_ring *ring = SCCalloc(1, total);
if (ring == NULL)
return NULL;
*(int64_t *)(uintptr_t)&ring->buf_ofs = (int64_t)slots_end;
*(uint32_t *)(uintptr_t)&ring->num_slots = num_slots;
*(uint32_t *)(uintptr_t)&ring->nr_buf_size = buf_size;
ring->head = ring->cur = 0;
/* mark all but one slot as owned by us so nm_ring_space is not zero */
ring->tail = num_slots ? num_slots - 1 : 0;
return ring;
}
/* point a slot at buffer buf_idx, set its length and flags, and fill the
* buffer with a byte so we can check the copy order. */
static void NetmapTestSetSlot(struct netmap_ring *ring, uint32_t slot_idx, uint32_t buf_idx,
uint16_t len, uint16_t flags, uint8_t fill)
{
ring->slot[slot_idx].buf_idx = buf_idx;
ring->slot[slot_idx].len = len;
ring->slot[slot_idx].flags = flags;
memset(NETMAP_BUF(ring, buf_idx), fill, len);
}
static void NetmapTestNtvInit(NetmapThreadVars *ntv, uint32_t coalesce_size)
{
memset(ntv, 0, sizeof(*ntv));
ntv->coalesce_size = coalesce_size;
ntv->coalesce_buf = SCCalloc(1, coalesce_size);
if (ntv->coalesce_buf == NULL)
return;
}
/* one slot, no fragments. stays as is and keeps pointing into the ring so
* zero-copy still works. */
static int NetmapReadSingleSlotTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(8, 2048);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, MAX_PAYLOAD_SIZE);
FAIL_IF_NULL(ntv.coalesce_buf);
NetmapTestSetSlot(ring, 0, 0, 100, 0, 0xAA);
struct nm_pkthdr hdr;
bool force_copy = true;
FAIL_IF_NOT(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF(force_copy);
FAIL_IF_NOT(hdr.len == 100);
FAIL_IF_NOT(hdr.buf == (u_char *)NETMAP_BUF(ring, 0));
FAIL_IF_NOT(ring->cur == 1);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
/* two fragments whose buffers sit next to each other. the whole frame must be
* rebuilt in the coalesce buffer. */
static int NetmapReadTwoSlotAdjacentTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(8, 2048);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, MAX_PAYLOAD_SIZE);
FAIL_IF_NULL(ntv.coalesce_buf);
NetmapTestSetSlot(ring, 0, 0, 60, NS_MOREFRAG, 0x11);
NetmapTestSetSlot(ring, 1, 1, 40, 0, 0x22);
struct nm_pkthdr hdr;
bool force_copy = false;
FAIL_IF_NOT(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF_NOT(force_copy);
FAIL_IF_NOT(hdr.len == 100);
FAIL_IF_NOT(hdr.buf == ntv.coalesce_buf);
for (uint32_t i = 0; i < 60; i++)
FAIL_IF_NOT(ntv.coalesce_buf[i] == 0x11);
for (uint32_t i = 60; i < 100; i++)
FAIL_IF_NOT(ntv.coalesce_buf[i] == 0x22);
FAIL_IF_NOT(ring->cur == 2);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
/* two fragments whose buffers are far apart. this is what the old code got
* wrong. both slots must still be copied in order. */
static int NetmapReadTwoSlotNonAdjacentTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(8, 2048);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, MAX_PAYLOAD_SIZE);
FAIL_IF_NULL(ntv.coalesce_buf);
NetmapTestSetSlot(ring, 0, 5, 60, NS_MOREFRAG, 0x33);
NetmapTestSetSlot(ring, 1, 2, 40, 0, 0x44);
struct nm_pkthdr hdr;
bool force_copy = false;
FAIL_IF_NOT(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF_NOT(force_copy);
FAIL_IF_NOT(hdr.len == 100);
FAIL_IF_NOT(hdr.buf == ntv.coalesce_buf);
for (uint32_t i = 0; i < 60; i++)
FAIL_IF_NOT(ntv.coalesce_buf[i] == 0x33);
for (uint32_t i = 60; i < 100; i++)
FAIL_IF_NOT(ntv.coalesce_buf[i] == 0x44);
FAIL_IF_NOT(ring->cur == 2);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
/* a two slot packet that wraps around the end of the ring (slot 3 then slot
* 0). the walk has to follow the wrap and rebuild the frame. */
static int NetmapReadRingWrapTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(4, 2048);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, MAX_PAYLOAD_SIZE);
FAIL_IF_NULL(ntv.coalesce_buf);
NetmapTestSetSlot(ring, 3, 3, 60, NS_MOREFRAG, 0x55);
NetmapTestSetSlot(ring, 0, 0, 40, 0, 0x66);
ring->head = ring->cur = 3;
/* two slots owned, 3 then 0 */
ring->tail = 1;
struct nm_pkthdr hdr;
bool force_copy = false;
FAIL_IF_NOT(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF_NOT(force_copy);
FAIL_IF_NOT(hdr.len == 100);
FAIL_IF_NOT(hdr.buf == ntv.coalesce_buf);
for (uint32_t i = 0; i < 60; i++)
FAIL_IF_NOT(ntv.coalesce_buf[i] == 0x55);
for (uint32_t i = 60; i < 100; i++)
FAIL_IF_NOT(ntv.coalesce_buf[i] == 0x66);
FAIL_IF_NOT(ring->cur == 1);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
/* an empty leading fragment must not break the copy of the next one. */
static int NetmapReadZeroLenFragTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(8, 2048);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, MAX_PAYLOAD_SIZE);
FAIL_IF_NULL(ntv.coalesce_buf);
NetmapTestSetSlot(ring, 0, 0, 0, NS_MOREFRAG, 0x00);
NetmapTestSetSlot(ring, 1, 1, 50, 0, 0x77);
struct nm_pkthdr hdr;
bool force_copy = false;
FAIL_IF_NOT(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF_NOT(force_copy);
FAIL_IF_NOT(hdr.len == 50);
for (uint32_t i = 0; i < 50; i++)
FAIL_IF_NOT(ntv.coalesce_buf[i] == 0x77);
FAIL_IF_NOT(ring->cur == 2);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
/* a frame bigger than the coalesce buffer must be dropped, not copied. */
static int NetmapReadOverSizeTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(8, 2048);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, 100);
FAIL_IF_NULL(ntv.coalesce_buf);
NetmapTestSetSlot(ring, 0, 0, 60, NS_MOREFRAG, 0x11);
NetmapTestSetSlot(ring, 1, 1, 60, 0, 0x22);
struct nm_pkthdr hdr;
bool force_copy = false;
FAIL_IF(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF_NOT(ntv.drops == 1);
FAIL_IF_NOT(ring->cur == 2);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
/* an oversize frame with fragments after the overflow point must be dropped
* whole, so the trailing slots are not read back as a bogus packet. */
static int NetmapReadOverSizeTrailingFragTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(8, 2048);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, 100);
FAIL_IF_NULL(ntv.coalesce_buf);
NetmapTestSetSlot(ring, 0, 0, 60, NS_MOREFRAG, 0x11);
NetmapTestSetSlot(ring, 1, 1, 60, NS_MOREFRAG, 0x22);
NetmapTestSetSlot(ring, 2, 2, 20, 0, 0x33);
struct nm_pkthdr hdr;
bool force_copy = false;
FAIL_IF(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF_NOT(ntv.drops == 1);
FAIL_IF_NOT(ring->cur == 3);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
/* a chain that never ends must be dropped once every slot we own has been
* looked at, and the ring must still move on so the reader cannot spin. */
static int NetmapReadTooManyFragsTest(void)
{
struct netmap_ring *ring = NetmapTestRingNew(8, 64);
FAIL_IF_NULL(ring);
NetmapThreadVars ntv;
NetmapTestNtvInit(&ntv, MAX_PAYLOAD_SIZE);
FAIL_IF_NULL(ntv.coalesce_buf);
for (uint32_t k = 0; k < 5; k++)
NetmapTestSetSlot(ring, k, k, 10, NS_MOREFRAG, (uint8_t)(0x10 + k));
ring->head = ring->cur = 0;
ring->tail = 5;
struct nm_pkthdr hdr;
bool force_copy = false;
FAIL_IF(NetmapReadOnePacket(ring, &ntv, &hdr, &force_copy));
FAIL_IF_NOT(ntv.drops == 1);
FAIL_IF_NOT(ring->cur == 5);
SCFree(ntv.coalesce_buf);
SCFree(ring);
PASS;
}
static void NetmapRegisterTests(void)
{
UtRegisterTest("NetmapReadSingleSlotTest", NetmapReadSingleSlotTest);
UtRegisterTest("NetmapReadTwoSlotAdjacentTest", NetmapReadTwoSlotAdjacentTest);
UtRegisterTest("NetmapReadTwoSlotNonAdjacentTest", NetmapReadTwoSlotNonAdjacentTest);
UtRegisterTest("NetmapReadRingWrapTest", NetmapReadRingWrapTest);
UtRegisterTest("NetmapReadZeroLenFragTest", NetmapReadZeroLenFragTest);
UtRegisterTest("NetmapReadOverSizeTest", NetmapReadOverSizeTest);
UtRegisterTest("NetmapReadOverSizeTrailingFragTest", NetmapReadOverSizeTrailingFragTest);
UtRegisterTest("NetmapReadTooManyFragsTest", NetmapReadTooManyFragsTest);
}
Loading…
Cancel
Save