> bpf: Add bpf_trampoline_multi_prog_attach/detach()

TL;DR: bpf_trampoline_multi_prog_detach() may, if one unlink fails (e.g.
-ENOMEM), still commit and free old images after a failed batch poke, so other
targets could run freed trampoline memory.

> diff --git a/kernel/bpf/trampoline.c b/kernel/bpf/trampoline.c
> --- a/kernel/bpf/trampoline.c
> +++ b/kernel/bpf/trampoline.c

[ ... ]

> @@ -1918,6 +1923,217 @@ static int bpf_text_poke_batch(struct bpf_text_poke 
> **pokes, u32 cnt)
> +static void bpf_trampoline_multi_prog_commit(struct bpf_trampoline *tr)
> +{
> +     struct bpf_tramp_image *old_image = tr->multi_attach.old_image;
> +
> +     tr->cur_image = tr->multi_attach.new_image;
> +     tr->multi_attach.old_image = NULL;
> +     tr->multi_attach.new_image = NULL;
> +     tr->multi_attach.old_flags = 0;
> +
> +     if (old_image)
> +             bpf_tramp_image_put(old_image);
> +}
> +
> +static void bpf_trampoline_multi_prog_rollback(struct bpf_trampoline *tr)
> +{
> +     if (tr->multi_attach.new_image)
> +             bpf_tramp_image_put(tr->multi_attach.new_image);
> +
> +     tr->flags = tr->multi_attach.old_flags;
> +     tr->multi_attach.old_image = NULL;
> +     tr->multi_attach.new_image = NULL;
> +     tr->multi_attach.old_flags = 0;
> +}

[ ... ]

> +void bpf_trampoline_multi_prog_detach(struct bpf_tracing_multi_link *link)
> +{
> +     struct bpf_tracing_multi_node *mnode;
> +     int i, err;
> +
> +     trampoline_lock_all();
> +
> +     for_each_mnode(mnode, link) {
> +             bpf_trampoline_multi_attach_init(mnode->trampoline);
> +             err = __bpf_trampoline_unlink_prog(&mnode->node, 
> mnode->trampoline, NULL,
> +                                                &trampoline_multi_prog_ops, 
> NULL);
> +             if (WARN_ONCE(err, "__bpf_trampoline_unlink_prog failed: %d\n", 
> err))
> +                     bpf_trampoline_multi_prog_rollback(mnode->trampoline);
> +     }

If __bpf_trampoline_unlink_prog() fails for one trampoline here, can this
leave other targets calling freed trampoline images?

The failure is reachable.  bpf_trampoline_update() returns an error when
bpf_trampoline_get_progs() or bpf_tramp_image_alloc() fails (-ENOMEM), or
when bpf_jit_charge_modmem() returns -EPERM because bpf_jit_limit is
exceeded and the task closing the link fd is not bpf_capable().  The comment
in bpf_trampoline_multi_attach_free() already treats this failure as
reachable on the ftrace detach path.

Say the unlink fails for trampoline B:

1. bpf_trampoline_multi_attach_init() has set old_image = cur_image, which
   is still live in the text, and new_image = NULL.

2. bpf_trampoline_multi_prog_rollback() then clears old_image to NULL as
   well.

3. bpf_text_poke_init() therefore builds a poke for B with old_addr and
   new_addr both NULL (BPF_MOD_NOP to BPF_MOD_NOP), while B's entry still
   holds a call or jump to its live image.

4. On x86, bpf_arch_text_poke_batch() checks every poke before patching
   anything:

       if (memcmp(poke->ip, old_insn, X86_PATCH_SIZE))
           return -EBUSY;

   B's check fails, so no text is patched for any target in the link.  The
   generic fallback rolls back the pokes it already applied, with the same
   result.

> +
> +     for_each_mnode(mnode, link) {
> +             link->pokes[i] = &mnode->poke;
> +             bpf_text_poke_init(link->pokes[i], mnode->trampoline);
> +     }
> +
> +     WARN_ON_ONCE(bpf_text_poke_batch(link->pokes, link->nodes_cnt));
> +
> +     for_each_mnode(mnode, link)
> +             bpf_trampoline_multi_prog_commit(mnode->trampoline);

The batch error only triggers WARN_ON_ONCE(), and
bpf_trampoline_multi_prog_commit() still runs for every node.

For every other target A in the link, old_A is passed to
bpf_tramp_image_put() and freed after a tasks-rcu grace period.  A's entry
still calls old_A, because it was never repointed, so the next run of target
A executes freed trampoline memory.  Meanwhile new_A becomes cur_image
without ever having been installed.

For B itself, commit sets cur_image to NULL while the text still calls the
old image.  That image is never put, so it leaks along with its trampoline
reference.  If another prog (for example a plain fentry) is still attached
to B and is later detached, bpf_trampoline_update() reaches total == 0 and
calls unregister_fentry(), which dereferences tr->cur_image->image while
cur_image is NULL.  Any new attach to B instead goes through
register_fentry(), expects a nop at the entry, and fails with -EBUSY.

The ftrace-based bpf_trampoline_multi_detach() avoids this.  On unlink
failure it leaves cur_image unchanged, and bpf_trampoline_multi_attach_free()
skips freeing old_image when old_image == cur_image.  Could the prog detach
path get the same treatment?

For example, on unlink failure the trampoline could be kept on its current
image (new_image = old_image, so the poke is an identity poke and commit
neither puts nor drops cur_image), or the node could be left out of the
batch.  It also seems that images should not be committed when
bpf_text_poke_batch() fails.

This path is not called by this commit.  It is wired up later in the series
by b56b0344ff12 ("bpf: Add tracing_multi link support for bpf progs"), where
bpf_tracing_multi_link_release() calls bpf_trampoline_multi_prog_detach().
The function is unchanged at the end of the series, so no later commit
fixes this.


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/38273555988

Reply via email to