* [PATCH bpf-next 1/2] bpf: Guard conntrack opts error writes
2026-06-16 5:42 [PATCH bpf-next 0/2] bpf: Guard conntrack opts error writes Yiyang Chen
@ 2026-06-16 5:42 ` Yiyang Chen
2026-06-16 5:42 ` [PATCH bpf-next 2/2] selftests/bpf: Cover small " Yiyang Chen
1 sibling, 0 replies; 4+ messages in thread
From: Yiyang Chen @ 2026-06-16 5:42 UTC (permalink / raw)
To: bpf, netfilter-devel
Cc: Yiyang Chen, pablo, fw, phil, davem, edumazet, kuba, pabeni,
horms, andrii, eddyz87, ast, daniel, memxor, martin.lau, song,
yonghong.song, jolsa, emil, shuah, kartikey406, coreteam, netdev,
linux-kernel, linux-kselftest
The conntrack lookup and allocation kfuncs take an opts pointer
together with an opts__sz argument. The verifier checks only the memory
range described by opts__sz, but the wrappers unconditionally write
opts->error whenever the internal lookup or allocation helper returns an
error.
For an invalid size smaller than the end of opts->error, that write can
land outside the verifier-checked range. Keep returning NULL for invalid
arguments, but only report the error through opts->error when the
supplied size includes the field.
This preserves error reporting for the supported 12-byte and 16-byte
layouts, and for other invalid sizes that still include opts->error.
Fixes: b4c2b9593a1c ("net/netfilter: Add unstable CT lookup helpers for XDP and TC-BPF")
Fixes: d7e79c97c00c ("net: netfilter: Add kfuncs to allocate and insert CT")
Signed-off-by: Yiyang Chen <chenyy23@mails.tsinghua.edu.cn>
---
net/netfilter/nf_conntrack_bpf.c | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/net/netfilter/nf_conntrack_bpf.c b/net/netfilter/nf_conntrack_bpf.c
index 40c261cd0af38..3c182024ec509 100644
--- a/net/netfilter/nf_conntrack_bpf.c
+++ b/net/netfilter/nf_conntrack_bpf.c
@@ -65,6 +65,11 @@ enum {
NF_BPF_CT_OPTS_SZ = 16,
};
+static bool bpf_ct_opts_has_error(u32 opts_len)
+{
+ return opts_len >= offsetofend(struct bpf_ct_opts, error);
+}
+
static int bpf_nf_ct_tuple_parse(struct bpf_sock_tuple *bpf_tuple,
u32 tuple_len, u8 protonum, u8 dir,
struct nf_conntrack_tuple *tuple)
@@ -298,7 +303,8 @@ bpf_xdp_ct_alloc(struct xdp_md *xdp_ctx, struct bpf_sock_tuple *bpf_tuple,
nfct = __bpf_nf_ct_alloc_entry(dev_net(ctx->rxq->dev), bpf_tuple, tuple__sz,
opts, opts__sz, 10);
if (IS_ERR(nfct)) {
- opts->error = PTR_ERR(nfct);
+ if (bpf_ct_opts_has_error(opts__sz))
+ opts->error = PTR_ERR(nfct);
return NULL;
}
@@ -332,7 +338,8 @@ bpf_xdp_ct_lookup(struct xdp_md *xdp_ctx, struct bpf_sock_tuple *bpf_tuple,
caller_net = dev_net(ctx->rxq->dev);
nfct = __bpf_nf_ct_lookup(caller_net, bpf_tuple, tuple__sz, opts, opts__sz);
if (IS_ERR(nfct)) {
- opts->error = PTR_ERR(nfct);
+ if (bpf_ct_opts_has_error(opts__sz))
+ opts->error = PTR_ERR(nfct);
return NULL;
}
return nfct;
@@ -364,7 +371,8 @@ bpf_skb_ct_alloc(struct __sk_buff *skb_ctx, struct bpf_sock_tuple *bpf_tuple,
net = skb->dev ? dev_net(skb->dev) : sock_net(skb->sk);
nfct = __bpf_nf_ct_alloc_entry(net, bpf_tuple, tuple__sz, opts, opts__sz, 10);
if (IS_ERR(nfct)) {
- opts->error = PTR_ERR(nfct);
+ if (bpf_ct_opts_has_error(opts__sz))
+ opts->error = PTR_ERR(nfct);
return NULL;
}
@@ -398,7 +406,8 @@ bpf_skb_ct_lookup(struct __sk_buff *skb_ctx, struct bpf_sock_tuple *bpf_tuple,
caller_net = skb->dev ? dev_net(skb->dev) : sock_net(skb->sk);
nfct = __bpf_nf_ct_lookup(caller_net, bpf_tuple, tuple__sz, opts, opts__sz);
if (IS_ERR(nfct)) {
- opts->error = PTR_ERR(nfct);
+ if (bpf_ct_opts_has_error(opts__sz))
+ opts->error = PTR_ERR(nfct);
return NULL;
}
return nfct;
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH bpf-next 2/2] selftests/bpf: Cover small conntrack opts error writes
2026-06-16 5:42 [PATCH bpf-next 0/2] bpf: Guard conntrack opts error writes Yiyang Chen
2026-06-16 5:42 ` [PATCH bpf-next 1/2] " Yiyang Chen
@ 2026-06-16 5:42 ` Yiyang Chen
2026-06-16 6:19 ` bot+bpf-ci
1 sibling, 1 reply; 4+ messages in thread
From: Yiyang Chen @ 2026-06-16 5:42 UTC (permalink / raw)
To: bpf, netfilter-devel
Cc: Yiyang Chen, pablo, fw, phil, davem, edumazet, kuba, pabeni,
horms, andrii, eddyz87, ast, daniel, memxor, martin.lau, song,
yonghong.song, jolsa, emil, shuah, kartikey406, coreteam, netdev,
linux-kernel, linux-kselftest
Add a conntrack kfunc regression check for opts__sz values that do not
cover opts->error. The BPF program initializes opts->error with a guard
value, calls the lookup and allocation kfuncs with opts__sz set to
sizeof(opts->netns_id), and verifies that the guard is still intact
after the kfunc returns NULL.
Without the conntrack wrapper guard, the kfunc error path overwrites
that guard with -EINVAL even though the verifier checked only the first
four bytes of the options object.
Signed-off-by: Yiyang Chen <chenyy23@mails.tsinghua.edu.cn>
---
.../testing/selftests/bpf/prog_tests/bpf_nf.c | 6 +++++
.../testing/selftests/bpf/progs/test_bpf_nf.c | 26 +++++++++++++++++++
2 files changed, 32 insertions(+)
diff --git a/tools/testing/selftests/bpf/prog_tests/bpf_nf.c b/tools/testing/selftests/bpf/prog_tests/bpf_nf.c
index b33dba4b126e2..14d4c1793aed5 100644
--- a/tools/testing/selftests/bpf/prog_tests/bpf_nf.c
+++ b/tools/testing/selftests/bpf/prog_tests/bpf_nf.c
@@ -5,6 +5,8 @@
#include "test_bpf_nf.skel.h"
#include "test_bpf_nf_fail.skel.h"
+#define CT_OPTS_ERROR_GUARD 0x12345678
+
static char log_buf[1024 * 1024];
struct {
@@ -119,6 +121,10 @@ static void test_bpf_nf_ct(int mode)
ASSERT_EQ(skel->bss->test_einval_reserved_new, -EINVAL, "Test EINVAL for reserved in new struct not set to 0");
ASSERT_EQ(skel->bss->test_einval_netns_id, -EINVAL, "Test EINVAL for netns_id < -1");
ASSERT_EQ(skel->bss->test_einval_len_opts, -EINVAL, "Test EINVAL for len__opts != NF_BPF_CT_OPTS_SZ");
+ ASSERT_EQ(skel->bss->test_einval_len_opts_small_lookup, CT_OPTS_ERROR_GUARD,
+ "Test no error write for lookup opts__sz before error field");
+ ASSERT_EQ(skel->bss->test_einval_len_opts_small_alloc, CT_OPTS_ERROR_GUARD,
+ "Test no error write for alloc opts__sz before error field");
ASSERT_EQ(skel->bss->test_eproto_l4proto, -EPROTO, "Test EPROTO for l4proto != TCP or UDP");
ASSERT_EQ(skel->bss->test_enonet_netns_id, -ENONET, "Test ENONET for bad but valid netns_id");
ASSERT_EQ(skel->bss->test_enoent_lookup, -ENOENT, "Test ENOENT for failed lookup");
diff --git a/tools/testing/selftests/bpf/progs/test_bpf_nf.c b/tools/testing/selftests/bpf/progs/test_bpf_nf.c
index 076fbf03a1268..df43649ecb785 100644
--- a/tools/testing/selftests/bpf/progs/test_bpf_nf.c
+++ b/tools/testing/selftests/bpf/progs/test_bpf_nf.c
@@ -10,6 +10,8 @@
#define EINVAL 22
#define ENOENT 2
+#define CT_OPTS_ERROR_GUARD 0x12345678
+
#define NF_CT_ZONE_DIR_ORIG (1 << IP_CT_DIR_ORIGINAL)
#define NF_CT_ZONE_DIR_REPL (1 << IP_CT_DIR_REPLY)
@@ -19,6 +21,8 @@ int test_einval_reserved = 0;
int test_einval_reserved_new = 0;
int test_einval_netns_id = 0;
int test_einval_len_opts = 0;
+int test_einval_len_opts_small_lookup = 0;
+int test_einval_len_opts_small_alloc = 0;
int test_eproto_l4proto = 0;
int test_enonet_netns_id = 0;
int test_enoent_lookup = 0;
@@ -124,6 +128,28 @@ nf_ct_test(struct nf_conn *(*lookup_fn)(void *, struct bpf_sock_tuple *, u32,
else
test_einval_len_opts = opts_def.error;
+ opts_def.error = CT_OPTS_ERROR_GUARD;
+ ct = lookup_fn(ctx, &bpf_tuple, sizeof(bpf_tuple.ipv4), &opts_def,
+ sizeof(opts_def.netns_id));
+ if (ct) {
+ bpf_ct_release(ct);
+ test_einval_len_opts_small_lookup = -EINVAL;
+ } else {
+ test_einval_len_opts_small_lookup = opts_def.error;
+ }
+
+ opts_def.error = CT_OPTS_ERROR_GUARD;
+ ct = alloc_fn(ctx, &bpf_tuple, sizeof(bpf_tuple.ipv4), &opts_def,
+ sizeof(opts_def.netns_id));
+ if (ct) {
+ ct = bpf_ct_insert_entry(ct);
+ if (ct)
+ bpf_ct_release(ct);
+ test_einval_len_opts_small_alloc = -EINVAL;
+ } else {
+ test_einval_len_opts_small_alloc = opts_def.error;
+ }
+
opts_def.l4proto = IPPROTO_ICMP;
ct = lookup_fn(ctx, &bpf_tuple, sizeof(bpf_tuple.ipv4), &opts_def,
sizeof(opts_def));
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread