From nobody Fri Oct 2 05:28:36 2026 Received: from out-188.mta0.migadu.com (out-188.mta0.migadu.com [91.218.175.188]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 811FA352C4E for ; Wed, 5 Aug 2026 04:05:09 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.188 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785902711; cv=none; b=aGz0qTaX4VE6HJJWScMKg3Fo25KJIvEYMxTv/TCNn21w4PMCDlj6cfS/A5mKTqyShxpxgp8Z7Zx7X4z6hi9i95xMc2RH+jZZ+XYsrwmhBYpeQw7T9A34Hc0/Q17bmT2acIQGGNuRsUkIAD7CxP0OJpXmgH+Pl5XE47Z3FBxqric= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785902711; c=relaxed/simple; bh=h7uAGJBjtGoS/ep5Zzp6kY2tjC6QbsXh10qEgOILh0I=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=RgUjYvsP/BxGE6jnLH6i3VRVwCEF2aaINKIUzJKInKuJqSfrGzr8ZfOFbSq79I4UZfPes6UoTaYJHB2Yg+HPNUyoA90fMfoIt7ttslAmIfc4HNpNpT5JIMUwEa435vfcsDCavi+S+iYZgdeeAhc/w9foj/eSO/utOPwVgnvlsBQ= 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=mqR+FCD7; arc=none smtp.client-ip=91.218.175.188 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="mqR+FCD7" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1785902707; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=TYXN6Zjkj2RG7e1IZ+JvD2cu/6XXDmjjam5GabPUNes=; b=mqR+FCD7qPyE54X4u9cbs9fW2Kvn1ItA7Mf9fsIcFeXSOHarBU6nltmwkaCR4ZTlimgOnu 7iA9ci3ILNDChrG9lAMNorRMb8j/c7O8DNobTdgkqH8b5k4pHdt+Km4TOa2y/52BMir2jh aQG4zl4PWw/NhYspFkAr5DxtbElVUqk= From: "Hui Zhu" To: Alexei Starovoitov , Daniel Borkmann , John Fastabend , Andrii Nakryiko , Eduard Zingerman , Kumar Kartikeya Dwivedi , Martin KaFai Lau , Song Liu , Yonghong Song , Jiri Olsa , Emil Tsalapatis , Ihor Solodrai , KP Singh , Matt Bobrowski , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org Cc: Hui Zhu Subject: [PATCH bpf-next v2 1/3] bpf: Fix UAF in bpf_trampoline_multi_detach on update failure Date: Wed, 5 Aug 2026 12:04:06 +0800 Message-ID: <0276810360a8c0e57aab95a292ff6453242b969c.1785902527.git.zhuhui@kylinos.cn> In-Reply-To: References: 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Hui Zhu Two UAF scenarios exist in bpf_trampoline_multi_detach() error paths: 1. If __bpf_trampoline_unlink_prog() fails, cur_image =3D=3D old_image and ftrace still points to it, but bpf_trampoline_multi_attach_free() unconditionally frees old_image. Fix: only free old_image when it differs from cur_image. 2. If the batch update_ftrace_direct_del/mod() fails, ftrace still points to old_image, but _free() frees it. Fix: use rollback (restores cur_image =3D old_image) instead of _free() for affected mnodes. Rollback keeps the image alive but the caller still frees the prog whose call is baked into it. Pin the prog on old_image via a new pinned_prog field in struct bpf_tramp_image (released in bpf_tramp_image_free()). bpf_trampoline_put() must not free a trampoline whose cur_image was left behind by rollback -- ftrace may still call into it. Leak the trampoline instead (it's already unlinked from lookup tables). pinned_prog is a single pointer: if multiple progs need pinning on the same image (rare), only the last is tracked and earlier refs are leaked (not a UAF). This is an intentional trade-off. Also make bpf_trampoline_multi_detach() return void since callers cannot usefully react to failures. Fixes: aef4dfa790b2 ("bpf: Add bpf_trampoline_multi_attach/detach functions= ") Signed-off-by: Hui Zhu --- include/linux/bpf.h | 20 +++++-- kernel/bpf/trampoline.c | 112 +++++++++++++++++++++++++++++++++++---- kernel/trace/bpf_trace.c | 2 +- 3 files changed, 118 insertions(+), 16 deletions(-) diff --git a/include/linux/bpf.h b/include/linux/bpf.h index 73bacfc6444d..cd32c6f54eeb 100644 --- a/include/linux/bpf.h +++ b/include/linux/bpf.h @@ -1372,6 +1372,17 @@ struct bpf_tramp_image { struct rcu_head rcu; struct work_struct work; }; + /* + * Extra reference on the bpf_prog whose call is baked into this + * image's machine code, held only when a required ftrace + * direct-call update failed while retiring/replacing this image + * (see bpf_trampoline_multi_attach()/_detach() in trampoline.c). + * ftrace may still be directing calls into this image, so neither + * the image nor the pinned prog can be freed until a later, + * successful ftrace update proves this image is no longer in use. + * Released in bpf_tramp_image_free() alongside the image itself. + */ + struct bpf_prog *pinned_prog; }; =20 struct bpf_trampoline { @@ -1518,8 +1529,8 @@ int arch_prepare_bpf_dispatcher(void *image, void *bu= f, s64 *funcs, int num_func =20 int bpf_trampoline_multi_attach(struct bpf_prog *prog, u32 *ids, struct bpf_tracing_multi_link *link); -int bpf_trampoline_multi_detach(struct bpf_prog *prog, - struct bpf_tracing_multi_link *link); +void bpf_trampoline_multi_detach(struct bpf_prog *prog, + struct bpf_tracing_multi_link *link); void bpf_trampoline_set_flags(struct bpf_trampoline *tr, u32 flags); =20 /* @@ -1639,10 +1650,9 @@ static inline int bpf_trampoline_multi_attach(struct= bpf_prog *prog, u32 *ids, { return -ENOTSUPP; } -static inline int bpf_trampoline_multi_detach(struct bpf_prog *prog, - struct bpf_tracing_multi_link *link) +static inline void bpf_trampoline_multi_detach(struct bpf_prog *prog, + struct bpf_tracing_multi_link *link) { - return -ENOTSUPP; } static inline void bpf_trampoline_set_flags(struct bpf_trampoline *tr, u32= flags) {} #endif diff --git a/kernel/bpf/trampoline.c b/kernel/bpf/trampoline.c index ed7999ad6c66..c08d1a09e638 100644 --- a/kernel/bpf/trampoline.c +++ b/kernel/bpf/trampoline.c @@ -535,6 +535,14 @@ static void bpf_tramp_image_free(struct bpf_tramp_imag= e *im) arch_free_bpf_trampoline(im->image, im->size); bpf_jit_uncharge_modmem(im->size); percpu_ref_exit(&im->pcref); + /* + * This image is confirmed no longer reachable from ftrace (that's + * why we're freeing it), so it's now safe to drop the reference we + * pinned on its behalf while it may have still been live - see + * bpf_trampoline_multi_attach()/_detach(). + */ + if (im->pinned_prog) + bpf_prog_put(im->pinned_prog); kfree_rcu(im, rcu); } =20 @@ -1216,6 +1224,28 @@ void bpf_trampoline_put(struct bpf_trampoline *tr) */ hlist_del(&tr->hlist_key); hlist_del(&tr->hlist_ip); + + /* + * tr->cur_image should already be NULL here. A non-NULL value means + * bpf_trampoline_multi_attach_rollback() left an image behind + * because a required ftrace direct-call update failed (see + * bpf_trampoline_multi_detach()), so ftrace may still be calling + * into it - and, in turn, into the bpf_prog pinned in + * tr->cur_image->pinned_prog. We have no reliable way to confirm + * ftrace has since stopped referencing it, so freeing + * tr->cur_image (and dropping the pinned prog's reference) here + * would risk a use-after-free. + * + * tr has just been unlinked from the lookup tables above, so any + * future attach to this function allocates a fresh trampoline; + * this one, its stuck image, and the pinned prog reference are + * deliberately leaked instead of freed. This is rare (it only + * happens after a genuine ftrace direct-call update failure) and + * bounded (at most one image), so it is far preferable to a UAF. + */ + if (WARN_ON_ONCE(tr->cur_image)) + goto out; + direct_ops_free(tr); kfree(tr); out: @@ -1595,7 +1625,18 @@ static void bpf_trampoline_multi_attach_init(struct = bpf_trampoline *tr) =20 static void bpf_trampoline_multi_attach_free(struct bpf_trampoline *tr) { - if (tr->multi_attach.old_image) + /* + * Only free old_image if it is no longer the active image. + * When bpf_trampoline_update() fails before modify_fentry_multi()/ + * unregister_fentry_multi() is called, cur_image is unchanged + * (cur_image =3D=3D old_image) and ftrace still points to it. Freeing + * it would cause a UAF when ftrace calls into the freed memory. + * On success, cur_image is either a new image or NULL, so + * old_image !=3D cur_image correctly identifies a stale image that + * is safe to free. + */ + if (tr->multi_attach.old_image && + tr->multi_attach.old_image !=3D tr->cur_image) bpf_tramp_image_put(tr->multi_attach.old_image); =20 tr->multi_attach.old_image =3D NULL; @@ -1719,11 +1760,11 @@ int bpf_trampoline_multi_attach(struct bpf_prog *pr= og, u32 *ids, return err; } =20 -int bpf_trampoline_multi_detach(struct bpf_prog *prog, struct bpf_tracing_= multi_link *link) +void bpf_trampoline_multi_detach(struct bpf_prog *prog, struct bpf_tracing= _multi_link *link) { struct bpf_tracing_multi_data *data =3D &link->data; struct bpf_tracing_multi_node *mnode; - int i, err; + int i, err, err_unreg =3D 0, err_mod =3D 0; =20 trampoline_lock_all(); =20 @@ -1735,13 +1776,65 @@ int bpf_trampoline_multi_detach(struct bpf_prog *pr= og, struct bpf_tracing_multi_ WARN_ONCE(err, "__bpf_trampoline_unlink_prog failed: %d\n", err); } =20 - if (ftrace_hash_count(data->unreg)) - WARN_ON_ONCE(update_ftrace_direct_del(&direct_ops, data->unreg)); - if (ftrace_hash_count(data->modify)) - WARN_ON_ONCE(update_ftrace_direct_mod(&direct_ops, data->modify, true)); + if (ftrace_hash_count(data->unreg)) { + err_unreg =3D update_ftrace_direct_del(&direct_ops, data->unreg); + WARN_ON_ONCE(err_unreg); + } + if (ftrace_hash_count(data->modify)) { + err_mod =3D update_ftrace_direct_mod(&direct_ops, data->modify, true); + WARN_ON_ONCE(err_mod); + } =20 - for_each_mnode(mnode, link) - bpf_trampoline_multi_attach_free(mnode->trampoline); + for_each_mnode(mnode, link) { + struct bpf_trampoline *tr =3D mnode->trampoline; + + /* If the batch ftrace update failed for this mnode's path, + * ftrace still points to old_image. Use rollback to restore + * cur_image to old_image (putting the new cur_image if any) + * so the trampoline keeps the image ftrace is calling. + * + * A link only reaches detach after a successful attach, so + * tr->cur_image (captured above as old_image) is always + * non-NULL here; the NULL check only mirrors the one in + * bpf_trampoline_multi_attach_free()/_rollback()'s shared + * pattern and guards against tr->multi_attach being reused + * without a prior _init() call. + * + * This relies on update_ftrace_direct_del/mod being atomic: + * on failure, NO IPs in the hash are modified in ftrace (all + * validation/allocation happens before any ftrace record is + * touched). If this assumption is broken in the future (i.e., + * partial success becomes possible), this rollback logic would + * need to be revisited. + * + * cur_image =3D=3D NULL indicates the unreg path (total =3D=3D 0); + * cur_image !=3D NULL indicates the modify path (total > 0). + * + * Rollback alone only prevents freeing the trampoline image + * while ftrace may still branch into it; it does not keep + * the underlying bpf_prog alive, and the caller tears down + * link->prog once this function returns. So pin @prog (whose + * call is baked into old_image's machine code) on old_image + * before restoring it as cur_image: the pin is released once + * old_image is eventually retired for real by a later, + * successful update on this trampoline (see + * bpf_trampoline_multi_attach_free() and + * bpf_tramp_image_free()), or safely leaked alongside the + * image if the trampoline is torn down first instead (see + * bpf_trampoline_put()). + */ + if (tr->multi_attach.old_image && + tr->multi_attach.old_image !=3D tr->cur_image && + ((err_unreg && !tr->cur_image) || + (err_mod && tr->cur_image))) { + WARN_ON_ONCE(tr->multi_attach.old_image->pinned_prog); + bpf_prog_inc(prog); + tr->multi_attach.old_image->pinned_prog =3D prog; + bpf_trampoline_multi_attach_rollback(tr); + } else { + bpf_trampoline_multi_attach_free(tr); + } + } =20 trampoline_unlock_all(); =20 @@ -1749,7 +1842,6 @@ int bpf_trampoline_multi_detach(struct bpf_prog *prog= , struct bpf_tracing_multi_ bpf_trampoline_put(mnode->trampoline); =20 clear_tracing_multi_data(data); - return 0; } =20 #undef for_each_mnode_cnt diff --git a/kernel/trace/bpf_trace.c b/kernel/trace/bpf_trace.c index 891897f8a1b3..29260951aa87 100644 --- a/kernel/trace/bpf_trace.c +++ b/kernel/trace/bpf_trace.c @@ -3687,7 +3687,7 @@ static void bpf_tracing_multi_link_release(struct bpf= _link *link) struct bpf_tracing_multi_link *tr_link =3D container_of(link, struct bpf_tracing_multi_link, link); =20 - WARN_ON_ONCE(bpf_trampoline_multi_detach(link->prog, tr_link)); + bpf_trampoline_multi_detach(link->prog, tr_link); } =20 static void bpf_tracing_multi_link_dealloc(struct bpf_link *link) --=20 2.53.0 From nobody Fri Oct 2 05:28:36 2026 Received: from out-184.mta0.migadu.com (out-184.mta0.migadu.com [91.218.175.184]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E46BA3C455B for ; Wed, 5 Aug 2026 04:05:13 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.184 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785902715; cv=none; b=IFG1KIg1J9XIabc6bdd1Di1mLktcbhk7T9j7440weHQPP3IKvw+mJJXgdvwJwgUFct5yTJi/a1AURnsqpj/PHoecwENdR1abcSXJISV9Q1T+qKgj8tRsaZBXdiPZJ171FCZ1CB8jS4Gfv3RTwfPBYkrRic7hl+TuM5PjctsiA90= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785902715; c=relaxed/simple; bh=hp3BxxjQXqMUl4xCXaHkuCNKHzH71oiL+uOouF/16Sg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=dZAWfYkc5itOYXwm7LQb8zjEH1OBxISqRejxewY3wijuDMHahM9JKm/N13mtzbrxvEV4HnDNSWr2o8OkD9iY/NUxMVBCwPsSxEyl/WSf1yY1suYilSXVxsXBtbxSX6cEmT7JRCLXIPinKe/vMnMNpjf8wLSqk9t33OsLtpzRs7U= 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=rreJZX/w; arc=none smtp.client-ip=91.218.175.184 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="rreJZX/w" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1785902711; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=cf+NSdykdJD4K4q6YC2i5+/xG5MrJMnjBSYsMFeGNQk=; b=rreJZX/wn9AqowKmJpMhI/vw1qNff5JAKHiIJqSBVDLqtwcOkBBlWRMG6bFl5rnrjfpK9V 6esH6Kf12l787ETLs8Hk9nZrfcEJwt723GbXrsr0hpdTJALRPXO+iM9XWUWYCkJIMZSWjx U4znrYxsh8ewVhLFf0ogk7fr6qRjbk0= From: "Hui Zhu" To: Alexei Starovoitov , Daniel Borkmann , John Fastabend , Andrii Nakryiko , Eduard Zingerman , Kumar Kartikeya Dwivedi , Martin KaFai Lau , Song Liu , Yonghong Song , Jiri Olsa , Emil Tsalapatis , Ihor Solodrai , KP Singh , Matt Bobrowski , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org Cc: Hui Zhu Subject: [PATCH bpf-next v2 2/3] bpf: Fix prog UAF in bpf_trampoline_multi_attach() register-path rollback Date: Wed, 5 Aug 2026 12:04:07 +0800 Message-ID: <6edcc3d19ab91511372a9c3f00d90fdc66a31d83.1785902527.git.zhuhui@kylinos.cn> In-Reply-To: References: 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Hui Zhu In bpf_trampoline_multi_attach(), if update_ftrace_direct_mod() fails, the rollback tries to undo update_ftrace_direct_add() for register-path mnodes via update_ftrace_direct_del(). If that undo also fails, ftrace still calls into rtr->cur_image, but the unconditional rollback frees it -- a UAF of both the image and the prog baked into it. Fix: for register-path mnodes (old_image =3D=3D NULL) whose undo failed while cur_image is set, pin the prog on cur_image instead of rolling back, reusing the pinned_prog mechanism from the detach path. Fixes: aef4dfa790b2 ("bpf: Add bpf_trampoline_multi_attach/detach functions= ") Signed-off-by: Hui Zhu --- kernel/bpf/trampoline.c | 51 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/kernel/bpf/trampoline.c b/kernel/bpf/trampoline.c index c08d1a09e638..7fac27374ece 100644 --- a/kernel/bpf/trampoline.c +++ b/kernel/bpf/trampoline.c @@ -1668,7 +1668,7 @@ int bpf_trampoline_multi_attach(struct bpf_prog *prog= , u32 *ids, struct btf *btf =3D prog->aux->attach_btf; struct bpf_tracing_multi_node *mnode; struct bpf_trampoline *tr; - int i, err, rollback_cnt; + int i, err, rollback_cnt, err_undo_reg =3D 0; u64 key; =20 for_each_mnode(mnode, link) { @@ -1728,8 +1728,10 @@ int bpf_trampoline_multi_attach(struct bpf_prog *pro= g, u32 *ids, if (ftrace_hash_count(data->modify)) { err =3D update_ftrace_direct_mod(&direct_ops, data->modify, true); if (err) { - if (ftrace_hash_count(data->reg)) - WARN_ON_ONCE(update_ftrace_direct_del(&direct_ops, data->reg)); + if (ftrace_hash_count(data->reg)) { + err_undo_reg =3D update_ftrace_direct_del(&direct_ops, data->reg); + WARN_ON_ONCE(err_undo_reg); + } goto rollback_unlink; } } @@ -1744,8 +1746,47 @@ int bpf_trampoline_multi_attach(struct bpf_prog *pro= g, u32 *ids, =20 rollback_unlink: for_each_mnode_cnt(mnode, link, rollback_cnt) { - bpf_trampoline_remove_prog(mnode->trampoline, &mnode->node); - bpf_trampoline_multi_attach_rollback(mnode->trampoline); + struct bpf_trampoline *rtr =3D mnode->trampoline; + /* + * register_fentry_multi()/modify_fentry_multi() set + * rtr->cur_image before any ftrace call is made, and + * bpf_trampoline_multi_attach_init() captured whatever was + * live before that into rtr->multi_attach.old_image. A NULL + * old_image means this ip had no prior direct caller, i.e. + * this mnode went through the "register" (data->reg) path + * rather than "modify" (data->modify). + */ + bool via_register =3D !rtr->multi_attach.old_image; + + bpf_trampoline_remove_prog(rtr, &mnode->node); + + /* + * If this mnode used the register path and the + * update_ftrace_direct_del() above meant to undo its + * earlier, successful update_ftrace_direct_add() failed, + * ftrace is still actually calling into rtr->cur_image + * (which has @prog's call baked into its machine code) even + * though this attach is being reported as failed. Freeing + * rtr->cur_image via the normal rollback (which would also + * let the caller free @prog once this function returns its + * error) would be a use-after-free, so instead pin @prog on + * it and leave rtr->cur_image untouched: rtr->multi_attach + * is a scratch area only meaningful between _init() and + * _free()/_rollback(), so skipping _rollback() here does + * not leave it in an inconsistent state (old_image is NULL + * on the register path anyway). This image (and the pinned + * prog reference) is subsequently either properly retired by + * a later, successful update on the same trampoline, or + * safely leaked when the trampoline is torn down - see + * bpf_trampoline_multi_attach_free() and bpf_trampoline_put(). + */ + if (via_register && err_undo_reg && rtr->cur_image) { + WARN_ON_ONCE(rtr->cur_image->pinned_prog); + bpf_prog_inc(prog); + rtr->cur_image->pinned_prog =3D prog; + } else { + bpf_trampoline_multi_attach_rollback(rtr); + } } =20 trampoline_unlock_all(); --=20 2.53.0 From nobody Fri Oct 2 05:28:36 2026 Received: from out-178.mta0.migadu.com (out-178.mta0.migadu.com [91.218.175.178]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 98D893C870E for ; Wed, 5 Aug 2026 04:05:17 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.178 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785902719; cv=none; b=r9LnkrBvFDnRiphUVmCZ6hXqHsMCZYrgomAP5UGBKbQsvlWl8q52KvkY8gK1eauuTOFRq+U4fV4T/iVBOGByOloCsL+stzV1txCFFlLT5sbJHU5xVsXMPV2TSCNnULFmBnpxuBPBU7P4MUq4y+weA96CIkf19Xb63+++/IUfHgU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785902719; c=relaxed/simple; bh=VJshfN0f2GhM35uVuDa+UMCpOWB/U95MkVkRlcMCKdM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=YKg9/it45KQRcOcP/y6DEeSf5k4MbCaT+2b8piNnRIZGsIfvGo2sN9b7yRZWhEMsOhIL/HNDv1LCHTT6ed4J00DHfl3EUROIFQFclFcEwHEmeGcosaL9OkZw1WvgD3l5HfVE33m5ha0tjogKGpPlo5uK+Rd2Csg7M75QMYzUEjM= 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=BcP0BIMd; arc=none smtp.client-ip=91.218.175.178 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="BcP0BIMd" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1785902715; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=p1MDUL8pgO9d7aOgyIh4zK7UhpDcJDYhUJ+F2MSyPzw=; b=BcP0BIMd0TKBmOW3AXk0SGsudPQxP8LX73ngCbLJK6HVRgVaUGiravhIGlrD06yU0uhlBV hZbGRrGFw36/fRj3Uns+OK9RniyDnZ5pq9eejUc+ls9sKkxju5E4LgAIT+Zfe1yLqtzMzF q+6RUIUZxkoJLG2QW1yFUk5Bh9/JOLQ= From: "Hui Zhu" To: Alexei Starovoitov , Daniel Borkmann , John Fastabend , Andrii Nakryiko , Eduard Zingerman , Kumar Kartikeya Dwivedi , Martin KaFai Lau , Song Liu , Yonghong Song , Jiri Olsa , Emil Tsalapatis , Ihor Solodrai , KP Singh , Matt Bobrowski , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers , bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org Cc: Hui Zhu Subject: [PATCH bpf-next v2 3/3] bpf: Fix prog UAF in __bpf_trampoline_unlink_prog() on update failure Date: Wed, 5 Aug 2026 12:04:08 +0800 Message-ID: <0e91fde8222bcfc0b318cfa2dcc3e06cc476c1a2.1785902527.git.zhuhui@kylinos.cn> In-Reply-To: References: 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Hui Zhu If bpf_trampoline_update() fails in __bpf_trampoline_unlink_prog(), cur_image is unchanged and still has node->link->prog's call baked into it. All callers only WARN_ON_ONCE() the failure and then unconditionally free the prog -- a UAF. Fix: on failure, pin node->link->prog onto tr->cur_image via the pinned_prog mechanism, so it outlives the link. This covers both the multi (bpf_trampoline_multi_detach) and non-multi (bpf_tracing_link_release, bpf_shim_tramp_link_release) paths. Fixes: aef4dfa790b2 ("bpf: Add bpf_trampoline_multi_attach/detach functions= ") Signed-off-by: Hui Zhu --- kernel/bpf/trampoline.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/kernel/bpf/trampoline.c b/kernel/bpf/trampoline.c index 7fac27374ece..6071261fd66b 100644 --- a/kernel/bpf/trampoline.c +++ b/kernel/bpf/trampoline.c @@ -991,7 +991,25 @@ static int __bpf_trampoline_unlink_prog(struct bpf_tra= mp_node *node, return err; } bpf_trampoline_remove_prog(tr, node); - return bpf_trampoline_update(tr, true /* lock_direct_mutex */, ops, data); + err =3D bpf_trampoline_update(tr, true /* lock_direct_mutex */, ops, data= ); + /* + * If the update above failed, tr->cur_image is unchanged, i.e. ftrace + * (or the direct jump, for the standard ops) is still actually + * calling into an image with node->link->prog's call baked into its + * machine code, even though this unlink is being reported as failed. + * Every caller of this function only WARN_ON_ONCE()'s a failure here + * and then unconditionally frees the underlying bpf_prog, which + * would be a use-after-free the next time that image is executed. + * Pin an extra reference on the prog onto tr->cur_image so it + * outlives this link, mirroring the fix applied to + * bpf_trampoline_multi_attach()/bpf_trampoline_multi_detach(). + */ + if (err && tr->cur_image) { + WARN_ON_ONCE(tr->cur_image->pinned_prog); + bpf_prog_inc(node->link->prog); + tr->cur_image->pinned_prog =3D node->link->prog; + } + return err; } =20 /* bpf_trampoline_unlink_prog() should never fail. */ --=20 2.53.0