Thread (36 messages) read the whole thread 36 messages, 7 authors, 2026-04-28

Re: [PATCH bpf-next v3 4/9] bpf: Refactor object relationship tracking and fix dynptr UAF bug

From: Amery Hung <hidden>
Date: 2026-04-27 20:22:08
Also in: bpf

On Fri, Apr 24, 2026 at 3:48 PM Eduard Zingerman [off-list ref] wrote:
On Tue, 2026-04-21 at 15:10 -0700, Amery Hung wrote:

Tbh, I find current state of affairs with id/ref_obj_id/parent_id hard
to follow. The release_reference() is an improvement, but the means by
which the fields are propagated to bpf_reg_state objects are convoluted.
I wonder if having a separate "object table" in bpf_verifier_state and
having bpf_reg_state->id refer to objects within this table would make
things more straight forward.
I think this is a good idea in the long term. First, we need to make
id a stable and unique object identifier (i.e., always assign id to
objects; different objects have different ids). Then, we can create
the object table indexed by id and each entry contains the ref_obj_id
and parent_id moved from bpf_reg_state. Then, there will be helpers
managing the lifetime, relationship (e.g., bpf_obj_create,
bpf_obj_release, bpf_obj_clone) with clear semantics to centralize how
id,parent_id,ref_obj_id are manipulated.

If this makes sense, I can send this as a follow up patchset.
A few nits below.

[...]
quoted
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
[...]
quoted
@@ -1323,6 +1335,7 @@ struct bpf_dynptr_desc {
      enum bpf_dynptr_type type;
      u32 id;
      u32 ref_obj_id;
+     u32 parent_id;
 };
Nit: would be nice to have a comment describing when the above fields
     are populated.
Ack
quoted
 struct bpf_kfunc_call_arg_meta {
@@ -1334,6 +1347,7 @@ struct bpf_kfunc_call_arg_meta {
      const char *func_name;
      /* Out parameters */
      u32 ref_obj_id;
+     u32 id;
Nit: would be nice to have a comment here too.
Ack
[...]
quoted
diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c
index 8478d2c6ed5b..72bd3bcda5fb 100644
--- a/kernel/bpf/states.c
+++ b/kernel/bpf/states.c
@@ -494,7 +494,8 @@ static bool regs_exact(const struct bpf_reg_state *rold,
 {
      return memcmp(rold, rcur, offsetof(struct bpf_reg_state, id)) == 0 &&
             check_ids(rold->id, rcur->id, idmap) &&
-            check_ids(rold->ref_obj_id, rcur->ref_obj_id, idmap);
+            check_ids(rold->ref_obj_id, rcur->ref_obj_id, idmap) &&
+            check_ids(rold->parent_id, rcur->parent_id, idmap);
Nit: these check_ids() become repetitive, maybe add a utility function
     checking id/ref_obj_id/parent_id?
Will add a utility function.
quoted
 }

 enum exact_level {
@@ -619,7 +620,8 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
                     range_within(rold, rcur) &&
                     tnum_in(rold->var_off, rcur->var_off) &&
                     check_ids(rold->id, rcur->id, idmap) &&
-                    check_ids(rold->ref_obj_id, rcur->ref_obj_id, idmap);
+                    check_ids(rold->ref_obj_id, rcur->ref_obj_id, idmap) &&
+                    check_ids(rold->parent_id, rcur->parent_id, idmap);
      case PTR_TO_PACKET_META:
      case PTR_TO_PACKET:
              /* We must have at least as much range as the old ptr
@@ -799,7 +801,8 @@ static bool stacksafe(struct bpf_verifier_env *env, struct bpf_func_state *old,
                      cur_reg = &cur->stack[spi].spilled_ptr;
                      if (old_reg->dynptr.type != cur_reg->dynptr.type ||
                          old_reg->dynptr.first_slot != cur_reg->dynptr.first_slot ||
-                         !check_ids(old_reg->ref_obj_id, cur_reg->ref_obj_id, idmap))
+                         !check_ids(old_reg->ref_obj_id, cur_reg->ref_obj_id, idmap) ||
+                         !check_ids(old_reg->parent_id, cur_reg->parent_id, idmap))
Not something changed by the current patch, but still a question:
this path ignores old_reg->id, is it a bug?
Yes. I will also check id.
quoted
                              return false;
                      break;
              case STACK_ITER:
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 0313b7d5f6c9..908a3af0e7c4 100644
[...]
quoted
@@ -241,6 +241,7 @@ struct bpf_call_arg_meta {
      int mem_size;
      u64 msize_max_value;
      int ref_obj_id;
+     u32 id;
Nit: please add a comment here as well.
Ack
[...]
quoted
@@ -635,11 +636,12 @@ static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
                                      struct bpf_func_state *state, int spi);

 static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
-                                enum bpf_arg_type arg_type, int insn_idx, int clone_ref_obj_id)
+                                enum bpf_arg_type arg_type, int insn_idx, int parent_id,
+                                struct bpf_dynptr_desc *dynptr)
Having both parent_id and dynptr->parent_id as parameters of this
function is very confusing, but I don't have a suggestion on how to
better deal with it.
Does tweaking the argument name and reorder the if-else block make it
more obvious how these two arguments are used?

if (clone_dynptr->type != BPF_DYNPTR_TYPE_INVALID) { /* bpf_dynptr_clone() */
        ref_obj_id = clone_dynptr->ref_obj_id;
        parent_id = clone_dynptr->parent_id;
} else { /* dynptr constructors */
        if (dynptr_type_referenced(type)) {
                ref_obj_id = acquire_reference(env, insn_idx);
                if (ref_obj_id < 0)
                        return ref_obj_id;
        }
}
[...]
quoted
@@ -8489,6 +8444,7 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
                      return -EACCES;
              }
              meta->ref_obj_id = reg->ref_obj_id;
+             meta->id = reg->id;
Could you please leave a comment here explaining when e.g.
meta->ref_obj_id and meta->id would have same or different ids here?
Maybe a few lines of BPF C code.
Ack
quoted
      }

      switch (base_type(arg_type)) {
@@ -9111,26 +9067,75 @@ static int release_reference_nomark(struct bpf_verifier_state *state, int ref_ob
[...]
quoted
+/* Release id and objects referencing the id iteratively in a DFS manner */
+static int release_reference(struct bpf_verifier_env *env, int id)
+{
+     u32 mask = (1 << STACK_SPILL) | (1 << STACK_DYNPTR);
      struct bpf_verifier_state *vstate = env->cur_state;
+     struct bpf_idmap *idstack = &env->idmap_scratch;
+     struct bpf_stack_state *stack;
      struct bpf_func_state *state;
      struct bpf_reg_state *reg;
-     int err;
+     int root_id = id, err;

-     err = release_reference_nomark(vstate, ref_obj_id);
-     if (err)
-             return err;
+     idstack->cnt = 0;
+     idstack_push(idstack, id);

-     bpf_for_each_reg_in_vstate(vstate, state, reg, ({
-             if (reg->ref_obj_id == ref_obj_id)
-                     mark_reg_invalid(env, reg);
-     }));
+     if (find_reference_state(vstate, id))
+             WARN_ON_ONCE(release_reference_nomark(vstate, id));
+
+     while ((id = idstack_pop(idstack))) {
+             bpf_for_each_reg_in_vstate_mask(vstate, state, reg, stack, mask, ({
+                     if (reg->id != id && reg->parent_id != id && reg->ref_obj_id != id)
+                             continue;
+
+                     if (reg->ref_obj_id && id != root_id) {
+                             struct bpf_reference_state *ref_state;
+
+                             ref_state = find_reference_state(env->cur_state, reg->ref_obj_id);
+                             verbose(env, "Unreleased reference id=%d alloc_insn=%d when releasing id=%d\n",
+                                     ref_state->id, ref_state->insn_idx, root_id);
+                             return -EINVAL;
+                     }
+
+                     if (reg->id != id) {
+                             err = idstack_push(idstack, reg->id);
+                             if (err)
+                                     return err;
+                     }
+
+                     if (!stack || stack->slot_type[BPF_REG_SIZE - 1] == STACK_SPILL)
+                             mark_reg_invalid(env, reg);
+                     else if (stack->slot_type[BPF_REG_SIZE - 1] == STACK_DYNPTR)
+                             invalidate_dynptr(env, state, stack);
invalidate_dynptr() rewrites to stack slots, can it be the case that
this body of bpf_for_each_reg_in_vstate_mask() is computed for first
and second dynptr stack slots, hence triggering invalidate_dynptr() to
rewrite three slots instead of two?
invalidate_dynptr() will mark the two stack slots as STACK_INVALID so
I didn't bother to check whether it is the first slot or not. Am I
missing anything?
quoted
+             }));
+     }

      return 0;
 }
[...]
quoted
@@ -12009,6 +12009,7 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
                              return -EFAULT;
                      }
                      meta->ref_obj_id = reg->ref_obj_id;
+                     meta->id = reg->id;
And here a comment describing when this happens would be helpful.
Ack
[...]
quoted
@@ -12171,15 +12171,10 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
                              }

                              dynptr_arg_type |= (unsigned int)get_dynptr_type_flag(parent_type);
-                             clone_ref_obj_id = meta->dynptr.ref_obj_id;
-                             if (dynptr_type_refcounted(parent_type) && !clone_ref_obj_id) {
-                                     verifier_bug(env, "missing ref obj id for parent of clone");
-                                     return -EFAULT;
-                             }
                      }

-                     ret = process_dynptr_func(env, regno, insn_idx, dynptr_arg_type, clone_ref_obj_id,
-                                               &meta->dynptr);
+                     ret = process_dynptr_func(env, regno, insn_idx, dynptr_arg_type,
+                                               meta->ref_obj_id ? meta->id : 0, &meta->dynptr);
And an example here as well.
Ack
quoted
                      if (ret < 0)
                              return ret;
                      break;
[...]
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help