* [PATCH bpf 1/2] bpf: Use dedicated workqueue for freeing BPF programs
2026-11-02 7:34 [PATCH bpf 0/2] bpf: Avoid workqueue deadlock when freeing programs Emil Tsalapatis
@ 2026-11-02 7:34 ` Emil Tsalapatis
2026-11-02 17:58 ` Alexei Starovoitov
2026-11-02 7:34 ` [PATCH bpf 2/2] selftests/bpf: Add a selftest for program allocation/freeing Emil Tsalapatis
2026-11-02 17:55 ` [PATCH bpf 0/2] bpf: Avoid workqueue deadlock when freeing programs Andrii Nakryiko
2 siblings, 1 reply; 6+ messages in thread
From: Emil Tsalapatis @ 2026-11-02 7:34 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Move deferred BPF program frees to a dedicated workqueue.
BPF free workers can exhaust a CPU's system workqueue active slots while
waiting for pack_mutex. The worker holding pack_mutex then waits for a
vmalloc purge helper on that same queue, which cannot run until a slot
is freed.
Fixes: 6b88aaec81db ("x86/bpf: Enable EXECMEM_ROX_CACHE for BPF allocations")
Reported-by: Andrii Nakryiko <andrii@kernel.org>
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
kernel/bpf/core.c | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
index 7f11555a5070..a4994438a8af 100644
--- a/kernel/bpf/core.c
+++ b/kernel/bpf/core.c
@@ -23,6 +23,7 @@
#include <linux/skbuff.h>
#include <linux/static_call.h>
#include <linux/vmalloc.h>
+#include <linux/workqueue.h>
#include <linux/prandom.h>
#include <linux/bpf.h>
#include <linux/btf.h>
@@ -3073,6 +3074,17 @@ static void bpf_free_used_btfs(struct bpf_prog_aux *aux)
kfree(aux->used_btfs);
}
+static struct workqueue_struct *bpf_prog_free_wq __ro_after_init;
+
+static int __init bpf_prog_free_wq_init(void)
+{
+ bpf_prog_free_wq = alloc_workqueue("bpf_prog_free", WQ_UNBOUND, 0);
+ if (!bpf_prog_free_wq)
+ panic("Failed to allocate BPF program free workqueue\n");
+ return 0;
+}
+early_initcall(bpf_prog_free_wq_init);
+
static void bpf_prog_free_deferred(struct work_struct *work)
{
struct bpf_prog_aux *aux;
@@ -3122,7 +3134,12 @@ void bpf_prog_free(struct bpf_prog *fp)
bpf_prog_put(aux->dst_prog);
bpf_token_put(aux->token);
INIT_WORK(&aux->work, bpf_prog_free_deferred);
- schedule_work(&aux->work);
+ /*
+ * A pack_mutex holder may wait for vmalloc work on system_percpu_wq.
+ * Keep program frees off that queue so mutex waiters cannot exhaust
+ * the active slots needed by the vmalloc work.
+ */
+ queue_work(bpf_prog_free_wq, &aux->work);
}
EXPORT_SYMBOL_GPL(bpf_prog_free);
--
2.52.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH bpf 2/2] selftests/bpf: Add a selftest for program allocation/freeing
2026-11-02 7:34 [PATCH bpf 0/2] bpf: Avoid workqueue deadlock when freeing programs Emil Tsalapatis
2026-11-02 7:34 ` [PATCH bpf 1/2] bpf: Use dedicated workqueue for freeing BPF programs Emil Tsalapatis
@ 2026-11-02 7:34 ` Emil Tsalapatis
2026-11-02 7:47 ` sashiko-bot
2026-11-02 17:55 ` [PATCH bpf 0/2] bpf: Avoid workqueue deadlock when freeing programs Andrii Nakryiko
2 siblings, 1 reply; 6+ messages in thread
From: Emil Tsalapatis @ 2026-11-02 7:34 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Add a selftest that stresses BPF program allocation/free
paths.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
tools/testing/selftests/bpf/.gitignore | 1 +
tools/testing/selftests/bpf/Makefile | 1 +
.../selftests/bpf/bpf_prog_free_stress.c | 184 ++++++++++++++++++
3 files changed, 186 insertions(+)
create mode 100644 tools/testing/selftests/bpf/bpf_prog_free_stress.c
diff --git a/tools/testing/selftests/bpf/.gitignore b/tools/testing/selftests/bpf/.gitignore
index 986a6389186b..ba72f8ac1f72 100644
--- a/tools/testing/selftests/bpf/.gitignore
+++ b/tools/testing/selftests/bpf/.gitignore
@@ -2,6 +2,7 @@
bpftool
bpf-helpers*
bpf-syscall*
+/bpf_prog_free_stress
test_verifier
test_maps
test_lru_map
diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index 93c707116fad..e59e07c4bd12 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -137,6 +137,7 @@ TEST_KMOD_TARGETS = $(addprefix $(OUTPUT)/,$(TEST_KMODS))
# Compile but not part of 'make run_tests'
TEST_GEN_PROGS_EXTENDED = \
+ bpf_prog_free_stress \
bench \
flow_dissector_load \
test_cpp \
diff --git a/tools/testing/selftests/bpf/bpf_prog_free_stress.c b/tools/testing/selftests/bpf/bpf_prog_free_stress.c
new file mode 100644
index 000000000000..8c761845c703
--- /dev/null
+++ b/tools/testing/selftests/bpf/bpf_prog_free_stress.c
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <err.h>
+#include <linux/bpf.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/syscall.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "../../../include/linux/filter.h"
+
+/*
+ * Spread allocations over vmap nodes, then queue frees from CPU0, whose purge
+ * helper runs asynchronously. Tiny programs fill packs densely enough to build
+ * a backlog exceeding the system workqueue's active-work limit.
+ */
+#define LOAD_CPUS 64
+#define PROGRAMS 65536
+#define WAITERS 1024
+#define DEFAULT_DURATION_SEC 5
+
+static int fds[PROGRAMS];
+static const struct bpf_insn insns[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN(),
+};
+
+static void pin_cpu(int cpu)
+{
+ cpu_set_t mask;
+
+ CPU_ZERO(&mask);
+ CPU_SET(cpu, &mask);
+ if (sched_setaffinity(0, sizeof(mask), &mask))
+ err(EXIT_FAILURE, "sched_setaffinity CPU%d", cpu);
+}
+
+static int load_prog(void)
+{
+ union bpf_attr attr = {
+ .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
+ .insn_cnt = sizeof(insns) / sizeof(insns[0]),
+ .insns = (uintptr_t)insns,
+ .license = (uintptr_t)"GPL",
+ };
+ int fd;
+
+ fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
+ if (fd < 0)
+ err(EXIT_FAILURE, "BPF_PROG_LOAD");
+ return fd;
+}
+
+static void *load_worker(void *arg)
+{
+ int cpu = (uintptr_t)arg;
+ int first = cpu * (PROGRAMS / LOAD_CPUS);
+ int i;
+
+ pin_cpu(cpu);
+ for (i = first; i < first + PROGRAMS / LOAD_CPUS; i++)
+ fds[i] = load_prog();
+ return NULL;
+}
+
+static void *load_waiter(void *arg)
+{
+ (void)arg;
+
+ pin_cpu(0);
+ if (close(load_prog()))
+ err(EXIT_FAILURE, "close waiter program");
+ return NULL;
+}
+
+static void check_pthread(int error, const char *operation)
+{
+ if (error)
+ errx(EXIT_FAILURE, "%s: %s", operation, strerror(error));
+}
+
+static void run_threads(int count, void *(*worker)(void *))
+{
+ pthread_t threads[WAITERS];
+ pthread_attr_t attr;
+ int i;
+
+ check_pthread(pthread_attr_init(&attr), "pthread_attr_init");
+ check_pthread(pthread_attr_setstacksize(&attr, 64 * 1024),
+ "pthread_attr_setstacksize");
+ for (i = 0; i < count; i++)
+ check_pthread(pthread_create(&threads[i], &attr, worker,
+ (void *)(uintptr_t)i),
+ "pthread_create");
+ for (i = 0; i < count; i++)
+ check_pthread(pthread_join(threads[i], NULL), "pthread_join");
+ check_pthread(pthread_attr_destroy(&attr), "pthread_attr_destroy");
+}
+
+static long monotonic_sec(void)
+{
+ struct timespec now;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &now))
+ err(EXIT_FAILURE, "clock_gettime");
+ return now.tv_sec;
+}
+
+static long parse_duration(int argc, char **argv)
+{
+ char *end;
+ long duration;
+
+ if (argc == 1)
+ return DEFAULT_DURATION_SEC;
+ if (argc != 2)
+ errx(EXIT_FAILURE, "usage: %s [duration_sec]", argv[0]);
+
+ errno = 0;
+ duration = strtol(argv[1], &end, 10);
+ if (errno || *end || duration <= 0)
+ errx(EXIT_FAILURE, "usage: %s [duration_sec]", argv[0]);
+ return duration;
+}
+
+static void run_burst(int round)
+{
+ struct sched_param priority = { .sched_priority = 80 };
+ int i;
+
+ printf("Round %d: loading %d programs across %d CPUs\n", round,
+ PROGRAMS, LOAD_CPUS);
+ run_threads(LOAD_CPUS, load_worker);
+ printf("Round %d: loaded %d programs; closing from CPU0\n", round,
+ PROGRAMS);
+
+ if (sched_setscheduler(0, SCHED_FIFO, &priority))
+ err(EXIT_FAILURE, "sched_setscheduler SCHED_FIFO");
+ for (i = 0; i < PROGRAMS; i++)
+ if (close(fds[i]))
+ err(EXIT_FAILURE, "close program %d", i);
+ priority.sched_priority = 0;
+ if (sched_setscheduler(0, SCHED_OTHER, &priority))
+ err(EXIT_FAILURE, "sched_setscheduler SCHED_OTHER");
+
+ printf("Closed %d programs; starting %d allocation waiters\n",
+ PROGRAMS, WAITERS);
+ run_threads(WAITERS, load_waiter);
+}
+
+int main(int argc, char **argv)
+{
+ struct rlimit limit = { PROGRAMS + 1024, PROGRAMS + 1024 };
+ long duration = parse_duration(argc, argv);
+ long start;
+ int round = 0;
+
+ setvbuf(stdout, NULL, _IOLBF, 0);
+ if (sysconf(_SC_NPROCESSORS_ONLN) < LOAD_CPUS)
+ errx(EXIT_FAILURE, "need at least %d online CPUs", LOAD_CPUS);
+
+ if (setrlimit(RLIMIT_NOFILE, &limit))
+ err(EXIT_FAILURE, "setrlimit RLIMIT_NOFILE");
+ limit.rlim_cur = limit.rlim_max = RLIM_INFINITY;
+ if (setrlimit(RLIMIT_MEMLOCK, &limit))
+ err(EXIT_FAILURE, "setrlimit RLIMIT_MEMLOCK");
+ pin_cpu(0);
+
+ printf("Running for %ld seconds\n", duration);
+ start = monotonic_sec();
+ do {
+ run_burst(++round);
+ } while (monotonic_sec() - start < duration);
+
+ printf("Completed %d rounds\n", round);
+ return EXIT_SUCCESS;
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [PATCH bpf 0/2] bpf: Avoid workqueue deadlock when freeing programs
2026-11-02 7:34 [PATCH bpf 0/2] bpf: Avoid workqueue deadlock when freeing programs Emil Tsalapatis
2026-11-02 7:34 ` [PATCH bpf 1/2] bpf: Use dedicated workqueue for freeing BPF programs Emil Tsalapatis
2026-11-02 7:34 ` [PATCH bpf 2/2] selftests/bpf: Add a selftest for program allocation/freeing Emil Tsalapatis
@ 2026-11-02 17:55 ` Andrii Nakryiko
2 siblings, 0 replies; 6+ messages in thread
From: Andrii Nakryiko @ 2026-11-02 17:55 UTC (permalink / raw)
To: Emil Tsalapatis; +Cc: bpf, ast, andrii, eddyz87, memxor, daniel
On Sun, Nov 1, 2026 at 11:34 PM Emil Tsalapatis <emil@etsalapatis.com> wrote:
>
> BPF program freeing currently runs from the system workqueue. With BPF
> program pack allocations, freeing the last program in a pack can take
> pack_mutex and call into execmem/vmalloc code to change page permissions.
> That path may queue a vmalloc purge helper to the same CPU and wait for it
> to finish.
>
> If enough other BPF program free workers are already active on that CPU,
> they can all block on pack_mutex and consume the system workqueue active
> slots. The worker holding pack_mutex then waits for the vmalloc purge work,
> but that purge work cannot run because the queue is full of pack_mutex
> waiters:
>
> [ 24.646667] INFO: task kworker/0:4:1291 blocked for more than 10 seconds.
> [ 24.646669] Not tainted 7.3.0-00001-gdeaa120a9b25-dirty #11
> [ 24.646670] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
> [ 24.646671] task:kworker/0:4 state:D stack:0 pid:1291 tgid:1291 ppid:2 task_flags:0x4208060 flags:0x00080000
> [ 24.646675] Workqueue: events bpf_prog_free_deferred
> [ 24.646678] Call Trace:
> [ 24.646680] <TASK>
> [ 24.646686] __schedule+0x5bc/0x1430
> [ 24.646702] schedule+0x3f/0x100
> [ 24.646706] schedule_timeout+0x106/0x110
> [ 24.646711] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 24.646713] ? mark_held_locks+0x49/0x80
> [ 24.646720] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 24.646723] ? lockdep_hardirqs_on_prepare+0xdc/0x190
> [ 24.646726] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 24.646732] __wait_for_common+0xc1/0x1d0
> [ 24.646735] ? __pfx_schedule_timeout+0x10/0x10
> [ 24.646748] ? __flush_work+0x1f8/0x550
> [ 24.646751] __flush_work+0x309/0x550
> [ 24.646759] ? __pfx_wq_barrier_func+0x10/0x10
> [ 24.646783] __purge_vmap_area_lazy+0x3e4/0x440
> [ 24.646796] _vm_unmap_aliases+0x2d5/0x330
> [ 24.646810] change_page_attr_set_clr+0xb9/0x580
> [ 24.646817] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 24.646833] set_memory_nx+0x3b/0x50
> [ 24.646838] __execmem_cache_free+0x35/0xb0
> [ 24.646848] execmem_free+0x98/0x160
> [ 24.646864] bpf_prog_pack_free+0x152/0x190
> [ 24.646871] bpf_jit_binary_pack_free+0x17/0x90
> [ 24.646876] bpf_jit_free+0x5e/0x130
> [ 24.646882] process_one_work+0x289/0x790
> [ 24.646886] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 24.646888] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 24.646902] worker_thread+0x1b3/0x370
> [ 24.646908] ? __pfx_worker_thread+0x10/0x10
> [ 24.646911] kthread+0xf7/0x130
> [ 24.646914] ? __pfx_kthread+0x10/0x10
> [ 24.646920] ret_from_fork+0x293/0x300
> [ 24.646923] ? __pfx_kthread+0x10/0x10
> [ 24.646927] ret_from_fork_asm+0x1a/0x30
> [ 24.646947] </TASK>
>
> Move deferred BPF program freeing to a dedicated workqueue to avoid
> this.
>
Hey Emil,
My AI says that:
a) we might have similar problems due to trampoline freeing, so maybe
we should address both in the same fix?
b) there apparently was fix by Josef touching this code, so now your
fix doesn't apply cleanly, please double check
and me saying
c) do we really need to have an extra binary (bpf_prog_free_stress)
that we won't be really testing regularly? it's good as reproducer and
to confirm the fix, but I wouldn't check it into selftests
pw-bot: cr
> Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
>
> Emil Tsalapatis (2):
> bpf: Use dedicated workqueue for freeing BPF programs
> selftests/bpf: Add a selftest for program allocation/freeing
>
> kernel/bpf/core.c | 19 +-
> tools/testing/selftests/bpf/.gitignore | 1 +
> tools/testing/selftests/bpf/Makefile | 1 +
> .../selftests/bpf/bpf_prog_free_stress.c | 184 ++++++++++++++++++
> 4 files changed, 204 insertions(+), 1 deletion(-)
> create mode 100644 tools/testing/selftests/bpf/bpf_prog_free_stress.c
>
> --
> 2.52.0
>
>
^ permalink raw reply [flat|nested] 6+ messages in thread