* [PATCH v1 0/5] perf bpf-filter / lock contention: Add lock_held() sample filter and RCU/SRCU support
@ 2026-10-17 5:45 Ian Rogers
2026-10-17 5:45 ` [PATCH v1 1/5] perf lock contention: Share common BPF and lock type definitions in lock_data.h Ian Rogers
` (4 more replies)
0 siblings, 5 replies; 11+ messages in thread
From: Ian Rogers @ 2026-10-17 5:45 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Ian Rogers, Adrian Hunter,
James Clark, linux-perf-users, linux-kernel, rcu
This series adds a new BPF sample filter predicate, `lock_held(<lock>)`
(along with `lock_held_read(<lock>)` / `lock_held_shared(<lock>)`,
`lock_held_write(<lock>)` / `lock_held_exclusive(<lock>)`, and `:R` / `:W`
mode suffixes), to `perf record --filter`, allowing perf event samples to
be recorded only when a specified kernel lock is held (or not held with
`!`) by the running kernel thread or CPU. It also extends both
`lock_held()` and `perf lock contention --use-bpf` to support RCU and
SRCU.
For example:
# perf record -a -g -e cycles --filter 'lock_held(&cgroup_mutex)' -- ...
# perf record -a -g -e cycles --filter 'lock_held_read(&mmap_lock)' -- ...
# perf record -a -g -e cycles --filter 'lock_held(&mmap_lock:W)' -- ...
# perf record -a -g -e cycles --filter 'lock_held_read(rcu)' -- ...
# perf lock contention -a -b -Y rcu,srcu -- sleep 1
Supported `<lock>` specifications match `perf lock contention`:
- Global/static kernel lock symbols (with or without leading `&`, e.g.
`lock_held(&cgroup_mutex)` or `lock_held(cgroup_mutex)`)
- Hexadecimal kernel lock addresses
- Slab cache names prefixed with `&` (e.g. `lock_held(&dentry)`,
`lock_held_read(&dentry)`)
- Well-known per-process, semi-global, and RCU locks: `mmap_lock`,
`siglock`, `rq_lock`, `zone_lock`, `rcu`, `rcu-trace`
- Lock types and flags: `mutex`, `spinlock`, `rwsem`, `rwsem:R`,
`rwsem:W`, `rwlock`, `rwlock:R`, `rwlock:W`, `rt-mutex`,
`percpu-rwsem`, `pcpu-sem:W`, `srcu`, `srcu:R`, `srcu:W`, `rcu:R`,
`rcu:W`, `semaphore`
Limitations and weaknesses in determining whether a lock is held:
- Inlined lock/unlock operations:
- When the kernel is configured to inline spinlock or rwlock operations
(`CONFIG_INLINE_SPIN_LOCK`, `CONFIG_INLINE_READ_LOCK`,
`CONFIG_INLINE_WRITE_LOCK`, etc., which are enabled on many
`!CONFIG_PREEMPT_RT` / `!CONFIG_PROVE_LOCKING` builds unless
`CONFIG_UNINLINE_SPIN_UNLOCK` or lockdep/preemption forces them out of
line), `_raw_spin_lock*` / `_raw_spin_unlock*` and
`_raw_read_lock*` / `_raw_write_lock*` are inlined into callers and
have no BTF function entry points for `fentry`/`fexit` to attach to.
- Fast-path `percpu_down_read()` / `percpu_up_read()` and
`srcu_read_lock_fast()` / `srcu_read_unlock_fast()` are inlined
per-CPU counter increments without function calls, so only
`percpu_down_write()` / `percpu_up_write()` and out-of-line
`__srcu_read_lock*()` / `__srcu_read_unlock*()` can be hooked.
- On `!CONFIG_PREEMPT_RCU` kernels, `rcu_read_lock()` compiles to
`preempt_disable()` and does not maintain
`current->rcu_read_lock_nesting` in `task_struct`, so `lock_held(rcu)`
read-side inspection requires `CONFIG_PREEMPT_RCU`.
- Lack of an inspectable owner field on non-exclusive / spinning locks:
- While `struct mutex`, `struct rt_mutex`, and write-locked
`struct rw_semaphore` store the owning `task_struct *` in `lock->owner`
(allowing `perf_sample_filter` to inspect kernel memory directly on
sample overflow when filtering by lock address), spinlocks, rwlocks,
semaphores, SRCU, and reader-locked `rw_semaphore`s (where
`__up_read()` does not clear `sem->owner` when `!CONFIG_DEBUG_RWSEMS`)
do not maintain an authoritative per-task owner pointer.
- Tracking these locks (as well as slab-cache and lock-type filters)
relies on `fexit` (acquire) and `fentry` (release) BPF hooks
maintaining the `held_locks` BPF hash map:
- Any such lock already held before `perf record` attaches its BPF
programs will not be present in `held_locks` until it is released
and re-acquired.
- The attached `fentry`/`fexit` hooks execute on every kernel lock
acquire and release for the hooked lock family across the system,
adding tracing overhead outside of perf sample handling.
Patch overview:
1. "perf lock contention: Share common BPF and lock type definitions in
lock_data.h": Moves `lock_type_table` to `util/lock-contention.c` and
`LCB_F_*` flags, CO-RE structs, and common BPF lock helpers
(`get_lock_owner`, `get_current_mmap_lock`, `get_current_siglock`,
`get_lock_addr`, `check_lock_type`, `do_collect_lock_syms`,
`do_slab_cache_iter`) into `tools/perf/util/bpf_skel/lock_data.h`.
2. "perf bpf-filter: Add lock_held() sample filter": Implements
`lock_held()`, `lock_held_read()`/`lock_held_shared()`,
`lock_held_write()`/`lock_held_exclusive()`, and `:R`/`:W` mode
suffixes using direct lock owner inspection where possible (`mutex`,
`rt_mutex`, write-locked `rw_semaphore`) and a `held_locks` BPF map
updated by selectively attached `fentry`/`fexit` hooks.
3. "perf test: Add tests for lock_held() BPF sample filter": Adds unit
tests in `tests/parse-events.c` and shell tests in
`tests/shell/record_bpf_filter.sh`.
4. "perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU,
and SRCU": Supports `rcu` (`rcu:R` via direct
`current->rcu_read_lock_nesting` inspection with zero tracing hooks;
`rcu:W` via `synchronize_rcu*()` / `rcu_barrier()` hooks),
`rcu-trace` (via `current->trc_reader_nesting`), and `srcu`
(`srcu:R` via `__srcu_read_lock*` / `__srcu_read_unlock*`; `srcu:W`
via `synchronize_srcu*()` / `srcu_barrier()`).
5. "perf lock contention: Extend BPF lock contention to RCU and SRCU":
Extends `perf lock contention --use-bpf` to track RCU and SRCU
grace-period and barrier waits (`-Y rcu`, `-Y srcu`, `-L rcu`).
Assisted-by: Antigravity:gemini-3.1-pro
Ian Rogers (5):
perf lock contention: Share common BPF and lock type definitions in
lock_data.h
perf bpf-filter: Add lock_held() sample filter
perf test: Add tests for lock_held() BPF sample filter
perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU,
and SRCU
perf lock contention: Extend BPF lock contention to RCU and SRCU
tools/perf/Documentation/perf-lock.txt | 7 +-
tools/perf/Documentation/perf-record.txt | 51 +-
tools/perf/builtin-lock.c | 27 -
tools/perf/tests/parse-events.c | 121 ++
tools/perf/tests/shell/lock_contention.sh | 14 +
tools/perf/tests/shell/record_bpf_filter.sh | 119 ++
tools/perf/util/bpf-filter.c | 1070 ++++++++++++++++-
tools/perf/util/bpf-filter.h | 13 +
tools/perf/util/bpf-filter.l | 31 +-
tools/perf/util/bpf-filter.y | 46 +-
tools/perf/util/bpf_lock_contention.c | 167 ++-
.../perf/util/bpf_skel/lock_contention.bpf.c | 410 +++----
tools/perf/util/bpf_skel/lock_data.h | 360 +++++-
tools/perf/util/bpf_skel/sample-filter.h | 71 ++
tools/perf/util/bpf_skel/sample_filter.bpf.c | 1051 ++++++++++++++++
tools/perf/util/bpf_skel/vmlinux/vmlinux.h | 7 +
tools/perf/util/lock-contention.c | 22 +
tools/perf/util/lock-contention.h | 15 +
tools/perf/util/parse-events.c | 3 +-
19 files changed, 3273 insertions(+), 332 deletions(-)
--
2.56.0.212.g129ede20d9-goog
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v1 1/5] perf lock contention: Share common BPF and lock type definitions in lock_data.h
2026-10-17 5:45 [PATCH v1 0/5] perf bpf-filter / lock contention: Add lock_held() sample filter and RCU/SRCU support Ian Rogers
@ 2026-10-17 5:45 ` Ian Rogers
2026-10-17 5:52 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 2/5] perf bpf-filter: Add lock_held() sample filter Ian Rogers
` (3 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Ian Rogers @ 2026-10-17 5:45 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Ian Rogers, Adrian Hunter,
James Clark, linux-perf-users, linux-kernel, rcu
Move lock_type_table, LCB_F_* flags, and the common BPF lock helpers
(get_lock_owner, get_current_mmap_lock, get_current_siglock,
get_lock_addr, check_lock_type, do_collect_zone_lock,
do_collect_lock_syms, do_slab_cache_iter) and shared CO-RE structs into
tools/perf/util/bpf_skel/lock_data.h so they can be reused by BPF
sample filtering.
Also add rw_semaphore.count and struct rt_mutex_base to the minimal
vmlinux.h and extend get_lock_owner() to support rt_mutex and check
writer-ownership and handoff flags.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/builtin-lock.c | 27 --
.../perf/util/bpf_skel/lock_contention.bpf.c | 265 +--------------
tools/perf/util/bpf_skel/lock_data.h | 316 ++++++++++++++++++
tools/perf/util/bpf_skel/vmlinux/vmlinux.h | 5 +
tools/perf/util/lock-contention.c | 18 +
tools/perf/util/lock-contention.h | 13 +
6 files changed, 370 insertions(+), 274 deletions(-)
diff --git a/tools/perf/builtin-lock.c b/tools/perf/builtin-lock.c
index d5c0d55cb82d..feaf1cfd08cf 100644
--- a/tools/perf/builtin-lock.c
+++ b/tools/perf/builtin-lock.c
@@ -1477,33 +1477,6 @@ static void sort_result(void)
}
}
-static const struct {
- unsigned int flags;
- /*
- * Name of the lock flags (access), with delimeter ':'.
- * For example, rwsem:R of rwsem:W.
- */
- const char *flags_name;
- /* Name of the lock (type), for example, rwlock or rwsem. */
- const char *lock_name;
-} lock_type_table[] = {
- { 0, "semaphore", "semaphore" },
- { LCB_F_SPIN, "spinlock", "spinlock" },
- { LCB_F_SPIN | LCB_F_READ, "rwlock:R", "rwlock" },
- { LCB_F_SPIN | LCB_F_WRITE, "rwlock:W", "rwlock" },
- { LCB_F_READ, "rwsem:R", "rwsem" },
- { LCB_F_WRITE, "rwsem:W", "rwsem" },
- { LCB_F_RT, "rt-mutex", "rt-mutex" },
- { LCB_F_RT | LCB_F_READ, "rwlock-rt:R", "rwlock-rt" },
- { LCB_F_RT | LCB_F_WRITE, "rwlock-rt:W", "rwlock-rt" },
- { LCB_F_PERCPU | LCB_F_READ, "pcpu-sem:R", "percpu-rwsem" },
- { LCB_F_PERCPU | LCB_F_WRITE, "pcpu-sem:W", "percpu-rwsem" },
- { LCB_F_MUTEX, "mutex", "mutex" },
- { LCB_F_MUTEX | LCB_F_SPIN, "mutex", "mutex" },
- /* alias for optimistic spinning only */
- { LCB_F_MUTEX | LCB_F_SPIN, "mutex:spin", "mutex-spin" },
-};
-
static const char *get_type_flags_name(unsigned int flags)
{
flags &= LCB_F_TYPE_MASK;
diff --git a/tools/perf/util/bpf_skel/lock_contention.bpf.c b/tools/perf/util/bpf_skel/lock_contention.bpf.c
index 0d9c6f55050e..0f2118006005 100644
--- a/tools/perf/util/bpf_skel/lock_contention.bpf.c
+++ b/tools/perf/util/bpf_skel/lock_contention.bpf.c
@@ -8,23 +8,9 @@
#include "lock_data.h"
-/* for collect_lock_syms(). 4096 was rejected by the verifier */
-#define MAX_CPUS 1024
-
-/* for collect_zone_lock(). It should be more than the actual zones. */
-#define MAX_ZONES 10
-
/* for do_lock_delay(). Arbitrarily set to 1 million. */
#define MAX_LOOP (1U << 20)
-/* lock contention flags from include/trace/events/lock.h */
-#define LCB_F_SPIN (1U << 0)
-#define LCB_F_READ (1U << 1)
-#define LCB_F_WRITE (1U << 2)
-#define LCB_F_RT (1U << 3)
-#define LCB_F_PERCPU (1U << 4)
-#define LCB_F_MUTEX (1U << 5)
-
/* callstack storage */
struct {
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
@@ -96,13 +82,6 @@ struct {
__uint(max_entries, MAX_ENTRIES);
} task_data SEC(".maps");
-struct {
- __uint(type, BPF_MAP_TYPE_HASH);
- __uint(key_size, sizeof(__u64));
- __uint(value_size, sizeof(__u32));
- __uint(max_entries, MAX_ENTRIES);
-} lock_syms SEC(".maps");
-
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(key_size, sizeof(__u32));
@@ -138,6 +117,13 @@ struct {
__uint(max_entries, 1);
} cgroup_filter SEC(".maps");
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(key_size, sizeof(__u64));
+ __uint(value_size, sizeof(__u32));
+ __uint(max_entries, MAX_ENTRIES);
+} lock_syms SEC(".maps");
+
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(key_size, sizeof(long));
@@ -159,22 +145,6 @@ struct {
__uint(max_entries, 1);
} lock_delays SEC(".maps");
-struct rw_semaphore___old {
- struct task_struct *owner;
-} __attribute__((preserve_access_index));
-
-struct rw_semaphore___new {
- atomic_long_t owner;
-} __attribute__((preserve_access_index));
-
-struct mm_struct___old {
- struct rw_semaphore mmap_sem;
-} __attribute__((preserve_access_index));
-
-struct mm_struct___new {
- struct rw_semaphore mmap_lock;
-} __attribute__((preserve_access_index));
-
struct cas_ctx {
struct contention_data *data;
u64 duration;
@@ -182,8 +152,6 @@ struct cas_ctx {
int min_done;
};
-extern struct kmem_cache *bpf_get_kmem_cache(u64 addr) __ksym __weak;
-
/* control flags */
const volatile int has_cpu;
const volatile int has_task;
@@ -202,6 +170,11 @@ const volatile int lock_delay;
/* determine the key of lock stat */
const volatile int aggr_mode;
+const volatile __u64 contig_page_data_addr;
+const volatile __u64 node_data_addr;
+const volatile int nr_nodes;
+const volatile int sizeof_zone;
+
int enabled;
int perf_subsys_id = -1;
@@ -222,8 +195,6 @@ int data_map_full;
struct task_struct *bpf_task_from_pid(s32 pid) __ksym __weak;
void bpf_task_release(struct task_struct *p) __ksym __weak;
-static inline __u32 check_lock_type(__u64 lock, __u32 flags);
-
static inline __u64 get_current_cgroup_id(void)
{
struct task_struct *task;
@@ -347,85 +318,6 @@ static inline int update_task_data(struct task_struct *task)
return 0;
}
-#ifndef __has_builtin
-# define __has_builtin(x) 0
-#endif
-
-static inline struct task_struct *get_lock_owner(__u64 lock, __u32 flags)
-{
- struct task_struct *task;
- __u64 owner = 0;
-
- if (flags & LCB_F_MUTEX) {
- struct mutex *mutex = (void *)lock;
- owner = BPF_CORE_READ(mutex, owner.counter);
- } else if (flags == LCB_F_READ || flags == LCB_F_WRITE) {
- /*
- * Support for the BPF_TYPE_MATCHES argument to the
- * __builtin_preserve_type_info builtin was added at some point during
- * development of clang 15 and it's what is needed for
- * bpf_core_type_matches.
- */
-#if __has_builtin(__builtin_preserve_type_info) && __clang_major__ >= 15
- if (bpf_core_type_matches(struct rw_semaphore___old)) {
- struct rw_semaphore___old *rwsem = (void *)lock;
- owner = (unsigned long)BPF_CORE_READ(rwsem, owner);
- } else if (bpf_core_type_matches(struct rw_semaphore___new)) {
- struct rw_semaphore___new *rwsem = (void *)lock;
- owner = BPF_CORE_READ(rwsem, owner.counter);
- }
-#else
- /* assume new struct */
- struct rw_semaphore *rwsem = (void *)lock;
- owner = BPF_CORE_READ(rwsem, owner.counter);
-#endif
- }
-
- if (!owner)
- return NULL;
-
- task = (void *)(owner & ~7UL);
- return task;
-}
-
-static inline __u32 check_lock_type(__u64 lock, __u32 flags)
-{
- struct task_struct *curr;
- struct mm_struct___old *mm_old;
- struct mm_struct___new *mm_new;
- struct sighand_struct *sighand;
-
- switch (flags) {
- case LCB_F_READ: /* rwsem */
- case LCB_F_WRITE:
- curr = bpf_get_current_task_btf();
- if (curr->mm == NULL)
- break;
- mm_new = (void *)curr->mm;
- if (bpf_core_field_exists(mm_new->mmap_lock)) {
- if (&mm_new->mmap_lock == (void *)lock)
- return LCD_F_MMAP_LOCK;
- break;
- }
- mm_old = (void *)curr->mm;
- if (bpf_core_field_exists(mm_old->mmap_sem)) {
- if (&mm_old->mmap_sem == (void *)lock)
- return LCD_F_MMAP_LOCK;
- }
- break;
- case LCB_F_SPIN: /* spinlock */
- curr = bpf_get_current_task_btf();
- sighand = curr->sighand;
-
- if (sighand && &sighand->siglock == (void *)lock)
- return LCD_F_SIGHAND_LOCK;
- break;
- default:
- break;
- }
- return 0;
-}
-
static inline long delay_callback(__u64 idx, void *arg)
{
__u64 target = *(__u64 *)arg;
@@ -908,145 +800,24 @@ int contention_end(u64 *ctx)
return 0;
}
-extern struct rq runqueues __ksym;
-
-const volatile __u64 contig_page_data_addr;
-const volatile __u64 node_data_addr;
-const volatile int nr_nodes;
-const volatile int sizeof_zone;
-
-struct rq___old {
- raw_spinlock_t lock;
-} __attribute__((preserve_access_index));
-
-struct rq___new {
- raw_spinlock_t __lock;
-} __attribute__((preserve_access_index));
-
-static void collect_zone_lock(void)
-{
- __u64 nr_zones, zone_off;
- __u64 lock_addr, lock_off;
- __u32 lock_flag = LOCK_CLASS_ZONE_LOCK;
-
- zone_off = offsetof(struct pglist_data, node_zones);
- lock_off = offsetof(struct zone, lock);
-
- if (contig_page_data_addr) {
- struct pglist_data *contig_page_data;
-
- contig_page_data = (void *)(long)contig_page_data_addr;
- nr_zones = BPF_CORE_READ(contig_page_data, nr_zones);
-
- for (int i = 0; i < MAX_ZONES; i++) {
- __u64 zone_addr;
-
- if (i >= nr_zones)
- break;
-
- zone_addr = contig_page_data_addr + (sizeof_zone * i) + zone_off;
- lock_addr = zone_addr + lock_off;
-
- bpf_map_update_elem(&lock_syms, &lock_addr, &lock_flag, BPF_ANY);
- }
- } else if (nr_nodes > 0) {
- struct pglist_data **node_data = (void *)(long)node_data_addr;
-
- for (int i = 0; i < nr_nodes; i++) {
- struct pglist_data *pgdat = NULL;
- int err;
-
- err = bpf_core_read(&pgdat, sizeof(pgdat), &node_data[i]);
- if (err < 0 || pgdat == NULL)
- break;
-
- nr_zones = BPF_CORE_READ(pgdat, nr_zones);
- for (int k = 0; k < MAX_ZONES; k++) {
- __u64 zone_addr;
-
- if (k >= nr_zones)
- break;
-
- zone_addr = (__u64)(void *)pgdat + (sizeof_zone * k) + zone_off;
- lock_addr = zone_addr + lock_off;
-
- bpf_map_update_elem(&lock_syms, &lock_addr, &lock_flag, BPF_ANY);
- }
- }
- }
-}
-
SEC("raw_tp/bpf_test_finish")
-int BPF_PROG(collect_lock_syms)
+int BPF_PROG(end_timestamp)
{
- __u64 lock_addr, lock_off;
- __u32 lock_flag;
-
- if (bpf_core_field_exists(struct rq___new, __lock))
- lock_off = offsetof(struct rq___new, __lock);
- else
- lock_off = offsetof(struct rq___old, lock);
-
- for (int i = 0; i < MAX_CPUS; i++) {
- struct rq *rq = bpf_per_cpu_ptr(&runqueues, i);
-
- if (rq == NULL)
- break;
-
- lock_addr = (__u64)(void *)rq + lock_off;
- lock_flag = LOCK_CLASS_RQLOCK;
- bpf_map_update_elem(&lock_syms, &lock_addr, &lock_flag, BPF_ANY);
- }
-
- collect_zone_lock();
-
+ end_ts = bpf_ktime_get_ns();
return 0;
}
SEC("raw_tp/bpf_test_finish")
-int BPF_PROG(end_timestamp)
+int BPF_PROG(collect_lock_syms)
{
- end_ts = bpf_ktime_get_ns();
- return 0;
+ return do_collect_lock_syms(&lock_syms, contig_page_data_addr,
+ node_data_addr, nr_nodes, sizeof_zone);
}
-/*
- * bpf_iter__kmem_cache added recently so old kernels don't have it in the
- * vmlinux.h. But we cannot add it here since it will cause a compiler error
- * due to redefinition of the struct on later kernels.
- *
- * So it uses a CO-RE trick to access the member only if it has the type.
- * This will support both old and new kernels without compiler errors.
- */
-struct bpf_iter__kmem_cache___new {
- struct kmem_cache *s;
-} __attribute__((preserve_access_index));
-
SEC("iter/kmem_cache")
int slab_cache_iter(void *ctx)
{
- struct kmem_cache *s = NULL;
- struct slab_cache_data d;
- const char *nameptr;
-
- if (bpf_core_type_exists(struct bpf_iter__kmem_cache)) {
- struct bpf_iter__kmem_cache___new *iter = ctx;
-
- s = iter->s;
- }
-
- if (s == NULL)
- return 0;
-
- nameptr = s->name;
- bpf_probe_read_kernel_str(d.name, sizeof(d.name), nameptr);
-
- d.id = ++slab_cache_id << LCB_F_SLAB_ID_SHIFT;
- if (d.id >= LCB_F_SLAB_ID_END)
- return 0;
-
- bpf_map_update_elem(&slab_caches, &s, &d, BPF_NOEXIST);
- return 0;
+ return do_slab_cache_iter(ctx, &slab_caches, &slab_cache_id);
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
diff --git a/tools/perf/util/bpf_skel/lock_data.h b/tools/perf/util/bpf_skel/lock_data.h
index 652e114e6b87..fed445c9d8b4 100644
--- a/tools/perf/util/bpf_skel/lock_data.h
+++ b/tools/perf/util/bpf_skel/lock_data.h
@@ -45,6 +45,14 @@ struct contention_task_data {
#define LCB_F_SLAB_ID_END (1U << 26)
#define LCB_F_SLAB_ID_MASK 0x03FF0000U
+/* lock contention flags from include/trace/events/lock.h */
+#define LCB_F_SPIN (1U << 0)
+#define LCB_F_READ (1U << 1)
+#define LCB_F_WRITE (1U << 2)
+#define LCB_F_RT (1U << 3)
+#define LCB_F_PERCPU (1U << 4)
+#define LCB_F_MUTEX (1U << 5)
+
#define LCB_F_TYPE_MAX (1U << 7)
#define LCB_F_TYPE_MASK 0x0000007FU
@@ -76,4 +84,312 @@ struct slab_cache_data {
char name[SLAB_NAME_MAX];
};
+#ifdef __BPF__
+
+/* for collect_lock_syms(). 4096 was rejected by the verifier */
+#define MAX_CPUS 1024
+
+/* for collect_zone_lock(). It should be more than the actual zones. */
+#define MAX_ZONES 10
+
+extern struct kmem_cache *bpf_get_kmem_cache(u64 addr) __ksym __weak;
+extern struct rq runqueues __ksym __weak;
+
+#ifndef __has_builtin
+# define __has_builtin(x) 0
+#endif
+
+struct rw_semaphore___old {
+ struct task_struct *owner;
+} __attribute__((preserve_access_index));
+
+struct rw_semaphore___new {
+ atomic_long_t count;
+ atomic_long_t owner;
+} __attribute__((preserve_access_index));
+
+struct mm_struct___old {
+ struct rw_semaphore mmap_sem;
+} __attribute__((preserve_access_index));
+
+struct mm_struct___new {
+ struct rw_semaphore mmap_lock;
+} __attribute__((preserve_access_index));
+
+struct rq___old {
+ raw_spinlock_t lock;
+} __attribute__((preserve_access_index));
+
+struct rq___new {
+ raw_spinlock_t __lock;
+} __attribute__((preserve_access_index));
+
+/*
+ * bpf_iter__kmem_cache added recently so old kernels don't have it in the
+ * vmlinux.h. But we cannot add it here since it will cause a compiler error
+ * due to redefinition of the struct on later kernels.
+ *
+ * So it uses a CO-RE trick to access the member only if it has the type.
+ * This will support both old and new kernels without compiler errors.
+ */
+struct bpf_iter__kmem_cache___new {
+ struct kmem_cache *s;
+} __attribute__((preserve_access_index));
+
+static __always_inline struct task_struct *get_lock_owner(__u64 lock, __u32 flags)
+{
+ struct task_struct *task;
+ __u64 owner = 0;
+
+ if (flags & LCB_F_MUTEX) {
+ struct mutex *mutex = (void *)lock;
+
+ owner = BPF_CORE_READ(mutex, owner.counter);
+ if (owner & 0x4UL)
+ return NULL;
+ } else if (flags == LCB_F_READ || flags == LCB_F_WRITE) {
+ struct rw_semaphore___new *rwsem = (void *)lock;
+
+ if (flags == LCB_F_WRITE && bpf_core_field_exists(rwsem->count.counter)) {
+ __u64 count = BPF_CORE_READ(rwsem, count.counter);
+
+ /* Must have RWSEM_WRITER_LOCKED (bit 0) set */
+ if (!(count & 1UL))
+ return NULL;
+ }
+ /*
+ * Support for the BPF_TYPE_MATCHES argument to the
+ * __builtin_preserve_type_info builtin was added at some point during
+ * development of clang 15 and it's what is needed for
+ * bpf_core_type_matches.
+ */
+#if __has_builtin(__builtin_preserve_type_info) && __clang_major__ >= 15
+ if (bpf_core_type_matches(struct rw_semaphore___old)) {
+ struct rw_semaphore___old *old = (void *)lock;
+
+ owner = (unsigned long)BPF_CORE_READ(old, owner);
+ } else if (bpf_core_type_matches(struct rw_semaphore___new)) {
+ owner = BPF_CORE_READ(rwsem, owner.counter);
+ }
+#else
+ /* assume new struct */
+ owner = BPF_CORE_READ(rwsem, owner.counter);
+#endif
+ /* RWSEM_READER_OWNED is bit 0 of owner */
+ if (flags == LCB_F_WRITE && (owner & 1UL))
+ return NULL;
+ } else if (flags == LCB_F_RT) {
+ struct rt_mutex_base *rtm = (void *)lock;
+
+ if (bpf_core_type_exists(struct rt_mutex_base)) {
+ owner = (__u64)BPF_CORE_READ(rtm, owner);
+ return (void *)(owner & ~1UL);
+ }
+ }
+
+ if (!owner)
+ return NULL;
+
+ task = (void *)(owner & ~7UL);
+ return task;
+}
+
+static __always_inline __u64 get_current_mmap_lock(__u64 curr)
+{
+ struct task_struct *task = (void *)curr;
+ struct mm_struct___new *mm_new = NULL;
+ struct mm_struct___old *mm_old = NULL;
+ __u64 mm;
+
+ if (!curr)
+ return 0;
+
+ mm = (__u64)BPF_CORE_READ(task, mm);
+ if (!mm)
+ return 0;
+
+ if (bpf_core_field_exists(mm_new->mmap_lock))
+ return mm + bpf_core_field_offset(mm_new->mmap_lock);
+
+ if (bpf_core_field_exists(mm_old->mmap_sem))
+ return mm + bpf_core_field_offset(mm_old->mmap_sem);
+
+ return 0;
+}
+
+static __always_inline __u64 get_current_siglock(__u64 curr)
+{
+ struct task_struct *task = (void *)curr;
+ struct sighand_struct *sighand = NULL;
+ __u64 sh;
+
+ if (!curr)
+ return 0;
+
+ sh = (__u64)BPF_CORE_READ(task, sighand);
+ if (!sh)
+ return 0;
+
+ return sh + bpf_core_field_offset(sighand->siglock);
+}
+
+/*
+ * Read the 1st function argument (`lock` / `sem` / `ssp`) as a 64-bit scalar.
+ *
+ * Using `bpf_get_func_arg()` rather than reading `ctx[0]` directly ensures the
+ * BPF verifier tracks the address as a `SCALAR_VALUE` instead of a typed
+ * `PTR_TO_BTF_ID`, which is required when passing the address to kfuncs such
+ * as `bpf_get_kmem_cache(u64 addr)`.
+ */
+static __always_inline __u64 get_lock_addr(void *ctx)
+{
+ __u64 lock_addr = 0;
+
+ bpf_get_func_arg(ctx, 0, &lock_addr);
+ return lock_addr;
+}
+
+static __always_inline __u32 check_lock_type(__u64 lock, __u32 flags)
+{
+ __u64 curr;
+
+ switch (flags) {
+ case LCB_F_READ: /* rwsem */
+ case LCB_F_WRITE:
+ curr = bpf_get_current_task();
+ if (get_current_mmap_lock(curr) == lock)
+ return LCD_F_MMAP_LOCK;
+ break;
+ case LCB_F_SPIN: /* spinlock */
+ curr = bpf_get_current_task();
+ if (get_current_siglock(curr) == lock)
+ return LCD_F_SIGHAND_LOCK;
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+static __always_inline void do_collect_zone_lock(void *lock_syms_map,
+ __u64 contig_page_data_addr,
+ __u64 node_data_addr,
+ int nr_nodes,
+ int sizeof_zone)
+{
+ __u64 nr_zones, zone_off;
+ __u64 lock_addr, lock_off;
+ __u32 lock_flag = LOCK_CLASS_ZONE_LOCK;
+
+ zone_off = offsetof(struct pglist_data, node_zones);
+ lock_off = offsetof(struct zone, lock);
+
+ if (contig_page_data_addr) {
+ struct pglist_data *contig_page_data;
+
+ contig_page_data = (void *)(long)contig_page_data_addr;
+ nr_zones = BPF_CORE_READ(contig_page_data, nr_zones);
+
+ for (int i = 0; i < MAX_ZONES; i++) {
+ __u64 zone_addr;
+
+ if (i >= nr_zones)
+ break;
+
+ zone_addr = contig_page_data_addr + (sizeof_zone * i) + zone_off;
+ lock_addr = zone_addr + lock_off;
+
+ bpf_map_update_elem(lock_syms_map, &lock_addr, &lock_flag, BPF_ANY);
+ }
+ } else if (nr_nodes > 0) {
+ struct pglist_data **node_data = (void *)(long)node_data_addr;
+
+ for (int i = 0; i < nr_nodes; i++) {
+ struct pglist_data *pgdat = NULL;
+ int err;
+
+ err = bpf_core_read(&pgdat, sizeof(pgdat), &node_data[i]);
+ if (err < 0)
+ break;
+ if (pgdat == NULL)
+ continue;
+
+ nr_zones = BPF_CORE_READ(pgdat, nr_zones);
+ for (int k = 0; k < MAX_ZONES; k++) {
+ __u64 zone_addr;
+
+ if (k >= nr_zones)
+ break;
+
+ zone_addr = (__u64)(void *)pgdat + (sizeof_zone * k) + zone_off;
+ lock_addr = zone_addr + lock_off;
+
+ bpf_map_update_elem(lock_syms_map, &lock_addr, &lock_flag, BPF_ANY);
+ }
+ }
+ }
+}
+
+static __always_inline int do_collect_lock_syms(void *lock_syms_map,
+ __u64 contig_page_data_addr,
+ __u64 node_data_addr,
+ int nr_nodes,
+ int sizeof_zone)
+{
+ __u64 lock_addr, lock_off;
+ __u32 lock_flag;
+
+ if (bpf_core_field_exists(struct rq___new, __lock))
+ lock_off = offsetof(struct rq___new, __lock);
+ else
+ lock_off = offsetof(struct rq___old, lock);
+
+ if (&runqueues) {
+ for (int i = 0; i < MAX_CPUS; i++) {
+ struct rq *rq = bpf_per_cpu_ptr(&runqueues, i);
+
+ if (rq == NULL)
+ break;
+
+ lock_addr = (__u64)(void *)rq + lock_off;
+ lock_flag = LOCK_CLASS_RQLOCK;
+ bpf_map_update_elem(lock_syms_map, &lock_addr, &lock_flag, BPF_ANY);
+ }
+ }
+
+ do_collect_zone_lock(lock_syms_map, contig_page_data_addr,
+ node_data_addr, nr_nodes, sizeof_zone);
+
+ return 0;
+}
+
+static __always_inline int do_slab_cache_iter(void *ctx, void *slab_caches_map,
+ __u32 *slab_cache_id)
+{
+ struct kmem_cache *s = NULL;
+ struct slab_cache_data d = {};
+ const char *nameptr;
+
+ if (bpf_core_type_exists(struct bpf_iter__kmem_cache)) {
+ struct bpf_iter__kmem_cache___new *iter = ctx;
+
+ s = iter->s;
+ }
+
+ if (s == NULL)
+ return 0;
+
+ nameptr = s->name;
+ bpf_probe_read_kernel_str(d.name, sizeof(d.name), nameptr);
+
+ d.id = ++(*slab_cache_id) << LCB_F_SLAB_ID_SHIFT;
+ if (d.id >= LCB_F_SLAB_ID_END)
+ return 0;
+
+ bpf_map_update_elem(slab_caches_map, &s, &d, BPF_NOEXIST);
+ return 0;
+}
+
+#endif /* __BPF__ */
+
#endif /* UTIL_BPF_SKEL_LOCK_DATA_H */
diff --git a/tools/perf/util/bpf_skel/vmlinux/vmlinux.h b/tools/perf/util/bpf_skel/vmlinux/vmlinux.h
index 94e42b022ab1..b75ddc6846bb 100644
--- a/tools/perf/util/bpf_skel/vmlinux/vmlinux.h
+++ b/tools/perf/util/bpf_skel/vmlinux/vmlinux.h
@@ -76,6 +76,7 @@ struct signal_struct {
} __attribute__((preserve_access_index));
struct rw_semaphore {
+ atomic_long_t count;
atomic_long_t owner;
} __attribute__((preserve_access_index));
@@ -83,6 +84,10 @@ struct mutex {
atomic_long_t owner;
} __attribute__((preserve_access_index));
+struct rt_mutex_base {
+ struct task_struct *owner;
+} __attribute__((preserve_access_index));
+
struct kernfs_node {
u64 id;
} __attribute__((preserve_access_index));
diff --git a/tools/perf/util/lock-contention.c b/tools/perf/util/lock-contention.c
index 119a7206f3cd..a1bb78d6e29c 100644
--- a/tools/perf/util/lock-contention.c
+++ b/tools/perf/util/lock-contention.c
@@ -22,6 +22,24 @@ struct callstack_filter {
static LIST_HEAD(callstack_filters);
struct hlist_head *lockhash_table;
+const struct perf_lock_type lock_type_table[] = {
+ { 0, "semaphore", "semaphore" },
+ { LCB_F_SPIN, "spinlock", "spinlock" },
+ { LCB_F_SPIN | LCB_F_READ, "rwlock:R", "rwlock" },
+ { LCB_F_SPIN | LCB_F_WRITE, "rwlock:W", "rwlock" },
+ { LCB_F_READ, "rwsem:R", "rwsem" },
+ { LCB_F_WRITE, "rwsem:W", "rwsem" },
+ { LCB_F_RT, "rt-mutex", "rt-mutex" },
+ { LCB_F_RT | LCB_F_READ, "rwlock-rt:R", "rwlock-rt" },
+ { LCB_F_RT | LCB_F_WRITE, "rwlock-rt:W", "rwlock-rt" },
+ { LCB_F_PERCPU | LCB_F_READ, "pcpu-sem:R", "percpu-rwsem" },
+ { LCB_F_PERCPU | LCB_F_WRITE, "pcpu-sem:W", "percpu-rwsem" },
+ { LCB_F_MUTEX, "mutex", "mutex" },
+ { LCB_F_MUTEX | LCB_F_SPIN, "mutex", "mutex" },
+ /* alias for optimistic spinning only */
+ { LCB_F_MUTEX | LCB_F_SPIN, "mutex:spin", "mutex-spin" },
+};
+
int parse_call_stack(const struct option *opt __maybe_unused, const char *str,
int unset __maybe_unused)
{
diff --git a/tools/perf/util/lock-contention.h b/tools/perf/util/lock-contention.h
index 59c94190b092..68e9bd77e8a9 100644
--- a/tools/perf/util/lock-contention.h
+++ b/tools/perf/util/lock-contention.h
@@ -129,6 +129,19 @@ struct thread_stat {
#define LCB_F_PERCPU (1U << 4)
#define LCB_F_MUTEX (1U << 5)
+struct perf_lock_type {
+ unsigned int flags;
+ /*
+ * Name of the lock flags (access), with delimiter ':'.
+ * For example, rwsem:R or rwsem:W.
+ */
+ const char *flags_name;
+ /* Name of the lock (type), for example, rwlock or rwsem. */
+ const char *lock_name;
+};
+
+extern const struct perf_lock_type lock_type_table[14];
+
struct evlist;
struct machine;
struct target;
--
2.56.0.212.g129ede20d9-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v1 2/5] perf bpf-filter: Add lock_held() sample filter
2026-10-17 5:45 [PATCH v1 0/5] perf bpf-filter / lock contention: Add lock_held() sample filter and RCU/SRCU support Ian Rogers
2026-10-17 5:45 ` [PATCH v1 1/5] perf lock contention: Share common BPF and lock type definitions in lock_data.h Ian Rogers
@ 2026-10-17 5:45 ` Ian Rogers
2026-10-17 6:09 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 3/5] perf test: Add tests for lock_held() BPF " Ian Rogers
` (2 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Ian Rogers @ 2026-10-17 5:45 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Ian Rogers, Adrian Hunter,
James Clark, linux-perf-users, linux-kernel, rcu
Add BPF sample filter predicates lock_held(<lock>),
lock_held_read(<lock>) / lock_held_shared(<lock>), and
lock_held_write(<lock>) / lock_held_exclusive(<lock>) (as well as ':R'
and ':W' suffixes on <lock>) to allow recording perf event samples only
when a specified kernel lock is held (or not held with '!').
Supported <lock> specifications match perf lock contention:
- Global/static kernel lock symbols (with or without '&', e.g.
lock_held(&cgroup_mutex) or lock_held(cgroup_mutex))
- Hexadecimal kernel lock addresses
- Slab cache names prefixed with '&' (e.g. lock_held(&dentry),
lock_held_read(&dentry))
- Per-process and semi-global locks: mmap_lock, siglock, rq_lock,
zone_lock
- Lock types and flags: mutex, spinlock, rwsem, rwsem:R, rwsem:W,
rwlock, rwlock:R, rwlock:W, rt-mutex, percpu-rwsem, pcpu-sem:W,
semaphore
Supported lock hold modes by lock family:
- Both read/shared and write/exclusive: rwsem (including mmap_lock)
and rwlock.
- Write/exclusive only: mutex, spinlock (including siglock, rq_lock,
zone_lock), rt-mutex, semaphore, and percpu-rwsem (percpu_down_write()
/ percpu_up_write(); fast-path percpu_down_read() / percpu_up_read()
are inlined per-CPU counter operations without function entry points).
When evaluating the filter on sample overflow, perf_sample_filter first
inspects the lock's owner field directly in kernel memory where
possible (for mutexes, rt-mutexes, and write-locked rwsems), and falls
back to a held_locks BPF hash map (tracking separate read_count and
write_count) populated by fentry/fexit hooks on kernel lock/unlock
functions. To minimize tracing overhead, only the BPF hooks for the
requested lock family and mode are autoloaded and attached.
Example usage:
# perf record -a -g -e cycles --filter 'lock_held(&mmap_lock)' \
-- perf bench mem mmap -t 2 -l 100
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.342 MB perf.data (318 samples) ]
# perf report --stdio --no-children
# Overhead Command Shared Object Symbol
# ........ ........ ................. ........................
#
18.24% mem-mmap [kernel.kallsyms] [k] clear_page_erms
|
---clear_page_erms
get_page_from_freelist
__alloc_pages_noprof
folio_alloc_mpol_noprof
vma_alloc_folio_noprof
do_anonymous_page
__handle_mm_fault
handle_mm_fault
do_user_addr_fault
exc_page_fault
asm_exc_page_fault
worker_thread
Read/shared vs write/exclusive holding can also be filtered separately:
# perf record -a -g -e cycles --filter 'lock_held_read(&mmap_lock)' ...
# perf record -a -g -e cycles --filter 'lock_held_write(&mmap_lock)' ...
# perf record -a -g -e cycles --filter 'lock_held(&mmap_lock:W)' ...
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/Documentation/perf-record.txt | 41 +-
tools/perf/util/bpf-filter.c | 934 ++++++++++++++++++-
tools/perf/util/bpf-filter.h | 13 +
tools/perf/util/bpf-filter.l | 31 +-
tools/perf/util/bpf-filter.y | 46 +-
tools/perf/util/bpf_skel/sample-filter.h | 59 ++
tools/perf/util/bpf_skel/sample_filter.bpf.c | 920 ++++++++++++++++++
tools/perf/util/parse-events.c | 3 +-
8 files changed, 2021 insertions(+), 26 deletions(-)
diff --git a/tools/perf/Documentation/perf-record.txt b/tools/perf/Documentation/perf-record.txt
index 2351f7b4f5fc..246ae085744d 100644
--- a/tools/perf/Documentation/perf-record.txt
+++ b/tools/perf/Documentation/perf-record.txt
@@ -204,7 +204,46 @@ OPTIONS
ip, id, tid, pid, cpu, time, addr, period, txn, weight, phys_addr,
code_pgsz, data_pgsz, weight1, weight2, weight3, ins_lat, retire_lat,
p_stage_cyc, mem_op, mem_lvl, mem_snoop, mem_remote, mem_lock,
- mem_dtlb, mem_blk, mem_hops, uid, gid
+ mem_dtlb, mem_blk, mem_hops, uid, gid, cgroup, lock_held(<lock>),
+ lock_held_read(<lock>), lock_held_shared(<lock>),
+ lock_held_write(<lock>), lock_held_exclusive(<lock>)
+
+ The 'lock_held(<lock>)' predicate (or '!lock_held(<lock>)') filters
+ samples based on whether the specified kernel lock is currently held
+ by the kernel thread or CPU. To differentiate between shared/read
+ holding and exclusive/write holding, use 'lock_held_read(<lock>)'
+ (alias 'lock_held_shared(<lock>)') or 'lock_held_write(<lock>)'
+ (alias 'lock_held_exclusive(<lock>)'), or append ':R' or ':W' to
+ <lock> (e.g. 'lock_held(&mmap_lock:R)' or 'lock_held(&mmap_lock:W)').
+ <lock> can be a kernel lock symbol name (with or without a leading
+ '&', e.g. 'lock_held(&cgroup_mutex)' or 'lock_held(cgroup_mutex)'),
+ a hexadecimal lock address, a slab cache name prefixed with '&'
+ (e.g. 'lock_held(&dentry)', 'lock_held_read(&dentry)'), a well-known
+ lock name ('mmap_lock', 'siglock', 'rq_lock', 'zone_lock'), or a lock
+ type ('mutex', 'spinlock', 'rwsem', 'rwsem:R', 'rwsem:W', 'rwlock',
+ 'rwlock:R', 'rwlock:W', 'rt-mutex', 'percpu-rwsem', 'pcpu-sem:W',
+ 'semaphore'). Both read/shared and write/exclusive tracking are
+ supported for 'rwsem' (including 'mmap_lock') and 'rwlock';
+ 'mutex', 'spinlock' (including 'siglock', 'rq_lock', 'zone_lock'),
+ 'rt-mutex', 'semaphore', and 'percpu-rwsem' ('percpu_down_write()')
+ support write/exclusive tracking only.
+
+ Note that 'lock_held(<lock>)' introduces overhead outside of sample
+ overflow handling: in addition to checking the lock's owner field on
+ each sample where possible (such as for mutexes, rt-mutexes, and
+ write-locked rwsems), it attaches BPF fentry/fexit programs to kernel
+ lock acquire and release functions to track held locks (such as
+ spinlocks, rwlocks, reader-locked rwsems, and slab/type filters) in a
+ BPF map. These BPF hooks run on every kernel lock acquisition and
+ release for the attached lock family, even when no perf sample is
+ being taken. To minimize this overhead, only the hooks for the
+ relevant lock family and mode are attached when known (for example,
+ 'lock_held_read(&mmap_lock)' only hooks rwsem read functions,
+ 'lock_held_write(&mmap_lock)' only hooks rwsem write functions,
+ '&cgroup_mutex' only hooks mutex functions, and 'rq_lock' only hooks
+ spinlock functions), whereas slab filters ('&<slab>'), raw hex
+ addresses, or symbols without a recognizable lock-type suffix attach
+ hooks across all lock families (including high-frequency spinlocks).
The <operator> can be one of:
==, !=, >, >=, <, <=, &
diff --git a/tools/perf/util/bpf-filter.c b/tools/perf/util/bpf-filter.c
index 72e83ac1d6a4..35d6f7c63868 100644
--- a/tools/perf/util/bpf-filter.c
+++ b/tools/perf/util/bpf-filter.c
@@ -40,21 +40,35 @@
* The 'dropped' map is to keep how many samples it dropped by the filter and
* it will be reported as lost samples.
*/
+#include <errno.h>
+#include <stdio.h>
#include <stdlib.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
#include <fcntl.h>
+#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <bpf/bpf.h>
+#include <bpf/btf.h>
#include <linux/err.h>
+#include <linux/kernel.h>
#include <linux/list.h>
+#include <linux/string.h>
+#include <linux/zalloc.h>
#include <api/fs/fs.h>
#include <internal/xyarray.h>
#include <perf/threadmap.h>
+#include <symbol/kallsyms.h>
#include "util/cap.h"
#include "util/debug.h"
#include "util/evsel.h"
+#include "util/lock-contention.h"
+#include "util/string2.h"
+#include "util/symbol_conf.h"
#include "util/target.h"
#include "util/thread_map.h"
#include "util/bpf-utils.h"
@@ -63,6 +77,7 @@
#include <util/bpf-filter-flex.h>
#include <util/bpf-filter-bison.h>
+#include "bpf_skel/lock_data.h"
#include "bpf_skel/sample-filter.h"
#include "bpf_skel/sample_filter.skel.h"
@@ -127,7 +142,8 @@ static int check_sample_flags(struct evsel *evsel, struct perf_bpf_filter_expr *
(evsel->core.attr.sample_type & (1 << (expr->term - PBF_TERM_SAMPLE_START))))
return 0;
- if (expr->term == PBF_TERM_UID || expr->term == PBF_TERM_GID) {
+ if (expr->term == PBF_TERM_UID || expr->term == PBF_TERM_GID ||
+ expr->term == PBF_TERM_LOCK_HELD) {
/* Not dependent on the sample_type as computed from a BPF helper. */
return 0;
}
@@ -155,9 +171,713 @@ static int check_sample_flags(struct evsel *evsel, struct perf_bpf_filter_expr *
return -1;
}
-static int get_filter_entries(struct evsel *evsel, struct perf_bpf_filter_entry *entry)
+/*
+ * Program groups for `lock_held(...)` BPF tracing hooks.
+ *
+ * When a `lock_held(...)` filter is specified, only the BPF tracing program
+ * groups required for the requested lock(s) are autoloaded and attached.
+ * If no `lock_held(...)` filter is present, none of these programs are loaded.
+ */
+enum lock_prog_group {
+ LOCK_PROG_MUTEX = 1U << 0,
+ LOCK_PROG_RWSEM_READ = 1U << 1,
+ LOCK_PROG_RWSEM_WRITE = 1U << 2,
+ LOCK_PROG_RWSEM = LOCK_PROG_RWSEM_READ | LOCK_PROG_RWSEM_WRITE,
+ LOCK_PROG_SPIN = 1U << 3,
+ LOCK_PROG_RWLOCK_READ = 1U << 4,
+ LOCK_PROG_RWLOCK_WRITE = 1U << 5,
+ LOCK_PROG_RWLOCK = LOCK_PROG_RWLOCK_READ | LOCK_PROG_RWLOCK_WRITE,
+ LOCK_PROG_RTMUTEX = 1U << 6,
+ LOCK_PROG_SEM = 1U << 7,
+ LOCK_PROG_PERCPU_RWSEM = 1U << 8,
+ LOCK_PROG_ALL = 0x1ffU,
+};
+
+static unsigned int lock_flags_to_prog_group(unsigned int flags)
+{
+ if (flags & LCB_F_MUTEX)
+ return LOCK_PROG_MUTEX;
+ if (flags & LCB_F_RT)
+ return LOCK_PROG_RTMUTEX;
+ if (flags & LCB_F_PERCPU)
+ return LOCK_PROG_PERCPU_RWSEM;
+ if (flags == LCB_F_SPIN)
+ return LOCK_PROG_SPIN;
+ if (flags & LCB_F_SPIN)
+ return (flags & LCB_F_READ) ? LOCK_PROG_RWLOCK_READ : LOCK_PROG_RWLOCK_WRITE;
+ if (flags & LCB_F_READ)
+ return LOCK_PROG_RWSEM_READ;
+ if (flags & LCB_F_WRITE)
+ return LOCK_PROG_RWSEM_WRITE;
+ return LOCK_PROG_SEM;
+}
+
+struct lock_prog_desc {
+ size_t prog_offset;
+ const char *btf_func;
+ unsigned int group;
+};
+
+#define LOCK_FPROG(field, func, grp) \
+ { offsetof(struct sample_filter_bpf, progs.field), func, grp }
+
+static const struct lock_prog_desc lock_progs[] = {
+ /* Mutex */
+ LOCK_FPROG(fexit_mutex_lock, "mutex_lock", LOCK_PROG_MUTEX),
+ LOCK_FPROG(fexit_mutex_lock_nested, "mutex_lock_nested", LOCK_PROG_MUTEX),
+ LOCK_FPROG(fexit_mutex_lock_io, "mutex_lock_io", LOCK_PROG_MUTEX),
+ LOCK_FPROG(fexit_mutex_lock_io_nested, "mutex_lock_io_nested", LOCK_PROG_MUTEX),
+ LOCK_FPROG(fexit_mutex_lock_interruptible, "mutex_lock_interruptible", LOCK_PROG_MUTEX),
+ LOCK_FPROG(fexit_mutex_lock_interruptible_nested, "mutex_lock_interruptible_nested",
+ LOCK_PROG_MUTEX),
+ LOCK_FPROG(fexit_mutex_lock_killable, "mutex_lock_killable", LOCK_PROG_MUTEX),
+ LOCK_FPROG(fexit_mutex_trylock, "mutex_trylock", LOCK_PROG_MUTEX),
+ LOCK_FPROG(fentry_mutex_unlock, "mutex_unlock", LOCK_PROG_MUTEX),
+ /* RW semaphore */
+ LOCK_FPROG(fexit_down_read, "down_read", LOCK_PROG_RWSEM_READ),
+ LOCK_FPROG(fexit_down_read_nested, "down_read_nested", LOCK_PROG_RWSEM_READ),
+ LOCK_FPROG(fexit_down_read_interruptible, "down_read_interruptible", LOCK_PROG_RWSEM_READ),
+ LOCK_FPROG(fexit_down_read_killable, "down_read_killable", LOCK_PROG_RWSEM_READ),
+ LOCK_FPROG(fexit_down_read_killable_nested, "down_read_killable_nested",
+ LOCK_PROG_RWSEM_READ),
+ LOCK_FPROG(fexit_down_read_trylock, "down_read_trylock", LOCK_PROG_RWSEM_READ),
+ LOCK_FPROG(fexit_down_write, "down_write", LOCK_PROG_RWSEM_WRITE),
+ LOCK_FPROG(fexit_down_write_nested, "down_write_nested", LOCK_PROG_RWSEM_WRITE),
+ LOCK_FPROG(fexit_down_write_killable, "down_write_killable", LOCK_PROG_RWSEM_WRITE),
+ LOCK_FPROG(fexit_down_write_killable_nested, "down_write_killable_nested",
+ LOCK_PROG_RWSEM_WRITE),
+ LOCK_FPROG(fexit_down_write_trylock, "down_write_trylock", LOCK_PROG_RWSEM_WRITE),
+ LOCK_FPROG(fexit_downgrade_write, "downgrade_write", LOCK_PROG_RWSEM),
+ LOCK_FPROG(fentry_up_read, "up_read", LOCK_PROG_RWSEM_READ),
+ LOCK_FPROG(fentry_up_write, "up_write", LOCK_PROG_RWSEM_WRITE),
+ /* Spinlock */
+ LOCK_FPROG(fexit_raw_spin_lock, "_raw_spin_lock", LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_lock_nested, "_raw_spin_lock_nested", LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_lock_irq, "_raw_spin_lock_irq", LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_lock_irq_disable, "_raw_spin_lock_irq_disable", LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_lock_irqsave, "_raw_spin_lock_irqsave", LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_lock_irqsave_nested, "_raw_spin_lock_irqsave_nested",
+ LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_lock_bh, "_raw_spin_lock_bh", LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_trylock, "_raw_spin_trylock", LOCK_PROG_SPIN),
+ LOCK_FPROG(fexit_raw_spin_trylock_bh, "_raw_spin_trylock_bh", LOCK_PROG_SPIN),
+ LOCK_FPROG(fentry_raw_spin_unlock, "_raw_spin_unlock", LOCK_PROG_SPIN),
+ LOCK_FPROG(fentry_raw_spin_unlock_irq, "_raw_spin_unlock_irq", LOCK_PROG_SPIN),
+ LOCK_FPROG(fentry_raw_spin_unlock_irq_enable, "_raw_spin_unlock_irq_enable",
+ LOCK_PROG_SPIN),
+ LOCK_FPROG(fentry_raw_spin_unlock_irqrestore, "_raw_spin_unlock_irqrestore",
+ LOCK_PROG_SPIN),
+ LOCK_FPROG(fentry_raw_spin_unlock_bh, "_raw_spin_unlock_bh", LOCK_PROG_SPIN),
+ /* RWlock */
+ LOCK_FPROG(fexit_raw_read_lock, "_raw_read_lock", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fexit_raw_read_lock_irq, "_raw_read_lock_irq", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fexit_raw_read_lock_irqsave, "_raw_read_lock_irqsave", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fexit_raw_read_lock_bh, "_raw_read_lock_bh", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fexit_raw_read_trylock, "_raw_read_trylock", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fentry_raw_read_unlock, "_raw_read_unlock", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fentry_raw_read_unlock_irq, "_raw_read_unlock_irq", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fentry_raw_read_unlock_irqrestore, "_raw_read_unlock_irqrestore",
+ LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fentry_raw_read_unlock_bh, "_raw_read_unlock_bh", LOCK_PROG_RWLOCK_READ),
+ LOCK_FPROG(fexit_raw_write_lock, "_raw_write_lock", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fexit_raw_write_lock_nested, "_raw_write_lock_nested", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fexit_raw_write_lock_irq, "_raw_write_lock_irq", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fexit_raw_write_lock_irqsave, "_raw_write_lock_irqsave", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fexit_raw_write_lock_bh, "_raw_write_lock_bh", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fexit_raw_write_trylock, "_raw_write_trylock", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fentry_raw_write_unlock, "_raw_write_unlock", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fentry_raw_write_unlock_irq, "_raw_write_unlock_irq", LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fentry_raw_write_unlock_irqrestore, "_raw_write_unlock_irqrestore",
+ LOCK_PROG_RWLOCK_WRITE),
+ LOCK_FPROG(fentry_raw_write_unlock_bh, "_raw_write_unlock_bh", LOCK_PROG_RWLOCK_WRITE),
+ /* RT-Mutex */
+ LOCK_FPROG(fexit_rt_mutex_lock, "rt_mutex_lock", LOCK_PROG_RTMUTEX),
+ LOCK_FPROG(fexit_rt_mutex_lock_nested, "rt_mutex_lock_nested", LOCK_PROG_RTMUTEX),
+ LOCK_FPROG(fexit_rt_mutex_lock_interruptible, "rt_mutex_lock_interruptible",
+ LOCK_PROG_RTMUTEX),
+ LOCK_FPROG(fexit_rt_mutex_lock_killable, "rt_mutex_lock_killable", LOCK_PROG_RTMUTEX),
+ LOCK_FPROG(fexit_rt_mutex_trylock, "rt_mutex_trylock", LOCK_PROG_RTMUTEX),
+ LOCK_FPROG(fentry_rt_mutex_unlock, "rt_mutex_unlock", LOCK_PROG_RTMUTEX),
+ /* Semaphore */
+ LOCK_FPROG(fexit_down, "down", LOCK_PROG_SEM),
+ LOCK_FPROG(fexit_down_interruptible, "down_interruptible", LOCK_PROG_SEM),
+ LOCK_FPROG(fexit_down_killable, "down_killable", LOCK_PROG_SEM),
+ LOCK_FPROG(fexit_down_timeout, "down_timeout", LOCK_PROG_SEM),
+ LOCK_FPROG(fexit_down_trylock, "down_trylock", LOCK_PROG_SEM),
+ LOCK_FPROG(fentry_up, "up", LOCK_PROG_SEM),
+ /* Percpu-RWsem */
+ LOCK_FPROG(fexit_percpu_down_write, "percpu_down_write", LOCK_PROG_PERCPU_RWSEM),
+ LOCK_FPROG(fentry_percpu_up_write, "percpu_up_write", LOCK_PROG_PERCPU_RWSEM),
+};
+
+#undef LOCK_FPROG
+
+static int split_lock_name_mode(const char *name, int expr_part,
+ char *base_buf, size_t buf_sz,
+ bool *has_amp, unsigned int *mode_out)
+{
+ const char *bare;
+ size_t len;
+ unsigned int expr_mode = expr_part & PBF_LOCK_MODE_MASK;
+ unsigned int suffix_mode = 0;
+
+ if (!name || !*name)
+ return -EINVAL;
+
+ *has_amp = (name[0] == '&');
+ bare = *has_amp ? name + 1 : name;
+ len = strlen(bare);
+ if (len == 0)
+ return -EINVAL;
+
+ if (len > 2 && !strcmp(bare + len - 2, ":R")) {
+ suffix_mode = PBF_LOCK_MODE_READ;
+ len -= 2;
+ } else if (len > 2 && !strcmp(bare + len - 2, ":W")) {
+ suffix_mode = PBF_LOCK_MODE_WRITE;
+ len -= 2;
+ }
+
+ if (len == 0 || len >= buf_sz)
+ return -EINVAL;
+
+ if (expr_mode && expr_mode != PBF_LOCK_MODE_ANY &&
+ suffix_mode && expr_mode != suffix_mode)
+ return -EINVAL;
+
+ memcpy(base_buf, bare, len);
+ base_buf[len] = '\0';
+
+ if (suffix_mode)
+ *mode_out = suffix_mode;
+ else if (expr_mode)
+ *mode_out = expr_mode;
+ else
+ *mode_out = PBF_LOCK_MODE_ANY;
+
+ return 0;
+}
+
+/*
+ * Parse a lock type name or flags name (e.g. "mutex", "rwsem", "rwsem:R",
+ * "spinlock", "percpu-rwsem").
+ *
+ * A lock type name may correspond to up to two flag combinations (e.g.
+ * "rwsem" matches both `LCB_F_READ` and `LCB_F_WRITE`). These are packed
+ * into `*encoded_pair` as `flag1 | (flag2 << 8)` (with 0xff if `flag2` is
+ * unused) for O(1) lookup in BPF, and accumulated as a bitmask in `*type_mask`.
+ */
+static bool parse_lock_type_name(const char *name, unsigned int mode,
+ struct sample_filter_bpf *skel,
+ u64 *encoded_pair, unsigned int *prog_groups)
+{
+ bool found = false;
+ u32 flag1 = 0xff, flag2 = 0xff;
+ unsigned int groups = 0;
+ const char *lookup = !strcmp(name, "pcpu-sem") ? "percpu-rwsem" : name;
+
+ for (size_t i = 0; i < ARRAY_SIZE(lock_type_table); i++) {
+ u32 f = lock_type_table[i].flags;
+ bool is_read = (f & LCB_F_READ) != 0;
+
+ if (strcmp(lock_type_table[i].lock_name, lookup) &&
+ strcmp(lock_type_table[i].flags_name, name))
+ continue;
+
+ /*
+ * percpu_down_read() / percpu_up_read() fast paths are inlined
+ * per-CPU counter increments without function entry points, so
+ * only writer holding (percpu_down_write / percpu_up_write) can
+ * be tracked for percpu-rwsem. rwlock-rt is also not hooked.
+ */
+ if ((f & LCB_F_PERCPU) && is_read)
+ continue;
+ if ((f & LCB_F_RT) && (f & (LCB_F_READ | LCB_F_WRITE)))
+ continue;
+
+ if (is_read && !(mode & PBF_LOCK_MODE_READ))
+ continue;
+ if (!is_read && !(mode & PBF_LOCK_MODE_WRITE))
+ continue;
+
+ if (!found)
+ flag1 = f;
+ else if (f != flag1)
+ flag2 = f;
+ if (skel && f < LCB_F_TYPE_MAX) {
+ skel->bss->has_lock_type = 1;
+ skel->bss->lock_type_filter[f] = 1;
+ }
+ groups |= lock_flags_to_prog_group(f);
+ found = true;
+ }
+
+ if (!found)
+ return false;
+
+ if (encoded_pair)
+ *encoded_pair = flag1 | ((u64)flag2 << 8);
+ if (prog_groups)
+ *prog_groups |= groups;
+ return true;
+}
+
+struct lock_sym_cb_args {
+ const char *name;
+ u64 addr;
+ bool found;
+};
+
+static int find_lock_sym_cb(void *arg, const char *name,
+ char type __maybe_unused, u64 start)
+{
+ struct lock_sym_cb_args *args = arg;
+
+ if (!strcmp(name, args->name)) {
+ args->found = true;
+ args->addr = start;
+ if (start != 0)
+ return 1;
+ }
+ return 0;
+}
+
+static int find_kernel_lock_sym(const char *name, u64 *addr)
{
- int i = 0;
+ struct lock_sym_cb_args args = {
+ .name = name,
+ };
+ const char *kallsyms_filename = "/proc/kallsyms";
+
+ if (symbol_conf.kallsyms_name != NULL)
+ kallsyms_filename = symbol_conf.kallsyms_name;
+
+ if (kallsyms__parse(kallsyms_filename, &args, find_lock_sym_cb) < 0)
+ return -ENOENT;
+
+ if (!args.found)
+ return -ENOENT;
+
+ if (args.addr == 0) {
+ pr_err("Error: Kernel symbol '%s' address is 0 (check permissions / kptr_restrict)\n",
+ name);
+ return -EACCES;
+ }
+
+ *addr = args.addr;
+ return 0;
+}
+
+static bool parse_lock_hex_addr(const char *str, u64 *addr)
+{
+ char *end;
+ unsigned long long val;
+
+ if (!str || !*str)
+ return false;
+
+ errno = 0;
+ val = strtoull(str, &end, 16);
+ if (*end != '\0' || errno != 0 || val == 0)
+ return false;
+
+ *addr = val;
+ return true;
+}
+
+static void init_numa_data(struct sample_filter_bpf *skel, struct btf *btf)
+{
+ u64 addr = 0;
+ char *buf = NULL, *p;
+ size_t len;
+ long last = -1;
+ int ret;
+
+ if (!btf)
+ return;
+
+ ret = btf__find_by_name_kind(btf, "zone", BTF_KIND_STRUCT);
+ if (ret < 0)
+ return;
+
+ ret = btf__resolve_size(btf, ret);
+ if (ret < 0)
+ return;
+
+ skel->rodata->sizeof_zone = ret;
+
+ if (find_kernel_lock_sym("contig_page_data", &addr) == 0) {
+ skel->rodata->contig_page_data_addr = addr;
+ return;
+ }
+
+ if (find_kernel_lock_sym("node_data", &addr) == 0)
+ skel->rodata->node_data_addr = addr;
+
+ if (sysfs__read_str("devices/system/node/online", &buf, &len) >= 0) {
+ p = buf;
+ while (p && *p) {
+ char *end;
+
+ last = strtol(p, &end, 0);
+ if (end == p)
+ break;
+ p = end;
+ if (*p == ',' || *p == '-' || *p == '\n')
+ p++;
+ }
+ skel->rodata->nr_nodes = last + 1;
+ free(buf);
+ }
+}
+
+/*
+ * Enable or disable autoload for each lock-tracking BPF program in the
+ * skeleton before `sample_filter_bpf__load()` is called.
+ *
+ * Each program is enabled only if its group is in `needed_groups` and its
+ * target kernel function or tracepoint exists in the running kernel's BTF.
+ */
+static int configure_lock_progs(struct sample_filter_bpf *skel, struct btf *btf,
+ unsigned int needed_groups, bool needs_lock_syms,
+ bool needs_slab_iter)
+{
+ unsigned int enabled_progs = 0;
+
+ for (size_t i = 0; i < ARRAY_SIZE(lock_progs); i++) {
+ struct bpf_program **progp = (void *)skel + lock_progs[i].prog_offset;
+ struct bpf_program *prog = *progp;
+ bool enable = false;
+
+ if ((needed_groups & lock_progs[i].group) && btf &&
+ lock_progs[i].btf_func) {
+ if (btf__find_by_name_kind(btf, lock_progs[i].btf_func,
+ BTF_KIND_FUNC) > 0)
+ enable = true;
+ }
+
+ if (enable)
+ enabled_progs++;
+ bpf_program__set_autoload(prog, enable);
+ }
+
+ bpf_program__set_autoload(skel->progs.collect_lock_syms, needs_lock_syms);
+ if (needs_slab_iter && btf &&
+ btf__find_by_name_kind(btf, "bpf_iter__kmem_cache", BTF_KIND_STRUCT) > 0) {
+ bpf_program__set_autoload(skel->progs.slab_cache_iter, true);
+ } else {
+ bpf_program__set_autoload(skel->progs.slab_cache_iter, false);
+ }
+
+ if (needed_groups && !enabled_progs) {
+ pr_err("Error: Kernel BTF lacks required lock tracing entry points\n");
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+/*
+ * Pre-scan a single filter expression prior to loading the BPF skeleton in
+ * order to determine which BPF maps need to be sized and which lock tracing
+ * program groups need to be autoloaded.
+ */
+static void prescan_one_lock_expr(struct perf_bpf_filter_expr *expr,
+ bool *has_lock_filter,
+ unsigned int *prog_groups,
+ bool *needs_lock_syms,
+ bool *needs_slab_iter)
+{
+ char base[128];
+ bool has_amp;
+ unsigned int mode;
+ u64 dummy;
+
+ if (expr->term != PBF_TERM_LOCK_HELD || !expr->lock_name)
+ return;
+
+ *has_lock_filter = true;
+ if (split_lock_name_mode(expr->lock_name, expr->part, base,
+ sizeof(base), &has_amp, &mode) < 0)
+ return;
+
+ if (!strcmp(base, "mmap_lock") || !strcmp(base, "mmap_sem")) {
+ if (mode & PBF_LOCK_MODE_READ)
+ *prog_groups |= LOCK_PROG_RWSEM_READ;
+ if (mode & PBF_LOCK_MODE_WRITE)
+ *prog_groups |= LOCK_PROG_RWSEM_WRITE;
+ return;
+ }
+ if (!strcmp(base, "siglock")) {
+ *prog_groups |= LOCK_PROG_SPIN;
+ return;
+ }
+ if (!strcmp(base, "rq_lock") || !strcmp(base, "zone_lock")) {
+ *prog_groups |= LOCK_PROG_SPIN;
+ *needs_lock_syms = true;
+ return;
+ }
+ if (!has_amp && parse_lock_type_name(base, mode, NULL, NULL, prog_groups))
+ return;
+
+ if (!has_amp && parse_lock_hex_addr(base, &dummy)) {
+ *prog_groups |= LOCK_PROG_ALL;
+ return;
+ }
+
+ if (has_amp)
+ *needs_slab_iter = true;
+
+ if (strtailcmp(base, "rt_mutex") == 0 || strtailcmp(base, "rtmutex") == 0) {
+ *prog_groups |= LOCK_PROG_RTMUTEX;
+ } else if (strtailcmp(base, "mutex") == 0) {
+ *prog_groups |= LOCK_PROG_MUTEX;
+ } else if (strtailcmp(base, "percpu_rwsem") == 0 ||
+ strtailcmp(base, "pcpu_rwsem") == 0) {
+ *prog_groups |= LOCK_PROG_PERCPU_RWSEM;
+ } else if (strtailcmp(base, "rwsem") == 0) {
+ if (mode & PBF_LOCK_MODE_READ)
+ *prog_groups |= LOCK_PROG_RWSEM_READ;
+ if (mode & PBF_LOCK_MODE_WRITE)
+ *prog_groups |= LOCK_PROG_RWSEM_WRITE;
+ } else if (strtailcmp(base, "spinlock") == 0) {
+ *prog_groups |= LOCK_PROG_SPIN;
+ } else if (strtailcmp(base, "rwlock") == 0) {
+ if (mode & PBF_LOCK_MODE_READ)
+ *prog_groups |= LOCK_PROG_RWLOCK_READ;
+ if (mode & PBF_LOCK_MODE_WRITE)
+ *prog_groups |= LOCK_PROG_RWLOCK_WRITE;
+ } else {
+ *prog_groups |= LOCK_PROG_ALL;
+ }
+
+ if (!has_amp && find_kernel_lock_sym(base, &dummy) != 0)
+ *needs_slab_iter = true;
+}
+
+static void prescan_lock_filters(struct evsel *evsel, bool *has_lock_filter,
+ unsigned int *prog_groups,
+ bool *needs_lock_syms,
+ bool *needs_slab_iter)
+{
+ struct perf_bpf_filter_expr *expr, *group;
+
+ *has_lock_filter = false;
+ *prog_groups = 0;
+ *needs_lock_syms = false;
+ *needs_slab_iter = false;
+
+ list_for_each_entry(expr, &evsel->bpf_filters, list) {
+ prescan_one_lock_expr(expr, has_lock_filter, prog_groups,
+ needs_lock_syms, needs_slab_iter);
+ if (expr->op == PBF_OP_GROUP_BEGIN) {
+ list_for_each_entry(group, &expr->groups, list) {
+ prescan_one_lock_expr(group, has_lock_filter,
+ prog_groups, needs_lock_syms,
+ needs_slab_iter);
+ }
+ }
+ }
+}
+
+static void run_slab_cache_iter(struct sample_filter_bpf *skel)
+{
+ struct bpf_link *link;
+ int fd;
+ char buf[256];
+
+ if (!bpf_program__autoload(skel->progs.slab_cache_iter))
+ return;
+
+ link = bpf_program__attach_iter(skel->progs.slab_cache_iter, NULL);
+ if (!link || IS_ERR(link)) {
+ pr_debug("cannot attach slab cache iter\n");
+ return;
+ }
+
+ fd = bpf_iter_create(bpf_link__fd(link));
+ if (fd >= 0) {
+ while (read(fd, buf, sizeof(buf)) > 0)
+ continue;
+ close(fd);
+ }
+
+ bpf_link__destroy(link);
+}
+
+static bool find_slab_cache(struct sample_filter_bpf *skel, const char *name,
+ u64 *kmem_cache_addr)
+{
+ int cache_fd;
+ u64 key, *prev_key = NULL;
+
+ if (!skel || !bpf_program__autoload(skel->progs.slab_cache_iter))
+ return false;
+
+ cache_fd = bpf_map__fd(skel->maps.slab_caches);
+ while (!bpf_map_get_next_key(cache_fd, prev_key, &key)) {
+ struct slab_cache_data data;
+
+ if (bpf_map_lookup_elem(cache_fd, &key, &data) < 0)
+ break;
+
+ if (!strcmp(data.name, name)) {
+ *kmem_cache_addr = key;
+ return true;
+ }
+ prev_key = &key;
+ }
+ return false;
+}
+
+/*
+ * Resolve a `lock_held(<name>)` filter expression into `entry->part`
+ * (`enum perf_bpf_lock_target_kind`) and `entry->target`, and populate
+ * the corresponding BPF filter maps and BSS control flags.
+ *
+ * Resolution order:
+ * 1. Well-known per-process / semi-global locks: `mmap_lock`, `siglock`,
+ * `rq_lock`, `zone_lock` (with or without leading '&').
+ * 2. Lock types / flags (when no leading '&'): `mutex`, `spinlock`,
+ * `rwsem`, `rwsem:R`, `rwsem:W`, `rwlock`, `percpu-rwsem`,
+ * `semaphore`, etc.
+ * 3. Hexadecimal lock address (when no leading '&').
+ * 4. Slab cache name (when prefixed with '&', e.g. `&dentry`).
+ * 5. Global/static kernel lock symbol in kallsyms (with or without leading '&',
+ * e.g. `&cgroup_mutex` or `cgroup_mutex`).
+ * 6. Slab cache name without leading '&' fallback.
+ */
+static int resolve_lock_expr(struct sample_filter_bpf *skel,
+ struct perf_bpf_filter_expr *expr,
+ struct perf_bpf_filter_entry *entry)
+{
+ const char *name = expr->lock_name;
+ char base[128];
+ bool has_amp;
+ unsigned int mode;
+ u64 addr = 0, encoded_pair = 0;
+ u8 one = 1;
+ int ret;
+
+ if (split_lock_name_mode(name, expr->part, base, sizeof(base),
+ &has_amp, &mode) < 0) {
+ pr_err("Error: Invalid kernel lock filter '%s'\n", name ?: "");
+ return -EINVAL;
+ }
+
+ if (!strcmp(base, "mmap_lock") || !strcmp(base, "mmap_sem")) {
+ entry->part = PBF_LOCK_MMAP | mode;
+ entry->target = 0;
+ if (skel)
+ skel->bss->has_mmap_lock = 1;
+ return 0;
+ }
+
+ if (!strcmp(base, "siglock")) {
+ if (!(mode & PBF_LOCK_MODE_WRITE)) {
+ pr_err("Error: Lock '%s' is exclusive and does not support read/shared mode\n",
+ name);
+ return -EINVAL;
+ }
+ entry->part = PBF_LOCK_SIGHAND | mode;
+ entry->target = 0;
+ if (skel)
+ skel->bss->has_siglock = 1;
+ return 0;
+ }
+
+ if (!strcmp(base, "rq_lock")) {
+ if (!(mode & PBF_LOCK_MODE_WRITE)) {
+ pr_err("Error: Lock '%s' is exclusive and does not support read/shared mode\n",
+ name);
+ return -EINVAL;
+ }
+ entry->part = PBF_LOCK_CLASS | mode;
+ entry->target = LOCK_CLASS_RQLOCK;
+ if (skel)
+ skel->bss->has_lock_class = 1;
+ return 0;
+ }
+
+ if (!strcmp(base, "zone_lock")) {
+ if (!(mode & PBF_LOCK_MODE_WRITE)) {
+ pr_err("Error: Lock '%s' is exclusive and does not support read/shared mode\n",
+ name);
+ return -EINVAL;
+ }
+ entry->part = PBF_LOCK_CLASS | mode;
+ entry->target = LOCK_CLASS_ZONE_LOCK;
+ if (skel)
+ skel->bss->has_lock_class = 1;
+ return 0;
+ }
+
+ if (!has_amp && parse_lock_type_name(base, mode, skel, &encoded_pair, NULL)) {
+ entry->part = PBF_LOCK_TYPE | mode;
+ entry->target = encoded_pair;
+ return 0;
+ }
+
+ if (!has_amp && parse_lock_type_name(base, PBF_LOCK_MODE_ANY, NULL, NULL, NULL)) {
+ pr_err("Error: Lock type '%s' does not support the requested read/write mode\n",
+ name);
+ return -EINVAL;
+ }
+
+ if (!has_amp && parse_lock_hex_addr(base, &addr)) {
+ entry->part = PBF_LOCK_ADDR | mode;
+ entry->target = addr;
+ if (skel) {
+ int fd = bpf_map__fd(skel->maps.lock_addr_filter);
+
+ bpf_map_update_elem(fd, &addr, &one, BPF_ANY);
+ skel->bss->has_lock_addr = 1;
+ }
+ return 0;
+ }
+
+ if (has_amp && find_slab_cache(skel, base, &addr)) {
+ int fd = bpf_map__fd(skel->maps.slab_filter);
+
+ entry->part = PBF_LOCK_SLAB | mode;
+ entry->target = addr;
+ bpf_map_update_elem(fd, &addr, &one, BPF_ANY);
+ skel->bss->has_lock_slab = 1;
+ return 0;
+ }
+
+ ret = find_kernel_lock_sym(base, &addr);
+ if (ret == 0) {
+ entry->part = PBF_LOCK_ADDR | mode;
+ entry->target = addr;
+ if (skel) {
+ int fd = bpf_map__fd(skel->maps.lock_addr_filter);
+
+ bpf_map_update_elem(fd, &addr, &one, BPF_ANY);
+ skel->bss->has_lock_addr = 1;
+ }
+ return 0;
+ }
+ if (ret == -EACCES)
+ return ret;
+
+ if (!has_amp && find_slab_cache(skel, base, &addr)) {
+ int fd = bpf_map__fd(skel->maps.slab_filter);
+
+ entry->part = PBF_LOCK_SLAB | mode;
+ entry->target = addr;
+ bpf_map_update_elem(fd, &addr, &one, BPF_ANY);
+ skel->bss->has_lock_slab = 1;
+ return 0;
+ }
+
+ pr_err("Error: Cannot resolve kernel lock '%s'\n", name);
+ return -EINVAL;
+}
+
+static int get_filter_entries(struct evsel *evsel, struct sample_filter_bpf *skel,
+ struct perf_bpf_filter_entry *entry)
+{
+ int i = 0, ret;
struct perf_bpf_filter_expr *expr;
list_for_each_entry(expr, &evsel->bpf_filters, list) {
@@ -171,6 +891,12 @@ static int get_filter_entries(struct evsel *evsel, struct perf_bpf_filter_entry
entry[i].part = expr->part;
entry[i].term = expr->term;
entry[i].value = expr->val;
+ entry[i].target = 0;
+ if (expr->term == PBF_TERM_LOCK_HELD) {
+ ret = resolve_lock_expr(skel, expr, &entry[i]);
+ if (ret < 0)
+ return ret;
+ }
i++;
if (expr->op == PBF_OP_GROUP_BEGIN) {
@@ -184,6 +910,12 @@ static int get_filter_entries(struct evsel *evsel, struct perf_bpf_filter_entry
entry[i].part = group->part;
entry[i].term = group->term;
entry[i].value = group->val;
+ entry[i].target = 0;
+ if (group->term == PBF_TERM_LOCK_HELD) {
+ ret = resolve_lock_expr(skel, group, &entry[i]);
+ if (ret < 0)
+ return ret;
+ }
i++;
}
@@ -423,10 +1155,13 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
{
int i, x, y, fd, ret;
struct sample_filter_bpf *skel = NULL;
+ struct btf *btf = NULL;
struct bpf_program *prog;
struct bpf_link *link;
struct perf_bpf_filter_entry *entry;
bool needs_idx_hash = !target__has_cpu(target);
+ bool has_lock_filter = false, needs_lock_syms = false, needs_slab_iter = false;
+ unsigned int prog_groups = 0;
#if LIBBPF_CURRENT_VERSION_GEQ(1, 7)
DECLARE_LIBBPF_OPTS(bpf_perf_event_opts, pe_opts,
.dont_enable = true);
@@ -438,15 +1173,24 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
if (entry == NULL)
return -1;
- ret = get_filter_entries(evsel, entry);
- if (ret < 0) {
- pr_err("Failed to process filter entries\n");
- goto err;
- }
+ prescan_lock_filters(evsel, &has_lock_filter, &prog_groups,
+ &needs_lock_syms, &needs_slab_iter);
- if (needs_idx_hash && geteuid() != 0) {
+ if (needs_idx_hash && geteuid() != 0 && !perf_cap__capable(CAP_BPF)) {
int zero = 0;
+ if (has_lock_filter) {
+ pr_err("Error: lock_held BPF filter requires CAP_BPF capability\n");
+ ret = -EPERM;
+ goto err;
+ }
+
+ ret = get_filter_entries(evsel, NULL, entry);
+ if (ret < 0) {
+ pr_err("Failed to process filter entries\n");
+ goto err;
+ }
+
/* The filters map is shared among other processes */
ret = create_idx_hash(evsel, entry);
if (ret < 0)
@@ -484,13 +1228,69 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
return 0;
}
- skel = sample_filter_bpf__open_and_load();
+ skel = sample_filter_bpf__open();
if (!skel) {
+ ret = -errno;
+ pr_err("Failed to open perf sample-filter BPF skeleton\n");
+ goto err;
+ }
+
+ if (has_lock_filter) {
+ btf = btf__load_vmlinux_btf();
+ bpf_map__set_max_entries(skel->maps.held_locks, MAX_ENTRIES);
+ bpf_map__set_max_entries(skel->maps.lock_addr_filter, MAX_FILTERS);
+ if (needs_slab_iter) {
+ bpf_map__set_max_entries(skel->maps.slab_filter, MAX_FILTERS);
+ bpf_map__set_max_entries(skel->maps.slab_caches, MAX_ENTRIES);
+ }
+ if (needs_lock_syms) {
+ bpf_map__set_max_entries(skel->maps.lock_syms, MAX_ENTRIES);
+ init_numa_data(skel, btf);
+ }
+ ret = configure_lock_progs(skel, btf, prog_groups, needs_lock_syms,
+ needs_slab_iter);
+ if (ret < 0)
+ goto err;
+ } else {
+ configure_lock_progs(skel, NULL, 0, false, false);
+ }
+
+ if (sample_filter_bpf__load(skel) < 0) {
ret = -errno;
pr_err("Failed to load perf sample-filter BPF skeleton\n");
goto err;
}
+ if (has_lock_filter && needs_slab_iter)
+ run_slab_cache_iter(skel);
+
+ ret = get_filter_entries(evsel, skel, entry);
+ if (ret < 0) {
+ pr_err("Failed to process filter entries\n");
+ goto err;
+ }
+
+ if (has_lock_filter) {
+ if (needs_lock_syms && skel->bss->has_lock_class) {
+ DECLARE_LIBBPF_OPTS(bpf_test_run_opts, opts,
+ .flags = BPF_F_TEST_RUN_ON_CPU,
+ );
+ int prog_fd = bpf_program__fd(skel->progs.collect_lock_syms);
+
+ bpf_prog_test_run_opts(prog_fd, &opts);
+ }
+ bpf_program__set_autoattach(skel->progs.perf_sample_filter, false);
+ bpf_program__set_autoattach(skel->progs.collect_lock_syms, false);
+ bpf_program__set_autoattach(skel->progs.slab_cache_iter, false);
+
+ ret = sample_filter_bpf__attach(skel);
+ if (ret < 0) {
+ pr_err("Failed to attach lock sample-filter BPF programs\n");
+ goto err;
+ }
+ skel->bss->lock_enabled = 1;
+ }
+
i = 0;
fd = bpf_map__fd(skel->maps.filters);
@@ -513,11 +1313,13 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
}
}
}
+ btf__free(btf);
free(entry);
evsel->bpf_skel = skel;
return 0;
err:
+ btf__free(btf);
free(entry);
if (!list_empty(&pinned_filters)) {
struct pinned_filter_idx *pfi, *tmp;
@@ -532,6 +1334,21 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
return ret;
}
+void perf_bpf_filter_expr__delete(struct perf_bpf_filter_expr *expr)
+{
+ struct perf_bpf_filter_expr *group, *tmp;
+
+ if (!expr)
+ return;
+
+ list_for_each_entry_safe(group, tmp, &expr->groups, list) {
+ list_del(&group->list);
+ perf_bpf_filter_expr__delete(group);
+ }
+ zfree(&expr->lock_name);
+ free(expr);
+}
+
int perf_bpf_filter__destroy(struct evsel *evsel)
{
struct perf_bpf_filter_expr *expr, *tmp;
@@ -539,7 +1356,7 @@ int perf_bpf_filter__destroy(struct evsel *evsel)
list_for_each_entry_safe(expr, tmp, &evsel->bpf_filters, list) {
list_del(&expr->list);
- free(expr);
+ perf_bpf_filter_expr__delete(expr);
}
sample_filter_bpf__destroy(evsel->bpf_skel);
@@ -591,7 +1408,7 @@ struct perf_bpf_filter_expr *perf_bpf_filter_expr__new(enum perf_bpf_filter_term
{
struct perf_bpf_filter_expr *expr;
- expr = malloc(sizeof(*expr));
+ expr = zalloc(sizeof(*expr));
if (expr != NULL) {
expr->term = term;
expr->part = part;
@@ -602,6 +1419,28 @@ struct perf_bpf_filter_expr *perf_bpf_filter_expr__new(enum perf_bpf_filter_term
return expr;
}
+struct perf_bpf_filter_expr *perf_bpf_filter_expr__new_lock(const char *lock_name,
+ int mode,
+ enum perf_bpf_filter_op op,
+ unsigned long val)
+{
+ struct perf_bpf_filter_expr *expr;
+
+ if (!lock_name || !*lock_name)
+ return NULL;
+
+ expr = perf_bpf_filter_expr__new(PBF_TERM_LOCK_HELD, mode, op, val);
+ if (!expr)
+ return NULL;
+
+ expr->lock_name = strdup(lock_name);
+ if (!expr->lock_name) {
+ free(expr);
+ return NULL;
+ }
+ return expr;
+}
+
static bool check_bpf_filter_capable(void)
{
int fd;
@@ -622,14 +1461,11 @@ static bool check_bpf_filter_capable(void)
return false;
}
-int perf_bpf_filter__parse(struct list_head *expr_head, const char *str)
+int __perf_bpf_filter__parse(struct list_head *expr_head, const char *str)
{
YY_BUFFER_STATE buffer;
int ret;
- if (!check_bpf_filter_capable())
- return -EPERM;
-
buffer = perf_bpf_filter__scan_string(str);
ret = perf_bpf_filter_parse(expr_head);
@@ -641,6 +1477,53 @@ int perf_bpf_filter__parse(struct list_head *expr_head, const char *str)
return ret;
}
+int perf_bpf_filter__parse(struct list_head *expr_head, const char *str)
+{
+ if (!check_bpf_filter_capable())
+ return -EPERM;
+
+ return __perf_bpf_filter__parse(expr_head, str);
+}
+
+static int pin_filter_object(struct sample_filter_bpf *skel, const char *path)
+{
+ char *prog_path = NULL;
+ int ret;
+
+ ret = bpf_object__pin_maps(skel->obj, path);
+ if (ret < 0)
+ return ret;
+
+ if (asprintf(&prog_path, "%s/perf_sample_filter", path) < 0) {
+ bpf_object__unpin_maps(skel->obj, path);
+ return -ENOMEM;
+ }
+
+ ret = bpf_program__pin(skel->progs.perf_sample_filter, prog_path);
+ if (ret < 0)
+ bpf_object__unpin_maps(skel->obj, path);
+
+ free(prog_path);
+ return ret;
+}
+
+static int unpin_filter_object(struct sample_filter_bpf *skel, const char *path)
+{
+ char *prog_path = NULL;
+ int ret, map_ret;
+
+ if (asprintf(&prog_path, "%s/perf_sample_filter", path) < 0) {
+ bpf_object__unpin_maps(skel->obj, path);
+ return -ENOMEM;
+ }
+
+ ret = bpf_program__unpin(skel->progs.perf_sample_filter, prog_path);
+ free(prog_path);
+
+ map_ret = bpf_object__unpin_maps(skel->obj, path);
+ return ret < 0 ? ret : map_ret;
+}
+
int perf_bpf_filter__pin(void)
{
struct sample_filter_bpf *skel;
@@ -654,6 +1537,8 @@ int perf_bpf_filter__pin(void)
goto err;
}
+ configure_lock_progs(skel, NULL, 0, false, false);
+
/* pinned program will use pid-hash */
bpf_map__set_max_entries(skel->maps.filters, MAX_FILTERS);
bpf_map__set_max_entries(skel->maps.event_hash, MAX_EVT_HASH);
@@ -674,7 +1559,7 @@ int perf_bpf_filter__pin(void)
goto err;
}
- ret = bpf_object__pin(skel->obj, path);
+ ret = pin_filter_object(skel, path);
if (ret < 0) {
pr_err("Failed to pin BPF filter objects\n");
goto err;
@@ -683,7 +1568,7 @@ int perf_bpf_filter__pin(void)
/* setup access permissions for the pinned objects */
dir_fd = open(path, O_PATH);
if (dir_fd < 0) {
- bpf_object__unpin(skel->obj, path);
+ unpin_filter_object(skel, path);
ret = dir_fd;
goto err;
}
@@ -727,6 +1612,8 @@ int perf_bpf_filter__pin(void)
err_close:
close(dir_fd);
+ if (ret < 0)
+ unpin_filter_object(skel, path);
err:
free(path);
@@ -740,13 +1627,20 @@ int perf_bpf_filter__unpin(void)
char *path = NULL;
int ret = -1;
- skel = sample_filter_bpf__open_and_load();
+ skel = sample_filter_bpf__open();
if (!skel) {
ret = -errno;
pr_err("Failed to open perf sample-filter BPF skeleton\n");
goto err;
}
+ configure_lock_progs(skel, NULL, 0, false, false);
+ if (sample_filter_bpf__load(skel) < 0) {
+ ret = -errno;
+ pr_err("Failed to load perf sample-filter BPF skeleton\n");
+ goto err;
+ }
+
if (asprintf(&path, "%s/fs/bpf/%s", sysfs__mountpoint(),
PERF_BPF_FILTER_PIN_PATH) < 0) {
ret = -errno;
@@ -754,7 +1648,7 @@ int perf_bpf_filter__unpin(void)
goto err;
}
- ret = bpf_object__unpin(skel->obj, path);
+ ret = unpin_filter_object(skel, path);
err:
free(path);
diff --git a/tools/perf/util/bpf-filter.h b/tools/perf/util/bpf-filter.h
index 818c554b91b2..fda95ff226b3 100644
--- a/tools/perf/util/bpf-filter.h
+++ b/tools/perf/util/bpf-filter.h
@@ -14,6 +14,13 @@ struct perf_bpf_filter_expr {
int part;
enum perf_bpf_filter_term term;
unsigned long val;
+ /*
+ * Raw lock descriptor string for PBF_TERM_LOCK_HELD expressions
+ * (e.g. "&cgroup_mutex", "mmap_lock", "&dentry", "rwsem:R").
+ * Resolved into `entry.part` and `entry.target` during
+ * perf_bpf_filter__prepare().
+ */
+ char *lock_name;
};
struct evsel;
@@ -27,6 +34,12 @@ struct perf_bpf_filter_expr *perf_bpf_filter_expr__new(enum perf_bpf_filter_term
int part,
enum perf_bpf_filter_op op,
unsigned long val);
+struct perf_bpf_filter_expr *perf_bpf_filter_expr__new_lock(const char *lock_name,
+ int mode,
+ enum perf_bpf_filter_op op,
+ unsigned long val);
+void perf_bpf_filter_expr__delete(struct perf_bpf_filter_expr *expr);
+int __perf_bpf_filter__parse(struct list_head *expr_head, const char *str);
int perf_bpf_filter__parse(struct list_head *expr_head, const char *str);
int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target);
int perf_bpf_filter__destroy(struct evsel *evsel);
diff --git a/tools/perf/util/bpf-filter.l b/tools/perf/util/bpf-filter.l
index 1be9df6550fc..b4022110f1e5 100644
--- a/tools/perf/util/bpf-filter.l
+++ b/tools/perf/util/bpf-filter.l
@@ -1,9 +1,12 @@
%option prefix="perf_bpf_filter_"
%option noyywrap
+%x LOCK_ARG
+
%{
#include <stdio.h>
#include <stdlib.h>
+#include <string.h>
#include <linux/perf_event.h>
#include "bpf-filter.h"
@@ -76,7 +79,7 @@ static int path_or_error(void)
num_dec [0-9]+
num_hex 0[Xx][0-9a-fA-F]+
space [ \t]+
-path [^ \t\n,]+
+path [^ \t\n,()!=>&|<]+
ident [_a-zA-Z][_a-zA-Z0-9]+
%%
@@ -116,7 +119,31 @@ mem_hops { return sample_part(PBF_TERM_DATA_SRC, 8); }
uid { return sample(PBF_TERM_UID); }
gid { return sample(PBF_TERM_GID); }
cgroup { return sample_path(PBF_TERM_CGROUP); }
-
+lock_held[ \t]*"(" {
+ BEGIN(LOCK_ARG);
+ perf_bpf_filter_lval.num = PBF_LOCK_MODE_ANY;
+ return BFT_LOCK_HELD;
+ }
+(lock_held_read|lock_held_shared)[ \t]*"(" {
+ BEGIN(LOCK_ARG);
+ perf_bpf_filter_lval.num = PBF_LOCK_MODE_READ;
+ return BFT_LOCK_HELD;
+ }
+(lock_held_write|lock_held_exclusive)[ \t]*"(" {
+ BEGIN(LOCK_ARG);
+ perf_bpf_filter_lval.num = PBF_LOCK_MODE_WRITE;
+ return BFT_LOCK_HELD;
+ }
+
+<LOCK_ARG>{space} { }
+<LOCK_ARG>[^ \t\n()]+ {
+ perf_bpf_filter_lval.lock_name = strdup(perf_bpf_filter_text);
+ return BFT_LOCK_NAME;
+ }
+<LOCK_ARG>")" { BEGIN(INITIAL); return ')'; }
+<LOCK_ARG>.|\n { BEGIN(INITIAL); return BFT_ERROR; }
+
+"!" { return '!'; }
"==" { return operator(PBF_OP_EQ); }
"!=" { return operator(PBF_OP_NEQ); }
">" { return operator(PBF_OP_GT); }
diff --git a/tools/perf/util/bpf-filter.y b/tools/perf/util/bpf-filter.y
index 5a79a8e7a45b..6ee6edef13f7 100644
--- a/tools/perf/util/bpf-filter.y
+++ b/tools/perf/util/bpf-filter.y
@@ -11,6 +11,7 @@
#include <string.h>
#include <linux/compiler.h>
#include <linux/list.h>
+#include <linux/zalloc.h>
#include "bpf-filter.h"
#include "cgroup.h"
@@ -31,6 +32,11 @@ static void perf_bpf_filter_error(struct list_head *expr __maybe_unused,
{
unsigned long num;
char *path;
+ char *lock_name;
+ struct {
+ char *name;
+ int mode;
+ } lock;
struct {
enum perf_bpf_filter_term term;
int part;
@@ -40,12 +46,17 @@ static void perf_bpf_filter_error(struct list_head *expr __maybe_unused,
}
%token BFT_SAMPLE BFT_SAMPLE_PATH BFT_OP BFT_ERROR BFT_NUM BFT_LOGICAL_OR BFT_PATH
+%token BFT_LOCK_HELD BFT_LOCK_NAME
%type <expr> filter_term filter_expr
-%destructor { free ($$); } <expr>
+%destructor { perf_bpf_filter_expr__delete($$); } <expr>
+%destructor { free($$); } <lock_name>
+%destructor { free($$.name); } <lock>
%type <sample> BFT_SAMPLE BFT_SAMPLE_PATH
%type <op> BFT_OP
-%type <num> BFT_NUM
+%type <num> BFT_NUM BFT_LOCK_HELD
%type <path> BFT_PATH
+%type <lock_name> BFT_LOCK_NAME
+%type <lock> lock_term
%%
@@ -105,5 +116,36 @@ BFT_SAMPLE_PATH BFT_OP BFT_PATH
$$ = perf_bpf_filter_expr__new($1.term, $1.part, $2, cgroup_id);
cgroup__put(cgrp);
}
+|
+lock_term
+{
+ $$ = perf_bpf_filter_expr__new_lock($1.name, $1.mode, PBF_OP_EQ, 1);
+ zfree(&$1.name);
+ if (!$$)
+ YYERROR;
+}
+|
+'!' lock_term
+{
+ $$ = perf_bpf_filter_expr__new_lock($2.name, $2.mode, PBF_OP_EQ, 0);
+ zfree(&$2.name);
+ if (!$$)
+ YYERROR;
+}
+|
+lock_term BFT_OP BFT_NUM
+{
+ $$ = perf_bpf_filter_expr__new_lock($1.name, $1.mode, $2, $3);
+ zfree(&$1.name);
+ if (!$$)
+ YYERROR;
+}
+
+lock_term:
+BFT_LOCK_HELD BFT_LOCK_NAME ')'
+{
+ $$.name = $2;
+ $$.mode = $1;
+}
%%
diff --git a/tools/perf/util/bpf_skel/sample-filter.h b/tools/perf/util/bpf_skel/sample-filter.h
index 683fec85e71e..cd711d1d04b6 100644
--- a/tools/perf/util/bpf_skel/sample-filter.h
+++ b/tools/perf/util/bpf_skel/sample-filter.h
@@ -53,14 +53,73 @@ enum perf_bpf_filter_term {
/* Terms computed from BPF helpers. */
PBF_TERM_UID,
PBF_TERM_GID,
+ /*
+ * Kernel lock held predicate (`lock_held(...)`). Evaluates to 1 if the
+ * specified kernel lock is currently held by the running task/CPU, or
+ * 0 otherwise. The lock specification kind is stored in `entry.part`
+ * (`enum perf_bpf_lock_target_kind`) and the resolved target address,
+ * class ID, or type flag encoding is stored in `entry.target`.
+ */
+ PBF_TERM_LOCK_HELD,
};
+/*
+ * Lock target categories for PBF_TERM_LOCK_HELD filters.
+ * Stored in `perf_bpf_filter_entry.part`.
+ */
+enum perf_bpf_lock_target_kind {
+ PBF_LOCK_NONE = 0,
+ /*
+ * Specific global or static lock address (resolved from kallsyms or
+ * given as a hex address). `entry.target` holds the kernel address.
+ */
+ PBF_LOCK_ADDR,
+ /*
+ * Dynamic lock allocated inside a slab cache (`&slab_name`).
+ * `entry.target` holds the kernel `struct kmem_cache *` address.
+ */
+ PBF_LOCK_SLAB,
+ /*
+ * Per-process `current->mm->mmap_lock` (or `mmap_sem`).
+ */
+ PBF_LOCK_MMAP,
+ /*
+ * Per-process `current->sighand->siglock`.
+ */
+ PBF_LOCK_SIGHAND,
+ /*
+ * Semi-global lock class (`rq_lock` or `zone_lock`).
+ * `entry.target` holds `enum lock_class_sym` (LOCK_CLASS_RQLOCK or
+ * LOCK_CLASS_ZONE_LOCK).
+ */
+ PBF_LOCK_CLASS,
+ /*
+ * Lock type filter (`mutex`, `spinlock`, `rwsem`, `rwsem:R`, etc.).
+ * `entry.target` packs up to two `LCB_F_*` flag combinations in the
+ * low two bytes: `flag1 | (flag2 << 8)` (with 0xff indicating unused).
+ */
+ PBF_LOCK_TYPE,
+};
+
+/*
+ * `perf_bpf_filter_entry.part` layout for PBF_TERM_LOCK_HELD:
+ * Bits 0..3: `enum perf_bpf_lock_target_kind`
+ * Bit 4: PBF_LOCK_MODE_READ (match read/shared lock holding)
+ * Bit 5: PBF_LOCK_MODE_WRITE (match write/exclusive lock holding)
+ */
+#define PBF_LOCK_KIND_MASK 0x0fU
+#define PBF_LOCK_MODE_READ (1U << 4)
+#define PBF_LOCK_MODE_WRITE (1U << 5)
+#define PBF_LOCK_MODE_ANY (PBF_LOCK_MODE_READ | PBF_LOCK_MODE_WRITE)
+#define PBF_LOCK_MODE_MASK PBF_LOCK_MODE_ANY
+
/* BPF map entry for filtering */
struct perf_bpf_filter_entry {
enum perf_bpf_filter_op op;
__u32 part; /* sub-sample type info when it has multiple values */
enum perf_bpf_filter_term term;
__u64 value;
+ __u64 target; /* lock address, kmem_cache *, class, or type flags */
};
struct idx_hash_key {
diff --git a/tools/perf/util/bpf_skel/sample_filter.bpf.c b/tools/perf/util/bpf_skel/sample_filter.bpf.c
index e5666d4c1722..fbd2e51815d7 100644
--- a/tools/perf/util/bpf_skel/sample_filter.bpf.c
+++ b/tools/perf/util/bpf_skel/sample_filter.bpf.c
@@ -5,8 +5,47 @@
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
+#include "lock_data.h"
#include "sample-filter.h"
+/*
+ * Key for the `held_locks` BPF hash map tracking currently held kernel locks.
+ *
+ * - `addr_or_id`:
+ * - PBF_LOCK_ADDR: kernel virtual address of the lock instance
+ * - PBF_LOCK_CLASS: `enum lock_class_sym` (LOCK_CLASS_RQLOCK / LOCK_CLASS_ZONE_LOCK)
+ * - PBF_LOCK_SLAB: kernel `struct kmem_cache *` address
+ * - PBF_LOCK_TYPE: `LCB_F_*` lock type flags
+ * - `owner_id`:
+ * - For sleeping locks: kernel task TID (`(__u32)bpf_get_current_pid_tgid()`),
+ * since the holder may sleep and migrate across CPUs while holding the lock.
+ * - For spinning locks (`LCB_F_SPIN` without `LCB_F_MUTEX`) or idle tasks
+ * (`pid == 0`): `0x80000000U | bpf_get_smp_processor_id()`, since preemption
+ * is disabled on the holding CPU and spinlocks such as `rq_lock` may be
+ * acquired by one task and released by the next task across `context_switch()`.
+ * - `kind`: `enum perf_bpf_lock_target_kind`.
+ */
+struct held_lock_key {
+ __u64 addr_or_id;
+ __u32 owner_id;
+ __u32 kind;
+};
+
+/*
+ * Value for `held_locks` entries.
+ * `read_count` and `write_count` separately track shared/read (`LCB_F_READ`)
+ * and exclusive/write acquisitions (including nested class/slab/type
+ * acquisitions by the same owner). `flags` stores the `LCB_F_*` type flags of
+ * the lock so release tracepoints that do not carry flags (such as
+ * `lock:contended_release`) can decrement matching class/slab/type entries.
+ */
+struct held_lock_val {
+ __u32 read_count;
+ __u32 write_count;
+ __u32 flags;
+ __u32 pad;
+};
+
/* BPF map that will be filled by user space */
struct filters {
__uint(type, BPF_MAP_TYPE_HASH);
@@ -43,10 +82,198 @@ struct lost_count {
__uint(max_entries, 1);
} dropped SEC(".maps");
+/* Currently held kernel locks tracked by fentry/fexit and lock tracepoints. */
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __type(key, struct held_lock_key);
+ __type(value, struct held_lock_val);
+ __uint(max_entries, 1);
+} held_locks SEC(".maps");
+
+/* Target lock addresses for PBF_LOCK_ADDR filters. */
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __type(key, __u64);
+ __type(value, __u8);
+ __uint(max_entries, 1);
+} lock_addr_filter SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(key_size, sizeof(__u64));
+ __uint(value_size, sizeof(__u32));
+ __uint(max_entries, 1);
+} lock_syms SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(key_size, sizeof(__u64));
+ __uint(value_size, sizeof(__u8));
+ __uint(max_entries, 1);
+} slab_filter SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(key_size, sizeof(__u64));
+ __uint(value_size, sizeof(struct slab_cache_data));
+ __uint(max_entries, 1);
+} slab_caches SEC(".maps");
+
volatile const int use_idx_hash;
+const volatile __u64 contig_page_data_addr;
+const volatile __u64 node_data_addr;
+const volatile int nr_nodes;
+const volatile int sizeof_zone;
+
+__u32 slab_cache_id;
+
+/* Control flags set by userspace when lock_held(...) filters are active. */
+int lock_enabled;
+int has_lock_addr;
+int has_lock_slab;
+int has_lock_class;
+int has_lock_type;
+int has_mmap_lock;
+int has_siglock;
+__u8 lock_type_filter[LCB_F_TYPE_MAX];
+
void *bpf_cast_to_kern_ctx(void *) __ksym;
+/*
+ * Direct lock owner inspection helper using `get_lock_owner()` from `lock_data.h`.
+ *
+ * For locks that maintain a task_struct pointer in their `owner` field
+ * (`struct mutex`, write-locked `struct rw_semaphore`, and `struct rt_mutex`),
+ * `perf_sample_filter` can inspect the lock structure directly in kernel memory
+ * and compare the owner against `current` (`bpf_get_current_task()`).
+ *
+ * Note that for reader-locked `rw_semaphore`s, `__up_read()` does not clear
+ * `sem->owner` when `CONFIG_DEBUG_RWSEMS` is disabled, so reader locks and
+ * spinlocks are checked via the `held_locks` BPF map instead.
+ */
+static __always_inline bool is_direct_lock_owner(__u64 lock_addr, __u64 curr)
+{
+ if (!curr)
+ return false;
+
+ return (__u64)get_lock_owner(lock_addr, LCB_F_MUTEX) == curr ||
+ (__u64)get_lock_owner(lock_addr, LCB_F_WRITE) == curr ||
+ (__u64)get_lock_owner(lock_addr, LCB_F_RT) == curr;
+}
+
+static __always_inline bool match_held_val(struct held_lock_val *val, __u32 mode)
+{
+ if (!val)
+ return false;
+ if ((mode & PBF_LOCK_MODE_READ) && val->read_count > 0)
+ return true;
+ if ((mode & PBF_LOCK_MODE_WRITE) && val->write_count > 0)
+ return true;
+ return false;
+}
+
+static __always_inline __u64 is_lock_in_map(__u64 addr_or_id, __u32 kind,
+ __u32 mode, __u32 pid, __u32 cpu_id)
+{
+ struct held_lock_key key = {
+ .addr_or_id = addr_or_id,
+ .owner_id = pid,
+ .kind = kind,
+ };
+ struct held_lock_val *val;
+
+ if (pid) {
+ val = bpf_map_lookup_elem(&held_locks, &key);
+ if (match_held_val(val, mode))
+ return 1;
+ }
+
+ key.owner_id = cpu_id;
+ val = bpf_map_lookup_elem(&held_locks, &key);
+ if (match_held_val(val, mode))
+ return 1;
+
+ return 0;
+}
+
+static __always_inline __u64 is_lock_type_in_map(__u64 target, __u32 mode,
+ __u32 pid, __u32 cpu_id)
+{
+ __u32 flag1 = target & 0xff;
+ __u32 flag2 = (target >> 8) & 0xff;
+
+ if (is_lock_in_map(flag1, PBF_LOCK_TYPE, mode, pid, cpu_id))
+ return 1;
+ if (flag2 != 0xff && is_lock_in_map(flag2, PBF_LOCK_TYPE, mode, pid, cpu_id))
+ return 1;
+ return 0;
+}
+
+static __always_inline __u64 check_lock_held(struct perf_bpf_filter_entry *entry)
+{
+ __u64 curr;
+ __u32 pid;
+ __u32 cpu_id;
+ __u32 kind;
+ __u32 mode;
+ __u64 lock_addr;
+
+ /*
+ * Pinned filters (use_idx_hash != 0) share a world-writable filters
+ * map and do not support lock_held(). Reject evaluation when pinned
+ * or when lock_enabled is not set so unprivileged users cannot probe
+ * arbitrary kernel addresses via entry->target.
+ */
+ if (use_idx_hash || !lock_enabled)
+ return 0;
+
+ curr = bpf_get_current_task();
+ pid = (__u32)bpf_get_current_pid_tgid();
+ cpu_id = 0x80000000U | bpf_get_smp_processor_id();
+ kind = entry->part & PBF_LOCK_KIND_MASK;
+ mode = entry->part & PBF_LOCK_MODE_MASK;
+
+ if (!mode)
+ mode = PBF_LOCK_MODE_ANY;
+
+ switch (kind) {
+ case PBF_LOCK_ADDR:
+ lock_addr = entry->target;
+ if ((mode & PBF_LOCK_MODE_WRITE) &&
+ is_direct_lock_owner(lock_addr, curr))
+ return 1;
+ return is_lock_in_map(lock_addr, PBF_LOCK_ADDR, mode, pid, cpu_id);
+
+ case PBF_LOCK_MMAP:
+ lock_addr = get_current_mmap_lock(curr);
+ if (!lock_addr)
+ return 0;
+ if ((mode & PBF_LOCK_MODE_WRITE) &&
+ (__u64)get_lock_owner(lock_addr, LCB_F_WRITE) == curr)
+ return 1;
+ return is_lock_in_map(lock_addr, PBF_LOCK_ADDR, mode, pid, cpu_id);
+
+ case PBF_LOCK_SIGHAND:
+ lock_addr = get_current_siglock(curr);
+ if (!lock_addr)
+ return 0;
+ return is_lock_in_map(lock_addr, PBF_LOCK_ADDR, mode, pid, cpu_id);
+
+ case PBF_LOCK_CLASS:
+ return is_lock_in_map(entry->target, PBF_LOCK_CLASS, mode, pid, cpu_id);
+
+ case PBF_LOCK_SLAB:
+ return is_lock_in_map(entry->target, PBF_LOCK_SLAB, mode, pid, cpu_id);
+
+ case PBF_LOCK_TYPE:
+ return is_lock_type_in_map(entry->target, mode, pid, cpu_id);
+
+ default:
+ return 0;
+ }
+}
+
/* new kernel perf_sample_data definition */
struct perf_sample_data___new {
__u64 sample_flags;
@@ -175,6 +402,8 @@ static inline __u64 perf_get_sample(struct bpf_perf_event_data_kern *kctx,
return bpf_get_current_uid_gid() & 0xFFFFFFFF;
case PBF_TERM_GID:
return bpf_get_current_uid_gid() >> 32;
+ case PBF_TERM_LOCK_HELD:
+ return check_lock_held(entry);
case PBF_TERM_NONE:
case __PBF_UNUSED_TERM4:
case __PBF_UNUSED_TERM5:
@@ -295,4 +524,695 @@ int perf_sample_filter(void *ctx)
return 0;
}
+static __always_inline __u32 get_lock_owner_id(__u32 flags)
+{
+ __u32 owner_id;
+
+ if ((flags & (LCB_F_SPIN | LCB_F_MUTEX)) == LCB_F_SPIN)
+ return 0x80000000U | bpf_get_smp_processor_id();
+
+ owner_id = (__u32)bpf_get_current_pid_tgid();
+ if (!owner_id)
+ owner_id = 0x80000000U | bpf_get_smp_processor_id();
+ return owner_id;
+}
+
+static __always_inline void update_held_lock_count(__u64 addr_or_id, __u32 owner_id,
+ __u32 kind, __u32 flags, bool acquired)
+{
+ struct held_lock_key key = {
+ .addr_or_id = addr_or_id,
+ .owner_id = owner_id,
+ .kind = kind,
+ };
+ struct held_lock_val *val;
+ bool is_read = (flags & LCB_F_READ) != 0;
+
+ val = bpf_map_lookup_elem(&held_locks, &key);
+ if (acquired) {
+ if (val) {
+ if (is_read)
+ __sync_fetch_and_add(&val->read_count, 1);
+ else
+ __sync_fetch_and_add(&val->write_count, 1);
+ } else {
+ struct held_lock_val first = {
+ .read_count = is_read ? 1 : 0,
+ .write_count = is_read ? 0 : 1,
+ .flags = flags,
+ };
+
+ bpf_map_update_elem(&held_locks, &key, &first, BPF_ANY);
+ }
+ } else if (val) {
+ if (is_read) {
+ if (val->read_count <= 1 && val->write_count == 0)
+ bpf_map_delete_elem(&held_locks, &key);
+ else if (val->read_count > 0)
+ __sync_fetch_and_add(&val->read_count, -1);
+ } else {
+ if (val->write_count <= 1 && val->read_count == 0)
+ bpf_map_delete_elem(&held_locks, &key);
+ else if (val->write_count > 0)
+ __sync_fetch_and_add(&val->write_count, -1);
+ }
+ }
+}
+
+static __always_inline int record_lock_acquired_owner(__u64 lock_addr, __u32 flags, __u32 owner_id)
+{
+ struct held_lock_key addr_key = {
+ .addr_or_id = lock_addr,
+ .owner_id = owner_id,
+ .kind = PBF_LOCK_ADDR,
+ };
+ struct held_lock_val *existing;
+ bool matched = false;
+ bool is_read = (flags & LCB_F_READ) != 0;
+ __u32 class_id = 0;
+ __u64 kmem_cache_addr = 0;
+ __u32 type_flag = flags & LCB_F_TYPE_MASK;
+ bool match_type = false;
+
+ if (!lock_enabled || !lock_addr)
+ return 0;
+
+ if (has_lock_addr && bpf_map_lookup_elem(&lock_addr_filter, &lock_addr))
+ matched = true;
+
+ if (has_mmap_lock || has_siglock) {
+ __u32 ltype = check_lock_type(lock_addr, flags);
+
+ if ((has_mmap_lock && ltype == LCD_F_MMAP_LOCK) ||
+ (has_siglock && ltype == LCD_F_SIGHAND_LOCK))
+ matched = true;
+ }
+
+ if (has_lock_class && flags == LCB_F_SPIN) {
+ __u32 *cid = bpf_map_lookup_elem(&lock_syms, &lock_addr);
+
+ if (cid) {
+ class_id = *cid;
+ matched = true;
+ }
+ }
+
+ if (has_lock_slab && bpf_get_kmem_cache) {
+ kmem_cache_addr = (__u64)bpf_get_kmem_cache(lock_addr);
+ if (kmem_cache_addr && bpf_map_lookup_elem(&slab_filter, &kmem_cache_addr))
+ matched = true;
+ else
+ kmem_cache_addr = 0;
+ }
+
+ if (has_lock_type && type_flag < LCB_F_TYPE_MAX && lock_type_filter[type_flag]) {
+ match_type = true;
+ matched = true;
+ }
+
+ if (!matched)
+ return 0;
+
+ existing = bpf_map_lookup_elem(&held_locks, &addr_key);
+ if (existing) {
+ /* Non-reader locks are not recursive; avoid double-counting */
+ if (!is_read && existing->write_count > 0)
+ return 0;
+ if (is_read)
+ __sync_fetch_and_add(&existing->read_count, 1);
+ else
+ __sync_fetch_and_add(&existing->write_count, 1);
+ } else {
+ struct held_lock_val first = {
+ .read_count = is_read ? 1 : 0,
+ .write_count = is_read ? 0 : 1,
+ .flags = flags,
+ };
+
+ if (bpf_map_update_elem(&held_locks, &addr_key, &first, BPF_ANY) < 0)
+ return 0;
+ }
+
+ if (class_id)
+ update_held_lock_count(class_id, owner_id, PBF_LOCK_CLASS, flags, true);
+ if (kmem_cache_addr) {
+ update_held_lock_count((__u64)kmem_cache_addr, owner_id,
+ PBF_LOCK_SLAB, flags, true);
+ }
+ if (match_type)
+ update_held_lock_count(type_flag, owner_id, PBF_LOCK_TYPE, flags, true);
+
+ return 0;
+}
+
+static __always_inline int record_lock_acquired(void *ctx, __u32 flags)
+{
+ return record_lock_acquired_owner(get_lock_addr(ctx), flags, get_lock_owner_id(flags));
+}
+
+static __always_inline int record_lock_released_addr(__u64 lock_addr, __u32 flags)
+{
+ __u32 owner_id = get_lock_owner_id(flags);
+ struct held_lock_key addr_key = {
+ .addr_or_id = lock_addr,
+ .owner_id = owner_id,
+ .kind = PBF_LOCK_ADDR,
+ };
+ struct held_lock_val *existing;
+ __u32 type_flag = flags & LCB_F_TYPE_MASK;
+ bool is_read = (flags & LCB_F_READ) != 0;
+
+ if (!lock_enabled || !lock_addr)
+ return 0;
+
+ existing = bpf_map_lookup_elem(&held_locks, &addr_key);
+ if (!existing)
+ return 0;
+
+ if (is_read) {
+ if (existing->read_count <= 1 && existing->write_count == 0)
+ bpf_map_delete_elem(&held_locks, &addr_key);
+ else if (existing->read_count > 0)
+ __sync_fetch_and_add(&existing->read_count, -1);
+ } else {
+ if (existing->write_count == 0 && existing->read_count == 0)
+ bpf_map_delete_elem(&held_locks, &addr_key);
+ else if (existing->read_count == 0)
+ bpf_map_delete_elem(&held_locks, &addr_key);
+ else
+ existing->write_count = 0;
+ }
+
+ if (has_lock_class && flags == LCB_F_SPIN) {
+ __u32 *cid = bpf_map_lookup_elem(&lock_syms, &lock_addr);
+
+ if (cid)
+ update_held_lock_count(*cid, owner_id, PBF_LOCK_CLASS, flags, false);
+ }
+
+ if (has_lock_slab && bpf_get_kmem_cache) {
+ __u64 kmem_cache_addr = (__u64)bpf_get_kmem_cache(lock_addr);
+
+ if (kmem_cache_addr && bpf_map_lookup_elem(&slab_filter, &kmem_cache_addr)) {
+ update_held_lock_count(kmem_cache_addr, owner_id,
+ PBF_LOCK_SLAB, flags, false);
+ }
+ }
+
+ if (has_lock_type && type_flag < LCB_F_TYPE_MAX && lock_type_filter[type_flag])
+ update_held_lock_count(type_flag, owner_id, PBF_LOCK_TYPE, flags, false);
+
+ return 0;
+}
+
+static __always_inline int record_lock_released(void *ctx, __u32 flags)
+{
+ return record_lock_released_addr(get_lock_addr(ctx), flags);
+}
+
+/* Mutex acquire/release hooks */
+SEC("fexit/mutex_lock")
+int BPF_PROG(fexit_mutex_lock, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_MUTEX);
+}
+
+SEC("fexit/mutex_lock_nested")
+int BPF_PROG(fexit_mutex_lock_nested, void *lock, unsigned int subclass)
+{
+ return record_lock_acquired(ctx, LCB_F_MUTEX);
+}
+
+SEC("fexit/mutex_lock_io")
+int BPF_PROG(fexit_mutex_lock_io, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_MUTEX);
+}
+
+SEC("fexit/mutex_lock_io_nested")
+int BPF_PROG(fexit_mutex_lock_io_nested, void *lock, unsigned int subclass)
+{
+ return record_lock_acquired(ctx, LCB_F_MUTEX);
+}
+
+SEC("fexit/mutex_lock_interruptible")
+int BPF_PROG(fexit_mutex_lock_interruptible, void *lock, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_MUTEX);
+ return 0;
+}
+
+SEC("fexit/mutex_lock_interruptible_nested")
+int BPF_PROG(fexit_mutex_lock_interruptible_nested, void *lock, unsigned int subclass, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_MUTEX);
+ return 0;
+}
+
+SEC("fexit/mutex_lock_killable")
+int BPF_PROG(fexit_mutex_lock_killable, void *lock, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_MUTEX);
+ return 0;
+}
+
+SEC("fexit/mutex_trylock")
+int BPF_PROG(fexit_mutex_trylock, void *lock, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_MUTEX);
+ return 0;
+}
+
+SEC("fentry/mutex_unlock")
+int BPF_PROG(fentry_mutex_unlock, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_MUTEX);
+}
+
+/* RW semaphore acquire/release hooks */
+SEC("fexit/down_read")
+int BPF_PROG(fexit_down_read, void *sem)
+{
+ return record_lock_acquired(ctx, LCB_F_READ);
+}
+
+SEC("fexit/down_read_nested")
+int BPF_PROG(fexit_down_read_nested, void *sem, int subclass)
+{
+ return record_lock_acquired(ctx, LCB_F_READ);
+}
+
+SEC("fexit/down_read_interruptible")
+int BPF_PROG(fexit_down_read_interruptible, void *sem, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_READ);
+ return 0;
+}
+
+SEC("fexit/down_read_killable")
+int BPF_PROG(fexit_down_read_killable, void *sem, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_READ);
+ return 0;
+}
+
+SEC("fexit/down_read_killable_nested")
+int BPF_PROG(fexit_down_read_killable_nested, void *sem, int subclass, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_READ);
+ return 0;
+}
+
+SEC("fexit/down_read_trylock")
+int BPF_PROG(fexit_down_read_trylock, void *sem, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_READ);
+ return 0;
+}
+
+SEC("fexit/down_write")
+int BPF_PROG(fexit_down_write, void *sem)
+{
+ return record_lock_acquired(ctx, LCB_F_WRITE);
+}
+
+SEC("fexit/down_write_nested")
+int BPF_PROG(fexit_down_write_nested, void *sem, int subclass)
+{
+ return record_lock_acquired(ctx, LCB_F_WRITE);
+}
+
+SEC("fexit/down_write_killable")
+int BPF_PROG(fexit_down_write_killable, void *sem, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_WRITE);
+ return 0;
+}
+
+SEC("fexit/down_write_killable_nested")
+int BPF_PROG(fexit_down_write_killable_nested, void *sem, int subclass, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_WRITE);
+ return 0;
+}
+
+SEC("fexit/down_write_trylock")
+int BPF_PROG(fexit_down_write_trylock, void *sem, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_WRITE);
+ return 0;
+}
+
+SEC("fexit/downgrade_write")
+int BPF_PROG(fexit_downgrade_write, void *sem)
+{
+ record_lock_acquired(ctx, LCB_F_READ);
+ return record_lock_released(ctx, LCB_F_WRITE);
+}
+
+SEC("fentry/up_read")
+int BPF_PROG(fentry_up_read, void *sem)
+{
+ return record_lock_released(ctx, LCB_F_READ);
+}
+
+SEC("fentry/up_write")
+int BPF_PROG(fentry_up_write, void *sem)
+{
+ return record_lock_released(ctx, LCB_F_WRITE);
+}
+
+/* Spinlock acquire/release hooks */
+SEC("fexit/_raw_spin_lock")
+int BPF_PROG(fexit_raw_spin_lock, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN);
+}
+
+SEC("fexit/_raw_spin_lock_nested")
+int BPF_PROG(fexit_raw_spin_lock_nested, void *lock, int subclass)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN);
+}
+
+SEC("fexit/_raw_spin_lock_irq")
+int BPF_PROG(fexit_raw_spin_lock_irq, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN);
+}
+
+SEC("fexit/_raw_spin_lock_irq_disable")
+int BPF_PROG(fexit_raw_spin_lock_irq_disable, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN);
+}
+
+SEC("fexit/_raw_spin_lock_irqsave")
+int BPF_PROG(fexit_raw_spin_lock_irqsave, void *lock, unsigned long flags)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN);
+}
+
+SEC("fexit/_raw_spin_lock_irqsave_nested")
+int BPF_PROG(fexit_raw_spin_lock_irqsave_nested, void *lock, int subclass, unsigned long flags)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN);
+}
+
+SEC("fexit/_raw_spin_lock_bh")
+int BPF_PROG(fexit_raw_spin_lock_bh, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN);
+}
+
+SEC("fexit/_raw_spin_trylock")
+int BPF_PROG(fexit_raw_spin_trylock, void *lock, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_SPIN);
+ return 0;
+}
+
+SEC("fexit/_raw_spin_trylock_bh")
+int BPF_PROG(fexit_raw_spin_trylock_bh, void *lock, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_SPIN);
+ return 0;
+}
+
+SEC("fentry/_raw_spin_unlock")
+int BPF_PROG(fentry_raw_spin_unlock, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN);
+}
+
+SEC("fentry/_raw_spin_unlock_irq")
+int BPF_PROG(fentry_raw_spin_unlock_irq, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN);
+}
+
+SEC("fentry/_raw_spin_unlock_irq_enable")
+int BPF_PROG(fentry_raw_spin_unlock_irq_enable, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN);
+}
+
+SEC("fentry/_raw_spin_unlock_irqrestore")
+int BPF_PROG(fentry_raw_spin_unlock_irqrestore, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN);
+}
+
+SEC("fentry/_raw_spin_unlock_bh")
+int BPF_PROG(fentry_raw_spin_unlock_bh, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN);
+}
+
+/* RWlock acquire/release hooks */
+SEC("fexit/_raw_read_lock")
+int BPF_PROG(fexit_raw_read_lock, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fexit/_raw_read_lock_irq")
+int BPF_PROG(fexit_raw_read_lock_irq, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fexit/_raw_read_lock_irqsave")
+int BPF_PROG(fexit_raw_read_lock_irqsave, void *lock, unsigned long flags)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fexit/_raw_read_lock_bh")
+int BPF_PROG(fexit_raw_read_lock_bh, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fexit/_raw_read_trylock")
+int BPF_PROG(fexit_raw_read_trylock, void *lock, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_READ);
+ return 0;
+}
+
+SEC("fentry/_raw_read_unlock")
+int BPF_PROG(fentry_raw_read_unlock, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fentry/_raw_read_unlock_irq")
+int BPF_PROG(fentry_raw_read_unlock_irq, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fentry/_raw_read_unlock_irqrestore")
+int BPF_PROG(fentry_raw_read_unlock_irqrestore, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fentry/_raw_read_unlock_bh")
+int BPF_PROG(fentry_raw_read_unlock_bh, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_READ);
+}
+
+SEC("fexit/_raw_write_lock")
+int BPF_PROG(fexit_raw_write_lock, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fexit/_raw_write_lock_nested")
+int BPF_PROG(fexit_raw_write_lock_nested, void *lock, int subclass)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fexit/_raw_write_lock_irq")
+int BPF_PROG(fexit_raw_write_lock_irq, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fexit/_raw_write_lock_irqsave")
+int BPF_PROG(fexit_raw_write_lock_irqsave, void *lock, unsigned long flags)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fexit/_raw_write_lock_bh")
+int BPF_PROG(fexit_raw_write_lock_bh, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fexit/_raw_write_trylock")
+int BPF_PROG(fexit_raw_write_trylock, void *lock, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_SPIN | LCB_F_WRITE);
+ return 0;
+}
+
+SEC("fentry/_raw_write_unlock")
+int BPF_PROG(fentry_raw_write_unlock, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fentry/_raw_write_unlock_irq")
+int BPF_PROG(fentry_raw_write_unlock_irq, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fentry/_raw_write_unlock_irqrestore")
+int BPF_PROG(fentry_raw_write_unlock_irqrestore, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+SEC("fentry/_raw_write_unlock_bh")
+int BPF_PROG(fentry_raw_write_unlock_bh, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_SPIN | LCB_F_WRITE);
+}
+
+/* RT-Mutex acquire/release hooks */
+SEC("fexit/rt_mutex_lock")
+int BPF_PROG(fexit_rt_mutex_lock, void *lock)
+{
+ return record_lock_acquired(ctx, LCB_F_RT);
+}
+
+SEC("fexit/rt_mutex_lock_nested")
+int BPF_PROG(fexit_rt_mutex_lock_nested, void *lock, unsigned int subclass)
+{
+ return record_lock_acquired(ctx, LCB_F_RT);
+}
+
+SEC("fexit/rt_mutex_lock_interruptible")
+int BPF_PROG(fexit_rt_mutex_lock_interruptible, void *lock, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_RT);
+ return 0;
+}
+
+SEC("fexit/rt_mutex_lock_killable")
+int BPF_PROG(fexit_rt_mutex_lock_killable, void *lock, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, LCB_F_RT);
+ return 0;
+}
+
+SEC("fexit/rt_mutex_trylock")
+int BPF_PROG(fexit_rt_mutex_trylock, void *lock, int ret)
+{
+ if (ret)
+ record_lock_acquired(ctx, LCB_F_RT);
+ return 0;
+}
+
+SEC("fentry/rt_mutex_unlock")
+int BPF_PROG(fentry_rt_mutex_unlock, void *lock)
+{
+ return record_lock_released(ctx, LCB_F_RT);
+}
+
+/* Semaphore acquire/release hooks */
+SEC("fexit/down")
+int BPF_PROG(fexit_down, void *sem)
+{
+ return record_lock_acquired(ctx, 0);
+}
+
+SEC("fexit/down_interruptible")
+int BPF_PROG(fexit_down_interruptible, void *sem, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, 0);
+ return 0;
+}
+
+SEC("fexit/down_killable")
+int BPF_PROG(fexit_down_killable, void *sem, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, 0);
+ return 0;
+}
+
+SEC("fexit/down_timeout")
+int BPF_PROG(fexit_down_timeout, void *sem, long timeout, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, 0);
+ return 0;
+}
+
+SEC("fexit/down_trylock")
+int BPF_PROG(fexit_down_trylock, void *sem, int ret)
+{
+ if (ret == 0)
+ record_lock_acquired(ctx, 0);
+ return 0;
+}
+
+SEC("fentry/up")
+int BPF_PROG(fentry_up, void *sem)
+{
+ return record_lock_released(ctx, 0);
+}
+
+/* Percpu-rwsem write acquire/release hooks */
+SEC("fexit/percpu_down_write")
+int BPF_PROG(fexit_percpu_down_write, void *sem)
+{
+ return record_lock_acquired(ctx, LCB_F_PERCPU | LCB_F_WRITE);
+}
+
+SEC("fentry/percpu_up_write")
+int BPF_PROG(fentry_percpu_up_write, void *sem)
+{
+ return record_lock_released(ctx, LCB_F_PERCPU | LCB_F_WRITE);
+}
+
+SEC("raw_tp/bpf_test_finish")
+int BPF_PROG(collect_lock_syms)
+{
+ return do_collect_lock_syms(&lock_syms, contig_page_data_addr,
+ node_data_addr, nr_nodes, sizeof_zone);
+}
+
+SEC("iter/kmem_cache")
+int slab_cache_iter(void *ctx)
+{
+ return do_slab_cache_iter(ctx, &slab_caches, &slab_cache_id);
+}
+
char LICENSE[] SEC("license") = "Dual BSD/GPL";
diff --git a/tools/perf/util/parse-events.c b/tools/perf/util/parse-events.c
index 2f3317ce17f0..1fcb424b75c0 100644
--- a/tools/perf/util/parse-events.c
+++ b/tools/perf/util/parse-events.c
@@ -2596,7 +2596,8 @@ foreach_evsel_in_last_glob(struct evlist *evlist,
/* Will a tracepoint filter work for str or should a BPF filter be used? */
static bool is_possible_tp_filter(const char *str)
{
- return strstr(str, "uid") == NULL;
+ return strstr(str, "uid") == NULL && strstr(str, "gid") == NULL &&
+ strstr(str, "lock_held") == NULL;
}
static int set_filter(struct evsel *evsel, const void *arg)
--
2.56.0.212.g129ede20d9-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v1 3/5] perf test: Add tests for lock_held() BPF sample filter
2026-10-17 5:45 [PATCH v1 0/5] perf bpf-filter / lock contention: Add lock_held() sample filter and RCU/SRCU support Ian Rogers
2026-10-17 5:45 ` [PATCH v1 1/5] perf lock contention: Share common BPF and lock type definitions in lock_data.h Ian Rogers
2026-10-17 5:45 ` [PATCH v1 2/5] perf bpf-filter: Add lock_held() sample filter Ian Rogers
@ 2026-10-17 5:45 ` Ian Rogers
2026-10-17 5:49 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 4/5] perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU, and SRCU Ian Rogers
2026-10-17 5:45 ` [PATCH v1 5/5] perf lock contention: Extend BPF lock contention to RCU " Ian Rogers
4 siblings, 1 reply; 11+ messages in thread
From: Ian Rogers @ 2026-10-17 5:45 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Ian Rogers, Adrian Hunter,
James Clark, linux-perf-users, linux-kernel, rcu
Add unit tests in parse-events.c for parsing lock_held(),
lock_held_read(), and lock_held_exclusive() BPF filter expressions
(standalone predicates, negated predicates, and compound group
expressions) and extend record_bpf_filter.sh with lock_held(),
lock_held_read()/lock_held_write(), and ':R'/':W' recording tests.
Example output:
# perf test -v 'Parsing of BPF sample filters'
5: Parse event definition strings :
5.7: Parsing of BPF sample filters : Ok
# perf test -v 'perf record sample filtering (by BPF) tests'
112: perf record sample filtering (by BPF) tests :
Checking BPF-filter privilege
Basic bpf-filter test
Basic bpf-filter test [Success]
Failing bpf-filter test
Failing bpf-filter test [Success]
Group bpf-filter test
Group bpf-filter test [Success]
Multiple bpf-filter test
Multiple bpf-filter test [Success]
Cgroup bpf-filter test
Cgroup bpf-filter test [Success]
Lock-held bpf-filter test
Lock-held bpf-filter test [Success]
112: perf record sample filtering (by BPF) tests : Ok
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/tests/parse-events.c | 108 ++++++++++++++++++++
tools/perf/tests/shell/record_bpf_filter.sh | 99 ++++++++++++++++++
2 files changed, 207 insertions(+)
diff --git a/tools/perf/tests/parse-events.c b/tools/perf/tests/parse-events.c
index 5f6f95c8a5b4..4cf9192762f2 100644
--- a/tools/perf/tests/parse-events.c
+++ b/tools/perf/tests/parse-events.c
@@ -8,9 +8,11 @@
#include "debug.h"
#include "pmu.h"
#include "pmus.h"
+#include "bpf-filter.h"
#include "strbuf.h"
#include <dirent.h>
#include <errno.h>
+#include <string.h>
#include "fncache.h"
#include <sys/types.h>
#include <sys/stat.h>
@@ -2938,6 +2940,111 @@ static int test__pmu_events_alias2(struct test_suite *test __maybe_unused,
return ret;
}
+#ifdef HAVE_BPF_SKEL
+static void clear_bpf_filter_exprs(struct list_head *expr_head)
+{
+ struct perf_bpf_filter_expr *expr, *tmp;
+
+ list_for_each_entry_safe(expr, tmp, expr_head, list) {
+ list_del(&expr->list);
+ perf_bpf_filter_expr__delete(expr);
+ }
+}
+
+static int do_test_bpf_filters(struct list_head *expr_head)
+{
+ struct perf_bpf_filter_expr *expr, *group;
+ const char *compound = "lock_held(rwsem:R) == 1 || lock_held(&dentry) != 0, period > 100";
+
+ /* Simple lock_held(&cgroup_mutex) */
+ TEST_ASSERT_VAL("parse lock_held(&cgroup_mutex)",
+ __perf_bpf_filter__parse(expr_head, "lock_held(&cgroup_mutex)") == 0);
+ TEST_ASSERT_VAL("not empty", !list_empty(expr_head));
+ expr = list_first_entry(expr_head, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("term is PBF_TERM_LOCK_HELD", expr->term == PBF_TERM_LOCK_HELD);
+ TEST_ASSERT_VAL("op is PBF_OP_EQ", expr->op == PBF_OP_EQ);
+ TEST_ASSERT_VAL("val is 1", expr->val == 1);
+ TEST_ASSERT_VAL("lock_name is &cgroup_mutex",
+ expr->lock_name && !strcmp(expr->lock_name, "&cgroup_mutex"));
+ clear_bpf_filter_exprs(expr_head);
+
+ /* Negated !lock_held(cgroup_mutex) */
+ TEST_ASSERT_VAL("parse !lock_held(cgroup_mutex)",
+ __perf_bpf_filter__parse(expr_head, "!lock_held(cgroup_mutex)") == 0);
+ TEST_ASSERT_VAL("not empty", !list_empty(expr_head));
+ expr = list_first_entry(expr_head, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("term is PBF_TERM_LOCK_HELD", expr->term == PBF_TERM_LOCK_HELD);
+ TEST_ASSERT_VAL("op is PBF_OP_EQ", expr->op == PBF_OP_EQ);
+ TEST_ASSERT_VAL("val is 0", expr->val == 0);
+ TEST_ASSERT_VAL("lock_name is cgroup_mutex",
+ expr->lock_name && !strcmp(expr->lock_name, "cgroup_mutex"));
+ clear_bpf_filter_exprs(expr_head);
+
+ /* Read/shared and write/exclusive predicates */
+ TEST_ASSERT_VAL("parse lock_held_read(&mmap_lock)",
+ __perf_bpf_filter__parse(expr_head, "lock_held_read(&mmap_lock)") == 0);
+ TEST_ASSERT_VAL("not empty", !list_empty(expr_head));
+ expr = list_first_entry(expr_head, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("term is PBF_TERM_LOCK_HELD", expr->term == PBF_TERM_LOCK_HELD);
+ TEST_ASSERT_VAL("part is PBF_LOCK_MODE_READ", expr->part == PBF_LOCK_MODE_READ);
+ TEST_ASSERT_VAL("op is PBF_OP_EQ", expr->op == PBF_OP_EQ);
+ TEST_ASSERT_VAL("val is 1", expr->val == 1);
+ TEST_ASSERT_VAL("lock_name is &mmap_lock",
+ expr->lock_name && !strcmp(expr->lock_name, "&mmap_lock"));
+ clear_bpf_filter_exprs(expr_head);
+
+ TEST_ASSERT_VAL("parse !lock_held_exclusive(mmap_lock)",
+ __perf_bpf_filter__parse(expr_head,
+ "!lock_held_exclusive(mmap_lock)") == 0);
+ TEST_ASSERT_VAL("not empty", !list_empty(expr_head));
+ expr = list_first_entry(expr_head, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("term is PBF_TERM_LOCK_HELD", expr->term == PBF_TERM_LOCK_HELD);
+ TEST_ASSERT_VAL("part is PBF_LOCK_MODE_WRITE", expr->part == PBF_LOCK_MODE_WRITE);
+ TEST_ASSERT_VAL("op is PBF_OP_EQ", expr->op == PBF_OP_EQ);
+ TEST_ASSERT_VAL("val is 0", expr->val == 0);
+ TEST_ASSERT_VAL("lock_name is mmap_lock",
+ expr->lock_name && !strcmp(expr->lock_name, "mmap_lock"));
+ clear_bpf_filter_exprs(expr_head);
+
+ /*
+ * Comparison and compound group:
+ * lock_held(rwsem:R) == 1 || lock_held(&dentry) != 0, period > 100
+ */
+ TEST_ASSERT_VAL("parse compound lock_held filter",
+ __perf_bpf_filter__parse(expr_head, compound) == 0);
+ TEST_ASSERT_VAL("not empty", !list_empty(expr_head));
+ expr = list_first_entry(expr_head, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("group op", expr->op == PBF_OP_GROUP_BEGIN);
+ TEST_ASSERT_VAL("groups not empty", !list_empty(&expr->groups));
+ group = list_first_entry(&expr->groups, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("first group member lock_name",
+ group->lock_name && !strcmp(group->lock_name, "rwsem:R"));
+ TEST_ASSERT_VAL("first group member op", group->op == PBF_OP_EQ && group->val == 1);
+ group = list_last_entry(&expr->groups, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("second group member lock_name",
+ group->lock_name && !strcmp(group->lock_name, "&dentry"));
+ TEST_ASSERT_VAL("second group member op", group->op == PBF_OP_NEQ && group->val == 0);
+ expr = list_last_entry(expr_head, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("second term is period", expr->term == PBF_TERM_PERIOD && expr->val == 100);
+
+ return TEST_OK;
+}
+#endif
+
+static int test__bpf_filters(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+#ifdef HAVE_BPF_SKEL
+ struct list_head expr_head = LIST_HEAD_INIT(expr_head);
+ int ret = do_test_bpf_filters(&expr_head);
+
+ clear_bpf_filter_exprs(&expr_head);
+ return ret;
+#else
+ return TEST_SKIP;
+#endif
+}
+
static struct test_case tests__parse_events[] = {
TEST_CASE_REASON("Test event parsing",
events2,
@@ -2952,6 +3059,7 @@ static struct test_case tests__parse_events[] = {
"no aliases in sysfs"),
TEST_CASE("Parsing of aliased events", pmu_events_alias2),
TEST_CASE("Parsing of terms (event modifiers)", terms2),
+ TEST_CASE("Parsing of BPF sample filters", bpf_filters),
{ .name = NULL, }
};
diff --git a/tools/perf/tests/shell/record_bpf_filter.sh b/tools/perf/tests/shell/record_bpf_filter.sh
index 383574cb3bd3..7ec75d969bd6 100755
--- a/tools/perf/tests/shell/record_bpf_filter.sh
+++ b/tools/perf/tests/shell/record_bpf_filter.sh
@@ -176,6 +176,101 @@ test_bpf_filter_cgroup() {
echo "Cgroup bpf-filter test [Success]"
}
+test_bpf_filter_lock_held() {
+ echo "Lock-held bpf-filter test"
+ lock_sym="cgroup_mutex"
+
+ if [ -r /proc/kallsyms ] && ! grep -qw "${lock_sym}" /proc/kallsyms
+ then
+ lock_sym="slab_mutex"
+ fi
+
+ if ! perf record -e task-clock --filter "lock_held(&${lock_sym})" \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ if [ "$(id -u)" != 0 ]
+ then
+ echo "Lock-held bpf-filter test [Skipped permission]"
+ return
+ fi
+ if [ -r /proc/kallsyms ] && ! grep -qw "${lock_sym}" /proc/kallsyms
+ then
+ echo "Lock-held bpf-filter test [Skipped missing ${lock_sym}]"
+ return
+ fi
+ echo "Lock-held bpf-filter test [Failed &${lock_sym}]"
+ err=1
+ return
+ fi
+
+ if ! perf record -e task-clock --filter "!lock_held(${lock_sym})" \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed !lock_held(${lock_sym})]"
+ err=1
+ return
+ fi
+
+ if ! perf record -e task-clock --filter 'lock_held(mmap_lock)' \
+ -o "${perfdata}" perf bench mem mmap -t 2 -l 10 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed mmap_lock]"
+ err=1
+ return
+ fi
+
+ if ! perf record -e task-clock --filter 'lock_held(rq_lock)' \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed rq_lock]"
+ err=1
+ return
+ fi
+
+ if ! perf record -e task-clock --filter 'lock_held(mutex) || lock_held(spinlock)' \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed lock type]"
+ err=1
+ return
+ fi
+
+ if ! perf record -e task-clock \
+ --filter 'lock_held_read(&mmap_lock) || lock_held_write(&mmap_lock)' \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed lock_held_read/write(&mmap_lock)]"
+ err=1
+ return
+ fi
+
+ if ! perf record -e task-clock --filter 'lock_held(&mmap_lock:R) || lock_held(&mmap_lock:W)' \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed &mmap_lock:R/:W]"
+ err=1
+ return
+ fi
+
+ if perf record -e task-clock --filter 'lock_held_read(rq_lock)' \
+ -o /dev/null true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed lock_held_read(rq_lock) should fail]"
+ err=1
+ return
+ fi
+
+ if perf record -e task-clock --filter 'lock_held(nonexistent_lock_xyz)' \
+ -o /dev/null true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed nonexistent lock should fail]"
+ err=1
+ return
+ fi
+
+ echo "Lock-held bpf-filter test [Success]"
+}
+
test_bpf_filter_priv
if [ $err = 0 ]; then
@@ -198,5 +293,9 @@ if [ $err = 0 ]; then
test_bpf_filter_cgroup
fi
+if [ $err = 0 ]; then
+ test_bpf_filter_lock_held
+fi
+
cleanup
exit $err
--
2.56.0.212.g129ede20d9-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v1 4/5] perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU, and SRCU
2026-10-17 5:45 [PATCH v1 0/5] perf bpf-filter / lock contention: Add lock_held() sample filter and RCU/SRCU support Ian Rogers
` (2 preceding siblings ...)
2026-10-17 5:45 ` [PATCH v1 3/5] perf test: Add tests for lock_held() BPF " Ian Rogers
@ 2026-10-17 5:45 ` Ian Rogers
2026-10-17 5:56 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 5/5] perf lock contention: Extend BPF lock contention to RCU " Ian Rogers
4 siblings, 1 reply; 11+ messages in thread
From: Ian Rogers @ 2026-10-17 5:45 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Ian Rogers, Adrian Hunter,
James Clark, linux-perf-users, linux-kernel, rcu
Extend the lock_held(<lock>) BPF sample filter to support RCU, Tasks
Trace RCU, and SRCU locks:
- 'rcu' (supports both read/shared and write/exclusive modes):
- Read/shared ('lock_held_read(rcu)', 'lock_held(rcu:R)',
'lock_held(rcu_read_lock)'): directly inspects
current->rcu_read_lock_nesting in perf_sample_filter via CO-RE (on
CONFIG_PREEMPT_RCU kernels), accounting for the baseline
rcu_read_lock() nesting acquired by the perf overflow handler itself
(bpf_overflow_handler() and do_perf_sw_event()) with zero extra
fentry/fexit tracing hooks.
- Write/exclusive ('lock_held_write(rcu)', 'lock_held(rcu:W)'):
attaches fentry/fexit hooks to synchronize_rcu(),
synchronize_rcu_expedited(), synchronize_rcu_tasks(), and
rcu_barrier() to track grace-period and barrier waits.
- 'srcu' and specific SRCU symbols (e.g. '&srcu_symbol', supports both
read/shared and write/exclusive modes):
- Read/shared ('lock_held_read(srcu)', 'lock_held(srcu:R)'): attaches
fexit/__srcu_read_lock and fentry/__srcu_read_unlock.
- Write/exclusive ('lock_held_write(srcu)', 'lock_held(srcu:W)'):
attaches fentry/fexit on synchronize_srcu(),
synchronize_srcu_expedited(), and srcu_barrier().
- 'rcu-trace' / 'rcu_trace' / 'rcu_read_lock_trace' (supports
read/shared mode only): directly inspects
current->trc_reader_nesting > 0 on CONFIG_TASKS_TRACE_RCU kernels
with zero extra tracing hooks.
Example usage:
# perf record -a -g -e cycles --filter 'lock_held_read(rcu)' -- sleep 1
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 1.482 MB perf.data (412 samples) ]
# perf report --stdio --no-children
# Overhead Command Shared Object Symbol
# ........ ....... ................. ........................
#
14.32% perf [kernel.kallsyms] [k] __d_lookup_rcu
|
---__d_lookup_rcu
lookup_fast
walk_component
link_path_walk
path_lookupat
filename_lookup
vfs_statx
__do_sys_newstat
do_syscall_64
entry_SYSCALL_64_after_hwframe
# perf record -a -g -e task-clock --filter 'lock_held(rcu:W)' -- sleep 1
# perf record -a -g -e cycles --filter 'lock_held(srcu)' -- sleep 1
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/Documentation/perf-record.txt | 42 +++--
tools/perf/tests/parse-events.c | 13 ++
tools/perf/tests/shell/record_bpf_filter.sh | 20 +++
tools/perf/util/bpf-filter.c | 158 +++++++++++++++++--
tools/perf/util/bpf_skel/lock_data.h | 44 +++++-
tools/perf/util/bpf_skel/sample-filter.h | 12 ++
tools/perf/util/bpf_skel/sample_filter.bpf.c | 135 +++++++++++++++-
tools/perf/util/bpf_skel/vmlinux/vmlinux.h | 2 +
tools/perf/util/lock-contention.c | 4 +
tools/perf/util/lock-contention.h | 4 +-
10 files changed, 402 insertions(+), 32 deletions(-)
diff --git a/tools/perf/Documentation/perf-record.txt b/tools/perf/Documentation/perf-record.txt
index 246ae085744d..c8ccfb0e0ce6 100644
--- a/tools/perf/Documentation/perf-record.txt
+++ b/tools/perf/Documentation/perf-record.txt
@@ -219,28 +219,38 @@ OPTIONS
'&', e.g. 'lock_held(&cgroup_mutex)' or 'lock_held(cgroup_mutex)'),
a hexadecimal lock address, a slab cache name prefixed with '&'
(e.g. 'lock_held(&dentry)', 'lock_held_read(&dentry)'), a well-known
- lock name ('mmap_lock', 'siglock', 'rq_lock', 'zone_lock'), or a lock
- type ('mutex', 'spinlock', 'rwsem', 'rwsem:R', 'rwsem:W', 'rwlock',
- 'rwlock:R', 'rwlock:W', 'rt-mutex', 'percpu-rwsem', 'pcpu-sem:W',
- 'semaphore'). Both read/shared and write/exclusive tracking are
- supported for 'rwsem' (including 'mmap_lock') and 'rwlock';
+ lock name ('mmap_lock', 'siglock', 'rq_lock', 'zone_lock', 'rcu',
+ 'rcu-trace'), or a lock type ('mutex', 'spinlock', 'rwsem', 'rwsem:R',
+ 'rwsem:W', 'rwlock', 'rwlock:R', 'rwlock:W', 'rt-mutex',
+ 'percpu-rwsem', 'pcpu-sem:W', 'srcu', 'srcu:R', 'srcu:W', 'rcu:R',
+ 'rcu:W', 'semaphore'). Both read/shared and write/exclusive tracking
+ are supported for 'rwsem' (including 'mmap_lock'), 'rwlock', 'rcu',
+ and 'srcu' (where read/shared mode matches 'rcu_read_lock()' /
+ 'srcu_read_lock()' critical sections and write/exclusive mode matches
+ grace-period waits 'synchronize_rcu*()', 'synchronize_srcu*()',
+ 'rcu_barrier()', and 'srcu_barrier()'); 'rcu-trace'
+ ('rcu_read_lock_trace()') supports read/shared tracking only; and
'mutex', 'spinlock' (including 'siglock', 'rq_lock', 'zone_lock'),
'rt-mutex', 'semaphore', and 'percpu-rwsem' ('percpu_down_write()')
support write/exclusive tracking only.
Note that 'lock_held(<lock>)' introduces overhead outside of sample
overflow handling: in addition to checking the lock's owner field on
- each sample where possible (such as for mutexes, rt-mutexes, and
- write-locked rwsems), it attaches BPF fentry/fexit programs to kernel
- lock acquire and release functions to track held locks (such as
- spinlocks, rwlocks, reader-locked rwsems, and slab/type filters) in a
- BPF map. These BPF hooks run on every kernel lock acquisition and
- release for the attached lock family, even when no perf sample is
- being taken. To minimize this overhead, only the hooks for the
- relevant lock family and mode are attached when known (for example,
- 'lock_held_read(&mmap_lock)' only hooks rwsem read functions,
- 'lock_held_write(&mmap_lock)' only hooks rwsem write functions,
- '&cgroup_mutex' only hooks mutex functions, and 'rq_lock' only hooks
+ each sample where possible (such as for mutexes, rt-mutexes,
+ write-locked rwsems, and 'rcu:R' / 'rcu-trace' which inspect
+ 'current->rcu_read_lock_nesting' / 'current->trc_reader_nesting'
+ directly with no extra tracing hooks), it attaches BPF fentry/fexit
+ programs to kernel lock acquire and release functions to track held
+ locks (such as spinlocks, rwlocks, reader-locked rwsems, SRCU locks,
+ RCU synchronize waits, and slab/type filters) in a BPF map. These
+ BPF hooks run on every kernel lock acquisition and release for the
+ attached lock family, even when no perf sample is being taken. To
+ minimize this overhead, only the hooks for the relevant lock family
+ and mode are attached when known (for example, 'lock_held_read(rcu)'
+ and 'rcu-trace' attach no extra hooks, 'lock_held_read(&mmap_lock)'
+ only hooks rwsem read functions, 'lock_held_write(&mmap_lock)' only
+ hooks rwsem write functions, '&cgroup_mutex' only hooks mutex
+ functions, 'srcu' only hooks SRCU functions, and 'rq_lock' only hooks
spinlock functions), whereas slab filters ('&<slab>'), raw hex
addresses, or symbols without a recognizable lock-type suffix attach
hooks across all lock families (including high-frequency spinlocks).
diff --git a/tools/perf/tests/parse-events.c b/tools/perf/tests/parse-events.c
index 4cf9192762f2..730466b7ebf6 100644
--- a/tools/perf/tests/parse-events.c
+++ b/tools/perf/tests/parse-events.c
@@ -2980,6 +2980,19 @@ static int do_test_bpf_filters(struct list_head *expr_head)
expr->lock_name && !strcmp(expr->lock_name, "cgroup_mutex"));
clear_bpf_filter_exprs(expr_head);
+ /* RCU and SRCU lock_held(rcu) || lock_held(srcu) */
+ TEST_ASSERT_VAL("parse lock_held(rcu)",
+ __perf_bpf_filter__parse(expr_head, "lock_held(rcu)") == 0);
+ TEST_ASSERT_VAL("not empty", !list_empty(expr_head));
+ expr = list_first_entry(expr_head, struct perf_bpf_filter_expr, list);
+ TEST_ASSERT_VAL("term is PBF_TERM_LOCK_HELD", expr->term == PBF_TERM_LOCK_HELD);
+ TEST_ASSERT_VAL("part is PBF_LOCK_MODE_ANY", expr->part == PBF_LOCK_MODE_ANY);
+ TEST_ASSERT_VAL("op is PBF_OP_EQ", expr->op == PBF_OP_EQ);
+ TEST_ASSERT_VAL("val is 1", expr->val == 1);
+ TEST_ASSERT_VAL("lock_name is rcu",
+ expr->lock_name && !strcmp(expr->lock_name, "rcu"));
+ clear_bpf_filter_exprs(expr_head);
+
/* Read/shared and write/exclusive predicates */
TEST_ASSERT_VAL("parse lock_held_read(&mmap_lock)",
__perf_bpf_filter__parse(expr_head, "lock_held_read(&mmap_lock)") == 0);
diff --git a/tools/perf/tests/shell/record_bpf_filter.sh b/tools/perf/tests/shell/record_bpf_filter.sh
index 7ec75d969bd6..39ba628022db 100755
--- a/tools/perf/tests/shell/record_bpf_filter.sh
+++ b/tools/perf/tests/shell/record_bpf_filter.sh
@@ -235,6 +235,26 @@ test_bpf_filter_lock_held() {
return
fi
+ if ! perf record -e task-clock --filter 'lock_held_write(rcu) || lock_held(srcu)' \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ echo "Lock-held bpf-filter test [Failed rcu:W/srcu]"
+ err=1
+ return
+ fi
+
+ if ! perf record -e task-clock --filter 'lock_held_read(rcu)' \
+ -o "${perfdata}" true 2> /dev/null
+ then
+ if ! perf record -e task-clock --filter 'lock_held_read(rcu)' \
+ -o /dev/null true 2>&1 | grep -q CONFIG_PREEMPT_RCU
+ then
+ echo "Lock-held bpf-filter test [Failed lock_held_read(rcu)]"
+ err=1
+ return
+ fi
+ fi
+
if ! perf record -e task-clock \
--filter 'lock_held_read(&mmap_lock) || lock_held_write(&mmap_lock)' \
-o "${perfdata}" true 2> /dev/null
diff --git a/tools/perf/util/bpf-filter.c b/tools/perf/util/bpf-filter.c
index 35d6f7c63868..d4d7f7a50f18 100644
--- a/tools/perf/util/bpf-filter.c
+++ b/tools/perf/util/bpf-filter.c
@@ -190,11 +190,19 @@ enum lock_prog_group {
LOCK_PROG_RTMUTEX = 1U << 6,
LOCK_PROG_SEM = 1U << 7,
LOCK_PROG_PERCPU_RWSEM = 1U << 8,
- LOCK_PROG_ALL = 0x1ffU,
+ LOCK_PROG_SRCU_READ = 1U << 9,
+ LOCK_PROG_SRCU_WRITE = 1U << 10,
+ LOCK_PROG_SRCU = LOCK_PROG_SRCU_READ | LOCK_PROG_SRCU_WRITE,
+ LOCK_PROG_RCU_WRITE = 1U << 11,
+ LOCK_PROG_ALL = 0xfffU,
};
static unsigned int lock_flags_to_prog_group(unsigned int flags)
{
+ if (flags & LCB_F_RCU)
+ return (flags & LCB_F_WRITE) ? LOCK_PROG_RCU_WRITE : 0;
+ if (flags & LCB_F_SRCU)
+ return (flags & LCB_F_READ) ? LOCK_PROG_SRCU_READ : LOCK_PROG_SRCU_WRITE;
if (flags & LCB_F_MUTEX)
return LOCK_PROG_MUTEX;
if (flags & LCB_F_RT)
@@ -308,6 +316,32 @@ static const struct lock_prog_desc lock_progs[] = {
/* Percpu-RWsem */
LOCK_FPROG(fexit_percpu_down_write, "percpu_down_write", LOCK_PROG_PERCPU_RWSEM),
LOCK_FPROG(fentry_percpu_up_write, "percpu_up_write", LOCK_PROG_PERCPU_RWSEM),
+ /* SRCU */
+ LOCK_FPROG(fexit_srcu_read_lock, "__srcu_read_lock", LOCK_PROG_SRCU_READ),
+ LOCK_FPROG(fentry_srcu_read_unlock, "__srcu_read_unlock", LOCK_PROG_SRCU_READ),
+ LOCK_FPROG(fexit_srcu_read_lock_nmisafe, "__srcu_read_lock_nmisafe",
+ LOCK_PROG_SRCU_READ),
+ LOCK_FPROG(fentry_srcu_read_unlock_nmisafe, "__srcu_read_unlock_nmisafe",
+ LOCK_PROG_SRCU_READ),
+ LOCK_FPROG(fentry_synchronize_srcu, "synchronize_srcu", LOCK_PROG_SRCU_WRITE),
+ LOCK_FPROG(fexit_synchronize_srcu, "synchronize_srcu", LOCK_PROG_SRCU_WRITE),
+ LOCK_FPROG(fentry_synchronize_srcu_expedited, "synchronize_srcu_expedited",
+ LOCK_PROG_SRCU_WRITE),
+ LOCK_FPROG(fexit_synchronize_srcu_expedited, "synchronize_srcu_expedited",
+ LOCK_PROG_SRCU_WRITE),
+ LOCK_FPROG(fentry_srcu_barrier, "srcu_barrier", LOCK_PROG_SRCU_WRITE),
+ LOCK_FPROG(fexit_srcu_barrier, "srcu_barrier", LOCK_PROG_SRCU_WRITE),
+ /* RCU grace-period synchronize / barrier */
+ LOCK_FPROG(fentry_synchronize_rcu, "synchronize_rcu", LOCK_PROG_RCU_WRITE),
+ LOCK_FPROG(fexit_synchronize_rcu, "synchronize_rcu", LOCK_PROG_RCU_WRITE),
+ LOCK_FPROG(fentry_synchronize_rcu_expedited, "synchronize_rcu_expedited",
+ LOCK_PROG_RCU_WRITE),
+ LOCK_FPROG(fexit_synchronize_rcu_expedited, "synchronize_rcu_expedited",
+ LOCK_PROG_RCU_WRITE),
+ LOCK_FPROG(fentry_synchronize_rcu_tasks, "synchronize_rcu_tasks", LOCK_PROG_RCU_WRITE),
+ LOCK_FPROG(fexit_synchronize_rcu_tasks, "synchronize_rcu_tasks", LOCK_PROG_RCU_WRITE),
+ LOCK_FPROG(fentry_rcu_barrier, "rcu_barrier", LOCK_PROG_RCU_WRITE),
+ LOCK_FPROG(fexit_rcu_barrier, "rcu_barrier", LOCK_PROG_RCU_WRITE),
};
#undef LOCK_FPROG
@@ -616,6 +650,17 @@ static void prescan_one_lock_expr(struct perf_bpf_filter_expr *expr,
*needs_lock_syms = true;
return;
}
+ if (!strcmp(base, "rcu")) {
+ if (mode & PBF_LOCK_MODE_WRITE)
+ *prog_groups |= LOCK_PROG_RCU_WRITE;
+ return;
+ }
+ if (!strcmp(base, "rcu_read_lock") ||
+ !strcmp(base, "rcu-trace") || !strcmp(base, "rcu_trace") ||
+ !strcmp(base, "rcu_read_lock_trace")) {
+ /* Inspected directly on task_struct; no tracing hooks needed. */
+ return;
+ }
if (!has_amp && parse_lock_type_name(base, mode, NULL, NULL, prog_groups))
return;
@@ -646,6 +691,11 @@ static void prescan_one_lock_expr(struct perf_bpf_filter_expr *expr,
*prog_groups |= LOCK_PROG_RWLOCK_READ;
if (mode & PBF_LOCK_MODE_WRITE)
*prog_groups |= LOCK_PROG_RWLOCK_WRITE;
+ } else if (strtailcmp(base, "srcu") == 0) {
+ if (mode & PBF_LOCK_MODE_READ)
+ *prog_groups |= LOCK_PROG_SRCU_READ;
+ if (mode & PBF_LOCK_MODE_WRITE)
+ *prog_groups |= LOCK_PROG_SRCU_WRITE;
} else {
*prog_groups |= LOCK_PROG_ALL;
}
@@ -729,16 +779,54 @@ static bool find_slab_cache(struct sample_filter_bpf *skel, const char *name,
return false;
}
+static u64 rcu_min_depth(struct evsel *evsel)
+{
+ if (evsel && evsel->core.attr.type == PERF_TYPE_SOFTWARE &&
+ evsel->core.attr.config != PERF_COUNT_SW_CPU_CLOCK &&
+ evsel->core.attr.config != PERF_COUNT_SW_TASK_CLOCK)
+ return 2;
+ return 1;
+}
+
+static bool btf_has_task_field(struct btf *btf, const char *field_name)
+{
+ const struct btf_type *t;
+ const struct btf_member *m;
+ s32 id;
+ u16 i, vlen;
+
+ if (!btf)
+ return false;
+
+ id = btf__find_by_name_kind(btf, "task_struct", BTF_KIND_STRUCT);
+ if (id < 0)
+ return false;
+
+ t = btf__type_by_id(btf, id);
+ if (!t)
+ return false;
+
+ m = btf_members(t);
+ vlen = btf_vlen(t);
+ for (i = 0; i < vlen; i++, m++) {
+ const char *name = btf__name_by_offset(btf, m->name_off);
+
+ if (name && !strcmp(name, field_name))
+ return true;
+ }
+ return false;
+}
+
/*
* Resolve a `lock_held(<name>)` filter expression into `entry->part`
* (`enum perf_bpf_lock_target_kind`) and `entry->target`, and populate
* the corresponding BPF filter maps and BSS control flags.
*
* Resolution order:
- * 1. Well-known per-process / semi-global locks: `mmap_lock`, `siglock`,
- * `rq_lock`, `zone_lock` (with or without leading '&').
+ * 1. Well-known per-process / semi-global / RCU locks: `mmap_lock`, `siglock`,
+ * `rq_lock`, `zone_lock`, `rcu`, `rcu-trace` (with or without leading '&').
* 2. Lock types / flags (when no leading '&'): `mutex`, `spinlock`,
- * `rwsem`, `rwsem:R`, `rwsem:W`, `rwlock`, `percpu-rwsem`,
+ * `rwsem`, `rwsem:R`, `rwsem:W`, `rwlock`, `percpu-rwsem`, `srcu`,
* `semaphore`, etc.
* 3. Hexadecimal lock address (when no leading '&').
* 4. Slab cache name (when prefixed with '&', e.g. `&dentry`).
@@ -746,8 +834,8 @@ static bool find_slab_cache(struct sample_filter_bpf *skel, const char *name,
* e.g. `&cgroup_mutex` or `cgroup_mutex`).
* 6. Slab cache name without leading '&' fallback.
*/
-static int resolve_lock_expr(struct sample_filter_bpf *skel,
- struct perf_bpf_filter_expr *expr,
+static int resolve_lock_expr(struct evsel *evsel, struct sample_filter_bpf *skel,
+ struct btf *btf, struct perf_bpf_filter_expr *expr,
struct perf_bpf_filter_entry *entry)
{
const char *name = expr->lock_name;
@@ -811,6 +899,48 @@ static int resolve_lock_expr(struct sample_filter_bpf *skel,
return 0;
}
+ if (!strcmp(base, "rcu_read_lock")) {
+ if (mode == PBF_LOCK_MODE_WRITE) {
+ pr_err("Error: Lock '%s' only supports read/shared mode\n", name);
+ return -EINVAL;
+ }
+ if (btf && !btf_has_task_field(btf, "rcu_read_lock_nesting")) {
+ pr_err("Error: lock_held(rcu) requires CONFIG_PREEMPT_RCU\n");
+ return -EOPNOTSUPP;
+ }
+ entry->part = PBF_LOCK_RCU | PBF_LOCK_MODE_READ;
+ entry->target = rcu_min_depth(evsel);
+ return 0;
+ }
+
+ if (!strcmp(base, "rcu")) {
+ if ((mode & PBF_LOCK_MODE_READ) && btf &&
+ !btf_has_task_field(btf, "rcu_read_lock_nesting")) {
+ pr_err("Error: lock_held(rcu) requires CONFIG_PREEMPT_RCU\n");
+ return -EOPNOTSUPP;
+ }
+ entry->part = PBF_LOCK_RCU | mode;
+ entry->target = rcu_min_depth(evsel);
+ if ((mode & PBF_LOCK_MODE_WRITE) && skel)
+ skel->bss->has_rcu_lock = 1;
+ return 0;
+ }
+
+ if (!strcmp(base, "rcu-trace") || !strcmp(base, "rcu_trace") ||
+ !strcmp(base, "rcu_read_lock_trace")) {
+ if (!(mode & PBF_LOCK_MODE_READ) || mode == PBF_LOCK_MODE_WRITE) {
+ pr_err("Error: Lock '%s' only supports read/shared mode\n", name);
+ return -EINVAL;
+ }
+ if (btf && !btf_has_task_field(btf, "trc_reader_nesting")) {
+ pr_err("Error: lock_held(rcu-trace) requires CONFIG_TASKS_TRACE_RCU\n");
+ return -EOPNOTSUPP;
+ }
+ entry->part = PBF_LOCK_RCU_TRACE | PBF_LOCK_MODE_READ;
+ entry->target = 0;
+ return 0;
+ }
+
if (!has_amp && parse_lock_type_name(base, mode, skel, &encoded_pair, NULL)) {
entry->part = PBF_LOCK_TYPE | mode;
entry->target = encoded_pair;
@@ -875,7 +1005,7 @@ static int resolve_lock_expr(struct sample_filter_bpf *skel,
}
static int get_filter_entries(struct evsel *evsel, struct sample_filter_bpf *skel,
- struct perf_bpf_filter_entry *entry)
+ struct btf *btf, struct perf_bpf_filter_entry *entry)
{
int i = 0, ret;
struct perf_bpf_filter_expr *expr;
@@ -893,7 +1023,7 @@ static int get_filter_entries(struct evsel *evsel, struct sample_filter_bpf *ske
entry[i].value = expr->val;
entry[i].target = 0;
if (expr->term == PBF_TERM_LOCK_HELD) {
- ret = resolve_lock_expr(skel, expr, &entry[i]);
+ ret = resolve_lock_expr(evsel, skel, btf, expr, &entry[i]);
if (ret < 0)
return ret;
}
@@ -912,7 +1042,7 @@ static int get_filter_entries(struct evsel *evsel, struct sample_filter_bpf *ske
entry[i].value = group->val;
entry[i].target = 0;
if (group->term == PBF_TERM_LOCK_HELD) {
- ret = resolve_lock_expr(skel, group, &entry[i]);
+ ret = resolve_lock_expr(evsel, skel, btf, group, &entry[i]);
if (ret < 0)
return ret;
}
@@ -1185,7 +1315,7 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
goto err;
}
- ret = get_filter_entries(evsel, NULL, entry);
+ ret = get_filter_entries(evsel, NULL, NULL, entry);
if (ret < 0) {
pr_err("Failed to process filter entries\n");
goto err;
@@ -1247,6 +1377,12 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
bpf_map__set_max_entries(skel->maps.lock_syms, MAX_ENTRIES);
init_numa_data(skel, btf);
}
+ if (prog_groups & LOCK_PROG_RCU_WRITE) {
+ u64 rcu_addr = 1ULL;
+
+ find_kernel_lock_sym("rcu_state", &rcu_addr);
+ skel->rodata->rcu_state_addr = rcu_addr;
+ }
ret = configure_lock_progs(skel, btf, prog_groups, needs_lock_syms,
needs_slab_iter);
if (ret < 0)
@@ -1264,7 +1400,7 @@ int perf_bpf_filter__prepare(struct evsel *evsel, struct target *target)
if (has_lock_filter && needs_slab_iter)
run_slab_cache_iter(skel);
- ret = get_filter_entries(evsel, skel, entry);
+ ret = get_filter_entries(evsel, skel, btf, entry);
if (ret < 0) {
pr_err("Failed to process filter entries\n");
goto err;
diff --git a/tools/perf/util/bpf_skel/lock_data.h b/tools/perf/util/bpf_skel/lock_data.h
index fed445c9d8b4..e32ba230d5f5 100644
--- a/tools/perf/util/bpf_skel/lock_data.h
+++ b/tools/perf/util/bpf_skel/lock_data.h
@@ -39,6 +39,7 @@ struct contention_task_data {
*/
#define LCD_F_MMAP_LOCK (1U << 31)
#define LCD_F_SIGHAND_LOCK (1U << 30)
+#define LCD_F_RCU_LOCK (1U << 29)
#define LCB_F_SLAB_ID_SHIFT 16
#define LCB_F_SLAB_ID_START (1U << 16)
@@ -52,9 +53,11 @@ struct contention_task_data {
#define LCB_F_RT (1U << 3)
#define LCB_F_PERCPU (1U << 4)
#define LCB_F_MUTEX (1U << 5)
+#define LCB_F_SRCU (1U << 6)
+#define LCB_F_RCU (1U << 7)
-#define LCB_F_TYPE_MAX (1U << 7)
-#define LCB_F_TYPE_MASK 0x0000007FU
+#define LCB_F_TYPE_MAX (1U << 8)
+#define LCB_F_TYPE_MASK 0x000000FFU
#define SLAB_NAME_MAX 28
@@ -233,6 +236,40 @@ static __always_inline __u64 get_current_siglock(__u64 curr)
return sh + bpf_core_field_offset(sighand->siglock);
}
+/*
+ * Check if `curr` is inside a preemptible RCU read-side critical section
+ * (`rcu_read_lock()`).
+ *
+ * Because `bpf_overflow_handler()` in `kernel/events/core.c` acquires
+ * `rcu_read_lock()` before invoking `perf_sample_filter` (and
+ * `do_perf_sw_event()` acquires an additional `rcu_read_lock()` for
+ * non-clock software events), `min_depth` is the baseline nesting count
+ * contributed by the perf event overflow handler itself (1 or 2).
+ */
+static __always_inline bool is_rcu_lock_held(__u64 curr, int min_depth)
+{
+ struct task_struct *task = (void *)curr;
+
+ if (!curr || !bpf_core_field_exists(task->rcu_read_lock_nesting))
+ return false;
+
+ return BPF_CORE_READ(task, rcu_read_lock_nesting) > min_depth;
+}
+
+/*
+ * Check if `curr` is inside a Tasks Trace RCU read-side critical section
+ * (`rcu_read_lock_trace()`).
+ */
+static __always_inline bool is_rcu_trace_lock_held(__u64 curr)
+{
+ struct task_struct *task = (void *)curr;
+
+ if (!curr || !bpf_core_field_exists(task->trc_reader_nesting))
+ return false;
+
+ return BPF_CORE_READ(task, trc_reader_nesting) > 0;
+}
+
/*
* Read the 1st function argument (`lock` / `sem` / `ssp`) as a 64-bit scalar.
*
@@ -265,6 +302,9 @@ static __always_inline __u32 check_lock_type(__u64 lock, __u32 flags)
if (get_current_siglock(curr) == lock)
return LCD_F_SIGHAND_LOCK;
break;
+ case LCB_F_RCU:
+ case LCB_F_RCU | LCB_F_WRITE:
+ return LCD_F_RCU_LOCK;
default:
break;
}
diff --git a/tools/perf/util/bpf_skel/sample-filter.h b/tools/perf/util/bpf_skel/sample-filter.h
index cd711d1d04b6..37c108608bbc 100644
--- a/tools/perf/util/bpf_skel/sample-filter.h
+++ b/tools/perf/util/bpf_skel/sample-filter.h
@@ -99,6 +99,18 @@ enum perf_bpf_lock_target_kind {
* low two bytes: `flag1 | (flag2 << 8)` (with 0xff indicating unused).
*/
PBF_LOCK_TYPE,
+ /*
+ * Preemptible RCU read-side lock (`rcu` / `rcu_read_lock`).
+ * Inspected directly via `current->rcu_read_lock_nesting > entry.target`,
+ * where `entry.target` is the baseline RCU nesting depth added by the
+ * kernel perf overflow path.
+ */
+ PBF_LOCK_RCU,
+ /*
+ * Tasks Trace RCU read-side lock (`rcu-trace` / `rcu_read_lock_trace`).
+ * Inspected directly via `current->trc_reader_nesting > 0`.
+ */
+ PBF_LOCK_RCU_TRACE,
};
/*
diff --git a/tools/perf/util/bpf_skel/sample_filter.bpf.c b/tools/perf/util/bpf_skel/sample_filter.bpf.c
index fbd2e51815d7..22d9b01e5851 100644
--- a/tools/perf/util/bpf_skel/sample_filter.bpf.c
+++ b/tools/perf/util/bpf_skel/sample_filter.bpf.c
@@ -123,6 +123,7 @@ volatile const int use_idx_hash;
const volatile __u64 contig_page_data_addr;
const volatile __u64 node_data_addr;
+const volatile __u64 rcu_state_addr;
const volatile int nr_nodes;
const volatile int sizeof_zone;
@@ -136,6 +137,7 @@ int has_lock_class;
int has_lock_type;
int has_mmap_lock;
int has_siglock;
+int has_rcu_lock;
__u8 lock_type_filter[LCB_F_TYPE_MAX];
void *bpf_cast_to_kern_ctx(void *) __ksym;
@@ -269,6 +271,20 @@ static __always_inline __u64 check_lock_held(struct perf_bpf_filter_entry *entry
case PBF_LOCK_TYPE:
return is_lock_type_in_map(entry->target, mode, pid, cpu_id);
+ case PBF_LOCK_RCU:
+ if ((mode & PBF_LOCK_MODE_READ) &&
+ is_rcu_lock_held(curr, (int)entry->target))
+ return 1;
+ if ((mode & PBF_LOCK_MODE_WRITE) && rcu_state_addr &&
+ is_lock_in_map(rcu_state_addr, PBF_LOCK_ADDR, PBF_LOCK_MODE_WRITE, pid, cpu_id))
+ return 1;
+ return 0;
+
+ case PBF_LOCK_RCU_TRACE:
+ if ((mode & PBF_LOCK_MODE_READ) && is_rcu_trace_lock_held(curr))
+ return 1;
+ return 0;
+
default:
return 0;
}
@@ -600,11 +616,12 @@ static __always_inline int record_lock_acquired_owner(__u64 lock_addr, __u32 fla
if (has_lock_addr && bpf_map_lookup_elem(&lock_addr_filter, &lock_addr))
matched = true;
- if (has_mmap_lock || has_siglock) {
+ if (has_mmap_lock || has_siglock || has_rcu_lock) {
__u32 ltype = check_lock_type(lock_addr, flags);
if ((has_mmap_lock && ltype == LCD_F_MMAP_LOCK) ||
- (has_siglock && ltype == LCD_F_SIGHAND_LOCK))
+ (has_siglock && ltype == LCD_F_SIGHAND_LOCK) ||
+ (has_rcu_lock && ltype == LCD_F_RCU_LOCK))
matched = true;
}
@@ -1202,6 +1219,120 @@ int BPF_PROG(fentry_percpu_up_write, void *sem)
return record_lock_released(ctx, LCB_F_PERCPU | LCB_F_WRITE);
}
+/* SRCU acquire/release and synchronize hooks */
+SEC("fexit/__srcu_read_lock")
+int BPF_PROG(fexit_srcu_read_lock, void *ssp, int ret)
+{
+ return record_lock_acquired(ctx, LCB_F_SRCU | LCB_F_READ);
+}
+
+SEC("fentry/__srcu_read_unlock")
+int BPF_PROG(fentry_srcu_read_unlock, void *ssp)
+{
+ return record_lock_released(ctx, LCB_F_SRCU | LCB_F_READ);
+}
+
+SEC("fexit/__srcu_read_lock_nmisafe")
+int BPF_PROG(fexit_srcu_read_lock_nmisafe, void *ssp, int ret)
+{
+ return record_lock_acquired(ctx, LCB_F_SRCU | LCB_F_READ);
+}
+
+SEC("fentry/__srcu_read_unlock_nmisafe")
+int BPF_PROG(fentry_srcu_read_unlock_nmisafe, void *ssp)
+{
+ return record_lock_released(ctx, LCB_F_SRCU | LCB_F_READ);
+}
+
+SEC("fentry/synchronize_srcu")
+int BPF_PROG(fentry_synchronize_srcu, void *ssp)
+{
+ return record_lock_acquired(ctx, LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fexit/synchronize_srcu")
+int BPF_PROG(fexit_synchronize_srcu, void *ssp)
+{
+ return record_lock_released(ctx, LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fentry/synchronize_srcu_expedited")
+int BPF_PROG(fentry_synchronize_srcu_expedited, void *ssp)
+{
+ return record_lock_acquired(ctx, LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fexit/synchronize_srcu_expedited")
+int BPF_PROG(fexit_synchronize_srcu_expedited, void *ssp)
+{
+ return record_lock_released(ctx, LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fentry/srcu_barrier")
+int BPF_PROG(fentry_srcu_barrier, void *ssp)
+{
+ return record_lock_acquired(ctx, LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fexit/srcu_barrier")
+int BPF_PROG(fexit_srcu_barrier, void *ssp)
+{
+ return record_lock_released(ctx, LCB_F_SRCU | LCB_F_WRITE);
+}
+
+/* RCU grace-period synchronize / barrier hooks */
+SEC("fentry/synchronize_rcu")
+int BPF_PROG(fentry_synchronize_rcu)
+{
+ return record_lock_acquired_owner(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE,
+ get_lock_owner_id(LCB_F_RCU | LCB_F_WRITE));
+}
+
+SEC("fexit/synchronize_rcu")
+int BPF_PROG(fexit_synchronize_rcu)
+{
+ return record_lock_released_addr(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
+SEC("fentry/synchronize_rcu_expedited")
+int BPF_PROG(fentry_synchronize_rcu_expedited)
+{
+ return record_lock_acquired_owner(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE,
+ get_lock_owner_id(LCB_F_RCU | LCB_F_WRITE));
+}
+
+SEC("fexit/synchronize_rcu_expedited")
+int BPF_PROG(fexit_synchronize_rcu_expedited)
+{
+ return record_lock_released_addr(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
+SEC("fentry/synchronize_rcu_tasks")
+int BPF_PROG(fentry_synchronize_rcu_tasks)
+{
+ return record_lock_acquired_owner(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE,
+ get_lock_owner_id(LCB_F_RCU | LCB_F_WRITE));
+}
+
+SEC("fexit/synchronize_rcu_tasks")
+int BPF_PROG(fexit_synchronize_rcu_tasks)
+{
+ return record_lock_released_addr(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
+SEC("fentry/rcu_barrier")
+int BPF_PROG(fentry_rcu_barrier)
+{
+ return record_lock_acquired_owner(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE,
+ get_lock_owner_id(LCB_F_RCU | LCB_F_WRITE));
+}
+
+SEC("fexit/rcu_barrier")
+int BPF_PROG(fexit_rcu_barrier)
+{
+ return record_lock_released_addr(rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
SEC("raw_tp/bpf_test_finish")
int BPF_PROG(collect_lock_syms)
{
diff --git a/tools/perf/util/bpf_skel/vmlinux/vmlinux.h b/tools/perf/util/bpf_skel/vmlinux/vmlinux.h
index b75ddc6846bb..1fb91a829d54 100644
--- a/tools/perf/util/bpf_skel/vmlinux/vmlinux.h
+++ b/tools/perf/util/bpf_skel/vmlinux/vmlinux.h
@@ -119,6 +119,8 @@ struct task_struct {
struct signal_struct *signal;
struct sighand_struct *sighand;
struct css_set *cgroups;
+ int rcu_read_lock_nesting;
+ int trc_reader_nesting;
} __attribute__((preserve_access_index));
struct trace_entry {
diff --git a/tools/perf/util/lock-contention.c b/tools/perf/util/lock-contention.c
index a1bb78d6e29c..b45b075f3346 100644
--- a/tools/perf/util/lock-contention.c
+++ b/tools/perf/util/lock-contention.c
@@ -38,6 +38,10 @@ const struct perf_lock_type lock_type_table[] = {
{ LCB_F_MUTEX | LCB_F_SPIN, "mutex", "mutex" },
/* alias for optimistic spinning only */
{ LCB_F_MUTEX | LCB_F_SPIN, "mutex:spin", "mutex-spin" },
+ { LCB_F_SRCU | LCB_F_READ, "srcu:R", "srcu" },
+ { LCB_F_SRCU | LCB_F_WRITE, "srcu:W", "srcu" },
+ { LCB_F_RCU | LCB_F_READ, "rcu:R", "rcu" },
+ { LCB_F_RCU | LCB_F_WRITE, "rcu:W", "rcu" },
};
int parse_call_stack(const struct option *opt __maybe_unused, const char *str,
diff --git a/tools/perf/util/lock-contention.h b/tools/perf/util/lock-contention.h
index 68e9bd77e8a9..88b2c11244ad 100644
--- a/tools/perf/util/lock-contention.h
+++ b/tools/perf/util/lock-contention.h
@@ -128,6 +128,8 @@ struct thread_stat {
#define LCB_F_RT (1U << 3)
#define LCB_F_PERCPU (1U << 4)
#define LCB_F_MUTEX (1U << 5)
+#define LCB_F_SRCU (1U << 6)
+#define LCB_F_RCU (1U << 7)
struct perf_lock_type {
unsigned int flags;
@@ -140,7 +142,7 @@ struct perf_lock_type {
const char *lock_name;
};
-extern const struct perf_lock_type lock_type_table[14];
+extern const struct perf_lock_type lock_type_table[18];
struct evlist;
struct machine;
--
2.56.0.212.g129ede20d9-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v1 5/5] perf lock contention: Extend BPF lock contention to RCU and SRCU
2026-10-17 5:45 [PATCH v1 0/5] perf bpf-filter / lock contention: Add lock_held() sample filter and RCU/SRCU support Ian Rogers
` (3 preceding siblings ...)
2026-10-17 5:45 ` [PATCH v1 4/5] perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU, and SRCU Ian Rogers
@ 2026-10-17 5:45 ` Ian Rogers
2026-10-17 6:08 ` sashiko-bot
4 siblings, 1 reply; 11+ messages in thread
From: Ian Rogers @ 2026-10-17 5:45 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Ian Rogers, Adrian Hunter,
James Clark, linux-perf-users, linux-kernel, rcu
Extend 'perf lock contention --use-bpf' to track RCU and SRCU
grace-period and barrier waits:
- Attach fentry/fexit programs to synchronize_rcu(),
synchronize_rcu_expedited(), synchronize_rcu_tasks(), rcu_barrier(),
synchronize_srcu(), synchronize_srcu_expedited(), and srcu_barrier()
when available in BTF.
- Support filtering by lock type ('-Y rcu', '-Y rcu:W', '-Y srcu',
'-Y srcu:W') and by lock name ('-L rcu' or specific srcu_struct
symbol names).
Example usage:
# perf lock con -a -b -Y rcu,srcu -F wait_total,avg_wait -- sleep 1
total wait avg wait type caller
35.12 ms 8.78 ms rcu:W cleanup_net+0x284
7.84 ms 3.92 ms srcu:W __fsnotify_parent+0x114
# perf lock con -a -b -l -L rcu -F wait_total,avg_wait -- sleep 1
total wait avg wait address symbol
35.12 ms 8.78 ms ffffffff83754dc0 rcu (rcu)
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/Documentation/perf-lock.txt | 7 +-
tools/perf/tests/shell/lock_contention.sh | 14 ++
tools/perf/util/bpf_lock_contention.c | 167 ++++++++++++++--
.../perf/util/bpf_skel/lock_contention.bpf.c | 183 ++++++++++++++----
4 files changed, 322 insertions(+), 49 deletions(-)
diff --git a/tools/perf/Documentation/perf-lock.txt b/tools/perf/Documentation/perf-lock.txt
index c17b3e318169..bfeb98c976e3 100644
--- a/tools/perf/Documentation/perf-lock.txt
+++ b/tools/perf/Documentation/perf-lock.txt
@@ -188,11 +188,14 @@ CONTENTION OPTIONS
Show lock contention only for given lock types (comma separated list).
Available values are:
semaphore, spinlock, rwlock, rwlock:R, rwlock:W, rwsem, rwsem:R, rwsem:W,
- rtmutex, rwlock-rt, rwlock-rt:R, rwlock-rt:W, percpu-rwmem, pcpu-sem,
- pcpu-sem:R, pcpu-sem:W, mutex
+ rtmutex, rwlock-rt, rwlock-rt:R, rwlock-rt:W, percpu-rwsem, pcpu-sem,
+ pcpu-sem:R, pcpu-sem:W, mutex, srcu, srcu:W, rcu, rcu:W
Note that RW-variant of locks have :R and :W suffix. Names without the
suffix are shortcuts for the both variants. Ex) rwsem = rwsem:R + rwsem:W.
+ For RCU and SRCU with --use-bpf, contention tracks grace-period and
+ barrier waits (synchronize_rcu*(), synchronize_srcu*(), rcu_barrier(),
+ srcu_barrier()).
-L::
--lock-filter=<value>::
diff --git a/tools/perf/tests/shell/lock_contention.sh b/tools/perf/tests/shell/lock_contention.sh
index 5df9a0cd48a3..c6d37c910d4b 100755
--- a/tools/perf/tests/shell/lock_contention.sh
+++ b/tools/perf/tests/shell/lock_contention.sh
@@ -181,6 +181,13 @@ test_type_filter()
err=1
exit
fi
+
+ perf lock con -a -b -Y rcu -q -- true > /dev/null 2> ${result}
+ if [ "$(grep -c -v rcu "${result}")" != "0" ]; then
+ echo "[Fail] BPF result should not have non-rcu locks:" "$(cat "${result}")"
+ err=1
+ exit
+ fi
}
test_lock_filter()
@@ -224,6 +231,13 @@ test_lock_filter()
err=1
exit
fi
+
+ perf lock con -a -b -L rcu -q -- true > /dev/null 2> ${result}
+ if [ "$(grep -c -v rcu "${result}")" != "0" ]; then
+ echo "[Fail] BPF result should not have non-rcu locks:" "$(cat "${result}")"
+ err=1
+ exit
+ fi
}
test_stack_filter()
diff --git a/tools/perf/util/bpf_lock_contention.c b/tools/perf/util/bpf_lock_contention.c
index c20bd075664e..fa7df9187b08 100644
--- a/tools/perf/util/bpf_lock_contention.c
+++ b/tools/perf/util/bpf_lock_contention.c
@@ -16,6 +16,7 @@
#include <bpf/bpf.h>
#include <bpf/btf.h>
#include <inttypes.h>
+#include <unistd.h>
#include "bpf_skel/lock_contention.skel.h"
#include "bpf_skel/lock_data.h"
@@ -180,6 +181,129 @@ static void init_numa_data(struct lock_contention *con)
free(buf);
}
+static bool has_bpf_fentry_support(struct btf *btf)
+{
+ /*
+ * Verify both BPF_TRACE_FENTRY trampoline support (5.5+) and
+ * bpf_get_func_arg() helper availability (5.17+) used by
+ * get_lock_addr() in the SRCU fentry/fexit hooks.
+ */
+ struct bpf_insn insns[] = {
+ { .code = BPF_ALU64 | BPF_MOV | BPF_K, .dst_reg = BPF_REG_2, .imm = 0 },
+ { .code = BPF_ALU64 | BPF_MOV | BPF_X, .dst_reg = BPF_REG_3,
+ .src_reg = BPF_REG_10 },
+ { .code = BPF_ALU64 | BPF_ADD | BPF_K, .dst_reg = BPF_REG_3, .imm = -8 },
+ { .code = BPF_JMP | BPF_CALL, .imm = BPF_FUNC_get_func_arg },
+ { .code = BPF_ALU64 | BPF_MOV | BPF_K, .dst_reg = BPF_REG_0, .imm = 0 },
+ { .code = BPF_JMP | BPF_EXIT },
+ };
+ LIBBPF_OPTS(bpf_prog_load_opts, opts,
+ .expected_attach_type = BPF_TRACE_FENTRY,
+ );
+ s32 btf_id;
+ int prog_fd, link_fd;
+
+ if (!btf)
+ return false;
+
+ btf_id = btf__find_by_name_kind(btf, "synchronize_rcu", BTF_KIND_FUNC);
+ if (btf_id <= 0)
+ return false;
+
+ opts.attach_btf_id = btf_id;
+ prog_fd = bpf_prog_load(BPF_PROG_TYPE_TRACING, NULL, "GPL",
+ insns, ARRAY_SIZE(insns), &opts);
+ if (prog_fd < 0)
+ return false;
+
+ link_fd = bpf_raw_tracepoint_open(NULL, prog_fd);
+ if (link_fd >= 0)
+ close(link_fd);
+ close(prog_fd);
+ return link_fd >= 0;
+}
+
+static void configure_rcu_contention_progs(struct lock_contention *con,
+ bool has_rcu_lock)
+{
+ static const struct {
+ size_t prog_offset;
+ const char *func;
+ bool is_srcu;
+ } rcu_progs[] = {
+ { offsetof(struct lock_contention_bpf, progs.fentry_synchronize_rcu),
+ "synchronize_rcu", false },
+ { offsetof(struct lock_contention_bpf, progs.fexit_synchronize_rcu),
+ "synchronize_rcu", false },
+ { offsetof(struct lock_contention_bpf, progs.fentry_synchronize_rcu_expedited),
+ "synchronize_rcu_expedited", false },
+ { offsetof(struct lock_contention_bpf, progs.fexit_synchronize_rcu_expedited),
+ "synchronize_rcu_expedited", false },
+ { offsetof(struct lock_contention_bpf, progs.fentry_synchronize_rcu_tasks),
+ "synchronize_rcu_tasks", false },
+ { offsetof(struct lock_contention_bpf, progs.fexit_synchronize_rcu_tasks),
+ "synchronize_rcu_tasks", false },
+ { offsetof(struct lock_contention_bpf, progs.fentry_rcu_barrier),
+ "rcu_barrier", false },
+ { offsetof(struct lock_contention_bpf, progs.fexit_rcu_barrier),
+ "rcu_barrier", false },
+ { offsetof(struct lock_contention_bpf, progs.fentry_synchronize_srcu),
+ "synchronize_srcu", true },
+ { offsetof(struct lock_contention_bpf, progs.fexit_synchronize_srcu),
+ "synchronize_srcu", true },
+ { offsetof(struct lock_contention_bpf, progs.fentry_synchronize_srcu_expedited),
+ "synchronize_srcu_expedited", true },
+ { offsetof(struct lock_contention_bpf, progs.fexit_synchronize_srcu_expedited),
+ "synchronize_srcu_expedited", true },
+ { offsetof(struct lock_contention_bpf, progs.fentry_srcu_barrier),
+ "srcu_barrier", true },
+ { offsetof(struct lock_contention_bpf, progs.fexit_srcu_barrier),
+ "srcu_barrier", true },
+ };
+ struct symbol *sym;
+ struct map *kmap;
+ bool need_rcu = true, need_srcu = true;
+ bool fentry_ok;
+
+ sym = machine__find_kernel_symbol_by_name(con->machine, "rcu_state", &kmap);
+ if (sym) {
+ skel->rodata->rcu_state_addr = map__unmap_ip(kmap, sym->start);
+ map__put(kmap);
+ } else {
+ skel->rodata->rcu_state_addr = 1ULL;
+ }
+
+ if (con->filters->nr_types) {
+ need_rcu = false;
+ need_srcu = false;
+ for (int i = 0; i < con->filters->nr_types; i++) {
+ if (con->filters->types[i] & LCB_F_RCU)
+ need_rcu = true;
+ if (con->filters->types[i] & LCB_F_SRCU)
+ need_srcu = true;
+ }
+ }
+
+ if (con->filters->nr_syms || con->filters->nr_addrs || con->filters->nr_slabs) {
+ if (!has_rcu_lock && !con->filters->nr_addrs)
+ need_rcu = false;
+ if (!con->filters->nr_addrs && !con->filters->nr_slabs)
+ need_srcu = false;
+ }
+
+ fentry_ok = (need_rcu || need_srcu) && has_bpf_fentry_support(con->btf);
+
+ for (size_t i = 0; i < ARRAY_SIZE(rcu_progs); i++) {
+ struct bpf_program **progp = (void *)skel + rcu_progs[i].prog_offset;
+ bool want = rcu_progs[i].is_srcu ? need_srcu : need_rcu;
+ bool enable = fentry_ok && want &&
+ btf__find_by_name_kind(con->btf, rcu_progs[i].func,
+ BTF_KIND_FUNC) > 0;
+
+ bpf_program__set_autoload(*progp, enable);
+ }
+}
+
int lock_contention_prepare(struct lock_contention *con)
{
int i, fd;
@@ -187,6 +311,7 @@ int lock_contention_prepare(struct lock_contention *con)
struct evlist *evlist = con->evlist;
struct target *target = con->target;
bool has_mmap_lock = false;
+ bool has_rcu_lock = false;
/* make sure it loads the kernel map before lookup */
map__load(machine__kernel_map(con->machine));
@@ -200,6 +325,7 @@ int lock_contention_prepare(struct lock_contention *con)
bpf_map__set_value_size(skel->maps.stacks, con->max_stack * sizeof(u64));
bpf_map__set_max_entries(skel->maps.lock_stat, con->map_nr_entries);
bpf_map__set_max_entries(skel->maps.tstamp, con->map_nr_entries);
+ bpf_map__set_max_entries(skel->maps.tstamp_rcu, con->map_nr_entries);
if (con->aggr_mode == LOCK_AGGR_TASK)
bpf_map__set_max_entries(skel->maps.task_data, con->map_nr_entries);
@@ -249,6 +375,10 @@ int lock_contention_prepare(struct lock_contention *con)
has_mmap_lock = true;
continue;
}
+ if (!strcmp(con->filters->syms[i], "rcu")) {
+ has_rcu_lock = true;
+ continue;
+ }
sym = machine__find_kernel_symbol_by_name(con->machine,
con->filters->syms[i],
@@ -269,7 +399,7 @@ int lock_contention_prepare(struct lock_contention *con)
addrs[con->filters->nr_addrs++] = map__unmap_ip(kmap, sym->start);
con->filters->addrs = addrs;
}
- naddrs = con->filters->nr_addrs ?: has_mmap_lock;
+ naddrs = con->filters->nr_addrs ?: (has_mmap_lock || has_rcu_lock);
skel->rodata->has_addr = 1;
}
@@ -305,6 +435,7 @@ int lock_contention_prepare(struct lock_contention *con)
skel->rodata->needs_callstack = con->save_callstack;
skel->rodata->lock_owner = con->owner;
skel->rodata->has_mmap_lock = has_mmap_lock;
+ skel->rodata->has_rcu_lock = has_rcu_lock;
if (con->aggr_mode == LOCK_AGGR_CGROUP || con->filters->nr_cgrps) {
if (cgroup_is_v2("perf_event"))
@@ -321,6 +452,7 @@ int lock_contention_prepare(struct lock_contention *con)
bpf_map__set_max_entries(skel->maps.slab_filter, nslabs);
init_numa_data(con);
+ configure_rcu_contention_progs(con, has_rcu_lock);
if (lock_contention_bpf__load(skel) < 0) {
pr_err("Failed to load lock-contention BPF skeleton\n");
@@ -503,23 +635,30 @@ static void account_end_timestamp(struct lock_contention *con)
enum lock_aggr_mode aggr_mode = con->aggr_mode;
struct tstamp_data ts_data, *cpu_data;
- /* Iterate per-task tstamp map (key = TID) */
- ts_fd = bpf_map__fd(skel->maps.tstamp);
+ /* Iterate per-task tstamp maps (key = TID) */
+ int ts_fds[] = {
+ bpf_map__fd(skel->maps.tstamp),
+ bpf_map__fd(skel->maps.tstamp_rcu),
+ };
+
stat_fd = bpf_map__fd(skel->maps.lock_stat);
- prev_key = NULL;
- while (!bpf_map_get_next_key(ts_fd, prev_key, &key)) {
- if (bpf_map_lookup_elem(ts_fd, &key, &ts_data) == 0) {
- int pid = key;
+ for (size_t i = 0; i < ARRAY_SIZE(ts_fds); i++) {
+ ts_fd = ts_fds[i];
+ prev_key = NULL;
+ while (!bpf_map_get_next_key(ts_fd, prev_key, &key)) {
+ if (bpf_map_lookup_elem(ts_fd, &key, &ts_data) == 0) {
+ int pid = key;
- if (aggr_mode == LOCK_AGGR_TASK && con->owner)
- pid = ts_data.flags;
+ if (aggr_mode == LOCK_AGGR_TASK && con->owner)
+ pid = ts_data.flags;
- update_lock_stat(stat_fd, pid, end_ts, aggr_mode,
- &ts_data);
- }
+ update_lock_stat(stat_fd, pid, end_ts, aggr_mode,
+ &ts_data);
+ }
- prev_key = &key;
+ prev_key = &key;
+ }
}
/* Now it'll check per-cpu tstamp map which doesn't have TID. */
@@ -604,6 +743,8 @@ static const char *lock_contention_get_name(struct lock_contention *con,
return "mmap_lock";
if (flags & LCD_F_SIGHAND_LOCK)
return "siglock";
+ if (flags & LCD_F_RCU_LOCK)
+ return "rcu";
/* global locks with symbols */
sym = machine__find_kernel_symbol(machine, key->lock_addr_or_cgroup, &kmap);
diff --git a/tools/perf/util/bpf_skel/lock_contention.bpf.c b/tools/perf/util/bpf_skel/lock_contention.bpf.c
index 0f2118006005..257e3402e815 100644
--- a/tools/perf/util/bpf_skel/lock_contention.bpf.c
+++ b/tools/perf/util/bpf_skel/lock_contention.bpf.c
@@ -59,6 +59,18 @@ struct {
__uint(max_entries, MAX_ENTRIES);
} tstamp SEC(".maps");
+/*
+ * Separate per-task timestamp map for RCU/SRCU grace-period and barrier waits
+ * so they do not collide with inner sleeping lock contentions (such as
+ * rcu_barrier_mutex or srcu_cb_mutex) acquired during the wait.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __type(key, int);
+ __type(value, struct tstamp_data);
+ __uint(max_entries, MAX_ENTRIES);
+} tstamp_rcu SEC(".maps");
+
/* maintain per-CPU timestamp at the beginning of contention */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
@@ -160,6 +172,7 @@ const volatile int has_addr;
const volatile int has_cgroup;
const volatile int has_slab;
const volatile int has_mmap_lock;
+const volatile int has_rcu_lock;
const volatile int needs_callstack;
const volatile int stack_skip;
const volatile int lock_owner;
@@ -172,6 +185,7 @@ const volatile int aggr_mode;
const volatile __u64 contig_page_data_addr;
const volatile __u64 node_data_addr;
+const volatile __u64 rcu_state_addr;
const volatile int nr_nodes;
const volatile int sizeof_zone;
@@ -218,7 +232,7 @@ static inline __u64 get_current_cgroup_id(void)
return BPF_CORE_READ(cgrp, kn, id);
}
-static inline int can_record(u64 *ctx)
+static __always_inline int can_record(__u64 lock, __u32 flags)
{
bool is_addr_ok = false;
@@ -242,7 +256,6 @@ static inline int can_record(u64 *ctx)
if (has_type) {
__u8 *ok;
- __u32 flags = (__u32)ctx[1];
ok = bpf_map_lookup_elem(&type_filter, &flags);
if (!ok)
@@ -251,10 +264,9 @@ static inline int can_record(u64 *ctx)
if (has_addr) {
__u8 *ok;
- __u64 addr = ctx[0];
- ok = bpf_map_lookup_elem(&addr_filter, &addr);
- if (!ok && !has_slab && !has_mmap_lock)
+ ok = bpf_map_lookup_elem(&addr_filter, &lock);
+ if (!ok && !has_slab && !has_mmap_lock && !has_rcu_lock)
return 0;
is_addr_ok = !!ok;
@@ -272,26 +284,27 @@ static inline int can_record(u64 *ctx)
if (is_addr_ok)
return 1;
- /* slab and mmap_lock are part of the addr_filter */
+ /* slab, mmap_lock, and rcu_lock are part of the addr_filter */
if (has_slab && bpf_get_kmem_cache) {
__u8 *ok;
- __u64 addr = ctx[0];
long kmem_cache_addr;
- kmem_cache_addr = (long)bpf_get_kmem_cache(addr);
+ kmem_cache_addr = (long)bpf_get_kmem_cache(lock);
ok = bpf_map_lookup_elem(&slab_filter, &kmem_cache_addr);
if (ok)
return 1;
- else if (!has_mmap_lock)
+ else if (!has_mmap_lock && !has_rcu_lock)
return 0;
}
- if (has_mmap_lock) {
- __u64 lock = ctx[0];
- __u32 flag = ctx[1];
+ if (has_mmap_lock || has_rcu_lock) {
+ __u32 ltype = check_lock_type(lock, flags);
- if (check_lock_type(lock, flag) != LCD_F_MMAP_LOCK)
- return 0;
+ if (has_mmap_lock && ltype == LCD_F_MMAP_LOCK)
+ return 1;
+ if (has_rcu_lock && ltype == LCD_F_RCU_LOCK)
+ return 1;
+ return 0;
}
return 1;
@@ -351,6 +364,7 @@ static inline struct tstamp_data *get_tstamp_elem(__u32 flags)
{
__u32 pid;
struct tstamp_data *pelem;
+ void *map;
/* Use per-cpu array map for spinlock and rwlock */
if ((flags & (LCB_F_SPIN | LCB_F_MUTEX)) == LCB_F_SPIN) {
@@ -363,8 +377,9 @@ static inline struct tstamp_data *get_tstamp_elem(__u32 flags)
return pelem;
}
+ map = (flags & (LCB_F_RCU | LCB_F_SRCU)) ? (void *)&tstamp_rcu : (void *)&tstamp;
pid = bpf_get_current_pid_tgid();
- pelem = bpf_map_lookup_elem(&tstamp, &pid);
+ pelem = bpf_map_lookup_elem(map, &pid);
/* Do not update the element for nested locks */
if (pelem && pelem->lock)
return NULL;
@@ -372,12 +387,12 @@ static inline struct tstamp_data *get_tstamp_elem(__u32 flags)
if (pelem == NULL) {
struct tstamp_data zero = {};
- if (bpf_map_update_elem(&tstamp, &pid, &zero, BPF_NOEXIST) < 0) {
+ if (bpf_map_update_elem(map, &pid, &zero, BPF_NOEXIST) < 0) {
__sync_fetch_and_add(&task_fail, 1);
return NULL;
}
- pelem = bpf_map_lookup_elem(&tstamp, &pid);
+ pelem = bpf_map_lookup_elem(map, &pid);
if (pelem == NULL) {
__sync_fetch_and_add(&task_fail, 1);
return NULL;
@@ -474,21 +489,20 @@ static inline void update_owner_stat(u32 id, u64 duration, u32 flags)
}
}
-SEC("tp_btf/contention_begin")
-int contention_begin(u64 *ctx)
+static __always_inline int do_contention_begin(void *ctx, __u64 lock, __u32 flags)
{
struct tstamp_data *pelem;
- if (!enabled || !can_record(ctx))
+ if (!enabled || !can_record(lock, flags))
return 0;
- pelem = get_tstamp_elem(ctx[1]);
+ pelem = get_tstamp_elem(flags);
if (pelem == NULL)
return 0;
pelem->timestamp = bpf_ktime_get_ns();
- pelem->lock = (__u64)ctx[0];
- pelem->flags = (__u32)ctx[1];
+ pelem->lock = lock;
+ pelem->flags = flags;
if (aggr_mode == LOCK_AGGR_CGROUP)
pelem->cgroup_id = get_current_cgroup_id();
@@ -588,8 +602,7 @@ int contention_begin(u64 *ctx)
return 0;
}
-SEC("tp_btf/contention_end")
-int contention_end(u64 *ctx)
+static __always_inline int do_contention_end(__u64 lock, __s32 ret)
{
__u32 pid = 0, idx = 0;
struct tstamp_data *pelem;
@@ -597,7 +610,7 @@ int contention_end(u64 *ctx)
struct contention_data *data;
__u64 timestamp;
__u64 duration;
- bool need_delete = false;
+ void *ts_map = NULL;
if (!enabled)
return 0;
@@ -613,14 +626,19 @@ int contention_end(u64 *ctx)
*/
pelem = bpf_map_lookup_elem(&tstamp_cpu, &idx);
if (pelem && pelem->lock) {
- if (pelem->lock != ctx[0])
+ if (pelem->lock != lock)
return 0;
} else {
pid = bpf_get_current_pid_tgid();
pelem = bpf_map_lookup_elem(&tstamp, &pid);
- if (!pelem || pelem->lock != ctx[0])
- return 0;
- need_delete = true;
+ if (pelem && pelem->lock == lock) {
+ ts_map = &tstamp;
+ } else {
+ pelem = bpf_map_lookup_elem(&tstamp_rcu, &pid);
+ if (!pelem || pelem->lock != lock)
+ return 0;
+ ts_map = &tstamp_rcu;
+ }
}
timestamp = bpf_ktime_get_ns();
@@ -648,7 +666,6 @@ int contention_end(u64 *ctx)
*/
else {
u32 i = 0;
- s32 ret = (s32)ctx[1];
u64 *buf;
otdata->timestamp = timestamp;
@@ -713,7 +730,7 @@ int contention_end(u64 *ctx)
if (lock_owner)
key.pid = pelem->flags;
else {
- if (!need_delete)
+ if (!ts_map)
pid = bpf_get_current_pid_tgid();
key.pid = pid;
}
@@ -795,11 +812,109 @@ int contention_end(u64 *ctx)
check_lock_delay(pelem->lock);
pelem->lock = 0;
- if (need_delete)
- bpf_map_delete_elem(&tstamp, &pid);
+ if (ts_map)
+ bpf_map_delete_elem(ts_map, &pid);
return 0;
}
+SEC("tp_btf/contention_begin")
+int contention_begin(u64 *ctx)
+{
+ return do_contention_begin(ctx, (__u64)ctx[0], (__u32)ctx[1]);
+}
+
+SEC("tp_btf/contention_end")
+int contention_end(u64 *ctx)
+{
+ return do_contention_end((__u64)ctx[0], (__s32)ctx[1]);
+}
+
+/* RCU grace-period synchronize / barrier contention hooks */
+SEC("fentry/synchronize_rcu")
+int BPF_PROG(fentry_synchronize_rcu)
+{
+ return do_contention_begin(ctx, rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
+SEC("fexit/synchronize_rcu")
+int BPF_PROG(fexit_synchronize_rcu)
+{
+ return do_contention_end(rcu_state_addr, 0);
+}
+
+SEC("fentry/synchronize_rcu_expedited")
+int BPF_PROG(fentry_synchronize_rcu_expedited)
+{
+ return do_contention_begin(ctx, rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
+SEC("fexit/synchronize_rcu_expedited")
+int BPF_PROG(fexit_synchronize_rcu_expedited)
+{
+ return do_contention_end(rcu_state_addr, 0);
+}
+
+SEC("fentry/synchronize_rcu_tasks")
+int BPF_PROG(fentry_synchronize_rcu_tasks)
+{
+ return do_contention_begin(ctx, rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
+SEC("fexit/synchronize_rcu_tasks")
+int BPF_PROG(fexit_synchronize_rcu_tasks)
+{
+ return do_contention_end(rcu_state_addr, 0);
+}
+
+SEC("fentry/rcu_barrier")
+int BPF_PROG(fentry_rcu_barrier)
+{
+ return do_contention_begin(ctx, rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
+}
+
+SEC("fexit/rcu_barrier")
+int BPF_PROG(fexit_rcu_barrier)
+{
+ return do_contention_end(rcu_state_addr, 0);
+}
+
+/* SRCU grace-period synchronize / barrier contention hooks */
+SEC("fentry/synchronize_srcu")
+int BPF_PROG(fentry_synchronize_srcu, void *ssp)
+{
+ return do_contention_begin(ctx, get_lock_addr(ctx), LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fexit/synchronize_srcu")
+int BPF_PROG(fexit_synchronize_srcu, void *ssp)
+{
+ return do_contention_end(get_lock_addr(ctx), 0);
+}
+
+SEC("fentry/synchronize_srcu_expedited")
+int BPF_PROG(fentry_synchronize_srcu_expedited, void *ssp)
+{
+ return do_contention_begin(ctx, get_lock_addr(ctx), LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fexit/synchronize_srcu_expedited")
+int BPF_PROG(fexit_synchronize_srcu_expedited, void *ssp)
+{
+ return do_contention_end(get_lock_addr(ctx), 0);
+}
+
+SEC("fentry/srcu_barrier")
+int BPF_PROG(fentry_srcu_barrier, void *ssp)
+{
+ return do_contention_begin(ctx, get_lock_addr(ctx), LCB_F_SRCU | LCB_F_WRITE);
+}
+
+SEC("fexit/srcu_barrier")
+int BPF_PROG(fexit_srcu_barrier, void *ssp)
+{
+ return do_contention_end(get_lock_addr(ctx), 0);
+}
+
SEC("raw_tp/bpf_test_finish")
int BPF_PROG(end_timestamp)
{
--
2.56.0.212.g129ede20d9-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v1 3/5] perf test: Add tests for lock_held() BPF sample filter
2026-10-17 5:45 ` [PATCH v1 3/5] perf test: Add tests for lock_held() BPF " Ian Rogers
@ 2026-10-17 5:49 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 5:49 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users
> Add unit tests in parse-events.c for parsing lock_held(),
> lock_held_read(), and lock_held_exclusive() BPF filter expressions
> (standalone predicates, negated predicates, and compound group
> expressions) and extend record_bpf_filter.sh with lock_held(),
> lock_held_read()/lock_held_write(), and ':R'/':W' recording tests.
>
> Example output:
>
> # perf test -v 'Parsing of BPF sample filters'
> 5: Parse event definition strings :
> 5.7: Parsing of BPF sample filters : Ok
>
> # perf test -v 'perf record sample filtering (by BPF) tests'
> 112: perf record sample filtering (by BPF) tests :
> Checking BPF-filter privilege
> Basic bpf-filter test
> Basic bpf-filter test [Success]
> Failing bpf-filter test
> Failing bpf-filter test [Success]
> Group bpf-filter test
> Group bpf-filter test [Success]
> Multiple bpf-filter test
> Multiple bpf-filter test [Success]
> Cgroup bpf-filter test
> Cgroup bpf-filter test [Success]
> Lock-held bpf-filter test
> Lock-held bpf-filter test [Success]
> 112: perf record sample filtering (by BPF) tests : Ok
>
> Signed-off-by: Ian Rogers <irogers@google.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261017054555.1655407-1-irogers@google.com?part=3
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v1 1/5] perf lock contention: Share common BPF and lock type definitions in lock_data.h
2026-10-17 5:45 ` [PATCH v1 1/5] perf lock contention: Share common BPF and lock type definitions in lock_data.h Ian Rogers
@ 2026-10-17 5:52 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 5:52 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users
> Move lock_type_table, LCB_F_* flags, and the common BPF lock helpers
> (get_lock_owner, get_current_mmap_lock, get_current_siglock,
> get_lock_addr, check_lock_type, do_collect_zone_lock,
> do_collect_lock_syms, do_slab_cache_iter) and shared CO-RE structs into
> tools/perf/util/bpf_skel/lock_data.h so they can be reused by BPF
> sample filtering.
>
> Also add rw_semaphore.count and struct rt_mutex_base to the minimal
> vmlinux.h and extend get_lock_owner() to support rt_mutex and check
> writer-ownership and handoff flags.
>
> Signed-off-by: Ian Rogers <irogers@google.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261017054555.1655407-1-irogers@google.com?part=1
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v1 4/5] perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU, and SRCU
2026-10-17 5:45 ` [PATCH v1 4/5] perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU, and SRCU Ian Rogers
@ 2026-10-17 5:56 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 5:56 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users
> Extend the lock_held(<lock>) BPF sample filter to support RCU, Tasks
> Trace RCU, and SRCU locks:
>
> - 'rcu' (supports both read/shared and write/exclusive modes):
> - Read/shared ('lock_held_read(rcu)', 'lock_held(rcu:R)',
> 'lock_held(rcu_read_lock)'): directly inspects
> current->rcu_read_lock_nesting in perf_sample_filter via CO-RE (on
> CONFIG_PREEMPT_RCU kernels), accounting for the baseline
> rcu_read_lock() nesting acquired by the perf overflow handler itself
> (bpf_overflow_handler() and do_perf_sw_event()) with zero extra
> fentry/fexit tracing hooks.
> - Write/exclusive ('lock_held_write(rcu)', 'lock_held(rcu:W)'):
> attaches fentry/fexit hooks to synchronize_rcu(),
> synchronize_rcu_expedited(), synchronize_rcu_tasks(), and
> rcu_barrier() to track grace-period and barrier waits.
> [ ... ]
>
> # perf record -a -g -e task-clock --filter 'lock_held(rcu:W)' -- sleep 1
> # perf record -a -g -e cycles --filter 'lock_held(srcu)' -- sleep 1
>
> Signed-off-by: Ian Rogers <irogers@google.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261017054555.1655407-1-irogers@google.com?part=4
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v1 5/5] perf lock contention: Extend BPF lock contention to RCU and SRCU
2026-10-17 5:45 ` [PATCH v1 5/5] perf lock contention: Extend BPF lock contention to RCU " Ian Rogers
@ 2026-10-17 6:08 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 6:08 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] perf lock contention: wrong caller stack skip in do_contention_begin() for fentry
--
commit 9ca74d6f51b3408e9a151b639dd644cd10562df1
Author: Ian Rogers <irogers@google.com>
perf lock contention: Extend BPF lock contention to RCU and SRCU
This commit extends 'perf lock contention --use-bpf' to track RCU and SRCU
grace-period and barrier waits using fentry/fexit BPF programs and adds
lock type and name filtering support for rcu and srcu.
> diff --git a/tools/perf/util/bpf_skel/lock_contention.bpf.c b/tools/perf/util/bpf_skel/lock_contention.bpf.c
> index 0f2118006005a..257e3402e8151 100644
> --- a/tools/perf/util/bpf_skel/lock_contention.bpf.c
> +++ b/tools/perf/util/bpf_skel/lock_contention.bpf.c
[ ... ]
> @@ -474,21 +489,20 @@ static inline void update_owner_stat(u32 id, u64 duration, u32 flags)
> }
> }
>
> -SEC("tp_btf/contention_begin")
> -int contention_begin(u64 *ctx)
> +static __always_inline int do_contention_begin(void *ctx, __u64 lock, __u32 flags)
> {
> struct tstamp_data *pelem;
>
> - if (!enabled || !can_record(ctx))
> + if (!enabled || !can_record(lock, flags))
> return 0;
>
> - pelem = get_tstamp_elem(ctx[1]);
> + pelem = get_tstamp_elem(flags);
> if (pelem == NULL)
> return 0;
>
> pelem->timestamp = bpf_ktime_get_ns();
> - pelem->lock = (__u64)ctx[0];
> - pelem->flags = (__u32)ctx[1];
> + pelem->lock = lock;
> + pelem->flags = flags;
> if (aggr_mode == LOCK_AGGR_CGROUP)
> pelem->cgroup_id = get_current_cgroup_id();
>
[ ... ]
> @@ -795,11 +812,109 @@ int contention_end(u64 *ctx)
> check_lock_delay(pelem->lock);
>
> pelem->lock = 0;
> - if (need_delete)
> - bpf_map_delete_elem(&tstamp, &pid);
> + if (ts_map)
> + bpf_map_delete_elem(ts_map, &pid);
> return 0;
> }
>
> +SEC("tp_btf/contention_begin")
> +int contention_begin(u64 *ctx)
> +{
> + return do_contention_begin(ctx, (__u64)ctx[0], (__u32)ctx[1]);
> +}
> +
> +SEC("tp_btf/contention_end")
> +int contention_end(u64 *ctx)
> +{
> + return do_contention_end((__u64)ctx[0], (__s32)ctx[1]);
> +}
> +
> +/* RCU grace-period synchronize / barrier contention hooks */
> +SEC("fentry/synchronize_rcu")
> +int BPF_PROG(fentry_synchronize_rcu)
> +{
> + return do_contention_begin(ctx, rcu_state_addr, LCB_F_RCU | LCB_F_WRITE);
> +}
[Severity: Medium]
In do_contention_begin() in tools/perf/util/bpf_skel/lock_contention.bpf.c,
bpf_get_stackid() uses stack_skip (which defaults to CONTENTION_STACK_SKIP =
4) unconditionally for both tp_btf/contention_begin and the new fentry RCU
and SRCU programs:
skip_owner:
pelem->stack_id = bpf_get_stackid(ctx, &stacks,
BPF_F_FAST_STACK_CMP | stack_skip);
For tp_btf/contention_begin, the tracepoint fires inside a lock slowpath via
tracepoint dispatch:
caller -> mutex_lock -> __mutex_lock_slowpath -> __mutex_lock ->
trace_contention_begin -> __bpf_trace_contention_begin -> bpf_trace_run2 ->
bpf_prog
Skipping 4 frames lands inside the locking functions (.sched.text,
.spinlock.text, or trace_contention_begin), which lock_contention_get_name()
in tools/perf/util/bpf_lock_contention.c then skips via
machine__is_lock_function() to reach the direct caller:
/* LOCK_AGGR_CALLER: skip lock internal functions */
while (machine__is_lock_function(machine, stack_trace[idx]) &&
idx < con->max_stack - 1)
idx++;
addr = stack_trace[idx];
In contrast, fentry_synchronize_rcu() (and the other six RCU/SRCU fentry
programs) attach directly to the top-level API functions. In an fentry
program, the callchain captured by bpf_get_stackid_raw_tp() ->
perf_callchain_kernel() only has two frames before the traced function
itself: ip[0] is the BPF prog, ip[1] is the BPF trampoline, ip[2] is the
traced function (e.g., synchronize_rcu), ip[3] is the direct caller, and
ip[4] is the caller's caller.
Won't skipping stack_skip = 4 frames in bpf_get_stackid() skip past ip[3],
dropping the direct caller from the recorded stack trace, attributing RCU
and SRCU contention in LOCK_AGGR_CALLER mode to the caller's caller (ip[4]),
and causing callstack filtering (-S) in match_callstack_filter() to miss the
direct caller?
Also, if the fentry stack skip is adjusted so that the traced RCU/SRCU
function itself can appear at stack_trace[0], machine__is_lock_function() in
tools/perf/util/machine.c only checks .sched.text, .spinlock.text,
__traceiter_contention_begin, and trace_contention_begin. Since
synchronize_rcu*(), synchronize_srcu*(), rcu_barrier(), and srcu_barrier()
are not in .sched.text or .spinlock.text, should machine__is_lock_function()
or the fentry skip depth account for those functions as well?
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261017054555.1655407-1-irogers@google.com?part=5
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v1 2/5] perf bpf-filter: Add lock_held() sample filter
2026-10-17 5:45 ` [PATCH v1 2/5] perf bpf-filter: Add lock_held() sample filter Ian Rogers
@ 2026-10-17 6:09 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 6:09 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] perf bpf-filter: held_locks leak and count corruption in fexit_downgrade_write()
- [Medium] perf bpf-filter: prescan_one_lock_expr() misses percpu_rw_semaphore symbols
- [Medium] perf bpf-filter: configure_lock_progs() ignores missing inlined spin/rwlock unlocks
- [Medium] perf bpf-filter: missing CONFIG_DEBUG_LOCK_ALLOC mutex hooks in lock_progs
- [Medium] perf bpf-filter: unguarded mutex->owner read in is_direct_lock_owner() on RT
--
commit 362a7c2a8d1d1d0355b0ac1b022fdffe73aae6c0
Author: Ian Rogers <irogers@google.com>
perf bpf-filter: Add lock_held() sample filter
This commit adds BPF sample filter predicates lock_held(), lock_held_read(),
lock_held_shared(), lock_held_write(), and lock_held_exclusive() to record
perf event samples only when a specified kernel lock is held. It uses direct
kernel lock owner inspection where possible and falls back to a held_locks
BPF hash map populated by fentry/fexit hooks on kernel lock and unlock
functions.
> diff --git a/tools/perf/util/bpf-filter.c b/tools/perf/util/bpf-filter.c
> index 72e83ac1d6a4b..35d6f7c638684 100644
> --- a/tools/perf/util/bpf-filter.c
> +++ b/tools/perf/util/bpf-filter.c
[ ... ]
> +static const struct lock_prog_desc lock_progs[] = {
> + /* Mutex */
> + LOCK_FPROG(fexit_mutex_lock, "mutex_lock", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_nested, "mutex_lock_nested", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_io, "mutex_lock_io", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_io_nested, "mutex_lock_io_nested", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_interruptible, "mutex_lock_interruptible", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_interruptible_nested, "mutex_lock_interruptible_nested",
> + LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_killable, "mutex_lock_killable", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_trylock, "mutex_trylock", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fentry_mutex_unlock, "mutex_unlock", LOCK_PROG_MUTEX),
[Severity: Medium]
On kernels built with CONFIG_DEBUG_LOCK_ALLOC=y, will mutex acquisitions via
mutex_lock_killable(), mutex_trylock(), and mutex_lock_nest_lock() be missed
by the held_locks map?
In include/linux/mutex.h, when CONFIG_DEBUG_LOCK_ALLOC=y, those functions are
macros that expand to different underlying entry points:
#define mutex_lock(lock) mutex_lock_nested(lock, 0)
#define mutex_lock_interruptible(lock) mutex_lock_interruptible_nested(lock, 0)
#define mutex_lock_killable(lock) _mutex_lock_killable(lock, 0, NULL)
#define mutex_lock_io(lock) mutex_lock_io_nested(lock, 0)
#define mutex_lock_nest_lock(lock, nest_lock) _mutex_lock_nest_lock(lock, &(nest_lock)->dep_map)
#define mutex_trylock(lock) _mutex_trylock_nest_lock(lock, NULL)
While lock_progs[] and sample_filter.bpf.c hook mutex_lock_nested,
mutex_lock_interruptible_nested, and mutex_lock_io_nested, they only hook
mutex_lock_killable and mutex_trylock rather than _mutex_lock_killable,
_mutex_trylock_nest_lock, and _mutex_lock_nest_lock.
For filters that rely on held_locks rather than direct owner inspection (such
as PBF_LOCK_SLAB and PBF_LOCK_TYPE), wouldn't mutexes acquired via
mutex_lock_killable() or mutex_trylock() go untracked on lockdep-enabled
kernels?
[ ... ]
> +static int configure_lock_progs(struct sample_filter_bpf *skel, struct btf *btf,
> + unsigned int needed_groups, bool needs_lock_syms,
> + bool needs_slab_iter)
> +{
> + unsigned int enabled_progs = 0;
> +
> + for (size_t i = 0; i < ARRAY_SIZE(lock_progs); i++) {
> + struct bpf_program **progp = (void *)skel + lock_progs[i].prog_offset;
> + struct bpf_program *prog = *progp;
> + bool enable = false;
> +
> + if ((needed_groups & lock_progs[i].group) && btf &&
> + lock_progs[i].btf_func) {
> + if (btf__find_by_name_kind(btf, lock_progs[i].btf_func,
> + BTF_KIND_FUNC) > 0)
> + enable = true;
> + }
> +
> + if (enable)
> + enabled_progs++;
> + bpf_program__set_autoload(prog, enable);
> + }
> +
> + bpf_program__set_autoload(skel->progs.collect_lock_syms, needs_lock_syms);
> + if (needs_slab_iter && btf &&
> + btf__find_by_name_kind(btf, "bpf_iter__kmem_cache", BTF_KIND_STRUCT) > 0) {
> + bpf_program__set_autoload(skel->progs.slab_cache_iter, true);
> + } else {
> + bpf_program__set_autoload(skel->progs.slab_cache_iter, false);
> + }
> +
> + if (needed_groups && !enabled_progs) {
> + pr_err("Error: Kernel BTF lacks required lock tracing entry points\n");
> + return -ENOTSUP;
> + }
> + return 0;
> +}
[Severity: Medium]
What happens on kernels where spinlock and rwlock unlock functions are
inlined?
In kernel/Kconfig.locks, when !CONFIG_PREEMPTION && !CONFIG_DEBUG_SPINLOCK
(for example CONFIG_PREEMPT_NONE or CONFIG_PREEMPT_VOLUNTARY without
CONFIG_PREEMPT_DYNAMIC), CONFIG_UNINLINE_SPIN_UNLOCK is not set and
CONFIG_INLINE_SPIN_UNLOCK_IRQ, CONFIG_INLINE_READ_UNLOCK,
CONFIG_INLINE_READ_UNLOCK_IRQ, CONFIG_INLINE_WRITE_UNLOCK, and
CONFIG_INLINE_WRITE_UNLOCK_IRQ are enabled.
In that configuration, _raw_spin_unlock, _raw_spin_unlock_irq,
_raw_read_unlock, _raw_read_unlock_irq, _raw_write_unlock, and
_raw_write_unlock_irq are inlined and absent from vmlinux BTF, while the
acquire functions and *_unlock_irqrestore / *_unlock_bh remain out-of-line
in BTF.
Because configure_lock_progs() enables each program individually if its BTF
function exists and only fails when enabled_progs == 0, it will attach the
spinlock and rwlock acquire hooks without the corresponding unlock hooks.
Wouldn't any spinlock or rwlock released via an inlined unlock (including
rq_lock, zone_lock, and siglock) remain permanently recorded as held in
held_locks?
> +/*
> + * Pre-scan a single filter expression prior to loading the BPF skeleton in
> + * order to determine which BPF maps need to be sized and which lock tracing
> + * program groups need to be autoloaded.
> + */
> +static void prescan_one_lock_expr(struct perf_bpf_filter_expr *expr,
> + bool *has_lock_filter,
> + unsigned int *prog_groups,
> + bool *needs_lock_syms,
> + bool *needs_slab_iter)
> +{
[ ... ]
> + if (strtailcmp(base, "rt_mutex") == 0 || strtailcmp(base, "rtmutex") == 0) {
> + *prog_groups |= LOCK_PROG_RTMUTEX;
> + } else if (strtailcmp(base, "mutex") == 0) {
> + *prog_groups |= LOCK_PROG_MUTEX;
> + } else if (strtailcmp(base, "percpu_rwsem") == 0 ||
> + strtailcmp(base, "pcpu_rwsem") == 0) {
> + *prog_groups |= LOCK_PROG_PERCPU_RWSEM;
> + } else if (strtailcmp(base, "rwsem") == 0) {
> + if (mode & PBF_LOCK_MODE_READ)
> + *prog_groups |= LOCK_PROG_RWSEM_READ;
> + if (mode & PBF_LOCK_MODE_WRITE)
> + *prog_groups |= LOCK_PROG_RWSEM_WRITE;
[Severity: Medium]
Can prescan_one_lock_expr() misclassify kernel struct percpu_rw_semaphore
symbols as regular rwsems?
Kernel percpu_rw_semaphore locks defined with DEFINE_PERCPU_RWSEM or
DEFINE_STATIC_PERCPU_RWSEM typically end in _rwsem rather than _percpu_rwsem
(for example cgroup_threadgroup_rwsem, file_rwsem, or scx_fork_rwsem).
In prescan_one_lock_expr(), those symbols match strtailcmp(base, "rwsem") == 0
and only enable LOCK_PROG_RWSEM_READ and/or LOCK_PROG_RWSEM_WRITE, leaving
LOCK_PROG_PERCPU_RWSEM (fexit_percpu_down_write and fentry_percpu_up_write)
disabled. Since is_direct_lock_owner() only inspects struct mutex, struct
rw_semaphore, and struct rt_mutex_base, won't filtering on a
percpu_rw_semaphore symbol like cgroup_threadgroup_rwsem always evaluate to 0?
[ ... ]
> diff --git a/tools/perf/util/bpf_skel/sample_filter.bpf.c b/tools/perf/util/bpf_skel/sample_filter.bpf.c
> index e5666d4c17228..fbd2e51815d75 100644
> --- a/tools/perf/util/bpf_skel/sample_filter.bpf.c
> +++ b/tools/perf/util/bpf_skel/sample_filter.bpf.c
[ ... ]
> +static __always_inline bool is_direct_lock_owner(__u64 lock_addr, __u64 curr)
> +{
> + if (!curr)
> + return false;
> +
> + return (__u64)get_lock_owner(lock_addr, LCB_F_MUTEX) == curr ||
> + (__u64)get_lock_owner(lock_addr, LCB_F_WRITE) == curr ||
> + (__u64)get_lock_owner(lock_addr, LCB_F_RT) == curr;
> +}
[Severity: Medium]
Will calling get_lock_owner(lock_addr, LCB_F_MUTEX) from
is_direct_lock_owner() break loading sample_filter_bpf on CONFIG_PREEMPT_RT=y
kernels?
perf_sample_filter() inlines perf_get_sample() -> check_lock_held() ->
is_direct_lock_owner(), which calls get_lock_owner(lock_addr, LCB_F_MUTEX) in
tools/perf/util/bpf_skel/lock_data.h:
if (flags & LCB_F_MUTEX) {
struct mutex *mutex = (void *)lock;
owner = BPF_CORE_READ(mutex, owner.counter);
if (owner & 0x4UL)
return NULL;
}
On CONFIG_PREEMPT_RT=y kernels, struct mutex wraps struct rt_mutex_base
rtmutex and does not have an owner field. Because get_lock_owner() does not
guard BPF_CORE_READ(mutex, owner.counter) with bpf_core_field_exists(),
wouldn't libbpf poison the instruction during CO-RE relocation and cause the
BPF verifier to reject perf_sample_filter on PREEMPT_RT kernels even when
lock_held() is not used?
[ ... ]
> +static __always_inline int record_lock_released_addr(__u64 lock_addr, __u32 flags)
> +{
> + __u32 owner_id = get_lock_owner_id(flags);
> + struct held_lock_key addr_key = {
> + .addr_or_id = lock_addr,
> + .owner_id = owner_id,
> + .kind = PBF_LOCK_ADDR,
> + };
> + struct held_lock_val *existing;
> + __u32 type_flag = flags & LCB_F_TYPE_MASK;
> + bool is_read = (flags & LCB_F_READ) != 0;
> +
> + if (!lock_enabled || !lock_addr)
> + return 0;
> +
> + existing = bpf_map_lookup_elem(&held_locks, &addr_key);
> + if (!existing)
> + return 0;
> +
> + if (is_read) {
> + if (existing->read_count <= 1 && existing->write_count == 0)
> + bpf_map_delete_elem(&held_locks, &addr_key);
> + else if (existing->read_count > 0)
> + __sync_fetch_and_add(&existing->read_count, -1);
> + } else {
> + if (existing->write_count == 0 && existing->read_count == 0)
> + bpf_map_delete_elem(&held_locks, &addr_key);
> + else if (existing->read_count == 0)
> + bpf_map_delete_elem(&held_locks, &addr_key);
> + else
> + existing->write_count = 0;
> + }
[ ... ]
> +SEC("fexit/downgrade_write")
> +int BPF_PROG(fexit_downgrade_write, void *sem)
> +{
> + record_lock_acquired(ctx, LCB_F_READ);
> + return record_lock_released(ctx, LCB_F_WRITE);
> +}
[Severity: Medium]
Can fexit_downgrade_write() leak held_locks map entries under write-only rwsem
filters and corrupt PBF_LOCK_SLAB / PBF_LOCK_TYPE write counts?
First, in lock_progs[], fexit_downgrade_write has group LOCK_PROG_RWSEM
(LOCK_PROG_RWSEM_READ | LOCK_PROG_RWSEM_WRITE), while fentry_up_read has group
LOCK_PROG_RWSEM_READ:
LOCK_FPROG(fexit_downgrade_write, "downgrade_write", LOCK_PROG_RWSEM),
LOCK_FPROG(fentry_up_read, "up_read", LOCK_PROG_RWSEM_READ),
LOCK_FPROG(fentry_up_write, "up_write", LOCK_PROG_RWSEM_WRITE),
When a write-only filter such as lock_held_write(&mmap_lock) is used,
prescan_one_lock_expr() sets only LOCK_PROG_RWSEM_WRITE. Because
(needed_groups & lock_progs[i].group) is non-zero for LOCK_PROG_RWSEM,
configure_lock_progs() enables fexit_downgrade_write while leaving
fentry_up_read disabled.
When the kernel calls downgrade_write() (for example mmap_write_downgrade()),
fexit_downgrade_write() calls record_lock_acquired(ctx, LCB_F_READ), setting
existing->read_count = 1, and then calls record_lock_released(ctx,
LCB_F_WRITE), which sees existing->read_count != 0 and sets
existing->write_count = 0 instead of deleting addr_key. Since fentry_up_read
is not attached, up_read() never decrements read_count, leaking the held_locks
entry (and any PBF_LOCK_SLAB read_count).
Second, because fexit_downgrade_write() calls record_lock_acquired(ctx,
LCB_F_READ) before record_lock_released(ctx, LCB_F_WRITE), if downgrade_write()
is called on an rwsem whose initial down_write() was not recorded in
held_locks (such as under a read-only filter where LOCK_PROG_RWSEM_WRITE is
disabled, or when acquired before lock_enabled = 1), record_lock_acquired(ctx,
LCB_F_READ) creates addr_key with read_count = 1 and write_count = 0. The
subsequent record_lock_released(ctx, LCB_F_WRITE) then finds existing != NULL
even though existing->write_count == 0, and proceeds to decrement write_count
for PBF_LOCK_SLAB and PBF_LOCK_TYPE.
Should fexit_downgrade_write release LCB_F_WRITE before acquiring LCB_F_READ
(and only record LCB_F_READ when LOCK_PROG_RWSEM_READ is active), and should
record_lock_released_addr() verify that the corresponding read_count or
write_count was non-zero before decrementing the aggregate slab/type counts?
[ ... ]
完美! Let's double check all rules:
- No markdown headers, no backticks, no ALL CAPS shouting (except inside quoted code or config/macro names that are literally in code). Wait: `CONFIG_DEBUG_LOCK_ALLOC=y`, `PBF_LOCK_SLAB`, `PBF_LOCK_TYPE`, `CONFIG_PREEMPT_RT=y`, `LOCK_PROG_RWSEM` are exact code identifiers. Let's make sure there are no non-code ALL CAPS words.
- No line numbers mentioned anywhere.
- Ends with a blank line.
- Severity tags `[Severity: Medium]` on their own line right above each inline comment block.
- All 5 findings included.
Everything is clean and strictly follows instructions. I'm ready to output.完美! Let's emit the final response.commit 362a7c2a8d1d1d0355b0ac1b022fdffe73aae6c0
Author: Ian Rogers <irogers@google.com>
perf bpf-filter: Add lock_held() sample filter
This commit adds BPF sample filter predicates lock_held(), lock_held_read(),
lock_held_shared(), lock_held_write(), and lock_held_exclusive() to record
perf event samples only when a specified kernel lock is held. It uses direct
kernel lock owner inspection where possible and falls back to a held_locks
BPF hash map populated by fentry/fexit hooks on kernel lock and unlock
functions.
> diff --git a/tools/perf/util/bpf-filter.c b/tools/perf/util/bpf-filter.c
> index 72e83ac1d6a4b..35d6f7c638684 100644
> --- a/tools/perf/util/bpf-filter.c
> +++ b/tools/perf/util/bpf-filter.c
[ ... ]
> +static const struct lock_prog_desc lock_progs[] = {
> + /* Mutex */
> + LOCK_FPROG(fexit_mutex_lock, "mutex_lock", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_nested, "mutex_lock_nested", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_io, "mutex_lock_io", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_io_nested, "mutex_lock_io_nested", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_interruptible, "mutex_lock_interruptible", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_interruptible_nested, "mutex_lock_interruptible_nested",
> + LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_lock_killable, "mutex_lock_killable", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fexit_mutex_trylock, "mutex_trylock", LOCK_PROG_MUTEX),
> + LOCK_FPROG(fentry_mutex_unlock, "mutex_unlock", LOCK_PROG_MUTEX),
[Severity: Medium]
On kernels built with CONFIG_DEBUG_LOCK_ALLOC=y, will mutex acquisitions via
mutex_lock_killable(), mutex_trylock(), and mutex_lock_nest_lock() be missed
by the held_locks map?
In include/linux/mutex.h, when CONFIG_DEBUG_LOCK_ALLOC=y, those functions are
macros that expand to different underlying entry points:
#define mutex_lock(lock) mutex_lock_nested(lock, 0)
#define mutex_lock_interruptible(lock) mutex_lock_interruptible_nested(lock, 0)
#define mutex_lock_killable(lock) _mutex_lock_killable(lock, 0, NULL)
#define mutex_lock_io(lock) mutex_lock_io_nested(lock, 0)
#define mutex_lock_nest_lock(lock, nest_lock) _mutex_lock_nest_lock(lock, &(nest_lock)->dep_map)
#define mutex_trylock(lock) _mutex_trylock_nest_lock(lock, NULL)
While lock_progs[] and sample_filter.bpf.c hook mutex_lock_nested,
mutex_lock_interruptible_nested, and mutex_lock_io_nested, they only hook
mutex_lock_killable and mutex_trylock rather than _mutex_lock_killable,
_mutex_trylock_nest_lock, and _mutex_lock_nest_lock.
For filters that rely on held_locks rather than direct owner inspection (such
as PBF_LOCK_SLAB and PBF_LOCK_TYPE), wouldn't mutexes acquired via
mutex_lock_killable() or mutex_trylock() go untracked on lockdep-enabled
kernels?
[ ... ]
> +static int configure_lock_progs(struct sample_filter_bpf *skel, struct btf *btf,
> + unsigned int needed_groups, bool needs_lock_syms,
> + bool needs_slab_iter)
> +{
> + unsigned int enabled_progs = 0;
> +
> + for (size_t i = 0; i < ARRAY_SIZE(lock_progs); i++) {
> + struct bpf_program **progp = (void *)skel + lock_progs[i].prog_offset;
> + struct bpf_program *prog = *progp;
> + bool enable = false;
> +
> + if ((needed_groups & lock_progs[i].group) && btf &&
> + lock_progs[i].btf_func) {
> + if (btf__find_by_name_kind(btf, lock_progs[i].btf_func,
> + BTF_KIND_FUNC) > 0)
> + enable = true;
> + }
> +
> + if (enable)
> + enabled_progs++;
> + bpf_program__set_autoload(prog, enable);
> + }
> +
> + bpf_program__set_autoload(skel->progs.collect_lock_syms, needs_lock_syms);
> + if (needs_slab_iter && btf &&
> + btf__find_by_name_kind(btf, "bpf_iter__kmem_cache", BTF_KIND_STRUCT) > 0) {
> + bpf_program__set_autoload(skel->progs.slab_cache_iter, true);
> + } else {
> + bpf_program__set_autoload(skel->progs.slab_cache_iter, false);
> + }
> +
> + if (needed_groups && !enabled_progs) {
> + pr_err("Error: Kernel BTF lacks required lock tracing entry points\n");
> + return -ENOTSUP;
> + }
> + return 0;
> +}
[Severity: Medium]
What happens on kernels where spinlock and rwlock unlock functions are
inlined?
In kernel/Kconfig.locks, when !CONFIG_PREEMPTION && !CONFIG_DEBUG_SPINLOCK
(for example CONFIG_PREEMPT_NONE or CONFIG_PREEMPT_VOLUNTARY without
CONFIG_PREEMPT_DYNAMIC), CONFIG_UNINLINE_SPIN_UNLOCK is not set and
CONFIG_INLINE_SPIN_UNLOCK_IRQ, CONFIG_INLINE_READ_UNLOCK,
CONFIG_INLINE_READ_UNLOCK_IRQ, CONFIG_INLINE_WRITE_UNLOCK, and
CONFIG_INLINE_WRITE_UNLOCK_IRQ are enabled.
In that configuration, _raw_spin_unlock, _raw_spin_unlock_irq,
_raw_read_unlock, _raw_read_unlock_irq, _raw_write_unlock, and
_raw_write_unlock_irq are inlined and absent from vmlinux BTF, while the
acquire functions and *_unlock_irqrestore / *_unlock_bh remain out-of-line
in BTF.
Because configure_lock_progs() enables each program individually if its BTF
function exists and only fails when enabled_progs == 0, it will attach the
spinlock and rwlock acquire hooks without the corresponding unlock hooks.
Wouldn't any spinlock or rwlock released via an inlined unlock (including
rq_lock, zone_lock, and siglock) remain permanently recorded as held in
held_locks?
> +/*
> + * Pre-scan a single filter expression prior to loading the BPF skeleton in
> + * order to determine which BPF maps need to be sized and which lock tracing
> + * program groups need to be autoloaded.
> + */
> +static void prescan_one_lock_expr(struct perf_bpf_filter_expr *expr,
> + bool *has_lock_filter,
> + unsigned int *prog_groups,
> + bool *needs_lock_syms,
> + bool *needs_slab_iter)
> +{
[ ... ]
> + if (strtailcmp(base, "rt_mutex") == 0 || strtailcmp(base, "rtmutex") == 0) {
> + *prog_groups |= LOCK_PROG_RTMUTEX;
> + } else if (strtailcmp(base, "mutex") == 0) {
> + *prog_groups |= LOCK_PROG_MUTEX;
> + } else if (strtailcmp(base, "percpu_rwsem") == 0 ||
> + strtailcmp(base, "pcpu_rwsem") == 0) {
> + *prog_groups |= LOCK_PROG_PERCPU_RWSEM;
> + } else if (strtailcmp(base, "rwsem") == 0) {
> + if (mode & PBF_LOCK_MODE_READ)
> + *prog_groups |= LOCK_PROG_RWSEM_READ;
> + if (mode & PBF_LOCK_MODE_WRITE)
> + *prog_groups |= LOCK_PROG_RWSEM_WRITE;
[Severity: Medium]
Can prescan_one_lock_expr() misclassify kernel struct percpu_rw_semaphore
symbols as regular rwsems?
Kernel percpu_rw_semaphore locks defined with DEFINE_PERCPU_RWSEM or
DEFINE_STATIC_PERCPU_RWSEM typically end in _rwsem rather than _percpu_rwsem
(for example cgroup_threadgroup_rwsem, file_rwsem, or scx_fork_rwsem).
In prescan_one_lock_expr(), those symbols match strtailcmp(base, "rwsem") == 0
and only enable LOCK_PROG_RWSEM_READ and/or LOCK_PROG_RWSEM_WRITE, leaving
LOCK_PROG_PERCPU_RWSEM (fexit_percpu_down_write and fentry_percpu_up_write)
disabled. Since is_direct_lock_owner() only inspects struct mutex, struct
rw_semaphore, and struct rt_mutex_base, won't filtering on a
percpu_rw_semaphore symbol like cgroup_threadgroup_rwsem always evaluate to 0?
[ ... ]
> diff --git a/tools/perf/util/bpf_skel/sample_filter.bpf.c b/tools/perf/util/bpf_skel/sample_filter.bpf.c
> index e5666d4c17228..fbd2e51815d75 100644
> --- a/tools/perf/util/bpf_skel/sample_filter.bpf.c
> +++ b/tools/perf/util/bpf_skel/sample_filter.bpf.c
[ ... ]
> +static __always_inline bool is_direct_lock_owner(__u64 lock_addr, __u64 curr)
> +{
> + if (!curr)
> + return false;
> +
> + return (__u64)get_lock_owner(lock_addr, LCB_F_MUTEX) == curr ||
> + (__u64)get_lock_owner(lock_addr, LCB_F_WRITE) == curr ||
> + (__u64)get_lock_owner(lock_addr, LCB_F_RT) == curr;
> +}
[Severity: Medium]
Will calling get_lock_owner(lock_addr, LCB_F_MUTEX) from
is_direct_lock_owner() break loading sample_filter_bpf on CONFIG_PREEMPT_RT=y
kernels?
perf_sample_filter() inlines perf_get_sample() -> check_lock_held() ->
is_direct_lock_owner(), which calls get_lock_owner(lock_addr, LCB_F_MUTEX) in
tools/perf/util/bpf_skel/lock_data.h:
if (flags & LCB_F_MUTEX) {
struct mutex *mutex = (void *)lock;
owner = BPF_CORE_READ(mutex, owner.counter);
if (owner & 0x4UL)
return NULL;
}
On CONFIG_PREEMPT_RT=y kernels, struct mutex wraps struct rt_mutex_base
rtmutex and does not have an owner field. Because get_lock_owner() does not
guard BPF_CORE_READ(mutex, owner.counter) with bpf_core_field_exists(),
wouldn't libbpf poison the instruction during CO-RE relocation and cause the
BPF verifier to reject perf_sample_filter on PREEMPT_RT kernels even when
lock_held() is not used?
[ ... ]
> +static __always_inline int record_lock_released_addr(__u64 lock_addr, __u32 flags)
> +{
> + __u32 owner_id = get_lock_owner_id(flags);
> + struct held_lock_key addr_key = {
> + .addr_or_id = lock_addr,
> + .owner_id = owner_id,
> + .kind = PBF_LOCK_ADDR,
> + };
> + struct held_lock_val *existing;
> + __u32 type_flag = flags & LCB_F_TYPE_MASK;
> + bool is_read = (flags & LCB_F_READ) != 0;
> +
> + if (!lock_enabled || !lock_addr)
> + return 0;
> +
> + existing = bpf_map_lookup_elem(&held_locks, &addr_key);
> + if (!existing)
> + return 0;
> +
> + if (is_read) {
> + if (existing->read_count <= 1 && existing->write_count == 0)
> + bpf_map_delete_elem(&held_locks, &addr_key);
> + else if (existing->read_count > 0)
> + __sync_fetch_and_add(&existing->read_count, -1);
> + } else {
> + if (existing->write_count == 0 && existing->read_count == 0)
> + bpf_map_delete_elem(&held_locks, &addr_key);
> + else if (existing->read_count == 0)
> + bpf_map_delete_elem(&held_locks, &addr_key);
> + else
> + existing->write_count = 0;
> + }
[ ... ]
> +SEC("fexit/downgrade_write")
> +int BPF_PROG(fexit_downgrade_write, void *sem)
> +{
> + record_lock_acquired(ctx, LCB_F_READ);
> + return record_lock_released(ctx, LCB_F_WRITE);
> +}
[Severity: Medium]
Can fexit_downgrade_write() leak held_locks map entries under write-only rwsem
filters and corrupt PBF_LOCK_SLAB / PBF_LOCK_TYPE write counts?
First, in lock_progs[], fexit_downgrade_write has group LOCK_PROG_RWSEM
(LOCK_PROG_RWSEM_READ | LOCK_PROG_RWSEM_WRITE), while fentry_up_read has group
LOCK_PROG_RWSEM_READ:
LOCK_FPROG(fexit_downgrade_write, "downgrade_write", LOCK_PROG_RWSEM),
LOCK_FPROG(fentry_up_read, "up_read", LOCK_PROG_RWSEM_READ),
LOCK_FPROG(fentry_up_write, "up_write", LOCK_PROG_RWSEM_WRITE),
When a write-only filter such as lock_held_write(&mmap_lock) is used,
prescan_one_lock_expr() sets only LOCK_PROG_RWSEM_WRITE. Because
(needed_groups & lock_progs[i].group) is non-zero for LOCK_PROG_RWSEM,
configure_lock_progs() enables fexit_downgrade_write while leaving
fentry_up_read disabled.
When the kernel calls downgrade_write() (for example mmap_write_downgrade()),
fexit_downgrade_write() calls record_lock_acquired(ctx, LCB_F_READ), setting
existing->read_count = 1, and then calls record_lock_released(ctx,
LCB_F_WRITE), which sees existing->read_count != 0 and sets
existing->write_count = 0 instead of deleting addr_key. Since fentry_up_read
is not attached, up_read() never decrements read_count, leaking the held_locks
entry (and any PBF_LOCK_SLAB read_count).
Second, because fexit_downgrade_write() calls record_lock_acquired(ctx,
LCB_F_READ) before record_lock_released(ctx, LCB_F_WRITE), if downgrade_write()
is called on an rwsem whose initial down_write() was not recorded in
held_locks (such as under a read-only filter where LOCK_PROG_RWSEM_WRITE is
disabled, or when acquired before lock_enabled = 1), record_lock_acquired(ctx,
LCB_F_READ) creates addr_key with read_count = 1 and write_count = 0. The
subsequent record_lock_released(ctx, LCB_F_WRITE) then finds existing != NULL
even though existing->write_count == 0, and proceeds to decrement write_count
for PBF_LOCK_SLAB and PBF_LOCK_TYPE.
Should fexit_downgrade_write release LCB_F_WRITE before acquiring LCB_F_READ
(and only record LCB_F_READ when LOCK_PROG_RWSEM_READ is active), and should
record_lock_released_addr() verify that the corresponding read_count or
write_count was non-zero before decrementing the aggregate slab/type counts?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261017054555.1655407-1-irogers@google.com?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-17 6:09 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-17 5:45 [PATCH v1 0/5] perf bpf-filter / lock contention: Add lock_held() sample filter and RCU/SRCU support Ian Rogers
2026-10-17 5:45 ` [PATCH v1 1/5] perf lock contention: Share common BPF and lock type definitions in lock_data.h Ian Rogers
2026-10-17 5:52 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 2/5] perf bpf-filter: Add lock_held() sample filter Ian Rogers
2026-10-17 6:09 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 3/5] perf test: Add tests for lock_held() BPF " Ian Rogers
2026-10-17 5:49 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 4/5] perf bpf-filter: Extend lock_held() to support RCU, Tasks Trace RCU, and SRCU Ian Rogers
2026-10-17 5:56 ` sashiko-bot
2026-10-17 5:45 ` [PATCH v1 5/5] perf lock contention: Extend BPF lock contention to RCU " Ian Rogers
2026-10-17 6:08 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox