Re: [PATCH 2/8] uprobes: revamp uprobe refcounting and lifetime management

Jiri Olsa
2024-08-02T11:11:55Z

On Wed, Jul 31, 2024 at 02:42:50PM -0700, Andrii Nakryiko wrote:

SNIP

> -/*
> - * There could be threads that have already hit the breakpoint. They
> - * will recheck the current insn and restart if find_uprobe() fails.
> - * See find_active_uprobe().
> - */
> -static void delete_uprobe(struct uprobe *uprobe)
> -{
> - if (WARN_ON(!uprobe_is_active(uprobe)))
> - return;
> -
> - write_lock(&uprobes_treelock);
> - rb_erase(&uprobe->rb_node, &uprobes_tree);
> - write_unlock(&uprobes_treelock);
> - RB_CLEAR_NODE(&uprobe->rb_node); /* for uprobe_is_active() */
> -}
> -
> struct map_info {
> struct map_info *next;
> struct mm_struct *mm;
> @@ -1094,17 +1120,12 @@ void uprobe_unregister(struct uprobe *uprobe, struct uprobe_consumer *uc)
> int err;
>
> down_write(&uprobe->register_rwsem);
> - if (WARN_ON(!consumer_del(uprobe, uc)))
> + if (WARN_ON(!consumer_del(uprobe, uc))) {
> err = -ENOENT;
> - else
> + } else {
> err = register_for_each_vma(uprobe, NULL);
> -
> - /* TODO : cant unregister? schedule a worker thread */
> - if (!err) {
> - if (!uprobe->consumers)
> - delete_uprobe(uprobe);

ok, so removing this call is why the consumer test is failing, right?

IIUC the previous behaviour was to remove uprobe from the tree
even when there's active uprobe ref for installed uretprobe

so following scenario will now behaves differently:

install uretprobe/consumer-1 on foo
foo {
remove uretprobe/consumer-1 (A)
install uretprobe/consumer-2 on foo (B)
}

before the removal of consumer-1 (A) would remove the uprobe object
from the tree, so the installation of consumer-2 (b) would create
new uprobe object which would not be triggered at foo return because
it got installed too late (after foo uprobe was triggered)

the behaviour with this patch is that removal of consumer-1 (A) will
not remove the uprobe object (that happens only when we run out of
refs), and the following install of consumer-2 will use the existing
uprobe object so the consumer-2 will be triggered on foo return

uff ;-)

but I think it's better, because we get more hits

jirka

> - else
> - err = -EBUSY;
> + /* TODO : cant unregister? schedule a worker thread */
> + WARN(err, "leaking uprobe due to failed unregistration");
> }
> up_write(&uprobe->register_rwsem);
>
> @@ -1159,27 +1180,16 @@ struct uprobe *uprobe_register(struct inode *inode,
> if (!IS_ALIGNED(ref_ctr_offset, sizeof(short)))
> return ERR_PTR(-EINVAL);
>
> - retry:
> uprobe = alloc_uprobe(inode, offset, ref_ctr_offset);
> if (IS_ERR(uprobe))
> return uprobe;
>
> - /*
> - * We can race with uprobe_unregister()->delete_uprobe().
> - * Check uprobe_is_active() and retry if it is false.
> - */
> down_write(&uprobe->register_rwsem);
> - ret = -EAGAIN;
> - if (likely(uprobe_is_active(uprobe))) {
> - consumer_add(uprobe, uc);
> - ret = register_for_each_vma(uprobe, uc);
> - }
> + consumer_add(uprobe, uc);
> + ret = register_for_each_vma(uprobe, uc);
> up_write(&uprobe->register_rwsem);
> - put_uprobe(uprobe);
>
> if (ret) {
> - if (unlikely(ret == -EAGAIN))
> - goto retry;
> uprobe_unregister(uprobe, uc);
> return ERR_PTR(ret);

SNIP