rv/tlob: Add task latency over budget RV monitor
Subsystems (4)
v3 cover rv/tlob: Add task latency over budget RV monitor
From: Wen Yang <[email protected]>
This series introduces tlob (task latency over budget), a per-task hybrid automaton RV monitor that measures elapsed wall-clock time across a user-delimited code section and fires when the time exceeds a configurable budget.
The series applies cleanly on top of:
[1] git://git.kernel.org/pub/scm/linux/kernel/git/gmonaco/linux.git rv-fixes-7.1
"rv fixes for v7.1"
Background
----------
The existing wwnr monitor uses a two-state DA to detect tasks that are woken but never run. tlob extends the RV framework to a three-state hybrid automaton:
running (initial) -- on CPU
waiting -- in the scheduler runqueue, not yet on CPU
sleeping -- blocked on a lock, I/O, or similar resource
A single HA clock invariant, clk_elapsed < BUDGET_NS(), is active in all states. The framework enforces it via a per-task hrtimer. On expiry, error_env_tlob is emitted, followed by detail_env_tlob which carries a per-state time breakdown (running_ns, waiting_ns, sleeping_ns) that pinpoints whether the overrun occurred in the running, waiting, or sleeping state.
Userspace interface
-------------------
Tasks are registered for monitoring by writing to the tracefs monitor file:
# echo "p /path/to/binary:START_OFFSET STOP_OFFSET threshold=NS" \
> /sys/kernel/tracing/rv/monitors/tlob/monitorTwo uprobes are registered at START_OFFSET (entry) and STOP_OFFSET (exit) of the delimited section. When a task executes the entry uprobe, the monitor starts; when the task reaches the exit uprobe or the budget expires, monitoring stops and the slot is returned to the pool.
Multiple uprobe pairs can be registered for the same binary or different binaries. Each task can have at most one active monitoring session; if a task hits a start uprobe while already monitored, the prior session is cancelled and a new one begins.
Series structure
----------------
Patch 1: rv/da: introduce DA_MON_ALLOCATION_STRATEGY
Consolidates per-object DA storage allocation under a compile-time selector with three strategies:
DA_ALLOC_AUTO (default) - lock-free kmalloc_nolock; unbounded
DA_ALLOC_POOL - pre-allocated fixed-size pool
DA_ALLOC_MANUAL - caller pre-inserts storage
da_handle_start_event() and da_handle_start_run_event() call da_prepare_storage() which resolves at compile time to the correct allocation function.
This patch also includes critical correctness fixes for the pool implementation:
- Add tracepoint_synchronize_unregister() in da_monitor_destroy_pool() to fix UAF where in-flight handlers access freed pool storage
- Fix duplicate hash entry race in da_create_or_get_pool() via concurrent-insert detection under RCU
- Add capacity field to fix build error (DA_MON_POOL_SIZE undeclared in da_pool_return_cb)
Patch 2: rv: add generic uprobe infrastructure for RV monitors
Introduces rv_uprobe, a thin wrapper around uprobe_consumer for RV monitors. Provides rv_uprobe_register(), rv_uprobe_unregister(), and rv_uprobe_sync() for safe teardown.
Patch 3: rv/tlob: add tlob model DOT file
The formal model used to generate tlob.h.
Patch 4: rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check
Fixes a bug where ha_invariant_passed_ns() returned 0 early when env_store was invalid (U64_MAX), leaving it at U64_MAX and causing ha_check_invariant_ns() to always pass. The fix calls ha_reset_clk_ns() then ha_set_invariant_ns() on first use.
Patch 5: rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable
Allows tlob to override EVENT_NONE_LBL for its start_tlob self-loop.
Patch 6: rv/tlob: add tlob hybrid automaton monitor
The main tlob implementation, including:
- Three-state HA (running/waiting/sleeping)
- Per-task hrtimer enforcement (HRTIMER_MODE_REL_HARD)
- DA_ALLOC_POOL for allocation-free hot path
- Uprobe registration via tracefs monitor file
- Per-state time accumulation (running_ns, waiting_ns, sleeping_ns)
- HIGH_RES_TIMERS dependency in Kconfig
Patches 7-9: Tests
- KUnit tests for tlob monitor
- Selftest infrastructure fixes
- tlob selftests (uprobe binding, state tracking, violation detection)
Changes since v2
----------------
All feedback from Gabriele Monaco has been addressed:
-- Patch 02 (per-task slot ordering / ha_monitor_reset_env):
Dropped from v3; rebased on top of Gabriele's series [1].
-- Patch 03 (verificationtest-ktap):
Changed to use realpath for robustness as suggested.
-- Patch 04 (pre-allocated storage pool):
Complete redesign as DA_MON_ALLOCATION_STRATEGY:
- Three strategies (AUTO/POOL/MANUAL) via compile-time macro
- da_monitor_init_prealloc() removed; da_monitor_init() selects internally
- da_create_or_get_kmalloc() removed (no viable use case)
- nomiss updated to use DA_ALLOC_MANUAL
- da_extra_cleanup() hook added for per-entry teardown
Critical bug fixes included in this patch:
- tracepoint_synchronize_unregister() added to da_monitor_destroy_pool() to prevent UAF from in-flight handlers accessing freed pool storage
- Duplicate hash entry race fixed in da_create_or_get_pool() via concurrent-insert detection and slot return under RCU
- capacity field added to fix DA_MON_POOL_SIZE undeclared build error
-- Patch 05 (generic uprobe infrastructure):
Carried unchanged into v3.
-- Patch 06 (rvgen __init arrow reset):
Carried unchanged into v3.
-- Patch 08 (tlob monitor):
Split and refactored:
- ioctl interface deferred to follow-up series (tracefs-only in v3)
- Handler simplification: three inline helpers (tlob_acc_running/ waiting/sleeping) with scoped_guard(rcu)
- do_prev/do_next flags removed (da_handle_event skips unmonitored)
- scoped_guard(rcu) and guard(mutex) applied throughout
- tlob_stop_all() removed; da_extra_cleanup() hook used instead
- start_tlob self-loop added to DOT model as suggested
- ha_setup_invariants() guards against redundant timer restart
- HIGH_RES_TIMERS dependency added to Kconfig
Additional improvements in v3
------------------------------
Beyond the v2 feedback, this version includes:
1. Simplified tlob monitor implementation:
- Removed redundant tlob_num_monitored atomic counter (da_handle_event already handles unmonitored tasks via hash lookup)
- Eliminated extra cacheline touch on every sched_switch/sched_wakeup
- Several repeated pattern simplifications.
2. Extracted common accumulation logic:
- __tlob_acc() using offsetof() replaces three nearly-identical functions
- Reduces code duplication while maintaining type safety
3. Complete test coverage:
- KUnit tests for core functionality
- Comprehensive selftests for uprobe integration, state tracking, and violation detection
Testing
-------
All patches have been tested on:
- x86_64 with CONFIG_PREEMPT_RT
- All KUnit tests pass
- All selftests pass with verificationtest-ktap
[1] git://git.kernel.org/pub/scm/linux/kernel/git/gmonaco/linux.git rv-fixes-7.1
"rv fixes for v7.1"
Wen Yang (9):
rv/da: introduce DA_MON_ALLOCATION_STRATEGY
rv: add generic uprobe infrastructure for RV monitors rv/tlob: add tlob model DOT file
rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable rv/tlob: add tlob hybrid automaton monitor
rv/tlob: add KUnit tests for the tlob monitor selftests/verification: fix verificationtest-ktap for out-of-tree
execution
selftests/verification: add tlob selftests
Documentation/trace/rv/index.rst | 1 + Documentation/trace/rv/monitor_tlob.rst | 177 ++++ include/rv/da_monitor.h | 276 ++++- include/rv/ha_monitor.h | 22 +- include/rv/rv_uprobe.h | 119 +++ kernel/trace/rv/Kconfig | 5 + kernel/trace/rv/Makefile | 3 + kernel/trace/rv/monitors/nomiss/nomiss.c | 6 +- kernel/trace/rv/monitors/tlob/.kunitconfig | 6 + kernel/trace/rv/monitors/tlob/Kconfig | 19 + kernel/trace/rv/monitors/tlob/tlob.c | 968 ++++++++++++++++++ kernel/trace/rv/monitors/tlob/tlob.h | 148 +++ kernel/trace/rv/monitors/tlob/tlob_kunit.c | 92 ++ kernel/trace/rv/monitors/tlob/tlob_trace.h | 49 + kernel/trace/rv/rv_trace.h | 1 + kernel/trace/rv/rv_uprobe.c | 182 ++++ .../testing/selftests/verification/.gitignore | 2 + tools/testing/selftests/verification/Makefile | 19 +- .../verification/test.d/tlob/Makefile | 20 + .../verification/test.d/tlob/test.d/functions | 1 + .../verification/test.d/tlob/tlob_sym.c | 189 ++++ .../verification/test.d/tlob/tlob_target.c | 138 +++ .../verification/test.d/tlob/uprobe_bind.tc | 37 + .../test.d/tlob/uprobe_detail_running.tc | 51 + .../test.d/tlob/uprobe_detail_sleeping.tc | 50 + .../test.d/tlob/uprobe_detail_waiting.tc | 66 ++ .../verification/test.d/tlob/uprobe_multi.tc | 64 ++ .../test.d/tlob/uprobe_no_event.tc | 19 + .../test.d/tlob/uprobe_violation.tc | 67 ++ .../verification/verificationtest-ktap | 4 +- tools/verification/models/tlob.dot | 22 + 31 files changed, 2789 insertions(+), 34 deletions(-) create mode 100644 Documentation/trace/rv/monitor_tlob.rst create mode 100644 include/rv/rv_uprobe.h create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig create mode 100644 kernel/trace/rv/monitors/tlob/Kconfig create mode 100644 kernel/trace/rv/monitors/tlob/tlob.c create mode 100644 kernel/trace/rv/monitors/tlob/tlob.h create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c create mode 100644 kernel/trace/rv/monitors/tlob/tlob_trace.h create mode 100644 kernel/trace/rv/rv_uprobe.c create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile create mode 100644 tools/testing/selftests/verification/test.d/tlob/test.d/functions create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_sym.c create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_target.c create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc create mode 100644 tools/verification/models/tlob.dot
Signature · 1 line
2.43.0
PATCH v3 1/9 rv/da: introduce DA_MON_ALLOCATION_STRATEGY
From: Wen Yang <[email protected]>
Consolidate per-object DA monitor storage allocation under a single compile-time selector, replacing the ad-hoc da_monitor_init_prealloc() API.
Three strategies are provided:
DA_ALLOC_AUTO (default) - lock-free kmalloc_nolock on the hot path;
unbounded capacity. Preserves the existing behaviour for all monitors that do not set DA_MON_ALLOCATION_STRATEGY.
DA_ALLOC_POOL - pre-allocated fixed-size pool. Requires the
monitor to define DA_MON_POOL_SIZE; enforced with #error. da_prepare_storage() acquires spinlock_t (O(1), irqsave); must be called from task context on PREEMPT_RT where spinlock_t is a sleeping lock.
DA_ALLOC_MANUAL - caller pre-inserts storage via
da_create_empty_storage() before the first da_handle_start_event(); the framework only links the target field. Useful for monitors that allocate storage from known-safe task context (e.g. a syscall path) and then hand it to a tracepoint handler on the hot path.
da_handle_start_event() and da_handle_start_run_event() both call da_prepare_storage() which resolves at compile time to the correct allocation function, so no runtime dispatch is needed.
da_monitor_init_prealloc() is removed; da_monitor_init() selects pool or kmalloc initialisation internally based on the strategy.
A da_extra_cleanup() hook macro is added: the default is a no-op; a monitor may define it as a function called by da_monitor_destroy() on each remaining entry before hash_del_rcu().
nomiss is updated to DA_ALLOC_MANUAL: it calls da_create_empty_storage() from handle_sys_enter() (the sched_setscheduler syscall path, safe task context), then da_fill_empty_storage() links the sched_dl_entity target on the first da_handle_start_run_event() call in handle_sched_switch().
--- include/rv/da_monitor.h | 276 +++++++++++++++++++++-- kernel/trace/rv/monitors/nomiss/nomiss.c | 6 +- 2 files changed, 254 insertions(+), 28 deletions(-)
include/rv/da_monitor.h +251 -25
@@ -14,6 +14,26 @@#ifndef _RV_DA_MONITOR_H#define _RV_DA_MONITOR_H +/* +* Allocation strategies for RV_MON_PER_OBJ monitors. +* +* Define DA_MON_ALLOCATION_STRATEGY before including this header. +* DA_ALLOC_AUTO - lock-free kmalloc on the hot path; unbounded capacity. +* DA_ALLOC_POOL - pre-allocated fixed-size pool; requires DA_MON_POOL_SIZE. +* da_prepare_storage() acquires spinlock_t (O(1), irqsave); +* must be called from task context on PREEMPT_RT where +* spinlock_t is a sleeping lock. +* DA_ALLOC_MANUAL - caller inserts storage before da_handle_start_event(); +* the framework only links the target field. +*/ +#define DA_ALLOC_AUTO 0 +#define DA_ALLOC_POOL 1 +#define DA_ALLOC_MANUAL 2 + +#ifndef DA_MON_ALLOCATION_STRATEGY +# define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO +#endif +#include <rv/automata.h> #include <linux/rv.h> #include <linux/stringify.h>
@@ -66,6 +86,19 @@ static struct rv_monitor rv_this;#define da_monitor_sync_hook ()#endif +/* +* Hook for per-object teardown during da_monitor_destroy(). +* +* Called for each entry still in the hash table when the monitor is +* destroyed. Invoked before da_monitor_reset() and hash_del_rcu(), so +* it is safe to call ha_cancel_timer_sync() here. +* +* Define before including this header. Default is a no-op. +*/ +#ifndef da_extra_cleanup +#define da_extra_cleanup (da_mon) +#endif +/* * Type for the target id, default to int but can be overridden. * A long type can work as hash table key (PER_OBJ) but will be downgraded to
@@ -398,6 +431,16 @@ static inline void da_monitor_destroy(void)* Functions to define, init and get a per-object monitor. */ +/* +* DA_MON_POOL_SIZE must be defined before this header is included (directly or +* transitively via ha_monitor.h) when DA_ALLOC_POOL is selected. In practice +* this means defining it after the monitor's model header (which supplies the +* capacity constant) and before the ha_monitor.h include. +*/ +#if DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_POOL && !defined(DA_MON_POOL_SIZE ) +# error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be defined before including this header" +#endif +struct da_monitor_storage {da_id_type id;monitor_target target;
@@ -495,18 +538,6 @@ static inline da_id_type da_get_id(struct da_monitor *da_mon) return container_of(da_mon, struct da_monitor_storage, rv.da_mon)->id; } -/* -* da_create_or_get - create the per-object storage if not already there -* -* This needs a lookup so should be guarded by RCU, the condition is checked -* directly in da_create_storage() -*/ -static inline void da_create_or_get (da_id_type id,monitor_target target) -{ -guard (rcu)(); -da_create_storage (id, target,da_get_monitor (id, target)); -} -/* * da_fill_empty_storage - store the target in a pre-allocated storage *
@@ -537,15 +568,96 @@ static inline monitor_target da_get_target_by_id(da_id_type id)return mon_storage->target; } +/* +* Per-object pool state. +* +* Zero-initialised by default (storage == NULL ⟹ kmalloc mode). A monitor +* opts into pool mode by defining DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL +* and DA_MON_POOL_SIZE before including this header; da_monitor_init() then +* pre-allocates the pool internally. +* +* Because every field is wrapped in this struct and the struct itself is a +* per-TU static, each monitor that includes this header gets a completely +* independent pool. A kmalloc monitor (e.g. nomiss) and a pool monitor +* (e.g. tlob) therefore coexist without any interference. +* +* da_pool_return_cb runs from softirq (non-PREEMPT_RT) or rcuc kthread +* (PREEMPT_RT); spin_lock_irqsave handles both. +*/ +struct da_per_obj_pool { +struct da_monitor_storage *storage;/* non-NULL ⟹ pool mode */ +struct da_monitor_storage **free;/* kmalloc'd pointer stack */ +unsigned int free_top; +unsigned int capacity;/* total number of slots */ +spinlock_t lock; +}; + +static struct da_per_obj_pool da_pool = { + .lock =__SPIN_LOCK_UNLOCKED (da_pool.lock), +}; + +static void da_pool_return_cb (struct rcu_head *head) +{ +struct da_monitor_storage *ms = + container_of(head, struct da_monitor_storage, rcu); +unsigned long flags; + +spin_lock_irqsave (&da_pool.lock, flags); +if (!WARN_ON_ONCE (!da_pool.free || da_pool.free_top >= da_pool.capacity)) + da_pool.free[da_pool.free_top++] = ms; +spin_unlock_irqrestore (&da_pool.lock, flags); +} + +/* +* da_create_or_get_pool - pop a slot and insert it into the hash. +* +* Returns the new da_monitor on success, NULL if the pool is exhausted, or +* the existing da_monitor if a concurrent caller already inserted the same id +* (in which case the popped slot is returned to the free stack). +* +* Must be called inside an RCU read-side critical section (guard(rcu)()). +*/ +static inline struct da_monitor * +da_create_or_get_pool (da_id_type id,monitor_target target) +{ +struct da_monitor_storage *mon_storage, *existing; +unsigned long flags; + +spin_lock_irqsave (&da_pool.lock, flags); +if (!da_pool.free_top) { +spin_unlock_irqrestore (&da_pool.lock, flags); +return NULL ; + } + mon_storage = da_pool.free[--da_pool.free_top]; +spin_unlock_irqrestore (&da_pool.lock, flags); + + mon_storage->id = id; + mon_storage->target = target; + +/* +* A concurrent caller may have inserted the same id between our spinlock +* release and here. Return the slot to the pool and yield to the winner. +*/ + existing =__da_get_mon_storage (id); +if (unlikely (existing)) { +spin_lock_irqsave (&da_pool.lock, flags); + da_pool.free[da_pool.free_top++] = mon_storage; +spin_unlock_irqrestore (&da_pool.lock, flags); +return &existing->rv.da_mon; + } +hash_add_rcu (da_monitor_ht, &mon_storage->node, id); +return &mon_storage->rv.da_mon; +} + +/* * da_destroy_storage - destroy the per-object storage * -* The caller is responsible to synchronise writers, either with locks or -* implicitly. For instance, if da_destroy_storage is called at sched_exit and -* da_create_storage can never occur after that, it's safe to call this without -* locks. -* This function includes an RCU read-side critical section to synchronise -* against da_monitor_destroy(). +* Pool mode: removes from hash and returns the slot via call_rcu(). +* Kmalloc mode: removes from hash and frees via kfree_rcu(). +* +* Includes an RCU read-side critical section to synchronise against +* da_monitor_destroy(). */ static inline void da_destroy_storage (da_id_type id) {
@@ -558,7 +670,11 @@ static inline void da_destroy_storage(da_id_type id)return ;da_monitor_reset_hook (&mon_storage->rv.da_mon);hash_del_rcu (&mon_storage->node); +#if DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_POOL +call_rcu (&mon_storage->rcu, da_pool_return_cb); +#else kfree_rcu (mon_storage, rcu); +#endif }static void __da_monitor_reset_all (void (*reset)(struct da_monitor *))
@@ -581,13 +697,87 @@ static inline void da_monitor_reset_state_all(void)__da_monitor_reset_all (da_monitor_reset_state); } +/* Not part of the public API; called by da_monitor_init() for DA_ALLOC_POOL. */ +static inline int __da_monitor_init_pool (unsigned int prealloc_count) +{ + da_pool.storage =kcalloc (prealloc_count,sizeof (*da_pool.storage), +GFP_KERNEL ); +if (!da_pool.storage) +return -ENOMEM ; + + da_pool.free =kmalloc_array (prealloc_count,sizeof (*da_pool.free), +GFP_KERNEL ); +if (!da_pool.free) { +kfree (da_pool.storage); + da_pool.storage =NULL ; +return -ENOMEM ; + } + + da_pool.capacity = prealloc_count; + da_pool.free_top =0 ; +for (unsigned int i =0 ; i < prealloc_count; i++) + da_pool.free[da_pool.free_top++] = &da_pool.storage[i]; +return 0 ; +} + +/* +* da_monitor_init - initialise the per-object monitor +* +* Selects the allocation path at compile time based on DA_MON_ALLOCATION_STRATEGY: +* DA_ALLOC_POOL - pre-allocates DA_MON_POOL_SIZE storage slots. +* DA_ALLOC_AUTO / DA_ALLOC_MANUAL - initialises the hash table only. +*/ static inline int da_monitor_init (void ) {hash_init (da_monitor_ht); +#if DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_POOL +return __da_monitor_init_pool (DA_MON_POOL_SIZE ); +#else return 0 ; +#endif } -static inline void da_monitor_destroy (void ) +static inline void da_monitor_destroy_pool (void ) +{ +struct da_monitor_storage *ms; +struct hlist_node *tmp; +int bkt; + +/* +* Ensure all in-flight tracepoint handlers that may hold a raw pointer +* to a pool slot (e.g. tlob_stop_task after its RCU guard exits) have +* completed before we begin tearing down the pool. Mirrors the same +* call in da_monitor_destroy_kmalloc(). +*/ +tracepoint_synchronize_unregister (); + +/* +* Drain any entries that were not stopped before destroy (e.g. +* uprobe-started sessions whose stop probe never fired). Call +* da_extra_cleanup() before hash_del_rcu() so the hook may safely +* call ha_cancel_timer_sync() while the monitor is still reachable. +*/ +hash_for_each_safe (da_monitor_ht, bkt, tmp, ms, node) { +da_extra_cleanup (&ms->rv.da_mon); +hash_del_rcu (&ms->node); +call_rcu (&ms->rcu, da_pool_return_cb); + } + +/* +* rcu_barrier() drains every pending call_rcu() callback, including +* both da_pool_return_cb() and any monitor-specific free callbacks +* (e.g. tlob_free_rcu) enqueued by da_extra_cleanup(). +*/ +rcu_barrier (); +kfree (da_pool.storage); + da_pool.storage =NULL ; +kfree (da_pool.free); + da_pool.free =NULL ; + da_pool.free_top =0 ; + da_pool.capacity =0 ; +} + +static inline void da_monitor_destroy_kmalloc (void ) {struct da_monitor_storage *mon_storage;struct hlist_node *tmp;
@@ -607,15 +797,51 @@ static inline void da_monitor_destroy(void) }/* -* Allow the per-object monitors to run allocation manually, necessary if the -* start condition is in a context problematic for allocation (e.g. scheduling). -* In such case, if the storage was pre-allocated without a target, set it now. +* da_monitor_destroy - tear down the per-object monitor +* +* DA_ALLOC_POOL: calls tracepoint_synchronize_unregister() to drain any +* in-flight handlers, then iterates the hash draining remaining entries via +* da_extra_cleanup() + hash_del_rcu() + call_rcu(), then rcu_barrier() to +* wait for all pending da_pool_return_cb() callbacks before freeing the pool. +* DA_ALLOC_AUTO / DA_ALLOC_MANUAL: drains remaining entries after +* tracepoint_synchronize_unregister() + synchronize_rcu(). */ -#ifdef DA_SKIP_AUTO_ALLOC -#define da_prepare_storage da_fill_empty_storage +static inline void da_monitor_destroy (void ) +{ +#if DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_POOL +da_monitor_destroy_pool ();#else +da_monitor_destroy_kmalloc (); +#endif +} + +/* +* da_prepare_storage - obtain (or create) the da_monitor for (id, target) +* +* The implementation is selected at compile time by DA_MON_ALLOCATION_STRATEGY: +* +* DA_ALLOC_AUTO - calls da_create_storage() (lock-free kmalloc_nolock). +* DA_ALLOC_POOL - if an entry already exists, returns it; otherwise pops a +* slot from the pre-allocated pool and re-looks it up. +* Returns NULL if the pool is exhausted. +* DA_ALLOC_MANUAL - caller has already inserted storage via da_create_empty_storage(); +* only fills in the target field if it was left NULL. +*/ +#if DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_POOL +static inline struct da_monitor *da_prepare_storage (da_id_type id, +monitor_target target, +struct da_monitor *da_mon) +{ +if (da_mon) +return da_mon; +/* da_create_or_get_pool() returns the da_monitor directly; no re-lookup needed. */ +return da_create_or_get_pool (id, target); +} +#elif DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_MANUAL +#define da_prepare_storage da_fill_empty_storage +#else /* DA_ALLOC_AUTO */ #define da_prepare_storage da_create_storage -#endif /* DA_SKIP_AUTO_ALLOC */ +#endif #endif /* RV_MON_TYPE */
kernel/trace/rv/monitors/nomiss/nomiss.c +3 -3
@@ -17,8 +17,8 @@#define RV_MON_TYPE RV_MON_PER_OBJ#define HA_TIMER_TYPE HA_TIMER_WHEEL -/* The start condition is on sched_switch, it's dangerous to allocate there */ -#define DA_SKIP_AUTO_ALLOC +/* Allocate storage in sched_setscheduler; sched_switch is too hot to alloc. */ +#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUALtypedef struct sched_dl_entity *monitor_target ;#include "nomiss.h" #include <rv/ha_monitor.h>
@@ -214,7 +214,7 @@ static void handle_sys_enter(void *data, struct pt_regs *regs, long id)if (p->policy ==SCHED_DEADLINE )da_reset (EXPAND_ID_TASK (p));else if (new_policy ==SCHED_DEADLINE ) -da_create_or_get (EXPAND_ID_TASK (p)); +da_create_empty_storage (get_entity_id (&p->dl,task_cpu (p),DL_TASK )); }static void handle_sched_wakeup (void *data,struct task_struct *tsk)
Signature · 1 line
2.43.0
PATCH v3 2/9 add generic uprobe infrastructure for RV monitors
In reply to [email protected]From: Wen Yang <[email protected]>
Introduce rv_uprobe, a thin wrapper around uprobe_consumer providing rv_uprobe_attach_path(), rv_uprobe_attach(), and rv_uprobe_detach() for RV monitors. An opaque priv pointer is forwarded unchanged to entry/return handlers so monitors can carry per-binding state (e.g. a latency threshold) to the hot path without any global lookup.
rv_uprobe_detach() is fully synchronous (nosync + sync + path_put + kfree), closing the use-after-free window present in open-coded patterns where kfree() precedes uprobe_unregister_sync().
--- include/rv/rv_uprobe.h | 119 +++++++++++++++++++++++ kernel/trace/rv/Kconfig | 4 + kernel/trace/rv/Makefile | 1 + kernel/trace/rv/rv_uprobe.c | 182 ++++++++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 include/rv/rv_uprobe.h create mode 100644 kernel/trace/rv/rv_uprobe.c
include/rv/rv_uprobe.h new file +119 -0
@@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* +* Generic uprobe infrastructure for RV monitors. +* +*/ + +#ifndef _RV_UPROBE_H +#define _RV_UPROBE_H + +#include <linux/path.h> +#include <linux/types.h> + +struct pt_regs ; + +/** +* struct rv_uprobe - a single uprobe registered on behalf of an RV monitor +* +* @offset: byte offset within the ELF binary where the probe is installed +* @priv: monitor-private pointer; set at attach time, never touched by +* this layer; passed unchanged to entry_fn / ret_fn +* @path: resolved path of the probed binary (read-only after attach); +* callers may use path.dentry for identity comparisons +* +* The implementation fields (uprobe_consumer, uprobe handle, callbacks) are +* private to rv_uprobe.c and are not exposed here; monitors must not access +* them directly. +*/ +struct rv_uprobe { +/* public: read-only after rv_uprobe_attach*() */ +loff_t offset; +void *priv; +struct path path; +}; + +/** +* rv_uprobe_attach_path - register an uprobe given an already-resolved path +* @path: path of the target binary; rv_uprobe takes its own reference +* @offset: byte offset within the binary +* @entry_fn: called on probe hit (entry); may be NULL +* @ret_fn: called on function return (uretprobe); may be NULL +* @priv: opaque pointer forwarded to callbacks unchanged +* +* Use this variant when the caller has already resolved the path (e.g. to +* register multiple probes on the same binary with a single kern_path call). +* The inode is derived internally via d_real_inode(), so inode and path are +* always consistent. +* +* Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure. +*/ +struct rv_uprobe *rv_uprobe_attach_path (struct path *path,loff_t offset, +int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), +int (*ret_fn)(struct rv_uprobe *p,unsigned long func, +struct pt_regs *regs,__u64 *data), +void *priv); + +/** +* rv_uprobe_attach - resolve binpath and register an uprobe +* @binpath: absolute path to the target binary +* @offset: byte offset within the binary +* @entry_fn: called on probe hit (entry); may be NULL +* @ret_fn: called on function return (uretprobe); may be NULL +* @priv: opaque pointer forwarded to callbacks unchanged +* +* Resolves binpath via kern_path(), then delegates to rv_uprobe_attach_path(). +* +* Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure. +*/ +struct rv_uprobe *rv_uprobe_attach (const char *binpath,loff_t offset, +int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), +int (*ret_fn)(struct rv_uprobe *p,unsigned long func, +struct pt_regs *regs,__u64 *data), +void *priv); + +/** +* rv_uprobe_detach - synchronously unregister an uprobe and free it +* @p: probe to detach; may be NULL (no-op) +* +* Calls uprobe_unregister_nosync(), then uprobe_unregister_sync() to wait +* for any in-progress handler to finish, then releases the path reference +* and frees the rv_uprobe struct. The caller's priv data is NOT freed. +* +* When removing a single probe, prefer this over the three-phase API. +* Safe to call from process context only (uprobe_unregister_sync() may +* schedule). +*/ +void rv_uprobe_detach (struct rv_uprobe *p); + +/** +* rv_uprobe_unregister_nosync - dequeue an uprobe without waiting +* @p: probe to dequeue; may be NULL (no-op) +* +* Removes the uprobe from the uprobe subsystem but does NOT wait for +* in-flight handlers to complete. The caller must call rv_uprobe_sync() +* before calling rv_uprobe_free() on the same probe. +* +* Use this to batch multiple deregistrations before a single rv_uprobe_sync(). +*/ +void rv_uprobe_unregister_nosync (struct rv_uprobe *p); + +/** +* rv_uprobe_sync - wait for all in-flight uprobe handlers to complete +* +* Global barrier: waits for every in-flight uprobe handler across the system +* to finish. Call once after a batch of rv_uprobe_unregister_nosync() calls +* and before any rv_uprobe_free() call. +*/ +void rv_uprobe_sync (void ); + +/** +* rv_uprobe_free - release resources of a previously deregistered probe +* @p: probe to free; may be NULL (no-op) +* +* Releases the path reference and frees the rv_uprobe struct. Must only +* be called after rv_uprobe_sync() has returned. The caller's priv data +* is NOT freed. +*/ +void rv_uprobe_free (struct rv_uprobe *p); + +#endif /* _RV_UPROBE_H */
kernel/trace/rv/Kconfig +4 -0
@@ -59,6 +59,10 @@ config RV_PER_TASK_MONITORS This option configures the maximum number of per-task RV monitors that can run simultaneously. +config RV_UPROBE + bool + depends on RV && UPROBES + source "kernel/trace/rv/monitors/wip/Kconfig" source "kernel/trace/rv/monitors/wwnr/Kconfig"
kernel/trace/rv/Makefile +1 -0
@@ -21,6 +21,7 @@ obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o # Add new monitors here +obj-$(CONFIG_RV_UPROBE) += rv_uprobe.o obj-$(CONFIG_RV_REACTORS) += rv_reactors.o obj-$(CONFIG_RV_REACT_PRINTK) += reactor_printk.o obj-$(CONFIG_RV_REACT_PANIC) += reactor_panic.o
kernel/trace/rv/rv_uprobe.c new file +182 -0
182 lines not on this page · raw message
Signature · 1 line
2.43.0
PATCH v3 3/9 rv/tlob: add tlob model DOT file
In reply to [email protected]From: Wen Yang <[email protected]>
Add tools/verification/models/tlob.dot, the Graphviz specification of the tlob hybrid automaton. The model has three states (running, waiting, sleeping) connected by four transitions (switch_in, preempt, wakeup, sleep) with a single clock invariant clk_elapsed < BUDGET_NS() active in all states.
--- tools/verification/models/tlob.dot | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tools/verification/models/tlob.dot
tools/verification/models/tlob.dot new file +22 -0
@@ -0,0 +1,22 @@ +digraph state_automaton { + center = true; + size = "7,11"; + {node [shape = plaintext, style=invis, label=""] "__init_running"}; + {node [shape = ellipse] "running"}; + {node [shape = plaintext] "running"}; + {node [shape = plaintext] "waiting"}; + {node [shape = plaintext] "sleeping"}; + "__init_running" -> "running"; + "running" -> "running" [ label = "start;reset(clk_elapsed)" ]; + "running" [label = "running\nclk_elapsed < BUDGET_NS()", color = green3]; + "waiting" [label = "waiting\nclk_elapsed < BUDGET_NS()"]; + "sleeping" [label = "sleeping\nclk_elapsed < BUDGET_NS()"]; + "running" -> "sleeping" [ label = "sleep" ]; + "running" -> "waiting" [ label = "preempt" ]; + "waiting" -> "running" [ label = "switch_in" ]; + "sleeping" -> "waiting" [ label = "wakeup" ]; + { rank = min ; + "__init_running"; + "running"; + } +}
Signature · 1 line
2.43.0
PATCH v3 4/9 rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check
In reply to [email protected]From: Wen Yang <[email protected]>
The function is documented as "prepare the invariant and return the time since reset", but on the first call (env_store == U64_MAX) it exits early without calling ha_set_invariant_ns():
if (ha_monitor_env_invalid(ha_mon, env)) /* env_store == U64_MAX */
return 0; /* ha_set_invariant_ns skipped, env_store stays U64_MAX */
...
ha_set_invariant_ns(ha_mon, env, expire - passed, time_ns);This leaves env_store == U64_MAX, so ha_check_invariant_ns() always passes on the first activation regardless of elapsed time:
return READ_ONCE(ha_mon->env_store[env]) >= time_ns; /* U64_MAX >= any */
Fix: establish the guard before converting to the invariant:
if (ha_monitor_env_invalid(ha_mon, env))
ha_reset_clk_ns(ha_mon, env, time_ns); /* guard: env_store = time_ns */
passed = ha_get_env(ha_mon, env, time_ns);
ha_set_invariant_ns(ha_mon, env, expire - passed, time_ns);
/* invariant: env_store = time_ns + expire */Apply the same fix to ha_invariant_passed_jiffy().
--- include/rv/ha_monitor.h | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-)
include/rv/ha_monitor.h +13 -4
@@ -365,16 +365,22 @@ static inline bool ha_check_invariant_ns(struct ha_monitor *ha_mon, }/* * ha_invariant_passed_ns - prepare the invariant and return the time since reset +* +* If the env has not been initialised yet (first entry into a state with an +* invariant), anchor the guard clock at the current time so that the full +* budget is available from this point. This preserves the documented +* guard→invariant ordering: ha_set_invariant_ns() is always preceded by a +* valid guard representation in env_store. */ static inline u64 ha_invariant_passed_ns (struct ha_monitor *ha_mon,enum envs env,u64 expire,u64 time_ns) { -u64 passed =0 ; +u64 passed;if (env <0 || env >=ENV_MAX_STORED )return 0 ;if (ha_monitor_env_invalid (ha_mon, env)) -return 0 ; +ha_reset_clk_ns (ha_mon, env, time_ns); passed =ha_get_env (ha_mon, env, time_ns);ha_set_invariant_ns (ha_mon, env, expire - passed, time_ns);return passed;
@@ -404,16 +410,19 @@ static inline bool ha_check_invariant_jiffy(struct ha_monitor *ha_mon, }/* * ha_invariant_passed_jiffy - prepare the invariant and return the time since reset +* +* Same first-use semantics as ha_invariant_passed_ns(): anchor the guard clock +* now if the env has not been initialised. */ static inline u64 ha_invariant_passed_jiffy (struct ha_monitor *ha_mon,enum envs env,u64 expire,u64 time_ns) { -u64 passed =0 ; +u64 passed;if (env <0 || env >=ENV_MAX_STORED )return 0 ;if (ha_monitor_env_invalid (ha_mon, env)) -return 0 ; +ha_reset_clk_jiffy (ha_mon, env); passed =ha_get_env (ha_mon, env, time_ns);ha_set_invariant_jiffy (ha_mon, env, expire - passed);return passed;
Signature · 1 line
2.43.0
PATCH v3 5/9 rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable
In reply to [email protected]From: Wen Yang <[email protected]>
Wrap the two definitions with #ifndef guards so that HA-based monitors can substitute their own implementations before including this header:
/* in monitor.c, before #include <rv/ha_monitor.h> */ #define da_monitor_reset_hook my_monitor_reset_env #define EVENT_NONE_LBL "idle"
No behaviour change for monitors that do not override either macro.
--- include/rv/ha_monitor.h | 5 +++++ 1 file changed, 5 insertions(+)
include/rv/ha_monitor.h +5 -0
@@ -36,7 +36,10 @@ static bool ha_monitor_handle_constraint(struct da_monitor *da_mon, da_id_type id);#define da_monitor_event_hook ha_monitor_handle_constraint#define da_monitor_init_hook ha_monitor_init_env +/* Allow monitors to override da_monitor_reset_hook before including this header. */ +#ifndef da_monitor_reset_hook#define da_monitor_reset_hook ha_monitor_reset_env +#endif #define da_monitor_sync_hook () synchronize_rcu()#if !defined(HA_SKIP_AUTO_CLEANUP ) &&RV_MON_TYPE ==RV_MON_PER_TASK
@@ -75,7 +78,9 @@ _Static_assert(offsetof(struct ha_monitor, da_mon) == 0,#define ENV_INVALID_VALUE U64_MAX/* Error with no event occurs only on timeouts */ #define EVENT_NONE EVENT_MAX +#ifndef EVENT_NONE_LBL #define EVENT_NONE_LBL "none" +#endif #define ENV_BUFFER_SIZE 64#ifdef CONFIG_RV_REACTORS
Signature · 1 line
2.43.0
PATCH v3 6/9 rv/tlob: add tlob hybrid automaton monitor
In reply to [email protected]From: Wen Yang <[email protected]>
Add tlob (task latency over budget), a per-task hybrid automaton RV monitor that tracks elapsed wall-clock time across a user-delimited code section and emits error_env_tlob when the elapsed time exceeds a configurable budget.
The monitor uses RV_MON_PER_OBJ with three states (running, waiting, sleeping) driven by sched_switch and sched_wakeup tracepoints, and a single clock invariant clk_elapsed < budget enforced by an hrtimer (HRTIMER_MODE_REL_HARD). On violation, detail_env_tlob provides a per-state time breakdown (running_ns, waiting_ns, sleeping_ns).
Per-task state is managed via DA_ALLOC_POOL to avoid allocation on the scheduler tracepoint path. Uprobe pairs are registered through the tracefs monitor file as "p PATH:OFFSET_START OFFSET_STOP threshold=NS".
Also adds ha_cancel_timer_sync() to ha_monitor.h, a blocking cancel variant needed by tlob's stop_task path to ensure the hrtimer callback has completed before the per-task monitor state is freed.
--- Documentation/trace/rv/index.rst | 1 + Documentation/trace/rv/monitor_tlob.rst | 177 ++++ kernel/trace/rv/Kconfig | 1 + kernel/trace/rv/Makefile | 1 + kernel/trace/rv/monitors/tlob/Kconfig | 12 + kernel/trace/rv/monitors/tlob/tlob.c | 968 +++++++++++++++++++++ kernel/trace/rv/monitors/tlob/tlob.h | 148 ++++ kernel/trace/rv/monitors/tlob/tlob_trace.h | 49 ++ kernel/trace/rv/rv_trace.h | 1 + 9 files changed, 1358 insertions(+) create mode 100644 Documentation/trace/rv/monitor_tlob.rst create mode 100644 kernel/trace/rv/monitors/tlob/Kconfig create mode 100644 kernel/trace/rv/monitors/tlob/tlob.c create mode 100644 kernel/trace/rv/monitors/tlob/tlob.h create mode 100644 kernel/trace/rv/monitors/tlob/tlob_trace.h
Documentation/trace/rv/index.rst +1 -0
@@ -16,5 +16,6 @@ Runtime Verification monitor_wwnr.rst monitor_sched.rst monitor_rtapp.rst + monitor_tlob.rst monitor_stall.rst monitor_deadline.rst
Documentation/trace/rv/monitor_tlob.rst new file +177 -0
177 lines not on this page · raw message
kernel/trace/rv/Kconfig +1 -0
@@ -85,6 +85,7 @@ source "kernel/trace/rv/monitors/sleep/Kconfig" source "kernel/trace/rv/monitors/stall/Kconfig" source "kernel/trace/rv/monitors/deadline/Kconfig" source "kernel/trace/rv/monitors/nomiss/Kconfig" +source "kernel/trace/rv/monitors/tlob/Kconfig" # Add new deadline monitors here # Add new monitors here
kernel/trace/rv/Makefile +1 -0
@@ -20,6 +20,7 @@ obj-$(CONFIG_RV_MON_OPID) += monitors/opid/opid.o obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o +obj-$(CONFIG_RV_MON_TLOB) += monitors/tlob/tlob.o # Add new monitors here obj-$(CONFIG_RV_UPROBE) += rv_uprobe.o obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
kernel/trace/rv/monitors/tlob/Kconfig new file +12 -0
@@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +config RV_MON_TLOB + depends on RV && UPROBES && HIGH_RES_TIMERS + select HA_MON_EVENTS_ID + select RV_UPROBE + bool "tlob monitor" + help + Enable the tlob (task latency over budget) hybrid-automaton RV + monitor. tlob tracks per-task elapsed wall-clock time across a + user-delimited code section and emits error_env_tlob when the + elapsed time exceeds a configurable per-invocation budget.
kernel/trace/rv/monitors/tlob/tlob.c new file +968 -0
968 lines not on this page · raw message
kernel/trace/rv/monitors/tlob/tlob.h new file +148 -0
148 lines not on this page · raw message
kernel/trace/rv/monitors/tlob/tlob_trace.h new file +49 -0
49 lines not on this page · raw message
kernel/trace/rv/rv_trace.h +1 -0
@@ -189,6 +189,7 @@ DECLARE_EVENT_CLASS(error_env_da_monitor_id,#include <monitors/stall/stall_trace.h> #include <monitors/nomiss/nomiss_trace.h> +#include <monitors/tlob/tlob_trace.h> // Add new monitors based on CONFIG_HA_MON_EVENTS_ID here #endif
Signature · 1 line
2.43.0
PATCH v3 7/9 rv/tlob: add KUnit tests for the tlob monitor
In reply to [email protected]From: Wen Yang <[email protected]>
Add CONFIG_TLOB_KUNIT_TEST (tristate, depends on RV_MON_TLOB && KUNIT, default KUNIT_ALL_TESTS) with a single test suite covering the uprobe line parser: valid bindings are accepted, malformed ones return -EINVAL, and out-of-range thresholds return -ERANGE.
--- kernel/trace/rv/Makefile | 1 + kernel/trace/rv/monitors/tlob/.kunitconfig | 6 ++ kernel/trace/rv/monitors/tlob/Kconfig | 7 ++ kernel/trace/rv/monitors/tlob/tlob_kunit.c | 92 ++++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c
kernel/trace/rv/Makefile +1 -0
@@ -21,6 +21,7 @@ obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o obj-$(CONFIG_RV_MON_TLOB) += monitors/tlob/tlob.o +obj-$(CONFIG_TLOB_KUNIT_TEST) += monitors/tlob/tlob_kunit.o # Add new monitors here obj-$(CONFIG_RV_UPROBE) += rv_uprobe.o obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
kernel/trace/rv/monitors/tlob/.kunitconfig new file +6 -0
@@ -0,0 +1,6 @@ +CONFIG_FTRACE=y +CONFIG_KUNIT=y +CONFIG_MODULES=y +CONFIG_RV=y +CONFIG_RV_MON_TLOB=y +CONFIG_TLOB_KUNIT_TEST=y
kernel/trace/rv/monitors/tlob/Kconfig +7 -0
@@ -10,3 +10,10 @@ config RV_MON_TLOB monitor. tlob tracks per-task elapsed wall-clock time across a user-delimited code section and emits error_env_tlob when the elapsed time exceeds a configurable per-invocation budget. + +config TLOB_KUNIT_TEST + tristate "KUnit tests for tlob monitor" if !KUNIT_ALL_TESTS + depends on RV_MON_TLOB && KUNIT + default KUNIT_ALL_TESTS + help + Enable KUnit unit tests for the tlob RV monitor.
kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +92 -0
92 lines not on this page · raw message
Signature · 1 line
2.43.0
PATCH v3 8/9 selftests/verification: fix verificationtest-ktap for out-of-tree execution
In reply to [email protected]From: Wen Yang <[email protected]>
verificationtest-ktap used CWD-relative paths which broke when invoked outside the verification directory (e.g. via vng).
Resolve paths via realpath "$(dirname "$0")" so the script works from any working directory. Accept an optional subdirectory argument interpreted relative to the script's directory.
--- tools/testing/selftests/verification/verificationtest-ktap | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-)
tools/testing/selftests/verification/verificationtest-ktap +3 -1
@@ -5,4 +5,6 @@ # # Copyright (C) Arm Ltd., 2023 -../ftrace/ftracetest -K -v --rv ../verification +dir=$(realpath "$(dirname "$0")") +testdir=$(cd "$dir" && realpath "${1:-.}") +"$dir/../ftrace/ftracetest" -K -v --rv "$testdir"
Signature · 1 line
2.43.0
PATCH v3 9/9 selftests/verification: add tlob selftests
In reply to [email protected]From: Wen Yang <[email protected]>
Add selftest coverage for the tlob uprobe monitoring interface under tools/testing/selftests/verification/.
test.d/tlob/ contains both the helper sources (tlob_target, tlob_sym) and the seven test scripts so the test suite is self-contained. tlob_target provides busy-spin, sleep, and preempt workloads; tlob_sym resolves ELF symbol offsets for uprobe registration.
Seven test scripts exercise uprobe binding management, budget violation detection, and per-state time accounting (running_ns, waiting_ns, sleeping_ns).
--- .../testing/selftests/verification/.gitignore | 2 + tools/testing/selftests/verification/Makefile | 19 +- .../verification/test.d/tlob/Makefile | 20 ++ .../verification/test.d/tlob/test.d/functions | 1 + .../verification/test.d/tlob/tlob_sym.c | 189 ++++++++++++++++++ .../verification/test.d/tlob/tlob_target.c | 138 +++++++++++++ .../verification/test.d/tlob/uprobe_bind.tc | 37 ++++ .../test.d/tlob/uprobe_detail_running.tc | 51 +++++ .../test.d/tlob/uprobe_detail_sleeping.tc | 50 +++++ .../test.d/tlob/uprobe_detail_waiting.tc | 66 ++++++ .../verification/test.d/tlob/uprobe_multi.tc | 64 ++++++ .../test.d/tlob/uprobe_no_event.tc | 19 ++ .../test.d/tlob/uprobe_violation.tc | 67 +++++++ 13 files changed, 722 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile create mode 100644 tools/testing/selftests/verification/test.d/tlob/test.d/functions create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_sym.c create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_target.c create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
tools/testing/selftests/verification/.gitignore +2 -0
@@ -1,2 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only logs +test.d/tlob/tlob_sym +test.d/tlob/tlob_target
tools/testing/selftests/verification/Makefile +18 -1
@@ -1,8 +1,25 @@ # SPDX-License-Identifier: GPL-2.0 -all: TEST_PROGS := verificationtest-ktap TEST_FILES := test.d settings EXTRA_CLEAN := $(OUTPUT)/logs/* +# Subdirectories that provide binaries used by the test runner. +# Each entry must contain a Makefile that accepts OUTDIR= and +# deposits its binaries there. +BUILD_SUBDIRS := test.d/tlob + include ../lib.mk + +all: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) + +clean: $(patsubst %,_clean_%,$(BUILD_SUBDIRS)) + +.PHONY: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) \ + $(patsubst %,_clean_%,$(BUILD_SUBDIRS)) + +$(patsubst %,_build_%,$(BUILD_SUBDIRS)): _build_%: + $(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)" + +$(patsubst %,_clean_%,$(BUILD_SUBDIRS)): _clean_%: + $(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
tools/testing/selftests/verification/test.d/tlob/Makefile new file +20 -0
@@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0 +# Builds tlob selftest helper binaries in the directory of this Makefile. +# +# Invoked by ../../Makefile via BUILD_SUBDIRS; outputs tlob_sym and +# tlob_target alongside the .tc scripts so they are self-contained. + +CFLAGS += $(TOOLS_INCLUDES) + +.PHONY: all +all: tlob_sym tlob_target + +tlob_sym: tlob_sym.c + $(CC) $(CFLAGS) -o $@ $< + +tlob_target: tlob_target.c + $(CC) $(CFLAGS) -o $@ $< + +.PHONY: clean +clean: + $(RM) tlob_sym tlob_target
tools/testing/selftests/verification/test.d/tlob/test.d/functions new file +1 -0
@@ -0,0 +1 @@ +. "${TOP_DIR%/*}/functions"
tools/testing/selftests/verification/test.d/tlob/tlob_sym.c new file +189 -0
189 lines not on this page · raw message
tools/testing/selftests/verification/test.d/tlob/tlob_target.c new file +138 -0
138 lines not on this page · raw message
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +37 -0
@@ -0,0 +1,37 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor uprobe binding (visible in monitor file, removable, duplicate rejected) +# requires: tlob:monitor + +RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET="${RV_BINDIR}/tlob_target" +TLOB_SYM="${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=monitors/tlob/monitor + +busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null) +stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null) +[ -n "$busy_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupported + +"$UPROBE_TARGET" 30000 & +busy_pid=$! +sleep 0.05 + +echo 1 > monitors/tlob/enable +echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=5000000000" > "$TLOB_MONITOR" + +# Binding must appear in monitor file with canonical hex-offset format. +grep -qE "^p ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=[0-9]+$" "$TLOB_MONITOR" +grep -q "threshold=5000000000" "$TLOB_MONITOR" + +# Duplicate offset_start must be rejected. +! echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=9999000" > "$TLOB_MONITOR" 2>/dev/null + +# Remove the binding; it must no longer appear. +echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" +! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR" + +kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true +echo 0 > monitors/tlob/enable
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc new file +51 -0
51 lines not on this page · raw message
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc new file +50 -0
50 lines not on this page · raw message
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc new file +66 -0
66 lines not on this page · raw message
tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +64 -0
64 lines not on this page · raw message
tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file +19 -0
@@ -0,0 +1,19 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor no spurious events without active uprobe binding +# requires: tlob:monitor + +TLOB_MONITOR=monitors/tlob/monitor + +echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +sleep 0.5 + +! grep -q "error_env_tlob" /sys/kernel/tracing/trace + +echo 0 > monitors/tlob/enable +echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo > /sys/kernel/tracing/trace
tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +67 -0
67 lines not on this page · raw message
Signature · 1 line
2.43.0
Hi Gabriele,
Gentle ping on this series. Please let me know if there are any concerns or if further changes are needed.
Thanks for your time,
Wen
Quoted 240 lines · On 6/8/26 00:13, [email protected] wrote:
From: Wen Yang <[email protected]>
This series introduces tlob (task latency over budget), a per-task hybrid automaton RV monitor that measures elapsed wall-clock time across a user-delimited code section and fires when the time exceeds a configurable budget.
The series applies cleanly on top of:
[1] git://git.kernel.org/pub/scm/linux/kernel/git/gmonaco/linux.git rv-fixes-7.1 "rv fixes for v7.1"Background
----------
The existing wwnr monitor uses a two-state DA to detect tasks that are woken but never run. tlob extends the RV framework to a three-state hybrid automaton:running (initial) -- on CPU
waiting -- in the scheduler runqueue, not yet on CPU
sleeping -- blocked on a lock, I/O, or similar resourceA single HA clock invariant,
clk_elapsed<BUDGET_NS(), is active in all states. The framework enforces it via a per-task hrtimer. On expiry,error_env_tlobis emitted, followed bydetail_env_tlobwhich carries a per-state time breakdown (running_ns,waiting_ns,sleeping_ns) that pinpoints whether the overrun occurred in the running, waiting, or sleeping state.Userspace interface
-------------------
Tasks are registered for monitoring by writing to the tracefs monitor file:# echo "p /path/to/binary:START_OFFSET STOP_OFFSET threshold=NS" \ > /sys/kernel/tracing/rv/monitors/tlob/monitorTwo uprobes are registered at
START_OFFSET(entry) andSTOP_OFFSET(exit) of the delimited section. When a task executes the entry uprobe, the monitor starts; when the task reaches the exit uprobe or the budget expires, monitoring stops and the slot is returned to the pool.Multiple uprobe pairs can be registered for the same binary or different binaries. Each task can have at most one active monitoring session; if a task hits a start uprobe while already monitored, the prior session is cancelled and a new one begins.
Series structure
----------------
Patch 1: rv/da: introduceDA_MON_ALLOCATION_STRATEGY
Consolidates per-object DA storage allocation under a compile-time selector with three strategies:
DA_ALLOC_AUTO(default) - lock-freekmalloc_nolock; unbounded
DA_ALLOC_POOL- pre-allocated fixed-size pool
DA_ALLOC_MANUAL- caller pre-inserts storage
da_handle_start_event()andda_handle_start_run_event()callda_prepare_storage()which resolves at compile time to the correct allocation function.This patch also includes critical correctness fixes for the pool implementation:
- Addtracepoint_synchronize_unregister()inda_monitor_destroy_pool()to fix UAF where in-flight handlers access freed pool storage
- Fix duplicate hash entry race inda_create_or_get_pool()via concurrent-insert detection under RCU
- Add capacity field to fix build error (DA_MON_POOL_SIZEundeclared inda_pool_return_cb)Patch 2: rv: add generic uprobe infrastructure for RV monitors
Introducesrv_uprobe, a thin wrapper arounduprobe_consumerfor RV monitors. Providesrv_uprobe_register(),rv_uprobe_unregister(), andrv_uprobe_sync()for safe teardown.Patch 3: rv/tlob: add tlob model DOT file
The formal model used to generatetlob.h.Patch 4: rv/ha: fix
ha_invariant_passed_nssilent bypass of invariant check
Fixes a bug whereha_invariant_passed_ns()returned 0 early whenenv_storewas invalid (U64_MAX), leaving it atU64_MAXand causingha_check_invariant_ns()to always pass. The fix callsha_reset_clk_ns()thenha_set_invariant_ns()on first use.Patch 5: rv/ha: make
da_monitor_reset_hookandEVENT_NONE_LBLoverridable
Allows tlob to overrideEVENT_NONE_LBLfor itsstart_tlobself-loop.Patch 6: rv/tlob: add tlob hybrid automaton monitor
The main tlob implementation, including:
- Three-state HA (running/waiting/sleeping)
- Per-task hrtimer enforcement (HRTIMER_MODE_REL_HARD)
-DA_ALLOC_POOLfor allocation-free hot path
- Uprobe registration via tracefs monitor file
- Per-state time accumulation (running_ns,waiting_ns,sleeping_ns)
-HIGH_RES_TIMERSdependency inKconfigPatches 7-9: Tests
- KUnit tests for tlob monitor
- Selftest infrastructure fixes
- tlob selftests (uprobe binding, state tracking, violation detection)Changes since v2
----------------
All feedback from Gabriele Monaco has been addressed:-- Patch 02 (per-task slot ordering /
ha_monitor_reset_env):
Dropped from v3; rebased on top of Gabriele's series [1].-- Patch 03 (verificationtest-ktap):
Changed to use realpath for robustness as suggested.-- Patch 04 (pre-allocated storage pool):
Complete redesign asDA_MON_ALLOCATION_STRATEGY:
- Three strategies (AUTO/POOL/MANUAL) via compile-time macro
-da_monitor_init_prealloc()removed;da_monitor_init()selects internally
-da_create_or_get_kmalloc()removed (no viable use case)
- nomiss updated to useDA_ALLOC_MANUAL
-da_extra_cleanup()hook added for per-entry teardownCritical bug fixes included in this patch:
-tracepoint_synchronize_unregister()added toda_monitor_destroy_pool()to prevent UAF from in-flight handlers accessing freed pool storage
- Duplicate hash entry race fixed inda_create_or_get_pool()via concurrent-insert detection and slot return under RCU
- capacity field added to fixDA_MON_POOL_SIZEundeclared build error-- Patch 05 (generic uprobe infrastructure):
Carried unchanged into v3.-- Patch 06 (rvgen
__initarrow reset):
Carried unchanged into v3.-- Patch 08 (tlob monitor):
Split and refactored:
- ioctl interface deferred to follow-up series (tracefs-only in v3)
- Handler simplification: three inline helpers (tlob_acc_running/ waiting/sleeping) withscoped_guard(rcu)
- do_prev/do_next flags removed (da_handle_eventskips unmonitored)
-scoped_guard(rcu)and guard(mutex) applied throughout
-tlob_stop_all()removed;da_extra_cleanup()hook used instead
-start_tlobself-loop added to DOT model as suggested
-ha_setup_invariants()guards against redundant timer restart
-HIGH_RES_TIMERSdependency added toKconfigAdditional improvements in v3
------------------------------
Beyond the v2 feedback, this version includes:1. Simplified tlob monitor implementation:
- Removed redundanttlob_num_monitoredatomic counter (da_handle_eventalready handles unmonitored tasks via hash lookup)
- Eliminated extra cacheline touch on every sched_switch/sched_wakeup
- Several repeated pattern simplifications.2. Extracted common accumulation logic:
-__tlob_acc()usingoffsetof()replaces three nearly-identical functions
- Reduces code duplication while maintaining type safety3. Complete test coverage:
- KUnit tests for core functionality
- Comprehensive selftests for uprobe integration, state tracking, and violation detectionTesting
-------
All patches have been tested on:
-x86_64withCONFIG_PREEMPT_RT
- All KUnit tests pass
- All selftests pass with verificationtest-ktap[1]
git://git.kernel.org/pub/scm/linux/kernel/git/gmonaco/linux.gitrv-fixes-7.1"rv fixes for v7.1"Wen Yang (9):
rv/da: introduceDA_MON_ALLOCATION_STRATEGY
rv: add generic uprobe infrastructure for RV monitors rv/tlob: add tlob model DOT file
rv/ha: fixha_invariant_passed_nssilent bypass of invariant check rv/ha: makeda_monitor_reset_hookandEVENT_NONE_LBLoverridable rv/tlob: add tlob hybrid automaton monitor
rv/tlob: add KUnit tests for the tlob monitor selftests/verification: fix verificationtest-ktap for out-of-tree
execution
selftests/verification: add tlob selftestsDocumentation/trace/rv/index.rst | 1 + Documentation/trace/rv/monitor_tlob.rst | 177 ++++ include/rv/da_monitor.h | 276 ++++- include/rv/ha_monitor.h | 22 +- include/rv/rv_uprobe.h | 119 +++ kernel/trace/rv/Kconfig | 5 + kernel/trace/rv/Makefile | 3 + kernel/trace/rv/monitors/nomiss/nomiss.c | 6 +- kernel/trace/rv/monitors/tlob/.kunitconfig | 6 + kernel/trace/rv/monitors/tlob/Kconfig | 19 + kernel/trace/rv/monitors/tlob/tlob.c | 968 ++++++++++++++++++ kernel/trace/rv/monitors/tlob/tlob.h | 148 +++ kernel/trace/rv/monitors/tlob/tlob_kunit.c | 92 ++ kernel/trace/rv/monitors/tlob/tlob_trace.h | 49 + kernel/trace/rv/rv_trace.h | 1 + kernel/trace/rv/rv_uprobe.c | 182 ++++ .../testing/selftests/verification/.gitignore | 2 + tools/testing/selftests/verification/Makefile | 19 +- .../verification/test.d/tlob/Makefile | 20 + .../verification/test.d/tlob/test.d/functions | 1 + .../verification/test.d/tlob/tlob_sym.c | 189 ++++ .../verification/test.d/tlob/tlob_target.c | 138 +++ .../verification/test.d/tlob/uprobe_bind.tc | 37 + .../test.d/tlob/uprobe_detail_running.tc | 51 + .../test.d/tlob/uprobe_detail_sleeping.tc | 50 + .../test.d/tlob/uprobe_detail_waiting.tc | 66 ++ .../verification/test.d/tlob/uprobe_multi.tc | 64 ++ .../test.d/tlob/uprobe_no_event.tc | 19 + .../test.d/tlob/uprobe_violation.tc | 67 ++ .../verification/verificationtest-ktap | 4 +- tools/verification/models/tlob.dot | 22 + 31 files changed, 2789 insertions(+), 34 deletions(-)create mode 100644
Documentation/trace/rv/monitor_tlob.rst
create mode 100644include/rv/rv_uprobe.h
create mode 100644kernel/trace/rv/monitors/tlob/.kunitconfig
create mode 100644kernel/trace/rv/monitors/tlob/Kconfig
create mode 100644kernel/trace/rv/monitors/tlob/tlob.c
create mode 100644kernel/trace/rv/monitors/tlob/tlob.h
create mode 100644kernel/trace/rv/monitors/tlob/tlob_kunit.c
create mode 100644kernel/trace/rv/monitors/tlob/tlob_trace.h
create mode 100644kernel/trace/rv/rv_uprobe.c
create mode 100644tools/testing/selftests/verification/test.d/tlob/Makefilecreate mode 100644tools/testing/selftests/verification/test.d/tlob/test.d/functionscreate mode 100644tools/testing/selftests/verification/test.d/tlob/tlob_sym.ccreate mode 100644tools/testing/selftests/verification/test.d/tlob/tlob_target.ccreate mode 100644tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tccreate mode 100644tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tccreate mode 100644tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tccreate mode 100644tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tccreate mode 100644tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tccreate mode 100644tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tccreate mode 100644tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tccreate mode 100644tools/verification/models/tlob.dot
Gabriele Monaco
maintainerRe: [PATCH v3 1/9] rv/da: introduce DA_MON_ALLOCATION_STRATEGY
In reply to [email protected]include/rv/da_monitor.h line 35
Quoted 3 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
include/rv/da_monitor.h +3
-0+#ifndef DA_MON_ALLOCATION_STRATEGY +# define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO +#endif
I'm not sure the space goes there according to kernel coding style, we don't use it in this file and clang-format removes it. I'd keep consistency.
...
Quoted 3 lines
+/*
+ *DA_MON_POOL_SIZEmust be defined before this header is included
I don't think we need to be this verbose (also, ha_monitor may not even be included if that's a da_monitor). I would stop at the line above.
Quoted 8 lines
(directly or
+ * transitively viaha_monitor.h) whenDA_ALLOC_POOLis selected. In practice
+ * this means defining it after the monitor's model header (which supplies the
+ * capacity constant) and before theha_monitor.hinclude.
+ */
Quoted 4 lines
+#if
DA_MON_ALLOCATION_STRATEGY==DA_ALLOC_POOL&& !defined(DA_MON_POOL_SIZE)
+# error "DA_ALLOC_POOLrequiresDA_MON_POOL_SIZEto be defined before including this header"
Same here I'd keep consistency and remove the space before error.
Quoted 1 line
+#endif
...
Quoted 4 lines
+/*
+ * Per-object pool state.
+ *
+ * Zero-initialised by default (storage == NULL ⟹ kmalloc mode). A
Mmh, ⟹ doesn't seem to print that well on my terminal, let's perhaps use plain old ASCII => . Also I don't find what you put in parentheses to be adding much value, we could even omit it.
I remember discussing about this so I may have missed your answer, but why don't we handle this pool as a simple kmem_cache/mempool instead of implementing a similar logic from scratch?
Quoted 33 lines
monitor
+ * opts into pool mode by definingDA_MON_ALLOCATION_STRATEGYDA_ALLOC_POOL
+ * andDA_MON_POOL_SIZEbefore including this header;da_monitor_init()then
+ * pre-allocates the pool internally.
+ *
+ * Because every field is wrapped in this struct and the struct itself is a
+ * per-TU static, each monitor that includes this header gets a completely
+ * independent pool. A kmalloc monitor (e.g. nomiss) and a pool monitor
+ * (e.g. tlob) therefore coexist without any interference.
+ *
+ *da_pool_return_cbruns from softirq (non-PREEMPT_RT) or rcuc kthread
+ * (PREEMPT_RT);spin_lock_irqsavehandles both.
+ */(quoted diff) +2
-0+struct da_per_obj_pool { + struct da_monitor_storage *storage; /* non-NULL ⟹ poolmode */
(quoted diff) +1
-0+ struct da_monitor_storage **free; /* kmalloc'd pointerstack */
(quoted diff) +2
-0+ unsigned int free_top; + unsigned int capacity; /* total number ofslots */
include/rv/da_monitor.h +6
-0+ spinlock_t lock; +}; + +static struct da_per_obj_pool da_pool = { + .lock =__SPIN_LOCK_UNLOCKED (da_pool.lock), +};
...
Quoted 22 lines
/*
*da_destroy_storage- destroy the per-object storage
*
- * The caller is responsible to synchronise writers, either with locks or
- * implicitly. For instance, ifda_destroy_storageis called atsched_exitand
- *da_create_storagecan never occur after that, it's safe to call this without
- * locks.
- * This function includes an RCU read-side critical section to synchronise
- * againstda_monitor_destroy().
+ * Pool mode: removes from hash and returns the slot viacall_rcu().
+ * Kmalloc mode: removes from hash and frees viakfree_rcu().
+ *
+ * Includes an RCU read-side critical section to synchronise against
+ *da_monitor_destroy().
*/
static inline voidda_destroy_storage(da_id_type id)
{
include/rv/da_monitor.h static inline void da_destroy_storage(da_id_type · line 676
Quoted 7 lines
include/rv/da_monitor.h +3
-0@@ -558,7 +670,11 @@ static inline void da_destroy_storage(da_id_type id) return; da_monitor_reset_hook(&mon_storage->rv.da_mon); hash_del_rcu(&mon_storage->node); +#if DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_POOL +call_rcu (&mon_storage->rcu, da_pool_return_cb); +#else
ifdeffery in functions is discouraged as quite unreadable. Since DA_MON_ALLOCATION_STRATEGY is guaranteed to be defined, simple C ifs are going to be mostly equivalent (the compiler will cut instead of the preprocessor, but still).
Quoted 3 lines
include/rv/da_monitor.h +1
-0kfree_rcu(mon_storage, rcu); +#endif }
...
Quoted 13 lines
include/rv/da_monitor.h +5
-0+ +/* +* da_monitor_init - initialise the per-object monitor +* +* Selects the allocation path at compile time based on
DA_MON_ALLOCATION_STRATEGY:
+ * DA_ALLOC_POOL - pre-allocatesDA_MON_POOL_SIZEstorage slots.
+ *DA_ALLOC_AUTO/DA_ALLOC_MANUAL- initialises the hash table only.
+ */
static inline intda_monitor_init(void)
{
hash_init(da_monitor_ht);
include/rv/da_monitor.h static inline void da_monitor_reset_state_all(void) · line 735
Quoted 3 lines
include/rv/da_monitor.h +3
-0+#if DA_MON_ALLOCATION_STRATEGY ==DA_ALLOC_POOL +return __da_monitor_init_pool (DA_MON_POOL_SIZE ); +#else
Same here, use if()
Quoted 18 lines
return 0;
+#endif
}include/rv/da_monitor.h +8
-1-static inline void da_monitor_destroy (void ) +static inline void da_monitor_destroy_pool (void ) +{ +struct da_monitor_storage *ms; +struct hlist_node *tmp; +int bkt; + +/* +* Ensure all in-flight tracepoint handlers that may hold a raw pointer
(quoted diff) +1
-0+ * to a pool slot (e.g. tlob_stop_task after its RCU guardexits) have
(quoted diff) +1
-0+ * completed before we begin tearing down the pool. Mirrorsthe same
include/rv/da_monitor.h static inline void da_monitor_reset_state_all(void) · line 753
Quoted 4 lines
include/rv/da_monitor.h +4
-0+* call in da_monitor_destroy_kmalloc(). +*/ +tracepoint_synchronize_unregister (); +
This is common between the pool and kmalloc flavours, you can leave it in da_monitor_destroy() before branching.
Quoted 9 lines
(quoted diff) +2
-0+ /* + * Drain any entries that were not stopped before destroy(e.g.
(quoted diff) +1
-0+ * uprobe-started sessions whose stop probe never fired).Call
(quoted diff) +1
-0+ * da_extra_cleanup() before hash_del_rcu() so the hook maysafely
(quoted diff) +1
-0+ * call ha_cancel_timer_sync() while the monitor is stillreachable.
include/rv/da_monitor.h static inline void da_monitor_reset_state_all(void) · line 764
Quoted 6 lines
include/rv/da_monitor.h +6
-0+*/ +hash_for_each_safe (da_monitor_ht, bkt, tmp, ms, node) { +da_extra_cleanup (&ms->rv.da_mon); +hash_del_rcu (&ms->node); +call_rcu (&ms->rcu, da_pool_return_cb); + }
Cannot you make also this common? da_extra_cleanup() should be called in all flavours and the only difference I see here is the rcu callback.
Also do you really need call_rcu() ? Since we should already have waited for a grace period, you can probably call the function directly. If not, I'd still try and make both flavours consistent (sync + free OR call_rcu
+ barrier, not both).
Quoted 6 lines
include/rv/da_monitor.h +3
-0+ +/* +* rcu_barrier() drains every pending call_rcu() callback, including
(quoted diff) +1
-0+ * both da_pool_return_cb() and any monitor-specific freecallbacks
include/rv/da_monitor.h static inline void da_monitor_reset_state_all(void) · line 777
Quoted 9 lines
include/rv/da_monitor.h +9
-0+* (e.g. tlob_free_rcu) enqueued by da_extra_cleanup(). +*/ +rcu_barrier (); +kfree (da_pool.storage); + da_pool.storage =NULL ; +kfree (da_pool.free); + da_pool.free =NULL ; + da_pool.free_top =0 ; + da_pool.capacity =0 ;
Only this part is really specific to this allocation flavour, if you want it in a separate function, go ahead, but the rest should probably share as much code as possible (especially the cleanup/synchronisation mess we just worked out).
Thanks,
Gabriele
Gabriele Monaco
maintainerRe: [PATCH v3 4/9] rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check
In reply to [email protected]Quoted 30 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
The function is documented as "prepare the invariant and return the time
since reset", but on the first call (env_store==U64_MAX) it exits early without callingha_set_invariant_ns():if (
ha_monitor_env_invalid(ha_mon, env)) /*env_store==U64_MAX*/
return 0; /*ha_set_invariant_nsskipped,env_storestaysU64_MAX*/
...
ha_set_invariant_ns(ha_mon, env, expire - passed, time_ns);This leaves
env_store==U64_MAX, soha_check_invariant_ns()always passes on the first activation regardless of elapsed time:return
READ_ONCE(ha_mon->env_store[env])>= time_ns; /*U64_MAX>= any */Fix: establish the guard before converting to the invariant:
if (
ha_monitor_env_invalid(ha_mon, env))ha_reset_clk_ns(ha_mon, env, time_ns); /* guard:env_store=time_ns*/
passed =ha_get_env(ha_mon, env, time_ns);ha_set_invariant_ns(ha_mon, env, expire - passed, time_ns); /* invariant:env_store=time_ns+ expire */
Quoted 3 lines
Apply the same fix to
ha_invariant_passed_jiffy().Signed-off-by Wen Yang <[email protected]>
This is neat and probably works just fine in your case. Anyway I'm planning on a more seamless solution not involving invalid states at all. Initialisation would include a reset, which should work better on monitors doing da_handle_start(), so not running the first event. That's, by the way, pretty much what you wanted by doing reset() on the (virtual) init transition.
We first needs Nam's simplification though [1]. We can synchronise during this development cycle, you probably won't really need to bother for now.
Thanks,
Gabriele
Quoted 66 lines
---
include/rv/ha_monitor.h | 17 +++++++++++++----1 file changed, 13 insertions(+), 4 deletions(-)include/rv/ha_monitor.h +0
-0@@ -365,16 +365,22 @@ static inline bool ha_check_invariant_ns(struct ha_monitor *ha_mon, } /** ha_invariant_passed_ns - prepare the invariant and return the time since reset
+ *
+ * If the env has not been initialised yet (first entry into a state with an
+ * invariant), anchor the guard clock at the current time so that the full
+ * budget is available from this point. This preserves the documented
+ * guard→invariant ordering:ha_set_invariant_ns()is always preceded by a
+ * valid guard representation inenv_store.
*/
static inline u64 ha_invariant_passed_ns(structha_monitor*ha_mon, enum envs env,
u64 expire, u64time_ns) {include/rv/ha_monitor.h +1
-1-u64 passed =0 ; +u64 passed;
if (env < 0 || env >=ENV_MAX_STORED)
return 0;
if (ha_monitor_env_invalid(ha_mon, env))include/rv/ha_monitor.h +1
-1-return 0 ; +ha_reset_clk_ns (ha_mon, env, time_ns);passed =
ha_get_env(ha_mon, env, time_ns);ha_set_invariant_ns(ha_mon, env, expire - passed, time_ns); return passed;include/rv/ha_monitor.h +0
-0@@ -404,16 +410,19 @@ static inline bool ha_check_invariant_jiffy(struct ha_monitor *ha_mon, } /** ha_invariant_passed_jiffy - prepare the invariant and return the time since reset
+ *
+ * Same first-use semantics asha_invariant_passed_ns(): anchor the guard clock
+ * now if the env has not been initialised.
*/
static inline u64 ha_invariant_passed_jiffy(structha_monitor*ha_mon, enum envs env,
u64 expire, u64time_ns) {include/rv/ha_monitor.h +1
-1-u64 passed =0 ; +u64 passed;
if (env < 0 || env >=ENV_MAX_STORED)
return 0;
if (ha_monitor_env_invalid(ha_mon, env))include/rv/ha_monitor.h +1
-1-return 0 ; +ha_reset_clk_jiffy (ha_mon, env);passed =
ha_get_env(ha_mon, env, time_ns);ha_set_invariant_jiffy(ha_mon, env, expire - passed); return passed;
Re: [PATCH v3 5/9] rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable
In reply to [email protected]Quoted 28 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
Wrap the two definitions with #ifndef guards so that HA-based monitors
can substitute their own implementations before including this header:/* in
monitor.c, before #include <rv/ha_monitor.h> */ #define da_monitor_reset_hookmy_monitor_reset_env#define EVENT_NONE_LBL "idle"No behaviour change for monitors that do not override either macro.
Signed-off-by Wen Yang <[email protected]>---
include/rv/ha_monitor.h | 5 +++++1 file changed, 5 insertions(+)include/rv/ha_monitor.h +1
-0@@ -36,7 +36,10 @@ static bool ha_monitor_handle_constraint(structda_monitor *da_mon, da_id_type id); #define da_monitor_event_hook ha_monitor_handle_constraint #define da_monitor_init_hook ha_monitor_init_env +/* Allow monitors to override da_monitor_reset_hook before including
Quoted 1 line
this header. */
Just a nit: users can do that but shouldn't do it mindlessly, so let's add a line like:
"Make sure you still call ha_monitor_reset_env() or reset timers
otherwise."
Other than that looks good
Thanks,
Gabriele
Quoted 17 lines
+#ifndef
da_monitor_reset_hook
#defineda_monitor_reset_hookha_monitor_reset_env+#endif
#defineda_monitor_sync_hook()synchronize_rcu()
#if !defined(HA_SKIP_AUTO_CLEANUP) &&RV_MON_TYPE==RV_MON_PER_TASKinclude/rv/ha_monitor.h +2
-0@@ -75,7 +78,9 @@ _Static_assert(offsetof(struct ha_monitor, da_mon) == 0, #define ENV_INVALID_VALUE U64_MAX /* Error with no event occurs only on timeouts */ #define EVENT_NONE EVENT_MAX +#ifndef EVENT_NONE_LBL #define EVENT_NONE_LBL "none" +#endif #define ENV_BUFFER_SIZE 64
#ifdefCONFIG_RV_REACTORS
Gabriele Monaco
maintainerRe: [PATCH v3 6/9] rv/tlob: add tlob hybrid automaton monitor
In reply to [email protected]Quoted 19 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
Add tlob (task latency over budget), a per-task hybrid automaton RV monitor that tracks elapsed wall-clock time across a user-delimited code section and emits
error_env_tlobwhen the elapsed time exceeds a configurable budget.The monitor uses
RV_MON_PER_OBJwith three states (running, waiting, sleeping) driven bysched_switchandsched_wakeuptracepoints, and a single clock invariantclk_elapsed< budget enforced by an hrtimer (HRTIMER_MODE_REL_HARD). On violation,detail_env_tlobprovides a per-state time breakdown (running_ns,waiting_ns,sleeping_ns).Per-task state is managed via
DA_ALLOC_POOLto avoid allocation on the
scheduler tracepoint path. Uprobe pairs are registered through the tracefs monitor file as "p PATH:OFFSET_STARTOFFSET_STOPthreshold=NS".
Quoted 6 lines
Also adds
ha_cancel_timer_sync()toha_monitor.h, a blocking cancel variant needed by tlob'sstop_taskpath to ensure the hrtimer callback
has completed before the per-task monitor state is freed.Suggested-by Gabriele Monaco <[email protected]>
I'm not sure this applies to be fair, I'm reviewing the patch and as part of that I'm suggesting things, but the main idea is yours [1].
Unless you mess up, I'm going to appear as reviewer anyway ;)
Quoted 2 lines
Signed-off-by Wen Yang <[email protected]>---
...
Quoted 10 lines
kernel/trace/rv/Kconfig +0
-0@@ -85,6 +85,7 @@ source "kernel/trace/rv/monitors/sleep/Kconfig"source "
kernel/trace/rv/monitors/stall/Kconfig" source "kernel/trace/rv/monitors/deadline/Kconfig" source "kernel/trace/rv/monitors/nomiss/Kconfig" +source "kernel/trace/rv/monitors/tlob/Kconfig" # Add new deadline monitors here
Your line should be after the marker for deadline monitors, this is required for the Kconfig to look good with nested monitors.
Just let rvgen handle the way it looks (V2 was fine, just above the general monitor marker):
# Add new deadline monitors here
+source "kernel/trace/rv/monitors/tlob/Kconfig"
# Add new monitors here
Quoted 13 lines
# Add new monitors herekernel/trace/rv/monitors/tlob/tlob.c new file +3
-0@@ -0,0 +1,968 @@ +#include <linux/hrtimer.h> +#include <linux/kernel.h> +#include <linux/ktime.h>
kernel/trace/rv/monitors/tlob/tlob.c line 13
Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10
-0+#include <linux/module.h> +#include <linux/init.h> +#include <linux/namei.h> +#include <linux/rv.h> +#include <linux/sched.h> +#include <linux/slab.h> +#include <linux/tracefs.h> +#include <kunit/visibility.h> +#include <rv/instrumentation.h> +#include <rv/rv_uprobe.h>
You added a few includes I don't believe you need: hrtimer, ktime, sched (included in events/sched) and tracefs (included in rv).
You can keep the others as they come from the template, all monitors include them anyway.
kernel/trace/rv/monitors/tlob/tlob.c line 41
Quoted 1 line
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+#include "../../rv.h"
This is <rv.h> the includepath allows that just like rv_trace.
Quoted 5 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +5
-0+ +#define MODULE_NAME "tlob" + +#include <trace/events/sched.h> +#include <rv_trace.h>
...
Quoted 17 lines
+/* Type for da_monitor_storage.target; must be defined before the includes. */
kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+typedef struct tlob_task_state *monitor_target ; + +/* Forward-declared so da_monitor_reset_hook works before
ha_monitor.h. */kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+static inline void tlob_reset_notify (struct da_monitor *da_mon); +#define da_monitor_reset_hook tlob_reset_notify + +/* Override EVENT_NONE_LBL so the timer-fired violation shows "
budget_exceeded". */kernel/trace/rv/monitors/tlob/tlob.c new file +6
-0+#define EVENT_NONE_LBL "budget_exceeded" + +#include "tlob.h" + +/* +* DA_MON_POOL_SIZE must be defined HERE: after tlob.h (which
Quoted 7 lines
defines
+ *TLOB_MAX_MONITORED) and before #include <rv/ha_monitor.h> (which
+ * transitively includesda_monitor.hand expands__da_monitor_init_pool
+ * using this macro). Placing the define beforetlob.hor after
+ *ha_monitor.hboth cause a build error.
+ */
I don't think you need this comment here. I would add a comment in case moving it somewhere else would silently change the behaviour, but here the compiler is going to tell you what isn't defined if you really want to move it around.
Same for the one line comments above but those are less invasive.
Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+#define DA_MON_POOL_SIZE TLOB_MAX_MONITORED + +/* +* Forward-declare tlob_extra_cleanup so the #define below is valid when
+ *da_monitor.h(included viaha_monitor.h) expandsda_extra_cleanupinside
+ * da_monitor_destroy(). The full definition follows afterha_monitor.h.
+ */
I'd argue the same here. Though defining da_extra_cleanup() before is the actual catch (if you do after, the compiler won't tell you it's using a no-op). If you really want to leave a line for that, feel free to but I wouldn't be that verbose.
You can combine it to da_monitor_reset_hook() where the same rules apply.
Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7
-0+static inline void tlob_extra_cleanup (struct da_monitor *da_mon); +#define da_extra_cleanup tlob_extra_cleanup + +#include <rv/ha_monitor.h> + +/* +* Called from da_monitor_reset() on both normal stop and hrtimer expiry.
+ * On violation (stopping==0), emitsdetail_env_tlob.
+ */
kernel/trace/rv/monitors/tlob/tlob.c line 113
Quoted 6 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +6
-0+static inline void tlob_reset_notify (struct da_monitor *da_mon) +{ +struct ha_monitor *ha_mon =to_ha_monitor (da_mon); +struct tlob_task_state *ws; + +ha_monitor_reset_env (da_mon);
Could have the rest run only if trace_detail_env_tlob_enabled() and later use trace_call__detail_env_tlob() (the raw tracepoint without enabled check).
So if the tracepoint is disabled, this code almost doesn't exist.
Quoted 50 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +8
-0+ + ws =ha_get_target (ha_mon); +if (!ws) +return ; + +/* +* Emit per-state breakdown on budget violation only. +* stopping==0: timer callback owns this path (genuine overrun).
kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+* stopping==1: normal stop claimed ownership first; skip. +*/ +if (!atomic_read (&ws->stopping)) { + unsigned int curr_state = READ_ONCE(da_mon-Quoted 1 line
curr_state);kernel/trace/rv/monitors/tlob/tlob.c new file +5
-0+u64 running_ns, waiting_ns, sleeping_ns, partial_ns; +unsigned long flags; + +/* +* Snapshot accumulators; partial_ns covers
curr_statetimekernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+* not yet folded in (transition-out pending). +*/ +raw_spin_lock_irqsave (&ws->entry_lock, flags); + partial_ns = ktime_get_ns() - ktime_to_ns(ws-Quoted 1 line
last_ts);kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+ running_ns = ws->running_ns + + (curr_state == running_tlob ?
partial_ns: 0);kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+ waiting_ns = ws->waiting_ns + + (curr_state == waiting_tlob ?
partial_ns: 0);kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+ sleeping_ns = ws->sleeping_ns + + (curr_state == sleeping_tlob ?
partial_ns: 0);kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+raw_spin_unlock_irqrestore (&ws->entry_lock, flags); + + trace_detail_env_tlob(da_get_id(da_mon), ws-Quoted 1 line
threshold_ns,kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+ running_ns, waiting_ns,
sleeping_ns);kernel/trace/rv/monitors/tlob/tlob.c new file +6
-0+ } +} + +#define BUDGET_NS (ha_mon) (ha_get_target(ha_mon)->threshold_ns) + +/* HA constraint functions (called by ha_monitor_handle_constraint) */
kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+ +static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_tlob env,u64
time_ns)
kernel/trace/rv/monitors/tlob/tlob.c line 157
Quoted 5 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +5
-0+{ +if (env == clk_elapsed_tlob) +return ha_get_clk_ns (ha_mon, env, time_ns); +return ENV_INVALID_VALUE ; +}
So here you removed ha_reset_env() because that's done implicitly in the start. It seems to work, but you're going to need to define it if we apply an automatic reset at start (what I mentioned in the other review).
It's kinda idiomatic to have resets whenever you have clocks, yours is a special case because you don't really expect start (the only event with a reset) to happen again.
It's ok to redefine the functions the way you did, though you risk leaving them out of sync in case you update the model (rvgen's output is going to be quite different). Not sure if that's ever going to happen anyway.
Quoted 13 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+ +/* +* ha_verify_invariants - clk_elapsed < BUDGET_NS must hold in all states.
+ *
+ * The invariant is uniform across running/waiting/sleeping; check it
+ * unconditionally rather than enumerating each state.
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+static inline bool ha_verify_invariants(struct ha_monitor *ha_mon, + enum states curr_state, enumevents event,
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+enum states next_state, u64
Quoted 2 lines
time_ns)
+{
The compiler is probably going to produce something not that different if you keep the ifs, but this is smaller and cleaner. Again, you only need to track it manually but it looks alright now.
Quoted 27 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+ return ha_check_invariant_ns(ha_mon, clk_elapsed_tlob,
time_ns);kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+} + +/* +* Convert invariant (deadline) to guard (reset anchor) on state transitions.
+ *
+ * The conversion is identical for every departing state; skip only self-loops.
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+static inline void ha_convert_inv_guard(struct ha_monitor *ha_mon, + enum states curr_state, enumevents event,
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+enum states next_state, u64
time_ns)kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+{ +if (curr_state != next_state) + ha_inv_to_guard(ha_mon, clk_elapsed_tlob,
BUDGET_NS(ha_mon),time_ns);kernel/trace/rv/monitors/tlob/tlob.c new file +5
-0+} + +/* No per-event guard conditions for tlob; invariants suffice. */ +static inline bool ha_verify_guards(struct ha_monitor *ha_mon, + enum states curr_state, enumevents event,
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+ enum states next_state, u64
Quoted 2 lines
time_ns)
+{
Yeah you're not quite using a reset and you probably shouldn't so this is alright too.
Quoted 27 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7
-0+return true; +} + +/* +* Arm or cancel the HA budget timer on state transitions. +* +* The timer must run in every monitored state (running/waiting/sleeping),
+ * so arm it whenevernext_stateis any of the three. On a self- loop caused
+ * by a non-start event the timer is already running; skip the redundant
+ * restart. On a true state change the old timer is implicitly superseded by
+ * the newha_start_timer_ns()call.
+ *
+ * Guard on stopping:sched_switchevents can arrive afterha_cancel_timer_sync,
+ * restarting the timer and triggering an ODEBUG "activate active" splat.
+ * The_acquirepairs with thecmpxchg_releaseintlob_stop_task.
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+static inline void ha_setup_invariants(struct ha_monitor *ha_mon, + enum states curr_state, enumevents event,
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+ enum states next_state, u64
time_ns)
kernel/trace/rv/monitors/tlob/tlob.c line 212
Quoted 4 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+{ +if (next_state == curr_state && event != start_tlob) +return ; +
stopping is only ever set in the cleanup phase, right? You don't really need to cancel timers because you should have done it already, what about the slightly more readable:
if (atomic_read_acquire(&ha_get_target(ha_mon)->stopping)) return; if (next_state < state_max_tlob) ha_start_timer_ns(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon), time_ns); else ha_cancel_timer(ha_mon);
Quoted 9 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+ if (next_state < state_max_tlob) { + if (!atomic_read_acquire(&ha_get_target(ha_mon)-Quoted 1 line
stopping))
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+ ha_start_timer_ns(ha_mon, clk_elapsed_tlob,
BUDGET_NS(ha_mon),time_ns);kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+ }else { +ha_cancel_timer (ha_mon); + } +}
...
Quoted 15 lines
+/*
+ *da_extra_cleanup- per-task teardown called byda_monitor_destroy().
+ *
+ * Claims cleanup ownership via CAS; cancels the budget timer; decrements the
+ * monitored-task counter; and schedules the slab free viacall_rcu().
+ * Must run beforeda_monitor_reset()(i.e. beforehash_del_rcu()) so that
+ *ha_cancel_timer_sync()can safely access the still-registeredha_monitor.
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+static inline void tlob_extra_cleanup (struct da_monitor *da_mon) +{
kernel/trace/rv/monitors/tlob/tlob.c line 286
Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10
-0+struct ha_monitor *ha_mon =to_ha_monitor (da_mon); +struct tlob_task_state *ws =ha_get_target (ha_mon); + +if (!ws) +return ; + +if (atomic_cmpxchg_release (&ws->stopping,0 ,1 ) !=0 ) +return ; + +ha_cancel_timer_sync (ha_mon);
You shouldn't need to do this since that's standard HA functionality and is stopped already by the reset + sync RCU sequence.
kernel/trace/rv/monitors/tlob/tlob.c
Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+put_task_struct (ws->task); +call_rcu (&ws->rcu, tlob_free_rcu);
And since you can sleep and already waited for a grace period, you can probably just free now.
This is the same point I had in da_monitor_destroy_pool() vs da_monitor_destroy_kmalloc(): let's align what we do with sync/RCU.
Quoted 25 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7
-0+} + +/* +* __tlob_acc - accumulate elapsed ns into one per-state counter. +* +* Looks up the task's tlob_task_state under RCU, adds the interval +* [ws->last_ts, now] to the field at @offset within the state struct,
+ * and updates last_ts. Returns true if the task is monitored.
+ *
+ *entry_lockis a raw spinlock so this is safe from hardirq context.
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +12
-0+static inline bool __tlob_acc (struct task_struct *task,ktime_t now, + size_t offset) +{ +struct tlob_task_state *ws; +unsigned long flags; + +scoped_guard (rcu) { + ws =da_get_target_by_id (task->pid); +if (!ws) +return false; +raw_spin_lock_irqsave (&ws->entry_lock, flags); + *(u64 *)((char *)ws + offset) +=
Quoted 1 line
ktime_to_ns(ktime_sub(now, ws->last_ts));
I don't really like this, you have a few options to avoid raw pointer arithmetics:
1. a macro:
#define __tlob_acc(task, now, field) do { \
...
ws->field += ktime_stuff(now); \
...
}
static inline bool tlob_acc_running(struct task_struct *task, ktime_t now)
{
return __tlob_acc(task, now, running_ns);
}2. use an array in the struct and pass the index around:
enum tlob_acc {
running,
waiting,
sleeping,
max,
}
struct tlob_task_state {
...
u64 accs_ns[max];
...
};
bool __tlob_acc(struct task_struct *task, ktime_t now, enum tlob_acc acc)
{
...
ws->accs_ns[acc] += ktime_stuff(now);
...
}
static inline bool tlob_acc_running(struct task_struct *task, ktime_t now)
{
return __tlob_acc(task, now, running);
}Some folks don't like macros, so perhaps options 2 is cleaner. You may not even need the helper functions since calling the generic tlob_acc like this is still readable.
If you go down that path, you may also want to simplify the function using normal (unscoped) guards for both RCU and the lock, since you take them until the end of the function (won't work with the macro!).
Quoted 133 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7
-0+ ws->last_ts = now; +raw_spin_unlock_irqrestore (&ws->entry_lock, flags); + } +return true; +} + +/* Accumulate running_ns for prev; returns true if prev is monitored. */
+static inline booltlob_acc_running(struct task_struct *task, ktime_t now)kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+{ + return __tlob_acc(task, now, offsetof(struct
tlob_task_state,running_ns));kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+} + +/* Accumulate waiting_ns for next; returns true if next is monitored. */
+static inline booltlob_acc_waiting(struct task_struct *task, ktime_t now)kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+{ + return __tlob_acc(task, now, offsetof(struct
tlob_task_state,waiting_ns));kernel/trace/rv/monitors/tlob/tlob.c new file +11
-0+} + +/* +* handle_sched_switch - advance the DA on every context switch. +* +* Generates three DA events: +* prev, prev_state != 0 -> sleep_tlob (running -> sleeping) +* prev, prev_state == 0 -> preempt_tlob (running -> waiting) +* next -> switch_in_tlob (waiting -> running) +* +* A single ktime_get() at handler entry is shared by both acc calls so that
+ * prev'srunning_nsand next'swaiting_nsshare the same context- switch
+ * timestamp; neither absorbs handler overhead into its accumulator.
+ *
+ * Nowaiting->sleepingedge exists: a task can only block voluntarily
+ * (callschedule()) while it is executing on CPU, which corresponds to
+ * the running DA state. A task in the waiting state isTASK_RUNNINGin
+ * kernel terms (on the runqueue) and cannot block itself.
+ *
+ *da_handle_event()is called unconditionally: it skips tasks that have no
+ * monitor entry in the hash table.
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +10
-0+static void handle_sched_switch (void *data,bool preempt_unused, +struct task_struct *prev, +struct task_struct *next, +unsigned int prev_state) +{ +ktime_t now =ktime_get (); +bool prev_preempted = (prev_state ==0 ); + +/* +* No guard on tlob_num_monitored here: da_handle_event() internally
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* calls da_monitor_handling_event() which checks both
rv_monitoring_on()kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* and da_monitoring(da_mon). The hash lookup inside
da_get_monitor()kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* simply returns NULL for unmonitored tasks, which is equally fast as
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* an atomic_read() guard. By omitting the guard we avoid touching the
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* tlob_num_monitored cacheline on every global context- switch.
kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+*/ +if (tlob_acc_running (prev, now)) + da_handle_event(prev->pid, NULL, + prev_preempted ? preempt_tlob :
sleep_tlob);kernel/trace/rv/monitors/tlob/tlob.c new file +5
-0+if (tlob_acc_waiting (next, now)) +da_handle_event (next->pid,NULL , switch_in_tlob); +} + +/* Accumulate sleeping_ns on wakeup; returns true if task is monitored. */
+static inline booltlob_acc_sleeping(struct task_struct *task, ktime_t now)kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+{ + return __tlob_acc(task, now, offsetof(struct
tlob_task_state,sleeping_ns));kernel/trace/rv/monitors/tlob/tlob.c new file +6
-0+} + +/* +* handle_sched_wakeup - sleeping -> waiting transition. +* +* try_to_wake_up() skips TASK_RUNNING tasks, so this never fires for a
+ * task already in running or waiting state.
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +5
-0+static void handle_sched_wakeup (void *data,struct task_struct *p) +{ +ktime_t now =ktime_get (); + +/* Same reasoning as handle_sched_switch: rely on hash- lookup fast path. */
kernel/trace/rv/monitors/tlob/tlob.c new file +6
-0+if (tlob_acc_sleeping (p, now)) +da_handle_event (p->pid,NULL , wakeup_tlob); +} + +/* +* handle_sched_process_exit - clean up if a task exits without
TRACE_STOP.
+ *
+ * Called indo_exit()context; the task still has a valid pid here.
+ *tlob_stop_task()returns-ESRCHif the task is not monitored, which is fine.
+ */
+static void handle_sched_process_exit(void *data, structtask_struct*p,kernel/trace/rv/monitors/tlob/tlob.c new file +9
-0+ bool group_dead) +{ +tlob_stop_task (p); +} + + + +/** +* tlob_start_task - begin monitoring @task with budget @threshold_ns ns.
+ * @task: Task to monitor; may be current or another task.
+ * @threshold_ns: Latency budget in nanoseconds (wall-clock; running
+ waiting + sleeping).
+ * Must be in [1000,TLOB_MAX_THRESHOLD_NS].
+ *
+ * Returns 0,-ENODEV,-ERANGE,-EALREADY,-ENOMEM, or-ENOSPC.
+ */
kernel/trace/rv/monitors/tlob/tlob.c
Quoted 8 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +8
-0+int tlob_start_task(struct task_struct *task, u64 threshold_ns) +{ + struct tlob_task_state *ws; + + if (!da_monitor_enabled()) + return -ENODEV; + + if (threshold_ns < 1000 || threshold_ns >
Why not some TLOB_MIN_THRESHOLD_NS (that can of course be 1000)?
Quoted 66 lines
TLOB_MAX_THRESHOLD_NS)kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+return -ERANGE ; + +/* Serialise duplicate-check + pool-slot claim for the same pid. */
kernel/trace/rv/monitors/tlob/tlob.c new file +17
-0+guard (mutex)(&tlob_start_mutex); + +if (da_get_target_by_id (task->pid)) +return -EALREADY ; + + ws =kmem_cache_zalloc (tlob_state_cache,GFP_KERNEL ); +if (!ws) +return -ENOMEM ; + + ws->task = task; +get_task_struct (task); + ws->threshold_ns = threshold_ns; + ws->last_ts =ktime_get (); +raw_spin_lock_init (&ws->entry_lock); + +/* +* da_handle_start_run_event() claims a pool slot via
da_prepare_storage(),kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* initialises the monitor, and delivers start_tlob in one step: the
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* generated ha_setup_invariants() resets clk_elapsed and arms the timer.
kernel/trace/rv/monitors/tlob/tlob.c new file +18
-0+* Returns 0 if the pool is exhausted (-ENOSPC). +*/ +if (!da_handle_start_run_event (task->pid, ws, start_tlob)) { +put_task_struct (task); +kmem_cache_free (tlob_state_cache, ws); +return -ENOSPC ; + } + +return 0 ; +} +EXPORT_SYMBOL_GPL (tlob_start_task); + +/** +* tlob_stop_task - stop monitoring @task. +* @task: Task to stop. +* +* CAS on ws->stopping (0->1) under RCU claims cleanup ownership; +* the winner cancels the timer synchronously and frees all resources.
+ *
+ * Returns 0,-EOVERFLOW(budget exceeded),-ESRCH(not monitored),
+ * or-EAGAIN(concurrent caller claimed cleanup).
+ */kernel/trace/rv/monitors/tlob/tlob.c new file +15
-0+int tlob_stop_task (struct task_struct *task) +{ +struct da_monitor *da_mon; +struct ha_monitor *ha_mon; +struct tlob_task_state *ws; +bool budget_exceeded; + +scoped_guard (rcu) { + ws =da_get_target_by_id (task->pid); +if (!ws) +return -ESRCH ; + + da_mon =da_get_monitor (task->pid,NULL ); +if (unlikely (!da_mon)) { +/* ws in hash but da_mon gone; internal inconsistency. */
kernel/trace/rv/monitors/tlob/tlob.c line 483
Quoted 1 line
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+WARN_ON_ONCE (1 );
if (unlikely(WARN_ON_ONCE(!da_mon)))
Quoted 51 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7
-0+return -ESRCH ; + } + + ha_mon =to_ha_monitor (da_mon); + +/* +* CAS (0->1) claims cleanup ownership under RCU (ws guaranteed valid).
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* _release pairs with atomic_read_acquire in
ha_setup_invariants.kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+*/ + if (atomic_cmpxchg_release(&ws->stopping, 0, 1) !=0)
kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+return -EAGAIN ; + } + +/* Wait for in-flight timer callback before reading
da_monitoring. */kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+ha_cancel_timer_sync (ha_mon); + +/* Timer fired first -> budget exceeded; otherwise reset normally. */
kernel/trace/rv/monitors/tlob/tlob.c new file +15
-0+scoped_guard (rcu) { + budget_exceeded = !da_monitoring (da_mon); +if (!budget_exceeded) +da_monitor_reset (da_mon); + } +da_destroy_storage (task->pid); + +put_task_struct (ws->task); +call_rcu (&ws->rcu, tlob_free_rcu); +return budget_exceeded ? -EOVERFLOW :0 ; +} +EXPORT_SYMBOL_GPL (tlob_stop_task); + + +static int tlob_uprobe_entry_handler(struct rv_uprobe *p, struct
pt_regs*regs,kernel/trace/rv/monitors/tlob/tlob.c new file +9
-0+ __u64 *data) +{ +struct tlob_uprobe_binding *b = p->priv; + +tlob_start_task (current, b->threshold_ns); +return 0 ; +} + +static int tlob_uprobe_stop_handler(struct rv_uprobe *p, struct
pt_regs*regs,kernel/trace/rv/monitors/tlob/tlob.c new file +3
-0+ __u64 *data) +{ +tlob_stop_task (current);
kernel/trace/rv/monitors/tlob/tlob.c line 537
Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10
-0+return 0 ; +} + +/* +* Register start + stop entry uprobes for a binding. +* Called with tlob_uprobe_mutex held. +*/ +static int tlob_add_uprobe (u64 threshold_ns,const char *binpath, + loff_t offset_start, loff_t offset_stop) +{
You can use cleanup helpers here to save a couple of gotos (making the code less error prone and more readable):
kernel/trace/rv/monitors/tlob/tlob.c line 538
Quoted 1 line
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+struct tlob_uprobe_binding *b, *tmp_b;
struct tlob_uprobe_binding __free(kfree) *b = NULL, *tmp_b;
kernel/trace/rv/monitors/tlob/tlob.c line 540
Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+char pathbuf[TLOB_MAX_PATH ]; +struct path path;
struct path path __free(path_put) = {};
Quoted 54 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +27
-0+char *canon; +int ret; + +if (binpath[0 ] !='/' ) +return -EINVAL ; + + b =kzalloc_obj (*b,GFP_KERNEL ); +if (!b) +return -ENOMEM ; + + b->threshold_ns = threshold_ns; + b->offset_start = offset_start; + b->offset_stop = offset_stop; + + ret =kern_path (binpath,LOOKUP_FOLLOW , &path); +if (ret) + goto err_free; + +if (!d_is_reg (path.dentry)) { + ret = -EINVAL ; + goto err_path; + } + +/* Reject duplicate start offset for the same binary. */ +list_for_each_entry (tmp_b, &tlob_uprobe_list, list) { +if (tmp_b->offset_start == offset_start && + tmp_b->start_probe->path.dentry == path.dentry){
kernel/trace/rv/monitors/tlob/tlob.c new file +13
-0+ ret = -EEXIST ; + goto err_path; + } + } + + canon =d_path (&path, pathbuf,sizeof (pathbuf)); +if (IS_ERR (canon)) { + ret =PTR_ERR (canon); + goto err_path; + } +strscpy (b->binpath, canon,sizeof (b->binpath)); + +/* Both probes share b (priv) and path; attach_path refs path itself. */
kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+ b->start_probe = rv_uprobe_attach_path(&path, offset_start, +
tlob_uprobe_entry_handler, NULL, b);kernel/trace/rv/monitors/tlob/tlob.c new file +8
-0+if (IS_ERR (b->start_probe)) { + ret =PTR_ERR (b->start_probe); + b->start_probe =NULL ; + goto err_path; + } + + b->stop_probe = rv_uprobe_attach_path(&path, offset_stop, +
tlob_uprobe_stop_handler, NULL, b);
kernel/trace/rv/monitors/tlob/tlob.c line 598
Quoted 8 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +8
-0+if (IS_ERR (b->stop_probe)) { + ret =PTR_ERR (b->stop_probe); + b->stop_probe =NULL ; + goto err_start; + } + +path_put (&path); +list_add_tail (&b->list, &tlob_uprobe_list);
And finally tell the compiler you want to keep b in the happy path.
list_add_tail(&no_free_ptr(b)->list, &tlob_uprobe_list);
Not tested, but you got the gist, check include/linux/cleanup.h and grep around for users if it doesn't work as expected.
kernel/trace/rv/monitors/tlob/tlob.c line 602
Quoted 4 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+return 0 ; + +err_start: +rv_uprobe_detach (b->start_probe);
This will need to stay, unless you want to DEFINE_FREE() for this, may be fun but not required.
Quoted 6 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +6
-0+err_path: +path_put (&path); +err_free: +kfree (b); +return ret; +}
...
Quoted 3 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+ +/* Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention). */
This isn't quite a standard function comment (and you don't really need to make it so, it wouldn't add much value), but I'd say at least make it a multi-line comment which is more common in this location:
/*
* Parse ...
*/
...
Quoted 17 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +5
-0+static void __tlob_destroy_monitor (void ) +{ + rv_this.enabled =0 ; +/* +* Remove uprobes first; rv_uprobe_sync() inside ensures all in-flight
kernel/trace/rv/monitors/tlob/tlob.c new file +6
-0+* handlers have finished before we proceed. +*/ +tlob_remove_all_uprobes (); + +/* +* da_monitor_destroy() iterates any remaining entries via
da_extra_cleanupkernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* (tlob_extra_cleanup), cancels their timers, and frees their state.
kernel/trace/rv/monitors/tlob/tlob.c new file +1
-0+* rcu_barrier() inside drains both da_pool_return_cb and
tlob_free_rcu
kernel/trace/rv/monitors/tlob/tlob.c line 892
Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2
-0+* callbacks before the pool arrays are freed. +*/
I'm not sure we need this many comments, the behaviour may change and we don't need to describe what the function does before calling it.
If anything say /why/ you call a function before another, but I'd say it's trivial here so you can just drop both comments.
Quoted 4 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4
-0+ha_monitor_destroy (); +kmem_cache_destroy (tlob_state_cache); + tlob_state_cache =NULL ; +}
...
kernel/trace/rv/monitors/tlob/tlob.c
Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10
-0+static int __init register_tlob(void) +{ + int ret; + + ret = rv_register_monitor(&rv_this, NULL); + if (ret) + return ret; + + if (rv_this.root_d) { + if (!tracefs_create_file("monitor", 0644,
We have rv_create_file() , it's exactly the same but let's be consistent so if we ever decide to overload it, this won't stay behind.
Quoted 17 lines
rv_this.root_d, NULL,
kernel/trace/rv/monitors/tlob/tlob.c new file +8
-0+ &tlob_monitor_fops)) { +rv_unregister_monitor (&rv_this); +return -ENOMEM ; + } + } + +return 0 ; +}kernel/trace/rv/monitors/tlob/tlob.h new file +0
-0@@ -0,0 +1,148 @@
...
kernel/trace/rv/monitors/tlob/tlob.h line 44
Quoted 7 lines
kernel/trace/rv/monitors/tlob/tlob.h new file +7
-0+ +enum states_tlob { + running_tlob, + waiting_tlob, + sleeping_tlob, + state_max_tlob, +};
This is personal preference, so feel free to ignore it. The header file is (mostly) automatically generated and rvgen currently gives you a different order for enums/arrays. Maybe just reorder all those according to what rvgen does (just run it without the -a option and diff what comes out).
The main reason is again to make it easy to update the file in case the model/framework changes.
Thanks,
Gabriele
Gabriele Monaco
maintainerRe: [PATCH v3 2/9] rv: add generic uprobe infrastructure for RV monitors
In reply to [email protected]Quoted 8 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
Introduce
rv_uprobe, a thin wrapper arounduprobe_consumerprovidingrv_uprobe_attach_path(),rv_uprobe_attach(), andrv_uprobe_detach()for RV monitors. An opaque priv pointer is forwarded unchanged to entry/return handlers so monitors can carry per-binding state (e.g. a latency threshold) to the hot path without any global lookup.
Quoted 6 lines
rv_uprobe_detach()is fully synchronous (nosync + sync +path_put+ kfree), closing the use-after-free window present in open-coded patterns wherekfree()precedesuprobe_unregister_sync().Suggested-by Gabriele Monaco <[email protected]>Signed-off-by Wen Yang <[email protected]>
I find your implementation solid, but I wonder if it isn't a bit overdoing it. It's good to have helper functions abstracting away the common uprobes code, but I find the number of structures used to indirect/hide the process a bit confusing.
Users of this headers still know they're using uprobes, so we don't really need to hide it that much, plus the double function pointers are an additional performance hit we could easily skip.
As I get it, we want to pass a private pointer (essentially to get the threshold in tlob) and the uprobe infrastructure doesn't allow that transparently. So let's keep on using struct embedding but a bit more cut to the bone:
struct rv_uprobe {
struct uprobe_consumer uc;
struct uprobe *uprobe;
void *priv;
};Then handlers would use container_of() appropriately to derive the data they need.
static int tlob_uprobe_entry_handler(struct uprobe_consumer *self, struct pt_regs *regs, __u64 *data)
{
struct rv_uprobe *p = container_of(self, struct rv_uprobe, uc);
struct tlob_uprobe_binding *b = p->priv;
...
}You could even define the struct directly inside your private data and save one step (and no void *):
struct rv_uprobe {
struct uprobe_consumer uc;
struct uprobe *uprobe;
};
#define DECLARE_RV_UPROBE(name) struct rv_uprobe name
// And then
struct tlob_uprobe_binding {
u64 threshold_ns;
...
DECLARE_RV_UPROBE(start);
DECLARE_RV_UPROBE(stop);
}
static int tlob_uprobe_entry_handler(struct uprobe_consumer *self, struct pt_regs *regs, __u64 *data)
{
struct tlob_uprobe_binding *b = container_of(self, struct tlob_uprobe_binding, start.uc);
...
}Now I intentionally skipped offset, as I don't really see why carry it around, and path, as we could live without it by relying on inodes (uprobes reference-track them but you'd need to save them somewhere).
Mind, I only build tested this idea (appended after). This was supported by an LLM which did a few more changes like standardising names and return values of registration functions (a bit more consistent with other stuff, you don't have to commit with that though).
By the way, do we really need the duplicate check? Would it break if a user defines it twice? If not, we could simplify the tlob_add_uprobe() even further and doing all path related checks in the library, since those aren't quite tlob specific.
Thoughts?
Thanks,
Gabriele
**Suggested simplification:**
---
include/rv/rv_uprobe.h +31 -72
@@ -7,83 +7,56 @@#ifndef _RV_UPROBE_H#define _RV_UPROBE_H -#include <linux/path.h> #include <linux/types.h> +#include <linux/uprobes.h> struct pt_regs ; +struct inode ;/** * struct rv_uprobe - a single uprobe registered on behalf of an RV monitor * -* @offset: byte offset within the ELF binary where the probe is installed -* @priv: monitor-private pointer; set at attach time, never touched by -* this layer; passed unchanged to entry_fn / ret_fn -* @path: resolved path of the probed binary (read-only after attach); -* callers may use path.dentry for identity comparisons -* -* The implementation fields (uprobe_consumer, uprobe handle, callbacks) are -* private to rv_uprobe.c and are not exposed here; monitors must not access -* them directly. +* @uc: underlying uprobe consumer (publicly visible) +* @uprobe: active uprobe structure handle +* @inode: inode of the target binary (read-only after registration) */ struct rv_uprobe { -/* public: read-only after rv_uprobe_attach*() */ -loff_t offset; -void *priv; -struct path path; +struct uprobe_consumer uc; +struct uprobe *uprobe; +struct inode *inode; }; -/** -* rv_uprobe_attach_path - register an uprobe given an already-resolved path -* @path: path of the target binary; rv_uprobe takes its own reference -* @offset: byte offset within the binary -* @entry_fn: called on probe hit (entry); may be NULL -* @ret_fn: called on function return (uretprobe); may be NULL -* @priv: opaque pointer forwarded to callbacks unchanged -* -* Use this variant when the caller has already resolved the path (e.g. to -* register multiple probes on the same binary with a single kern_path call). -* The inode is derived internally via d_real_inode(), so inode and path are -* always consistent. -* -* Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure. -*/ -struct rv_uprobe *rv_uprobe_attach_path (struct path *path,loff_t offset, -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), -void *priv); +/* Seamless inline declaration of a named uprobe inside user structs */ +#define DECLARE_RV_UPROBE (name) \ + struct rv_uprobe name/** -* rv_uprobe_attach - resolve binpath and register an uprobe -* @binpath: absolute path to the target binary -* @offset: byte offset within the binary -* @entry_fn: called on probe hit (entry); may be NULL -* @ret_fn: called on function return (uretprobe); may be NULL -* @priv: opaque pointer forwarded to callbacks unchanged +* rv_uprobe_register - resolve binpath and register an uprobe +* @binpath: absolute path to the target binary +* @offset: byte offset within the binary +* @p: pointer to the allocated/embedded rv_uprobe structure +* @handler: called on probe hit (entry); may be NULL +* @ret_handler: called on function return (uretprobe); may be NULL * -* Resolves binpath via kern_path(), then delegates to rv_uprobe_attach_path(). +* Resolves binpath via kern_path(), registers the uprobe directly using the +* embedded `uprobe_consumer`, and immediately releases the path reference. * -* Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure. +* Returns 0 on success, or a negative error code on failure. */ -struct rv_uprobe *rv_uprobe_attach (const char *binpath,loff_t offset, -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), -void *priv); +int rv_uprobe_register (const char *binpath,loff_t offset,struct rv_uprobe *p, +int (*handler)(struct uprobe_consumer *self,struct pt_regs *regs,__u64 *data), +int (*ret_handler)(struct uprobe_consumer *self,unsigned long func, +struct pt_regs *regs,__u64 *data));/** -* rv_uprobe_detach - synchronously unregister an uprobe and free it -* @p: probe to detach; may be NULL (no-op) -* -* Calls uprobe_unregister_nosync(), then uprobe_unregister_sync() to wait -* for any in-progress handler to finish, then releases the path reference -* and frees the rv_uprobe struct. The caller's priv data is NOT freed. +* rv_uprobe_unregister - synchronously unregister a uprobe +* @p: probe to unregister; may be NULL (no-op) * -* When removing a single probe, prefer this over the three-phase API. -* Safe to call from process context only (uprobe_unregister_sync() may -* schedule). +* Dequeues the uprobe and waits synchronously for all in-flight handlers +* to complete. */ -void rv_uprobe_detach (struct rv_uprobe *p); +void rv_uprobe_unregister (struct rv_uprobe *p);/** * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
@@ -91,9 +64,7 @@ void rv_uprobe_detach(struct rv_uprobe *p);* * Removes the uprobe from the uprobe subsystem but does NOT wait for * in-flight handlers to complete. The caller must call rv_uprobe_sync() -* before calling rv_uprobe_free() on the same probe. -* -* Use this to batch multiple deregistrations before a single rv_uprobe_sync(). +* before freeing any container holding this probe. */ void rv_uprobe_unregister_nosync (struct rv_uprobe *p);
@@ -101,19 +72,8 @@ void rv_uprobe_unregister_nosync(struct rv_uprobe *p);* rv_uprobe_sync - wait for all in-flight uprobe handlers to complete * * Global barrier: waits for every in-flight uprobe handler across the system -* to finish. Call once after a batch of rv_uprobe_unregister_nosync() calls -* and before any rv_uprobe_free() call. +* to finish. */ void rv_uprobe_sync (void ); -/** -* rv_uprobe_free - release resources of a previously deregistered probe -* @p: probe to free; may be NULL (no-op) -* -* Releases the path reference and frees the rv_uprobe struct. Must only -* be called after rv_uprobe_sync() has returned. The caller's priv data -* is NOT freed. -*/ -void rv_uprobe_free (struct rv_uprobe *p); -#endif /* _RV_UPROBE_H */
kernel/trace/rv/monitors/tlob/tlob.c +30 -30
136 lines not on this page · raw message
kernel/trace/rv/rv_uprobe.c +36 -127
204 lines not on this page · raw message
Gabriele Monaco
maintainerRe: [PATCH v3 8/9] selftests/verification: fix verificationtest-ktap for out-of-tree execution
In reply to [email protected]Quoted 4 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
verificationtest-ktap used CWD-relative paths which broke when invoked outside the verification directory (e.g. via vng).
I still don't get this, I even run from my home directory and it works without this commit:
make -C linux/tools/testing/selftests/verification/ run_tests
It builds and runs your new tests just fine, how exactly do you run them? vng shouldn't affect this since it runs from the same directory as overlay mount, what errors are you getting?
The only issue I see is with the read only filesystems since ftracetest attempts to write logs, but that doesn't seem solved by your commit either. Adding the appropriate rwdir lets me run them just fine:
vng -v --rwdir tools/testing/selftests/verification/logs -- make -C tools/testing/selftests/verification run_tests
I'm suspecting you're trying to call verificationtest-ktap manually from somewhere else to call it only on tlob/, but that looks like asking for troubles. While you're at it I don't see why you don't just call ftracetest.
Gabriele
Quoted 24 lines
Resolve paths via realpath "$(dirname "$0")" so the script works from any working directory. Accept an optional subdirectory argument interpreted relative to the script's directory.
Suggested-by Gabriele Monaco <[email protected]>Signed-off-by Wen Yang <[email protected]>---
tools/testing/selftests/verification/verificationtest-ktap | 4 +++-1 file changed, 3 insertions(+), 1 deletion(-)(quoted diff) +0
-0ktap b/tools/testing/selftests/verification/verificationtest-ktap index
18f7fe324e2f..055747cef38a100755tools/testing/selftests/verification/verificationtest-ktap +0
-0@@ -5,4 +5,6 @@ # # Copyright (C) Arm Ltd., 2023
-../ftrace/ftracetest -K -v --rv ../verificationtools/testing/selftests/verification/verificationtest-ktap +3
-0+dir=$(realpath "$(dirname "$0")") +testdir=$(cd "$dir" && realpath "${1:-.}") +"$dir/../ftrace/ftracetest" -K -v --rv "$testdir"
Gabriele Monaco
maintainerRe: [PATCH v3 9/9] selftests/verification: add tlob selftests
In reply to [email protected]Quoted 16 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
Add selftest coverage for the tlob uprobe monitoring interface under
tools/testing/selftests/verification/.test.d/tlob/ contains both the helper sources (
tlob_target,tlob_sym) and the seven test scripts so the test suite is self-contained.tlob_targetprovides busy-spin, sleep, and preempt workloads;tlob_sym
resolves ELF symbol offsets for uprobe registration.Seven test scripts exercise uprobe binding management, budget violation
detection, and per-state time accounting (running_ns,waiting_ns,sleeping_ns).
Quoted 6 lines
Signed-off-by Wen Yang <[email protected]>---
.../testing/selftests/verification/.gitignore | 2 +tools/testing/selftests/verification/Makefile| 19 +- .../verification/test.d/tlob/Makefile | 20 ++ .../verification/test.d/tlob/test.d/functions | 1 +
Tests seems to work fine, thanks. I think I'm getting what you're trying to do and probably defining a dummy functions for tlob isn't the right thing to do.
ftracetest --rv doesn't quite work with different folders as you'd pick the ftrace functions. We shouldn't be adding a dummy functions script every time but rather fix ftracetest directly.
Try the attached patch, it seems to work on my side. Then you'd be able to use ftracetest --rv as you please (folder is mandatory, I don't think we need a version inside selftests/verification, verificationtest-ktap is only meant for the makefile just like ftracetest-ktap).
Thanks,
Gabriele
---
From 32242f83af8214a51c47167a1904d3663aa43870 Mon Sep 17 00:00:00 2001
From: Gabriele Monaco <[email protected]>
Date: Tue, 16 Jun 2026 16:26:53 +0200
Subject: [PATCH] selftests/tracing: support flexible helper functions
Target directory or file arguments passed to ftracetest had to contain their own nested test.d/functions files to properly override the top-level directory (TOP_DIR). This works for standard ftrace tests that would fall back to ftrace's functions, but doesn't for rv tests.
Introduce find_functions() to recursively check parent and grandparent directories of the target test directory/file to dynamically locate the appropriate root functions config.
This allows to define additional directories for rv selftests and run them with the appropriate rv functions:
ftracetest --rv tools/testing/selftests/verification/test.d/<topic>
--- tools/testing/selftests/ftrace/ftracetest | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-)
tools/testing/selftests/ftrace/ftracetest +19 -1
@@ -80,6 +80,23 @@ find_testcases() { #directory echo `find $1 -name \*.tc | sort` } +find_functions() { # [directory] [test_cases] + local BASE_DIR="$1" + if [ -f "$1" ]; then + BASE_DIR="`absdir $1`" + elif [ ! -d "$1" ]; then + return + fi + + if [ -f "$BASE_DIR"/test.d/functions ]; then + echo "$BASE_DIR" + elif [ -f "$BASE_DIR"/../test.d/functions ]; then + abspath "$BASE_DIR"/.. + elif [ -f "$BASE_DIR"/../../test.d/functions ]; then + abspath "$BASE_DIR"/../.. + fi +} + parse_opts() { # opts local OPT_TEST_CASES= local OPT_TEST_DIR=
@@ -159,7 +176,8 @@ parse_opts() { # opts if [ -n "$OPT_TEST_CASES" ]; then TEST_CASES=$OPT_TEST_CASES fi - if [ -n "$OPT_TEST_DIR" -a -f "$OPT_TEST_DIR"/test.d/functions ]; then + OPT_TEST_DIR="`find_functions $OPT_TEST_DIR $OPT_TEST_CASES`" + if [ -n "$OPT_TEST_DIR" ]; then TOP_DIR=$OPT_TEST_DIR TEST_DIR=$TOP_DIR/test.d fi
Signature · 1 line
2.54.0
Gabriele Monaco
maintainerRe: [PATCH v3 7/9] rv/tlob: add KUnit tests for the tlob monitor
In reply to [email protected]Quoted 15 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
+/*
+ * Valid add lines return-ENOENT(kern_path()finds no such file in the test
+ * environment) rather than 0; a non-(-EINVAL) return confirms the format was
+ * accepted by the parser.
+ */kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +8
-0+static void tlob_parse_valid_accepted (struct kunit *test) +{ +char buf[128 ]; +int i; + +for (i =0 ; i <ARRAY_SIZE (tlob_parse_valid); i++) { +strscpy (buf, tlob_parse_valid[i],sizeof (buf)); + KUNIT_EXPECT_NE(test,
Quoted 1 line
tlob_create_or_delete_uprobe(buf),-EINVAL);
Can you perhaps add those uprobes for real from this test case? Unit tests should not touch the system, you usually do that by:
1. stubbing the tested function when it starts doing bad things
2. test with dummy data not attaching anything (not applicable here)
3. test a different function not affecting the system
I think the cleanest here is 3. so you could just kunit test tlob_parse_uprobe_line() and tlob_parse_remove_line().
Alternatively just stub the entirety of tlob_add_uprobe() tlob_remove_uprobe_by_key() and maybe even check that they're called when expected and not called on failure (right now you aren't testing valid removals, probably because that's going to break).
I believe a good unit test should be validating the parsing logic only /or/ the add/remove logic (but that's hard, you can skip it or even check in selftests).
Right now your tests are trying to do both, so you don't know if failures came from the uprobes subsystem or allocation (you shouln't even get there from the unit test).
Then you can just check for success and not for ! EINVAL , which is confusing.
Thanks,
Gabriele
Quoted 44 lines
kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +11
-0+ } +} + +static void tlob_parse_invalid_rejected (struct kunit *test) +{ +char buf[128 ]; +int i; + +for (i =0 ; i <ARRAY_SIZE (tlob_parse_invalid); i++) { +strscpy (buf, tlob_parse_invalid[i],sizeof (buf)); + KUNIT_EXPECT_EQ(test,
tlob_create_or_delete_uprobe(buf),-EINVAL);kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +10
-0+ } +} + +static void tlob_parse_out_of_range_rejected (struct kunit *test) +{ +char buf[128 ]; +int i; + +for (i =0 ; i <ARRAY_SIZE (tlob_parse_out_of_range); i++) { + strscpy(buf, tlob_parse_out_of_range[i],sizeof(buf));
(quoted diff) +1
-0+ KUNIT_EXPECT_EQ(test,
tlob_create_or_delete_uprobe(buf),-ERANGE);kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +19
-0+ } +} + +static struct kunit_case tlob_parse_cases[] = { +KUNIT_CASE (tlob_parse_valid_accepted), +KUNIT_CASE (tlob_parse_invalid_rejected), +KUNIT_CASE (tlob_parse_out_of_range_rejected), + {} +}; + +static struct kunit_suite tlob_parse_suite = { + .name = "tlob_parse", + .test_cases = tlob_parse_cases, +}; + +kunit_test_suite (tlob_parse_suite); + +MODULE_DESCRIPTION ("KUnit tests for the tlob RV monitor" ); +MODULE_LICENSE ("GPL" );
Gabriele Monaco
maintainerRe: [PATCH v3 9/9] selftests/verification: add tlob selftests
In reply to [email protected]Quoted 11 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
Add selftest coverage for the tlob uprobe monitoring interface under
tools/testing/selftests/verification/.test.d/tlob/ contains both the helper sources (
tlob_target,tlob_sym) and the seven test scripts so the test suite is self-contained.tlob_targetprovides busy-spin, sleep, and preempt workloads;tlob_sym
resolves ELF symbol offsets for uprobe registration.
Quoted 6 lines
Seven test scripts exercise uprobe binding management, budget violation
detection, and per-state time accounting (running_ns,waiting_ns,sleeping_ns).Signed-off-by Wen Yang <[email protected]>
Tests look fine and coverage is good, thanks!
Minor comments follow.
Quoted 10 lines
---
.../testing/selftests/verification/.gitignore | 2 +tools/testing/selftests/verification/Makefile| 19 +- .../verification/test.d/tlob/Makefile | 20 ++ .../verification/test.d/tlob/test.d/functions | 1 + .../verification/test.d/tlob/tlob_sym.c | 189
++++++++++++++++++
.../verification/test.d/tlob/tlob_target.c | 138 +++++++++++++ .../verification/test.d/tlob/uprobe_bind.tc | 37 ++++
Quoted 3 lines
.../test.d/tlob/uprobe_detail_running.tc | 51 +++++ .../test.d/tlob/uprobe_detail_sleeping.tc | 50 +++++ .../test.d/tlob/uprobe_detail_waiting.tc | 66 ++++++
Not sure if this would work, but just to lower the maintenance burden, couldn't we put these 3 in the same test case? You could define a bash function and pass "running", "sleeping" or "waiting" and whether to launch the hog to that.
Only waiting uses a taskset and a slightly different ordering, but wouldn't they all work fine like that?
...
Quoted 7 lines
a/tools/testing/selftests/verification/test.d/tlob/tlob_sym.cb/tools/testing/selftests/verification/test.d/tlob/tlob_sym.cnew file mode 100644
index 000000000000..1b7ba1c6d95b
tools/testing/selftests/verification/test.d/tlob/tlob_sym.c line 10
Quoted 10 lines
tools/testing/selftests/verification/test.d/tlob/tlob_sym.c new file +10
-0@@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0 +/* +* tlob_sym.c - ELF symbol-to-file-offset utility for tlob selftests +* +* Usage: tlob_sym sym_offset <binary> <symbol> +* +* Prints the ELF file offset of <symbol> in <binary> to stdout. +* +* Exit: 0 = found, 1 = error / not found. +*/
I wonder if instead of maintaining a pure C solution we couldn't live with something like:
sym_offset() { # target symbol
readelf -W -S -s $1 | awk -v symbol="$2" '
{ gsub(/\[ /, "[") } # normalise section markers
$1 ~ /^\[[0-9]+\]$/ { sections[$1]="0x"$4; offsets[$1]="0x"$5 }
$1 ~ /^[0-9]+:$/ && $NF == symbol { addr="0x"$2; sec="["$7"]" }
END { printf "printf \"0x%%x\\n\" $((%s - %s + %s))\n", addr, sections[sec], offsets[sec] }
' | sh
}...
Quoted 15 lines
diff --git
a/tools/testing/selftests/verification/test.d/tlob/tlob_target.cb/tools/testing/selftests/verification/test.d/tlob/tlob_target.cnew file mode 100644
index 000000000000..0fdbc575d71dtools/testing/selftests/verification/test.d/tlob/tlob_target.c new file +5
-0@@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0 +/* +* tlob_target.c - uprobe target binary for tlob selftests. +* +* Provides three start/stop probe pairs, each designed to exercise a
+ * different dominant component of thedetail_env_tlobns breakdown:
Quoted 8 lines
+ *
+ * tlob_busy_work / tlob_busy_work_done - busy-spin:running_nsdominates
+ * tlob_sleep_work / tlob_sleep_work_done - nanosleep:sleeping_nsdominates
+ *tlob_preempt_work/tlob_preempt_work_done- busy-spin:waiting_nsdominates
+ * (needs an RT
In short tlob_preempt_work is the same as tlob_busy_work, isn't it? Do we need them both? Cannot you just have a hog in the test and keep using the same function?
...
tools/testing/selftests/verification/test.d/tlob/tlob_target.c line 133
Quoted 8 lines
tools/testing/selftests/verification/test.d/tlob/tlob_target.c new file +8
-0+ + do { + if (strcmp(mode, "sleep") == 0) + tlob_sleep_work(200); + else if (strcmp(mode, "preempt") == 0) + tlob_preempt_work(200); + else + tlob_busy_work(200 * 1000000UL);
The only difference I see is that you multiply by 1000000UL here for busy and in the function for preempt.
Cannot we make them all consistent (call with 200 and do the math inside)?
Quoted 12 lines
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file mode 100644
index 000000000000..1ac3db6ca7bbtools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3
-0@@ -0,0 +1,37 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor uprobe binding (visible in monitorfile, removable, duplicate rejected)
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc line 10
Quoted 7 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7
-0+# requires: tlob:monitor + +RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET="${RV_BINDIR}/tlob_target" +TLOB_SYM="${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported
If those aren't ready, the build system didn't work, I don't think we need to check here, it's just a clear error.
Quoted 8 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3
-0+TLOB_MONITOR=monitors/tlob/monitor + +busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work2>/dev/null)
+stop_offset=$("$TLOB_SYM"sym_offset"$UPROBE_TARGET"tlob_busy_work_done2>/dev/null)+[ -n "$busy_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupported
Kind of the same here, the rest of the test should probably fail (EINVAL in the monitor or whatever). The script will print everything with set -x and it should be clear what was missing.
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
Quoted 2 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +2
-0+command -v chrt > /dev/null || exit_unsupported +command -v taskset > /dev/null || exit_unsupported
Not sure how common it is not to have those, but this is exactly what the :program under requires: is for (see rv_wwnr_printk with stress-ng).
Thanks,
Gabriele
Gabriele Monaco
maintainerRe: [PATCH v3 9/9] selftests/verification: add tlob selftests
In reply to [email protected]Quoted 11 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
+grep -qE "^p ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=[0- 9]+$" "$TLOB_MONITOR"
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3
-0+grep -q "threshold=5000000000" "$TLOB_MONITOR" + +! echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset}threshold=9999000" > "$TLOB_MONITOR" 2>/dev/null
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3
-0+ +echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" +! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR"
...
Quoted 1 line
+! grep -q "
error_env_tlob"/sys/kernel/tracing/trace
There is a widespread misconception under selftests that ! cmd with set -e fails if the command succeeds, so you can use it to expect a program failure.
That isn't the case though [1], errexit (what set -e does), skips commands starting with ! where the result isn't checked.
Essentially if you want to say exit if the command succeeds, you can do:
cmd && exit 1 # explicit ! cmd || exit 1 # explicit
or still exploiting errexit
cmd && false ! cmd || false
I'm going to fix it in existing selftests using the last variant, but keep this in mind in your selftests as well. (Variants with ! are preferred because they also return 0 if cmd fails, as you'd expect, I personally prefer to use false instead of explicit exit, but that's up to you).
Thanks,
Gabriele
[1] - https://www.shellcheck.net/wiki/SC2251
Quoted 96 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +3
-0+echo 0 > monitors/tlob/enable +echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo > /sys/kernel/tracing/tracediff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.t c b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.t c
new file mode 100644
index 000000000000..d210d9c3a92d
---/dev/null
+++
b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.t ctools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3
-0@@ -0,0 +1,67 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor budget violation (error_env_tloband
detail_env_tlobfire with correct fields)tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +10
-0+# requires: tlob:monitor + +RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET="${RV_BINDIR}/tlob_target" +TLOB_SYM="${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=monitors/tlob/monitor + +busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work2>/dev/null)
+stop_offset=$("$TLOB_SYM"sym_offset"$UPROBE_TARGET"tlob_busy_work_done2>/dev/null)+[ -n "$busy_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupportedtools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +12
-0+ +"$UPROBE_TARGET" 30000 & +busy_pid=$! +sleep 0.05 + +echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +# 10 µs budget - fires almost immediately; task is busy-spinning on-CPU.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=10000" > "$TLOB_MONITOR"tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +6
-0+ +# wait up to 2 s for detail_env_tlob +found=0; i=0 +while [ "$i" -lt 20 ]; do + sleep 0.1 + grep -q "detail_env_tlob" /sys/kernel/tracing/trace && {found=1; break; }
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc new file +4
-0+ i=$((i+1)) +done + +echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR"2>/dev/null
+kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || truetools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +8
-0+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 0 > monitors/tlob/enable + +[ "$found" = "1" ] + +# error_env_tlob must carry the clk_elapsed environment field. +# The event label is "budget_exceeded" when detected by the hrtimercallback,
+# or the triggering sched event name when detected by the constraint path on a
+# preemption that races with the timer (common onPREEMPT_RT/ VM). Both are(quoted diff) +2
-0+# valid detections; check the env field instead of the label. +grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q "clk_elapsed="
(quoted diff) +2
-0+ +# detail_env_tlob must have all five fields with the correctthreshold
tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +8
-0+line=$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1) +echo "$line" | grep -q "pid=" +echo "$line" | grep -q "threshold_ns=10000" +echo "$line" | grep -q "running_ns=" +echo "$line" | grep -q "waiting_ns=" +echo "$line" | grep -q "sleeping_ns=" + +# Busy-spin keeps the task on-CPU: running_ns must exceed
sleeping_ns.tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc new file +5
-0+running=$(echo "$line" | sed 's/.*running_ns=\([0-9]*\).*/\1/') +sleeping=$(echo "$line" | sed 's/.*sleeping_ns=\([0-9]*\).*/\1/') +[ "$running" -gt "$sleeping" ] + +echo > /sys/kernel/tracing/trace
Re: [PATCH v3 2/9] rv: add generic uprobe infrastructure for RV monitors
In reply to Gabriele MonacoQuoted 83 lines · Gabriele Monaco
Quoted 8 lines · On Mon, 2026-06-08 at 00:13 +0800, [email protected] wrote:
From: Wen Yang <[email protected]>
Introduce
rv_uprobe, a thin wrapper arounduprobe_consumerprovidingrv_uprobe_attach_path(),rv_uprobe_attach(), andrv_uprobe_detach()for RV monitors. An opaque priv pointer is forwarded unchanged to entry/return handlers so monitors can carry per-binding state (e.g. a latency threshold) to the hot path without any global lookup.Quoted 6 lines
rv_uprobe_detach()is fully synchronous (nosync + sync +path_put+ kfree), closing the use-after-free window present in open-coded patterns wherekfree()precedesuprobe_unregister_sync().Suggested-by Gabriele Monaco <[email protected]>Signed-off-by Wen Yang <[email protected]>I find your implementation solid, but I wonder if it isn't a bit overdoing it. It's good to have helper functions abstracting away the common uprobes code, but I find the number of structures used to indirect/hide the process a bit confusing.
Users of this headers still know they're using uprobes, so we don't really need to hide it that much, plus the double function pointers are an additional performance hit we could easily skip.
As I get it, we want to pass a private pointer (essentially to get the threshold in tlob) and the uprobe infrastructure doesn't allow that transparently. So let's keep on using struct embedding but a bit more cut to the bone:
struct rv_uprobe { struct uprobe_consumer uc; struct uprobe *uprobe; void *priv; };Then handlers would use
container_of()appropriately to derive the data they need.static int tlob_uprobe_entry_handler(struct uprobe_consumer *self, struct pt_regs *regs, __u64 *data) { struct rv_uprobe *p = container_of(self, struct rv_uprobe, uc); struct tlob_uprobe_binding *b = p->priv; ... }You could even define the struct directly inside your private data and save one step (and no void *):
struct rv_uprobe { struct uprobe_consumer uc; struct uprobe *uprobe; }; #define DECLARE_RV_UPROBE(name) struct rv_uprobe name // And then struct tlob_uprobe_binding { u64 threshold_ns; ... DECLARE_RV_UPROBE(start); DECLARE_RV_UPROBE(stop); } static int tlob_uprobe_entry_handler(struct uprobe_consumer *self, struct pt_regs *regs, __u64 *data) { struct tlob_uprobe_binding *b = container_of(self, struct tlob_uprobe_binding, start.uc); ... }Now I intentionally skipped offset, as I don't really see why carry it around, and path, as we could live without it by relying on inodes (uprobes reference-track them but you'd need to save them somewhere).
Mind, I only build tested this idea (appended after). This was supported by an LLM which did a few more changes like standardising names and return values of registration functions (a bit more consistent with other stuff, you don't have to commit with that though).
Quoted 7 lines
By the way, do we really need the duplicate check? Would it break if a user defines it twice? If not, we could simplify the
tlob_add_uprobe()even further and doing all path related checks in the library, since those aren't quite tlob specific.Thoughts?
Hi Gabriele,
Thanks for the detailed v3 review. All feedback is addressed in v4;
only one item ([2-1]) required a design deviation due to a UAF found during PREEMPT_RT testing.
On [2-1]~[2-5]: embedded consumer causes UAF on PREEMPT_RT
-----------------------------------------------------------
The uprobe_bind selftest oopses on PREEMPT_RT(full):
handler_chain+0xc9: mov rax, [r15+0x18] ; advance list iterator
RAX: 000015ec00001f28 ; garbage — &uprobe->consumers after kfree
handler_chain() reads uc->cons_node.next after uc->handler() returns, still inside rcu_read_lock_trace(). That pointer is &uprobe->consumers (the list head embedded in struct uprobe), which gets freed through:
put_uprobe()
-> schedule_work(uprobe_free_deferred) /* async */
-> call_srcu(&uretprobes_srcu, ...)
-> call_rcu_tasks_trace(kfree_uprobe)
-> kfree(uprobe)rv_uprobe_sync() calls uprobe_unregister_sync() which calls synchronize_srcu(&uretprobes_srcu), but that only matters after the kworker has submitted work to uretprobes_srcu. On a loaded PREEMPT_RT box the kworker may not have run yet, so synchronize_srcu() returns immediately and kfree(uprobe) races with the still-iterating handler_chain():
CPU A CPU B
consumer_del() → list_del_rcu rcu_read_lock_trace()
put_uprobe() → schedule_work uc->handler() returns
rv_uprobe_sync(): reading cons_node.next...
synchronize_srcu(&uretprobes_srcu)
← idle; returns immediately
[kworker fires later]
kfree(uprobe) ← frees &uprobe->consumers
cons_node.next = freed mem → CRASH
With uc embedded in the binding (as [2-1] suggests), no amount of delaying kfree(binding) helps: uprobe->consumers is freed by a chain we don't control. The fix is to keep uc->cons_node in memory that outlives the handler_chain() iteration, which means a separate allocation freed only after rv_uprobe_sync():
rv_uprobe_unregister_nosync() /* list_del_rcu + schedule_work */
rv_uprobe_sync() /* waits for any already-submitted
srcu work */
rv_uprobe_free() /* kfree(impl) — safe, iteration is
done */
kfree(b) /* binding; never contained uc */
v4 keeps the public API shape you suggested with impl private:
struct rv_uprobe {
struct rv_uprobe_impl *impl; /* allocated byrv_uprobe_register() */
struct inode *inode;
};
#define DECLARE_RV_UPROBE(name) struct rv_uprobe name /* [2-2] */
int rv_uprobe_register(const char *binpath, loff_t offset,
struct rv_uprobe *p, handler_fn,
ret_handler_fn, void *priv); /* [2-4] */
void rv_uprobe_unregister(struct rv_uprobe *p); void rv_uprobe_unregister_nosync(struct rv_uprobe *p); void rv_uprobe_sync(void); void rv_uprobe_free(struct rv_uprobe *p); /*
[2-5] restored */
bool rv_uprobe_is_registered(const struct rv_uprobe *p); /*
[2-7] added */
void *rv_uprobe_get_priv(struct uprobe_consumer *uc);
Handler signature is (struct uprobe_consumer *self, ...) [2-3]; private data is retrieved via rv_uprobe_get_priv(self) instead of container_of().
Then, rv_uprobe_free() is restored ([2-5] partially reverted).
All other v3 items have been resolved, we are waiting for your comments on the above(embedded consumer causes UAF):
[1-1,1-3] `# define` / `# error` space removed
[1-4] `⟹` -> `=>`
[1-6,1-7] #if/#else in da_destroy_storage() and da_monitor_init()
replaced with plain if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
[1-8] tracepoint_synchronize_unregister() lifted to da_monitor_destroy before the pool/kmalloc branch
[2-2] DECLARE_RV_UPROBE(name) — kept [2-3] handler sig (struct uprobe_consumer *self, ...) — kept [2-4] rv_uprobe_register() returns int — kept [2-5] rv_uprobe_attach/detach removed; rv_uprobe_free() restored (see above) [2-6] offset, path fields removed; inode used for identity [2-7] rv_uprobe_is_registered() added
[6-1] Suggested-by removed [6-2] tlob Kconfig entry placed after the deadline monitors marker [6-3] Unnecessary includes removed (hrtimer.h, ktime.h, sched.h, tracefs.h) [6-4] `#include "../../rv.h"` -> `<rv.h>`
[6-5,6-6] Verbose comments around DA_MON_POOL_SIZE and da_extra_cleanup
simplified; early return in tlob_reset_notify() on !trace_detail_env_tlob_enabled()
[6-7] ha_setup_invariants():
if (atomic_read_acquire(&target->stopping)) return;
if (next_state < state_max_tlob) ha_start_timer_ns(...);
else ha_cancel_timer(ha_mon);
[6-8] offsetof() arithmetic -> enum-indexed array:
enum tlob_acc_idx { TLOB_ACC_RUNNING, TLOB_ACC_WAITING,
TLOB_ACC_SLEEPING, TLOB_ACC_MAX };
u64 accs_ns[TLOB_ACC_MAX];[6-9] 1000 → TLOB_MIN_THRESHOLD_NS
[6-10] WARN_ON_ONCE(!da_mon) -> if (unlikely(WARN_ON_ONCE(!da_mon))) [6-11] __free(kfree) + list_add_tail(&no_free_ptr(b)->list, ...) in
tlob_add_uprobe(); note that "b = no_free_ptr(b)" would restore b and re-trigger __free on return — the correct pattern is to use no_free_ptr() as the argument to list_add_tail() directly
[6-15] tlob.h enum/struct order aligned with rvgen output [6-16] tracefs_create_file() -> rv_create_file()
[7-1] KUnit tests call tlob_parse_uprobe_line/tlob_parse_remove_line
directly; no real uprobe creation from unit tests
[7-2] KUNIT_EXPECT_EQ(test, ..., 0) for valid-line cases
[8-1] ftracetest: walk-up algorithm to locate test.d/functions [9-4] tlob_busy/sleep/preempt_work unified to duration_ms
--
Best wishes,
Wen
Quoted 513 lines
Gabriele
**Suggested simplification:**
---
include/rv/rv_uprobe.h +31
-72@@ -7,83 +7,56 @@#ifndef _RV_UPROBE_H#define _RV_UPROBE_H -#include <linux/path.h> #include <linux/types.h> +#include <linux/uprobes.h> struct pt_regs ; +struct inode ;/** * struct rv_uprobe - a single uprobe registered on behalf of an RV monitor * -* @offset: byte offset within the ELF binary where the probe is installed -* @priv: monitor-private pointer; set at attach time, never touched by -* this layer; passed unchanged to entry_fn / ret_fn -* @path: resolved path of the probed binary (read-only after attach); -* callers may use path.dentry for identity comparisons -* -* The implementation fields (uprobe_consumer, uprobe handle, callbacks) are -* private to rv_uprobe.c and are not exposed here; monitors must not access -* them directly. +* @uc: underlying uprobe consumer (publicly visible) +* @uprobe: active uprobe structure handle +* @inode: inode of the target binary (read-only after registration) */ struct rv_uprobe { -/* public: read-only after rv_uprobe_attach*() */ -loff_t offset; -void *priv; -struct path path; +struct uprobe_consumer uc; +struct uprobe *uprobe; +struct inode *inode; }; -/** -* rv_uprobe_attach_path - register an uprobe given an already-resolved path -* @path: path of the target binary; rv_uprobe takes its own reference -* @offset: byte offset within the binary -* @entry_fn: called on probe hit (entry); may be NULL -* @ret_fn: called on function return (uretprobe); may be NULL -* @priv: opaque pointer forwarded to callbacks unchanged -* -* Use this variant when the caller has already resolved the path (e.g. to -* register multiple probes on the same binary with a single kern_path call). -* The inode is derived internally via d_real_inode(), so inode and path are -* always consistent. -* -* Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure. -*/ -struct rv_uprobe *rv_uprobe_attach_path (struct path *path,loff_t offset, -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), -void *priv); +/* Seamless inline declaration of a named uprobe inside user structs */ +#define DECLARE_RV_UPROBE (name) \ + struct rv_uprobe name/** -* rv_uprobe_attach - resolve binpath and register an uprobe -* @binpath: absolute path to the target binary -* @offset: byte offset within the binary -* @entry_fn: called on probe hit (entry); may be NULL -* @ret_fn: called on function return (uretprobe); may be NULL -* @priv: opaque pointer forwarded to callbacks unchanged +* rv_uprobe_register - resolve binpath and register an uprobe +* @binpath: absolute path to the target binary +* @offset: byte offset within the binary +* @p: pointer to the allocated/embedded rv_uprobe structure +* @handler: called on probe hit (entry); may be NULL +* @ret_handler: called on function return (uretprobe); may be NULL * -* Resolves binpath via kern_path(), then delegates to rv_uprobe_attach_path(). +* Resolves binpath via kern_path(), registers the uprobe directly using the +* embedded `uprobe_consumer`, and immediately releases the path reference. * -* Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure. +* Returns 0 on success, or a negative error code on failure. */ -struct rv_uprobe *rv_uprobe_attach (const char *binpath,loff_t offset, -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), -void *priv); +int rv_uprobe_register (const char *binpath,loff_t offset,struct rv_uprobe *p, +int (*handler)(struct uprobe_consumer *self,struct pt_regs *regs,__u64 *data), +int (*ret_handler)(struct uprobe_consumer *self,unsigned long func, +struct pt_regs *regs,__u64 *data));/** -* rv_uprobe_detach - synchronously unregister an uprobe and free it -* @p: probe to detach; may be NULL (no-op) -* -* Calls uprobe_unregister_nosync(), then uprobe_unregister_sync() to wait -* for any in-progress handler to finish, then releases the path reference -* and frees the rv_uprobe struct. The caller's priv data is NOT freed. +* rv_uprobe_unregister - synchronously unregister a uprobe +* @p: probe to unregister; may be NULL (no-op) * -* When removing a single probe, prefer this over the three-phase API. -* Safe to call from process context only (uprobe_unregister_sync() may -* schedule). +* Dequeues the uprobe and waits synchronously for all in-flight handlers +* to complete. */ -void rv_uprobe_detach (struct rv_uprobe *p); +void rv_uprobe_unregister (struct rv_uprobe *p);/** * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting @@ -91,9 +64,7 @@ void rv_uprobe_detach(struct rv_uprobe *p);* * Removes the uprobe from the uprobe subsystem but does NOT wait for * in-flight handlers to complete. The caller must call rv_uprobe_sync() -* before calling rv_uprobe_free() on the same probe. -* -* Use this to batch multiple deregistrations before a single rv_uprobe_sync(). +* before freeing any container holding this probe. */ void rv_uprobe_unregister_nosync (struct rv_uprobe *p);@@ -101,19 +72,8 @@ void rv_uprobe_unregister_nosync(struct rv_uprobe *p);* rv_uprobe_sync - wait for all in-flight uprobe handlers to complete * * Global barrier: waits for every in-flight uprobe handler across the system -* to finish. Call once after a batch of rv_uprobe_unregister_nosync() calls -* and before any rv_uprobe_free() call. +* to finish. */ void rv_uprobe_sync (void ); -/** -* rv_uprobe_free - release resources of a previously deregistered probe -* @p: probe to free; may be NULL (no-op) -* -* Releases the path reference and frees the rv_uprobe struct. Must only -* be called after rv_uprobe_sync() has returned. The caller's priv data -* is NOT freed. -*/ -void rv_uprobe_free (struct rv_uprobe *p); -#endif /* _RV_UPROBE_H */ kernel/trace/rv/monitors/tlob/tlob.c +30
-30@@ -252,8 +252,8 @@ struct tlob_uprobe_binding {char binpath[TLOB_MAX_PATH ];loff_t offset_start;loff_t offset_stop; -struct rv_uprobe *start_probe; -struct rv_uprobe *stop_probe; +DECLARE_RV_UPROBE (start_probe); +DECLARE_RV_UPROBE (stop_probe); };/* RCU callback: free the slab once no readers remain. */ @@ -512,16 +512,16 @@ int tlob_stop_task(struct task_struct *task)EXPORT_SYMBOL_GPL (tlob_stop_task); -static int tlob_uprobe_entry_handler (struct rv_uprobe *p,struct pt_regs *regs, +static int tlob_uprobe_entry_handler (struct uprobe_consumer *self,struct pt_regs *regs,__u64 *data) { -struct tlob_uprobe_binding *b = p->priv; + struct tlob_uprobe_binding *b = container_of(self, struct tlob_uprobe_binding, start_probe.uc);tlob_start_task (current, b->threshold_ns);return 0 ; } -static int tlob_uprobe_stop_handler (struct rv_uprobe *p,struct pt_regs *regs, +static int tlob_uprobe_stop_handler (struct uprobe_consumer *self,struct pt_regs *regs,__u64 *data) {tlob_stop_task (current);@@ -537,6 +537,7 @@ static int tlob_add_uprobe(u64 threshold_ns, const char *binpath, {struct tlob_uprobe_binding *b, *tmp_b;char pathbuf[TLOB_MAX_PATH ]; +struct inode *inode;struct path path;char *canon;int ret;@@ -561,10 +562,12 @@ static int tlob_add_uprobe(u64 threshold_ns, const char *binpath, goto err_path; } -/* Reject duplicate start offset for the same binary. */ + inode =d_real_inode (path.dentry); + +/* Reject duplicate start offset for the same binary inode. */ list_for_each_entry (tmp_b, &tlob_uprobe_list, list) {if (tmp_b->offset_start == offset_start && - tmp_b->start_probe->path.dentry == path.dentry) { + tmp_b->start_probe.inode == inode) { ret = -EEXIST ; goto err_path; }@@ -577,29 +580,25 @@ static int tlob_add_uprobe(u64 threshold_ns, const char *binpath, }strscpy (b->binpath, canon,sizeof (b->binpath)); -/* Both probes share b (priv) and path; attach_path refs path itself. */ - b->start_probe =rv_uprobe_attach_path (&path, offset_start, - tlob_uprobe_entry_handler,NULL , b); -if (IS_ERR (b->start_probe)) { - ret =PTR_ERR (b->start_probe); - b->start_probe =NULL ; - goto err_path; - } +path_put (&path); + +/* Both probes are registered directly on the embedded fields */ + ret =rv_uprobe_register (b->binpath, offset_start, &b->start_probe, + tlob_uprobe_entry_handler,NULL ); +if (ret) + goto err_free; - b->stop_probe =rv_uprobe_attach_path (&path, offset_stop, - tlob_uprobe_stop_handler,NULL , b); -if (IS_ERR (b->stop_probe)) { - ret =PTR_ERR (b->stop_probe); - b->stop_probe =NULL ; + ret =rv_uprobe_register (b->binpath, offset_stop, &b->stop_probe, + tlob_uprobe_stop_handler,NULL ); +if (ret) goto err_start; - } -path_put (&path);list_add_tail (&b->list, &tlob_uprobe_list);return 0 ; err_start: -rv_uprobe_detach (b->start_probe); +rv_uprobe_unregister (&b->start_probe); + goto err_free; err_path:path_put (&path); err_free:@@ -611,21 +610,24 @@ static int tlob_remove_uprobe_by_key(loff_t offset_start, const char *binpath) {struct tlob_uprobe_binding *b, *tmp;struct path remove_path; +struct inode *inode;int ret; ret =kern_path (binpath,LOOKUP_FOLLOW , &remove_path);if (ret)return ret; + inode =d_real_inode (remove_path.dentry); + ret = -ENOENT ;list_for_each_entry_safe (b, tmp, &tlob_uprobe_list, list) {if (b->offset_start != offset_start)continue ; -if (b->start_probe->path.dentry != remove_path.dentry) +if (b->start_probe.inode != inode)continue ;list_del (&b->list); -rv_uprobe_detach (b->start_probe); -rv_uprobe_detach (b->stop_probe); +rv_uprobe_unregister (&b->start_probe); +rv_uprobe_unregister (&b->stop_probe);kfree (b); ret =0 ;break ;@@ -643,8 +645,8 @@ static void tlob_remove_all_uprobes(void)mutex_lock (&tlob_uprobe_mutex);list_for_each_entry_safe (b, tmp, &tlob_uprobe_list, list) {list_move (&b->list, &pending); -rv_uprobe_unregister_nosync (b->start_probe); -rv_uprobe_unregister_nosync (b->stop_probe); +rv_uprobe_unregister_nosync (&b->start_probe); +rv_uprobe_unregister_nosync (&b->stop_probe); }mutex_unlock (&tlob_uprobe_mutex);@@ -658,8 +660,6 @@ static void tlob_remove_all_uprobes(void)rv_uprobe_sync ();list_for_each_entry_safe (b, tmp, &pending, list) { -rv_uprobe_free (b->start_probe); -rv_uprobe_free (b->stop_probe);kfree (b); } }kernel/trace/rv/rv_uprobe.c +36
-127@@ -10,149 +10,74 @@#include <linux/uprobes.h> #include <rv/rv_uprobe.h> -/* -* Private extension of struct rv_uprobe. Allocated by rv_uprobe_attach*() -* and returned to callers as &impl->pub. -*/ -struct rv_uprobe_impl { -struct rv_uprobe pub;/* must be first; callers hold &pub */ -struct uprobe_consumer uc; -struct uprobe *uprobe; -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data); -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data); -}; - -static int rv_uprobe_handler (struct uprobe_consumer *uc, -struct pt_regs *regs,__u64 *data) -{ - struct rv_uprobe_impl *impl = container_of(uc, struct rv_uprobe_impl, uc); - -if (impl->entry_fn) -return impl->entry_fn (&impl->pub, regs, data); -return 0 ; -} - -static int rv_uprobe_ret_handler (struct uprobe_consumer *uc, -unsigned long func, -struct pt_regs *regs,__u64 *data) -{ - struct rv_uprobe_impl *impl = container_of(uc, struct rv_uprobe_impl, uc); - -if (impl->ret_fn) -return impl->ret_fn (&impl->pub, func, regs, data); -return 0 ; -} - -static struct rv_uprobe * -__rv_uprobe_attach (struct inode *inode,struct path *path,loff_t offset, -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), -void *priv) -{ -struct rv_uprobe_impl *impl; -int ret; - -if (!entry_fn && !ret_fn) -return ERR_PTR (-EINVAL ); - - impl =kzalloc_obj (*impl,GFP_KERNEL ); -if (!impl) -return ERR_PTR (-ENOMEM ); - - impl->pub.offset = offset; - impl->pub.priv = priv; - impl->entry_fn = entry_fn; - impl->ret_fn = ret_fn; -path_get (path); - impl->pub.path = *path; - -if (entry_fn) - impl->uc.handler = rv_uprobe_handler; -if (ret_fn) - impl->uc.ret_handler = rv_uprobe_ret_handler; - - impl->uprobe =uprobe_register (inode, offset,0 , &impl->uc); -if (IS_ERR (impl->uprobe)) { - ret =PTR_ERR (impl->uprobe); -path_put (&impl->pub.path); -kfree (impl); -return ERR_PTR (ret); - } - -return &impl->pub; -} - -/** -* rv_uprobe_attach_path - register an uprobe given an already-resolved path -*/ -struct rv_uprobe *rv_uprobe_attach_path (struct path *path,loff_t offset, -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), -void *priv) -{ -struct inode *inode =d_real_inode (path->dentry); - -return __rv_uprobe_attach (inode, path, offset, entry_fn, ret_fn, priv); -} -EXPORT_SYMBOL_GPL (rv_uprobe_attach_path); -/** -* rv_uprobe_attach - resolve binpath and register an uprobe +* rv_uprobe_register - resolve binpath and register an uprobe */ -struct rv_uprobe *rv_uprobe_attach (const char *binpath,loff_t offset, -int (*entry_fn)(struct rv_uprobe *p,struct pt_regs *regs,__u64 *data), -int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), -void *priv) +int rv_uprobe_register (const char *binpath,loff_t offset,struct rv_uprobe *p, +int (*handler)(struct uprobe_consumer *self,struct pt_regs *regs,__u64 *data), +int (*ret_handler)(struct uprobe_consumer *self,unsigned long func, +struct pt_regs *regs,__u64 *data)) { -struct rv_uprobe *p; +struct inode *inode;struct path path;int ret; +if (!handler && !ret_handler) +return -EINVAL ; + ret =kern_path (binpath,LOOKUP_FOLLOW , &path);if (ret) -return ERR_PTR (ret); +return ret;if (!d_is_reg (path.dentry)) {path_put (&path); -return ERR_PTR (-EINVAL ); +return -EINVAL ; } - p =rv_uprobe_attach_path (&path, offset, entry_fn, ret_fn, priv); + inode =d_real_inode (path.dentry); + + p->uc.handler = handler; + p->uc.ret_handler = ret_handler; + p->inode = inode; + + p->uprobe =uprobe_register (inode, offset,0 , &p->uc);path_put (&path); -return p; + +if (IS_ERR (p->uprobe)) { + ret =PTR_ERR (p->uprobe); + p->uprobe =NULL ; + p->inode =NULL ; +return ret; + } + +return 0 ; } -EXPORT_SYMBOL_GPL (rv_uprobe_attach); +EXPORT_SYMBOL_GPL (rv_uprobe_register);/** -* rv_uprobe_detach - synchronously unregister an uprobe and free it +* rv_uprobe_unregister - synchronously unregister a uprobe */ -void rv_uprobe_detach (struct rv_uprobe *p) +void rv_uprobe_unregister (struct rv_uprobe *p) { -if (!p) +if (!p ||IS_ERR_OR_NULL (p->uprobe))return ;rv_uprobe_unregister_nosync (p);rv_uprobe_sync (); -rv_uprobe_free (p); } -EXPORT_SYMBOL_GPL (rv_uprobe_detach); +EXPORT_SYMBOL_GPL (rv_uprobe_unregister);/** * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting */ void rv_uprobe_unregister_nosync (struct rv_uprobe *p) { -struct rv_uprobe_impl *impl; - -if (!p) +if (!p ||IS_ERR_OR_NULL (p->uprobe))return ; - impl = container_of(p, struct rv_uprobe_impl, pub); -uprobe_unregister_nosync (impl->uprobe, &impl->uc); +uprobe_unregister_nosync (p->uprobe, &p->uc); + p->uprobe =NULL ; + p->inode =NULL ; }EXPORT_SYMBOL_GPL (rv_uprobe_unregister_nosync);@@ -164,19 +89,3 @@ void rv_uprobe_sync(void)uprobe_unregister_sync (); }EXPORT_SYMBOL_GPL (rv_uprobe_sync); - -/** -* rv_uprobe_free - release resources of a previously deregistered probe -*/ -void rv_uprobe_free (struct rv_uprobe *p) -{ -struct rv_uprobe_impl *impl; - -if (!p) -return ; - - impl = container_of(p, struct rv_uprobe_impl, pub); -path_put (&p->path); -kfree (impl); -} -EXPORT_SYMBOL_GPL (rv_uprobe_free);
Gabriele Monaco
maintainerRe: [PATCH v3 2/9] rv: add generic uprobe infrastructure for RV monitors
Quoted 25 lines · Wen Yang
On [2-1]~[2-5]: embedded consumer causes UAF on
PREEMPT_RT
-----------------------------------------------------------The
uprobe_bindselftest oopses onPREEMPT_RT(full):
handler_chain+0xc9: mov rax, [r15+0x18] ; advance list iterator
RAX: 000015ec00001f28 ; garbage —&uprobe->consumersafter kfree
handler_chain()readsuc->cons_node.nextafteruc->handler()returns, still inside rcu_read_lock_trace(). That pointer is&uprobe->consumers(the list head embedded in struct uprobe), which gets freed through:
put_uprobe()
-> schedule_work(uprobe_free_deferred) /* async */ ->call_srcu(&uretprobes_srcu, ...)
->call_rcu_tasks_trace(kfree_uprobe)-> kfree(uprobe)
rv_uprobe_sync()callsuprobe_unregister_sync()which callssynchronize_srcu(&uretprobes_srcu), but that only matters after the kworker has submitted work to uretprobes_srcu. On a loadedPREEMPT_RTbox the kworker may not have run yet, sosynchronize_srcu()returns immediately and kfree(uprobe) races with the still-iteratinghandler_chain():
Quoted 5 lines
CPU A CPU B
consumer_del()→ list_del_rcurcu_read_lock_trace()put_uprobe() → schedule_workuc->handler()returns rv_uprobe_sync(): reading cons_node.next...synchronize_srcu(&uretprobes_srcu)
If CPU B is in an RCU trace read-side critical section this doesn't return immediately, it returns after readers are done, doesn't it? (well, what you really care here is the synchronize_rcu_tasks_trace() but we do both).
Quoted 5 lines
← idle; returns immediately
[kworker fires later]
kfree(uprobe) ← frees&uprobe->consumers
cons_node.next = freed mem → CRASH
So I had a bit of a look and as far as I understand, we don't have any control over the uprobe allocation pattern (workqueues and whatnot) and we don't really care as long as we deregister it appropriately.
What we do control is the uprobe_consumer, that must be freed only after the uprobe was synchronously deregistered, that should guarantee no reader is going to reference it, shouldn't it?
uprobe_unregister_nosync() removes it from the cons_node list, so it's safe to assume any handler_chain() after the next RCU-trace grace period won't see it.
In the sketch I sent this is happening (all kfree(b) are after rv_uprobe_unregister() which does the sync). What am I missing here?
uprobe is also freed after both grace periods, so no reader should use that either.
The situation you're seeing isn't fully clear to me, I applied my sketch and don't see any splat on a vng box.
Quoted 9 lines
With uc embedded in the binding (as [2-1] suggests), no amount of delaying kfree(binding) helps:
uprobe->consumersis freed by a chain we don't control. The fix is to keepuc->cons_nodein memory that outlives thehandler_chain()iteration, which means a separate allocation freed only afterrv_uprobe_sync():rv_uprobe_unregister_nosync() /*
list_del_rcu+schedule_work*/ rv_uprobe_sync() /* waits for any already-submitted srcu work */
We of course need to do any free after this sync, but I don't get why we need an additional allocation since the following are both plain synchronous frees, why isn't a single one (on b) enough?
I'm a bit lost on this..
Thanks,
Gabriele
Quoted 642 lines
rv_uprobe_free() /* kfree(impl) — safe, iteration is done */
kfree(b) /* binding; never contained uc */v4 keeps the public API shape you suggested with impl private:
struct
rv_uprobe{
struct rv_uprobe_impl *impl; /* allocated byrv_uprobe_register()*/
struct inode *inode;
};
#define DECLARE_RV_UPROBE(name) structrv_uprobename /* [2-2] */int
rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe *p, handler_fn, ret_handler_fn, void *priv); /* [2-4] */ voidrv_uprobe_unregister(struct rv_uprobe *p);
voidrv_uprobe_unregister_nosync(struct rv_uprobe *p);
voidrv_uprobe_sync(void);
voidrv_uprobe_free(struct rv_uprobe *p); /* [2-5] restored */
boolrv_uprobe_is_registered(const struct rv_uprobe *p); /* [2-7] added */
void *rv_uprobe_get_priv(structuprobe_consumer*uc);Handler signature is (struct
uprobe_consumer*self, ...) [2-3]; private data is retrieved viarv_uprobe_get_priv(self)instead ofcontainer_of().Then,
rv_uprobe_free()is restored ([2-5] partially reverted).All other v3 items have been resolved, we are waiting for your comments on the above(embedded consumer causes UAF):
[1-1,1-3]
# define/# errorspace removed
[1-4]⟹->=>
[1-6,1-7] #if/#else inda_destroy_storage()andda_monitor_init()replaced with plain if (DA_MON_ALLOCATION_STRATEGY==DA_ALLOC_POOL)
[1-8]tracepoint_synchronize_unregister()lifted toda_monitor_destroybefore the pool/kmalloc branch[2-2]
DECLARE_RV_UPROBE(name)— kept
[2-3] handler sig (structuprobe_consumer*self, ...) — kept [2-4]rv_uprobe_register()returns int — kept
[2-5] rv_uprobe_attach/detach removed;rv_uprobe_free()restored (see above)
[2-6] offset, path fields removed; inode used for identity [2-7]rv_uprobe_is_registered()added[6-1] Suggested-by removed
[6-2] tlobKconfigentry placed after the deadline monitors marker [6-3] Unnecessary includes removed (hrtimer.h,ktime.h,sched.h,tracefs.h) [6-4]#include "../../rv.h"-><rv.h>
[6-5,6-6] Verbose comments aroundDA_MON_POOL_SIZEandda_extra_cleanupsimplified; early return intlob_reset_notify()on !trace_detail_env_tlob_enabled()
[6-7]ha_setup_invariants():
if (atomic_read_acquire(&target->stopping)) return;
if (next_state<state_max_tlob)ha_start_timer_ns(...); elseha_cancel_timer(ha_mon);
[6-8]offsetof()arithmetic -> enum-indexed array:
enumtlob_acc_idx{TLOB_ACC_RUNNING,TLOB_ACC_WAITING,TLOB_ACC_SLEEPING,TLOB_ACC_MAX}; u64 accs_ns[TLOB_ACC_MAX];
[6-9] 1000 →TLOB_MIN_THRESHOLD_NS
[6-10]WARN_ON_ONCE(!da_mon)-> if (unlikely(WARN_ON_ONCE(!da_mon))) [6-11]__free(kfree)+list_add_tail(&no_free_ptr(b)->list, ...)intlob_add_uprobe(); note that "b =no_free_ptr(b)" would restore b and re-trigger__freeon return — the correct pattern is to useno_free_ptr()as the argument tolist_add_tail()directly [6-15]tlob.henum/struct order aligned with rvgen output
[6-16]tracefs_create_file()->rv_create_file()[7-1] KUnit tests call tlob_parse_uprobe_line/tlob_parse_remove_line directly; no real uprobe creation from unit tests [7-2]
KUNIT_EXPECT_EQ(test, ..., 0)for valid-line cases[8-1] ftracetest: walk-up algorithm to locate test.d/functions [9-4] tlob_busy/sleep/preempt_work unified to
duration_ms--
Best wishes,
WenQuoted 555 lines
Gabriele
**Suggested simplification:**
---
include/rv/rv_uprobe.h +0
-0@@ -7,83 +7,56 @@ #ifndef _RV_UPROBE_H #define _RV_UPROBE_H
-#include <linux/path.h>
#include <linux/types.h>
+#include <linux/uprobes.h>
structpt_regs;
+struct inode;
/**
* structrv_uprobe- a single uprobe registered on behalf of an RV monitor
*
- * @offset: byte offset within the ELF binary where the probe is installed
- * @priv: monitor-private pointer; set at attach time, never touched by
- * this layer; passed unchanged toentry_fn/ret_fn
- * @path: resolved path of the probed binary (read-only after attach);
- * callers may use path.dentry for identity comparisons
- *
- * The implementation fields (uprobe_consumer, uprobe handle, callbacks) are
- * private torv_uprobe.cand are not exposed here; monitors must not access
- * them directly.
+ * @uc: underlying uprobe consumer (publicly visible)
+ * @uprobe: active uprobe structure handle
+ * @inode: inode of the target binary (read-only after registration) */
structrv_uprobe{include/rv/rv_uprobe.h +3
-4-/* public: read-only after rv_uprobe_attach*() */ -loff_t offset; -void *priv; -struct path path; +struct uprobe_consumer uc; +struct uprobe *uprobe; +struct inode *inode;};
-/**
- *rv_uprobe_attach_path- register an uprobe given an already-resolved path
- * @path: path of the target binary;rv_uprobetakes its own reference
- * @offset: byte offset within the binary
- * @entry_fn: called on probe hit (entry); may be NULL
- * @ret_fn: called on function return (uretprobe); may be NULL
- * @priv: opaque pointer forwarded to callbacks unchanged
- *
- * Use this variant when the caller has already resolved the path (e.g. to
- * register multiple probes on the same binary with a singlekern_pathcall).
- * The inode is derived internally viad_real_inode(), so inode and path are
- * always consistent.
- *
- * Returns a pointer to the newrv_uprobeon success,ERR_PTRon failure.
- */include/rv/rv_uprobe.h +0
-2-struct rv_uprobe *rv_uprobe_attach_path(struct path *path, loff_t offset, - int (*entry_fn)(struct rv_uprobe *p, struct pt_regs *regs, __u64*data),
include/rv/rv_uprobe.h +3
-3-int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), - void *priv); +/* Seamless inline declaration of a named uprobe inside user structs */ +#define DECLARE_RV_UPROBE (name) \ + struct rv_uprobe name
/**
- *rv_uprobe_attach- resolve binpath and register an uprobe
- * @binpath: absolute path to the target binary
- * @offset: byte offset within the binary
- * @entry_fn: called on probe hit (entry); may be NULL
- * @ret_fn: called on function return (uretprobe); may be NULL
- * @priv: opaque pointer forwarded to callbacks unchanged
+ *rv_uprobe_register- resolve binpath and register an uprobe
+ * @binpath: absolute path to the target binary
+ * @offset: byte offset within the binary
+ * @p: pointer to the allocated/embeddedrv_uprobestructure
+ * @handler: called on probe hit (entry); may be NULL
+ * @ret_handler: called on function return (uretprobe); may be NULL *
- * Resolves binpath viakern_path(), then delegates torv_uprobe_attach_path().
+ * Resolves binpath viakern_path(), registers the uprobe directly using the
+ * embeddeduprobe_consumer, and immediately releases the path reference. *
- * Returns a pointer to the newrv_uprobeon success,ERR_PTRon failure.
+ * Returns 0 on success, or a negative error code on failure. */include/rv/rv_uprobe.h +0
-2-struct rv_uprobe *rv_uprobe_attach(const char *binpath, loff_t offset, - int (*entry_fn)(struct rv_uprobe *p, struct pt_regs *regs, __u64*data),
include/rv/rv_uprobe.h +1
-3-int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), - void *priv); +int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe*p,
include/rv/rv_uprobe.h +1
-0+ int (*handler)(struct uprobe_consumer *self, struct pt_regs *regs,
__u64*data),include/rv/rv_uprobe.h +1
-0+ int (*ret_handler)(struct uprobe_consumer *self, unsigned longfunc,
include/rv/rv_uprobe.h +1
-0+ struct pt_regs *regs, __u64 *data));
/**
- *rv_uprobe_detach- synchronously unregister an uprobe and free it
- * @p: probe to detach; may be NULL (no-op)
- *
- * Callsuprobe_unregister_nosync(), thenuprobe_unregister_sync()to wait
- * for any in-progress handler to finish, then releases the path reference
- * and frees therv_uprobestruct. The caller's priv data is NOT freed.
+ *rv_uprobe_unregister- synchronously unregister a uprobe
+ * @p: probe to unregister; may be NULL (no-op)
*
- * When removing a single probe, prefer this over the three-phase API.
- * Safe to call from process context only (uprobe_unregister_sync()may
- * schedule).
+ * Dequeues the uprobe and waits synchronously for all in-flight handlers
+ * to complete.
*/include/rv/rv_uprobe.h +1
-1-void rv_uprobe_detach (struct rv_uprobe *p); +void rv_uprobe_unregister (struct rv_uprobe *p);
/**
*rv_uprobe_unregister_nosync- dequeue an uprobe without waitinginclude/rv/rv_uprobe.h +0
-3@@ -91,9 +64,7 @@ void rv_uprobe_detach(struct rv_uprobe *p); * * Removes the uprobe from the uprobe subsystem but does NOT wait for * in-flight handlers to complete. The caller must call rv_uprobe_sync() - * before calling rv_uprobe_free() on the same probe. - * - * Use this to batch multiple deregistrations before a single
rv_uprobe_sync().
+ * before freeing any container holding this probe. */
voidrv_uprobe_unregister_nosync(struct rv_uprobe *p);include/rv/rv_uprobe.h +0
-0@@ -101,19 +72,8 @@ void rv_uprobe_unregister_nosync(struct rv_uprobe *p); * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete * * Global barrier: waits for every in-flight uprobe handler across thesystem
- * to finish. Call once after a batch ofrv_uprobe_unregister_nosync()calls
- * and before anyrv_uprobe_free()call.
+ * to finish.
*/
voidrv_uprobe_sync(void);include/rv/rv_uprobe.h +0
-10-/** -* rv_uprobe_free - release resources of a previously deregistered probe -* @p: probe to free; may be NULL (no-op) -* -* Releases the path reference and frees the rv_uprobe struct. Must only -* be called after rv_uprobe_sync() has returned. The caller's priv data -* is NOT freed. -*/ -void rv_uprobe_free (struct rv_uprobe *p); -#endif /*
_RV_UPROBE_H*/kernel/trace/rv/monitors/tlob/tlob.c +2
-2@@ -252,8 +252,8 @@ struct tlob_uprobe_binding { char binpath[TLOB_MAX_PATH]; loff_t offset_start; loff_t offset_stop; -struct rv_uprobe *start_probe; -struct rv_uprobe *stop_probe; +DECLARE_RV_UPROBE (start_probe); +DECLARE_RV_UPROBE (stop_probe); };
/* RCU callback: free the slab once no readers remain. */kernel/trace/rv/monitors/tlob/tlob.c +0
-0@@ -512,16 +512,16 @@ int tlob_stop_task(struct task_struct *task) EXPORT_SYMBOL_GPL(tlob_stop_task);
-static int tlob_uprobe_entry_handler(structrv_uprobe*p, structpt_regs*regs,
+static int tlob_uprobe_entry_handler(structuprobe_consumer*self, structpt_regs*regs,
__u64*data)
{kernel/trace/rv/monitors/tlob/tlob.c +1
-1-struct tlob_uprobe_binding *b = p->priv; + struct tlob_uprobe_binding *b = container_of(self, struct
tlob_uprobe_binding, start_probe.uc);
tlob_start_task(current, b->threshold_ns);
return 0;
}
-static int tlob_uprobe_stop_handler(structrv_uprobe*p, structpt_regs*regs,
+static int tlob_uprobe_stop_handler(structuprobe_consumer*self, structpt_regs*regs,
__u64*data)
{
tlob_stop_task(current);kernel/trace/rv/monitors/tlob/tlob.c +1
-0@@ -537,6 +537,7 @@ static int tlob_add_uprobe(u64 threshold_ns, const char *binpath, { struct tlob_uprobe_binding *b, *tmp_b; char pathbuf[TLOB_MAX_PATH]; +struct inode *inode; struct path path; char *canon;int ret;
kernel/trace/rv/monitors/tlob/tlob.c +0
-0@@ -561,10 +562,12 @@ static int tlob_add_uprobe(u64 threshold_ns, constchar *binpath, goto err_path; }kernel/trace/rv/monitors/tlob/tlob.c +3
-1-/* Reject duplicate start offset for the same binary. */ + inode =d_real_inode (path.dentry); + +/* Reject duplicate start offset for the same binary inode. */
list_for_each_entry(tmp_b, &tlob_uprobe_list, list){ if (tmp_b->offset_start==offset_start&&kernel/trace/rv/monitors/tlob/tlob.c +1
-1- tmp_b->start_probe->path.dentry == path.dentry) { + tmp_b->start_probe.inode == inode) {ret =
-EEXIST;
gotoerr_path;
}kernel/trace/rv/monitors/tlob/tlob.c +0
-0@@ -577,29 +580,25 @@ static int tlob_add_uprobe(u64 threshold_ns, constchar *binpath, } strscpy(b->binpath, canon, sizeof(b->binpath));kernel/trace/rv/monitors/tlob/tlob.c +0
-1-/* Both probes share b (priv) and path; attach_path refs path itself. */
kernel/trace/rv/monitors/tlob/tlob.c +0
-2- b->start_probe = rv_uprobe_attach_path(&path, offset_start, - tlob_uprobe_entry_handler,NULL, b);
kernel/trace/rv/monitors/tlob/tlob.c +7
-5-if (IS_ERR (b->start_probe)) { - ret =PTR_ERR (b->start_probe); - b->start_probe =NULL ; - goto err_path; - } +path_put (&path); + +/* Both probes are registered directly on the embedded fields */ + ret =rv_uprobe_register (b->binpath, offset_start, &b->start_probe, + tlob_uprobe_entry_handler,NULL ); +if (ret) + goto err_free;kernel/trace/rv/monitors/tlob/tlob.c +0
-2- b->stop_probe = rv_uprobe_attach_path(&path, offset_stop, - tlob_uprobe_stop_handler,NULL, b);
kernel/trace/rv/monitors/tlob/tlob.c +3
-3-if (IS_ERR (b->stop_probe)) { - ret =PTR_ERR (b->stop_probe); - b->stop_probe =NULL ; + ret =rv_uprobe_register (b->binpath, offset_stop, &b->stop_probe, + tlob_uprobe_stop_handler,NULL ); +if (ret)goto
err_start;kernel/trace/rv/monitors/tlob/tlob.c +0
-1- }kernel/trace/rv/monitors/tlob/tlob.c +0
-1-path_put (&path);
list_add_tail(&b->list, &tlob_uprobe_list); return 0;
err_start:kernel/trace/rv/monitors/tlob/tlob.c +2
-1-rv_uprobe_detach (b->start_probe); +rv_uprobe_unregister (&b->start_probe); + goto err_free;
err_path:
path_put(&path);
err_free:kernel/trace/rv/monitors/tlob/tlob.c +1
-0@@ -611,21 +610,24 @@ static int tlob_remove_uprobe_by_key(loff_t offset_start, const char *binpath) { struct tlob_uprobe_binding *b, *tmp; struct path remove_path; +struct inode *inode; int ret;
ret =kern_path(binpath, LOOKUP_FOLLOW, &remove_path); if (ret)
return ret;kernel/trace/rv/monitors/tlob/tlob.c +2
-0+ inode =d_real_inode (remove_path.dentry); +ret =
-ENOENT;
list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list){ if (b->offset_start!=offset_start) continue;kernel/trace/rv/monitors/tlob/tlob.c +1
-1-if (b->start_probe->path.dentry != remove_path.dentry) +if (b->start_probe.inode != inode)continue;
list_del(&b->list);kernel/trace/rv/monitors/tlob/tlob.c +2
-2-rv_uprobe_detach (b->start_probe); -rv_uprobe_detach (b->stop_probe); +rv_uprobe_unregister (&b->start_probe); +rv_uprobe_unregister (&b->stop_probe);kfree(b);
ret = 0;
break;kernel/trace/rv/monitors/tlob/tlob.c +2
-2@@ -643,8 +645,8 @@ static void tlob_remove_all_uprobes(void) mutex_lock(&tlob_uprobe_mutex); list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) { list_move(&b->list, &pending); -rv_uprobe_unregister_nosync (b->start_probe); -rv_uprobe_unregister_nosync (b->stop_probe); +rv_uprobe_unregister_nosync (&b->start_probe); +rv_uprobe_unregister_nosync (&b->stop_probe); } mutex_unlock(&tlob_uprobe_mutex);kernel/trace/rv/monitors/tlob/tlob.c +0
-0@@ -658,8 +660,6 @@ static void tlob_remove_all_uprobes(void) rv_uprobe_sync();
list_for_each_entry_safe(b, tmp, &pending, list){kernel/trace/rv/monitors/tlob/tlob.c +0
-2-rv_uprobe_free (b->start_probe); -rv_uprobe_free (b->stop_probe);kfree(b);
}
}kernel/trace/rv/rv_uprobe.c +0
-0@@ -10,149 +10,74 @@ #include <linux/uprobes.h> #include <rv/rv_uprobe.h>kernel/trace/rv/rv_uprobe.c +0
-6-/* -* Private extension of struct rv_uprobe. Allocated by rv_uprobe_attach*() -* and returned to callers as &impl->pub. -*/ -struct rv_uprobe_impl { -struct rv_uprobe pub;/* must be first; callers hold &pub */
kernel/trace/rv/rv_uprobe.c +0
-3-struct uprobe_consumer uc; -struct uprobe *uprobe; - int (*entry_fn)(struct rv_uprobe *p, struct pt_regs *regs, __u64*data);
kernel/trace/rv/rv_uprobe.c +0
-8-int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data); -}; - -static int rv_uprobe_handler (struct uprobe_consumer *uc, - struct pt_regs *regs, __u64 *data) -{ - struct rv_uprobe_impl *impl = container_of(uc, struct
rv_uprobe_impl, uc);kernel/trace/rv/rv_uprobe.c +0
-11- -if (impl->entry_fn) -return impl->entry_fn (&impl->pub, regs, data); -return 0 ; -} - -static int rv_uprobe_ret_handler (struct uprobe_consumer *uc, -unsigned long func, -struct pt_regs *regs,__u64 *data) -{ - struct rv_uprobe_impl *impl = container_of(uc, struct
rv_uprobe_impl, uc);kernel/trace/rv/rv_uprobe.c +0
-9- -if (impl->ret_fn) -return impl->ret_fn (&impl->pub, func, regs, data); -return 0 ; -} - -static struct rv_uprobe * -__rv_uprobe_attach(struct inode *inode, struct path *path, loff_t offset, - int (*entry_fn)(struct rv_uprobe *p, struct pt_regs*regs,
__u64*data),kernel/trace/rv/rv_uprobe.c +0
-39- int (*ret_fn)(struct rv_uprobe *p, unsigned long func, - struct pt_regs *regs, __u64 *data), - void *priv) -{ -struct rv_uprobe_impl *impl; -int ret; - -if (!entry_fn && !ret_fn) -return ERR_PTR (-EINVAL ); - - impl =kzalloc_obj (*impl,GFP_KERNEL ); -if (!impl) -return ERR_PTR (-ENOMEM ); - - impl->pub.offset = offset; - impl->pub.priv = priv; - impl->entry_fn = entry_fn; - impl->ret_fn = ret_fn; -path_get (path); - impl->pub.path = *path; - -if (entry_fn) - impl->uc.handler = rv_uprobe_handler; -if (ret_fn) - impl->uc.ret_handler = rv_uprobe_ret_handler; - - impl->uprobe =uprobe_register (inode, offset,0 , &impl->uc); -if (IS_ERR (impl->uprobe)) { - ret =PTR_ERR (impl->uprobe); -path_put (&impl->pub.path); -kfree (impl); -return ERR_PTR (ret); - } - -return &impl->pub; -} - -/** -* rv_uprobe_attach_path - register an uprobe given an already-resolved path
- */kernel/trace/rv/rv_uprobe.c +0
-2-struct rv_uprobe *rv_uprobe_attach_path(struct path *path, loff_t offset, - int (*entry_fn)(struct rv_uprobe *p, struct pt_regs *regs, __u64*data),
kernel/trace/rv/rv_uprobe.c +0
-7-int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), - void *priv) -{ - struct inode *inode = d_real_inode(path->dentry); - - return __rv_uprobe_attach(inode, path, offset, entry_fn, ret_fn,priv);
kernel/trace/rv/rv_uprobe.c +0
-3-} -EXPORT_SYMBOL_GPL (rv_uprobe_attach_path); -/**
- *rv_uprobe_attach- resolve binpath and register an uprobe
+ *rv_uprobe_register- resolve binpath and register an uprobe */kernel/trace/rv/rv_uprobe.c +0
-2-struct rv_uprobe *rv_uprobe_attach(const char *binpath, loff_t offset, - int (*entry_fn)(struct rv_uprobe *p, struct pt_regs *regs, __u64*data),
kernel/trace/rv/rv_uprobe.c +1
-3-int (*ret_fn)(struct rv_uprobe *p,unsigned long func, -struct pt_regs *regs,__u64 *data), - void *priv) +int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe*p,
kernel/trace/rv/rv_uprobe.c +1
-0+ int (*handler)(struct uprobe_consumer *self, struct pt_regs *regs,
__u64*data),kernel/trace/rv/rv_uprobe.c +1
-0+ int (*ret_handler)(struct uprobe_consumer *self, unsigned longfunc,
kernel/trace/rv/rv_uprobe.c +1
-0+ struct pt_regs *regs, __u64 *data)){
kernel/trace/rv/rv_uprobe.c +1
-1-struct rv_uprobe *p; +struct inode *inode;struct path path;
int ret;kernel/trace/rv/rv_uprobe.c +3
-0+if (!handler && !ret_handler) +return -EINVAL ; +ret =
kern_path(binpath, LOOKUP_FOLLOW, &path); if (ret)kernel/trace/rv/rv_uprobe.c +1
-1-return ERR_PTR (ret); +return ret;
if (!d_is_reg(path.dentry)) {
path_put(&path);kernel/trace/rv/rv_uprobe.c +1
-1-return ERR_PTR (-EINVAL ); +return -EINVAL ;}
kernel/trace/rv/rv_uprobe.c +7
-1- p =rv_uprobe_attach_path (&path, offset, entry_fn, ret_fn, priv); + inode =d_real_inode (path.dentry); + + p->uc.handler = handler; + p->uc.ret_handler = ret_handler; + p->inode = inode; + + p->uprobe =uprobe_register (inode, offset,0 , &p->uc);
path_put(&path);kernel/trace/rv/rv_uprobe.c +9
-1-return p; + +if (IS_ERR (p->uprobe)) { + ret =PTR_ERR (p->uprobe); + p->uprobe =NULL ; + p->inode = NULL; +return ret; + } + +return 0 ;}
kernel/trace/rv/rv_uprobe.c +1
-1-EXPORT_SYMBOL_GPL (rv_uprobe_attach); +EXPORT_SYMBOL_GPL (rv_uprobe_register);
/**
- *rv_uprobe_detach- synchronously unregister an uprobe and free it
+ *rv_uprobe_unregister- synchronously unregister a uprobe */kernel/trace/rv/rv_uprobe.c +1
-1-void rv_uprobe_detach (struct rv_uprobe *p) +void rv_uprobe_unregister (struct rv_uprobe *p){
kernel/trace/rv/rv_uprobe.c +1
-1-if (!p) +if (!p ||IS_ERR_OR_NULL (p->uprobe))return;
rv_uprobe_unregister_nosync(p);
rv_uprobe_sync();kernel/trace/rv/rv_uprobe.c +0
-1-rv_uprobe_free (p);}
kernel/trace/rv/rv_uprobe.c +1
-1-EXPORT_SYMBOL_GPL (rv_uprobe_detach); +EXPORT_SYMBOL_GPL (rv_uprobe_unregister);
/**
*rv_uprobe_unregister_nosync- dequeue an uprobe without waiting */
voidrv_uprobe_unregister_nosync(struct rv_uprobe *p)
{kernel/trace/rv/rv_uprobe.c +1
-3-struct rv_uprobe_impl *impl; - -if (!p) +if (!p ||IS_ERR_OR_NULL (p->uprobe))return;
kernel/trace/rv/rv_uprobe.c +3
-2- impl = container_of(p, struct rv_uprobe_impl, pub); -uprobe_unregister_nosync (impl->uprobe, &impl->uc); +uprobe_unregister_nosync (p->uprobe, &p->uc); + p->uprobe =NULL ; + p->inode = NULL;}
EXPORT_SYMBOL_GPL(rv_uprobe_unregister_nosync);kernel/trace/rv/rv_uprobe.c +0
-16@@ -164,19 +89,3 @@ void rv_uprobe_sync(void) uprobe_unregister_sync(); } EXPORT_SYMBOL_GPL(rv_uprobe_sync); - -/** -* rv_uprobe_free - release resources of a previously deregistered probe -*/ -void rv_uprobe_free (struct rv_uprobe *p) -{ -struct rv_uprobe_impl *impl; - -if (!p) -return ; - - impl = container_of(p, struct rv_uprobe_impl, pub); -path_put (&p->path); -kfree (impl); -} -EXPORT_SYMBOL_GPL (rv_uprobe_free);
Re: [PATCH v3 2/9] rv: add generic uprobe infrastructure for RV monitors
Quoted 71 lines · Gabriele Monaco
Quoted 25 lines · Wen Yang
On [2-1]~[2-5]: embedded consumer causes UAF on
PREEMPT_RT
-----------------------------------------------------------The
uprobe_bindselftest oopses onPREEMPT_RT(full):
handler_chain+0xc9: mov rax, [r15+0x18] ; advance list iterator
RAX: 000015ec00001f28 ; garbage —&uprobe->consumersafter kfree
handler_chain()readsuc->cons_node.nextafteruc->handler()returns, still inside rcu_read_lock_trace(). That pointer is&uprobe->consumers(the list head embedded in struct uprobe), which gets freed through:
put_uprobe()
-> schedule_work(uprobe_free_deferred) /* async */ ->call_srcu(&uretprobes_srcu, ...)
->call_rcu_tasks_trace(kfree_uprobe)-> kfree(uprobe)
rv_uprobe_sync()callsuprobe_unregister_sync()which callssynchronize_srcu(&uretprobes_srcu), but that only matters after the kworker has submitted work to uretprobes_srcu. On a loadedPREEMPT_RTbox the kworker may not have run yet, sosynchronize_srcu()returns immediately and kfree(uprobe) races with the still-iteratinghandler_chain():Quoted 5 lines
CPU A CPU B
consumer_del()→ list_del_rcurcu_read_lock_trace()put_uprobe() → schedule_workuc->handler()returns rv_uprobe_sync(): reading cons_node.next...synchronize_srcu(&uretprobes_srcu)If CPU B is in an RCU trace read-side critical section this doesn't return immediately, it returns after readers are done, doesn't it? (well, what you really care here is the
synchronize_rcu_tasks_trace()but we do both).Quoted 5 lines
← idle; returns immediately
[kworker fires later]
kfree(uprobe) ← frees&uprobe->consumers
cons_node.next = freed mem → CRASHSo I had a bit of a look and as far as I understand, we don't have any control over the uprobe allocation pattern (workqueues and whatnot) and we don't really care as long as we deregister it appropriately.
What we do control is the
uprobe_consumer, that must be freed only after the uprobe was synchronously deregistered, that should guarantee no reader is going to reference it, shouldn't it?
uprobe_unregister_nosync()removes it from thecons_nodelist, so it's safe to assume anyhandler_chain()after the next RCU-trace grace period won't see it.In the sketch I sent this is happening (all kfree(b) are after
rv_uprobe_unregister()which does the sync). What am I missing here?uprobe is also freed after both grace periods, so no reader should use that either.
The situation you're seeing isn't fully clear to me, I applied my sketch and don't see any splat on a vng box.
Quoted 6 lines
With uc embedded in the binding (as [2-1] suggests), no amount of delaying kfree(binding) helps:
uprobe->consumersis freed by a chain we don't control. The fix is to keepuc->cons_nodein memory that outlives thehandler_chain()iteration, which means a separate allocation freed only afterrv_uprobe_sync():
Quoted 8 lines
Quoted 3 lines
rv_uprobe_unregister_nosync() /*
list_del_rcu+schedule_work*/ rv_uprobe_sync() /* waits for any already-submitted
srcu work */We of course need to do any free after this sync, but I don't get why we need an additional allocation since the following are both plain synchronous frees, why isn't a single one (on b) enough?
Thank you for the patient explanation.
You are right, and v4 implements the embedded approach. struct rv_uprobe directly embeds struct uprobe_consumer:
struct rv_uprobe {
struct uprobe_consumer uc;
struct uprobe *uprobe;
struct inode *inode;
void *priv;
int (*handler)(...);
int (*ret_handler)(...);
};rv_uprobe_free() is gone — no allocation means no explicit free. Afterrv_uprobe_unregister() (or rv_uprobe_unregister_nosync() + rv_uprobe_sync()), the caller frees the containing struct directly. In tlob:
rv_uprobe_unregister_nosync(&b->start_probe); rv_uprobe_unregister_nosync(&b->stop_probe); rv_uprobe_sync(); kfree(b); /* frees both embedded consumers */
This is the pattern from your sketch.
Regarding my earlier reply that argued for the separate allocation: I was wrong. The key barrier is synchronize_rcu_tasks_trace() (called first in uprobe_unregister_sync()), which waits for all rcu_read_lock_trace() readers including handler_chain(). After it returns, no cons_node.next read is in flight and embedding is safe.
We appreciate your thorough review. All of your comments have been addressed in v4. We'll run local tests for one or two days, and then it will be sent out shortly.
--
Best wishes,
Wen
Gabriele Monaco
maintainerRe: [PATCH v3 2/9] rv: add generic uprobe infrastructure for RV monitors
Quoted 4 lines · Wen Yang
Thank you for the patient explanation.
You are right, and v4 implements the embedded approach. structrv_uprobedirectly embeds structuprobe_consumer:
Quoted 8 lines
struct
rv_uprobe{
struct uprobe_consumer uc;
struct uprobe *uprobe;
struct inode *inode;
void *priv;
int (*handler)(...);
int (*ret_handler)(...);
};
Alright, great. Why are you still using double function pointers? Do you really need rv_uprobe->handler instead of just using rv_uprobe->uc.handler ? That also simplifies one function call down the road.
Thanks,
Gabriele
Quoted 27 lines
rv_uprobe_free()is gone — no allocation means no explicit free. Afterrv_uprobe_unregister()(orrv_uprobe_unregister_nosync()+rv_uprobe_sync()), the caller frees the containing struct directly. In tlob:
rv_uprobe_unregister_nosync(&b->start_probe);rv_uprobe_unregister_nosync(&b->stop_probe);rv_uprobe_sync();
kfree(b); /* frees both embedded consumers */This is the pattern from your sketch.
Regarding my earlier reply that argued for the separate allocation: I was wrong. The key barrier is
synchronize_rcu_tasks_trace()(called first inuprobe_unregister_sync()), which waits for allrcu_read_lock_trace()readers including handler_chain(). After it returns, no cons_node.next read is in flight and embedding is safe.We appreciate your thorough review. All of your comments have been addressed in v4.
We'll run local tests for one or two days, and then it will be sent out shortly.--
Best wishes,
Wen