| // SPDX-License-Identifier: GPL-2.0 |
| /* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ |
| #define _GNU_SOURCE |
| #include <sched.h> |
| #include <pthread.h> |
| #include <test_progs.h> |
| #include "timer_start_delete_race.skel.h" |
| |
| /* |
| * Test for race between bpf_timer_start() and map element deletion. |
| * |
| * The race scenario: |
| * - CPU 1: bpf_timer_start() proceeds to bpf_async_process() and is about |
| * to call hrtimer_start() but hasn't yet |
| * - CPU 2: map_delete_elem() calls __bpf_async_cancel_and_free(), since |
| * timer is not scheduled yet hrtimer_try_to_cancel() is a nop, |
| * then calls bpf_async_refcount_put() dropping refcnt to zero |
| * and scheduling call_rcu_tasks_trace() |
| * - CPU 1: continues and calls hrtimer_start() |
| * - After RCU tasks trace grace period: memory is freed |
| * - Timer callback fires on freed memory: UAF! |
| * |
| * This test stresses this race by having two threads: |
| * - Thread 1: repeatedly starts timers |
| * - Thread 2: repeatedly deletes map elements |
| * |
| * KASAN should detect use-after-free. |
| */ |
| |
| #define ITERATIONS 1000 |
| |
| struct ctx { |
| struct timer_start_delete_race *skel; |
| volatile bool start; |
| volatile bool stop; |
| int errors; |
| }; |
| |
| static void *start_timer_thread(void *arg) |
| { |
| struct ctx *ctx = arg; |
| cpu_set_t cpuset; |
| int fd, i; |
| |
| CPU_ZERO(&cpuset); |
| CPU_SET(0, &cpuset); |
| pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset); |
| |
| while (!ctx->start && !ctx->stop) |
| usleep(1); |
| if (ctx->stop) |
| return NULL; |
| |
| fd = bpf_program__fd(ctx->skel->progs.start_timer); |
| |
| for (i = 0; i < ITERATIONS && !ctx->stop; i++) { |
| LIBBPF_OPTS(bpf_test_run_opts, opts); |
| int err; |
| |
| err = bpf_prog_test_run_opts(fd, &opts); |
| if (err || opts.retval) { |
| ctx->errors++; |
| break; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static void *delete_elem_thread(void *arg) |
| { |
| struct ctx *ctx = arg; |
| cpu_set_t cpuset; |
| int fd, i; |
| |
| CPU_ZERO(&cpuset); |
| CPU_SET(1, &cpuset); |
| pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset); |
| |
| while (!ctx->start && !ctx->stop) |
| usleep(1); |
| if (ctx->stop) |
| return NULL; |
| |
| fd = bpf_program__fd(ctx->skel->progs.delete_elem); |
| |
| for (i = 0; i < ITERATIONS && !ctx->stop; i++) { |
| LIBBPF_OPTS(bpf_test_run_opts, opts); |
| int err; |
| |
| err = bpf_prog_test_run_opts(fd, &opts); |
| if (err || opts.retval) { |
| ctx->errors++; |
| break; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| void test_timer_start_delete_race(void) |
| { |
| struct timer_start_delete_race *skel; |
| pthread_t threads[2]; |
| struct ctx ctx = {}; |
| int err; |
| |
| skel = timer_start_delete_race__open_and_load(); |
| if (!ASSERT_OK_PTR(skel, "skel_open_and_load")) |
| return; |
| |
| ctx.skel = skel; |
| |
| err = pthread_create(&threads[0], NULL, start_timer_thread, &ctx); |
| if (!ASSERT_OK(err, "create start_timer_thread")) { |
| ctx.stop = true; |
| goto cleanup; |
| } |
| |
| err = pthread_create(&threads[1], NULL, delete_elem_thread, &ctx); |
| if (!ASSERT_OK(err, "create delete_elem_thread")) { |
| ctx.stop = true; |
| pthread_join(threads[0], NULL); |
| goto cleanup; |
| } |
| |
| ctx.start = true; |
| |
| pthread_join(threads[0], NULL); |
| pthread_join(threads[1], NULL); |
| |
| ASSERT_EQ(ctx.errors, 0, "thread_errors"); |
| |
| /* Either KASAN will catch UAF or kernel will crash or nothing happens */ |
| cleanup: |
| timer_start_delete_race__destroy(skel); |
| } |