Netdev List
 help / color / mirror / Atom feed
* [PATCH bpf-next v2 0/2] bpf: Reject packet dynptr slices in mutating helpers and subprograms
@ 2026-10-18  5:44 Ren Wei
  2026-10-18  5:44 ` [PATCH bpf-next v2 1/2] " Ren Wei
  2026-10-18  5:44 ` [PATCH bpf-next v2 2/2] selftests/bpf: Test rejection of packet dynptr slices in mutating helpers Ren Wei
  0 siblings, 2 replies; 3+ messages in thread
From: Ren Wei @ 2026-10-18  5:44 UTC (permalink / raw)
  To: bpf, netdev, ast
  Cc: daniel, andrii, eddyz87, memxor, martin.lau, song, yonghong.song,
	jolsa, emil, ihor.solodrai, john.fastabend, davem, kuba, hawk,
	sdf, drosen, ameryhung, xyx2021, chenyy23, vega, rakukuip, weir

From: Luxiao Xu <rakukuip@gmail.com>

Hi Linux kernel maintainers,

We found and validated a slab-use-after-free issue in the BPF verifier
(kernel/bpf/verifier.c) affecting packet-mutating helpers and
subprograms.

Impact Scope:
When an skb-backed dynptr slice (PTR_TO_MEM tagged with
DYNPTR_TYPE_SKB, DYNPTR_TYPE_XDP, or DYNPTR_TYPE_SKB_META) is passed
to helpers or subprograms that mutate packet data (e.g.
bpf_lwt_push_encap(), bpf_skb_store_bytes(), or bpf_dynptr_write()
into an skb dynptr), reallocation of the packet buffer via
skb_cow_head() or pskb_expand_head() frees the old head and
invalidates the slice during helper execution, leading to a
slab-use-after-free read or write.

Privilege Requirements:
Triggering this bug requires root privileges (UID 0 / CAP_NET_ADMIN
and CAP_BPF) to load BPF LWT programs, configure network dummy
devices, attach LWT encap routes, and adjust sysctl parameters. The
reproducer script and crash log reflect this privileged execution
context.

Testing & Validation Results:
The slab-use-after-free was reproduced on unpatched kernels using the
enclosed LWT reproducer, triggering a KASAN slab-use-after-free crash
under UID 0. With this v2 patch series applied, the verifier rejects
loading such programs at program load time with -EINVAL, preventing
the crash. We also verified compilation and ran negative verifier
selftests, all passing with zero regressions.

This bug is tracked at: https://bugtracker.nebusec.ai/f/11304
Closes: https://bugtracker.nebusec.ai/f/11304

In v1, we proposed a local workaround in net/core/lwt_bpf.c to buffer the
encap header across skb_cow_head(). As Alexei Starovoitov pointed out,
patching lwt_bpf.c alone only treats a symptom; any helper mutating
packet data (e.g. BPF_LWT_ENCAP_SEG6, bpf_skb_store_bytes(), or
bpf_dynptr_write() into an skb dynptr) suffers from the exact same
use-after-free read or write when the skb head buffer is reallocated.

