[PATCH bpf-next 8/9] uprobes: Don't leak the breakpoints if the unregistering task is killed
From: Alexei Starovoitov
Date: Wed Oct 21 2026 - 22:01:01 EST
From: Alexei Starovoitov <ast@xxxxxxxxxx>
__get_user_pages() returns -EINTR when current has a fatal signal pending,
and uprobe_write() begins with get_user_pages_remote(). When the task is
killed in uprobe_unregister_nosync() remove_breakpoint() fails in every mm
that register_for_each_vma() hasn't visited yet:
uprobe: <comm>:<pid> failed to unregister, leaking uprobe
uprobe stays in uprobes_tree. The breakpoints stay in these processes and
in their children, and every hit traps into the kernel for nothing.
The same happens when the task is killed in uprobe_register(). It cannot
remove the breakpoints that it installed. Since "uprobes: Don't sleep on
mmap_lock under dup_mmap_sem" busy mms come last, so the task that is
killed while it waits for one of them leaves the breakpoint in all other
mms.
Trying again after the failure isn't enough. The signal can interrupt
the removal after ref_ctr was decremented, and the second removal
decrements it again.
kworker has no signals. Remove the breakpoints from a work item and wait
for it. A kernel thread does the walk directly, and so does everybody when
nobody maps the file.
The price:
- A round trip through the workqueue for every removal, 9 usec.
- The removal runs with the priority and cpu mask of unbound kworker.
- The caller sleeps in flush_work() for the whole walk. When the walk is
longer than hung_task_timeout_secs the caller is reported as hung.
The tracer is killed while uprobe_multi attach waits for mmap_lock of
a busy process. 100 idle processes map the file. The number of them that
keep the breakpoint, 10 rounds:
without this series 0 or 1
without this patch 100
with this patch 0
uprobe_multi attach and detach in a loop, nobody is killed, 32 cpu VM,
median of 21-27 runs, msec:
detach attach
before after before after
4096 uprobes, 1 process 33.5 69.4 7.35 7.44
1024 uprobes, 100 processes 95.0 102 38.1 43.4
256 uprobes, 1000 processes 182 192 107 117
Signed-off-by: Alexei Starovoitov <ast@xxxxxxxxxx>
---
kernel/events/uprobes.c | 74 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 71 insertions(+), 3 deletions(-)
diff --git a/kernel/events/uprobes.c b/kernel/events/uprobes.c
index 92ccb730278f..361b52c9c3bc 100644
--- a/kernel/events/uprobes.c
+++ b/kernel/events/uprobes.c
@@ -1426,6 +1426,68 @@ register_for_each_vma(struct uprobe *uprobe, struct uprobe_consumer *new)
return walk.err;
}
+struct unregister_work {
+ struct work_struct work;
+ struct uprobe *uprobe;
+ int err;
+};
+
+static void unregister_work_fn(struct work_struct *work)
+{
+ struct unregister_work *uw = container_of(work, struct unregister_work, work);
+
+ uw->err = register_for_each_vma(uw->uprobe, NULL);
+}
+
+/*
+ * Remove the breakpoints. If current is killed, get_user_pages_remote() fails
+ * with -EINTR from then on: the breakpoints which are left stay for good, and
+ * the removal which was interrupted can be left half-way, with the reference
+ * counter decremented but the breakpoint still there. So let a kworker do the
+ * job, it has no signals. It takes the locks we would take ourselves.
+ * (A fork() of an mm with busy mmap_lock can still do one removal in a task
+ * which is being killed, see register_one_mm().)
+ *
+ * A kernel thread can do it itself. So can anybody if nobody maps the file:
+ * then there is no breakpoint, and uprobe_mmap() installs none which we would
+ * have to remove.
+ */
+static int unregister_all_vmas(struct uprobe *uprobe)
+{
+ struct address_space *mapping = uprobe->inode->i_mapping;
+ struct unregister_work uw = { .uprobe = uprobe };
+ bool mapped;
+ int err;
+
+ /*
+ * vma_prepare() takes a vma out of the tree and vma_complete() puts it
+ * back, under i_mmap_lock_write(). Don't mistake that for an empty tree.
+ */
+ i_mmap_lock_read(mapping);
+ mapped = mapping_mapped(mapping);
+ i_mmap_unlock_read(mapping);
+
+ if (!mapped || (current->flags & PF_KTHREAD)) {
+ err = register_for_each_vma(uprobe, NULL);
+ /*
+ * An error with a fatal signal pending: the file was mapped
+ * meanwhile, and get_user_pages_remote() returned -EINTR before
+ * it could see that there is nothing to remove. Nothing was
+ * written, so it is safe to let the kworker repeat the walk.
+ * (Kernel threads which unregister uprobes don't allow SIGKILL.)
+ */
+ if (likely(!err) || !fatal_signal_pending(current))
+ return err;
+ }
+
+ INIT_WORK_ONSTACK(&uw.work, unregister_work_fn);
+ queue_work(system_dfl_long_wq, &uw.work);
+ flush_work(&uw.work);
+ destroy_work_on_stack(&uw.work);
+
+ return uw.err;
+}
+
/**
* uprobe_unregister_nosync - unregister an already registered probe.
* @uprobe: uprobe to remove
@@ -1437,10 +1499,13 @@ void uprobe_unregister_nosync(struct uprobe *uprobe, struct uprobe_consumer *uc)
down_write(&uprobe->register_rwsem);
consumer_del(uprobe, uc);
- err = register_for_each_vma(uprobe, NULL);
+ err = unregister_all_vmas(uprobe);
up_write(&uprobe->register_rwsem);
- /* TODO : cant unregister? schedule a worker thread */
+ /*
+ * Can still fail, e.g. with -ENOMEM. Then the uprobe and the breakpoints
+ * which are left are leaked.
+ */
if (unlikely(err)) {
uprobe_warn(current, "unregister, leaking uprobe");
return;
@@ -1553,7 +1618,10 @@ int uprobe_apply(struct uprobe *uprobe, struct uprobe_consumer *uc, bool add)
rcu_read_lock_trace();
list_for_each_entry_rcu(con, &uprobe->consumers, cons_node, rcu_read_lock_trace_held()) {
if (con == uc) {
- ret = register_for_each_vma(uprobe, add ? uc : NULL);
+ if (add)
+ ret = register_for_each_vma(uprobe, uc);
+ else
+ ret = unregister_all_vmas(uprobe);
break;
}
}
--
2.55.0