Skip to contentSkip to conversation

rv/tlob: Add task latency over budget RV monitor

PATCH Superseded [email protected] 2 participants 25 messages Jun 7 – Jul 1

Subsystems (4)

v3 of 8 Superseded Latest is v8 →

lore
Share & tools
lore
#0 lore raw

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/monitor

Two 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
#1 lore raw

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
index 34b8fba9ecd4..eb7fc02ecb8a 100644
@@ -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
index 8ead8783c29f..ac4d334e757f 100644
@@ -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_MANUAL
 typedef 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
#3 lore raw

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
new file mode 100644
index 000000000000..9106c5c9275e
@@ -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
index 3884b14df375..e2e0033a00b9 100644
@@ -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
index 94498da35b37..f139b904bea3 100644
@@ -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
#9 lore raw

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
new file mode 100644
index 000000000000..a1834daff2ed
@@ -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
#10 lore raw

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
index 28d3c74cabfc..e5860900a337 100644
@@ -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
#12 lore raw

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
index e5860900a337..610da54c111f 100644
@@ -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
#14 lore raw

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
index 29769f06bb0f..1501545b5f08 100644
@@ -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
index e2e0033a00b9..ed2de31d0312 100644
@@ -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
index f139b904bea3..ae59e97f8682 100644
@@ -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
new file mode 100644
index 000000000000..b29a375de228
@@ -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
index 9622c269789c..a4bc215c1f15 100644
@@ -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
#16 lore raw

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
index ae59e97f8682..316d53398345 100644
@@ -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
new file mode 100644
index 000000000000..35d313dfc20d
@@ -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
index b29a375de228..7ec3326640c2 100644
@@ -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
#18 lore raw

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
index 18f7fe324e2f..055747cef38a 100755
@@ -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
#20 lore raw

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
index 2659417cb2c7..cbbd03ee16c7 100644
@@ -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
index aa8790c22a71..0b32bdfdb8db 100644
@@ -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
new file mode 100644
index 000000000000..29b3519b255f
@@ -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
new file mode 100644
index 000000000000..0b4c5e4344d2
@@ -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
new file mode 100644
index 000000000000..1ac3db6ca7bb
@@ -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
new file mode 100644
index 000000000000..a143635a60ce
@@ -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
author
#24 lore raw
In reply to [email protected]

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 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/monitor

Two 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

maintainer
#2 lore raw

Re: [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_SIZE must 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 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.
+ */

Quoted 4 lines

+#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"

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 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.
+ */

(quoted diff) +2 -0
+struct da_per_obj_pool {
+	struct da_monitor_storage  *storage;  /* non-NULL ⟹ pool

mode */

(quoted diff) +1 -0
+	struct da_monitor_storage **free;     /* kmalloc'd pointer

stack */

(quoted diff) +2 -0
+	unsigned int                free_top;
+	unsigned int                capacity; /* total number of

slots */

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, 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)
 {

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 -0
  	kfree_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-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);

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 guard

exits) have

(quoted diff) +1 -0
+	 * completed before we begin tearing down the pool.  Mirrors

the 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 may

safely

(quoted diff) +1 -0
+	 * call ha_cancel_timer_sync() while the monitor is still

reachable.

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 free

callbacks

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

maintainer
#11 lore raw

Re: [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 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 */

Quoted 3 lines

Apply the same fix to ha_invariant_passed_jiffy().

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

[1] -
https://lore.kernel.org/lkml/08188c28f274da63a3f8549add3086a92aef45e5.1780908661.git.namcao@linutronix.de

Quoted 66 lines

---
 include/rv/ha_monitor.h | 17 +++++++++++++----

 1 file changed, 13 insertions(+), 4 deletions(-)
include/rv/ha_monitor.h +0 -0
index 28d3c74cabfc..e5860900a337 100644
@@ -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) {

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 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) {

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;

maintainer Reviewed-by on v3 5/9
#13 lore raw

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_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 +1 -0
index e5860900a337..610da54c111f 100644
@@ -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
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
 #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

include/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


 #ifdef CONFIG_RV_REACTORS

maintainer
#15 lore raw

Re: [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_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".

Quoted 6 lines

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.

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 ;)

[1] - https://www.kernel.org/doc/html/latest/process/submitting-patches.html#using-reported-by-tested-by-reviewed-by-suggested-by-and-fixes

Quoted 2 lines

---

...

Quoted 10 lines
kernel/trace/rv/Kconfig +0 -0
index e2e0033a00b9..ed2de31d0312 100644
@@ -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 here

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
new file mode 100644
index 000000000000..d8e0c4794720
@@ -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 includes da_monitor.h and expands __da_monitor_init_pool
+ * using this macro).  Placing the define before tlob.h or after
+ * ha_monitor.h both 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 via ha_monitor.h) expands da_extra_cleanup inside
+ * da_monitor_destroy().  The full definition follows after ha_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), emits detail_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_state time

kernel/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, enum

events 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, enum

events 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, enum

events 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 whenever next_state is 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 new ha_start_timer_ns() call.
+ *
+ * Guard on stopping: sched_switch events can arrive after ha_cancel_timer_sync,
+ * restarting the timer and triggering an ODEBUG "activate active" splat.
+ * The _acquire pairs with the cmpxchg_release in tlob_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, enum

