diff --git a/src/Makefile.am b/src/Makefile.am index 7e27764a93..a606981fe1 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/source-netmap.c b/src/source-netmap.c index 08183d25a6..e2e67191ab 100644 --- a/src/source-netmap.c +++ b/src/source-netmap.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 } /** diff --git a/src/tests/source-netmap.c b/src/tests/source-netmap.c new file mode 100644 index 0000000000..46fd3e019f --- /dev/null +++ b/src/tests/source-netmap.c @@ -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); +}