From nobody Thu Sep 24 18:39:29 2026 Received: from mta0.migadu.com (out-198.mta0.migadu.com [91.218.175.198]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1CBAC35E944 for ; Tue, 22 Sep 2026 01:03:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.198 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039037; cv=none; b=Jt1VtBCatf+LkA5oiEaS28tsMsWLlJumgugocIfOn6IVpIF/IhKgndopI1zgVFPB0nyyvI4VnsKjaMXSzOEj0+6EkED5NxMNzdicwLmYAU/KAsl3ufghwfmqxOGkehwFF8zBc4kF0M1nFfbeWaNYK0+J3PvSx55qaf0SzMSXWck= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039037; c=relaxed/simple; bh=/3JbCIKnBZH40TkdFYl0W7WZ17knx7zjRr3o1DGPnAU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=dJdf48K1vrA5DXuFEpoe6Z5mICfOQ603zCmhOENcrDNXekSHNjma4R0OBo9uh95E2NHNinGEdOrzZriXQIDTZwucqDEgZfq7m2adaALj993qYO9GbppAiTGLUld4gSCFBIS9lM9EvQhXwz8kTrj3ceYPfPOPzawCidmtmBFPLHM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=PfiL9rL/; arc=none smtp.client-ip=91.218.175.198 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="PfiL9rL/" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=/3JbCIKnBZH40TkdFYl0W7WZ17knx7zjRr3o1DGPnAU=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1790039032; v=1; x=1790643832; b=PfiL9rL/lxMUmscT56JKP1E6QTikKKn47o3BrYWKG1hNePCA/CWSg0XQkGwUCJG+EdYwhYYZ /GNFoE0MuJ5ptSJqV/gqhMqPePxxHe2Et+IhNy1MIgKDuI3uk1lwPrG2wFUweYnu8+EqL7Xnkc5 SEpKwOnmeWd8uEVHoqgfEYjM= X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id 7d36f3c3f44034d2; Tue, 22 Sep 2026 01:03:52 +0000 X-Mizu-Trace-ID: 7d36f3c3f44034d2 X-Migadu-Flow: FLOW_OUT From: Ihor Solodrai To: Alexei Starovoitov , Andrii Nakryiko , Daniel Borkmann , Eduard Zingerman , Kumar Kartikeya Dwivedi Cc: Amery Hung , Emil Tsalapatis , Nicholas Carlini , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, kernel-team@meta.com Subject: [PATCH bpf-next v1 1/6] bpf: Introduce REF_TYPE_FRAME in the verifier Date: Mon, 21 Sep 2026 18:03:28 -0700 Message-ID: <20260922010333.1226537-2-ihor.solodrai@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260922010333.1226537-1-ihor.solodrai@linux.dev> References: <20260922010333.1226537-1-ihor.solodrai@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" A callback-calling helper can pass its callback a pointer that is only valid for the duration of the call. The verifier just gives such an argument a register type, which allows the callback to park the value, or something derived from it, and make it outlive the frame. The BPF program then can reuse it after the helper returns. Invalidating a value together with everything derived from it is what release_reference() already does, walking reg->parent_id across every frame and stack slot. What is missing is a type of reference that is not an object the program acquired and releases. Introduce REF_TYPE_FRAME: a reference owned by a callee frame. A set_callee_state_fn can declare an argument frame-scoped, and setup_func_entry() turns the declaration into a reference, and prepare_func_exit() drops it when the frame is popped, invalidating the argument and everything derived from it through the existing walk. Keep the new type invisible to find_reference_state(). release_reg() and ref_convert_owning_non_owning() look up purely by id and could otherwise destroy the anchor. Fix up a few pre-existing comments while at it. Signed-off-by: Ihor Solodrai --- This patch only introduces the mechanism: no helper declares a frame-scoped argument yet. It is used in the subsequent patches in the series, each fixing a separate bug. --- --- include/linux/bpf.h | 1 + include/linux/bpf_verifier.h | 11 +++- kernel/bpf/states.c | 4 ++ kernel/bpf/verifier.c | 121 ++++++++++++++++++++++++++++++++--- 4 files changed, 125 insertions(+), 12 deletions(-) diff --git a/include/linux/bpf.h b/include/linux/bpf.h index fd22db8bc6c5..d849e4873417 100644 --- a/include/linux/bpf.h +++ b/include/linux/bpf.h @@ -3118,6 +3118,7 @@ int bpf_iter_map_fill_link_info(const struct bpf_iter= _aux_info *aux, int map_set_for_each_callback_args(struct bpf_verifier_env *env, struct bpf_func_state *caller, struct bpf_func_state *callee); +void mark_frame_scoped_arg(struct bpf_func_state *callee, u32 regno); =20 int bpf_percpu_hash_copy(struct bpf_map *map, void *key, void *value, u64 = flags); int bpf_percpu_array_copy(struct bpf_map *map, void *key, void *value, u64= flags); diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index 92f528c45605..51c310ea4e82 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -307,15 +307,13 @@ struct bpf_stack_state { }; =20 struct bpf_reference_state { - /* Each reference object has a type. Ensure REF_TYPE_PTR is zero to - * default to pointer reference on zero initialization of a state. - */ enum ref_state_type { REF_TYPE_PTR =3D (1 << 1), REF_TYPE_IRQ =3D (1 << 2), REF_TYPE_LOCK =3D (1 << 3), REF_TYPE_RES_LOCK =3D (1 << 4), REF_TYPE_RES_LOCK_IRQ =3D (1 << 5), + REF_TYPE_FRAME =3D (1 << 6), REF_TYPE_LOCK_MASK =3D REF_TYPE_LOCK | REF_TYPE_RES_LOCK | REF_TYPE_RES_= LOCK_IRQ, } type; /* Track each reference created with a unique id, even if the same @@ -333,6 +331,8 @@ struct bpf_reference_state { * it matches on unlock. */ void *ptr; + /* For REF_TYPE_FRAME */ + u32 frameno; }; }; =20 @@ -387,6 +387,11 @@ struct bpf_func_state { u32 callback_depth; /* Instructions processed in this frame and callees on the current path. = */ u32 insns_subtotal; + /* + * Set for arguments valid until the frame is popped. + * Consumed by setup_func_entry(). + */ + u16 frame_scoped_args; =20 /* The following fields should be last. See copy_func_state() */ /* The state of the stack. Each element of the array describes BPF_REG_SI= ZE diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c index 66fb11b6c6a7..bf7efe5bcdca 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -898,6 +898,10 @@ static bool refsafe(struct bpf_verifier_state *old, st= ruct bpf_verifier_state *c break; case REF_TYPE_IRQ: break; + case REF_TYPE_FRAME: + if (old->refs[i].frameno !=3D cur->refs[i].frameno) + return false; + break; case REF_TYPE_LOCK: case REF_TYPE_RES_LOCK: case REF_TYPE_RES_LOCK_IRQ: diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index d62c0f74cff5..a0a9d3d18f63 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -1474,14 +1474,13 @@ static int grow_stack_arg_slots(struct bpf_verifier= _env *env, return 0; } =20 -/* Acquire a pointer id from the env and update the state->refs to include - * this new pointer reference. - * On success, returns a valid pointer id to associate with the register - * On failure, returns a negative errno. +/* Append an entry to @state->refs and record the instruction that created= it. + * The caller fills in the type and the id. + * On success, returns the new entry. On failure, returns NULL. */ -static struct bpf_reference_state *acquire_reference_state(struct bpf_veri= fier_env *env, int insn_idx) +static struct bpf_reference_state *__acquire_reference_state(struct bpf_ve= rifier_state *state, + int insn_idx) { - struct bpf_verifier_state *state =3D env->cur_state; int new_ofs =3D state->acquired_refs; int err; =20 @@ -1493,6 +1492,12 @@ static struct bpf_reference_state *acquire_reference= _state(struct bpf_verifier_e return &state->refs[new_ofs]; } =20 +static struct bpf_reference_state *acquire_reference_state(struct bpf_veri= fier_env *env, + int insn_idx) +{ + return __acquire_reference_state(env->cur_state, insn_idx); +} + static int acquire_reference(struct bpf_verifier_env *env, int insn_idx, i= nt parent_id) { struct bpf_reference_state *s; @@ -1507,6 +1512,31 @@ static int acquire_reference(struct bpf_verifier_env= *env, int insn_idx, int par return s->id; } =20 +/* Acquire a reference owned by frame @frameno of @state */ +static int acquire_frame_reference(struct bpf_verifier_env *env, struct bp= f_verifier_state *state, + int insn_idx, u32 frameno) +{ + struct bpf_reference_state *s; + + s =3D __acquire_reference_state(state, insn_idx); + if (!s) + return -ENOMEM; + s->type =3D REF_TYPE_FRAME; + s->id =3D ++env->id_gen; + s->frameno =3D frameno; + return s->id; +} + +/* + * Declare that @regno in @callee holds a value that stops being valid onc= e the + * frame is popped. setup_func_entry() turns each declaration into a frame= -owned + * reference. + */ +void mark_frame_scoped_arg(struct bpf_func_state *callee, u32 regno) +{ + callee->frame_scoped_args |=3D BIT(regno); +} + static int acquire_lock_state(struct bpf_verifier_env *env, int insn_idx, = enum ref_state_type type, int id, void *ptr) { @@ -10175,7 +10205,7 @@ static int release_reference(struct bpf_verifier_en= v *env, int id) continue; =20 /* Free objects derived from the current object */ - if (reg->parent_id =3D=3D id) { + if (reg->parent_id =3D=3D id && reg->id !=3D id) { err =3D idstack_push(idstack, reg->id); if (err) return err; @@ -10206,6 +10236,34 @@ static int release_reference(struct bpf_verifier_e= nv *env, int id) return 0; } =20 +/* Find the first reference owned by frame @frameno, or 0 if it owns none.= */ +static u32 frame_reference_id(struct bpf_verifier_state *state, u32 framen= o) +{ + int i; + + for (i =3D 0; i < state->acquired_refs; i++) + if (state->refs[i].type =3D=3D REF_TYPE_FRAME && + state->refs[i].frameno =3D=3D frameno) + return state->refs[i].id; + + return 0; +} + +static int release_frame_reference(struct bpf_verifier_env *env, int id) +{ + struct bpf_verifier_state *state =3D env->cur_state; + int i; + + for (i =3D 0; i < state->acquired_refs; i++) { + if (state->refs[i].type !=3D REF_TYPE_FRAME || state->refs[i].id !=3D id) + continue; + release_reference_state(state, i); + break; + } + + return release_reference(env, id); +} + static void invalidate_non_owning_refs(struct bpf_verifier_env *env) { struct bpf_func_state *unused; @@ -10301,7 +10359,8 @@ static int setup_func_entry(struct bpf_verifier_env= *env, int subprog, int calls struct bpf_verifier_state *state) { struct bpf_func_state *caller, *callee; - int err; + u16 scoped_args; + int err, regno; =20 if (state->curframe + 1 >=3D MAX_CALL_FRAMES) { verbose(env, "the call stack of %d frames is too deep\n", @@ -10333,6 +10392,30 @@ static int setup_func_entry(struct bpf_verifier_en= v *env, int subprog, int calls if (err) goto err_out; =20 + scoped_args =3D callee->frame_scoped_args; + callee->frame_scoped_args =3D 0; + for (regno =3D 0; regno < MAX_BPF_REG; regno++) { + int id; + + if (!(scoped_args & BIT(regno))) + continue; + + id =3D acquire_frame_reference(env, state, callsite, callee->frameno); + if (id < 0) { + err =3D id; + goto err_out; + } + /* + * The value is its own lifetime anchor: there is no associated + * object to borrow from, only the frame. parent_id =3D id here + * covers both possible derived references: + * - through the id (e.g. dynptr slice) + * - through parent_id (e.g. dynptr clone) + */ + callee->regs[regno].id =3D id; + callee->regs[regno].parent_id =3D id; + } + /* only increment it after check_reg_arg() finished */ state->curframe++; =20 @@ -10560,6 +10643,10 @@ static int push_callback_call(struct bpf_verifier_= env *env, struct bpf_insn *ins if (err) return err; =20 + if (verifier_bug_if(callee->frame_scoped_args, env, + "frame-scoped argument declared for async callback")) + return -EFAULT; + return 0; } =20 @@ -11033,7 +11120,7 @@ static int prepare_func_exit(struct bpf_verifier_en= v *env, int *insn_idx) struct bpf_func_state *caller, *callee; struct bpf_reg_state *r0; bool in_callback_fn; - u32 i, nregs; + u32 i, nregs, id; int err; =20 callee =3D state->frame[state->curframe]; @@ -11106,6 +11193,17 @@ static int prepare_func_exit(struct bpf_verifier_e= nv *env, int *insn_idx) verbose(env, "to caller at %d:\n", *insn_idx); print_verifier_state(env, state, caller->frameno, true); } + + /* + * Values the caller only guaranteed for the duration of the call stop + * being valid here. + */ + while ((id =3D frame_reference_id(state, callee->frameno))) { + err =3D release_frame_reference(env, id); + if (err) + return err; + } + account_processed_insns(env, callee, caller); /* clear everything in the callee. In case of exceptional exits using * bpf_throw, this will be done by copy_verifier_state for extra frames. = */ @@ -11283,6 +11381,11 @@ static int check_reference_leak(struct bpf_verifie= r_env *env, bool exception_exi return 0; =20 for (i =3D 0; i < state->acquired_refs; i++) { + if (!exception_exit && state->refs[i].type =3D=3D REF_TYPE_FRAME) { + verifier_bug(env, "frame %u reference id=3D%d alive at program exit", + state->refs[i].frameno, state->refs[i].id); + return -EFAULT; + } if (state->refs[i].type !=3D REF_TYPE_PTR) continue; /* Allow struct_ops programs to return a referenced kptr back to --=20 2.55.0 From nobody Thu Sep 24 18:39:29 2026 Received: from mta1.migadu.com (out-211.mta1.migadu.com [95.215.58.211]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 449B835C6AC for ; Tue, 22 Sep 2026 01:04:01 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.211 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039043; cv=none; b=USVMiOkD+DNWD4DovrZ/u0feKteMSYdzJPH8JNUqL6KKj0AGG1veAByjB1eeYeRIv4EXmaTfUxvdHtELOLB2Ear7BvpqCDxAuJ8D/CS4fGqufWTIacJsJauJPbLjohIWc5OA9K7ewqhX32ayA90b0S8Mh2gFnhwCsRNnAZd1eI0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039043; c=relaxed/simple; bh=zuITKrDzK/W04QHVPRkQzNSVbpIuObx0xfz+lo2tIGI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=kesSz7QeXI1AJev3J9LHhyQI3xRT5N/FwkzjxenHwpbEF0EWA8L1JBEN6T1EYOjspuFEXKpX+/2JBREyciwowthnrgp2RXnhZb0FJf4PyNYLDCyYiIzrm3OobjLeXV1A5+mDOFygfup+C4xTyJqV8e/N700u+FGiu7/F8xkl9d8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=po37Qn95; arc=none smtp.client-ip=95.215.58.211 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="po37Qn95" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=zuITKrDzK/W04QHVPRkQzNSVbpIuObx0xfz+lo2tIGI=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1790039040; v=1; x=1790643840; b=po37Qn95s8NCUgBqP9yLGPar77/YYmG/1NrsB63a8GCcJauT084FtuoSlEZRrmxPCdFoizIa Z9D6J1WVdfXRzJcMaV5ocg3izUpNYgZo6aanH+15NuYPiHmjJBOz63FNtvBntqNYYEwDwP2LKr3 Dd8fKAHknK4+8hVEre34TLYI= X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id 94195cd9d5b62b7a; Tue, 22 Sep 2026 01:04:00 +0000 X-Mizu-Trace-ID: 94195cd9d5b62b7a X-Migadu-Flow: FLOW_OUT From: Ihor Solodrai To: Alexei Starovoitov , Andrii Nakryiko , Daniel Borkmann , Eduard Zingerman , Kumar Kartikeya Dwivedi Cc: Amery Hung , Emil Tsalapatis , Nicholas Carlini , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, kernel-team@meta.com Subject: [PATCH bpf-next v1 2/6] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame Date: Mon, 21 Sep 2026 18:03:29 -0700 Message-ID: <20260922010333.1226537-3-ihor.solodrai@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260922010333.1226537-1-ihor.solodrai@linux.dev> References: <20260922010333.1226537-1-ihor.solodrai@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" bpf_user_ringbuf_drain() peeks a sample, initialises a struct bpf_dynptr_kern on its own stack, passes it to the callback, and calls __bpf_user_ringbuf_sample_release() as soon as the callback returns. Neither the descriptor nor the sample it describes is valid afterwards. set_user_ringbuf_callback_state() only gives the callback's R1 a type. The callback can therefore store the CONST_PTR_TO_DYNPTR register into callback_ctx, which points into a frame that outlives the call, and the program can use it after the drain returns. Three routes reach past the callback: - the register itself, spillable since v7.3-rc1, which points at a descriptor on reused kernel stack and gives the program an arbitrary kernel read/write - a bpf_dynptr_data() or bpf_dynptr_slice() result, which carries the dynptr's id as its parent_id - a bpf_dynptr_clone(), which is a by-value copy in the caller's frame and can be sliced after the drain returns Declare R1 frame-scoped so all three are invalidated when the callback frame is popped. Reported-by: Nicholas Carlini Signed-off-by: Ihor Solodrai --- kernel/bpf/verifier.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index a0a9d3d18f63..9775a6d38d3b 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -707,11 +707,15 @@ static void mark_dynptr_stack_regs(struct bpf_verifie= r_env *env, __mark_dynptr_reg(sreg2, type, false, id, parent_id); } =20 -static void mark_dynptr_cb_reg(struct bpf_verifier_env *env, - struct bpf_reg_state *reg, +/* + * A callback dynptr argument is valid only until the frame is popped, so + * setup_func_entry() assigns its id along with the frame reference. + */ +static void mark_dynptr_cb_reg(struct bpf_func_state *callee, u32 regno, enum bpf_dynptr_type type) { - __mark_dynptr_reg(reg, type, true, ++env->id_gen, 0); + __mark_dynptr_reg(&callee->regs[regno], type, true, 0, 0); + mark_frame_scoped_arg(callee, regno); } =20 static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env, @@ -10962,7 +10966,7 @@ static int set_user_ringbuf_callback_state(struct b= pf_verifier_env *env, * callback_fn(const struct bpf_dynptr_t* dynptr, void *callback_ctx); */ bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_0]); - mark_dynptr_cb_reg(env, &callee->regs[BPF_REG_1], BPF_DYNPTR_TYPE_LOCAL); + mark_dynptr_cb_reg(callee, BPF_REG_1, BPF_DYNPTR_TYPE_LOCAL); callee->regs[BPF_REG_2] =3D caller->regs[BPF_REG_3]; =20 /* unused */ --=20 2.55.0 From nobody Thu Sep 24 18:39:29 2026 Received: from mta0.migadu.com (out-206.mta0.migadu.com [91.218.175.206]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 414B1351C27 for ; Tue, 22 Sep 2026 01:04:08 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.206 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039051; cv=none; b=SFqQA0QZM3LaQJXIUja8v+FxIlp3M1Jpm8YiiaBH5E9W1E412R/+RrAh0yXVtXyD6zZ04+YEnXqJ0pchaAraZmfuqs7Q//fDf/v+MDzuPu3K486yWfInZq6AOltoiWe5dJQl6vVJyPyPRJbsZ4XlP95sj91e1oVLxm/5OfgIq6k= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039051; c=relaxed/simple; bh=BZocnogs0CgdLwdI0RpXwNe1/4+SbNzNm3pVU9Fn/kU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=ZBWj0ySKhxwqLVZtAy4h6CZWkNYdB8hP0aNfylZFnbxp8kRjgphHSAZvrc79op+5ewh0NBrb7nowNC2+2rbH0utbBCvj/ajvlpRz1oTNXbYQKM7rku1/SlQs371KiwI5xPrAfVOLFw1EckpcsuS6KARtQMtLWdwf8JGf4ugzobE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=HMXR3sCx; arc=none smtp.client-ip=91.218.175.206 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="HMXR3sCx" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=BZocnogs0CgdLwdI0RpXwNe1/4+SbNzNm3pVU9Fn/kU=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1790039047; v=1; x=1790643847; b=HMXR3sCx5v7J29GB7uodGr8n+vrP9R0UB9qxGFjCfzi8bT23dimfxvUdVtkXKIeCEqpzSNKq uEelYB1haW5wLh4hL8t0b1UGvRmH0jVlH4lBBPiXtvkeBZzGJxJ1nXvaPXQ5ZINBawpN5jCaMyX 0NLTLvWg04LvkLoz8XluOsAY= X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id 15fce196c86a403b; Tue, 22 Sep 2026 01:04:07 +0000 X-Mizu-Trace-ID: 15fce196c86a403b X-Migadu-Flow: FLOW_OUT From: Ihor Solodrai To: Alexei Starovoitov , Andrii Nakryiko , Daniel Borkmann , Eduard Zingerman , Kumar Kartikeya Dwivedi Cc: Amery Hung , Emil Tsalapatis , Nicholas Carlini , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, kernel-team@meta.com Subject: [PATCH bpf-next v1 3/6] bpf: Name the callback in frame-release diagnostics Date: Mon, 21 Sep 2026 18:03:30 -0700 Message-ID: <20260922010333.1226537-4-ihor.solodrai@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260922010333.1226537-1-ihor.solodrai@linux.dev> References: <20260922010333.1226537-1-ihor.solodrai@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" A frame-owned reference is dropped through the same descendant walk as a program-owned one, so a program that uses a callback argument after the callback returns is told "resource release invalidated this value". No resource was released, and nothing the program did caused it. Add __release_reference() with the reason parameter. The message names the callback rather than the frame because a callback is the only thing that declares a frame-scoped argument today, and it is what the program author recognises. Signed-off-by: Ihor Solodrai --- kernel/bpf/diagnostics.c | 3 +++ kernel/bpf/diagnostics.h | 1 + kernel/bpf/verifier.c | 18 +++++++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c index 5ecfa86ed49f..0610c3f6b334 100644 --- a/kernel/bpf/diagnostics.c +++ b/kernel/bpf/diagnostics.c @@ -2233,6 +2233,9 @@ static void diag_print_mod(struct bpf_verifier_env *e= nv, const struct bpf_diag_h "resource release invalidated " "this value"; break; + case BPF_DIAG_MOD_FRAME_RELEASE: + reason =3D "the callback that owned this value returned"; + break; case BPF_DIAG_MOD_PKT_DATA_CHANGE: reason =3D "packet data may have moved"; break; diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h index a4102fb049ec..b5cab4d79c1a 100644 --- a/kernel/bpf/diagnostics.h +++ b/kernel/bpf/diagnostics.h @@ -22,6 +22,7 @@ enum bpf_diag_mod_reason { BPF_DIAG_MOD_SPILL, BPF_DIAG_MOD_VAR_WRITE, BPF_DIAG_MOD_REF_RELEASE, + BPF_DIAG_MOD_FRAME_RELEASE, BPF_DIAG_MOD_PKT_DATA_CHANGE, BPF_DIAG_MOD_NON_OWN_REF, BPF_DIAG_MOD_CALLER_SAVED, diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 9775a6d38d3b..ba0c8c27b45f 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -10169,7 +10169,8 @@ static int idstack_pop(struct bpf_idmap *idmap) } =20 /* Release id and objects derived from it iteratively in a DFS manner */ -static int release_reference(struct bpf_verifier_env *env, int id) +static int __release_reference(struct bpf_verifier_env *env, int id, + enum bpf_diag_mod_reason reason) { u32 mask =3D (1 << STACK_SPILL) | (1 << STACK_DYNPTR); struct bpf_verifier_state *vstate =3D env->cur_state; @@ -10224,14 +10225,12 @@ static int release_reference(struct bpf_verifier_= env *env, int id) =20 if (reg->dynptr.first_slot) dyn_stack--; - bpf_diag_record_scrub(env, &dyn_stack[0].spilled_ptr, - BPF_DIAG_MOD_REF_RELEASE); - bpf_diag_record_scrub(env, &dyn_stack[1].spilled_ptr, - BPF_DIAG_MOD_REF_RELEASE); + bpf_diag_record_scrub(env, &dyn_stack[0].spilled_ptr, reason); + bpf_diag_record_scrub(env, &dyn_stack[1].spilled_ptr, reason); invalidate_dynptr(env, dyn_stack); continue; } - bpf_diag_record_scrub(env, reg, BPF_DIAG_MOD_REF_RELEASE); + bpf_diag_record_scrub(env, reg, reason); if (!stack || stack->slot_type[BPF_REG_SIZE - 1] =3D=3D STACK_SPILL) mark_reg_invalid(env, reg); })); @@ -10240,6 +10239,11 @@ static int release_reference(struct bpf_verifier_e= nv *env, int id) return 0; } =20 +static int release_reference(struct bpf_verifier_env *env, int id) +{ + return __release_reference(env, id, BPF_DIAG_MOD_REF_RELEASE); +} + /* Find the first reference owned by frame @frameno, or 0 if it owns none.= */ static u32 frame_reference_id(struct bpf_verifier_state *state, u32 framen= o) { @@ -10265,7 +10269,7 @@ static int release_frame_reference(struct bpf_verif= ier_env *env, int id) break; } =20 - return release_reference(env, id); + return __release_reference(env, id, BPF_DIAG_MOD_FRAME_RELEASE); } =20 static void invalidate_non_owning_refs(struct bpf_verifier_env *env) --=20 2.55.0 From nobody Thu Sep 24 18:39:29 2026 Received: from mta0.migadu.com (out-210.mta0.migadu.com [91.218.175.210]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6028935C1BD for ; Tue, 22 Sep 2026 01:04:16 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.210 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039058; cv=none; b=qa9MN6iNO3aA+CDwwyCXb2/jJS8x2Q41uNeszfhFeiCfywzAuCUVNvjnSgHYOyvl7iE4QOqQw6n0WwOTmKJGspKjX6oGvEBGH/9Ad1oNOlw9vqvu7Z7SNEP2nx/t642YZu1IrmH5otfOeKYLCgz58NkdaAUx3pE9BhRnSOwBFIc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039058; c=relaxed/simple; bh=MOFDZylnJkHMTUTkD8mTkGLhpA5ihuPrk4xk8kIvGvI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=u8985Q2Dm5BtBunXA7fSjfIvTNAf5c5evVmqxEb8v5JRmS7hzdPnsNqU9KGuwZXI3O8qYPcvi8JPznwWtzDtdI1cWvX5tSM+uZsBzYDZAaj3RfWGeFekaZfnkpcpFAdRPrFScQlSE9zjLylnPl0iCePxSUHJoLRgIFfNhE7BLIk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=oCVYKxuD; arc=none smtp.client-ip=91.218.175.210 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="oCVYKxuD" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=MOFDZylnJkHMTUTkD8mTkGLhpA5ihuPrk4xk8kIvGvI=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1790039054; v=1; x=1790643854; b=oCVYKxuD7Ir6sw2ZzM62+TGd2+KJ072TgjLcCpnjRz3zQ1IMba/XV6VRGRowz/TlI/ySGW4t 9O7L7Pia/VwYS3fLjXtPt866jbm7CBK8WxhclbGiSRysI9hT6EHoVPh7fl5Y6qPC4Vq7axA1Lzq DbIUP+y6LpFcNsqKGpjYa2NE= X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id d68bdd8ebbe95197; Tue, 22 Sep 2026 01:04:14 +0000 X-Mizu-Trace-ID: d68bdd8ebbe95197 X-Migadu-Flow: FLOW_OUT From: Ihor Solodrai To: Alexei Starovoitov , Andrii Nakryiko , Daniel Borkmann , Eduard Zingerman , Kumar Kartikeya Dwivedi Cc: Amery Hung , Emil Tsalapatis , Nicholas Carlini , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, kernel-team@meta.com Subject: [PATCH bpf-next v1 4/6] selftests/bpf: Cover the user ringbuf callback dynptr lifetime Date: Mon, 21 Sep 2026 18:03:31 -0700 Message-ID: <20260922010333.1226537-5-ihor.solodrai@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260922010333.1226537-1-ihor.solodrai@linux.dev> References: <20260922010333.1226537-1-ihor.solodrai@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a rejection test for each route the bpf_user_ringbuf_drain() callback dynptr can take out of its frame: - the CONST_PTR_TO_DYNPTR register parked in callback_ctx - a bpf_dynptr_data() slice - a bpf_dynptr_slice() slice - a bpf_dynptr_clone() written into the caller's frame - a slice taken from that clone after the drain returns - an inner drain's dynptr escaping into an outer callback The first also checks that the diagnostic names the callback. bpf_throw() from the callback reaches check_reference_leak() with the frame reference still live, and is rejected afterwards by check_max_stack_depth(). It matches that rejection in full, because "bpf_throw" alone also matches the reference-leak wording the callback must not produce. Signed-off-by: Ihor Solodrai --- .../selftests/bpf/progs/exceptions_fail.c | 20 +++ .../selftests/bpf/progs/user_ringbuf_fail.c | 143 ++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/exceptions_fail.c b/tools/te= sting/selftests/bpf/progs/exceptions_fail.c index 22503cf62e9f..86a0667ba348 100644 --- a/tools/testing/selftests/bpf/progs/exceptions_fail.c +++ b/tools/testing/selftests/bpf/progs/exceptions_fail.c @@ -31,6 +31,11 @@ struct { __type(value, struct hmap_elem); } hmap SEC(".maps"); =20 +struct { + __uint(type, BPF_MAP_TYPE_USER_RINGBUF); + __uint(max_entries, 4096); +} user_ringbuf SEC(".maps"); + private(A) struct bpf_spin_lock lock; private(A) struct bpf_rb_root rbtree __contains(foo, node); =20 @@ -110,6 +115,21 @@ static int timer_cb(void *map, int *key, struct bpf_ti= mer *timer) return 0; } =20 +static long drain_cb(struct bpf_dynptr *dynptr, void *context) +{ + bpf_throw(0); + return 0; +} + +SEC("?tc") +__failure +__msg("bpf_throw kfunc (insn {{[0-9]+}}) cannot be called from callback su= bprog {{[0-9]+}}") +int reject_user_ringbuf_callback_throw(struct __sk_buff *ctx) +{ + bpf_user_ringbuf_drain(&user_ringbuf, drain_cb, NULL, 0); + return 0; +} + SEC("?tc") __failure __msg("cannot be called from callback subprog") int reject_async_callback_throw(struct __sk_buff *ctx) diff --git a/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c b/tools/= testing/selftests/bpf/progs/user_ringbuf_fail.c index c0d0422b8030..a8d3acfa2bd2 100644 --- a/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c +++ b/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0 /* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */ =20 +#include #include #include #include "bpf_misc.h" +#include "bpf_kfuncs.h" =20 char _license[] SEC("license") =3D "GPL"; =20 @@ -243,3 +245,144 @@ int user_ringbuf_callback_const_ptr_to_dynptr_reg_off= (void *ctx) callback_adjust_bpf_dynptr_reg_off, NULL, 0); return 0; } + +/* The sample goes back to the producer as soon as the callback returns. */ +struct dynptr_ctx { + struct bpf_dynptr *saved; +}; + +static long callback_park_dynptr(struct bpf_dynptr *dynptr, void *context) +{ + struct dynptr_ctx *c =3D context; + + c->saved =3D dynptr; + return 0; +} + +SEC("?raw_tp") +__failure __msg("the callback that owned this value returned") +int user_ringbuf_callback_park_dynptr(void *ctx) +{ + struct dynptr_ctx c =3D {}; + char buf[8] =3D {}; + + bpf_user_ringbuf_drain(&user_ringbuf, callback_park_dynptr, &c, 0); + if (c.saved) + bpf_dynptr_read(buf, sizeof(buf), c.saved, 0, 0); + return buf[0]; +} + +struct slice_ctx { + char *p; +}; + +static long callback_park_data_slice(struct bpf_dynptr *dynptr, void *cont= ext) +{ + struct slice_ctx *c =3D context; + + c->p =3D bpf_dynptr_data(dynptr, 0, 8); + return 0; +} + +SEC("?raw_tp") +__failure __msg("the callback that owned this value returned") +int user_ringbuf_callback_park_data_slice(void *ctx) +{ + struct slice_ctx c =3D {}; + + bpf_user_ringbuf_drain(&user_ringbuf, callback_park_data_slice, &c, 0); + if (c.p) + return c.p[0]; + return 0; +} + +static long callback_park_kfunc_slice(struct bpf_dynptr *dynptr, void *con= text) +{ + struct slice_ctx *c =3D context; + + c->p =3D bpf_dynptr_slice(dynptr, 0, NULL, 8); + return 0; +} + +SEC("?raw_tp") +__failure __msg("the callback that owned this value returned") +int user_ringbuf_callback_park_kfunc_slice(void *ctx) +{ + struct slice_ctx c =3D {}; + + bpf_user_ringbuf_drain(&user_ringbuf, callback_park_kfunc_slice, &c, 0); + if (c.p) + return c.p[0]; + return 0; +} + +struct clone_ctx { + struct bpf_dynptr clone; + __u64 armed; +}; + +static long callback_park_clone(struct bpf_dynptr *dynptr, void *context) +{ + struct clone_ctx *c =3D context; + + bpf_dynptr_clone(dynptr, &c->clone); + c->armed =3D 1; + return 0; +} + +SEC("?raw_tp") +__failure __msg("Expected an initialized dynptr as R3") +int user_ringbuf_callback_park_clone(void *ctx) +{ + struct clone_ctx c =3D {}; + char buf[8] =3D {}; + + bpf_user_ringbuf_drain(&user_ringbuf, callback_park_clone, &c, 0); + if (c.armed) + bpf_dynptr_read(buf, sizeof(buf), &c.clone, 0, 0); + return buf[0]; +} + +SEC("?raw_tp") +__failure __msg("Expected an initialized dynptr as R1") +int user_ringbuf_callback_park_clone_then_slice(void *ctx) +{ + struct clone_ctx c =3D {}; + char *p; + + bpf_user_ringbuf_drain(&user_ringbuf, callback_park_clone, &c, 0); + if (c.armed) { + p =3D bpf_dynptr_data(&c.clone, 0, 8); + if (p) + return p[0]; + } + return 0; +} + +static long callback_park_inner(struct bpf_dynptr *dynptr, void *context) +{ + struct dynptr_ctx *c =3D context; + + c->saved =3D dynptr; + return 0; +} + +/* An inner drain's dynptr must not escape into the outer callback either.= */ +static long callback_park_outer(struct bpf_dynptr *dynptr, void *context) +{ + struct dynptr_ctx inner =3D {}; + char buf[8] =3D {}; + + bpf_user_ringbuf_drain(&user_ringbuf, callback_park_inner, &inner, 0); + if (inner.saved) + bpf_dynptr_read(buf, sizeof(buf), inner.saved, 0, 0); + return buf[0] ? 1 : 0; +} + +SEC("?raw_tp") +__failure __msg("the callback that owned this value returned") +int user_ringbuf_callback_nested_park_inner(void *ctx) +{ + bpf_user_ringbuf_drain(&user_ringbuf, callback_park_outer, NULL, 0); + return 0; +} --=20 2.55.0 From nobody Thu Sep 24 18:39:29 2026 Received: from mta0.migadu.com (out-214.mta0.migadu.com [91.218.175.214]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id BE6D235E944 for ; Tue, 22 Sep 2026 01:04:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.214 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039061; cv=none; b=pPSnnALMFcnFObjj5C4bAk4kIy78NtR0xOOI+zXyjz56Pc8nLxOK4/aA0Z0L7ZIWysMS+kuvmB7d8uzRXnco/Gpg9C8c4GOcih72Jap79L0ynAv2YaEOKommVtzWjSVv0fUP6T/VEb/x54yRc0iNTJX6AkNzQqlZ/f5Lts/W9ys= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039061; c=relaxed/simple; bh=B4IgAJFaBDe7+AnJdyCtufdv++YTnIeHzExnz5tpDk0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=u1ZQPxZLjox8PeVfS6Kax6lJE74bUdm+BGdgtdf7CyZMHkXOvV/LLppS5HM0IN/inKF4bVm7/DqL6rIVdsl142xhwl39RK4vmfDdBj2doOLxWoEEERdDnGyX2SV7UUa59XFV5lmM3uJk9oRXR0NVIp+87u2006TwIt9KSMoqFvQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=e82zZil2; arc=none smtp.client-ip=91.218.175.214 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="e82zZil2" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=B4IgAJFaBDe7+AnJdyCtufdv++YTnIeHzExnz5tpDk0=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1790039057; v=1; x=1790643857; b=e82zZil2PDhMKHEUK6gQWojxup4iHvokOTFuHyzaFSVjYNeoDq5CHUyxgr7EhsLM8KKEYt8s PO+aNg/92GWAXW74SrqtqN/CB4Hh3bnfrrSgg+9iTQqWg63GiOY0CKTGq2tHNwsI6SSuuOB33kr W7hACsRE+kX/LtwbvK+J9x/8= X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id fb8fc81bfb4299fe; Tue, 22 Sep 2026 01:04:17 +0000 X-Mizu-Trace-ID: fb8fc81bfb4299fe X-Migadu-Flow: FLOW_OUT From: Ihor Solodrai To: Alexei Starovoitov , Andrii Nakryiko , Daniel Borkmann , Eduard Zingerman , Kumar Kartikeya Dwivedi Cc: Amery Hung , Emil Tsalapatis , Nicholas Carlini , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, kernel-team@meta.com Subject: [PATCH bpf-next v1 5/6] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame Date: Mon, 21 Sep 2026 18:03:32 -0700 Message-ID: <20260922010333.1226537-6-ihor.solodrai@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260922010333.1226537-1-ihor.solodrai@linux.dev> References: <20260922010333.1226537-1-ihor.solodrai@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" bpf_for_each_array_elem() passes the callback the address of a u32 held in its own stack frame: u32 i, key, num_elems =3D 0; ... key =3D i; ret =3D callback_fn((u64)(long)map, (u64)(long)&key, ...); The callback can store that PTR_TO_MAP_KEY into callback_ctx and the program can dereference it after the iteration finishes. Loads through PTR_TO_MAP_KEY are not fault-protected and array key_size is fixed at 4, so it is a four-byte read-only leak of kernel stack. Declare the key frame-scoped for the array map ops rather than in the shared map_set_for_each_callback_args(): of the map_for_each_callback implementations, only array and percpu-array pass a key from their own frame. The hash family passes elem->key, which stays valid for as long as the program runs. map_key_from_value() does the same for array maps, for the timer, wq and task_work callbacks. Those are left alone: their signature is (map, key, value) with R4 and R5 uninitialised, so there is no callback_ctx to park a typed pointer in, and a pointer written into map memory loses its type. The element value is unaffected in every case: it lives until map teardown. The frame-owned reference consumes an id, so leak_prog and nested_cb now report id 5 rather than id 4; update the expected messages. Reported-by: Nicholas Carlini Signed-off-by: Ihor Solodrai --- kernel/bpf/arraymap.c | 18 ++++++++++++++++-- .../testing/selftests/bpf/prog_tests/cb_refs.c | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/kernel/bpf/arraymap.c b/kernel/bpf/arraymap.c index 0ce26b538075..44bd229873ca 100644 --- a/kernel/bpf/arraymap.c +++ b/kernel/bpf/arraymap.c @@ -855,6 +855,20 @@ static u64 array_map_mem_usage(const struct bpf_map *m= ap) return usage; } =20 +static int array_map_set_for_each_callback_args(struct bpf_verifier_env *e= nv, + struct bpf_func_state *caller, + struct bpf_func_state *callee) +{ + int err; + + err =3D map_set_for_each_callback_args(env, caller, callee); + if (err) + return err; + + mark_frame_scoped_arg(callee, BPF_REG_2); + return 0; +} + BTF_ID_LIST_SINGLE(array_map_btf_ids, struct, bpf_array) const struct bpf_map_ops array_map_ops =3D { .map_meta_equal =3D array_map_meta_equal, @@ -875,7 +889,7 @@ const struct bpf_map_ops array_map_ops =3D { .map_check_btf =3D array_map_check_btf, .map_lookup_batch =3D generic_map_lookup_batch, .map_update_batch =3D generic_map_update_batch, - .map_set_for_each_callback_args =3D map_set_for_each_callback_args, + .map_set_for_each_callback_args =3D array_map_set_for_each_callback_args, .map_for_each_callback =3D bpf_for_each_array_elem, .map_mem_usage =3D array_map_mem_usage, .map_btf_id =3D &array_map_btf_ids[0], @@ -900,7 +914,7 @@ const struct bpf_map_ops percpu_array_map_ops =3D { .map_check_btf =3D array_map_check_btf, .map_lookup_batch =3D generic_map_lookup_batch, .map_update_batch =3D generic_map_update_batch, - .map_set_for_each_callback_args =3D map_set_for_each_callback_args, + .map_set_for_each_callback_args =3D array_map_set_for_each_callback_args, .map_for_each_callback =3D bpf_for_each_array_elem, .map_mem_usage =3D array_map_mem_usage, .map_btf_id =3D &array_map_btf_ids[0], diff --git a/tools/testing/selftests/bpf/prog_tests/cb_refs.c b/tools/testi= ng/selftests/bpf/prog_tests/cb_refs.c index c32c6dab49bc..645f065c21f5 100644 --- a/tools/testing/selftests/bpf/prog_tests/cb_refs.c +++ b/tools/testing/selftests/bpf/prog_tests/cb_refs.c @@ -12,8 +12,8 @@ struct { const char *err_msg; } cb_refs_tests[] =3D { { "underflow_prog", "R1 type=3Dscalar expected=3Dptr_, trusted_ptr_, rcu_= ptr_" }, - { "leak_prog", "Unreleased reference id=3D4 alloc_insn=3D3" }, /* alloc_i= nsn=3D3{2,3} */ - { "nested_cb", "Unreleased reference id=3D4 alloc_insn=3D2" }, /* alloc_i= nsn=3D2{4,5} */ + { "leak_prog", "Unreleased reference id=3D5 alloc_insn=3D3" }, /* alloc_i= nsn=3D3{2,3} */ + { "nested_cb", "Unreleased reference id=3D5 alloc_insn=3D2" }, /* alloc_i= nsn=3D2{4,5} */ { "non_cb_transfer_ref", "Unreleased reference id=3D4 alloc_insn=3D1" }, = /* alloc_insn=3D1{1,2} */ }; =20 --=20 2.55.0 From nobody Thu Sep 24 18:39:29 2026 Received: from mta1.migadu.com (out-225.mta1.migadu.com [95.215.58.225]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 852CB361960 for ; Tue, 22 Sep 2026 01:04:23 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.225 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039065; cv=none; b=cGmWLwUvoYeoZTRAz6JqxWVLO5Yk8AGWVpvXPzBGpgw9l36bIAlGlNoDp1xKK2D2O49LQ4x2//Kjf/q6J8DrByxh41CfUvLI+Z8jlJ10wr3yNcXo/agTjkFSOs3z1H/JkyzGgkJ04Q1YvFaRUmSP6o6OhaSqJbOaKRdFl1bUkjM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790039065; c=relaxed/simple; bh=Vw7xOFj7NDfG3x/79uhiyl8/V5mf++Vc9MTJPJzRdcI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=D/gc36a/Q3XmSqSSvnk0arm7EvYc4iGyoNmNtZvxv9YrE2/FRyLqKSgFNIXxkzjaBIn7i/YlwohNZmU/9j7lPPDq317Ge15luFsfAfcGlxOm6E1MH5SAy71OSOHBFGtnUtFLJ5mjDlXVNEEFd0qmhkxM35b3pAHFOxjzmCY6qkM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=nnIKsi0K; arc=none smtp.client-ip=95.215.58.225 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="nnIKsi0K" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=Vw7xOFj7NDfG3x/79uhiyl8/V5mf++Vc9MTJPJzRdcI=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1790039061; v=1; x=1790643861; b=nnIKsi0KRGUx/kluNqMGRa4xKPlxP8nsEY7UdjALJJOObHAtl0CKafzsmOk+31wv876QLWkb HmJC+S0pUwUtC9qsl4LMvwws7txSwdQPsn8zq2A+Eid3puUQg7IdAXuLfQUk2Of3SvZ51dE0Ss9 EcOKJeFjpiExbMLIhHHKDHfY= X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id fe3c14a229ddcf9d; Tue, 22 Sep 2026 01:04:21 +0000 X-Mizu-Trace-ID: fe3c14a229ddcf9d X-Migadu-Flow: FLOW_OUT From: Ihor Solodrai To: Alexei Starovoitov , Andrii Nakryiko , Daniel Borkmann , Eduard Zingerman , Kumar Kartikeya Dwivedi Cc: Amery Hung , Emil Tsalapatis , Nicholas Carlini , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, kernel-team@meta.com Subject: [PATCH bpf-next v1 6/6] selftests/bpf: Cover callback-frame map key lifetime Date: Mon, 21 Sep 2026 18:03:33 -0700 Message-ID: <20260922010333.1226537-7-ihor.solodrai@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260922010333.1226537-1-ihor.solodrai@linux.dev> References: <20260922010333.1226537-1-ihor.solodrai@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Parking the key in callback_ctx and dereferencing it after the iteration is rejected for array and percpu-array maps, whose key lives in bpf_for_each_array_elem()'s frame. Two cases must keep verifying, and are the reason the declaration is not in the shared map_set_for_each_callback_args(): the same shape over a hash map, whose key points into the element, and parking the element value, which lives until map teardown. Signed-off-by: Ihor Solodrai --- .../bpf/progs/verifier_iterating_callbacks.c | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks= .c b/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c index 1fbcc5228306..2e0c56888953 100644 --- a/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c +++ b/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c @@ -9,6 +9,20 @@ struct { __type(value, __u64); } map SEC(".maps"); =20 +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 8); + __type(key, __u32); + __type(value, __u64); +} percpu_map SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8); + __type(key, __u32); + __type(value, __u64); +} hash_map SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_USER_RINGBUF); __uint(max_entries, 8); @@ -800,4 +814,81 @@ __naked void check_add_const_regsafe_off(void) : __clobber_common); } =20 +struct key_ctx { + __u32 *key; +}; + +static long park_key_cb(struct bpf_map *map, __u32 *key, __u64 *value, + void *context) +{ + struct key_ctx *c =3D context; + + c->key =3D key; + return 0; +} + +/* bpf_for_each_array_elem() passes a key from its own stack frame. */ +SEC("?raw_tp") +__failure __msg("invalid mem access 'scalar'") +int array_park_map_key(void *ctx) +{ + struct key_ctx c =3D {}; + + bpf_for_each_map_elem(&map, park_key_cb, &c, 0); + if (c.key) + return *c.key; + return 0; +} + +SEC("?raw_tp") +__failure __msg("invalid mem access 'scalar'") +int percpu_array_park_map_key(void *ctx) +{ + struct key_ctx c =3D {}; + + bpf_for_each_map_elem(&percpu_map, park_key_cb, &c, 0); + if (c.key) + return *c.key; + return 0; +} + +/* A hash key points into the element, which outlives the callback. */ +SEC("?raw_tp") +__success +int hash_park_map_key(void *ctx) +{ + struct key_ctx c =3D {}; + + bpf_for_each_map_elem(&hash_map, park_key_cb, &c, 0); + if (c.key) + return *c.key; + return 0; +} + +struct value_ctx { + __u64 *value; +}; + +static long park_value_cb(struct bpf_map *map, __u32 *key, __u64 *value, + void *context) +{ + struct value_ctx *c =3D context; + + c->value =3D value; + return 0; +} + +/* Only the key is frame-scoped; the element lives until map teardown. */ +SEC("?raw_tp") +__success +int array_park_map_value(void *ctx) +{ + struct value_ctx c =3D {}; + + bpf_for_each_map_elem(&map, park_value_cb, &c, 0); + if (c.value) + return *c.value; + return 0; +} + char _license[] SEC("license") =3D "GPL"; --=20 2.55.0