events 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 by da_monitor_destroy().
+ *
+ * Claims cleanup ownership via CAS; cancels the budget timer; decrements the
+ * monitored-task counter; and schedules the slab free via call_rcu().
+ * Must run before da_monitor_reset() (i.e. before hash_del_rcu()) so that
+ * ha_cancel_timer_sync() can safely access the still-registered ha_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_lock is 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 bool tlob_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 bool tlob_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's running_ns and next's waiting_ns share the same context- switch
+ * timestamp; neither absorbs handler overhead into its accumulator.
+ *
+ * No waiting->sleeping edge exists: a task can only block voluntarily
+ * (call schedule()) while it is executing on CPU, which corresponds to
+ * the running DA state.  A task in the waiting state is TASK_RUNNING in
+ * 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 bool tlob_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 in do_exit() context; the task still has a valid pid here.
+ * tlob_stop_task() returns -ESRCH if the task is not monitored, which is fine.
+ */
+static void handle_sched_process_exit(void *data, struct task_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_cleanup

kernel/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
new file mode 100644
index 000000000000..b6724e629c69
@@ -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

maintainer
#4 lore raw

Re: [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 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.

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 where kfree() precedes uprobe_unregister_sync().

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
index 9106c5c927..4fb3f50a63 100644
@@ -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

maintainer
#19 lore raw

Re: [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.

---
 tools/testing/selftests/verification/verificationtest-ktap | 4 +++-

 1 file changed, 3 insertions(+), 1 deletion(-)
(quoted diff) +0 -0

ktap b/tools/testing/selftests/verification/verificationtest-ktap index 18f7fe324e2f..055747cef38a 100755

tools/testing/selftests/verification/verificationtest-ktap +0 -0
@@ -5,4 +5,6 @@
  #
  # Copyright (C) Arm Ltd., 2023


-../ftrace/ftracetest -K -v --rv ../verification

tools/testing/selftests/verification/verificationtest-ktap +3 -0
+dir=$(realpath "$(dirname "$0")")
+testdir=$(cd "$dir" && realpath "${1:-.}")
+"$dir/../ftrace/ftracetest" -K -v --rv "$testdir"
maintainer
#21 lore raw

Re: [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_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).

Quoted 6 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 +

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
index 0a56bf209f..5d1b1f9380 100755
@@ -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
maintainer
#17 lore raw

Re: [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");
maintainer
#22 lore raw

Re: [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_target provides 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).

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.c b/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c new 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.c b/tools/testing/selftests/verification/test.d/tlob/tlob_target.c new file mode 100644
index 000000000000..0fdbc575d71d

tools/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 the detail_env_tlob ns breakdown:

Quoted 8 lines

+ *
+ *   tlob_busy_work    / tlob_busy_work_done    - busy-spin:
running_ns dominates
+ *   tlob_sleep_work   / tlob_sleep_work_done   - nanosleep:
sleeping_ns dominates
+ *   tlob_preempt_work / tlob_preempt_work_done - busy-spin:
waiting_ns dominates
+ *                                                (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..1ac3db6ca7bb

tools/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 monitor

file, 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_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

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

maintainer
#23 lore raw

Re: [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/trace

diff --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 c

tools/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_tlob

and detail_env_tlob fire 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_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
tools/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 || true

tools/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 hrtimer

callback,
+# or the triggering sched event name when detected by the constraint path on a
+# preemption that races with the timer (common on PREEMPT_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 correct

threshold

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
author
#5 lore raw

Re: [PATCH v3 2/9] rv: add generic uprobe infrastructure for RV monitors

In reply to Gabriele Monaco
Quoted 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 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.

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 where kfree() precedes uprobe_unregister_sync().

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 by
rv_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
index 9106c5c927..4fb3f50a63 100644
@@ -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
index d8e0c47947..28a6c740c7 100644
@@ -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
index 3d8b764dde..69b8b0c27e 100644
@@ -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);
maintainer
#6 lore raw

Re: [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_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():

Quoted 5 lines

   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)

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->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 */

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 by rv_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 555 lines

Gabriele

**Suggested simplification:**

---

include/rv/rv_uprobe.h +0 -0
index 9106c5c927..4fb3f50a63 100644
@@ -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 {

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_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.
- */

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/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. */

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 long

func,

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)
- *
- * 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.
   */

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 waiting

include/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. */
  void rv_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 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);

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
index d8e0c47947..28a6c740c7 100644
@@ -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(struct rv_uprobe *p, struct pt_regs *regs,
+static int tlob_uprobe_entry_handler(struct uprobe_consumer *self, struct pt_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(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);

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, const
 char *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;
   goto err_path;
   }

kernel/trace/rv/monitors/tlob/tlob.c +0 -0
@@ -577,29 +580,25 @@ static int tlob_add_uprobe(u64 threshold_ns, const
 char *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
index 3d8b764dde..69b8b0c27e 100644
@@ -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 long

func,

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 */
  void rv_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);
author
#7 lore raw

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_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():

Quoted 5 lines

   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)

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 6 lines

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():

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. After
rv_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

maintainer
#8 lore raw

Re: [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. struct rv_uprobe directly embeds struct uprobe_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.  After rv_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