BPF List
 help / color / mirror / Atom feed
* [PATCH bpf-next v4 0/2] bpf: Track iterator-owned BTF pointer lifetimes
@ 2026-09-17  5:19 Xu Yunxiang
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
  2026-09-17  5:19 ` [PATCH bpf-next v4 2/2] selftests/bpf: Test iterator " Xu Yunxiang
  0 siblings, 2 replies; 6+ messages in thread
From: Xu Yunxiang @ 2026-09-17  5:19 UTC (permalink / raw)
  To: bpf
  Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, yonghong.song,
	puranjay

Associate non-RCU iterator BTF results and their trusted fields with the
lifetime that protects them. Check child-resource cleanup and invalidate
previous results before modeling the next iterator result.

Use the source register lifetime in the common dynptr constructor path,
while preserving clone relationships and explicit-release ownership.

Changes in v4:
  - Rebase onto bpf-next as requested by Amery Hung.
  - Keep update_ref_obj() and reg_is_referenced() unchanged; introduce
    reg_lifetime_id() for owned and borrowed source lifetimes.
  - Use the common constructor path instead of a FILE-only special case.
  - Capture the source lifetime before marking a BTF field destination.
  - Move next-time invalidation into process_iter_arg().
  - Share the child-reference leak check with release_reference(), while
    removing only the initially released ID from acquired references.
  - Retain the existing lifetime selftests on the new base.

Previous posting:
https://lore.kernel.org/r/20260911084254.3481508-1-xyx2021@mail.ustc.edu.cn

Review that prompted this revision:
https://lore.kernel.org/r/CAMB2axMX07j49sZRmGFm2s==FMgKFzvKPWxc8hDvU=b3Lp=VOg@mail.gmail.com

This local v4 refresh is based on bpf-next 10c4f610b215.
The code and tests are unchanged by the rebase. The Fixes tag is scoped
to the task_vma snapshot implementation and its file reference.

Validation on this exact candidate, with a matching bpf_testmod:
  - W=1 verifier object and complete kernel/modules builds passed.
  - test_progs -t iters: 109 subtests passed, no skips or failures.
  - test_progs -t dynptr: 132 subtests passed, no skips or failures.
  - test_progs -t file_reader: 8 subtests passed, no skips or failures.

The first iters run aborted because the guest lacked libgcc_s.so.1 for
pthread_exit. After adding it, only iters was rerun, with the kernel,
runner and module unchanged. No kernel WARN, Oops or panic was found.
The full unfiltered BPF suite and sanitizer configurations were not run.
The new iterator cases assert verifier load outcomes and diagnostics;
they do not assert program execution return values.

Xu Yunxiang (2):
  bpf: Track iterator-owned BTF pointer lifetimes
  selftests/bpf: Test iterator BTF pointer lifetimes

 kernel/bpf/verifier.c                         |  88 ++++-
 .../selftests/bpf/progs/iters_testmod.c       | 332 ++++++++++++++++++
 2 files changed, 405 insertions(+), 15 deletions(-)


base-commit: 10c4f610b215bf961235141161992f010cf7e451
-- 
2.43.0


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

* [PATCH bpf-next v4 1/2] bpf: Track iterator-owned BTF pointer lifetimes
  2026-09-17  5:19 [PATCH bpf-next v4 0/2] bpf: Track iterator-owned BTF pointer lifetimes Xu Yunxiang
@ 2026-09-17  5:19 ` Xu Yunxiang
  2026-09-17  5:44   ` sashiko-bot
                     ` (2 more replies)
  2026-09-17  5:19 ` [PATCH bpf-next v4 2/2] selftests/bpf: Test iterator " Xu Yunxiang
  1 sibling, 3 replies; 6+ messages in thread
From: Xu Yunxiang @ 2026-09-17  5:19 UTC (permalink / raw)
  To: bpf
  Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, yonghong.song,
	puranjay

Non-RCU struct iterator results borrow their lifetime from the iterator's
current element. Associate these PTR_TO_BTF_ID results with the iterator
reference and invalidate the previous result when processing the next
iterator argument. This applies before modeling either next outcome;
destroy continues to invalidate the result through reference release.

Preserve that lifetime when loading a fully trusted BTF field. Capture the
source lifetime before marking the destination, as the registers can
alias. Keep RCU, user and percpu field results under their existing rules.

A borrowed field can also back a dynptr. For example, a FILE dynptr owns
reader state but borrows its file. Record the backing register's lifetime
in the common dynptr constructor path, using the acquired ID for an owned
reference and parent_id for a borrowed pointer. Keep clone handling and
the ownership meaning of reg_is_referenced() unchanged.

Reuse the existing child-reference leak check before advancing an iterator
and propagate destroy errors. Outstanding child resources must be
explicitly released before their backing lifetime ends. Preserve the
existing rule that only the initial ID is removed from acquired references
when release_reference() invalidates descendants.

This issue was found during BPF verifier testing with an in-house runtime
semantic checker.

Fixes: 4cbee026db54 ("bpf: return VMA snapshot from task_vma iterator")
Suggested-by: Amery Hung <ameryhung@gmail.com>
Assisted-by: LLM
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 kernel/bpf/verifier.c | 88 +++++++++++++++++++++++++++++++++++--------
 1 file changed, 73 insertions(+), 15 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 6c6b8d8520cdf..3d47487233b75 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -209,6 +209,9 @@ static int acquire_reference(struct bpf_verifier_env *env, int insn_idx, int par
 static int __release_reference_nomark(struct bpf_verifier_state *state, int id);
 static int release_reference_nomark(struct bpf_verifier_env *env, int id);
 static int release_reference(struct bpf_verifier_env *env, int id);
+static int check_reference_children_leak(struct bpf_verifier_env *env, int parent_id);
+static u32 reg_lifetime_id(struct bpf_verifier_env *env,
+			   const struct bpf_reg_state *reg);
 static void invalidate_non_owning_refs(struct bpf_verifier_env *env);
 static void invalidate_rcu_protected_refs(struct bpf_verifier_env *env);
 static bool in_rbtree_lock_required_cb(struct bpf_verifier_env *env);
@@ -753,8 +756,8 @@ static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_
 		if (err)
 			return err;
 
-		/* Track parent's id if the parent is a referenced object */
-		parent_id = ref_obj->id;
+		/* All non-clone constructors take their backing object in R1. */
+		parent_id = reg_lifetime_id(env, &cur_regs(env)[BPF_REG_1]);
 
 		if (dynptr_type_referenced(type)) {
 			int id;
@@ -1024,7 +1027,7 @@ static int unmark_stack_slots_iter(struct bpf_verifier_env *env,
 				   struct bpf_reg_state *reg, int nr_slots)
 {
 	struct bpf_func_state *state = bpf_func(env, reg);
-	int spi, i, j;
+	int spi, i, j, err;
 
 	spi = iter_get_spi(env, reg, nr_slots);
 	if (spi < 0)
@@ -1034,8 +1037,11 @@ static int unmark_stack_slots_iter(struct bpf_verifier_env *env,
 		struct bpf_stack_state *slot = &state->stack[spi - i];
 		struct bpf_reg_state *st = &slot->spilled_ptr;
 
-		if (i == 0)
-			WARN_ON_ONCE(release_reference(env, st->id));
+		if (i == 0) {
+			err = release_reference(env, st->id);
+			if (err)
+				return err;
+		}
 
 		bpf_mark_reg_not_init(env, st);
 
@@ -1577,6 +1583,12 @@ static bool reg_is_referenced(struct bpf_verifier_env *env, const struct bpf_reg
 	return find_reference_state(env->cur_state, reg->id);
 }
 
+static u32 reg_lifetime_id(struct bpf_verifier_env *env,
+			   const struct bpf_reg_state *reg)
+{
+	return reg_is_referenced(env, reg) ? reg->id : reg->parent_id;
+}
+
 static int release_lock_state(struct bpf_verifier_env *env, int type, int id, void *ptr)
 {
 	struct bpf_verifier_state *state = env->cur_state;
@@ -6219,9 +6231,14 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
 	}
 
 	if (atype == BPF_READ && value_regno >= 0) {
+		u32 parent_id = reg_lifetime_id(env, reg);
+
 		ret = mark_btf_ld_reg(env, regs, value_regno, ret, reg->btf, btf_id, flag);
 		if (ret < 0)
 			return ret;
+		if ((regs[value_regno].type & PTR_TRUSTED) &&
+		    !(regs[value_regno].type & (MEM_RCU | MEM_PERCPU | MEM_USER)))
+			regs[value_regno].parent_id = parent_id;
 	}
 
 	return 0;
@@ -7817,6 +7834,27 @@ static bool is_kfunc_arg_iter(struct bpf_call_arg_meta *meta, int arg_idx,
 	return btf_param_match_suffix(meta->btf, arg, "__iter");
 }
 
+static int invalidate_iter_owned_btf_ptrs(struct bpf_verifier_env *env, u32 parent_id)
+{
+	struct bpf_func_state *unused;
+	struct bpf_reg_state *reg;
+	int err;
+
+	err = check_reference_children_leak(env, parent_id);
+	if (err)
+		return err;
+
+	/* Struct iterators can release their previous element on next. */
+	bpf_for_each_reg_in_vstate(env->cur_state, unused, reg, ({
+		if (base_type(reg->type) != PTR_TO_BTF_ID || reg->parent_id != parent_id)
+			continue;
+		bpf_diag_record_scrub(env, reg, BPF_DIAG_MOD_REF_RELEASE);
+		mark_reg_invalid(env, reg);
+	}));
+
+	return 0;
+}
+
 static int process_iter_arg(struct bpf_verifier_env *env, struct bpf_reg_state *reg, argno_t argno, int insn_idx,
 			    struct bpf_call_arg_meta *meta)
 {
@@ -7914,6 +7952,13 @@ static int process_iter_arg(struct bpf_verifier_env *env, struct bpf_reg_state *
 		meta->iter.frameno = reg->frameno;
 		update_ref_obj(&meta->ref_obj, &state->stack[spi].spilled_ptr);
 
+		if (bpf_is_iter_next_kfunc(meta) &&
+		    !(state->stack[spi].spilled_ptr.type & MEM_RCU)) {
+			err = invalidate_iter_owned_btf_ptrs(env, meta->ref_obj.id);
+			if (err)
+				return err;
+		}
+
 		if (is_iter_destroy_kfunc(meta)) {
 			err = unmark_stack_slots_iter(env, reg, nr_slots);
 			if (err)
@@ -10088,6 +10133,23 @@ static int idstack_pop(struct bpf_idmap *idmap)
 	return idmap->map[--idmap->cnt].old;
 }
 
+static int check_reference_children_leak(struct bpf_verifier_env *env, int parent_id)
+{
+	struct bpf_verifier_state *state = env->cur_state;
+	int i;
+
+	for (i = 0; i < state->acquired_refs; i++) {
+		if (state->refs[i].type != REF_TYPE_PTR ||
+		    state->refs[i].parent_id != parent_id)
+			continue;
+		verbose(env, "Leaking reference id=%d alloc_insn=%d. Release it first.\n",
+			state->refs[i].id, state->refs[i].insn_idx);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
 /* Release id and objects derived from it iteratively in a DFS manner */
 static int release_reference(struct bpf_verifier_env *env, int id)
 {
@@ -10097,7 +10159,7 @@ static int release_reference(struct bpf_verifier_env *env, int id)
 	struct bpf_stack_state *stack;
 	struct bpf_func_state *state;
 	struct bpf_reg_state *reg;
-	int i, err;
+	int err;
 
 	idstack->cnt = 0;
 	err = idstack_push(idstack, id);
@@ -10114,15 +10176,9 @@ static int release_reference(struct bpf_verifier_env *env, int id)
 		 * Child references are inaccessible after parent is released,
 		 * any child references that exist at this point are a leak.
 		 */
-		for (i = 0; i < vstate->acquired_refs; i++) {
-			if (vstate->refs[i].type != REF_TYPE_PTR)
-				continue;
-			if (vstate->refs[i].parent_id != id)
-				continue;
-			verbose(env, "Leaking reference id=%d alloc_insn=%d. Release it first.\n",
-				vstate->refs[i].id, vstate->refs[i].insn_idx);
-			return -EINVAL;
-		}
+		err = check_reference_children_leak(env, id);
+		if (err)
+			return err;
 
 		bpf_for_each_reg_in_vstate_mask(vstate, state, reg, stack, mask, ({
 			if (reg->id != id && reg->parent_id != id)
@@ -14446,6 +14502,8 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 			regs[BPF_REG_0].btf = desc_btf;
 			regs[BPF_REG_0].type = type;
 			regs[BPF_REG_0].btf_id = ptr_type_id;
+			if (bpf_is_iter_next_kfunc(&meta) && !(type & MEM_RCU))
+				regs[BPF_REG_0].parent_id = meta.ref_obj.id;
 		}
 
 		if (is_kfunc_ret_null(&meta)) {
-- 
2.43.0


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

* [PATCH bpf-next v4 2/2] selftests/bpf: Test iterator BTF pointer lifetimes
  2026-09-17  5:19 [PATCH bpf-next v4 0/2] bpf: Track iterator-owned BTF pointer lifetimes Xu Yunxiang
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
@ 2026-09-17  5:19 ` Xu Yunxiang
  1 sibling, 0 replies; 6+ messages in thread
From: Xu Yunxiang @ 2026-09-17  5:19 UTC (permalink / raw)
  To: bpf
  Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, yonghong.song,
	puranjay

Add verifier coverage for iterator-owned BTF pointer lifetimes.
Cover current struct results, iterator transitions,
trusted fields and FILE dynptr cleanup obligations. Keep current-result,
explicit-discard and independent RCU lifetime controls.

These annotations check verifier outcomes; they do not assert runtime
execution of the newly added programs.

Assisted-by: LLM
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 .../selftests/bpf/progs/iters_testmod.c       | 332 ++++++++++++++++++
 1 file changed, 332 insertions(+)

diff --git a/tools/testing/selftests/bpf/progs/iters_testmod.c b/tools/testing/selftests/bpf/progs/iters_testmod.c
index f65cc9766633e..81581459f11e3 100644
--- a/tools/testing/selftests/bpf/progs/iters_testmod.c
+++ b/tools/testing/selftests/bpf/progs/iters_testmod.c
@@ -28,6 +28,298 @@ int iter_next_trusted(const void *ctx)
 	return 0;
 }
 
+SEC("raw_tp/sys_enter")
+__failure __msg("invalid mem access 'scalar'")
+int iter_next_trusted_after_destroy(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	bpf_iter_task_vma_destroy(&vma_it);
+	return vma_ptr->vm_start;
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__failure __msg("invalid mem access 'scalar'")
+int iter_next_trusted_after_next(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr, *next_vma_ptr;
+	u64 vm_start;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	next_vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!next_vma_ptr)
+		goto out;
+
+	vm_start = vma_ptr->vm_start;
+	bpf_iter_task_vma_destroy(&vma_it);
+	return vm_start;
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__failure __msg("invalid mem access 'scalar'")
+int iter_next_trusted_after_next_null(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr, *next_vma_ptr;
+	u64 vm_start;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	next_vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (next_vma_ptr)
+		goto out;
+
+	vm_start = vma_ptr->vm_start;
+	bpf_iter_task_vma_destroy(&vma_it);
+	return vm_start;
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__failure __msg("invalid mem access 'scalar'")
+int iter_next_trusted_field_after_destroy(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+	struct file *file_ptr;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	file_ptr = vma_ptr->vm_file;
+	bpf_iter_task_vma_destroy(&vma_it);
+	if (file_ptr)
+		return file_ptr->f_mode;
+	return 0;
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__failure __msg("invalid mem access 'scalar'")
+int iter_next_trusted_field_after_next(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr, *next_vma_ptr;
+	struct file *file_ptr;
+	u32 mode;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	file_ptr = vma_ptr->vm_file;
+	if (!file_ptr)
+		goto out;
+
+	next_vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!next_vma_ptr)
+		goto out;
+
+	mode = file_ptr->f_mode;
+	bpf_iter_task_vma_destroy(&vma_it);
+	return mode;
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__failure __msg("Leaking reference id=")
+int iter_next_file_dynptr_after_next(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+	struct bpf_dynptr dynptr;
+	struct file *file_ptr;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	file_ptr = vma_ptr->vm_file;
+	if (!file_ptr)
+		goto out;
+
+	bpf_dynptr_from_file(file_ptr, 0, &dynptr);
+	bpf_iter_task_vma_next(&vma_it);
+	bpf_dynptr_file_discard(&dynptr);
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__failure __msg("Leaking reference id=")
+int iter_next_file_dynptr_after_destroy(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+	struct bpf_dynptr dynptr;
+	struct file *file_ptr;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	file_ptr = vma_ptr->vm_file;
+	if (!file_ptr)
+		goto out;
+
+	bpf_dynptr_from_file(file_ptr, 0, &dynptr);
+	bpf_iter_task_vma_destroy(&vma_it);
+	bpf_dynptr_file_discard(&dynptr);
+	return 0;
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__success
+int iter_next_file_dynptr_discard_before_advance(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+	struct bpf_dynptr dynptr;
+	struct file *file_ptr;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	file_ptr = vma_ptr->vm_file;
+	if (!file_ptr)
+		goto out;
+
+	bpf_dynptr_from_file(file_ptr, 0, &dynptr);
+	bpf_dynptr_file_discard(&dynptr);
+	bpf_iter_task_vma_next(&vma_it);
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__success
+int iter_next_trusted_current_after_next(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (!vma_ptr)
+		goto out;
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (vma_ptr)
+		bpf_kfunc_trusted_vma_test(vma_ptr);
+out:
+	bpf_iter_task_vma_destroy(&vma_it);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__success
+int iter_next_trusted_rcu_field_after_destroy(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+	struct mm_struct *mm_ptr;
+	struct file *file_ptr = NULL;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (vma_ptr) {
+		mm_ptr = vma_ptr->vm_mm;
+		/* exe_file has RCU protection independent of the iterator. */
+		if (mm_ptr)
+			file_ptr = mm_ptr->exe_file;
+	}
+
+	bpf_iter_task_vma_destroy(&vma_it);
+	if (file_ptr)
+		return file_ptr->f_mode;
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__success
+int iter_next_trusted_rcu_field_after_next(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task_vma vma_it;
+	struct vm_area_struct *vma_ptr;
+	struct mm_struct *mm_ptr;
+	struct file *file_ptr = NULL;
+	u32 mode = 0;
+
+	bpf_iter_task_vma_new(&vma_it, cur_task, 0);
+
+	vma_ptr = bpf_iter_task_vma_next(&vma_it);
+	if (vma_ptr) {
+		mm_ptr = vma_ptr->vm_mm;
+		if (mm_ptr)
+			file_ptr = mm_ptr->exe_file;
+	}
+
+	bpf_iter_task_vma_next(&vma_it);
+	if (file_ptr)
+		mode = file_ptr->f_mode;
+	bpf_iter_task_vma_destroy(&vma_it);
+	return mode;
+}
+
 SEC("raw_tp/sys_enter")
 __failure __msg("Possibly NULL pointer passed to trusted R1")
 int iter_next_trusted_or_null(const void *ctx)
@@ -66,6 +358,46 @@ int iter_next_rcu(const void *ctx)
 	return 0;
 }
 
+SEC("raw_tp/sys_enter")
+__success
+int iter_next_rcu_after_destroy(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task task_it;
+	struct task_struct *task_ptr;
+
+	bpf_iter_task_new(&task_it, cur_task, 0);
+
+	task_ptr = bpf_iter_task_next(&task_it);
+	bpf_iter_task_destroy(&task_it);
+	if (!task_ptr)
+		return 0;
+
+	bpf_kfunc_rcu_task_test(task_ptr);
+	return 0;
+}
+
+SEC("raw_tp/sys_enter")
+__success
+int iter_next_rcu_after_next(const void *ctx)
+{
+	struct task_struct *cur_task = bpf_get_current_task_btf();
+	struct bpf_iter_task task_it;
+	struct task_struct *task_ptr;
+
+	bpf_iter_task_new(&task_it, cur_task, 0);
+
+	task_ptr = bpf_iter_task_next(&task_it);
+	if (!task_ptr)
+		goto out;
+
+	bpf_iter_task_next(&task_it);
+	bpf_kfunc_rcu_task_test(task_ptr);
+out:
+	bpf_iter_task_destroy(&task_it);
+	return 0;
+}
+
 SEC("raw_tp/sys_enter")
 __failure __msg("Possibly NULL pointer passed to trusted R1")
 int iter_next_rcu_or_null(const void *ctx)
-- 
2.43.0


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

* Re: [PATCH bpf-next v4 1/2] bpf: Track iterator-owned BTF pointer lifetimes
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
@ 2026-09-17  5:44   ` sashiko-bot
  2026-09-17  6:48   ` bot+bpf-ci
  2026-09-17 14:45   ` Alexei Starovoitov
  2 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-09-17  5:44 UTC (permalink / raw)
  To: Xu Yunxiang; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] bpf: bpf_iter_next() fails to invalidate dynptrs and PTR_TO_MEM slices
- [High] bpf: PTR_TO_MEM slices returned by kfuncs escape iterator lifetime tracking
--

commit 78eae9a9dcb52a34bea55102674e78d19a5f54b8
Author: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>

bpf: Track iterator-owned BTF pointer lifetimes

This commit introduces lifetime tracking for non-RCU struct iterator
results by associating PTR_TO_BTF_ID results with the iterator reference.
It invalidates the previous result when processing the next iterator
argument and propagates the backing register's lifetime for dynptrs.

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 6c6b8d8520cdf..3d47487233b75 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c

[ ... ]

> @@ -7817,6 +7834,27 @@ static bool is_kfunc_arg_iter(struct bpf_call_arg_meta *meta, int arg_idx,
>  	return btf_param_match_suffix(meta->btf, arg, "__iter");
>  }
>  
> +static int invalidate_iter_owned_btf_ptrs(struct bpf_verifier_env *env, u32 parent_id)
> +{
> +	struct bpf_func_state *unused;
> +	struct bpf_reg_state *reg;
> +	int err;
> +
> +	err = check_reference_children_leak(env, parent_id);
> +	if (err)
> +		return err;
> +
> +	/* Struct iterators can release their previous element on next. */
> +	bpf_for_each_reg_in_vstate(env->cur_state, unused, reg, ({
> +		if (base_type(reg->type) != PTR_TO_BTF_ID || reg->parent_id != parent_id)
> +			continue;
> +		bpf_diag_record_scrub(env, reg, BPF_DIAG_MOD_REF_RELEASE);
> +		mark_reg_invalid(env, reg);
> +	}));
> +
> +	return 0;
> +}

[Severity: High]
Does this code fail to invalidate dynptrs and PTR_TO_MEM slices derived
from the iterator element?

The bpf_for_each_reg_in_vstate() macro only iterates over spilled registers
using the 1 << STACK_SPILL mask, which completely skips STACK_DYNPTR slots.
Furthermore, the loop explicitly filters out anything that is not a
PTR_TO_BTF_ID.

If a BPF program constructs an unreferenced dynptr (e.g., via
bpf_dynptr_from_skb) or a PTR_TO_MEM slice from the iterator element, these
derived objects inherit the iterator's parent_id. Because they are skipped
by this logic, they will survive the bpf_iter_next() call and remain
accessible after the underlying memory is freed, potentially leading to a
use-after-free.

[ ... ]

> @@ -14446,6 +14502,8 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
>  			regs[BPF_REG_0].btf = desc_btf;
>  			regs[BPF_REG_0].type = type;
>  			regs[BPF_REG_0].btf_id = ptr_type_id;
> +			if (bpf_is_iter_next_kfunc(&meta) && !(type & MEM_RCU))
> +				regs[BPF_REG_0].parent_id = meta.ref_obj.id;
>  		}

[Severity: High]
Can PTR_TO_MEM slices returned by kfuncs escape iterator lifetime tracking
entirely?

When an iterator element is passed to a kfunc, the verifier attempts to
propagate lifetime tracking by populating meta->ref_obj in
check_func_arg() using this check:

	if (reg_is_referenced(env, reg))
		update_ref_obj(&meta->ref_obj, reg);

However, since non-RCU iterator elements track their lifetime purely via
parent_id and have id == 0, reg_is_referenced() evaluates to false. This
leaves meta->ref_obj.id as 0.

When check_kfunc_call() processes a returned PTR_TO_MEM slice, it only
assigns parent_id if meta.ref_obj.id is non-zero:

			/* Ensures we don't access the memory after a release_reference() */
			if (meta.ref_obj.id) {
				err = validate_ref_obj(env, &meta.ref_obj);
				if (err)
					return err;
				regs[BPF_REG_0].parent_id = meta.ref_obj.id;
			}

Because meta.ref_obj.id is 0, the returned PTR_TO_MEM slice receives no
parent_id. Does this allow the slice to escape the iterator's lifetime
tracking and outlive the iterator element, causing a use-after-free?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260917051948.1588826-1-xyx2021@mail.ustc.edu.cn?part=1

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

* Re: [PATCH bpf-next v4 1/2] bpf: Track iterator-owned BTF pointer lifetimes
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
  2026-09-17  5:44   ` sashiko-bot
@ 2026-09-17  6:48   ` bot+bpf-ci
  2026-09-17 14:45   ` Alexei Starovoitov
  2 siblings, 0 replies; 6+ messages in thread
From: bot+bpf-ci @ 2026-09-17  6:48 UTC (permalink / raw)
  To: xyx2021, bpf
  Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, yonghong.song,
	puranjay, ast, andrii, daniel, martin.lau, eddyz87, yonghong.song,
	mason, ihor.solodrai

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

> Non-RCU struct iterator results borrow their lifetime from the iterator's
> current element. Associate these PTR_TO_BTF_ID results with the iterator
> reference and invalidate the previous result when processing the next
> iterator argument. This applies before modeling either next outcome;
> destroy continues to invalidate the result through reference release.
>
> Preserve that lifetime when loading a fully trusted BTF field. Capture the
> source lifetime before marking the destination, as the registers can
> alias. Keep RCU, user and percpu field results under their existing rules.
>
> A borrowed field can also back a dynptr. For example, a FILE dynptr owns
> reader state but borrows its file. Record the backing register's lifetime
> in the common dynptr constructor path, using the acquired ID for an owned
> reference and parent_id for a borrowed pointer. Keep clone handling and
> the ownership meaning of reg_is_referenced() unchanged.
>
> Reuse the existing child-reference leak check before advancing an iterator
> and propagate destroy errors. Outstanding child resources must be
> explicitly released before their backing lifetime ends. Preserve the
> existing rule that only the initial ID is removed from acquired references
> when release_reference() invalidates descendants.

This isn't a bug, but could the changelog open with the unsafe program
pattern and note that trusted-field loads from any released reference are
now rejected, rather than walking the hunks in order?

The patch tightens acceptance outside iterators too. After 'file =
bpf_get_task_exe_file(task)', loading 'inode = file->f_inode' now
inherits the file's reference id, so release_reference() on bpf_put_file()
invalidates 'inode' and a previously-accepted program using it afterwards
is now rejected. That user-visible consequence isn't mentioned.

> Fixes: 4cbee026db54 ("bpf: return VMA snapshot from task_vma iterator")
> Suggested-by: Amery Hung <ameryhung@gmail.com>
> Assisted-by: LLM
> Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 6c6b8d8520cdf..3d47487233b75 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c

[ ... ]

> @@ -753,8 +756,8 @@ static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_
>  		if (err)
>  			return err;
>
> -		/* Track parent's id if the parent is a referenced object */
> -		parent_id = ref_obj->id;
> +		/* All non-clone constructors take their backing object in R1. */
> +		parent_id = reg_lifetime_id(env, &cur_regs(env)[BPF_REG_1]);

This isn't a bug, but would it help to anchor this invariant somewhere
checkable?

The removed comment described the local operation, but the new one asserts
a whole-tree invariant about every dynptr constructor's argument order.
The invariant does hold - bpf_dynptr_from_mem, bpf_ringbuf_reserve_dynptr,
bpf_dynptr_from_file, and the skb/xdp variants all take the backing object
in R1 - but nothing in the code enforces it, so a reader has to
re-enumerate every 'ARG_PTR_TO_DYNPTR | MEM_UNINIT' proto to confirm.

Could you note which protos it covers, or key the lookup off the argument
the verifier already identified rather than a fixed R1?

[ ... ]


---
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/35186779269

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

* Re: [PATCH bpf-next v4 1/2] bpf: Track iterator-owned BTF pointer lifetimes
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
  2026-09-17  5:44   ` sashiko-bot
  2026-09-17  6:48   ` bot+bpf-ci
@ 2026-09-17 14:45   ` Alexei Starovoitov
  2 siblings, 0 replies; 6+ messages in thread
From: Alexei Starovoitov @ 2026-09-17 14:45 UTC (permalink / raw)
  To: Xu Yunxiang, bpf
  Cc: daniel, andrii, eddyz87, memxor, ameryhung, yonghong.song,
	puranjay

On Thu, Sep 17, 2026 at 01:19 PM Xu Yunxiang <xyx2021@mail.ustc.edu.cn> wrote:

> @@ -6219,9 +6231,14 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
>  	}
>
>  	if (atype == BPF_READ && value_regno >= 0) {
> +		u32 parent_id = reg_lifetime_id(env, reg);
> +
>  		ret = mark_btf_ld_reg(env, regs, value_regno, ret, reg->btf, btf_id, flag);
>  		if (ret < 0)
>  			return ret;
> +		if ((regs[value_regno].type & PTR_TRUSTED) &&
> +		    !(regs[value_regno].type & (MEM_RCU | MEM_PERCPU | MEM_USER)))
> +			regs[value_regno].parent_id = parent_id;
>  	}

Any trusted field walked from a
referenced pointer now dies with that reference, e.g.

  file = bpf_get_task_exe_file(task);
  inode = file->f_inode;
  bpf_put_file(file);
  ... inode->i_ino ...

loads today and is rejected after this patch, since
release_reference() on bpf_put_file() invalidates every reg with
parent_id == file's ref id. That's the right thing to do in file case
and vma iter case
, but likely not the case for many other iters.

We need either opt-in or opt-out. Not sure which is better.
Certainly some flag.
Blank reject for all is probably overkill that might break valid progs.

pw-bot: cr

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

end of thread, other threads:[~2026-09-17 14:45 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-17  5:19 [PATCH bpf-next v4 0/2] bpf: Track iterator-owned BTF pointer lifetimes Xu Yunxiang
2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
2026-09-17  5:44   ` sashiko-bot
2026-09-17  6:48   ` bot+bpf-ci
2026-09-17 14:45   ` Alexei Starovoitov
2026-09-17  5:19 ` [PATCH bpf-next v4 2/2] selftests/bpf: Test iterator " Xu Yunxiang

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