On Sat, Apr 18, 2026 at 06:10:57AM +0000, [email protected] wrote:

SNIP

> > +           err = __bpf_trampoline_link_prog(&mnode->node, 
> > mnode->trampoline, NULL,
> > +                                            &trampoline_multi_ops, data);
> > +           if (err) {
> > +                   rollback_cnt = i;
> > +                   goto rollback_unlink;
> > +           }
> > +   }
> 
> When user-provided ids[] contains duplicate BTF IDs (or distinct IDs
> that resolve to the same trampoline key), multiple nodes point to the
> same struct bpf_trampoline. The link loop above then calls
> bpf_trampoline_multi_attach_init() more than once on that trampoline,
> overwriting the saved old_image with the newly assigned cur_image from
> the previous iteration.
> 
> Scenario with ids[0] == ids[1] and trampoline X starting with OLD_X:
> 
>   i=0: attach_init(X) saves old_image=OLD_X
>        __bpf_trampoline_link_prog() -> modify_fentry_multi() sets
>        X->cur_image=NEW_X (OLD_X refcount not dropped, intent is for
>        multi_attach_free() to release it later).
> 
>   i=1: attach_init(X) re-runs on the same trampoline and overwrites
>        old_image=NEW_X (the only saved reference to OLD_X is lost).
>        __bpf_trampoline_link_prog() returns -EBUSY (duplicate prog).
>        rollback_cnt=i=1; goto rollback_unlink.
> 
>   rollback_unlink calls bpf_trampoline_multi_attach_rollback(X) once:
> 
>     static void bpf_trampoline_multi_attach_rollback(struct bpf_trampoline 
> *tr)
>     {
>         if (tr->cur_image)
>             bpf_tramp_image_put(tr->cur_image);    /* puts NEW_X */
>         tr->cur_image = tr->multi_attach.old_image;  /* = NEW_X (stale!) */
>         ...
>     }
> 
> Result: OLD_X leaks (its refcount is never decremented). cur_image
> points at NEW_X which was just released via bpf_tramp_image_put().
> 
> The rollback_put loop calls bpf_trampoline_put() which frees the
> trampoline (prog was removed, progs_hlist is empty), so the dangling
> NEW_X pointer is cleaned up. However OLD_X remains allocated via
> module_alloc forever.
> 
> Reachability: commit dbf2afe2f603 ("bpf: Add support for tracing multi
> link") copies ids[] from userspace without deduplication. The series'
> selftests (commit 411fb40d4b2a "selftests/bpf: Add tracing multi attach
> fails test", 'fail#7 (kernel) attach with duplicate id') exercise this
> path explicitly, expecting -EBUSY.
> 
> The trigger requires the shared trampoline to already have cur_image !=
> NULL when the first iteration runs (i.e., another program attached to
> the same function), so modify_fentry_multi() is used and there is a
> real OLD_X to leak. The first-time-attach case (register_fentry_multi,
> OLD_X=NULL) only leaves cur_image dangling briefly until the trampoline
> is freed.
> 
> Can the code save old_image/old_flags once per trampoline instead of
> once per node, or store them per-mnode to avoid overwriting the saved
> state?

I think it's better to ensure the btf ids array does not contain
duplicate entries.. wil check

> 
> > +
> > +   rollback_cnt = link->nodes_cnt;
> > +   if (ftrace_hash_count(data->reg)) {
> > +           err = update_ftrace_direct_add(&direct_ops, data->reg);
> > +           if (err)
> > +                   goto rollback_unlink;
> > +   }
> > +
> > +   if (ftrace_hash_count(data->modify)) {
> > +           err = update_ftrace_direct_mod(&direct_ops, data->modify, true);
> > +           if (err) {
> > +                   WARN_ON_ONCE(update_ftrace_direct_del(&direct_ops, 
> > data->reg));
> > +                                                                          
> > ^^^^^^^^^^
> > +                   goto rollback_unlink;
> > +           }
> > +   }
> 
> The recovery path unconditionally calls update_ftrace_direct_del with
> data->reg, but update_ftrace_direct_add is only invoked when data->reg
> is non-empty:
> 
>     if (ftrace_hash_count(data->reg)) {
>         err = update_ftrace_direct_add(&direct_ops, data->reg);
>         ...
>     }
> 
> When update_ftrace_direct_mod fails and all trampolines already had
> cur_image != NULL (so nothing was added to data->reg),
> update_ftrace_direct_del is called with an empty hash.
> update_ftrace_direct_del at kernel/trace/ftrace.c:6511 checks:
> 
>     if (!hash_count(hash))
>         return -EINVAL;
> 
> so it returns -EINVAL and the WARN_ON_ONCE fires spuriously. Should
> this guard match the add path?
> 
>     if (ftrace_hash_count(data->reg))
>         WARN_ON_ONCE(update_ftrace_direct_del(&direct_ops, data->reg));

yes, replied in the other email

SNIP

> > +int bpf_check_attach_btf_id_multi(struct btf *btf, struct bpf_prog *prog, 
> > u32 btf_id,
> > +                             struct bpf_attach_target_info *tgt_info)
> > +{
> > +   const struct btf_type *t;
> > +   unsigned long addr;
> > +   const char *tname;
> > +   int err;
> > +
> > +   if (!btf_id || !btf)
> > +           return -EINVAL;
> > +
> > +   /* Check noreturn attachment. */
> > +   if (prog->expected_attach_type == BPF_TRACE_FEXIT_MULTI ||
> > +        btf_id_set_contains(&noreturn_deny, btf_id))
> > +           return -EINVAL;
> 
> Operator precedence issue in the noreturn check. The two subexpressions
> are joined by ||, so the function returns -EINVAL whenever EITHER:
> 
>   (a) the program's expected_attach_type is BPF_TRACE_FEXIT_MULTI
>       (regardless of whether btf_id targets a noreturn function), OR
> 
>   (b) the btf_id is in the noreturn_deny set (regardless of program
>       attach type, rejecting FENTRY_MULTI attachments to noreturn
>       functions as well).
> 
> The intent is to reject only FEXIT-like multi attachments to noreturn
> functions. The non-multi path in check_attach_btf_id() correctly uses
> &&:
> 
>     } else if ((prog->expected_attach_type == BPF_TRACE_FEXIT ||
>                prog->expected_attach_type == BPF_TRACE_FSESSION ||
>                prog->expected_attach_type == BPF_MODIFY_RETURN) &&
>                btf_id_set_contains(&noreturn_deny, btf_id)) {
>         ...
>         return -EINVAL;
>     }
> 
> Consequences once bpf_trampoline_multi_attach() is wired up from
> user-space (commit dbf2afe2f603 "bpf: Add support for tracing multi
> link"):
> 
>   1. Every BPF_TRACE_FEXIT_MULTI program attach request is rejected
>      with -EINVAL, even when the target function returns normally. This
>      disables BPF_TRACE_FEXIT_MULTI entirely.
> 
>   2. Any tracing_multi program (including BPF_TRACE_FENTRY_MULTI) is
>      rejected when its btf_id is in noreturn_deny (do_exit,
>      __x64_sys_exit, make_task_dead, etc.). Rejecting FENTRY_MULTI on
>      noreturn functions is an unintended behavioral restriction.
> 
> Forward search in the git range shows commit bfcad202ce99 ("bpf: Add
> support for tracing_multi link session") rewrites this hunk to:
> 
>     if ((prog->expected_attach_type == BPF_TRACE_FEXIT_MULTI ||
>          prog->expected_attach_type == BPF_TRACE_FSESSION_MULTI) &&
>          btf_id_set_contains(&noreturn_deny, btf_id))
>         return -EINVAL;
> 
> The parenthesization + && in the later commit confirms this is a
> precedence bug. Should this use && instead?

yes, replied in the other email

jirka

Reply via email to