[PATCH bpf-next v3] selftests/bpf: add XDP test for per-flow LRU_HASH window updates
HOTtoday
From: Anil Kaushik <hidden>
Date: 2026-10-02 05:38:47
Also in:
bpf, linux-kselftest, lkml
Subsystem:
bpf [general] (safe dynamic programs and tools), bpf [selftests] (test runners & infrastructure), kernel selftest framework, the rest, xdp (express data path) · Maintainers:
Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan, Shuah Khan, Linus Torvalds, David S. Miller, Jakub Kicinski, Jesper Dangaard Brouer, John Fastabend
The LRU_HASH selftests (test_lru_map) exercise BPF_MAP_TYPE_LRU_HASH
only from the syscall side. There is no coverage of an XDP program
updating an LRU_HASH map on the data path: inserting an entry with
BPF_NOEXIST on first sight of a flow, then updating a bounded array
inside the map value using a runtime (modulo) index.
That pattern is common in XDP flow-tracking programs and stresses two
things worth testing together: the verifier's bounds checking of a
value-internal array indexed by a runtime value, and per-flow key
isolation in an LRU map driven from XDP.
Add an XDP program that keys an LRU_HASH by the TCP/IPv4 5-tuple and
records packet lengths into value->pkt_len[seq % AGGREGATION_WINDOW],
plus a test_progs case driven by bpf_prog_test_run that checks:
- wrap: seq advances and the bounded array wraps, with the runtime
index accepted by the verifier;
- trunc: a short (parse-failing) packet neither inserts a new entry
nor mutates an existing one;
- isolate: two distinct 5-tuples get independent entries.
Selftest only; no kernel change.
Signed-off-by: Anil Kaushik <redacted>
---
v3:
- wrap: send the post-wrap packets with a different length and verify
only the wrapped slots change, so a wrong but in-range post-wrap
index is caught (BPF CI review).
- trunc: send well-formed but short frames to actually exercise the
IPv4 and TCP length checks, instead of a non-IPv4 frame that bailed
at the ethertype test (BPF CI review).
- No change to the BPF program, uapi or header.
v2: https://lore.kernel.org/netdev/20261002042201.3483076-1-anilkaushikwireless@gmail.com/ (local)
v1: https://lore.kernel.org/netdev/20260917135433.2260048-1-anilkaushikwireless@gmail.com/ (local)
.../selftests/bpf/prog_tests/xdp_lru_window.c | 216 ++++++++++++++++++
.../selftests/bpf/progs/xdp_lru_window.c | 81 +++++++
tools/testing/selftests/bpf/xdp_lru_window.h | 28 +++
3 files changed, 325 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/xdp_lru_window.c
create mode 100644 tools/testing/selftests/bpf/progs/xdp_lru_window.c
create mode 100644 tools/testing/selftests/bpf/xdp_lru_window.h
diff --git a/tools/testing/selftests/bpf/prog_tests/xdp_lru_window.c b/tools/testing/selftests/bpf/prog_tests/xdp_lru_window.c
new file mode 100644
index 000000000..5e353fb14
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/xdp_lru_window.c@@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <test_progs.h> +#include <network_helpers.h> +#include "xdp_lru_window.h" +#include "xdp_lru_window.skel.h" + +#define SRC_IP 0x0a000001 +#define DST_IP 0x0a000002 +#define SRC_PORT 12345 +#define DST_PORT 80 +#define ALT_DST_PORT 81 + +static struct xdp_lru_window *skel; +static int prog_fd, map_fd; + +static void fill_pkt(struct ipv4_packet *pkt, __u16 dport) +{ + *pkt = pkt_v4; + pkt->iph.saddr = htonl(SRC_IP); + pkt->iph.daddr = htonl(DST_IP); + pkt->tcp.source = htons(SRC_PORT); + pkt->tcp.dest = htons(dport); +} + +static void fill_key(struct xdp_lru_window_key *key, __u16 dport) +{ + memset(key, 0, sizeof(*key)); + key->saddr = htonl(SRC_IP); + key->daddr = htonl(DST_IP); + key->sport = htons(SRC_PORT); + key->dport = htons(dport); + key->proto = IPPROTO_TCP; +} + +static int run_pkt(const void *data, __u32 len, int *retval) +{ + LIBBPF_OPTS(bpf_test_run_opts, opts, + .data_in = data, + .data_size_in = len, + .repeat = 1, + ); + int err; + + err = bpf_prog_test_run_opts(prog_fd, &opts); + if (!ASSERT_OK(err, "test_run")) + return err; + if (retval) + *retval = opts.retval; + return 0; +} + +static int inject(int n, __u16 dport) +{ + struct ipv4_packet pkt; + int i, retval; + + fill_pkt(&pkt, dport); + for (i = 0; i < n; i++) { + if (run_pkt(&pkt, sizeof(pkt), &retval)) + return -1; + if (!ASSERT_EQ(retval, XDP_PASS, "retval")) + return -1; + } + return 0; +} + +/* Like inject(), but sends frames of a chosen on-wire length (>= the + * TCP/IPv4 headers) so the recorded pkt_len differs from the default. + */ +static int inject_len(int n, __u16 dport, __u32 len) +{ + unsigned char buf[sizeof(struct ipv4_packet) + 64] = {}; + struct ipv4_packet pkt; + int i, retval; + + fill_pkt(&pkt, dport); + memcpy(buf, &pkt, sizeof(pkt)); + if (len > sizeof(buf)) + len = sizeof(buf); + for (i = 0; i < n; i++) { + if (run_pkt(buf, len, &retval)) + return -1; + if (!ASSERT_EQ(retval, XDP_PASS, "retval")) + return -1; + } + return 0; +} + +static void reset_map(void) +{ + struct xdp_lru_window_key key, next; + int err; + + err = bpf_map_get_next_key(map_fd, NULL, &next); + while (!err) { + key = next; + err = bpf_map_get_next_key(map_fd, &key, &next); + bpf_map_delete_elem(map_fd, &key); + } +} + +static void test_one_and_wrap(void) +{ + struct xdp_lru_window_state st; + struct xdp_lru_window_key key; + __u32 base_len = sizeof(struct ipv4_packet); + __u32 new_len = base_len + 20; + int i; + + reset_map(); + if (inject(1, DST_PORT)) + return; + fill_key(&key, DST_PORT); + if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &st), "lookup")) + return; + ASSERT_EQ(st.seq, 1, "seq"); + ASSERT_EQ(st.pkt_len[0], base_len, "len0"); + + /* Fill the whole window with base-length packets (slots 0..W-1), + * then send 5 more of a different length so they wrap into slots + * 0..4. Distinct lengths let the checks below catch a wrong, but + * still in-range, post-wrap index. + */ + if (inject(AGGREGATION_WINDOW - 1, DST_PORT)) + return; + if (inject_len(5, DST_PORT, new_len)) + return; + if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &st), "lookup wrap")) + return; + ASSERT_EQ(st.seq, AGGREGATION_WINDOW + 5, "seq wrap"); + for (i = 0; i < 5; i++) + ASSERT_EQ(st.pkt_len[i], new_len, "wrapped slot"); + for (i = 5; i < AGGREGATION_WINDOW; i++) + ASSERT_EQ(st.pkt_len[i], base_len, "kept slot"); +} + +static void test_trunc(void) +{ + struct xdp_lru_window_state before, after; + struct xdp_lru_window_key key, next; + struct ipv4_packet pkt; + __u32 ip_trunc = sizeof(pkt_v4.eth); + __u32 tcp_trunc = sizeof(pkt_v4.eth) + sizeof(pkt_v4.iph); + int err, retval; + + fill_pkt(&pkt, DST_PORT); + reset_map(); + + /* Valid Ethernet/IP ethertype, but the IPv4 header is cut off: + * exercises the program's IPv4 length check. + */ + if (run_pkt(&pkt, ip_trunc, &retval)) + return; + ASSERT_EQ(retval, XDP_PASS, "ip trunc retval"); + err = bpf_map_get_next_key(map_fd, NULL, &next); + ASSERT_EQ(err, -ENOENT, "ip trunc no insert"); + + /* Full Ethernet + IPv4 header, but the TCP header is cut off: + * exercises the program's TCP length check. + */ + if (run_pkt(&pkt, tcp_trunc, &retval)) + return; + ASSERT_EQ(retval, XDP_PASS, "tcp trunc retval"); + err = bpf_map_get_next_key(map_fd, NULL, &next); + ASSERT_EQ(err, -ENOENT, "tcp trunc no insert"); + + /* A truncated packet must not mutate an already-tracked flow. */ + if (inject(1, DST_PORT)) + return; + fill_key(&key, DST_PORT); + if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &before), "setup")) + return; + if (run_pkt(&pkt, tcp_trunc, &retval)) + return; + ASSERT_EQ(retval, XDP_PASS, "trunc2 retval"); + if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &after), "after")) + return; + ASSERT_EQ(after.seq, before.seq, "trunc no mutate"); +} + +static void test_isolate(void) +{ + struct xdp_lru_window_state a, b; + struct xdp_lru_window_key key; + + reset_map(); + if (inject(2, DST_PORT) || inject(1, ALT_DST_PORT)) + return; + fill_key(&key, DST_PORT); + if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &a), "flow a")) + return; + fill_key(&key, ALT_DST_PORT); + if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &b), "flow b")) + return; + ASSERT_EQ(a.seq, 2, "seq a"); + ASSERT_EQ(b.seq, 1, "seq b"); +} + +void test_xdp_lru_window(void) +{ + skel = xdp_lru_window__open_and_load(); + if (!ASSERT_OK_PTR(skel, "open_and_load")) + return; + + prog_fd = bpf_program__fd(skel->progs.xdp_lru_window); + map_fd = bpf_map__fd(skel->maps.flow_table); + + if (test__start_subtest("wrap")) + test_one_and_wrap(); + if (test__start_subtest("trunc")) + test_trunc(); + if (test__start_subtest("isolate")) + test_isolate(); + + xdp_lru_window__destroy(skel); +}
diff --git a/tools/testing/selftests/bpf/progs/xdp_lru_window.c b/tools/testing/selftests/bpf/progs/xdp_lru_window.c
new file mode 100644
index 000000000..27aa1509e
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/xdp_lru_window.c@@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <vmlinux.h> +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_endian.h> +#include "xdp_lru_window.h" + +#ifndef ETH_P_IP +#define ETH_P_IP 0x0800 +#endif + +#ifndef EEXIST +#define EEXIST 17 +#endif + +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, XDP_LRU_WINDOW_FLOWS); + __type(key, struct xdp_lru_window_key); + __type(value, struct xdp_lru_window_state); +} flow_table SEC(".maps"); + +SEC("xdp") +int xdp_lru_window(struct xdp_md *ctx) +{ + void *data_end = (void *)(long)ctx->data_end; + void *data = (void *)(long)ctx->data; + struct xdp_lru_window_state init, *st; + struct xdp_lru_window_key key; + struct ethhdr *eth; + struct iphdr *iph; + struct tcphdr *th; + __u32 idx, pkt_len; + int err; + + eth = data; + if ((void *)(eth + 1) > data_end) + return XDP_PASS; + if (eth->h_proto != bpf_htons(ETH_P_IP)) + return XDP_PASS; + + iph = (void *)(eth + 1); + if ((void *)(iph + 1) > data_end) + return XDP_PASS; + if (iph->protocol != IPPROTO_TCP) + return XDP_PASS; + + th = (void *)(iph + 1); + if ((void *)(th + 1) > data_end) + return XDP_PASS; + + __builtin_memset(&key, 0, sizeof(key)); + key.saddr = iph->saddr; + key.daddr = iph->daddr; + key.sport = th->source; + key.dport = th->dest; + key.proto = iph->protocol; + pkt_len = data_end - data; + + st = bpf_map_lookup_elem(&flow_table, &key); + if (!st) { + __builtin_memset(&init, 0, sizeof(init)); + err = bpf_map_update_elem(&flow_table, &key, &init, + BPF_NOEXIST); + if (err && err != -EEXIST) + return XDP_PASS; + st = bpf_map_lookup_elem(&flow_table, &key); + if (!st) + return XDP_PASS; + } + + idx = st->seq % AGGREGATION_WINDOW; + barrier_var(idx); + if (idx >= AGGREGATION_WINDOW) + return XDP_PASS; + + st->pkt_len[idx] = pkt_len; + st->seq++; + return XDP_PASS; +} + +char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/xdp_lru_window.h b/tools/testing/selftests/bpf/xdp_lru_window.h
new file mode 100644
index 000000000..78434045c
--- /dev/null
+++ b/tools/testing/selftests/bpf/xdp_lru_window.h@@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __XDP_LRU_WINDOW_H +#define __XDP_LRU_WINDOW_H + +/* + * ABI for the XDP LRU rolling-window selftest. Existing test_lru_map + * coverage never enters XDP; XDP parse tests do not store a modulo + * index into an LRU map value. + */ + +#define AGGREGATION_WINDOW 50 +#define XDP_LRU_WINDOW_FLOWS 64 + +struct xdp_lru_window_key { + __be32 saddr; + __be32 daddr; + __be16 sport; + __be16 dport; + __u8 proto; + __u8 pad[3]; +}; + +struct xdp_lru_window_state { + __u32 seq; + __u32 pkt_len[AGGREGATION_WINDOW]; +}; + +#endif /* __XDP_LRU_WINDOW_H */
--
2.25.1