* [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types
@ 2026-08-14 21:52 Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 2/6] bpf: Treat a fault prone PTR_TO_MEM as a pointer type mismatch Daniel Borkmann
` (5 more replies)
0 siblings, 6 replies; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 21:52 UTC (permalink / raw)
To: eddyz87; +Cc: memxor, bpf
When the same BPF_LDX instruction is reached through paths that yield
different pointer types, save_aux_ptr_type() merges them into a single
type which is later used by bpf_convert_ctx_accesses() to decide whether
the load has to be rewritten into a BPF_PROBE_MEM one.
Before f2362a57aeff ("bpf: allow void* cast using bpf_rdonly_cast()")
the merge only accepted two PTR_TO_BTF_ID pointers and unconditionally
fell back to PTR_TO_BTF_ID | PTR_UNTRUSTED, so the merged type was always
one that gets the BPF_PROBE_MEM rewrite. However, the mentioned commit
widened the merge to also cover a PTR_TO_MEM base and replaced the
fallback by a union of the PTR_UNTRUSTED and MEM_RDONLY flags.
A union of flags though cannot express the property the later rewrite
is built upon, some examples:
- PTR_TO_MEM merged with PTR_TO_BTF_ID | PTR_UNTRUSTED gets
PTR_TO_MEM | PTR_UNTRUSTED but only the MEM_RDONLY variant is valid
- PTR_TO_MEM merged with a plain PTR_TO_BTF_ID gets PTR_TO_MEM
dropping the rewrite the latter type would have gotten
- PTR_TO_MEM | MEM_RDONLY merged with a plain PTR_TO_BTF_ID gets
PTR_TO_MEM | MEM_RDONLY which is not rewritten either since only
its PTR_UNTRUSTED variant is
In all three cases a program can take the unsafe path at runtime with a
NULL or otherwise bad pointer and panic the kernel on the faulting load:
BUG: kernel NULL pointer dereference, address: 0000000000000038
RIP: 0010:bpf_prog_77531a87032eeaf1_mixed_mem_btf_id_type+0x4b/0x65
Call Trace:
<TASK>
bpf_test_run+0x20b/0x460
bpf_prog_test_run_skb+0x650/0xbe0
__sys_bpf+0xb96/0x3140
__x64_sys_bpf+0x2c/0x40
do_syscall_64+0xba/0x590
Kernel panic - not syncing: Fatal exception in interrupt
Note that the last two shapes have to be fixed right here, otherwise
the merged type retains nothing which marks the load as fault prone,
thus no rule in bpf_convert_ctx_accesses() can recover it. Fix it by
normalizing the merged type instead.
Reuse it in is_load_acq_unsafe() to avoid open coding, and trim the
overly verbose comment which is more of an implementation detail of
bpf_convert_ctx_accesses() anyway.
Fixes: f2362a57aeff ("bpf: allow void* cast using bpf_rdonly_cast()")
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
v1 -> v2:
- update commit msg that this commit is still needed
include/linux/bpf_verifier.h | 10 ++++++++
kernel/bpf/verifier.c | 49 +++++++++++++++++-------------------
2 files changed, 33 insertions(+), 26 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 93f7c2075eea..114ae4540d2c 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1297,6 +1297,16 @@ static inline u32 type_flag(u32 type)
return type & ~BPF_BASE_TYPE_MASK;
}
+static inline bool bpf_may_fault_on_deref(enum bpf_reg_type type)
+{
+ /*
+ * The pointer types which must not be dereferenced without fault
+ * protection, that is, the ones bpf_convert_ctx_accesses() has to
+ * turn a BPF_LDX into a BPF_PROBE_MEM one for.
+ */
+ return type == PTR_TO_BTF_ID || (type_flag(type) & PTR_UNTRUSTED);
+}
+
static inline bool bpf_prog_has_arena_ctx_arg(const struct bpf_prog *prog)
{
int i;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 61ef43325c6f..a3d165e92175 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4828,19 +4828,11 @@ static bool is_load_acq_unsafe(struct bpf_verifier_env *env, int regno,
* A BPF_LOAD_ACQ is not rewritten to a BPF_PROBE_MEM load by the
* verifier, unlike a regular BPF_LDX. The JIT would emit a plain load
* with no exception table entry, so a fault (e.g. NULL deref) crashes
- * the kernel instead of being handled.
- *
- * Reject the source pointer types that a BPF_LDX would have had that
- * fault protection applied to, i.e. the ones bpf_convert_ctx_accesses()
- * turns into BPF_PROBE_MEM: a bare PTR_TO_BTF_ID and any PTR_UNTRUSTED
- * pointer (untrusted btf ids, untrusted MEM_ALLOC, rdonly untrusted
- * memory). A PTR_TRUSTED pointer is not among them, is not converted,
- * and stays allowed. Same for the other flagged PTR_TO_BTF_ID variants
- * (MEM_ALLOC, MEM_RCU, ...), hence the exact match on the base type.
+ * the kernel instead of being handled. Reject the source pointer types
+ * that would have needed that protection, the remaining ones stay
+ * allowed.
*/
- return insn->imm == BPF_LOAD_ACQ &&
- (reg->type == PTR_TO_BTF_ID ||
- (type_flag(reg->type) & PTR_UNTRUSTED));
+ return insn->imm == BPF_LOAD_ACQ && bpf_may_fault_on_deref(reg->type);
}
/* Return false if @regno contains a pointer whose type isn't supported for
@@ -17021,11 +17013,24 @@ static bool is_ptr_to_mem(enum bpf_reg_type type)
return base_type(type) == PTR_TO_MEM;
}
+static enum bpf_reg_type merge_ptr_types(enum bpf_reg_type type_a,
+ enum bpf_reg_type type_b)
+{
+ bool to_mem = is_ptr_to_mem(type_a) || is_ptr_to_mem(type_b);
+ enum bpf_reg_type type_merged = to_mem ? PTR_TO_MEM : PTR_TO_BTF_ID;
+
+ if (bpf_may_fault_on_deref(type_a) || bpf_may_fault_on_deref(type_b))
+ type_merged |= to_mem ? MEM_RDONLY | PTR_UNTRUSTED :
+ PTR_UNTRUSTED;
+ else
+ type_merged |= ((type_a | type_b) & MEM_RDONLY);
+ return type_merged;
+}
+
static int save_aux_ptr_type(struct bpf_verifier_env *env, enum bpf_reg_type type,
bool allow_trust_mismatch)
{
enum bpf_reg_type *prev_type = &env->insn_aux_data[env->insn_idx].ptr_type;
- enum bpf_reg_type merged_type;
if (*prev_type == NOT_INIT) {
/* Saw a valid insn
@@ -17046,20 +17051,12 @@ static int save_aux_ptr_type(struct bpf_verifier_env *env, enum bpf_reg_type typ
is_ptr_to_mem_or_btf_id(*prev_type)) {
/*
* Have to support a use case when one path through
- * the program yields TRUSTED pointer while another
- * is UNTRUSTED. Fallback to UNTRUSTED to generate
- * BPF_PROBE_MEM/BPF_PROBE_MEMSX.
- * Same behavior of MEM_RDONLY flag.
+ * the program yields a TRUSTED pointer while another
+ * is UNTRUSTED. Merge them into a type which keeps
+ * the BPF_PROBE_MEM/BPF_PROBE_MEMSX rewrite when
+ * either side needs it.
*/
- if (is_ptr_to_mem(type) || is_ptr_to_mem(*prev_type))
- merged_type = PTR_TO_MEM;
- else
- merged_type = PTR_TO_BTF_ID;
- if ((type & PTR_UNTRUSTED) || (*prev_type & PTR_UNTRUSTED))
- merged_type |= PTR_UNTRUSTED;
- if ((type & MEM_RDONLY) || (*prev_type & MEM_RDONLY))
- merged_type |= MEM_RDONLY;
- *prev_type = merged_type;
+ *prev_type = merge_ptr_types(type, *prev_type);
} else {
verbose(env, "same insn cannot be used with different pointers\n");
return -EINVAL;
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH bpf-next v2 2/6] bpf: Treat a fault prone PTR_TO_MEM as a pointer type mismatch
2026-08-14 21:52 [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types Daniel Borkmann
@ 2026-08-14 21:52 ` Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 3/6] bpf: Reject a store through a fault prone pointer Daniel Borkmann
` (4 subsequent siblings)
5 siblings, 0 replies; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 21:52 UTC (permalink / raw)
To: eddyz87; +Cc: memxor, bpf
reg_type_mismatch_ok() enumerates the pointer types which must not
silently share a BPF_LDX with a different one, since the type recorded
for the insn drives a rewrite in bpf_convert_ctx_accesses().
f2362a57aeff ("bpf: allow void* cast using bpf_rdonly_cast()") added
PTR_TO_MEM | MEM_RDONLY | PTR_UNTRUSTED as another type in need of one,
namely the BPF_PROBE_MEM rewrite, but did not add it there. Fix it by
adding the missing case to reg_type_mismatch_ok(), so that a PTR_TO_MEM
which may fault on deref is not mismatch ok anymore. The triage in
save_aux_ptr_type() then merges them.
Fixes: f2362a57aeff ("bpf: allow void* cast using bpf_rdonly_cast()")
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
v1 -> v2:
- new patch to address PTR_TO_MEM case differently (Eduard)
kernel/bpf/verifier.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a3d165e92175..2e6992569187 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -16974,6 +16974,8 @@ static bool reg_type_mismatch_ok(enum bpf_reg_type type)
case PTR_TO_BTF_ID:
case PTR_TO_ARENA:
return false;
+ case PTR_TO_MEM:
+ return !bpf_may_fault_on_deref(type);
default:
return true;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH bpf-next v2 3/6] bpf: Reject a store through a fault prone pointer
2026-08-14 21:52 [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 2/6] bpf: Treat a fault prone PTR_TO_MEM as a pointer type mismatch Daniel Borkmann
@ 2026-08-14 21:52 ` Daniel Borkmann
2026-08-14 22:40 ` bot+bpf-ci
2026-08-14 21:52 ` [PATCH bpf-next v2 4/6] bpf: Rewrite any fault prone load out of a mem or btf_id pointer Daniel Borkmann
` (3 subsequent siblings)
5 siblings, 1 reply; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 21:52 UTC (permalink / raw)
To: eddyz87; +Cc: memxor, bpf
check_ptr_to_btf_access() allows the program to store before the default
BTF access path gets to reject a non read access. ac65c710cc64 ("bpf:
Reject writes through untrusted BTF pointers") closed that for a
PTR_UNTRUSTED pointer, but a bare PTR_TO_BTF_ID may fault on a dereference
just the same and is let through.
A BPF_LDX gets the BPF_PROBE_MEM rewrite in bpf_convert_ctx_accesses()
and a bad address is handled, but a BPF_STX does not and cannot, there
is no probed store to rewrite. The store is emitted as a plain one without
an exception table entry and a bad address panics the kernel.
A bpf_qdisc program can reach this, bpf_qdisc_btf_struct_access() permits a
write to Qdisc::limit and Qdisc::next_sched is a plain struct Qdisc pointer
which the walk turns into the compat type:
struct Qdisc *next = sch->next_sched;
next->limit = 1000;
BUG: kernel NULL pointer dereference, address: 0000000000000014
RIP: 0010:bpf_prog_c6e14e7f32c8e325_bpf_fifo_enqueue+0x3a/0x12b
Code: [...] bf e8 03 00 00 <89> 7e 14 41 8b 7f 14 [...]
Kernel panic - not syncing: Fatal exception in interrupt
Fix by widen the check to bpf_may_fault_on_deref() so that it covers both.
Fixes: 27ae7997a661 ("bpf: Introduce BPF_PROG_TYPE_STRUCT_OPS")
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
v1 -> v2:
- new patch
kernel/bpf/verifier.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 2e6992569187..6610e2437047 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -5789,7 +5789,7 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
return -EACCES;
}
- if (atype != BPF_READ && (type_flag(reg->type) & PTR_UNTRUSTED)) {
+ if (atype != BPF_READ && bpf_may_fault_on_deref(reg->type)) {
verbose(env, "only read is supported\n");
return -EACCES;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH bpf-next v2 4/6] bpf: Rewrite any fault prone load out of a mem or btf_id pointer
2026-08-14 21:52 [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 2/6] bpf: Treat a fault prone PTR_TO_MEM as a pointer type mismatch Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 3/6] bpf: Reject a store through a fault prone pointer Daniel Borkmann
@ 2026-08-14 21:52 ` Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 5/6] selftests/bpf: Add tests for pointer type merge at a shared load Daniel Borkmann
` (2 subsequent siblings)
5 siblings, 0 replies; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 21:52 UTC (permalink / raw)
To: eddyz87; +Cc: memxor, bpf
bpf_convert_ctx_accesses() turns a BPF_LDX into a BPF_PROBE_MEM one by
matching the type recorded for the insn against a list of exact pointer
types. The list cannot keep up with the flag combinations the verifier
produces, and a type which is missing from it ends up as a plain load
without an exception table entry, so a bad address panics the kernel
instead of being handled.
Two such types exist today and are reachable:
- PTR_TO_BTF_ID | PTR_UNTRUSTED | MEM_ALLOC | NON_OWN_REF
- PTR_TO_BTF_ID | PTR_UNTRUSTED | MEM_RCU
Rather than adding the two, just drop the list and state the property
itself in the default case of the switch. This is a superset of what
the list matched, the untrusted PTR_TO_MEM does not have to carry
MEM_RDONLY for it anymore, and it stays in sync with the verifier side
which uses the same match in save_aux_ptr_type() and reg_type_mismatch_ok().
Assert that a fault prone type which does not get the rewrite for whatever
reason is rejected at load time rather than left to fault at runtime to
catch any future cases.
Fixes: 1b12171533a9 ("bpf: Mark direct ld of stashed bpf_{rb,list}_node as non-owning ref")
Fixes: 6fcd486b3a0a ("bpf: Refactor RCU enforcement in the verifier.")
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
v1 -> v2:
- new patch, don't match on full types (Eduard)
include/linux/bpf_verifier.h | 11 +++++++++
kernel/bpf/fixups.c | 47 ++++++++++++++++++++----------------
kernel/bpf/verifier.c | 15 ++----------
3 files changed, 39 insertions(+), 34 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 114ae4540d2c..7239cffa7b51 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1297,6 +1297,17 @@ static inline u32 type_flag(u32 type)
return type & ~BPF_BASE_TYPE_MASK;
}
+static inline bool bpf_is_ptr_to_mem_or_btf_id(enum bpf_reg_type type)
+{
+ switch (base_type(type)) {
+ case PTR_TO_MEM:
+ case PTR_TO_BTF_ID:
+ return true;
+ default:
+ return false;
+ }
+}
+
static inline bool bpf_may_fault_on_deref(enum bpf_reg_type type)
{
/*
diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
index c4bd70befbb5..796fba0505dc 100644
--- a/kernel/bpf/fixups.c
+++ b/kernel/bpf/fixups.c
@@ -811,6 +811,7 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
for (i = 0; i < insn_cnt; i++, insn++) {
bpf_convert_ctx_access_t convert_ctx_access;
+ enum bpf_reg_type ptr_type;
u8 mode;
if (env->insn_aux_data[i + delta].nospec) {
@@ -903,7 +904,8 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
continue;
}
- switch ((int)env->insn_aux_data[i + delta].ptr_type) {
+ ptr_type = env->insn_aux_data[i + delta].ptr_type;
+ switch ((int)ptr_type) {
case PTR_TO_CTX:
if (!ops->convert_ctx_access)
continue;
@@ -919,26 +921,6 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
case PTR_TO_XDP_SOCK:
convert_ctx_access = bpf_xdp_sock_convert_ctx_access;
break;
- case PTR_TO_BTF_ID:
- case PTR_TO_BTF_ID | PTR_UNTRUSTED:
- /* PTR_TO_BTF_ID | MEM_ALLOC always has a valid lifetime, unlike
- * PTR_TO_BTF_ID, and an active referenced id, but the same cannot
- * be said once it is marked PTR_UNTRUSTED, hence we must handle
- * any faults for loads into such types. BPF_WRITE is disallowed
- * for this case.
- */
- case PTR_TO_BTF_ID | MEM_ALLOC | PTR_UNTRUSTED:
- case PTR_TO_MEM | MEM_RDONLY | PTR_UNTRUSTED:
- if (type == BPF_READ) {
- if (BPF_MODE(insn->code) == BPF_MEM)
- insn->code = BPF_LDX | BPF_PROBE_MEM |
- BPF_SIZE((insn)->code);
- else
- insn->code = BPF_LDX | BPF_PROBE_MEMSX |
- BPF_SIZE((insn)->code);
- env->prog->aux->num_exentries++;
- }
- continue;
case PTR_TO_ARENA:
if (BPF_MODE(insn->code) == BPF_MEMSX) {
if (!bpf_jit_supports_insn(insn, true)) {
@@ -952,6 +934,29 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
env->prog->aux->num_exentries++;
continue;
default:
+ /*
+ * A pointer which may fault on a dereference must not
+ * be loaded from without fault protection, hence turn
+ * the BPF_LDX into a BPF_PROBE_MEM one so that a bad
+ * address is handled rather than panicking the kernel.
+ * A store through one is rejected earlier, there is no
+ * probed counterpart to rewrite it into.
+ */
+ if (bpf_is_ptr_to_mem_or_btf_id(ptr_type) &&
+ bpf_may_fault_on_deref(ptr_type) &&
+ type == BPF_READ) {
+ if (BPF_MODE(insn->code) == BPF_MEM)
+ insn->code = BPF_LDX | BPF_PROBE_MEM |
+ BPF_SIZE(insn->code);
+ else
+ insn->code = BPF_LDX | BPF_PROBE_MEMSX |
+ BPF_SIZE(insn->code);
+ env->prog->aux->num_exentries++;
+ continue;
+ }
+ if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
+ "access to a fault prone pointer is not rewritten as a probed one"))
+ return -EFAULT;
continue;
}
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 6610e2437047..6fff370749bf 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -16999,17 +16999,6 @@ static bool reg_type_mismatch(enum bpf_reg_type src, enum bpf_reg_type prev)
!reg_type_mismatch_ok(prev));
}
-static bool is_ptr_to_mem_or_btf_id(enum bpf_reg_type type)
-{
- switch (base_type(type)) {
- case PTR_TO_MEM:
- case PTR_TO_BTF_ID:
- return true;
- default:
- return false;
- }
-}
-
static bool is_ptr_to_mem(enum bpf_reg_type type)
{
return base_type(type) == PTR_TO_MEM;
@@ -17049,8 +17038,8 @@ static int save_aux_ptr_type(struct bpf_verifier_env *env, enum bpf_reg_type typ
* Reject it.
*/
if (allow_trust_mismatch &&
- is_ptr_to_mem_or_btf_id(type) &&
- is_ptr_to_mem_or_btf_id(*prev_type)) {
+ bpf_is_ptr_to_mem_or_btf_id(type) &&
+ bpf_is_ptr_to_mem_or_btf_id(*prev_type)) {
/*
* Have to support a use case when one path through
* the program yields a TRUSTED pointer while another
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH bpf-next v2 5/6] selftests/bpf: Add tests for pointer type merge at a shared load
2026-08-14 21:52 [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types Daniel Borkmann
` (2 preceding siblings ...)
2026-08-14 21:52 ` [PATCH bpf-next v2 4/6] bpf: Rewrite any fault prone load out of a mem or btf_id pointer Daniel Borkmann
@ 2026-08-14 21:52 ` Daniel Borkmann
2026-08-14 22:56 ` bot+bpf-ci
2026-08-14 21:53 ` [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers Daniel Borkmann
2026-08-14 22:15 ` [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types sashiko-bot
5 siblings, 1 reply; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 21:52 UTC (permalink / raw)
To: eddyz87; +Cc: memxor, bpf
Cover the ways in which the type recorded for a shared load used to lose
the BPF_PROBE_MEM rewrite which would then trigger a NULL deref if not
handled properly.
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t mem_rdonly_untrusted
[...]
#238/1 mem_rdonly_untrusted/btf_id_to_ptr_mem:OK
#238/2 mem_rdonly_untrusted/ldx_is_ok_bad_addr:OK
#238/3 mem_rdonly_untrusted/ldx_is_ok_good_addr:OK
#238/4 mem_rdonly_untrusted/offset_not_tracked:OK
#238/5 mem_rdonly_untrusted/stx_not_ok:OK
#238/6 mem_rdonly_untrusted/atomic_not_ok:OK
#238/7 mem_rdonly_untrusted/atomic_rmw_not_ok:OK
#238/8 mem_rdonly_untrusted/kfunc_param_not_ok:OK
#238/9 mem_rdonly_untrusted/mixed_mem_type:OK
#238/10 mem_rdonly_untrusted/mixed_mem_untrusted_btf_id_type:OK
#238/11 mem_rdonly_untrusted/mixed_mem_btf_id_type:OK
#238/12 mem_rdonly_untrusted/mixed_rdonly_mem_btf_id_type:OK
#238/13 mem_rdonly_untrusted/mixed_mem_mem_type:OK
#238/14 mem_rdonly_untrusted/mixed_map_value_mem_type:OK
#238/15 mem_rdonly_untrusted/mixed_stack_mem_type:OK
#238/16 mem_rdonly_untrusted/diff_size_access:OK
#238/17 mem_rdonly_untrusted/misaligned_access:OK
#238/18 mem_rdonly_untrusted/null_check:OK
#238/19 mem_rdonly_untrusted/ldx_is_ok_commuted_addr:OK
#238/20 mem_rdonly_untrusted/helper_param_not_ok:OK
#238 mem_rdonly_untrusted:OK
Summary: 1/20 PASSED, 0 SKIPPED, 0/0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
.../bpf/progs/mem_rdonly_untrusted.c | 254 ++++++++++++++++++
1 file changed, 254 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c b/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c
index b91271d4caa4..ac8bba3e7d02 100644
--- a/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c
+++ b/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c
@@ -3,6 +3,7 @@
#include <vmlinux.h>
#include <bpf/bpf_core_read.h>
#include "bpf_misc.h"
+#include "bpf_kfuncs.h"
#include "../test_kmods/bpf_testmod_kfunc.h"
SEC("tp_btf/sys_enter")
@@ -164,6 +165,259 @@ int mixed_mem_type(void *ctx)
return *p;
}
+struct {
+ __uint(type, BPF_MAP_TYPE_RINGBUF);
+ __uint(max_entries, 4096);
+} ringbuf SEC(".maps");
+
+int zero;
+
+static __noinline u64 *get_mem_or_untrusted_addr(u64 *mem)
+{
+ /*
+ * Try to avoid compiler hoisting load to if branches by using
+ * __noinline func.
+ */
+ if (zero)
+ return mem;
+ else
+ return bpf_rdonly_cast(0, bpf_core_type_id_kernel(struct sock));
+}
+
+SEC("socket")
+__success
+__log_level(2)
+__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=untrusted_ptr_sock")
+__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=ringbuf_mem")
+__retval(0)
+int mixed_mem_untrusted_btf_id_type(void *ctx)
+{
+ u64 *p, v;
+
+ p = bpf_ringbuf_reserve(&ringbuf, sizeof(*p), 0);
+ if (!p)
+ return 1;
+ *p = 42;
+ /*
+ * The load below is reached with PTR_TO_MEM | MEM_RINGBUF on one
+ * path and with PTR_TO_BTF_ID | PTR_UNTRUSTED on the other. The
+ * merged type has to keep the BPF_PROBE_MEM rewrite, otherwise
+ * the NULL deref taken at runtime panics the kernel instead of
+ * returning 0.
+ */
+ v = *get_mem_or_untrusted_addr(p);
+ bpf_ringbuf_discard(p, 0);
+ return v;
+}
+
+static __noinline u32 *get_mem_or_btf_id_addr(u32 *mem)
+{
+ struct task_struct *task;
+
+ /*
+ * Try to avoid compiler hoisting load to if branches by using
+ * __noinline func.
+ */
+ if (zero)
+ return mem;
+
+ task = bpf_get_current_task_btf();
+ /*
+ * A plain BTF pointer walk yields a bare PTR_TO_BTF_ID, and
+ * task->nameidata is NULL unless the task currently is in the
+ * middle of a path lookup.
+ */
+ return (u32 *)&task->nameidata->flags;
+}
+
+SEC("socket")
+__success
+__log_level(2)
+__msg("= *(u32 *)(r{{[0-9]}} +0){{.*}}=ptr_nameidata")
+__msg("= *(u32 *)(r{{[0-9]}} +0){{.*}}=ringbuf_mem")
+__retval(0)
+int mixed_mem_btf_id_type(void *ctx)
+{
+ u32 *p, v;
+
+ p = bpf_ringbuf_reserve(&ringbuf, sizeof(*p), 0);
+ if (!p)
+ return 1;
+ *p = 42;
+ /*
+ * Same as above, except that the other path yields a bare
+ * PTR_TO_BTF_ID. Merging it with PTR_TO_MEM used to drop the
+ * BPF_PROBE_MEM rewrite the bare PTR_TO_BTF_ID would have
+ * gotten on its own.
+ */
+ v = *get_mem_or_btf_id_addr(p);
+ bpf_ringbuf_discard(p, 0);
+ return v;
+}
+
+char dynptr_data[8];
+
+static __noinline u32 *get_rdonly_mem_or_btf_id_addr(u32 *mem)
+{
+ struct task_struct *task;
+
+ /*
+ * Try to avoid compiler hoisting load to if branches by using
+ * __noinline func.
+ */
+ if (zero)
+ return mem;
+
+ task = bpf_get_current_task_btf();
+ return (u32 *)&task->nameidata->flags;
+}
+
+SEC("socket")
+__success
+__log_level(2)
+__msg("r8 = *(u32 *)(r7 +0){{.*}}R7=ptr_nameidata")
+__msg("r8 = *(u32 *)(r7 +0){{.*}}R7=rdonly_mem")
+__retval(0)
+int mixed_rdonly_mem_btf_id_type(void *ctx)
+{
+ struct bpf_dynptr dptr;
+ char buf[sizeof(u32)];
+ u32 *p;
+ u64 v;
+
+ if (bpf_dynptr_from_mem(dynptr_data, sizeof(dynptr_data), 0, &dptr))
+ return 1;
+ p = bpf_dynptr_slice(&dptr, 0, buf, sizeof(buf));
+ if (!p)
+ return 1;
+ /*
+ * Same as above, except that the PTR_TO_MEM side already carries
+ * MEM_RDONLY. Merging it with a bare PTR_TO_BTF_ID used to yield
+ * PTR_TO_MEM | MEM_RDONLY, which is not rewritten either since
+ * only its PTR_UNTRUSTED variant is.
+ */
+ p = get_rdonly_mem_or_btf_id_addr(p);
+ /* asm block to have reliable match target for __msg. */
+ asm volatile (
+ "r7 = %[p];"
+ "r8 = *(u32 *)(r7 + 0);"
+ "%[v] = r8;"
+ : [v]"=r"(v)
+ : [p]"r"(p)
+ : "r7", "r8");
+ return v;
+}
+
+static __noinline u64 *get_mem_or_rdonly_untrusted_mem_addr(u64 *mem)
+{
+ u64 *p = bpf_rdonly_cast(0, 0);
+
+ /*
+ * Hoist the cast above the branch so that the PTR_TO_MEM |
+ * MEM_RINGBUF path is verified first and thus gets its type
+ * recorded first.
+ */
+ if (zero == 0)
+ return p;
+ return mem;
+}
+
+SEC("socket")
+__success
+__log_level(2)
+__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=ringbuf_mem")
+__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=rdonly_untrusted_mem")
+__retval(0)
+int mixed_mem_mem_type(void *ctx)
+{
+ u64 *p, v;
+
+ p = bpf_ringbuf_reserve(&ringbuf, sizeof(*p), 0);
+ if (!p)
+ return 1;
+ *p = 42;
+ /*
+ * Both paths are PTR_TO_MEM based, so they used to not trip the
+ * type mismatch check and skipped the merge altogether, leaving
+ * the insn with the PTR_TO_MEM | MEM_RINGBUF recorded first and
+ * hence without the BPF_PROBE_MEM rewrite the other path needs.
+ */
+ v = *get_mem_or_rdonly_untrusted_mem_addr(p);
+ bpf_ringbuf_discard(p, 0);
+ return v;
+}
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, u32);
+ __type(value, u64);
+} array SEC(".maps");
+
+static __noinline u64 *get_map_value_or_rdonly_untrusted_mem_addr(u64 *mem)
+{
+ u64 *p = bpf_rdonly_cast(0, 0);
+
+ /*
+ * Hoist the cast above the branch so that the PTR_TO_MAP_VALUE
+ * path is verified first and thus gets its type recorded first.
+ */
+ if (zero == 0)
+ return p;
+ return mem;
+}
+
+SEC("socket")
+__failure
+__msg("same insn cannot be used with different pointers")
+int mixed_map_value_mem_type(void *ctx)
+{
+ u64 *p, v;
+ u32 key = 0;
+
+ p = bpf_map_lookup_elem(&array, &key);
+ if (!p)
+ return 1;
+ /*
+ * PTR_TO_MAP_VALUE is neither PTR_TO_MEM nor PTR_TO_BTF_ID based,
+ * so it cannot be merged into a type which keeps the BPF_PROBE_MEM
+ * rewrite the PTR_TO_MEM | MEM_RDONLY | PTR_UNTRUSTED of the other
+ * path needs. Both bases were mismatch ok, hence the load used to be
+ * accepted with the PTR_TO_MAP_VALUE recorded and the NULL deref on
+ * the second path panicked the kernel.
+ */
+ v = *get_map_value_or_rdonly_untrusted_mem_addr(p);
+ return v;
+}
+
+SEC("socket")
+__failure
+__msg("same insn cannot be used with different pointers")
+int mixed_stack_mem_type(void *ctx)
+{
+ u64 *p = bpf_rdonly_cast(0, 0);
+ u64 s = 42, v;
+
+ /*
+ * Same as above, but for a PTR_TO_STACK on the other path. A
+ * subprog cannot return one, so the branch and the load have to
+ * be kept in the same frame, and the asm block is there to have
+ * the compiler not duplicate the latter.
+ */
+ asm volatile (
+ "r7 = %[p];"
+ "if %[zero] == 0 goto +1;"
+ "r7 = %[s];"
+ "r8 = *(u64 *)(r7 + 0);"
+ "%[v] = r8;"
+ : [v]"=r"(v)
+ : [p]"r"(p),
+ [s]"r"(&s),
+ [zero]"r"(zero)
+ : "r7", "r8");
+ return v;
+}
+
__attribute__((__aligned__(8)))
u8 global[] = {
0x11, 0x22, 0x33, 0x44,
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers
2026-08-14 21:52 [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types Daniel Borkmann
` (3 preceding siblings ...)
2026-08-14 21:52 ` [PATCH bpf-next v2 5/6] selftests/bpf: Add tests for pointer type merge at a shared load Daniel Borkmann
@ 2026-08-14 21:53 ` Daniel Borkmann
2026-08-14 22:03 ` sashiko-bot
2026-08-14 22:15 ` [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types sashiko-bot
5 siblings, 1 reply; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 21:53 UTC (permalink / raw)
To: eddyz87; +Cc: memxor, bpf
Cover the two loads which used to lose the BPF_PROBE_MEM rewrite, both
reached from an RCU read-side critical section.
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t rcu_read_lock
[...]
#332/1 rcu_read_lock/success:OK
#332/2 rcu_read_lock/rcuptr_acquire:OK
#332/3 rcu_read_lock/negative_tests_inproper_region:OK
#332/4 rcu_read_lock/negative_tests_rcuptr_misuse:OK
#332 rcu_read_lock:OK
Summary: 1/4 PASSED, 0 SKIPPED, 0/0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
v1 -> v2:
- new patch
.../selftests/bpf/prog_tests/rcu_read_lock.c | 2 +
.../selftests/bpf/progs/rcu_read_lock.c | 76 +++++++++++++++++++
2 files changed, 78 insertions(+)
diff --git a/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c b/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c
index 246eb259c08a..6a07b2b418d1 100644
--- a/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c
+++ b/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c
@@ -34,6 +34,8 @@ static void test_success(void)
bpf_program__set_autoload(skel->progs.rcu_read_lock_global_subprog, true);
bpf_program__set_autoload(skel->progs.rcu_read_lock_subprog_lock, true);
bpf_program__set_autoload(skel->progs.rcu_read_lock_subprog_unlock, true);
+ bpf_program__set_autoload(skel->progs.non_own_ref_untrusted_ld, true);
+ bpf_program__set_autoload(skel->progs.rcu_untrusted_union_ld, true);
err = rcu_read_lock__load(skel);
if (!ASSERT_OK(err, "skel_load"))
goto out;
diff --git a/tools/testing/selftests/bpf/progs/rcu_read_lock.c b/tools/testing/selftests/bpf/progs/rcu_read_lock.c
index b4e073168fb1..31d4081c3a9f 100644
--- a/tools/testing/selftests/bpf/progs/rcu_read_lock.c
+++ b/tools/testing/selftests/bpf/progs/rcu_read_lock.c
@@ -549,3 +549,79 @@ int rcu_read_lock_sleepable_global_subprog_indirect(void *ctx)
bpf_rcu_read_unlock();
return 0;
}
+
+struct rcu_node_data {
+ long key;
+ struct bpf_rb_node node;
+};
+
+struct rcu_node_stash {
+ struct rcu_node_data __kptr *node;
+};
+
+/*
+ * Necessary so that LLVM emits BTF for rcu_node_data rather than just a
+ * fwd reference to it, same as in progs/local_kptr_stash.c.
+ */
+struct rcu_node_data *just_here_because_btf_bug;
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, int);
+ __type(value, struct rcu_node_stash);
+} node_stash SEC(".maps");
+
+long non_own_ref_key;
+
+SEC("?fentry.s/" SYS_PREFIX "sys_getpgid")
+int non_own_ref_untrusted_ld(void *ctx)
+{
+ struct rcu_node_stash *stash;
+ struct rcu_node_data *node;
+ int key = 0;
+
+ stash = bpf_map_lookup_elem(&node_stash, &key);
+ if (!stash)
+ return 0;
+ bpf_rcu_read_lock();
+ node = stash->node;
+ if (!node) {
+ bpf_rcu_read_unlock();
+ return 0;
+ }
+ bpf_rcu_read_unlock();
+ /*
+ * The unlock leaves node as PTR_TO_BTF_ID | MEM_ALLOC | PTR_UNTRUSTED
+ * | NON_OWN_REF, and the load below has to get the BPF_PROBE_MEM
+ * rewrite for it, otherwise a bad address panics the kernel.
+ */
+ non_own_ref_key = node->key;
+ return 0;
+}
+
+long rcu_untrusted_wq_flags;
+
+SEC("?tp_btf/tcp_probe")
+int BPF_PROG(rcu_untrusted_union_ld, struct sock *sk)
+{
+ struct socket_wq *wq;
+
+ /*
+ * sk_wq sits in a two member union, so btf_struct_walk() marks the
+ * pointer PTR_UNTRUSTED, and the __rcu tag on the member adds MEM_RCU
+ * on top of it. struct sock is not on the __safe_rcu_or_null allow
+ * list, hence the two stay combined and the load below has to get the
+ * BPF_PROBE_MEM rewrite for PTR_TO_BTF_ID | PTR_UNTRUSTED | MEM_RCU,
+ * otherwise a bad address panics the kernel.
+ *
+ * The __rcu tag only reaches BTF on a clang built kernel, that is, one
+ * with CONFIG_PAHOLE_HAS_BTF_TAG. On a gcc built kernel the walk yields
+ * a plain untrusted pointer, which is rewritten either way.
+ */
+ wq = sk->sk_wq;
+ if (!wq)
+ return 0;
+ rcu_untrusted_wq_flags = wq->flags;
+ return 0;
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers
2026-08-14 21:53 ` [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers Daniel Borkmann
@ 2026-08-14 22:03 ` sashiko-bot
2026-08-14 22:28 ` Daniel Borkmann
0 siblings, 1 reply; 14+ messages in thread
From: sashiko-bot @ 2026-08-14 22:03 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The newly added BPF programs do not execute the vulnerable instructions at runtime and lack explicit verifier log assertions, failing to provide robust regression coverage.
--
--- Patch [6]: [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers ---
commit 6d523ae9972a5903a07a65fd29336ef6414702f5
Author: Daniel Borkmann <daniel@iogearbox.net>
selftests/bpf: Add tests for fault prone loads out of RCU pointers
This commit adds new BPF tests for ensuring the verifier correctly
applies the BPF_PROBE_MEM rewrite to fault-prone loads reached from an
RCU read-side critical section. Two new subprograms are added and
autoloaded in the rcu_read_lock test.
> diff --git a/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c b/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c
> --- a/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c
> +++ b/tools/testing/selftests/bpf/prog_tests/rcu_read_lock.c
> @@ -34,6 +34,8 @@ static void test_success(void)
> bpf_program__set_autoload(skel->progs.rcu_read_lock_global_subprog, true);
> bpf_program__set_autoload(skel->progs.rcu_read_lock_subprog_lock, true);
> bpf_program__set_autoload(skel->progs.rcu_read_lock_subprog_unlock, true);
> + bpf_program__set_autoload(skel->progs.non_own_ref_untrusted_ld, true);
> + bpf_program__set_autoload(skel->progs.rcu_untrusted_union_ld, true);
> err = rcu_read_lock__load(skel);
[Severity: Medium]
Is `rcu_untrusted_union_ld` reliably triggered by the test harness?
Looking at `test_success()`, the harness executes `syscall(SYS_getpgid)` to
trigger the fentry program, but there doesn't seem to be a trigger for the
`tp_btf/tcp_probe` program.
Also, would it be safer to use `test_loader` for these tests so we can
statically assert the verifier log output contains the `BPF_PROBE_MEM`
rewrite, rather than relying only on runtime execution?
[ ... ]
> diff --git a/tools/testing/selftests/bpf/progs/rcu_read_lock.c b/tools/testing/selftests/bpf/progs/rcu_read_lock.c
> --- a/tools/testing/selftests/bpf/progs/rcu_read_lock.c
> +++ b/tools/testing/selftests/bpf/progs/rcu_read_lock.c
[ ... ]
> +SEC("?fentry.s/" SYS_PREFIX "sys_getpgid")
> +int non_own_ref_untrusted_ld(void *ctx)
> +{
> + struct rcu_node_stash *stash;
> + struct rcu_node_data *node;
> + int key = 0;
> +
> + stash = bpf_map_lookup_elem(&node_stash, &key);
> + if (!stash)
> + return 0;
> + bpf_rcu_read_lock();
> + node = stash->node;
> + if (!node) {
> + bpf_rcu_read_unlock();
> + return 0;
> + }
[Severity: Medium]
Does this program end up returning early here on every run?
Since the `node_stash` map is never populated from the userspace test
harness, `stash->node` appears to always be NULL. If it returns early,
the test never actually reaches the fault-prone load below to trigger a
kernel panic if the rewrite were to fail.
> + bpf_rcu_read_unlock();
> + /*
> + * The unlock leaves node as PTR_TO_BTF_ID | MEM_ALLOC | PTR_UNTRUSTED
> + * | NON_OWN_REF, and the load below has to get the BPF_PROBE_MEM
> + * rewrite for it, otherwise a bad address panics the kernel.
> + */
> + non_own_ref_key = node->key;
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260814215301.709827-1-daniel@iogearbox.net?part=6
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types
2026-08-14 21:52 [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types Daniel Borkmann
` (4 preceding siblings ...)
2026-08-14 21:53 ` [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers Daniel Borkmann
@ 2026-08-14 22:15 ` sashiko-bot
2026-08-14 22:19 ` Daniel Borkmann
5 siblings, 1 reply; 14+ messages in thread
From: sashiko-bot @ 2026-08-14 22:15 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [Critical] The verifier fails to emit BPF_PROBE_MEM instructions when a safe pointer type (e.g., PTR_TO_STACK or PTR_TO_MAP_VALUE) and an untrusted memory pointer type (PTR_TO_MEM | PTR_UNTRUSTED) reach the same load instruction via different branches. This completely bypasses the newly introduced merge_ptr_types logic.
--
--- Patch [1]: [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types ---
commit f6edd0237d3f5f1dd3f7b1e12e20100d5ce150cd
Author: Daniel Borkmann <daniel@iogearbox.net>
bpf: Keep fault protection when merging pointer types
This commit fixes an issue where merging different pointer types in
save_aux_ptr_type() drops flags like PTR_UNTRUSTED. It introduces
merge_ptr_types() to properly normalize and combine pointer types so that the
BPF_PROBE_MEM rewrite remains intact.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 6ac1afced20bf..ad6ccd5cf51db 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -17092,20 +17097,12 @@ static int save_aux_ptr_type(struct bpf_verifier_env *env, enum bpf_reg_type typ
> is_ptr_to_mem_or_btf_id(*prev_type)) {
> /*
> * Have to support a use case when one path through
> - * the program yields TRUSTED pointer while another
> - * is UNTRUSTED. Fallback to UNTRUSTED to generate
> - * BPF_PROBE_MEM/BPF_PROBE_MEMSX.
> - * Same behavior of MEM_RDONLY flag.
> + * the program yields a TRUSTED pointer while another
> + * is UNTRUSTED. Merge them into a type which keeps
> + * the BPF_PROBE_MEM/BPF_PROBE_MEMSX rewrite when
> + * either side needs it.
> */
> - if (is_ptr_to_mem(type) || is_ptr_to_mem(*prev_type))
> - merged_type = PTR_TO_MEM;
> - else
> - merged_type = PTR_TO_BTF_ID;
> - if ((type & PTR_UNTRUSTED) || (*prev_type & PTR_UNTRUSTED))
> - merged_type |= PTR_UNTRUSTED;
> - if ((type & MEM_RDONLY) || (*prev_type & MEM_RDONLY))
> - merged_type |= MEM_RDONLY;
> - *prev_type = merged_type;
> + *prev_type = merge_ptr_types(type, *prev_type);
[Severity: Critical]
This is a pre-existing issue, but does this bypass conflict resolution
entirely if reg_type_mismatch() returns false for both types?
If one branch provides a safe pointer (like PTR_TO_STACK) and another branch
provides an untrusted pointer (PTR_TO_MEM | PTR_UNTRUSTED), they evaluate as:
kernel/bpf/verifier.c:reg_type_mismatch() {
return src != prev && (!reg_type_mismatch_ok(src) ||
!reg_type_mismatch_ok(prev));
}
Since neither base type is in the reg_type_mismatch_ok() switch statement,
it returns true for both types, causing reg_type_mismatch() to return false.
If the safe pointer reaches save_aux_ptr_type() first, we wouldn't enter
the else if (reg_type_mismatch(type, *prev_type)) block at all.
Does this mean merge_ptr_types() is skipped, the untrusted pointer type is
silently ignored, and we might fail to emit the required BPF_PROBE_MEM
rewrite?
> } else {
> verbose(env, "same insn cannot be used with different pointers\n");
> return -EINVAL;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260814215301.709827-1-daniel@iogearbox.net?part=1
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types
2026-08-14 22:15 ` [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types sashiko-bot
@ 2026-08-14 22:19 ` Daniel Borkmann
0 siblings, 0 replies; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 22:19 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf
On 8/15/26 12:15 AM, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
>
> Pre-existing issues:
> - [Critical] The verifier fails to emit BPF_PROBE_MEM instructions when a safe pointer type (e.g., PTR_TO_STACK or PTR_TO_MAP_VALUE) and an untrusted memory pointer type (PTR_TO_MEM | PTR_UNTRUSTED) reach the same load instruction via different branches. This completely bypasses the newly introduced merge_ptr_types logic.
(this is already addressed in 2/6 in the series)
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers
2026-08-14 22:03 ` sashiko-bot
@ 2026-08-14 22:28 ` Daniel Borkmann
0 siblings, 0 replies; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 22:28 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf
On 8/15/26 12:03 AM, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Medium] The newly added BPF programs do not execute the vulnerable instructions at runtime and lack explicit verifier log assertions, failing to provide robust regression coverage.
Nope, its basically load coverage (ASSERT_OK(err, "skel_load") in test_success()),
meaning we make sure the assertion on patch 4/6 in bpf_convert_ctx_accesses()
doesn't trigger if this is forgotten to get rewritten as probed pointer:
if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
"access to a fault prone pointer is not rewritten as a probed one"))
return -EFAULT;
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 3/6] bpf: Reject a store through a fault prone pointer
2026-08-14 21:52 ` [PATCH bpf-next v2 3/6] bpf: Reject a store through a fault prone pointer Daniel Borkmann
@ 2026-08-14 22:40 ` bot+bpf-ci
2026-08-14 23:26 ` Daniel Borkmann
0 siblings, 1 reply; 14+ messages in thread
From: bot+bpf-ci @ 2026-08-14 22:40 UTC (permalink / raw)
To: daniel, eddyz87
Cc: memxor, bpf, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2045 bytes --]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 814096ed835d..cbbb0c82d22c 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -5788,7 +5788,7 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
> return -EACCES;
> }
>
> - if (atype != BPF_READ && (type_flag(reg->type) & PTR_UNTRUSTED)) {
> + if (atype != BPF_READ && bpf_may_fault_on_deref(reg->type)) {
> verbose(env, "only read is supported\n");
> return -EACCES;
> }
Does this change also reject HID-BPF writes to struct hid_device fields?
The HID struct_ops hid_bpf_ops_btf_struct_access() whitelists writes to
hid_device::name, ::uniq, and ::phys via WRITE_RANGE entries in
drivers/hid/bpf/hid_bpf_struct_ops.c. A HID program receives a
'struct hid_bpf_ctx *' argument that is PTR_TO_BTF_ID | PTR_TRUSTED, but
walking hctx->hid produces a bare PTR_TO_BTF_ID because
struct hid_bpf_ctx appears in no BTF_TYPE_SAFE_TRUSTED list in
check_ptr_to_btf_access().
Before this commit, 'hctx->hid->name[i] = c' passed the PTR_UNTRUSTED
test and reached env->ops->btf_struct_access(), where
hid_bpf_ops_btf_struct_access() returned 0 for offsets in the whitelisted
ranges and the store succeeded. After this commit,
bpf_may_fault_on_deref(PTR_TO_BTF_ID) is true and the store is rejected
with 'only read is supported' before the btf_struct_access callback runs.
The three WRITE_RANGE(hid_device, ...) entries become unreachable. The
commit message notes the bpf_qdisc Qdisc::next_sched case but does not
mention HID-BPF. If the intent is to preserve HID-BPF write capability,
hid_bpf_ctx::hid would need a trusted annotation
(BTF_TYPE_SAFE_TRUSTED(struct hid_bpf_ctx) { struct hid_device *hid; })
so the walked register stays PTR_TRUSTED. Otherwise, should the removal
be documented?
---
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/31845722500
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 5/6] selftests/bpf: Add tests for pointer type merge at a shared load
2026-08-14 21:52 ` [PATCH bpf-next v2 5/6] selftests/bpf: Add tests for pointer type merge at a shared load Daniel Borkmann
@ 2026-08-14 22:56 ` bot+bpf-ci
2026-08-14 23:35 ` Daniel Borkmann
0 siblings, 1 reply; 14+ messages in thread
From: bot+bpf-ci @ 2026-08-14 22:56 UTC (permalink / raw)
To: daniel, eddyz87
Cc: memxor, bpf, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3366 bytes --]
> diff --git a/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c b/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c
> index b91271d4caa4..ac8bba3e7d02 100644
> --- a/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c
> +++ b/tools/testing/selftests/bpf/progs/mem_rdonly_untrusted.c
[ ... ]
> +SEC("socket")
> +__success
> +__log_level(2)
> +__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=untrusted_ptr_sock")
> +__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=ringbuf_mem")
> +__retval(0)
> +int mixed_mem_untrusted_btf_id_type(void *ctx)
[ ... ]
> +SEC("socket")
> +__success
> +__log_level(2)
> +__msg("= *(u32 *)(r{{[0-9]}} +0){{.*}}=ptr_nameidata")
> +__msg("= *(u32 *)(r{{[0-9]}} +0){{.*}}=ringbuf_mem")
> +__retval(0)
> +int mixed_mem_btf_id_type(void *ctx)
[ ... ]
> +SEC("socket")
> +__success
> +__log_level(2)
> +__msg("r8 = *(u32 *)(r7 +0){{.*}}R7=ptr_nameidata")
> +__msg("r8 = *(u32 *)(r7 +0){{.*}}R7=rdonly_mem")
> +__retval(0)
> +int mixed_rdonly_mem_btf_id_type(void *ctx)
[ ... ]
> +SEC("socket")
> +__success
> +__log_level(2)
> +__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=ringbuf_mem")
> +__msg("= *(u64 *)(r{{[0-9]}} +0){{.*}}=rdonly_untrusted_mem")
> +__retval(0)
> +int mixed_mem_mem_type(void *ctx)
[ ... ]
> +SEC("socket")
> +__failure
> +__msg("same insn cannot be used with different pointers")
> +int mixed_map_value_mem_type(void *ctx)
[ ... ]
> +SEC("socket")
> +__failure
> +__msg("same insn cannot be used with different pointers")
> +int mixed_stack_mem_type(void *ctx)
[ ... ]
A subsystem pattern flags this as potentially concerning: these six
tests cover only two of the four commits in the series. The three merge
shapes listed in dea41ba6e67d's changelog map 1:1 onto the
mixed_mem_untrusted_btf_id_type, mixed_mem_btf_id_type, and
mixed_rdonly_mem_btf_id_type tests, and the mixed_mem_mem_type,
mixed_map_value_mem_type, and mixed_stack_mem_type tests cover the new
PTR_TO_MEM mismatch rule from fbffa57329b8.
However, the other two commits appear untested:
bcfd0d6a160f changed check_ptr_to_btf_access() from checking
type_flag(reg->type) & PTR_UNTRUSTED to calling
bpf_may_fault_on_deref(), so a BPF_STX through a bare PTR_TO_BTF_ID is
now rejected with "only read is supported". The changelog documents a
reachable bpf_qdisc panic (next = sch->next_sched; next->limit = 1000;),
but no __failure / __msg("only read is supported") case is added here or
in progs/bpf_qdisc_*.c. Only verifier_global_ptr_args.c and
verifier_netfilter_ctx.c assert that message, and both do so for
PTR_UNTRUSTED pointers that the old check already caught, so the
newly-closed hole is untested.
bef47cf76444 states that PTR_TO_BTF_ID|PTR_UNTRUSTED|MEM_ALLOC|NON_OWN_REF
and PTR_TO_BTF_ID|PTR_UNTRUSTED|MEM_RCU were reachable and left as plain
loads (two Fixes: tags). The added tests only produce
ringbuf/rdonly/bare-BTF-ID types, all of which the old exact-match list
in bpf_convert_ctx_accesses() already handled or which the merge path
covers.
Would it make sense to add tests for the bcfd0d6a160f store-rejection path
and the bef47cf76444 MEM_ALLOC|NON_OWN_REF and MEM_RCU cases here?
---
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/31845722500
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 3/6] bpf: Reject a store through a fault prone pointer
2026-08-14 22:40 ` bot+bpf-ci
@ 2026-08-14 23:26 ` Daniel Borkmann
0 siblings, 0 replies; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 23:26 UTC (permalink / raw)
To: bot+bpf-ci, eddyz87
Cc: memxor, bpf, ast, andrii, martin.lau, yonghong.song, clm,
ihor.solodrai
On 8/15/26 12:40 AM, bot+bpf-ci@kernel.org wrote:
>> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
>> index 814096ed835d..cbbb0c82d22c 100644
>> --- a/kernel/bpf/verifier.c
>> +++ b/kernel/bpf/verifier.c
>> @@ -5788,7 +5788,7 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
>> return -EACCES;
>> }
>>
>> - if (atype != BPF_READ && (type_flag(reg->type) & PTR_UNTRUSTED)) {
>> + if (atype != BPF_READ && bpf_may_fault_on_deref(reg->type)) {
>> verbose(env, "only read is supported\n");
>> return -EACCES;
>> }
>
> Does this change also reject HID-BPF writes to struct hid_device fields?
>
> The HID struct_ops hid_bpf_ops_btf_struct_access() whitelists writes to
> hid_device::name, ::uniq, and ::phys via WRITE_RANGE entries in
> drivers/hid/bpf/hid_bpf_struct_ops.c. A HID program receives a
> 'struct hid_bpf_ctx *' argument that is PTR_TO_BTF_ID | PTR_TRUSTED, but
> walking hctx->hid produces a bare PTR_TO_BTF_ID because
> struct hid_bpf_ctx appears in no BTF_TYPE_SAFE_TRUSTED list in
> check_ptr_to_btf_access().
Hm, that doesn't seem right.. potentially sth like:
diff --git a/drivers/hid/bpf/hid_bpf_struct_ops.c b/drivers/hid/bpf/hid_bpf_struct_ops.c
index 702c22fae136..56c53aca4511 100644
--- a/drivers/hid/bpf/hid_bpf_struct_ops.c
+++ b/drivers/hid/bpf/hid_bpf_struct_ops.c
@@ -62,6 +62,10 @@ struct hid_bpf_offset_write_range {
u32 end;
};
+struct hid_bpf_ctx__safe_trusted {
+ struct hid_device *hid;
+};
+
static int hid_bpf_ops_btf_struct_access(struct bpf_verifier_log *log,
const struct bpf_reg_state *reg,
int off, int size)
@@ -86,6 +90,8 @@ static int hid_bpf_ops_btf_struct_access(struct bpf_verifier_log *log,
const char *cur = NULL;
int i;
+ BTF_TYPE_EMIT(struct hid_bpf_ctx__safe_trusted);
+
t = btf_type_by_id(reg->btf, reg->btf_id);
for (i = 0; i < ARRAY_SIZE(write_ranges); i++) {
^ permalink raw reply related [flat|nested] 14+ messages in thread
* Re: [PATCH bpf-next v2 5/6] selftests/bpf: Add tests for pointer type merge at a shared load
2026-08-14 22:56 ` bot+bpf-ci
@ 2026-08-14 23:35 ` Daniel Borkmann
0 siblings, 0 replies; 14+ messages in thread
From: Daniel Borkmann @ 2026-08-14 23:35 UTC (permalink / raw)
To: bot+bpf-ci, eddyz87
Cc: memxor, bpf, ast, andrii, martin.lau, yonghong.song, clm,
ihor.solodrai
On 8/15/26 12:56 AM, bot+bpf-ci@kernel.org wrote:
[...]
> However, the other two commits appear untested:
>
> bcfd0d6a160f changed check_ptr_to_btf_access() from checking
> type_flag(reg->type) & PTR_UNTRUSTED to calling
> bpf_may_fault_on_deref(), so a BPF_STX through a bare PTR_TO_BTF_ID is
> now rejected with "only read is supported". The changelog documents a
> reachable bpf_qdisc panic (next = sch->next_sched; next->limit = 1000;),
> but no __failure / __msg("only read is supported") case is added here or
> in progs/bpf_qdisc_*.c. Only verifier_global_ptr_args.c and
> verifier_netfilter_ctx.c assert that message, and both do so for
> PTR_UNTRUSTED pointers that the old check already caught, so the
> newly-closed hole is untested.
I can either follow-up together with the hid-bpf one or do a v3 and then add
also a qdisc write test if desired.
> bef47cf76444 states that PTR_TO_BTF_ID|PTR_UNTRUSTED|MEM_ALLOC|NON_OWN_REF
> and PTR_TO_BTF_ID|PTR_UNTRUSTED|MEM_RCU were reachable and left as plain
> loads (two Fixes: tags). The added tests only produce
> ringbuf/rdonly/bare-BTF-ID types, all of which the old exact-match list
> in bpf_convert_ctx_accesses() already handled or which the merge path
> covers.
Covered by the selftest in patch 6/6 (PTR_TO_BTF_ID | MEM_ALLOC | PTR_UNTRUSTED |
NON_OWN_REF and PTR_TO_BTF_ID | PTR_UNTRUSTED | MEM_RCU).
> Would it make sense to add tests for the bcfd0d6a160f store-rejection path
> and the bef47cf76444 MEM_ALLOC|NON_OWN_REF and MEM_RCU cases here?
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-08-14 23:35 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-14 21:52 [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 2/6] bpf: Treat a fault prone PTR_TO_MEM as a pointer type mismatch Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 3/6] bpf: Reject a store through a fault prone pointer Daniel Borkmann
2026-08-14 22:40 ` bot+bpf-ci
2026-08-14 23:26 ` Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 4/6] bpf: Rewrite any fault prone load out of a mem or btf_id pointer Daniel Borkmann
2026-08-14 21:52 ` [PATCH bpf-next v2 5/6] selftests/bpf: Add tests for pointer type merge at a shared load Daniel Borkmann
2026-08-14 22:56 ` bot+bpf-ci
2026-08-14 23:35 ` Daniel Borkmann
2026-08-14 21:53 ` [PATCH bpf-next v2 6/6] selftests/bpf: Add tests for fault prone loads out of RCU pointers Daniel Borkmann
2026-08-14 22:03 ` sashiko-bot
2026-08-14 22:28 ` Daniel Borkmann
2026-08-14 22:15 ` [PATCH bpf-next v2 1/6] bpf: Keep fault protection when merging pointer types sashiko-bot
2026-08-14 22:19 ` Daniel Borkmann
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox