mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH bpf-next v2] selftests/bpf: add XDP test for per-flow LRU_HASH window updates
@ 2026-10-02  4:22 Anil Kaushik
  2026-10-02  5:14 ` bot+bpf-ci
  0 siblings, 1 reply; 2+ messages in thread
From: Anil Kaushik @ 2026-10-02  4:22 UTC (permalink / raw)
  To: ast, daniel, davem, kuba, hawk, john.fastabend, andrii, eddyz87,
	memxor, shuah
  Cc: sdf, ihor.solodrai, martin.lau, song, yonghong.song, jolsa, emil,
	linux-kernel, netdev, bpf, linux-kselftest, Anil Kaushik

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 <anilkaushikwireless@gmail.com>
---
v2:
 - CC the XDP / BPF-selftests maintainers and reviewers; v1 was sent to
   the lists only and received no review.
 - Rewrite the commit message to explain the coverage gap and intent.
 - No change to the test code.

v1: https://lore.kernel.org/netdev/20260917135433.2260048-1-anilkaushikwireless@gmail.com/

 .../selftests/bpf/prog_tests/xdp_lru_window.c | 168 ++++++++++++++++++
 .../selftests/bpf/progs/xdp_lru_window.c      |  81 +++++++++
 tools/testing/selftests/bpf/xdp_lru_window.h  |  28 +++
 3 files changed, 277 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..55175c216
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/xdp_lru_window.c
@@ -0,0 +1,168 @@
+// 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;
+}
+
+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;
+	int i, n;
+
+	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], sizeof(struct ipv4_packet), "len0");
+
+	n = AGGREGATION_WINDOW + 5;
+	if (inject(n - 1, DST_PORT))
+		return;
+	if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &st), "lookup wrap"))
+		return;
+	ASSERT_EQ(st.seq, n, "seq wrap");
+	for (i = 0; i < AGGREGATION_WINDOW; i++)
+		ASSERT_EQ(st.pkt_len[i], sizeof(struct ipv4_packet), "slot");
+}
+
+static void test_trunc(void)
+{
+	struct xdp_lru_window_state before, after;
+	struct xdp_lru_window_key key, next;
+	unsigned char short_pkt[sizeof(struct ethhdr)] = {};
+	int err, retval;
+
+	reset_map();
+	err = run_pkt(short_pkt, sizeof(short_pkt), &retval);
+	if (err)
+		return;
+	ASSERT_EQ(retval, XDP_PASS, "trunc retval");
+	err = bpf_map_get_next_key(map_fd, NULL, &next);
+	ASSERT_EQ(err, -ENOENT, "trunc no insert");
+
+	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(short_pkt, sizeof(short_pkt), &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


^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: [PATCH bpf-next v2] selftests/bpf: add XDP test for per-flow LRU_HASH window updates
  2026-10-02  4:22 [PATCH bpf-next v2] selftests/bpf: add XDP test for per-flow LRU_HASH window updates Anil Kaushik
@ 2026-10-02  5:14 ` bot+bpf-ci
  0 siblings, 0 replies; 2+ messages in thread
From: bot+bpf-ci @ 2026-10-02  5:14 UTC (permalink / raw)
  To: anilkaushikwireless, ast, daniel, davem, kuba, hawk,
	john.fastabend, andrii, eddyz87, memxor, shuah
  Cc: sdf, ihor.solodrai, martin.lau, song, yonghong.song, jolsa, emil,
	linux-kernel, netdev, bpf, linux-kselftest, anilkaushikwireless,
	ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
	ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 4402 bytes --]

> selftests/bpf: add XDP test for per-flow LRU_HASH window updates

TL;DR:
- Test issue: wrap subtest uses identical packet lengths, so it can't detect a
  wrong post-wrap index
- trunc subtest sends a non-IPv4 frame, so it likely never reaches the
  truncation checks

> 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 000000000000..55175c2165ec
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/xdp_lru_window.c
> @@ -0,0 +1,168 @@

[ ... ]

> +static void test_one_and_wrap(void)
> +{
> +	struct xdp_lru_window_state st;
> +	struct xdp_lru_window_key key;
> +	int i, n;
> +
> +	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], sizeof(struct ipv4_packet), "len0");
> +
> +	n = AGGREGATION_WINDOW + 5;
> +	if (inject(n - 1, DST_PORT))
> +		return;
> +	if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &key, &st), "lookup wrap"))
> +		return;
> +	ASSERT_EQ(st.seq, n, "seq wrap");
> +	for (i = 0; i < AGGREGATION_WINDOW; i++)
> +		ASSERT_EQ(st.pkt_len[i], sizeof(struct ipv4_packet), "slot");
> +}

Can the final "slot" loop detect a wrong post-wrap write?

Every packet sent by inject() is the same pkt_v4 copy, so every write stores
sizeof(struct ipv4_packet) (54).  After the first 50 packets, slots 0..49
already hold 54, and packets 51..55 write 54 into slots 0..4 again.  The
loop therefore checks nothing that was not already true before the index
wrapped.

The "seq wrap" check only shows the program did not take the
"idx >= AGGREGATION_WINDOW" early return.  A miscompiled modulo that
produced a wrong but in-range index would still pass.

The commit message says this subtest checks that "the bounded array wraps".
Would it make sense to send the post-wrap packets with a different length
(run_pkt() accepts any len >= ETH_HLEN, and the program records
data_end - data), then check that slots 0..4 hold the new length while
slots 5..49 keep the old one?

[ ... ]

> +static void test_trunc(void)
> +{
> +	struct xdp_lru_window_state before, after;
> +	struct xdp_lru_window_key key, next;
> +	unsigned char short_pkt[sizeof(struct ethhdr)] = {};
> +	int err, retval;
> +
> +	reset_map();
> +	err = run_pkt(short_pkt, sizeof(short_pkt), &retval);
> +	if (err)
> +		return;
> +	ASSERT_EQ(retval, XDP_PASS, "trunc retval");
> +	err = bpf_map_get_next_key(map_fd, NULL, &next);
> +	ASSERT_EQ(err, -ENOENT, "trunc no insert");

Does this subtest reach any of the program's truncation checks?

short_pkt is 14 zero bytes, so eth->h_proto is 0.  In
progs/xdp_lru_window.c the program passes the Ethernet bounds check and then
returns at:

    if (eth->h_proto != bpf_htons(ETH_P_IP))
            return XDP_PASS;

The IPv4 length check ((void *)(iph + 1) > data_end) and the TCP length
check ((void *)(th + 1) > data_end) never run, here or in any other
subtest.

The "trunc no insert" and "trunc no mutate" assertions would pass the same
way for a full-length non-IPv4 frame, so this exercises the ethertype
filter rather than truncated-packet handling.  The commit message describes
this subtest as checking that "a short (parse-failing) packet neither
inserts a new entry nor mutates an existing one".

Should short_pkt have h_proto set to htons(ETH_P_IP) so the IPv4 length check
is reached?  To also reach the TCP header check, a truncated copy of pkt_v4
of sizeof(struct ethhdr) + sizeof(struct iphdr) bytes could be sent.
bpf_prog_test_run_xdp() only requires data_size_in >= ETH_HLEN, so both
sizes are accepted.

> +
> +	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(short_pkt, sizeof(short_pkt), &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");
> +}

[ ... ]


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/36965595251

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-10-02  5:14 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-02  4:22 [PATCH bpf-next v2] selftests/bpf: add XDP test for per-flow LRU_HASH window updates Anil Kaushik
2026-10-02  5:14 ` bot+bpf-ci

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®