* [PATCH bpf 1/2] bpf, verifier: fold reg->var_off into PTR_TO_FLOW_KEYS bounds check
2026-06-04 15:07 [PATCH bpf 0/2] bpf, verifier: fix PTR_TO_FLOW_KEYS constant-offset OOB Nuiqi Gui
@ 2026-06-04 15:07 ` Nuiqi Gui
2026-06-04 15:40 ` sashiko-bot
2026-06-04 15:07 ` [PATCH bpf 2/2] selftests/bpf: add tests for PTR_TO_FLOW_KEYS constant offset bounds Nuiqi Gui
2026-06-04 16:40 ` [PATCH bpf 0/2] bpf, verifier: fix PTR_TO_FLOW_KEYS constant-offset OOB Alexei Starovoitov
2 siblings, 1 reply; 6+ messages in thread
From: Nuiqi Gui @ 2026-06-04 15:07 UTC (permalink / raw)
To: ast, daniel, andrii
Cc: Eduard Zingerman, Nuiqi Gui, John Fastabend, Martin KaFai Lau,
Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song, Jiri Olsa, bpf,
linux-kernel
Constant pointer arithmetic on a PTR_TO_FLOW_KEYS register lands the
constant in reg->var_off (e.g. flow_keys(imm=4096)), but the
PTR_TO_FLOW_KEYS path in check_mem_access() passes only insn->off to
check_flow_keys_access() and never folds reg->var_off.value. The
verifier therefore accepts an access that, at runtime, dereferences past
struct bpf_flow_keys -- a verifier/runtime divergence that yields an
out-of-bounds read and write of kernel stack memory.
Commit 022ac0750883 ("bpf: use reg->var_off instead of reg->off for
pointers") removed the generic "off += reg->off" that check_mem_access()
applied before the per-type dispatch and replaced it with per-path
folding of reg->var_off.value (for example the ctx path now folds the
register offset via check_ctx_access()). The PTR_TO_FLOW_KEYS path was
not given the equivalent fold, so a constant offset that used to be
folded and rejected is now silently accepted:
before 022ac0750883: the offset stays in reg->off and is folded
generically, so the access is checked with off=4096 and rejected.
after 022ac0750883: the offset lands in reg->var_off, the flow_keys
path checks off=0 and accepts; at runtime the access dereferences
base + 0x1000.
For a BPF_PROG_TYPE_FLOW_DISSECTOR program the following is accepted:
r2 = *(u64 *)(r1 + 144) ; R2=flow_keys (PTR_TO_FLOW_KEYS)
r2 += 0x1000 ; R2=flow_keys(imm=4096), accepted
r0 = *(u64 *)(r2 + 0) ; accepted, var_off.value=0x1000 ignored
while the equivalent insn->off form
r0 = *(u64 *)(r2 + 0x1000)
has the same effective offset but is correctly rejected with
"invalid access to flow keys off=4096 size=8", which isolates the defect
to the missing var_off fold. Once attached as a flow dissector, the
accepted program reads kernel stack past struct bpf_flow_keys (a
kernel-stack / KASLR information leak) and can likewise write past it,
corrupting kernel memory.
Fix it by folding reg->var_off.value into the offset before the bounds
check and rejecting non-constant offsets, mirroring the other pointer
types (e.g. check_ctx_access()).
No released kernel is affected; the regression is confined to the 7.1
development cycle (reproduced on v7.1-rc1..rc5), and v7.0.x rejects the
program above.
Fixes: 022ac0750883 ("bpf: use reg->var_off instead of reg->off for pointers")
Signed-off-by: Nuiqi Gui <gnq25@mails.tsinghua.edu.cn>
---
kernel/bpf/verifier.c | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 7fb88e1cd7c4..e8a8dd89af06 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4997,9 +4997,22 @@ static int check_ctx_access(struct bpf_verifier_env *env, int insn_idx, u32 regn
return err;
}
-static int check_flow_keys_access(struct bpf_verifier_env *env, int off,
- int size)
+static int check_flow_keys_access(struct bpf_verifier_env *env, u32 regno,
+ int off, int size)
{
+ struct bpf_reg_state *reg = reg_state(env, regno);
+
+ /* Only a constant offset is allowed here; fold it into off. */
+ if (!tnum_is_const(reg->var_off)) {
+ char tn_buf[48];
+
+ tnum_strn(tn_buf, sizeof(tn_buf), reg->var_off);
+ verbose(env, "R%d invalid variable offset to flow keys: off=%d, var_off=%s\n",
+ regno, off, tn_buf);
+ return -EACCES;
+ }
+ off += reg->var_off.value;
+
if (size < 0 || off < 0 ||
(u64)off + size > sizeof(struct bpf_flow_keys)) {
verbose(env, "invalid access to flow keys off=%d size=%d\n",
@@ -6514,7 +6527,7 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, u32 regn
return -EACCES;
}
- err = check_flow_keys_access(env, off, size);
+ err = check_flow_keys_access(env, regno, off, size);
if (!err && t == BPF_READ && value_regno >= 0)
mark_reg_unknown(env, regs, value_regno);
} else if (type_is_sk_pointer(reg->type)) {
--
2.34.1
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH bpf 2/2] selftests/bpf: add tests for PTR_TO_FLOW_KEYS constant offset bounds
2026-06-04 15:07 [PATCH bpf 0/2] bpf, verifier: fix PTR_TO_FLOW_KEYS constant-offset OOB Nuiqi Gui
2026-06-04 15:07 ` [PATCH bpf 1/2] bpf, verifier: fold reg->var_off into PTR_TO_FLOW_KEYS bounds check Nuiqi Gui
@ 2026-06-04 15:07 ` Nuiqi Gui
2026-06-04 15:45 ` sashiko-bot
2026-06-04 16:40 ` [PATCH bpf 0/2] bpf, verifier: fix PTR_TO_FLOW_KEYS constant-offset OOB Alexei Starovoitov
2 siblings, 1 reply; 6+ messages in thread
From: Nuiqi Gui @ 2026-06-04 15:07 UTC (permalink / raw)
To: ast, daniel, andrii
Cc: Eduard Zingerman, Nuiqi Gui, Martin KaFai Lau,
Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song, Jiri Olsa,
Shuah Khan, Shenghao Yuan, Matt Bobrowski, Yazhou Tang,
Emil Tsalapatis, linux-kernel, bpf, linux-kselftest
Add verifier tests covering constant pointer arithmetic on a
PTR_TO_FLOW_KEYS register, which regressed with commit 022ac0750883
("bpf: use reg->var_off instead of reg->off for pointers"): an
out-of-bounds offset introduced as flow_keys += K and then dereferenced
at insn->off 0 was accepted, while the equivalent flow_keys + K direct
offset was rejected.
The tests check that:
- in-bounds constant arithmetic on the keys pointer is still accepted,
- an out-of-bounds offset introduced via constant arithmetic is rejected
for both read and write, with the same diagnostic as the direct
insn->off form.
Signed-off-by: Nuiqi Gui <gnq25@mails.tsinghua.edu.cn>
---
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../selftests/bpf/progs/verifier_flow_keys.c | 77 +++++++++++++++++++
2 files changed, 79 insertions(+)
create mode 100644 tools/testing/selftests/bpf/progs/verifier_flow_keys.c
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index 06cd24e37b3f..feaee223689b 100644
--- a/tools/testing/selftests/bpf/prog_tests/verifier.c
+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
@@ -38,6 +38,7 @@
#include "verifier_div0.skel.h"
#include "verifier_div_mod_bounds.skel.h"
#include "verifier_div_overflow.skel.h"
+#include "verifier_flow_keys.skel.h"
#include "verifier_global_subprogs.skel.h"
#include "verifier_global_ptr_args.skel.h"
#include "verifier_gotol.skel.h"
@@ -187,6 +188,7 @@ void test_verifier_direct_stack_access_wraparound(void) { RUN(verifier_direct_st
void test_verifier_div0(void) { RUN(verifier_div0); }
void test_verifier_div_mod_bounds(void) { RUN(verifier_div_mod_bounds); }
void test_verifier_div_overflow(void) { RUN(verifier_div_overflow); }
+void test_verifier_flow_keys(void) { RUN(verifier_flow_keys); }
void test_verifier_global_subprogs(void) { RUN(verifier_global_subprogs); }
void test_verifier_global_ptr_args(void) { RUN(verifier_global_ptr_args); }
void test_verifier_gotol(void) { RUN(verifier_gotol); }
diff --git a/tools/testing/selftests/bpf/progs/verifier_flow_keys.c b/tools/testing/selftests/bpf/progs/verifier_flow_keys.c
new file mode 100644
index 000000000000..512e5d1d2665
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/verifier_flow_keys.c
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Constant-offset bounds checks for PTR_TO_FLOW_KEYS pointer arithmetic. */
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+
+/* sizeof(struct bpf_flow_keys) is well under 4096, so +0x1000 is OOB. */
+
+SEC("flow_dissector")
+__description("flow_keys: in-bounds constant pointer arithmetic accepted")
+__success
+__naked void flow_keys_const_inbounds(void)
+{
+ asm volatile (" \
+ r1 = *(u64 *)(r1 + %[flow_keys]); \
+ r1 += 8; \
+ r0 = *(u64 *)(r1 + 0); \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm_const(flow_keys, offsetof(struct __sk_buff, flow_keys))
+ : __clobber_all);
+}
+
+SEC("flow_dissector")
+__description("flow_keys: OOB via constant pointer arithmetic rejected")
+__failure __msg("invalid access to flow keys off=4096 size=8")
+__naked void flow_keys_const_oob_read(void)
+{
+ asm volatile (" \
+ r1 = *(u64 *)(r1 + %[flow_keys]); \
+ r1 += 4096; \
+ r0 = *(u64 *)(r1 + 0); \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm_const(flow_keys, offsetof(struct __sk_buff, flow_keys))
+ : __clobber_all);
+}
+
+SEC("flow_dissector")
+__description("flow_keys: OOB write via constant pointer arithmetic rejected")
+__failure __msg("invalid access to flow keys off=4096 size=8")
+__naked void flow_keys_const_oob_write(void)
+{
+ asm volatile (" \
+ r1 = *(u64 *)(r1 + %[flow_keys]); \
+ r1 += 4096; \
+ r2 = 0; \
+ *(u64 *)(r1 + 0) = r2; \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm_const(flow_keys, offsetof(struct __sk_buff, flow_keys))
+ : __clobber_all);
+}
+
+/* Equivalent OOB expressed directly in insn->off; this form was always
+ * rejected and is kept to show both forms now share one diagnostic.
+ */
+SEC("flow_dissector")
+__description("flow_keys: OOB via insn->off rejected")
+__failure __msg("invalid access to flow keys off=4096 size=8")
+__naked void flow_keys_insn_off_oob(void)
+{
+ asm volatile (" \
+ r1 = *(u64 *)(r1 + %[flow_keys]); \
+ r0 = *(u64 *)(r1 + 4096); \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm_const(flow_keys, offsetof(struct __sk_buff, flow_keys))
+ : __clobber_all);
+}
+
+char _license[] SEC("license") = "GPL";
--
2.34.1
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [PATCH bpf 0/2] bpf, verifier: fix PTR_TO_FLOW_KEYS constant-offset OOB
2026-06-04 15:07 [PATCH bpf 0/2] bpf, verifier: fix PTR_TO_FLOW_KEYS constant-offset OOB Nuiqi Gui
2026-06-04 15:07 ` [PATCH bpf 1/2] bpf, verifier: fold reg->var_off into PTR_TO_FLOW_KEYS bounds check Nuiqi Gui
2026-06-04 15:07 ` [PATCH bpf 2/2] selftests/bpf: add tests for PTR_TO_FLOW_KEYS constant offset bounds Nuiqi Gui
@ 2026-06-04 16:40 ` Alexei Starovoitov
2 siblings, 0 replies; 6+ messages in thread
From: Alexei Starovoitov @ 2026-06-04 16:40 UTC (permalink / raw)
To: Nuiqi Gui
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, bpf
On Thu, Jun 4, 2026 at 8:08 AM Nuiqi Gui <gnq25@mails.tsinghua.edu.cn> wrote:
>
> A constant offset added to a PTR_TO_FLOW_KEYS register lands in
> reg->var_off, but check_flow_keys_access() bounds-checks only insn->off
> and never folds reg->var_off.value. A BPF_PROG_TYPE_FLOW_DISSECTOR
> program can therefore do "flow_keys += 0x1000; *(flow_keys + 0)" and have
> it accepted, then read/write kernel stack past struct bpf_flow_keys at
> runtime. Patch 1 folds reg->var_off.value into the offset (and rejects
> non-constant offsets), mirroring check_ctx_access(); patch 2 adds verifier
> selftests.
>
> This is a regression introduced in the 7.1 development cycle by commit
> 022ac0750883 ("bpf: use reg->var_off instead of reg->off for pointers"),
> which moved the constant offset from reg->off (folded generically before
> 022ac0750883) into reg->var_off without updating the flow_keys path. No
> released kernel is affected: v7.0.x rejects the program above, and the bug
> reproduces only on v7.1-rc1..rc5, so no stable backport is needed.
>
> It was first reported privately to security@kernel.org; per their guidance
> it is handled in the open as a normal regression fix.
Right and the fix should target bpf-next.
It's not a security issue.
pw-bot: cr
^ permalink raw reply [flat|nested] 6+ messages in thread