The architectural root cause is that the verifier rejects raw packet
pointers (PTR_TO_PACKET) for mutating helpers because they do not have
pkt_access, but allows an skb-backed dynptr slice (PTR_TO_MEM tagged with
DYNPTR_TYPE_SKB, DYNPTR_TYPE_XDP, or DYNPTR_TYPE_SKB_META) through
check_helper_mem_access() ever since commit 2012c867c800 ("bpf:
verifier: Accept dynptr mem as mem in helpers") stripped
DYNPTR_TYPE_FLAG_MASK for ARG_PTR_TO_MEM.

This series resolves the problem across mutating helpers and subprograms:
- Patch 1 rejects reg_is_dynptr_slice_pkt() arguments for helpers that
  change packet data in check_func_arg() (mirroring commit a6c1edfbe240
  ("bpf: Reject pkt arguments in mutating subprogs") for global
  subprograms). In addition, it models bpf_dynptr_copy and
  bpf_dynptr_memset as packet-changing when targeting skb dynptrs,
  marks subprograms invoking them as changes_pkt_data during CFG
  analysis, rejects passing packet dynptrs to mutating subprograms,
  provides a stack fallback for non-linear skb dynptr copies, and
  updates test_xdp_meta.c.
- Patch 2 adds dedicated negative tests in dynptr_fail.c.

Note on CFG and subprog semantics: CFG analysis conservatively marks a
subprogram that invokes bpf_dynptr_write (or a packet-changing dynptr
kfunc) as changes_pkt_data, without inspecting the dynptr's runtime
type. This is required because CFG analysis runs before do_check() and
has no register or stack state to reason about, and because a global
subprogram's dynptr parameter type-erases to BPF_DYNPTR_TYPE_LOCAL
during its isolated verification. This safe over-approximation is what
prevents an skb dynptr from reaching a subprogram that could reallocate
skb->head. The cost is that a caller holding a direct packet pointer
across such a subprogram call must re-read ctx->data/ctx->data_end
afterwards, which is the standard BPF verifier re-validation
requirement. We prefer soundness over completeness here, matching the
conservative CFG marking used by commit a6c1edfbe240 ("bpf: Reject pkt
arguments in mutating subprogs"). This is a verification-time
restriction only; runtime behavior of accepted programs is unchanged.

v1 -> v2:
- v1 Link:
  https://lore.kernel.org/all/cover.1791433504.git.rakukuip@gmail.com/
- Retarget series to bpf-next tree instead of net (Alexei Starovoitov).
- Fix root cause in BPF verifier by rejecting reg_is_dynptr_slice_pkt()
  arguments in check_func_arg() for helpers changing packet data instead
  of patching individual helpers (Alexei Starovoitov).
- Correct Fixes tag to commit 2012c867c800 ("bpf: verifier: Accept dynptr
  mem as mem in helpers") which first accepted dynptr slices as helper
  arguments (Alexei Starovoitov).
- Model bpf_dynptr_copy and bpf_dynptr_memset as packet-changing in
  bpf_is_kfunc_pkt_changing() and CFG analysis when writing to skb
  dynptrs, ensuring clear_all_pkt_pointers() clears packet slices.
- Reject passing packet dynptrs to subprograms that may change packet
  data in check_func_arg(), preventing global subprograms from mutating
  skb dynptrs.
- In bpf_dynptr_copy(), fall back to safe chunked copying through a
  stack buffer when writing to non-linear skb dynptrs.
- Update ing_cls_dynptr_write() in test_xdp_meta.c to use a stack buffer
  (Alexei Starovoitov).
- Add dedicated negative tests in dynptr_fail.c (Alexei Starovoitov).

=== details and PoC below ===

Bug details:

The BPF verifier permits an skb-backed bpf_dynptr_slice() pointer to
satisfy the helper's ARG_PTR_TO_MEM | MEM_RDONLY header argument.

In bpf_lwt_push_ip_encap(), skb_cow_head() is called to expand headroom.
If the skb headroom is insufficient or the skb is cloned,
skb_cow_head() invokes pskb_expand_head(), which allocates a new head
buffer and frees the old one. If the encapsulation header pointer hdr
points into the skb head, it becomes stale after reallocation.
Subsequent reads of iph and hdr in skb_postpush_rcsum(), UDP tunnel
header handling, and memcpy() lead to a use-after-free read.

The same issue affects BPF_LWT_ENCAP_SEG6 mode, bpf_skb_store_bytes()
when offset is in frags, bpf_dynptr_write() into skb dynptrs, and the
rest of seg6 helpers.

Reproducer:

The reproducer requires root privileges (UID 0 / CAP_NET_ADMIN and
CAP_BPF) and executes in a root shell. It attaches an LWT_XMIT BPF program
to an IPv4 route via "encap bpf xmit". The BPF program obtains an
skb-backed dynptr slice and calls bpf_lwt_push_encap(skb,
BPF_LWT_ENCAP_IP, hdr, 256). A subsequent packet transmission triggers
skb_cow_head() reallocation, resulting in the KASAN slab-use-after-free
under UID 0 shown in the crash log below.

=== BEGIN poc.bpf.c ===
    // SPDX-License-Identifier: GPL-2.0
    #include <linux/bpf.h>
    #include <linux/ip.h>

    #define HDR_LEN 256

    #define SEC(name) __attribute__((section(name), used))
    #define __ksym __attribute__((section(".ksyms")))
    #define __weak __attribute__((weak))

    extern int bpf_dynptr_from_skb(struct __sk_buff *skb, __u64 flags,
    			       struct bpf_dynptr *ptr__uninit) __ksym __weak;
    extern void *bpf_dynptr_slice(const struct bpf_dynptr *ptr,
				  __u32 offset, void *buffer,
				  __u32 buffer__szk) __ksym __weak;

    static long (*bpf_lwt_push_encap)(struct __sk_buff *skb, __u32 type,
    				  void *hdr, __u32 len) =
    	(void *)BPF_FUNC_lwt_push_encap;

    SEC("lwt_xmit")
    int trigger(struct __sk_buff *skb)
    {
    	struct bpf_dynptr ptr;
    	void *hdr;

    	if (bpf_dynptr_from_skb(skb, 0, &ptr))
    		return BPF_OK;

    	if (skb->len < HDR_LEN)
    		return BPF_OK;

    	hdr = bpf_dynptr_slice(&ptr, 0, NULL, HDR_LEN);
    	if (!hdr)
    		return BPF_OK;

    	bpf_lwt_push_encap(skb, BPF_LWT_ENCAP_IP, hdr, HDR_LEN);
    	return BPF_OK;
    }

    char _license[] SEC("license") = "GPL";
=== END poc.bpf.c ===

=== BEGIN poc.sh ===
    #!/bin/sh
    set -eu

    DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
    ROUTE_CIDR=${ROUTE_CIDR:-10.0.0.0/24}
    SRC_IP=${SRC_IP:-10.0.0.1}
    DST_IP=${DST_IP:-10.0.0.2}
    PING_SIZE=${PING_SIZE:-400}
    PING_COUNT=${PING_COUNT:-10}
    DEV=${DEV:-dummy0}

    cleanup() {
    	ip route del "$ROUTE_CIDR" 2>/dev/null || true
    	ip link del "$DEV" 2>/dev/null || true
    	ip addr del "$SRC_IP/32" dev lo 2>/dev/null || true
    }

    trap cleanup EXIT

    echo 0 > /proc/sys/kernel/panic_on_warn

    ip route del "$ROUTE_CIDR" 2>/dev/null || true
    ip link del "$DEV" 2>/dev/null || true
    ip addr del "$SRC_IP/32" dev lo 2>/dev/null || true

    ip link add "$DEV" type dummy
    ip link set lo up
    ip addr add "$SRC_IP/32" dev lo
    ip link set "$DEV" up
    ip route add "$ROUTE_CIDR" dev "$DEV" \
        encap bpf xmit obj "$DIR/poc.bpf.o" sec lwt_xmit

    i=1
    while [ "$i" -le "$PING_COUNT" ]; do
    	ping -c 1 -W 1 -s "$PING_SIZE" -I "$SRC_IP" "$DST_IP" \
    		>/dev/null 2>&1 || true
    	i=$((i + 1))
    done
=== END poc.sh ===

=== BEGIN crash log ===
    BUG: KASAN: slab-use-after-free in bpf_lwt_push_ip_encap+0x805/0x1ac0
    Read of size 256 at addr ffff88810c74e410 by task ping/10729
    CPU: 3 UID: 0 PID: 10729 Comm: ping Not tainted 6.12.95 #2
    Hardware name: QEMU Ubuntu 24.04 PC v2, BIOS 1.16.3-debian-1.16.3-2 04/01/2014
    Call Trace:
     <TASK>
     dump_stack_lvl+0x78/0xe0
     print_report+0xc6/0x620
     ? bpf_lwt_push_ip_encap+0x805/0x1ac0
     ? srso_alias_return_thunk+0x5/0xfbef5
     ? __virt_addr_valid+0x1f3/0x3d0
     ? bpf_lwt_push_ip_encap+0x805/0x1ac0
     kasan_report+0xd8/0x110
     ? bpf_lwt_push_ip_encap+0x805/0x1ac0
     kasan_check_range+0xf4/0x1a0
     __asan_memcpy+0x23/0x60
     bpf_lwt_push_ip_encap+0x805/0x1ac0
     ? srso_alias_return_thunk+0x5/0xfbef5
     bpf_lwt_xmit_push_encap+0x2b/0x40
     bpf_prog_62cb242b810c4a7f_trigger+0x6b/0x74
     run_lwt_bpf.isra.0+0x32f/0x8c0
     ? lwtunnel_xmit+0x102/0x4e0
     bpf_xmit+0x139/0x380
     lwtunnel_xmit+0x1f7/0x4e0
     ip_finish_output2+0x8be/0x1eb0
     ip_output+0x171/0x3b0
     ip_push_pending_frames+0x1e6/0x250
     ping_v4_sendmsg+0x719/0x16e0
     __sys_sendto+0x32e/0x3a0
     __x64_sys_sendto+0xe0/0x1c0
     do_syscall_64+0xc7/0x270
     entry_SYSCALL_64_after_hwframe+0x77/0x7f
     </TASK>
=== END crash log ===

Luxiao Xu (2):
  bpf: Reject packet dynptr slices in mutating helpers and subprograms
  selftests/bpf: Test rejection of packet dynptr slices in mutating
    helpers

 include/linux/bpf_verifier.h                  |  1 +
 kernel/bpf/cfg.c                              |  3 +-
 kernel/bpf/helpers.c                          |  7 +-
 kernel/bpf/verifier.c                         | 81 ++++++++++++++---
 .../testing/selftests/bpf/progs/dynptr_fail.c | 90 +++++++++++++++++++
 .../selftests/bpf/progs/test_xdp_meta.c       |  4 +-
 6 files changed, 170 insertions(+), 16 deletions(-)

-- 
2.43.0


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

* [PATCH bpf-next v2 1/2] bpf: Reject packet dynptr slices in mutating helpers and subprograms
  2026-10-18  5:44 [PATCH bpf-next v2 0/2] bpf: Reject packet dynptr slices in mutating helpers and subprograms Ren Wei
@ 2026-10-18  5:44 ` Ren Wei
  2026-10-18  5:44 ` [PATCH bpf-next v2 2/2] selftests/bpf: Test rejection of packet dynptr slices in mutating helpers Ren Wei
  1 sibling, 0 replies; 3+ messages in thread
From: Ren Wei @ 2026-10-18  5:44 UTC (permalink / raw)
  To: bpf, netdev, ast
  Cc: daniel, andrii, eddyz87, memxor, martin.lau, song, yonghong.song,
	jolsa, emil, ihor.solodrai, john.fastabend, davem, kuba, hawk,
	sdf, drosen, ameryhung, xyx2021, chenyy23, vega, rakukuip, weir

From: Luxiao Xu <rakukuip@gmail.com>

When a helper or subprogram changes packet data (e.g. bpf_lwt_push_encap(),
bpf_skb_store_bytes(), or bpf_dynptr_write() into an skb dynptr),
the packet head buffer may be reallocated, for example via
skb_cow_head() or pskb_expand_head().

While direct packet pointers (PTR_TO_PACKET) are rejected for these
helpers because they do not have pkt_access, an skb-backed dynptr
slice (PTR_TO_MEM tagged with DYNPTR_TYPE_SKB, DYNPTR_TYPE_XDP, or
DYNPTR_TYPE_SKB_META) was permitted by check_helper_mem_access() after
commit 2012c867c800 ("bpf: verifier: Accept dynptr mem as mem in
helpers") stripped DYNPTR_TYPE_FLAG_MASK for ARG_PTR_TO_MEM.

If a dynptr slice pointing to packet data is passed to a mutating
helper, reallocation of the packet head invalidates the slice while
the helper is executing, leading to a use-after-free read or write.

Reject reg_is_dynptr_slice_pkt() (as well as reg_is_pkt_pointer_any())
arguments in helpers that change packet data in check_func_arg(),
mirroring the pattern used for global subprograms in commit
a6c1edfbe240 ("bpf: Reject pkt arguments in mutating subprogs").

In addition:
- Model bpf_dynptr_copy and bpf_dynptr_memset as packet-changing in
  bpf_is_kfunc_pkt_changing() when writing to skb dynptrs, ensuring
  clear_all_pkt_pointers() clears existing packet slices across calls.
- Mark subprograms invoking BPF_FUNC_dynptr_write, bpf_dynptr_copy, or
  bpf_dynptr_memset as changes_pkt_data during CFG analysis.
- Reject passing packet dynptrs to subprograms that may change packet
  data in check_func_arg(), preventing global subprograms (which assume
  unspecialized BPF_DYNPTR_TYPE_LOCAL) from mutating skb dynptrs and
  invalidating slices across mutating calls.
- In bpf_dynptr_copy(), when the destination dynptr is a packet-backed
  skb and non-linear, fall back to safe chunked copying through a local
  stack buffer instead of passing a slice directly to __bpf_dynptr_write().
- Update ing_cls_dynptr_write() in test_xdp_meta.c to copy data to a
  stack buffer before passing it to bpf_dynptr_write().

Fixes: 2012c867c800 ("bpf: verifier: Accept dynptr mem as mem in helpers")
Cc: stable@vger.kernel.org
Reported-by: VEGA <vega@nebusec.ai>
Closes: https://bugtracker.nebusec.ai/f/11304
Assisted-by: LLM
Signed-off-by: Luxiao Xu <rakukuip@gmail.com>
Signed-off-by: Ren Wei <weir@nebusec.ai>
---
v1 -> v2:
- v1 Link: https://lore.kernel.org/all/68db88ada2fd01fc03f17dc7d54a705aa245ddea.1791433504.git.rakukuip@gmail.com/
- Retarget to bpf-next tree instead of net (Alexei Starovoitov).
- Reject reg_is_dynptr_slice_pkt() in check_func_arg() for helpers changing
  packet data instead of patching individual helpers (Alexei Starovoitov).
- Correct Fixes tag to commit 2012c867c800 ("bpf: verifier: Accept dynptr
  mem as mem in helpers") (Alexei Starovoitov).
- Model bpf_dynptr_copy and bpf_dynptr_memset as packet-changing in
  bpf_is_kfunc_pkt_changing() and CFG analysis when writing to skb dynptrs.
- Reject passing packet dynptrs to subprograms that may change packet
  data in check_func_arg().
- In bpf_dynptr_copy(), use a stack buffer fallback when writing to
  non-linear skb dynptrs.
- Update ing_cls_dynptr_write() in test_xdp_meta.c (Alexei Starovoitov).
 include/linux/bpf_verifier.h                  |  1 +
 kernel/bpf/cfg.c                              |  3 +-
 kernel/bpf/helpers.c                          |  7 +-
 kernel/bpf/verifier.c                         | 81 ++++++++++++++++---
 .../selftests/bpf/progs/test_xdp_meta.c       |  4 +-
 5 files changed, 80 insertions(+), 16 deletions(-)

diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 112ca1511da4..117d352aeb9f 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1784,6 +1784,7 @@ struct bpf_call_arg_meta {
 	u32 subprogno;
 	struct bpf_map_desc map;
 	struct bpf_dynptr_desc dynptr;
+	enum bpf_dynptr_type dynptr_dst_type;
 	struct ref_obj_desc ref_obj;
 	struct ret_mem_desc ret_mem;
 	struct arg_raw_mem_desc arg_raw_mem;
diff --git a/kernel/bpf/cfg.c b/kernel/bpf/cfg.c
index bce7b5abcfda..9382d1d82129 100644
--- a/kernel/bpf/cfg.c
+++ b/kernel/bpf/cfg.c
@@ -598,7 +598,8 @@ static int visit_insn(int t, struct bpf_verifier_env *env)
 			 */
 			if (ret == 0 && fp->might_sleep)
 				mark_subprog_might_sleep(env, t);
-			if (bpf_helper_changes_pkt_data(insn->imm))
+			if (bpf_helper_changes_pkt_data(insn->imm) ||
+			    insn->imm == BPF_FUNC_dynptr_write)
 				mark_subprog_changes_pkt_data(env, t);
 		} else if (insn->src_reg == BPF_PSEUDO_KFUNC_CALL) {
 			struct bpf_call_arg_meta meta;
diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index 439370d73cc5..fcf0cde53f1b 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -3272,6 +3272,11 @@ __bpf_kfunc int bpf_dynptr_clone(const struct bpf_dynptr *p,
 	return 0;
 }
 
+static bool dynptr_type_is_skb(enum bpf_dynptr_type type)
+{
+	return type == BPF_DYNPTR_TYPE_SKB || type == BPF_DYNPTR_TYPE_SKB_META;
+}
+
 /**
  * bpf_dynptr_copy() - Copy data from one dynptr to another.
  * @dst_ptr: Destination dynptr - where data should be copied to
@@ -3300,7 +3305,7 @@ __bpf_kfunc int bpf_dynptr_copy(const struct bpf_dynptr *dst_ptr, u64 dst_off,
 		return 0;
 	}
 
-	if (src_slice)
+	if (src_slice && !dynptr_type_is_skb(bpf_dynptr_get_type(dst)))
 		return __bpf_dynptr_write(dst, dst_off, src_slice, size, 0);
 
 	if (dst_slice)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index ec136cf377c1..d1a1a37e1f47 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -8477,6 +8477,9 @@ static int process_dynptr_func(struct bpf_verifier_env *env, struct bpf_reg_stat
 			reg = &bpf_stack_slot(state, spi)->spilled_ptr;
 		}
 
+		if (!meta->dynptr_dst_type)
+			meta->dynptr_dst_type = reg->dynptr.type;
+
 		meta->dynptr.type = reg->dynptr.type;
 		meta->dynptr.id = reg->id;
 		meta->dynptr.parent_id = reg->parent_id;
@@ -9616,6 +9619,24 @@ static enum bpf_access_type func_arg_access_type(enum bpf_arg_type arg_type)
 	return BPF_READ | BPF_WRITE;
 }
 
+static bool helper_changes_pkt_data(struct bpf_verifier_env *env,
+				    enum bpf_func_id func_id,
+				    const struct bpf_call_arg_meta *meta)
+{
+	if (bpf_helper_changes_pkt_data(func_id))
+		return true;
+
+	if (func_id == BPF_FUNC_dynptr_write) {
+		enum bpf_dynptr_type type = meta->dynptr_dst_type ?: meta->dynptr.type;
+
+		return type == BPF_DYNPTR_TYPE_SKB ||
+		       type == BPF_DYNPTR_TYPE_SKB_META ||
+		       (type == BPF_DYNPTR_TYPE_LOCAL && env->cur_state->curframe);
+	}
+
+	return false;
+}
+
 static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 prev_slot,
 			  struct bpf_call_arg_meta *meta,
 			  int insn_idx)
@@ -9903,6 +9924,22 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
 		enum bpf_access_type access_type;
 		bool known_memory;
 
+		/*
+		 * PTR_TO_PACKET gets passed as PTR_TO_MEM, preventing us from adjusting
+		 * bounds tracking information.
+		 */
+		if (((is_subprog(meta) && meta->subprog_may_change_pkt) ||
+		     (is_kfunc(meta) ? bpf_is_kfunc_pkt_changing(meta) :
+				       helper_changes_pkt_data(env, meta->func_id, meta))) &&
+		    (reg_is_pkt_pointer_any(reg) || reg_is_dynptr_slice_pkt(reg))) {
+			verbose(env,
+				"cannot pass packet pointer %s to %s(): function may change packet data\n",
+				reg_arg_name(env, argno),
+				is_subprog(meta) ? meta->func_name :
+				(is_kfunc(meta) ? meta->func_name : func_id_name(meta->func_id)));
+			return -EINVAL;
+		}
+
 		/* The access to this pointer is only checked when we hit the
 		 * next is_mem_size argument below.
 		 */
@@ -9927,17 +9964,7 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
 					bpf_diag_reg_type_plain(env, reg->type));
 			return err;
 		}
-		/*
-		 * PTR_TO_PACKET gets passed as PTR_TO_MEM, preventing us from adjusting
-		 * bounds tracking information.
-		 */
-		if (is_subprog(meta) && meta->subprog_may_change_pkt &&
-		    (reg_is_pkt_pointer_any(reg) || reg_is_dynptr_slice_pkt(reg))) {
-			verbose(env,
-				"cannot pass packet pointer %s to %s(): function may change packet data\n",
-				reg_arg_name(env, argno), meta->func_name);
-			return -EINVAL;
-		}
+
 		if (arg_type & MEM_ALIGNED)
 			err = check_ptr_alignment(env, reg, 0, arg_size, true);
 		break;
@@ -10005,6 +10032,17 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
 		break;
 	}
 	case ARG_PTR_TO_DYNPTR: {
+		enum bpf_dynptr_type dynptr_type = dynptr_reg_type(env, reg);
+
+		if (is_subprog(meta) && meta->subprog_may_change_pkt &&
+		    (dynptr_type == BPF_DYNPTR_TYPE_SKB ||
+		     dynptr_type == BPF_DYNPTR_TYPE_SKB_META ||
+		     dynptr_type == BPF_DYNPTR_TYPE_XDP)) {
+			verbose(env,
+				"cannot pass packet dynptr %s to %s(): function may change packet data\n",
+				reg_arg_name(env, argno), meta->func_name);
+			return -EINVAL;
+		}
 		if (is_bpf_dynptr_clone_kfunc(meta) &&
 		    (arg_type & MEM_UNINIT)) {
 			enum bpf_dynptr_type parent_type = meta->dynptr.type;
@@ -12541,7 +12579,8 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
 			return -EFAULT;
 
 		if (dynptr_type == BPF_DYNPTR_TYPE_SKB ||
-		    dynptr_type == BPF_DYNPTR_TYPE_SKB_META)
+		    dynptr_type == BPF_DYNPTR_TYPE_SKB_META ||
+		    (dynptr_type == BPF_DYNPTR_TYPE_LOCAL && env->cur_state->curframe))
 			/* this will trigger clear_all_pkt_pointers(), which will
 			 * invalidate all dynptr slices associated with the skb
 			 */
@@ -13355,6 +13394,8 @@ enum special_kfunc_type {
 	KF_bpf_dynptr_slice,
 	KF_bpf_dynptr_slice_rdwr,
 	KF_bpf_dynptr_clone,
+	KF_bpf_dynptr_copy,
+	KF_bpf_dynptr_memset,
 	KF_bpf_percpu_obj_new_impl,
 	KF_bpf_percpu_obj_new,
 	KF_bpf_percpu_obj_drop_impl,
@@ -13437,6 +13478,8 @@ BTF_ID_UNUSED
 BTF_ID(func, bpf_dynptr_slice)
 BTF_ID(func, bpf_dynptr_slice_rdwr)
 BTF_ID(func, bpf_dynptr_clone)
+BTF_ID(func, bpf_dynptr_copy)
+BTF_ID(func, bpf_dynptr_memset)
 BTF_ID(func, bpf_percpu_obj_new_impl)
 BTF_ID(func, bpf_percpu_obj_new)
 BTF_ID(func, bpf_percpu_obj_drop_impl)
@@ -13592,7 +13635,19 @@ static bool is_kfunc_bpf_preempt_enable(struct bpf_call_arg_meta *meta)
 
 bool bpf_is_kfunc_pkt_changing(struct bpf_call_arg_meta *meta)
 {
-	return is_kfunc_call(meta, special_kfunc_list[KF_bpf_xdp_pull_data]);
+	if (is_kfunc_call(meta, special_kfunc_list[KF_bpf_xdp_pull_data]))
+		return true;
+
+	if (is_kfunc_call(meta, special_kfunc_list[KF_bpf_dynptr_copy]) ||
+	    is_kfunc_call(meta, special_kfunc_list[KF_bpf_dynptr_memset])) {
+		enum bpf_dynptr_type type = meta->dynptr_dst_type ?: meta->dynptr.type;
+
+		return !type || type == BPF_DYNPTR_TYPE_SKB ||
+		       type == BPF_DYNPTR_TYPE_SKB_META ||
+		       type == BPF_DYNPTR_TYPE_LOCAL;
+	}
+
+	return false;
 }
 
 static u32 kfunc_abi_slots(const struct btf_func_model *fm)
diff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
index 08b03be0b891..e1e2a6bed625 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -95,6 +95,7 @@ SEC("tc")
 int ing_cls_dynptr_write(struct __sk_buff *ctx)
 {
 	struct bpf_dynptr data, meta;
+	__u8 buf[META_SIZE];
 	__u8 *src;
 
 	bpf_dynptr_from_skb(ctx, 0, &data);
@@ -103,7 +104,8 @@ int ing_cls_dynptr_write(struct __sk_buff *ctx)
 		return TC_ACT_SHOT;
 
 	bpf_dynptr_from_skb_meta(ctx, 0, &meta);
-	bpf_dynptr_write(&meta, 0, src, META_SIZE, 0);
+	__builtin_memcpy(buf, src, sizeof(buf));
+	bpf_dynptr_write(&meta, 0, buf, sizeof(buf), 0);
 
 	return TC_ACT_UNSPEC; /* pass */
 }
-- 
2.43.0


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

* [PATCH bpf-next v2 2/2] selftests/bpf: Test rejection of packet dynptr slices in mutating helpers
  2026-10-18  5:44 [PATCH bpf-next v2 0/2] bpf: Reject packet dynptr slices in mutating helpers and subprograms Ren Wei
  2026-10-18  5:44 ` [PATCH bpf-next v2 1/2] " Ren Wei
@ 2026-10-18  5:44 ` Ren Wei
  1 sibling, 0 replies; 3+ messages in thread
From: Ren Wei @ 2026-10-18  5:44 UTC (permalink / raw)
  To: bpf, netdev, ast
  Cc: daniel, andrii, eddyz87, memxor, martin.lau, song, yonghong.song,
	jolsa, emil, ihor.solodrai, john.fastabend, davem, kuba, hawk,
	sdf, drosen, ameryhung, xyx2021, chenyy23, vega, rakukuip, weir

From: Luxiao Xu <rakukuip@gmail.com>

Add negative verifier tests in dynptr_fail.c to verify that:
- Passing an skb-backed dynptr slice to bpf_lwt_push_encap() is rejected.
- Passing an skb-backed dynptr slice to bpf_skb_store_bytes() (with
  constant and variable length) is rejected.
- Passing an skb-backed dynptr slice to bpf_dynptr_write() is rejected.
- Passing a packet-backed dynptr to a mutating global subprogram is
  rejected.

Assisted-by: LLM
Signed-off-by: Luxiao Xu <rakukuip@gmail.com>
Signed-off-by: Ren Wei <weir@nebusec.ai>
---
v1 -> v2:
- v1 Link: https://lore.kernel.org/all/cover.1791433504.git.rakukuip@gmail.com/
- New patch in v2 series adding negative verifier tests in dynptr_fail.c
  (Alexei Starovoitov).
 .../testing/selftests/bpf/progs/dynptr_fail.c | 90 +++++++++++++++++++
 1 file changed, 90 insertions(+)

diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index c2247b38c849..11699d87ad2b 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -2279,3 +2279,93 @@ int dynptr_overwrite_ref_clone_slice_valid(void *ctx)
 
 	return 0;
 }
+
+/* Passing a packet dynptr slice to helper changing packet data should fail */
+SEC("?lwt_out")
+__failure __msg("cannot pass packet pointer")
+int slice_pkt_to_lwt_push_encap(struct __sk_buff *skb)
+{
+	struct bpf_dynptr ptr;
+	void *hdr;
+
+	bpf_dynptr_from_skb(skb, 0, &ptr);
+	hdr = bpf_dynptr_slice(&ptr, 0, NULL, 20);
+	if (!hdr)
+		return 1;
+
+	bpf_lwt_push_encap(skb, 0, hdr, 20);
+	return 0;
+}
+
+/* Passing a packet dynptr slice to bpf_skb_store_bytes should fail */
+SEC("?tc")
+__failure __msg("cannot pass packet pointer")
+int slice_pkt_to_skb_store_bytes(struct __sk_buff *skb)
+{
+	struct bpf_dynptr ptr;
+	void *from;
+
+	bpf_dynptr_from_skb(skb, 0, &ptr);
+	from = bpf_dynptr_slice(&ptr, 0, NULL, 10);
+	if (!from)
+		return 1;
+
+	bpf_skb_store_bytes(skb, 0, from, 10, 0);
+	return 0;
+}
+
+/* Passing a packet dynptr slice to bpf_dynptr_write should fail */
+SEC("?tc")
+__failure __msg("cannot pass packet pointer")
+int slice_pkt_to_dynptr_write(struct __sk_buff *skb)
+{
+	struct bpf_dynptr ptr;
+	void *from;
+
+	bpf_dynptr_from_skb(skb, 0, &ptr);
+	from = bpf_dynptr_slice(&ptr, 0, NULL, 10);
+	if (!from)
+		return 1;
+
+	bpf_dynptr_write(&ptr, 0, from, 10, 0);
+	return 0;
+}
+
+__noinline int mutating_global_subprog(struct bpf_dynptr *ptr)
+{
+	char buf[4] = {0};
+
+	return bpf_dynptr_write(ptr, 0, buf, sizeof(buf), 0);
+}
+
+/* Passing a packet dynptr to a mutating global subprog should fail */
+SEC("?tc")
+__failure __msg("cannot pass packet dynptr")
+int packet_dynptr_to_mutating_subprog(struct __sk_buff *skb)
+{
+	struct bpf_dynptr ptr;
+
+	bpf_dynptr_from_skb(skb, 0, &ptr);
+	return mutating_global_subprog(&ptr);
+}
+
+/* Passing a packet dynptr slice with variable length to bpf_skb_store_bytes should fail */
+SEC("?tc")
+__failure __msg("cannot pass packet pointer")
+int slice_pkt_to_skb_store_bytes_var_len(struct __sk_buff *skb)
+{
+	struct bpf_dynptr ptr;
+	__u32 len = skb->len;
+	void *from;
+
+	if (len > 10)
+		len = 10;
+
+	bpf_dynptr_from_skb(skb, 0, &ptr);
+	from = bpf_dynptr_slice(&ptr, 0, NULL, 10);
+	if (!from)
+		return 1;
+
+	bpf_skb_store_bytes(skb, 0, from, len, 0);
+	return 0;
+}
-- 
2.43.0


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

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

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-18  5:44 [PATCH bpf-next v2 0/2] bpf: Reject packet dynptr slices in mutating helpers and subprograms Ren Wei
2026-10-18  5:44 ` [PATCH bpf-next v2 1/2] " Ren Wei
2026-10-18  5:44 ` [PATCH bpf-next v2 2/2] selftests/bpf: Test rejection of packet dynptr slices in mutating helpers Ren Wei

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