From: Yuan Chen A perf_event program running in NMI context overwrites a rhtab element whose value holds a referenced task kptr. The old kptr must stay attached to the element (cancel semantics, matching hash maps); before the rhtab recycle fix the NMI update eagerly released it and the probe observed NULL. The test skips when no hardware PMU is available. Signed-off-by: Yuan Chen --- .../selftests/bpf/prog_tests/rhtab_kptr.c | 87 +++++++++++++++ .../testing/selftests/bpf/progs/rhtab_kptr.c | 101 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 tools/testing/selftests/bpf/prog_tests/rhtab_kptr.c create mode 100644 tools/testing/selftests/bpf/progs/rhtab_kptr.c diff --git a/tools/testing/selftests/bpf/prog_tests/rhtab_kptr.c b/tools/testing/selftests/bpf/prog_tests/rhtab_kptr.c new file mode 100644 index 000000000000..79bdcda95555 --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/rhtab_kptr.c @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 KylinSoft Co., Ltd. */ + +#include +#include +#include +#include +#include "rhtab_kptr.skel.h" + +static __u64 read_counter(struct rhtab_kptr *skel, u32 idx) +{ + __u64 vals[libbpf_num_possible_cpus()]; + __u64 sum = 0; + int i, err; + + err = bpf_map_lookup_elem(bpf_map__fd(skel->maps.counters), &idx, vals); + if (!ASSERT_OK(err, "lookup_counter")) + return 0; + for (i = 0; i < libbpf_num_possible_cpus(); i++) + sum += vals[i]; + return sum; +} + +void test_rhtab_kptr(void) +{ + struct perf_event_attr attr = { + .type = PERF_TYPE_HARDWARE, + .config = PERF_COUNT_HW_CPU_CYCLES, + .freq = 1, + .sample_freq = read_perf_max_sample_freq(), + .size = sizeof(struct perf_event_attr), + }; + LIBBPF_OPTS(bpf_test_run_opts, topts); + struct rhtab_kptr *skel; + __u32 key = 0; + __u64 zero = 0; + int pmu_fd; + + skel = rhtab_kptr__open_and_load(); + if (!ASSERT_OK_PTR(skel, "open_and_load")) + return; + + /* Create the element and stash a referenced task kptr in it. */ + if (!ASSERT_OK(bpf_map_update_elem(bpf_map__fd(skel->maps.rhtab), + &key, &zero, BPF_ANY), "create_elem")) + goto out; + if (!ASSERT_OK(bpf_prog_test_run_opts(bpf_program__fd(skel->progs.init_elem), + &topts), "test_run_init") || + !ASSERT_EQ(topts.retval, 0, "init_ret")) + goto out; + + pmu_fd = syscall(__NR_perf_event_open, &attr, -1, 0, -1, 0); + if (pmu_fd < 0) { + test__skip(); + goto out; + } + skel->links.nmi_update = bpf_program__attach_perf_event(skel->progs.nmi_update, + pmu_fd); + if (!ASSERT_OK_PTR(skel->links.nmi_update, "attach_perf_event")) { + close(pmu_fd); + goto out; + } + + /* Let the NMI handler overwrite the element. */ + usleep(100000); + + bpf_link__destroy(skel->links.nmi_update); + skel->links.nmi_update = NULL; + close(pmu_fd); + + /* + * The old kptr must still be attached to the element: the NMI update + * path only cancels NMI-safe fields, mirroring hash map semantics. + * Before the fix the kptr was released from the NMI context and the + * probe below would see NULL. + */ + topts.retval = 0; + if (!ASSERT_OK(bpf_prog_test_run_opts(bpf_program__fd(skel->progs.probe_elem), + &topts), "test_run_probe") || + !ASSERT_EQ(topts.retval, 0, "probe_ret")) + goto out; + + ASSERT_EQ(read_counter(skel, 2), 1, "xchg_non_null"); + ASSERT_EQ(read_counter(skel, 3), 0, "xchg_null"); +out: + rhtab_kptr__destroy(skel); +} diff --git a/tools/testing/selftests/bpf/progs/rhtab_kptr.c b/tools/testing/selftests/bpf/progs/rhtab_kptr.c new file mode 100644 index 000000000000..25800a2e24e6 --- /dev/null +++ b/tools/testing/selftests/bpf/progs/rhtab_kptr.c @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 KylinSoft Co., Ltd. */ + +/* + * Verify that a rhtab value update issued from a perf_event (NMI) program + * does not eagerly destroy referenced kptrs. rhtab must match the hash map + * semantics introduced by commit a3a81d247651 ("bpf: Cancel special fields + * on map value recycle"): only NMI-safe fields (timer, workqueue, task_work) + * are cancelled on update/delete, while kptrs stay attached to the recycled + * element until it is eventually freed. + * + * Without the fix, the NMI update releases the old kptr and probe_elem() + * observes NULL in the slot; with the fix the old kptr is inherited and + * probe_elem() observes a non-NULL pointer. + */ +#include +#include + +char LICENSE[] SEC("license") = "GPL"; + +struct val_t { + struct task_struct __kptr *tsk; +}; + +struct { + __uint(type, BPF_MAP_TYPE_RHASH); + __uint(max_entries, 16); + __uint(map_flags, BPF_F_NO_PREALLOC); + __type(key, __u32); + __type(value, struct val_t); +} rhtab SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 4); + __type(key, __u32); + __type(value, __u64); +} counters SEC(".maps"); + +/* 0: init ok, 1: nmi update ok, 2: probe xchg non-NULL, 3: probe xchg NULL */ +static __always_inline void bump(u32 idx) +{ + u64 *v = bpf_map_lookup_elem(&counters, &idx); + + if (v) + (*v)++; +} + +extern struct task_struct *bpf_task_acquire(struct task_struct *p) __ksym; +extern void bpf_task_release(struct task_struct *p) __ksym; + +SEC("perf_event") +int nmi_update(struct bpf_perf_event_data *ctx) +{ + struct val_t val = {}; + u32 key = 0; + + if (bpf_map_update_elem(&rhtab, &key, &val, BPF_ANY) == 0) + bump(1); + return 0; +} + +SEC("syscall") +int init_elem(void *ctx) +{ + struct val_t *val; + struct task_struct *task, *old; + u32 key = 0; + + val = bpf_map_lookup_elem(&rhtab, &key); + if (!val) + return 1; + task = bpf_task_acquire(bpf_get_current_task_btf()); + if (!task) + return 2; + old = bpf_kptr_xchg(&val->tsk, task); + if (old) + bpf_task_release(old); + bump(0); + return 0; +} + +SEC("syscall") +int probe_elem(void *ctx) +{ + struct val_t *val; + struct task_struct *old; + u32 key = 0; + + val = bpf_map_lookup_elem(&rhtab, &key); + if (!val) + return 1; + old = bpf_kptr_xchg(&val->tsk, NULL); + if (old) { + bpf_task_release(old); + bump(2); + } else { + bump(3); + } + return 0; +} -- 2.54.0