From: Hui Zhu <[email protected]>

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 == 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 <[email protected]>
---
 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 = 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 = 0;
        u64 key;
 
        for_each_mnode(mnode, link) {
@@ -1728,8 +1728,10 @@ int bpf_trampoline_multi_attach(struct bpf_prog *prog, 
u32 *ids,
        if (ftrace_hash_count(data->modify)) {
                err = 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 = 
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 *prog, 
u32 *ids,
 
 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 = 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 = !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 = prog;
+               } else {
+                       bpf_trampoline_multi_attach_rollback(rtr);
+               }
        }
 
        trampoline_unlock_all();
-- 
2.53.0


Reply via email to