Skip to contentSkip to conversation

rv/tlob: Add task latency over budget RV monitor

PATCH Superseded [email protected] 2 participants 22 messages Jul 8 – Aug 19

Subsystems (4)

v4 of 8 Superseded Latest is v8 →

lore
Share & tools
lore
#0 lore raw

v4 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 emits an error when the elapsed time exceeds a configurable budget.

The monitor tracks three states (running, waiting, sleeping) driven by sched_switch and sched_wakeup tracepoints. A single clock invariant, clk_elapsed < BUDGET_NS(), is enforced by a per-task hrtimer (HRTIMER_MODE_REL_HARD). On expiry, error_env_tlob is emitted, followed by detail_env_tlob carrying a per-state time breakdown (running_ns, waiting_ns, sleeping_ns).

Tasks are registered for monitoring by writing to the tracefs monitor file:

  echo "p /path/to/binary:OFFSET_START OFFSET_STOP threshold=NS" \
       > /sys/kernel/tracing/rv/monitors/tlob/monitor

Two uprobes delimit the measured section without modifying the target binary. Multiple uprobe pairs can be active simultaneously.

Series structure
----------------
Patch 1: rv/da: introduce DA_MON_ALLOCATION_STRATEGY
Compile-time allocation strategy selector for per-object DA storage. Three strategies: DA_ALLOC_AUTO (default kmalloc), DA_ALLOC_POOL (pre-allocated llist pool), DA_ALLOC_MANUAL (caller-managed).

Patch 2: rv: add generic uprobe infrastructure for RV monitors
Thin wrapper over uprobe_consumer for path resolution, registration, and safe synchronous teardown.

Patch 3: rv/tlob: add tlob model DOT file
Graphviz DOT specification of the tlob hybrid automaton.

Patch 4: rv/ha: fix ha_invariant_passed_ns silent bypass
Without this fix, ha_invariant_passed_ns() returns 0 on first call and leaves env_store at U64_MAX. Every subsequent state transition resets the hrtimer to the full budget instead of the remaining time, so the budget never expires for tasks that transition between states. This causes tlob's detail_sleeping and detail_waiting selftests to hang.

This patch is a minimal fix in the current dual-representation framework. Nam's series [1] refactors ha_monitor.h to a single-representation model; once it lands, tlob will need to be updated to the new API and this patch will be superseded by Gabriele's planned framework-level initialisation improvement. We carry it here to keep the series self-contained and testable.

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

Patches 5: rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL
#ifndef guards for da_monitor_reset_hook and EVENT_NONE_LBL.

Patch 6: rv/tlob: add tlob hybrid automaton monitor
Main tlob implementation.

Patches 7-8: Tests
KUnit tests for the uprobe-line parser; seven ftrace-style selftests.

Changes since v3
----------------
Patch 1 (rv/da):
- Pool redesigned from spinlock+pointer-stack to lock-free llist (cmpxchg-based): pool release from RCU callback context is now safe without acquiring a lock
- DA_MON_POOL_SIZE alone selects pool mode; no separate DA_MON_ALLOCATION_STRATEGY define required for monitors
- All strategy dispatch uses plain C if() (no ifdeffery in functions)
- da_monitor_destroy_pool() calls da_monitor_reset_all() and da_monitor_sync_hook() before the hash iteration, preventing hrtimer UAF when timer callbacks are still in flight

Patch 2 (rv_uprobe):
- struct rv_uprobe now embeds struct uprobe_consumer directly; no separate heap allocation per probe, no rv_uprobe_free()
- path_put() moved after uprobe_register() to keep the inode referenced across the call as required by the uprobe API

Patch 6 (tlob monitor):
- da_get_target_by_id() wrapped in scoped_guard(rcu) in tlob_start_task(): hash_for_each_possible_rcu() requires an explicit RCU read-side CS on PREEMPT_RT where spinlock_t does not provide one
- tlob_monitor_read() releases mutex before simple_read_from_buffer() to avoid holding the mutex across copy_to_user()
- kmem_cache replaced with a pre-allocated llist pool for tlob_task_state, eliminating GFP_ATOMIC in the measurement window
- EXPORT_SYMBOL_IF_KUNIT added for tlob_parse_uprobe_line() and tlob_parse_remove_line() (required when CONFIG_TLOB_KUNIT_TEST=m)
- tlob_parse_remove_line() rejects negative offsets
- Unnecessary includes removed; Kconfig entry placed after the deadline monitors marker

Patch 7 (KUnit):
- Tests now call tlob_parse_uprobe_line() and tlob_parse_remove_line() directly; no tracepoint enrollment, no uprobe or filesystem state

Patch 8 (selftests):
- ftracetest walk-up algorithm (Gabriele's suggestion) incorporated; the separate verificationtest-ktap fix patch is dropped and the series shrinks from 9 to 8 patches
- All test scripts use "! cmd || false" throughout

Testing
-------
Tested on x86_64 with CONFIG_PREEMPT_RT=y (kernel 7.1 + virtme-ng):
- All KUnit tests pass
- All 7 selftests pass (uprobe_bind, uprobe_violation, uprobe_no_event, uprobe_multi, uprobe_detail_{running,sleeping,waiting})

Wen Yang (8):
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: add tlob selftests

 Documentation/trace/rv/index.rst              |   1 +
 Documentation/trace/rv/monitor_tlob.rst       | 177 ++++
 include/rv/da_monitor.h                       | 247 ++++-
 include/rv/ha_monitor.h                       |  24 +-
 include/rv/rv_uprobe.h                        |  93 ++
 kernel/trace/rv/Kconfig                       |   8 +
 kernel/trace/rv/Makefile                      |   3 +
 kernel/trace/rv/monitors/nomiss/nomiss.c      |   6 +-
 kernel/trace/rv/monitors/tlob/.kunitconfig    |   8 +
 kernel/trace/rv/monitors/tlob/Kconfig         |  19 +
 kernel/trace/rv/monitors/tlob/tlob.c          | 970 ++++++++++++++++++
 kernel/trace/rv/monitors/tlob/tlob.h          | 153 +++
 kernel/trace/rv/monitors/tlob/tlob_kunit.c    | 139 +++
 kernel/trace/rv/monitors/tlob/tlob_trace.h    |  52 +
 kernel/trace/rv/rv_trace.h                    |   1 +
 kernel/trace/rv/rv_uprobe.c                   | 104 ++
 tools/testing/selftests/ftrace/ftracetest     |  18 +-
 .../testing/selftests/verification/.gitignore |   2 +
 tools/testing/selftests/verification/Makefile |  19 +-
 .../verification/test.d/tlob/Makefile         |  28 +
 .../test.d/tlob/run_tlob_tests.sh             |  90 ++
 .../verification/test.d/tlob/tlob_sym.c       | 209 ++++
 .../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 ++
 tools/verification/models/tlob.dot            |  22 +
 31 files changed, 2839 insertions(+), 46 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 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
 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.25.1
#1 lore raw

PATCH v4 1/8 rv/da: introduce DA_MON_ALLOCATION_STRATEGY

From: Wen Yang <[email protected]>

Per-object DA storage allocation is currently limited to kmalloc on demand. Add a compile-time selector so monitors can choose among three strategies:

DA_ALLOC_AUTO (default) - kmalloc per object on the monitor path
DA_ALLOC_POOL - pre-allocated fixed-size llist pool;
selected by defining DA_MON_POOL_SIZE
DA_ALLOC_MANUAL - caller pre-inserts storage; framework
only links the target field

The pool strategy uses a lock-free llist (cmpxchg, no spinlock) so pool release is safe from RCU callback context without acquiring a lock. Moving allocation before the measurement window also prevents kmalloc latency.

nomiss is updated to DA_ALLOC_MANUAL.

---
 include/rv/da_monitor.h                  | 247 +++++++++++++++++++----
 include/rv/ha_monitor.h                  |   6 +
 kernel/trace/rv/monitors/nomiss/nomiss.c |   6 +-
 3 files changed, 221 insertions(+), 38 deletions(-)
include/rv/da_monitor.h +212 -35
index 34b8fba9ecd4..9c9acc123e3b 100644
@@ -14,7 +14,56 @@
 #ifndef _RV_DA_MONITOR_H
 #define _RV_DA_MONITOR_H
 
+/*
+ * Allocation strategies for RV_MON_PER_OBJ monitors.
+ *
+ * Select the strategy with a single define before including this header:
+ *
+ *   #define DA_MON_POOL_SIZE N          - pool mode; N pre-allocated slots.
+ *                                         Implies DA_ALLOC_POOL automatically.
+ *   #define DA_MON_ALLOCATION_STRATEGY \
+ *           DA_ALLOC_MANUAL             - manual mode (see below).
+ *   (neither)                           - auto mode (default).
+ *
+ * Do not define both DA_MON_POOL_SIZE and DA_MON_ALLOCATION_STRATEGY.
+ *
+ * DA_ALLOC_AUTO   - lock-free kmalloc on the hot path; unbounded capacity.
+ * DA_ALLOC_POOL   - pre-allocated fixed-size pool; set by defining DA_MON_POOL_SIZE.
+ * 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
+
+#ifdef DA_MON_POOL_SIZE
+#ifdef DA_MON_ALLOCATION_STRATEGY
+#error "Define only one of DA_MON_POOL_SIZE or DA_MON_ALLOCATION_STRATEGY"
+#endif
+#if DA_MON_POOL_SIZE == 0
+#error "DA_MON_POOL_SIZE must be non-zero"
+#endif
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL
+#endif
+
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#endif
+
+/*
+ * Provide a zero default so da_monitor_init() can reference
+ * DA_MON_POOL_SIZE in a plain C if() without an #if guard; the
+ * compiler eliminates the dead branch.
+ */
+#ifndef DA_MON_POOL_SIZE
+#if DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL
+#error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be defined and non-zero"
+#endif
+#define DA_MON_POOL_SIZE 0
+#endif
+
 #include <rv/automata.h>
+#include <linux/llist.h>
 #include <linux/rv.h>
 #include <linux/stringify.h>
 #include <linux/bug.h>
@@ -66,6 +115,16 @@ static struct rv_monitor rv_this;
 #define da_monitor_sync_hook()
 #endif
 
+/*
+ * Per-object teardown hook, called after da_monitor_reset_all() +
+ * da_monitor_sync_hook() and before hash_del_rcu() for each entry.
+ * All HA timer callbacks have completed at this point.
+ * Define before including this header.  Default: 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
@@ -404,6 +463,12 @@ struct da_monitor_storage {
 	union rv_task_monitor rv;
 	struct hlist_node node;
 	struct rcu_head rcu;
+	/*
+	 * Mutually exclusive with rcu: rcu is live during the RCU callback
+	 * flight; free_node when the slot is in da_pool_free_list.
+	 * Present in all monitors to avoid #if-gating the pool helpers.
+	 */
+	struct llist_node free_node;
 };
 
 #ifndef DA_MONITOR_HT_BITS
@@ -495,18 +560,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 +590,79 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
 	return mon_storage->target;
 }
 
+/*
+ * Lock-free llist (cmpxchg) rather than kmem_cache/mempool: on
+ * PREEMPT_RT spinlock_t becomes a sleeping lock, which is forbidden
+ * in the rcuc kthread context where RCU callbacks run.
+ *
+ * Multiple producers (any context, any CPU) call llist_add; a single
+ * consumer (llist_del_first, serialised by the monitor's start lock)
+ * needs no additional synchronisation.
+ *
+ * Per-TU statics: each PER_OBJ monitor gets its own pool instance;
+ * da_pool_storage and da_pool_free_list are NULL/empty and the pool
+ * paths are dead code for non-pool monitors.
+ */
+static struct da_monitor_storage *da_pool_storage;
+static LLIST_HEAD(da_pool_free_list);
+
+static void da_pool_return_cb(struct rcu_head *head)
+{
+	struct da_monitor_storage *ms =
+		container_of(head, struct da_monitor_storage, rcu);
+
+	llist_add(&ms->free_node, &da_pool_free_list);
+}
+
+/*
+ * da_create_pool_storage - pop a free pool slot and insert it into the hash.
+ *
+ * Returns the new da_monitor, or NULL if the pool is exhausted.  Finding
+ * an existing entry for the same id fires WARN_ON_ONCE (double-start bug).
+ *
+ * Caller must hold an RCU read-side CS and the monitor's serialisation lock.
+ */
+static inline struct da_monitor *
+da_create_pool_storage(da_id_type id, monitor_target target,
+		       struct da_monitor *da_mon)
+{
+	struct da_monitor_storage *mon_storage, *existing;
+	struct llist_node *node;
+
+	if (da_mon)
+		return da_mon;
+
+	node = llist_del_first(&da_pool_free_list);
+	if (!node)
+		return NULL;
+	mon_storage = llist_entry(node, struct da_monitor_storage, free_node);
+
+	mon_storage->id = id;
+	mon_storage->target = target;
+
+	/*
+	 * The caller's serialization lock ensures llist_del_first() is
+	 * single-consumer, so no concurrent start for the same id is possible.
+	 * Reaching here indicates a programming error (double-start for the
+	 * same pid).
+	 */
+	existing = __da_get_mon_storage(id);
+	if (WARN_ON_ONCE(existing)) {
+		llist_add(&mon_storage->free_node, &da_pool_free_list);
+		return NULL;
+	}
+	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 +675,10 @@ 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);
-	kfree_rcu(mon_storage, rcu);
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+		call_rcu(&mon_storage->rcu, da_pool_return_cb);
+	else
+		kfree_rcu(mon_storage, rcu);
 }
 
 static void __da_monitor_reset_all(void (*reset)(struct da_monitor *))
@@ -581,41 +701,98 @@ static inline void da_monitor_reset_state_all(void)
 	__da_monitor_reset_all(da_monitor_reset_state);
 }
 
+/* Not part of the public API; called only by da_monitor_init(). */
+static inline int __da_monitor_init_pool(unsigned int prealloc_count)
+{
+	unsigned int i;
+
+	da_pool_storage = kcalloc(prealloc_count, sizeof(*da_pool_storage),
+				  GFP_KERNEL);
+	if (!da_pool_storage)
+		return -ENOMEM;
+
+	for (i = 0; i < prealloc_count; i++)
+		llist_add(&da_pool_storage[i].free_node, &da_pool_free_list);
+	return 0;
+}
+
+/*
+ * da_monitor_init - initialise the per-object monitor
+ */
 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);
 	return 0;
 }
 
+/*
+ * da_monitor_destroy - tear down the per-object monitor
+ *
+ * tracepoint_synchronize_unregister() flushes all in-flight tracepoint
+ * handlers and performs synchronize_rcu(), so no RCU reader holds a
+ * reference to any da_monitor_storage after it returns.
+ * da_monitor_reset_all() disables monitoring; combined with
+ * da_monitor_sync_hook() (synchronize_rcu() for HA), no timer callback
+ * can fire or be re-armed after this sequence.
+ *
+ * Pool mode: remaining hash entries are returned to the free list
+ * directly (no call_rcu needed -- see above).  rcu_barrier() drains any
+ * da_pool_return_cb() callbacks queued by earlier da_destroy_storage()
+ * calls before the backing array is freed.
+ */
 static inline void da_monitor_destroy(void)
 {
-	struct da_monitor_storage *mon_storage;
+	struct da_monitor_storage *ms;
 	struct hlist_node *tmp;
 	int bkt;
 
 	tracepoint_synchronize_unregister();
 	da_monitor_reset_all();
 	da_monitor_sync_hook();
-	/*
-	 * This function is called after all probes are disabled and no longer
-	 * pending, we can safely assume no concurrent user.
-	 */
-	hash_for_each_safe(da_monitor_ht, bkt, tmp, mon_storage, node) {
-		hash_del_rcu(&mon_storage->node);
-		kfree(mon_storage);
+
+	hash_for_each_safe(da_monitor_ht, bkt, tmp, ms, node) {
+		da_extra_cleanup(&ms->rv.da_mon);
+		hash_del_rcu(&ms->node);
+		/* No RCU readers remain; skip the grace period. */
+		if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL) {
+			llist_add(&ms->free_node, &da_pool_free_list);
+		} else {
+			kfree(ms);
+		}
+	}
+
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL) {
+		/*
+		 * Flush any da_pool_return_cb callbacks queued by
+		 * da_destroy_storage() during normal monitor operation.
+		 * After rcu_barrier(), no callback can reference pool slots;
+		 * the backing array is safe to free.
+		 */
+		rcu_barrier();
+		init_llist_head(&da_pool_free_list);
+		kfree(da_pool_storage);
+		da_pool_storage = NULL;
 	}
 }
 
 /*
- * 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_prepare_storage - allocate or retrieve storage for a monitoring session
+ *
+ * Dispatches to the strategy selected by DA_MON_ALLOCATION_STRATEGY.
+ * Caller must hold an RCU read-side CS.
  */
-#ifdef DA_SKIP_AUTO_ALLOC
-#define da_prepare_storage da_fill_empty_storage
-#else
-#define da_prepare_storage da_create_storage
-#endif /* DA_SKIP_AUTO_ALLOC */
+static inline struct da_monitor *
+da_prepare_storage(da_id_type id, monitor_target target,
+		   struct da_monitor *da_mon)
+{
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+		return da_create_pool_storage(id, target, da_mon);
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_MANUAL)
+		return da_fill_empty_storage(id, target, da_mon);
+	return da_create_storage(id, target, da_mon);
+}
 
 #endif /* RV_MON_TYPE */
 
include/rv/ha_monitor.h +6 -0
index 28d3c74cabfc..83199f90afe8 100644
@@ -365,6 +365,12 @@ 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)
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.25.1
#5 lore raw

PATCH v4 2/8 add generic uprobe infrastructure for RV monitors

In reply to [email protected]

From: Wen Yang <[email protected]>

Monitors that instrument user-space function boundaries need to resolve paths, register uprobes, and deregister them safely. Provide a thin wrapper so monitors share a single implementation of this boilerplate.

struct rv_uprobe embeds struct uprobe_consumer directly, avoiding a separate heap allocation per probe. Embedding is safe after rv_uprobe_unregister(): rv_uprobe_sync() calls uprobe_unregister_sync() which performs synchronize_rcu_tasks_trace(), waiting for all rcu_read_lock_trace() readers (handler_chain()) to complete on all CPUs before returning; the caller may then free the containing struct.

The API provides register, synchronous and nosync unregister, a global handler barrier (rv_uprobe_sync), and an active-state predicate. Handlers receive the uprobe_consumer pointer and recover per-probe state via container_of(uc, struct rv_uprobe, uc) or the containing struct.

---
 include/rv/rv_uprobe.h      |  93 ++++++++++++++++++++++++++++++++
 kernel/trace/rv/Kconfig     |   7 +++
 kernel/trace/rv/Makefile    |   1 +
 kernel/trace/rv/rv_uprobe.c | 104 ++++++++++++++++++++++++++++++++++++
 4 files changed, 205 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 +93 -0
new file mode 100644
index 000000000000..2eab5d193e13
@@ -0,0 +1,93 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ *
+ */
+
+#ifndef _RV_UPROBE_H
+#define _RV_UPROBE_H
+
+#include <linux/types.h>
+#include <linux/uprobes.h>
+
+struct pt_regs;
+struct inode;
+
+/**
+ * struct rv_uprobe - embeddable uprobe handle for RV monitors
+ *
+ * Embed via DECLARE_RV_UPROBE() in the caller's struct and pass &name to
+ * rv_uprobe_register().
+ *
+ * Lifetime: after rv_uprobe_unregister() (or rv_uprobe_unregister_nosync()
+ * followed by rv_uprobe_sync()) returns, synchronize_rcu_tasks_trace() has
+ * completed and no handler_chain() iteration can reference this struct.
+ * The caller may free the containing struct immediately after.
+ *
+ * @uc:     embedded uprobe_consumer; set uc.handler / uc.ret_handler before
+ *          calling rv_uprobe_register(); use container_of(uc, rv_uprobe, uc)
+ *          inside handlers to reach this struct or its container
+ * @uprobe: registered uprobe pointer (NULL when not registered)
+ * @inode:  inode of the probed binary (valid while registered)
+ */
+struct rv_uprobe {
+	struct uprobe_consumer	uc;
+	struct uprobe		*uprobe;
+	struct inode		*inode;
+};
+
+/* Embed a named rv_uprobe inside a caller struct */
+#define DECLARE_RV_UPROBE(name)		struct rv_uprobe name
+
+/**
+ * rv_uprobe_is_registered - test whether an uprobe is currently active
+ * @p: probe to test; may be NULL
+ */
+bool rv_uprobe_is_registered(const struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_register - initialise and register an uprobe
+ * @binpath: absolute path to the target binary
+ * @offset:  byte offset within the binary
+ * @p:       caller-provided rv_uprobe (embedded via DECLARE_RV_UPROBE);
+ *           p->uc.handler and/or p->uc.ret_handler must be set before this call
+ *
+ * Resolves the path and registers p->uc with the uprobe subsystem.
+ * No heap allocation is performed.
+ *
+ * Returns 0 on success, negative errno on failure.
+ */
+int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_unregister - synchronously unregister a uprobe
+ * @p: probe to unregister; may be NULL (no-op)
+ *
+ * Removes the consumer from the uprobe subsystem and waits for all in-flight
+ * handlers to complete (via synchronize_rcu_tasks_trace()).  After this
+ * returns, the containing struct may be safely freed by the caller.
+ * Use rv_uprobe_unregister_nosync() + rv_uprobe_sync() to batch multiple
+ * deregistrations before a single synchronisation.
+ */
+void rv_uprobe_unregister(struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
+ * @p: probe to dequeue; may be NULL (no-op)
+ *
+ * Removes the consumer without waiting for in-flight handlers.  The caller
+ * must call rv_uprobe_sync() before freeing the containing struct.
+ */
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete
+ *
+ * Global barrier: calls uprobe_unregister_sync() which performs
+ * synchronize_rcu_tasks_trace() + synchronize_srcu(&uretprobes_srcu).
+ * After this returns, no handler_chain() iteration referencing any
+ * previously deregistered consumer is still in progress.
+ */
+void rv_uprobe_sync(void);
+
+#endif /* _RV_UPROBE_H */
kernel/trace/rv/Kconfig +7 -0
index 3884b14df375..5bad1d63f411 100644
@@ -59,6 +59,13 @@ 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
+	help
+	  Generic uprobe infrastructure for RV monitors.  Provides path
+	  resolution, registration, and safe synchronous teardown.
+
 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 +104 -0

104 lines not on this page · raw message

Signature · 1 line
2.25.1
#8 lore raw

PATCH v4 3/8 rv/tlob: add tlob model DOT file

In reply to [email protected]

From: Wen Yang <[email protected]>

Add the Graphviz DOT specification of the tlob hybrid automaton to tools/verification/models/. The model has three states (running, waiting, sleeping), four transitions (switch_in, preempt, wakeup, sleep), and 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.25.1
#9 lore raw

PATCH v4 4/8 rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check

In reply to [email protected]

From: Wen Yang <[email protected]>

When env_store is U64_MAX (its initial sentinel value), ha_invariant_passed_ns() returns 0 immediately without initializing env_store to the current clock. Subsequent calls to ha_check_invariant_ns() then find env_store still at U64_MAX, causing the elapsed comparison to wrap and always report the invariant as satisfied, silently masking any violations.

Fix by calling ha_reset_clk_ns() to establish the guard on the first invocation instead of returning early. Apply the same fix to ha_invariant_passed_jiffy().

---
 include/rv/ha_monitor.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
include/rv/ha_monitor.h +4 -4
index 83199f90afe8..dddf5694bcc8 100644
@@ -375,12 +375,12 @@ static inline bool ha_check_invariant_ns(struct ha_monitor *ha_mon,
 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;
@@ -414,12 +414,12 @@ static inline bool ha_check_invariant_jiffy(struct ha_monitor *ha_mon,
 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.25.1
#11 lore raw

PATCH v4 5/8 rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable

In reply to [email protected]

From: Wen Yang <[email protected]>

Wrap both definitions with #ifndef guards so HA-based monitors can substitute their own implementations before including this header. tlob uses this to define a reset hook that cancels per-task hrtimers on monitor teardown.

Overrides must still call ha_monitor_reset_env() or cancel outstanding timers to avoid timer UAF.

No behaviour change for monitors that do not override either macro.

---
 include/rv/ha_monitor.h | 10 ++++++++++
 1 file changed, 10 insertions(+)
include/rv/ha_monitor.h +10 -0
index dddf5694bcc8..6d526eef8348 100644
@@ -36,7 +36,15 @@ 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. The override must still call ha_monitor_reset_env() or cancel
+ * timers explicitly.
+ */
+#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 +83,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.25.1
#12 lore raw

PATCH v4 6/8 rv/tlob: add tlob hybrid automaton monitor

In reply to [email protected]

From: Wen Yang <[email protected]>

tlob (task latency over budget) is a per-task hybrid automaton RV monitor that tracks wall-clock time across a user-delimited code section and emits an error when elapsed time exceeds a configurable budget.

The automaton has three states (running, waiting, sleeping) driven by sched_switch and sched_wakeup tracepoints, with a single clock invariant enforced by a per-task HRTIMER_MODE_REL_HARD timer. On budget expiry the monitor records a per-state time breakdown (running_ns, waiting_ns, sleeping_ns) before emitting error_env_tlob.

Uprobe pairs are registered through a tracefs monitor file as "p PATH:OFFSET_START OFFSET_STOP threshold=NS" so arbitrary code sections can be delimited without modifying the target binary.

Per-task state is allocated from a pre-allocated llist pool so the uprobe entry handler incurs no dynamic allocation inside the [T0, T1] measurement window. DA_ALLOC_POOL is used for the same reason on the da_monitor_storage side.

tlob_start_lock is a spinlock_t because the uprobe handler runs under rcu_read_lock_trace() (Tasks Trace SRCU), which permits sleeping on PREEMPT_RT where spinlock_t is an rt_mutex. da_get_target_by_id() is wrapped in scoped_guard(rcu) because hash_for_each_possible_rcu() requires an RCU read-side critical section independent of the lock.

---
 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       | 969 +++++++++++++++++++++
 kernel/trace/rv/monitors/tlob/tlob.h       | 151 ++++
 kernel/trace/rv/monitors/tlob/tlob_trace.h |  52 ++
 kernel/trace/rv/rv_trace.h                 |   1 +
 9 files changed, 1365 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 5bad1d63f411..ed0dc8241691 100644
@@ -90,6 +90,7 @@ source "kernel/trace/rv/monitors/deadline/Kconfig"
 source "kernel/trace/rv/monitors/nomiss/Kconfig"
 # Add new deadline monitors here
 
+source "kernel/trace/rv/monitors/tlob/Kconfig"
 # Add new monitors here
 
 config RV_REACTORS
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..aa43382073d2
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+config RV_MON_TLOB
+	bool "tlob monitor"
+	depends on RV && UPROBES && HIGH_RES_TIMERS
+	select HA_MON_EVENTS_ID
+	select RV_UPROBE
+	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 +969 -0

969 lines not on this page · raw message

kernel/trace/rv/monitors/tlob/tlob.h new file +151 -0

151 lines not on this page · raw message

kernel/trace/rv/monitors/tlob/tlob_trace.h new file +52 -0

52 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.25.1
#15 lore raw

PATCH v4 7/8 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 test suite covering the uprobe-line parser.

Tests call tlob_parse_uprobe_line() and tlob_parse_remove_line() directly rather than going through the top-level write handler, so they exercise parser logic only without touching the uprobe or filesystem subsystems.

Cases cover valid inputs, malformed paths and offsets (including negative values), out-of-range thresholds, and valid and invalid remove lines.

---
 kernel/trace/rv/Makefile                   |   1 +
 kernel/trace/rv/monitors/tlob/.kunitconfig |   8 ++
 kernel/trace/rv/monitors/tlob/Kconfig      |   7 ++
 kernel/trace/rv/monitors/tlob/tlob.c       |   7 +-
 kernel/trace/rv/monitors/tlob/tlob.h       |   4 +-
 kernel/trace/rv/monitors/tlob/tlob_kunit.c | 139 +++++++++++++++++++++
 6 files changed, 162 insertions(+), 4 deletions(-)
 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 +8 -0
new file mode 100644
index 000000000000..34ebf3b172ac
@@ -0,0 +1,8 @@
+CONFIG_FTRACE=y
+CONFIG_HIGH_RES_TIMERS=y
+CONFIG_KUNIT=y
+CONFIG_MODULES=y
+CONFIG_RV=y
+CONFIG_RV_MON_TLOB=y
+CONFIG_TLOB_KUNIT_TEST=y
+CONFIG_UPROBES=y
kernel/trace/rv/monitors/tlob/Kconfig +7 -0
index aa43382073d2..402ef2e5c076 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.c +4 -3
index b45e84195131..a6f9c371646c 100644
@@ -708,7 +708,7 @@ static ssize_t tlob_monitor_read(struct file *file,
  * PATH may contain ':'; the last ':' separates path from offset.
  * Returns 0, -EINVAL, or -ERANGE.
  */
-static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
 				  char **path_out,
 				  loff_t *start_out, loff_t *stop_out)
 {
@@ -782,6 +782,7 @@ static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
 	*stop_out  = (loff_t)stop_val;
 	return 0;
 }
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line);
 
 /*
  * Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention).
@@ -810,8 +811,9 @@ VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out,
 	*start_out = (loff_t)off;
 	return 0;
 }
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_remove_line);
 
-VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf)
+static int tlob_create_or_delete_uprobe(char *buf)
 {
 	loff_t offset_start, offset_stop;
 	u64 threshold_ns;
@@ -836,7 +838,6 @@ VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf)
 	mutex_unlock(&tlob_uprobe_mutex);
 	return ret;
 }
-EXPORT_SYMBOL_IF_KUNIT(tlob_create_or_delete_uprobe);
 
 static ssize_t tlob_monitor_write(struct file *file,
 				  const char __user *ubuf,
kernel/trace/rv/monitors/tlob/tlob.h +3 -1
index fceeba748c85..15bdf3b7fade 100644
@@ -145,7 +145,9 @@ int tlob_start_task(struct task_struct *task, u64 threshold_ns);
 int tlob_stop_task(struct task_struct *task);
 
 #if IS_ENABLED(CONFIG_KUNIT)
-int tlob_create_or_delete_uprobe(char *buf);
+int tlob_parse_uprobe_line(char *buf, u64 *thr_out, char **path_out,
+			   loff_t *start_out, loff_t *stop_out);
+int tlob_parse_remove_line(char *buf, char **path_out, loff_t *start_out);
 #endif /* CONFIG_KUNIT */
 
 #endif /* _RV_TLOB_H */
kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +139 -0

139 lines not on this page · raw message

Signature · 1 line
2.25.1
#17 lore raw

PATCH v4 8/8 selftests/verification: add tlob selftests

In reply to [email protected]

From: Wen Yang <[email protected]>

Add seven ftrace-style test scripts for the tlob RV monitor under tools/testing/selftests/verification/test.d/tlob/. The tests cover uprobe binding management, budget violation detection, and per-state time accounting.

Helper binaries tlob_target and tlob_sym are included in the same directory so the suite is self-contained. tlob_sym resolves ELF symbol offsets for uprobe registration; tlob_target provides busy-spin, sleep, and preempt workloads.

ftracetest is updated to walk up the directory tree when searching for test.d/functions, so monitor subdirectories can be passed as the test directory without placing a dummy functions shim in each new directory.

---
 tools/testing/selftests/ftrace/ftracetest     |  18 +-
 .../testing/selftests/verification/.gitignore |   2 +
 tools/testing/selftests/verification/Makefile |  19 +-
 .../verification/test.d/tlob/Makefile         |  28 +++
 .../test.d/tlob/run_tlob_tests.sh             |  90 ++++++++
 .../verification/test.d/tlob/tlob_sym.c       | 209 ++++++++++++++++++
 .../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 ++++++
 14 files changed, 854 insertions(+), 4 deletions(-)
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
 create mode 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
 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/ftrace/ftracetest +15 -3
index 0a56bf209f6c..91c007b0a74a 100755
@@ -159,9 +159,21 @@ 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
-    TOP_DIR=$OPT_TEST_DIR
-    TEST_DIR=$TOP_DIR/test.d
+  if [ -n "$OPT_TEST_DIR" ]; then
+    # Walk up from OPT_TEST_DIR to find the nearest ancestor containing
+    # test.d/functions, allowing monitor subdirectories to be passed directly.
+    dir=$OPT_TEST_DIR
+    while [ "$dir" != "/" ]; do
+      if [ -f "$dir/test.d/functions" ]; then
+        TOP_DIR=$dir
+        TEST_DIR=$TOP_DIR/test.d
+        break
+      fi
+      dir=$(dirname "$dir")
+    done
+    if [ -z "$TOP_DIR" ]; then
+      errexit "no test.d/functions found above $OPT_TEST_DIR"
+    fi
   fi
 }
 
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 +28 -0
new file mode 100644
index 000000000000..05a2d2599c4e
@@ -0,0 +1,28 @@
+# 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)
+
+# For standalone execution via vng
+FTRACETEST := ../../../ftrace/ftracetest
+LOGDIR ?= ../../logs
+
+.PHONY: all
+all: tlob_sym tlob_target
+
+tlob_sym: tlob_sym.c
+	$(CC) $(CFLAGS) -o $@ $<
+
+tlob_target: tlob_target.c
+	$(CC) $(CFLAGS) -o $@ $<
+
+.PHONY: run_tests
+run_tests: all
+	@./run_tlob_tests.sh
+
+.PHONY: clean
+clean:
+	$(RM) tlob_sym tlob_target
tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file +90 -0

90 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/tlob_sym.c new file +209 -0

209 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..4a1c18c7485a
@@ -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 || false
+
+# Remove the binding; it must no longer appear.
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR"
+! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR" || false
+
+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..bb2eeef17019
@@ -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 || false
+
+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.25.1
maintainer
#2 lore raw

Re: [PATCH v4 1/8] rv/da: introduce DA_MON_ALLOCATION_STRATEGY

In reply to [email protected]
Quoted 12 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Per-object DA storage allocation is currently limited to kmalloc on demand. Add a compile-time selector so monitors can choose among three strategies:

  DA_ALLOC_AUTO   (default) - kmalloc per object on the monitor path DA_ALLOC_POOL             - pre-allocated fixed-size llist pool; selected by defining DA_MON_POOL_SIZE DA_ALLOC_MANUAL           - caller pre-inserts storage; framework only links the target field

Quoted 4 lines

The pool strategy uses a lock-free llist (cmpxchg, no spinlock) so pool release is safe from RCU callback context without acquiring a lock. Moving allocation before the measurement window also prevents kmalloc latency.

Measurement window here is tlob's, remember RV isn't itself a measurement tool (yet, perhaps).
And isn't this also happening with other methods? We're trying to do allocation when the monitor starts (so before this measurement window).

I'm a bit puzzled since you're mentioning it many times, when have we done /allocations/ from RCU callbacks?
We surely do deallocations (kfree_rcu) but allocations are at most in RCU read-side critical sections and it's perfectly fine to take sleeping spinlocks there (that's a special kind of sleep under PREEMPT_RT). Besides I'm not quite sure spinlocks are that bad in RCU callbacks either (kfree surely takes them).
I'm not sure what you mean here but I don't think deallocation was ever a problem, was it?

Quoted 41 lines

nomiss is updated to DA_ALLOC_MANUAL.

---
 include/rv/da_monitor.h                  | 247 +++++++++++++++++++---- include/rv/ha_monitor.h                  |   6 + kernel/trace/rv/monitors/nomiss/nomiss.c |   6 +-

 3 files changed, 221 insertions(+), 38 deletions(-)
include/rv/da_monitor.h +0 -0
index 34b8fba9ecd4..9c9acc123e3b 100644
@@ -14,7 +14,56 @@
  #ifndef _RV_DA_MONITOR_H
  #define _RV_DA_MONITOR_H


+/*
+ * Allocation strategies for RV_MON_PER_OBJ monitors.
+ *
+ * Select the strategy with a single define before including this header:
+ *
+ *   #define DA_MON_POOL_SIZE N          - pool mode; N pre-allocated slots.
+ *                                         Implies DA_ALLOC_POOL automatically.
+ *   #define DA_MON_ALLOCATION_STRATEGY \
+ *           DA_ALLOC_MANUAL             - manual mode (see below).
+ *   (neither)                           - auto mode (default).
+ *
+ * Do not define both DA_MON_POOL_SIZE and DA_MON_ALLOCATION_STRATEGY.
+ *
+ * DA_ALLOC_AUTO   - lock-free kmalloc on the hot path; unbounded capacity.
+ * DA_ALLOC_POOL   - pre-allocated fixed-size pool; set by defining DA_MON_POOL_SIZE.
+ * DA_ALLOC_MANUAL - caller inserts storage before da_handle_start_event();
+ *                   the framework only links the target field.
+ */

include/rv/da_monitor.h +3 -0
+#define DA_ALLOC_AUTO   0
+#define DA_ALLOC_POOL   1
+#define DA_ALLOC_MANUAL 2

include/rv/da_monitor.h line 47

Quoted 10 lines
include/rv/da_monitor.h +10 -0
+
+#ifdef DA_MON_POOL_SIZE
+#ifdef DA_MON_ALLOCATION_STRATEGY
+#error "Define only one of DA_MON_POOL_SIZE or DA_MON_ALLOCATION_STRATEGY"
+#endif
+#if DA_MON_POOL_SIZE == 0
+#error "DA_MON_POOL_SIZE must be non-zero"
+#endif
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL
+#endif

Longer ifdefs should have comments to make them readable, like

  #endif /* DA_MON_POOL_SIZE */
Quoted 6 lines
include/rv/da_monitor.h +6 -0
+
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#endif
+
+/*

include/rv/da_monitor.h line 63

Quoted 10 lines
include/rv/da_monitor.h +10 -0
+ * Provide a zero default so da_monitor_init() can reference
+ * DA_MON_POOL_SIZE in a plain C if() without an #if guard; the
+ * compiler eliminates the dead branch.
+ */
+#ifndef DA_MON_POOL_SIZE
+#if DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL
+#error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be defined and non-zero"
+#endif
+#define DA_MON_POOL_SIZE 0
+#endif

Same here, better to have a comment.

Quoted 24 lines

+
 #include <rv/automata.h>
+#include <linux/llist.h>
 #include <linux/rv.h>
 #include <linux/stringify.h>
 #include <linux/bug.h>

include/rv/da_monitor.h +0 -0
@@ -66,6 +115,16 @@ static struct rv_monitor rv_this;
  #define da_monitor_sync_hook()
  #endif

include/rv/da_monitor.h +10 -0
+/*
+ * Per-object teardown hook, called after da_monitor_reset_all() +
+ * da_monitor_sync_hook() and before hash_del_rcu() for each entry.
+ * All HA timer callbacks have completed at this point.
+ * Define before including this header.  Default: 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

include/rv/da_monitor.h struct da_monitor_storage { · line 470

Quoted 8 lines
include/rv/da_monitor.h +5 -0
@@ -404,6 +463,12 @@ struct da_monitor_storage {
  	union rv_task_monitor rv;
  	struct hlist_node node;
  	struct rcu_head rcu;
+	/*
+	 * Mutually exclusive with rcu: rcu is live during the RCU callback
+	 * flight; free_node when the slot is in da_pool_free_list.
+	 * Present in all monitors to avoid #if-gating the pool helpers.
+	 */

I really don't understand much more about it by this comment, perhaps drop it here and make the separate usages clearer later?

By the way, if they are /really/ mutually exclusive and you want to save space, why not having them in an anonymous union?

Quoted 29 lines
include/rv/da_monitor.h +1 -0
+	struct llist_node free_node;
  };


 #ifndef DA_MONITOR_HT_BITS

include/rv/da_monitor.h +0 -0
@@ -495,18 +560,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)-

Quoted 1 line

id;

 }

include/rv/da_monitor.h +0 -12
-/*
- * 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 *

include/rv/da_monitor.h +0 -0
@@ -537,15 +590,79 @@ static inline monitor_target
 da_get_target_by_id(da_id_type id)
  	return mon_storage->target;
  }
Quoted 5 lines


+/*
+ * Lock-free llist (cmpxchg) rather than kmem_cache/mempool: on
+ * PREEMPT_RT spinlock_t becomes a sleeping lock, which is forbidden
+ * in the rcuc kthread context where RCU callbacks run.

This comment kind of implies we were using a kmem_cache, it's great for a changelog and helped me understand why you're doing this, but doesn't belong to the final version as is.

Quoted 3 lines

+ *
+ * Multiple producers (any context, any CPU) call llist_add; a single
+ * consumer (llist_del_first, serialised by the monitor's start lock)

Which monitor's start lock? There is no such a thing defined anywhere, maybe you wanted to say that monitors using this allocation scheme MUST lock during their start event. This by the way needs to be a global lock among all instances of the monitor (as you're indeed doing in tlob).

With that in mind, I don't really see how this is better than the original mempool: you still need to lock. There's nothing wrong in freeing stuff from RCU callbacks, that's what they're for.

Quoted 9 lines

+ * needs no additional synchronisation.
+ *
+ * Per-TU statics: each PER_OBJ monitor gets its own pool instance;
+ * da_pool_storage and da_pool_free_list are NULL/empty and the pool
+ * paths are dead code for non-pool monitors.
+ */

include/rv/da_monitor.h +2 -0
+static struct da_monitor_storage *da_pool_storage;
+static LLIST_HEAD(da_pool_free_list);

...

Quoted 15 lines

+++ b/kernel/trace/rv/monitors/nomiss/nomiss.c

include/rv/da_monitor.h +0 -0
@@ -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 */

include/rv/da_monitor.h +1 -1
-#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>

include/rv/da_monitor.h static void handle_sys_enter(void *data, struct pt_regs · line 219

Quoted 7 lines
include/rv/da_monitor.h +1 -1
@@ -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));

I'm starting to doubt this is the right thing to do. We do have the target (p) and that function doesn't check if the id already has a storage (which shouldn't happen but well, doesn't hurt checking).

This simplification is probably just not worth it, and doesn't look related to the rest of the change.

Thanks,
Gabriele

maintainer
#4 lore raw

Re: [PATCH v4 1/8] rv/da: introduce DA_MON_ALLOCATION_STRATEGY

In reply to [email protected]
Quoted 13 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

include/rv/ha_monitor.h +0 -0
index 28d3c74cabfc..83199f90afe8 100644
@@ -365,6 +365,12 @@ 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

Quoted 8 lines

+ * 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)

This hunk belongs to a later patch, doesn't it?

maintainer
#10 lore raw

Re: [PATCH v4 4/8] rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check

In reply to [email protected]
Quoted 9 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

When env_store is U64_MAX (its initial sentinel value), ha_invariant_passed_ns() returns 0 immediately without initializing env_store to the current clock.  Subsequent calls to ha_check_invariant_ns() then find env_store still at U64_MAX, causing the elapsed comparison to wrap and always report the invariant as satisfied, silently masking any violations.

Quoted 5 lines

Fix by calling ha_reset_clk_ns() to establish the guard on the first invocation instead of returning early.  Apply the same fix to ha_invariant_passed_jiffy().

While this looks alright, I have something else in mind for the env variables lifecycle. After [1] is merged, I'd like to merge [2] which is basically inspired on your idea of resetting variables on start, just it's embedded in the framework (and requires [1] to get rid of the double representation for env variables).
This is probably going to happen after the next merge window.

I believe you can keep this patch for now and your model wouldn't need change after adding [2] instead.

Thoughts?

Thanks,
Gabriele

[1] -
https://lore.kernel.org/lkml/c0f600dcbf3d8b487c944406851a39146f4d91fa.1781847583.git.namcao@linutronix.de/
[2] -
https://web.git.kernel.org/pub/scm/linux/kernel/git/gmonaco/linux.git/commit/?h=rv_hybrid_automata_enq_lark&id=d7c5c40c81d9b928aa9f86c4115429bffd223a8c

Quoted 42 lines

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

 1 file changed, 4 insertions(+), 4 deletions(-)
include/rv/ha_monitor.h +0 -0
index 83199f90afe8..dddf5694bcc8 100644
@@ -375,12 +375,12 @@ static inline bool ha_check_invariant_ns(struct
 ha_monitor *ha_mon,

 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
@@ -414,12 +414,12 @@ static inline bool ha_check_invariant_jiffy(struct
 ha_monitor *ha_mon,

 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
#13 lore raw

Re: [PATCH v4 6/8] rv/tlob: add tlob hybrid automaton monitor

In reply to [email protected]

kernel/trace/rv/monitors/tlob/tlob.c line 260

Quoted 11 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+/* Uprobe binding list; protected by tlob_uprobe_mutex. */
+static LIST_HEAD(tlob_uprobe_list);
+static DEFINE_MUTEX(tlob_uprobe_mutex);
+
+/*
+ * Serialises duplicate-check + da_handle_start_run_event() per pid.
+ * spinlock_t not raw_spinlock_t: uprobe handlers run under Tasks Trace
+ * SRCU (rcu_read_lock_trace()), which permits sleeping on PREEMPT_RT.
+ */

I don't think these comments add much value. spinlock_t is allowed in RCU critical sections anyway (it's some sort of preemption and RCU is preemptible under PREEMPT_RT).

Of course if it isn't really required we don't use a raw spinlock, you don't need a justification here.

Quoted 7 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+static DEFINE_SPINLOCK(tlob_start_lock);
+
+/* Per-uprobe-binding state: a start + stop probe pair for one binary region.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+struct tlob_uprobe_binding {
+	struct list_head	list;
+	u64			threshold_ns;

kernel/trace/rv/monitors/tlob/tlob.c line 276

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	char			binpath[TLOB_MAX_PATH];
+	loff_t			offset_start;
+	loff_t			offset_stop;
+	DECLARE_RV_UPROBE(start_probe);
+	DECLARE_RV_UPROBE(stop_probe);
+};
+
+/*
+ * Per-task teardown invoked by da_monitor_destroy() for each hash entry.
+ * CAS on stopping (0->1) claims exclusive cleanup ownership.

I find reading acronyms extremely annoying, since non-locking algorithms are already complex on their own, why don't you just say cmpxchg (which is searchable) instead of CAS. Sure CAS isn't an obscure acronym but I could find at least another 4 different definitions in the kernel tree.

Quoted 38 lines

+ *
+ * No per-entry ha_cancel_timer_sync(): da_monitor_destroy() calls
+ * da_monitor_reset_all() + synchronize_rcu() before this hook, and
+ * ha_mon_destroying prevents new timer callbacks from running.
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +33 -0
+static inline void tlob_extra_cleanup(struct da_monitor *da_mon)
+{
+	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;
+
+	put_task_struct(ws->task);
+	/*
+	 * da_monitor_destroy() has already called synchronize_rcu(); no
+	 * reader holds ws.  Return the slot directly without call_rcu.
+	 */
+	llist_add(&ws->free_node, &tlob_ws_free_list);
+}
+
+static inline bool __tlob_acc(struct task_struct *task, ktime_t now,
+			       enum tlob_acc_idx idx)
+{
+	struct tlob_task_state *ws;
+	unsigned long flags;
+
+	guard(rcu)();
+	ws = da_get_target_by_id(task->pid);
+	if (!ws)
+		return false;
+	raw_spin_lock_irqsave(&ws->entry_lock, flags);
+	ws->accs_ns[idx] += ktime_to_ns(ktime_sub(now, ws->last_ts));
+	ws->last_ts = now;
+	raw_spin_unlock_irqrestore(&ws->entry_lock, flags);

kernel/trace/rv/monitors/tlob/tlob.c line 324

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	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)
+{
+	return __tlob_acc(task, now, TLOB_ACC_RUNNING);
+}
+
+/* Accumulate waiting_ns for next; returns true if next is monitored. */

There's no next and prev here, plus you're describing __tlob_acc()'s behaviour 3 times, I'd say just document that (even if it isn't the primary facing function) and that's all.

Quoted 95 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +65 -0
+static inline bool tlob_acc_waiting(struct task_struct *task, ktime_t now)
+{
+	return __tlob_acc(task, now, TLOB_ACC_WAITING);
+}
+
+/*
+ * 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.
+ */
+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);
+
+	if (tlob_acc_running(prev, now))
+		da_handle_event(prev->pid, NULL,
+				prev_preempted ? preempt_tlob : sleep_tlob);
+	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)
+{
+	return __tlob_acc(task, now, TLOB_ACC_SLEEPING);
+}
+
+/*
+ * 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.
+ */
+static void handle_sched_wakeup(void *data, struct task_struct *p)
+{
+	ktime_t now = ktime_get();
+
+	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.
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +27 -0
+static void handle_sched_process_exit(void *data, struct task_struct *p,
+				       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, or -ENOSPC (pool at capacity).
+ */
+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 < TLOB_MIN_THRESHOLD_NS ||
+	    threshold_ns > TLOB_MAX_THRESHOLD_NS)
+		return -ERANGE;
+
+	/* Serialise duplicate-check + pool-slot claim; see tlob_start_lock.

*/

kernel/trace/rv/monitors/tlob/tlob.c line 424

Quoted 7 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+	guard(spinlock)(&tlob_start_lock);
+
+	/*
+	 * __da_get_mon_storage() uses hash_for_each_possible_rcu(), which
+	 * requires an RCU read-side critical section.  On PREEMPT_RT,
+	 * spinlock_t is an rt_mutex and does not satisfy this requirement.
+	 */

Also this is probably misleading, getting the monitor requires RCU, then the fact some in some configuration something other than read_lock_rcu() would work too (e.g. disabling preemption) is irrelevant.

Quoted 26 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +22 -0
+	scoped_guard(rcu) {
+		if (da_get_target_by_id(task->pid))
+			return -EALREADY;
+	}
+
+	/*
+	 * Both tlob_ws_alloc() and da_handle_start_run_event() pop from
+	 * pre-allocated pools of size TLOB_MAX_MONITORED; NULL return means
+	 * the pool is at capacity.
+	 */
+	ws = tlob_ws_alloc();
+	if (!ws)
+		return -ENOSPC;
+
+	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(),

(quoted diff) +2 -0
+	 * initialises the monitor, and delivers start_tlob in one step: the
+	 * generated ha_setup_invariants() resets clk_elapsed and arms the

timer.

Review

Quoted 2 lines
(quoted diff) +2 -0
+	 * Returns 0 if the da_monitor_storage pool is exhausted.
+	 */

And try not to be too specific about the internal implementation of the library, that can change without notice, we don't want to have to update all comments.

Here it is indeed non-trivial to assume no space if da_handle_start_run_event() fails, but that's only consequence of the fact that this ws is certainly new (by construction) and the monitor is
enabled: da_handle_start_run_event() can only fail if allocation failed.

da_handle_start_* functions return 1 if an event was handled, this is by the way not documented..

You could simply say something like:

da_handle_start_run_event() returns false if no event was handled, in
this case it can happen only if memory allocation failed

Quoted 54 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +40 -0
+	if (!da_handle_start_run_event(task->pid, ws, start_tlob)) {
+		put_task_struct(task);
+		tlob_ws_direct_return(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).
+ */
+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(WARN_ON_ONCE(!da_mon)))
+			return -ESRCH;
+
+		ha_mon = to_ha_monitor(da_mon);
+
+		/*
+		 * CAS (0->1) claims cleanup ownership under RCU (ws

guaranteed valid).

(quoted diff) +1 -0
+		 * _release pairs with atomic_read_acquire in

ha_setup_invariants.

kernel/trace/rv/monitors/tlob/tlob.c new file +11 -0
+		 */
+		if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
+			return -EAGAIN;
+	}
+	/*
+	 * ws and ha_mon are used below outside the RCU guard.  This is safe:
+	 * the winning CAS (stopping: 0->1) is the only path that frees ws,
+	 * and da_destroy_storage() below is the only call that returns the
+	 * pool slot.  No concurrent path can free either object.
+	 */
+

kernel/trace/rv/monitors/tlob/tlob.c line 512

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	/* Wait for in-flight timer callback before reading da_monitoring. */
+	ha_cancel_timer_sync(ha_mon);
+
+	/* Timer fired first -> budget exceeded; otherwise reset normally. */
+	scoped_guard(rcu) {
+		budget_exceeded = !da_monitoring(da_mon);
+		if (!budget_exceeded)
+			da_monitor_reset(da_mon);
+	}
+	da_destroy_storage(task->pid);

Here you're playing with the state machine, that looks fragile from a monitor code.
You could probably have budget_exceeded as part of your target and set it from tlob_reset_notify() when you know there was an expiration.

Perhaps "stop" could be yet another event in the state machine, bringing to the "stopped" state, which would automatically call reset(), you only need to grab ws->stopping before handling the event for tlob_reset_notify() to work as expected.
Though you'd probably still need ha_cancel_timer_sync() for the rare but not impossible in-flight timer not yet in RCU critical section..

I'd rather avoid as much as possible bringing in ha_mon/da_mon, but you probably cannot do better here, just you could avoid relying on da_monitoring().

What do you think?

Thanks,
Gabriele

maintainer
#6 lore raw

Re: [PATCH v4 2/8] rv: add generic uprobe infrastructure for RV monitors

In reply to [email protected]
Quoted 8 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

+++ b/kernel/trace/rv/Kconfig

(quoted diff) +0 -0
@@ -59,6 +59,13 @@ config RV_PER_TASK_MONITORS

    This option configures the maximum number of per-task RV monitors that can run
    simultaneously.

kernel/trace/rv/Kconfig config RV_PER_TASK_MONITORS · line 67

Quoted 6 lines
kernel/trace/rv/Kconfig +6 -0
+config RV_UPROBE
+	bool
+	depends on RV && UPROBES
+	help
+	  Generic uprobe infrastructure for RV monitors.  Provides path
+	  resolution, registration, and safe synchronous teardown.

This isn't exposed, it's selected automatically when required, I don't even think the help text is visible (menuconfig doesn't show it), do we really need it?

Quoted 4 lines

+
 source "kernel/trace/rv/monitors/wip/Kconfig"
 source "kernel/trace/rv/monitors/wwnr/Kconfig"

...

Quoted 25 lines

+++ b/kernel/trace/rv/rv_uprobe.c

kernel/trace/rv/rv_uprobe.c new file +22 -0
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ *
+ * struct rv_uprobe embeds struct uprobe_consumer directly.  This is safe
+ * because rv_uprobe_sync() calls uprobe_unregister_sync(), which calls
+ * synchronize_rcu_tasks_trace().  handler_chain() runs under
+ * rcu_read_lock_trace(), so after synchronize_rcu_tasks_trace() returns,
+ * all in-flight handler_chain() iterations, including any pending
+ * uc->cons_node.next reads, have completed on all CPUs.  The caller may
+ * then free the struct containing rv_uprobe immediately.
+ */
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/namei.h>
+#include <linux/uprobes.h>
+#include <rv/rv_uprobe.h>
+
+/**
+ * rv_uprobe_register - initialise and register an uprobe
+ */
+int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe

*p)

kernel/trace/rv/rv_uprobe.c line 29

Quoted 7 lines
kernel/trace/rv/rv_uprobe.c new file +7 -0
+{
+	struct inode *inode;
+	struct path path;
+	int ret;
+
+	if (!p->uc.handler && !p->uc.ret_handler)
+		return -EINVAL;

uprobe_register() does this already, do we need it here too?

Quoted 10 lines
kernel/trace/rv/rv_uprobe.c new file +10 -0
+
+	ret = kern_path(binpath, LOOKUP_FOLLOW, &path);
+	if (ret)
+		return ret;
+
+	if (!d_is_reg(path.dentry)) {
+		path_put(&path);
+		return -EINVAL;
+	}
+

kernel/trace/rv/rv_uprobe.c line 49

Quoted 10 lines
kernel/trace/rv/rv_uprobe.c new file +10 -0
+	inode = d_real_inode(path.dentry);
+	p->inode = inode;
+
+	/*
+	 * uprobe_register() requires the inode (and mount) to remain
+	 * referenced across the call.  Keep the path alive until after
+	 * uprobe_register() has stored its own reference, then release it.
+	 */
+	p->uprobe = uprobe_register(inode, offset, 0, &p->uc);
+	path_put(&path);

I believe I was mistaken here, as sashiko pointed out, uprobe_register() doesn't keep a reference to the inode, (explicitly stated in it's docs:
"Caller of uprobe_register() is required to keep @inode (and the containing mount) referenced.").

We should probably revert back to holding path instead of inode and putting it after synchronous cleanup. That's also what BPF does.

Thanks,
Gabriele

Quoted 55 lines
kernel/trace/rv/rv_uprobe.c new file +55 -0
+	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_register);
+
+/**
+ * rv_uprobe_is_registered - test whether an uprobe is currently active
+ */
+bool rv_uprobe_is_registered(const struct rv_uprobe *p)
+{
+	return p && p->uprobe;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_is_registered);
+
+/**
+ * rv_uprobe_unregister - synchronously unregister a uprobe
+ */
+void rv_uprobe_unregister(struct rv_uprobe *p)
+{
+	if (!p || !p->uprobe)
+		return;
+
+	rv_uprobe_unregister_nosync(p);
+	rv_uprobe_sync();
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister);
+
+/**
+ * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
+ */
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p)
+{
+	if (!p || !p->uprobe)
+		return;
+
+	uprobe_unregister_nosync(p->uprobe, &p->uc);
+	p->uprobe = NULL;
+	p->inode = NULL;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister_nosync);
+
+/**
+ * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete
+ */
+void rv_uprobe_sync(void)
+{
+	uprobe_unregister_sync();
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_sync);
maintainer
#18 lore raw

Re: [PATCH v4 8/8] selftests/verification: add tlob selftests

In reply to [email protected]
Quoted 7 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Add seven ftrace-style test scripts for the tlob RV monitor under tools/testing/selftests/verification/test.d/tlob/.  The tests cover uprobe binding management, budget violation detection, and per-state time accounting.

Quoted 8 lines

Helper binaries tlob_target and tlob_sym are included in the same directory so the suite is self-contained.  tlob_sym resolves ELF symbol offsets for uprobe registration; tlob_target provides busy-spin, sleep, and preempt workloads.

ftracetest is updated to walk up the directory tree when searching for test.d/functions, so monitor subdirectories can be passed as the test directory without placing a dummy functions shim in each new directory.

I believe this last part deserves its own patch, so ftrace folks won't lose track of it (and it's probably a good idea for them to Ack it, make sure they're Cc'd).

Quoted 60 lines

---
 tools/testing/selftests/ftrace/ftracetest     |  18 +- .../testing/selftests/verification/.gitignore |   2 + tools/testing/selftests/verification/Makefile |  19 +- .../verification/test.d/tlob/Makefile         |  28 +++ .../test.d/tlob/run_tlob_tests.sh             |  90 ++++++++ .../verification/test.d/tlob/tlob_sym.c       | 209 ++++++++++++++++++ .../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 ++++++

 14 files changed, 854 insertions(+), 4 deletions(-)

 create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile create mode 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh 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/ftrace/ftracetest +0 -0
index 0a56bf209f6c..91c007b0a74a 100755
@@ -159,9 +159,21 @@ 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 -    TOP_DIR=$OPT_TEST_DIR
-    TEST_DIR=$TOP_DIR/test.d
+  if [ -n "$OPT_TEST_DIR" ]; then
+    # Walk up from OPT_TEST_DIR to find the nearest ancestor containing +    # test.d/functions, allowing monitor subdirectories to be passed directly.
+    dir=$OPT_TEST_DIR
+    while [ "$dir" != "/" ]; do
+      if [ -f "$dir/test.d/functions" ]; then
+        TOP_DIR=$dir

Quoted 8 lines

+        TEST_DIR=$TOP_DIR/test.d
+        break
+      fi
+      dir=$(dirname "$dir")
+    done
+    if [ -z "$TOP_DIR" ]; then
+      errexit "no test.d/functions found above $OPT_TEST_DIR" +    fi

This won't happen, before calling parse_opts, the script initialises TOP_DIR to absdir $0 (ftracetest's folder). You can leave it as it is, no need to catch an error since it was gracefully continuing anyway.

Quoted 26 lines

   fi
 }

tools/testing/selftests/verification/.gitignore +0 -0
index 2659417cb2c7..cbbd03ee16c7 100644
@@ -1,2 +1,4 @@
  # SPDX-License-Identifier: GPL-2.0-only

 logs

tools/testing/selftests/verification/.gitignore +2 -0
+test.d/tlob/tlob_sym
+test.d/tlob/tlob_target
tools/testing/selftests/verification/Makefile +0 -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/*

tools/testing/selftests/verification/Makefile

Quoted 3 lines
tools/testing/selftests/verification/Makefile +3 -0
+# Subdirectories that provide binaries used by the test runner.
+# Each entry must contain a Makefile that accepts OUTDIR= and
+# deposits its binaries there.

Does the entry's Makefile support OUTDIR ? It doesn't look like it.

By the way, reimplementing things like this is probably going to break a few things like installation (e.g. for distros to package the kselftests) and clean target. Perhaps we could refactor it to use standard kselftests methods. See my mockup at the end.

Quoted 24 lines
tools/testing/selftests/verification/Makefile +2 -0
+BUILD_SUBDIRS := test.d/tlob
+

 include ../lib.mk

tools/testing/selftests/verification/Makefile +13 -0
+
+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 +1 -0
new file mode 100644
index 000000000000..05a2d2599c4e
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0

tools/testing/selftests/verification/test.d/tlob/Makefile line 11

Quoted 10 lines
tools/testing/selftests/verification/test.d/tlob/Makefile new file +10 -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)
+
+# For standalone execution via vng
+FTRACETEST := ../../../ftrace/ftracetest
+LOGDIR ?= ../../logs

Are those needed? They aren't used in the Makefile nor exported to the executed scripts, I'd just drop them.

Quoted 26 lines
tools/testing/selftests/verification/test.d/tlob/Makefile new file +17 -0
+
+.PHONY: all
+all: tlob_sym tlob_target
+
+tlob_sym: tlob_sym.c
+	$(CC) $(CFLAGS) -o $@ $<
+
+tlob_target: tlob_target.c
+	$(CC) $(CFLAGS) -o $@ $<
+
+.PHONY: run_tests
+run_tests: all
+	@./run_tlob_tests.sh
+
+.PHONY: clean
+clean:
+	$(RM) tlob_sym tlob_target

diff --git a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file mode 100755
index 000000000000..cd949756e713

tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file +1 -0
@@ -0,0 +1,90 @@
+#!/bin/bash

tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh line 11

Quoted 10 lines
tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file +10 -0
+# SPDX-License-Identifier: GPL-2.0
+#
+# Standalone runner for tlob selftests
+# Usage: ./run_tlob_tests.sh [options]
+#
+# Options:
+#   -v, --verbose    Verbose output
+#   -k, --keep       Keep test logs
+#   -l, --logdir DIR Log directory (default: ../../logs)
+#   -h, --help       Show this help

I get you want a way to run tlob tests alone, but can we reduce the amount of code to maintain? Why do we need to parse and forward arguments? Cannot we just pass "$@" to ftracetest?

See that in my mockup later.

Quoted 69 lines
tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file +69 -0
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+FTRACETEST="$SCRIPT_DIR/../../../ftrace/ftracetest"
+LOGDIR="$SCRIPT_DIR/../../logs"
+VERBOSE=""
+KEEP=""
+EXTRA_ARGS=""
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        -v|--verbose)
+            VERBOSE="-v"
+            shift
+            ;;
+        -k|--keep)
+            KEEP="-k"
+            shift
+            ;;
+        -l|--logdir)
+            LOGDIR="$2"
+            shift 2
+            ;;
+        -h|--help)
+            echo "Usage: $0 [options]"
+            echo ""
+            echo "Options:"
+            echo "  -v, --verbose    Verbose output"
+            echo "  -k, --keep       Keep test logs"
+            echo "  -l, --logdir DIR Log directory (default: ../../logs)"
+            echo "  -h, --help       Show this help"
+            echo ""
+            echo "Examples:"
+            echo "  $0                           # Run all tlob tests"
+            echo "  $0 -v                        # Run with verbose output"
+            echo "  $0 -v -l /tmp/tlob-logs      # Custom log directory"
+            echo ""
+            echo "With vng:"
+            echo "  vng -v --rwdir $LOGDIR -- $0"
+            exit 0
+            ;;
+        *)
+            EXTRA_ARGS="$EXTRA_ARGS $1"
+            shift
+            ;;
+    esac
+done
+
+# Build test helpers
+echo "Building tlob test helpers..."
+make -C "$SCRIPT_DIR" all
+
+# Check ftracetest exists
+if [ ! -x "$FTRACETEST" ]; then
+    echo "Error: $FTRACETEST not found or not executable"
+    echo "Make sure you're running from the correct directory"
+    exit 1
+fi
+
+# Create log directory
+mkdir -p "$LOGDIR"
+
+# Run tests
+echo "Running tlob selftests..."
+echo "Log directory: $LOGDIR"
+echo ""
+

tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh line 90

Quoted 10 lines
tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file +10 -0
+# Export RV_BINDIR so test scripts can find tlob_target and tlob_sym
+export RV_BINDIR="$SCRIPT_DIR"
+
+# Pass the test directory, not individual .tc files
+# ftracetest will discover all .tc files in the directory
+"$FTRACETEST" -K $VERBOSE $KEEP --rv --logdir "$LOGDIR" \
+    "$SCRIPT_DIR" $EXTRA_ARGS
+
+echo ""
+echo "Tests completed. Logs saved to: $LOGDIR"

I tried to refactor it to follow more standard selftest building, avoiding to maintain things ourselves. The only drawback is that you'd have to move the tlob_*.c files to selftests/verification , then we can still use RV_BINDIR but support it only via Makefile and run_tlob_tests.sh (so let's drop defining it in all tests and save inconvenience if things ever change).

By the way, this isn't necessarily bad, RV_BINDIR is a general term that any other selftest can end up using (and shouldn't point to tlob's directory).

I didn't do it to avoid confusion, but it may be more appropriate to change the name (e.g. RVTEST_ROOT or VERIFICATIONTEST_ROOT)?

Now you won't need separate Makefiles and everything should work seamlessly.

(I tested this in vng with both your script and the Makefile, but this is far from a deep testing, it should apply cleanly on your tree)

From 84eff70a20d79d0900714338903c0f5d568a45d9 Mon Sep 17 00:00:00 2001
From: Gabriele Monaco <[email protected]>
Date: Wed, 22 Jul 2026 15:29:21 +0200
Subject: [PATCH] selftests/verification: Simplify tlob tests

Squash this with the other patch should you accept it! Drop nested Makefile in favour of lib.mk and simplify run_tlob_tests

---
 .../testing/selftests/verification/.gitignore |  4 +-
 tools/testing/selftests/verification/Makefile | 20 +----
 .../verification/test.d/tlob/Makefile         | 28 -------
 .../test.d/tlob/run_tlob_tests.sh             | 79 +------------------
 .../verification/test.d/tlob/uprobe_bind.tc   |  1 -
 .../test.d/tlob/uprobe_detail_running.tc      |  1 -
 .../test.d/tlob/uprobe_detail_sleeping.tc     |  1 -
 .../test.d/tlob/uprobe_detail_waiting.tc      |  1 -
 .../verification/test.d/tlob/uprobe_multi.tc  |  1 -
 .../test.d/tlob/uprobe_violation.tc           |  1 -
 .../verification/{test.d/tlob => }/tlob_sym.c |  0
 .../{test.d/tlob => }/tlob_target.c           |  0
 12 files changed, 9 insertions(+), 128 deletions(-)
 delete mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_sym.c (100%)
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_target.c (100%)
tools/testing/selftests/verification/.gitignore +2 -2
index cbbd03ee16c7..d2f231f1bacb 100644
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 logs
-test.d/tlob/tlob_sym
-test.d/tlob/tlob_target
+tlob_sym
+tlob_target
tools/testing/selftests/verification/Makefile +3 -17
index 0b32bdfdb8db..17442e6bb87f 100644
@@ -4,22 +4,8 @@ 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
+TEST_GEN_FILES := tlob_sym tlob_target
 
-include ../lib.mk
-
-all: $(patsubst %,_build_%,$(BUILD_SUBDIRS))
-
-clean: $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
+export RV_BINDIR := $(OUTPUT)
 
-.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
+include ../lib.mk
tools/testing/selftests/verification/test.d/tlob/Makefile deleted +0 -28
deleted file mode 100644
index 05a2d2599c4e..000000000000
@@ -1,28 +0,0 @@
-# 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)
-
-# For standalone execution via vng
-FTRACETEST := ../../../ftrace/ftracetest
-LOGDIR ?= ../../logs
-
-.PHONY: all
-all: tlob_sym tlob_target
-
-tlob_sym: tlob_sym.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-tlob_target: tlob_target.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-.PHONY: run_tests
-run_tests: all
-	@./run_tlob_tests.sh
-
-.PHONY: clean
-clean:
-	$(RM) tlob_sym tlob_target
tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh +4 -75

93 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc +0 -1
index 4a1c18c7485a..be2f3555c30d 100644
@@ -3,7 +3,6 @@
 # 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
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc +0 -1
index afca157b5ea4..46c98ea03872 100644
@@ -3,7 +3,6 @@
 # description: Test tlob monitor detail running (running_ns dominates when task busy-spins between probes)
 # 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
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc +0 -1
index 0a6470b4cadb..7e82c7c7f98b 100644
@@ -3,7 +3,6 @@
 # description: Test tlob monitor detail sleeping (sleeping_ns dominates when task blocks between probes)
 # 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
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc +0 -1
index ef22fce700fc..43a33357f5ef 100644
@@ -3,7 +3,6 @@
 # description: Test tlob monitor detail waiting (waiting_ns dominates when task is preempted between probes)
 # 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
tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc +0 -1
index a798f3e9b3fa..3c606b354ad2 100644
@@ -3,7 +3,6 @@
 # description: Test tlob monitor multiple uprobe bindings (different offsets fire independently)
 # 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
tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc +0 -1
index 8a94bd679b88..ff8b736932ca 100644
@@ -3,7 +3,6 @@
 # description: Test tlob monitor budget violation (error_env_tlob and detail_env_tlob fire with correct fields)
 # 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
tools/testing/selftests/verification/tlob_sym.c renamed from tools/testing/selftests/verification/test.d/tlob/tlob_sym.c +0 -0
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
rename to tools/testing/selftests/verification/tlob_sym.c
tools/testing/selftests/verification/tlob_target.c renamed from tools/testing/selftests/verification/test.d/tlob/tlob_target.c +0 -0
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_target.c
rename to tools/testing/selftests/verification/tlob_target.c
Signature · 1 line
2.55.0
maintainer
#19 lore raw

Re: [PATCH v4 8/8] selftests/verification: add tlob selftests

Quoted 6 lines · Gabriele Monaco

I tried to refactor it to follow more standard selftest building, avoiding to maintain things ourselves. The only drawback is that you'd have to move the tlob_*.c files to selftests/verification , then we can still use RV_BINDIR but support it only via Makefile and run_tlob_tests.sh (so let's drop defining it in all tests and save inconvenience if things ever change).

I made a mistake in the Makefile (OUTPUT isn't populated before lib.mk), please find the fixed version attached.

And really, stop skipping the test result if there is a mistake in the build system, that should be LOUD. I removed all exit_unsupported for you (as I already mentioned, you can use chrt:program taskset:program for those instead, all other things are real errors).

From 088d371276d4bdb67d23941e178a351b6afa309a Mon Sep 17 00:00:00 2001
From: Gabriele Monaco <[email protected]>
Date: Wed, 22 Jul 2026 15:29:21 +0200
Subject: [PATCH] selftests/verification: Simplify tlob tests

Squash this with the other patch should you accept it! Drop nested Makefile in favour of lib.mk and simplify run_tlob_tests

---
 .../testing/selftests/verification/.gitignore |  4 +-
 tools/testing/selftests/verification/Makefile | 18 +----
 .../verification/test.d/tlob/Makefile         | 28 -------
 .../test.d/tlob/run_tlob_tests.sh             | 79 +------------------
 .../verification/test.d/tlob/uprobe_bind.tc   |  5 --
 .../test.d/tlob/uprobe_detail_running.tc      |  5 --
 .../test.d/tlob/uprobe_detail_sleeping.tc     |  5 --
 .../test.d/tlob/uprobe_detail_waiting.tc      | 10 +--
 .../verification/test.d/tlob/uprobe_multi.tc  |  1 -
 .../test.d/tlob/uprobe_violation.tc           |  1 -
 .../verification/{test.d/tlob => }/tlob_sym.c |  0
 .../{test.d/tlob => }/tlob_target.c           |  0
 12 files changed, 9 insertions(+), 147 deletions(-)
 delete mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_sym.c (100%)
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_target.c (100%)
tools/testing/selftests/verification/.gitignore +2 -2
index cbbd03ee16c7..d2f231f1bacb 100644
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 logs
-test.d/tlob/tlob_sym
-test.d/tlob/tlob_target
+tlob_sym
+tlob_target
tools/testing/selftests/verification/Makefile +2 -16
index 0b32bdfdb8db..aa17478076b1 100644
@@ -4,22 +4,8 @@ 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
+TEST_GEN_FILES := tlob_sym tlob_target
 
 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
+export RV_BINDIR := $(OUTPUT)
tools/testing/selftests/verification/test.d/tlob/Makefile deleted +0 -28
deleted file mode 100644
index 05a2d2599c4e..000000000000
@@ -1,28 +0,0 @@
-# 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)
-
-# For standalone execution via vng
-FTRACETEST := ../../../ftrace/ftracetest
-LOGDIR ?= ../../logs
-
-.PHONY: all
-all: tlob_sym tlob_target
-
-tlob_sym: tlob_sym.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-tlob_target: tlob_target.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-.PHONY: run_tests
-run_tests: all
-	@./run_tlob_tests.sh
-
-.PHONY: clean
-clean:
-	$(RM) tlob_sym tlob_target
tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh +4 -75

93 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc +0 -5
index 4a1c18c7485a..efa4ae47ce72 100644
@@ -3,17 +3,12 @@
 # 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=$!
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc +0 -5
index afca157b5ea4..ef53b1b70104 100644
@@ -3,17 +3,12 @@
 # description: Test tlob monitor detail running (running_ns dominates when task busy-spins between probes)
 # 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
 
 start_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 "$start_offset" ] || exit_unsupported
-[ -n "$stop_offset" ] || exit_unsupported
 
 "$UPROBE_TARGET" 5000 &
 busy_pid=$!
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc +0 -5
index 0a6470b4cadb..f97e18059e15 100644
@@ -3,17 +3,12 @@
 # description: Test tlob monitor detail sleeping (sleeping_ns dominates when task blocks between probes)
 # 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
 
 start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2>/dev/null)
 stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work_done 2>/dev/null)
-[ -n "$start_offset" ] || exit_unsupported
-[ -n "$stop_offset" ] || exit_unsupported
 
 "$UPROBE_TARGET" 5000 sleep &
 busy_pid=$!
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc +1 -9
index ef22fce700fc..0a5c41e57617 100644
@@ -1,22 +1,14 @@
 #!/bin/sh
 # SPDX-License-Identifier: GPL-2.0-or-later
 # description: Test tlob monitor detail waiting (waiting_ns dominates when task is preempted between probes)
-# requires: tlob:monitor
+# requires: tlob:monitor chrt:program taskset:program
 
-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
 
-command -v chrt    > /dev/null || exit_unsupported
-command -v taskset > /dev/null || exit_unsupported
-
 start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work 2>/dev/null)
 stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work_done 2>/dev/null)
-[ -n "$start_offset" ] || exit_unsupported
-[ -n "$stop_offset" ]  || exit_unsupported
 
 cpu=0
 
tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc +0 -1
index a798f3e9b3fa..3c606b354ad2 100644
@@ -3,7 +3,6 @@
 # description: Test tlob monitor multiple uprobe bindings (different offsets fire independently)
 # 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
tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc +0 -1
index 8a94bd679b88..ff8b736932ca 100644
@@ -3,7 +3,6 @@
 # description: Test tlob monitor budget violation (error_env_tlob and detail_env_tlob fire with correct fields)
 # 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
tools/testing/selftests/verification/tlob_sym.c renamed from tools/testing/selftests/verification/test.d/tlob/tlob_sym.c +0 -0
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
rename to tools/testing/selftests/verification/tlob_sym.c
tools/testing/selftests/verification/tlob_target.c renamed from tools/testing/selftests/verification/test.d/tlob/tlob_target.c +0 -0
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_target.c
rename to tools/testing/selftests/verification/tlob_target.c
Signature · 1 line
2.55.0
maintainer
#16 lore raw

Re: [PATCH v4 7/8] rv/tlob: add KUnit tests for the tlob monitor

In reply to [email protected]
Quoted 2 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

...

kernel/trace/rv/monitors/tlob/.kunitconfig line 8

Quoted 10 lines

+++ b/kernel/trace/rv/monitors/tlob/.kunitconfig

kernel/trace/rv/monitors/tlob/.kunitconfig new file +8 -0
@@ -0,0 +1,8 @@
+CONFIG_FTRACE=y
+CONFIG_HIGH_RES_TIMERS=y
+CONFIG_KUNIT=y
+CONFIG_MODULES=y
+CONFIG_RV=y
+CONFIG_RV_MON_TLOB=y
+CONFIG_TLOB_KUNIT_TEST=y
+CONFIG_UPROBES=y

Let's perhaps drop here what isn't a (mostly) direct dependency and make it slimmer? Others are requirements of those anyway (and if they stop being so, they aren't required for the test either).

CONFIG_KUNIT=y
CONFIG_UPROBES=y
CONFIG_RV=y
CONFIG_RV_MON_TLOB=y
CONFIG_TLOB_KUNIT_TEST=y

Quoted 45 lines
kernel/trace/rv/monitors/tlob/Kconfig +0 -0
index aa43382073d2..402ef2e5c076 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.

kernel/trace/rv/monitors/tlob/Kconfig +7 -0
+
+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.c +2 -1
index b45e84195131..a6f9c371646c 100644
@@ -708,7 +708,7 @@ static ssize_t tlob_monitor_read(struct file *file,
   * PATH may contain ':'; the last ':' separates path from offset.
   * Returns 0, -EINVAL, or -ERANGE.
   */
-static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
  				  char **path_out,
  				  loff_t *start_out, loff_t *stop_out)
  {
@@ -782,6 +782,7 @@ static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
  	*stop_out  = (loff_t)stop_val;
  	return 0;
  }
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line);


 /*
  * Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention).

kernel/trace/rv/monitors/tlob/tlob.c +1 -0
@@ -810,8 +811,9 @@ VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf,
 char **path_out,
  	*start_out = (loff_t)off;
  	return 0;
  }
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_remove_line);

kernel/trace/rv/monitors/tlob/tlob.c VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out, · line 816

Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c +1 -1
-VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf)
+static int tlob_create_or_delete_uprobe(char *buf)

That's a nit, but probably all the VISIBLE_IF_KUNIT / EXPORT_SYMBOL_IF_KUNIT should be added by this patch alone.

And definitely tlob_create_or_delete_uprobe shouldn't have it in the earlier patch just for it to be removed here.

Quoted 20 lines

 {
  loff_t offset_start, offset_stop;
  u64 threshold_ns;

kernel/trace/rv/monitors/tlob/tlob.c +0 -1
@@ -836,7 +838,6 @@ VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char
 *buf)
  	mutex_unlock(&tlob_uprobe_mutex);
  	return ret;
  }
-EXPORT_SYMBOL_IF_KUNIT(tlob_create_or_delete_uprobe);


 static ssize_t tlob_monitor_write(struct file *file, const char __user *ubuf,

kernel/trace/rv/monitors/tlob/tlob.h +0 -0
index fceeba748c85..15bdf3b7fade 100644
@@ -145,7 +145,9 @@ int tlob_start_task(struct task_struct *task, u64
 threshold_ns);
  int tlob_stop_task(struct task_struct *task);
Quoted 2 lines


 #if IS_ENABLED(CONFIG_KUNIT)

Any reason not to use IS_ENABLED(CONFIG_TLOB_KUNIT_TEST) ?

kernel/trace/rv/monitors/tlob/tlob.h int tlob_start_task(struct task_struct *task, u64 threshold_ns); · line 150

Quoted 4 lines
kernel/trace/rv/monitors/tlob/tlob.h +3 -1
-int tlob_create_or_delete_uprobe(char *buf);
+int tlob_parse_uprobe_line(char *buf, u64 *thr_out, char **path_out,
+			   loff_t *start_out, loff_t *stop_out);
+int tlob_parse_remove_line(char *buf, char **path_out, loff_t *start_out);

Same here, those can all be exported in this patch, no need to mention any KUNIT stuff earlier.

Quoted 10 lines

 #endif /* CONFIG_KUNIT */

 #endif /* _RV_TLOB_H */

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +0 -0
new file mode 100644
index 000000000000..7448f3fab959

kernel/trace/rv/monitors/tlob/tlob_kunit.c line 5

Quoted 5 lines
kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +5 -0
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the tlob RV monitor.
+ *
+ */

Besides the minor nits above, tests look good.

Thanks,
Gabriele

Quoted 143 lines
kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +19 -0
+#include <kunit/test.h>
+
+#include "tlob.h"
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+
+/* Valid "p PATH:START STOP threshold=NS" lines. */
+static const char * const tlob_parse_valid[] = {
+	"p /usr/bin/myapp:4768 4848 threshold=5000000",
+	"p /usr/bin/myapp:0x12a0 0x12f0 threshold=10000000",
+	"p /opt/my:app/bin:0x100 0x200 threshold=1000000",
+};
+
+/* Malformed "p ..." lines that must be rejected with -EINVAL. */
+static const char * const tlob_parse_invalid[] = {
+	"p :0x100 0x200 threshold=5000",
+	"p /usr/bin/myapp:0x100 threshold=5000",
+	"p /usr/bin/myapp:-1 0x200 threshold=5000",
+	"p /usr/bin/myapp:0x100 -1 threshold=5000000",	/* negative stop

offset */

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +37 -0
+	"p /usr/bin/myapp:0x100 0x200",
+	"p /usr/bin/myapp:0x100 0x100 threshold=5000",
+};
+
+/* threshold_ns out of valid range => -ERANGE. */
+static const char * const tlob_parse_out_of_range[] = {
+	"p /usr/bin/myapp:0x100 0x200 threshold=0",
+	"p /usr/bin/myapp:0x100 0x200 threshold=999",
+	"p /usr/bin/myapp:0x100 0x200 threshold=3600000000001",
+};
+
+/* Valid "-PATH:OFFSET_START" remove lines. */
+static const char * const tlob_remove_valid[] = {
+	"-/usr/bin/myapp:0x100",
+	"-/opt/my:app/bin:0x200",
+};
+
+/* Malformed remove lines that must be rejected with -EINVAL. */
+static const char * const tlob_remove_invalid[] = {
+	"-usr/bin/myapp:0x100",
+	"-/usr/bin/myapp",
+	"-/:0x100",
+	"-/usr/bin/myapp:-1",	/* negative offset */
+	"-/usr/bin/myapp:abc",
+};
+
+static void tlob_parse_valid_accepted(struct kunit *test)
+{
+	u64 thr;
+	char *path;
+	loff_t start, stop;
+	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_EQ(test, tlob_parse_uprobe_line(buf, &thr,

&path,

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +1 -0
+							     &start, &stop),

0);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +14 -0
+	}
+}
+
+static void tlob_parse_invalid_rejected(struct kunit *test)
+{
+	u64 thr;
+	char *path;
+	loff_t start, stop;
+	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_parse_uprobe_line(buf, &thr,

&path,

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +15 -1
+							     &start, &stop),
-EINVAL);
+	}
+}
+
+static void tlob_parse_out_of_range_rejected(struct kunit *test)
+{
+	u64 thr;
+	char *path;
+	loff_t start, stop;
+	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));
+		KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr,

&path,

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +14 -1
+							     &start, &stop),
-ERANGE);
+	}
+}
+
+static void tlob_remove_valid_accepted(struct kunit *test)
+{
+	char *path;
+	loff_t start;
+	char buf[128];
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(tlob_remove_valid); i++) {
+		strscpy(buf, tlob_remove_valid[i], sizeof(buf));
+		KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path,

&start), 0);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +13 -0
+	}
+}
+
+static void tlob_remove_invalid_rejected(struct kunit *test)
+{
+	char *path;
+	loff_t start;
+	char buf[128];
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(tlob_remove_invalid); i++) {
+		strscpy(buf, tlob_remove_invalid[i], sizeof(buf));
+		KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path,

&start), -EINVAL);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +21 -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),
+	KUNIT_CASE(tlob_remove_valid_accepted),
+	KUNIT_CASE(tlob_remove_invalid_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
#21 lore raw
In reply to [email protected]
Quoted 1 line · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Usually, within tracing, we try to make the commit titles consistent by capitalising them after the tag (as you did only for the cover letter).

Also try not to introduce new tags, we have poor consistency, but I prefer only to keep rv: when you modify kernel-side code (all your patches besides the selftests one, which is correct).
Try to follow what you see with git log --oneline <modified files>

Overall:

rv/da: introduce DA_MON_ALLOCATION_STRATEGY
rv: add generic uprobe infrastructure for RV monitors rv/tlob: add tlob model DOT file
...

becomes

rv: Introduce DA_MON_ALLOCATION_STRATEGY
rv: Add generic uprobe infrastructure for RV monitors
rv: Add tlob model DOT file
...

(note the uppercase A and I and the use of rv: , please apply these to other commits too).

Thanks,
Gabriele

Quoted 185 lines

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 emits an error when the elapsed time exceeds a configurable budget.

The monitor tracks three states (running, waiting, sleeping) driven by sched_switch and sched_wakeup tracepoints.  A single clock invariant, clk_elapsed < BUDGET_NS(), is enforced by a per-task hrtimer (HRTIMER_MODE_REL_HARD).  On expiry, error_env_tlob is emitted, followed by detail_env_tlob carrying a per-state time breakdown (running_ns, waiting_ns, sleeping_ns).

Tasks are registered for monitoring by writing to the tracefs monitor file:

  echo "p /path/to/binary:OFFSET_START OFFSET_STOP threshold=NS" \
       > /sys/kernel/tracing/rv/monitors/tlob/monitor

Two uprobes delimit the measured section without modifying the target binary.  Multiple uprobe pairs can be active simultaneously.

Series structure
----------------
Patch 1: rv/da: introduce DA_MON_ALLOCATION_STRATEGY Compile-time allocation strategy selector for per-object DA storage. Three strategies: DA_ALLOC_AUTO (default kmalloc), DA_ALLOC_POOL (pre-allocated llist pool), DA_ALLOC_MANUAL (caller-managed).

Patch 2: rv: add generic uprobe infrastructure for RV monitors Thin wrapper over uprobe_consumer for path resolution, registration, and safe synchronous teardown.

Patch 3: rv/tlob: add tlob model DOT file Graphviz DOT specification of the tlob hybrid automaton.

Patch 4: rv/ha: fix ha_invariant_passed_ns silent bypass Without this fix, ha_invariant_passed_ns() returns 0 on first call and leaves env_store at U64_MAX.  Every subsequent state transition resets the hrtimer to the full budget instead of the remaining time, so the budget never expires for tasks that transition between states.  This causes tlob's detail_sleeping and detail_waiting selftests to hang.

  This patch is a minimal fix in the current dual-representation framework. Nam's series [1] refactors ha_monitor.h to a single-representation model; once it lands, tlob will need to be updated to the new API and this patch will be superseded by Gabriele's planned framework-level initialisation improvement.  We carry it here to keep the series self-contained and testable.

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

Patches 5: rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL #ifndef guards for da_monitor_reset_hook and EVENT_NONE_LBL.

Patch 6: rv/tlob: add tlob hybrid automaton monitor Main tlob implementation.

Patches 7-8: Tests
  KUnit tests for the uprobe-line parser; seven ftrace-style selftests.

Changes since v3
----------------
Patch 1 (rv/da):
- Pool redesigned from spinlock+pointer-stack to lock-free llist (cmpxchg-based): pool release from RCU callback context is now safe without acquiring a lock
- DA_MON_POOL_SIZE alone selects pool mode; no separate DA_MON_ALLOCATION_STRATEGY define required for monitors
- All strategy dispatch uses plain C if() (no ifdeffery in functions)
- da_monitor_destroy_pool() calls da_monitor_reset_all() and da_monitor_sync_hook() before the hash iteration, preventing hrtimer UAF when timer callbacks are still in flight

Patch 2 (rv_uprobe):
- struct rv_uprobe now embeds struct uprobe_consumer directly; no separate heap allocation per probe, no rv_uprobe_free()
- path_put() moved after uprobe_register() to keep the inode referenced across the call as required by the uprobe API

Patch 6 (tlob monitor):
- da_get_target_by_id() wrapped in scoped_guard(rcu) in tlob_start_task(): hash_for_each_possible_rcu() requires an explicit RCU read-side CS on PREEMPT_RT where spinlock_t does not provide one
- tlob_monitor_read() releases mutex before simple_read_from_buffer() to avoid holding the mutex across copy_to_user()
- kmem_cache replaced with a pre-allocated llist pool for tlob_task_state, eliminating GFP_ATOMIC in the measurement window
- EXPORT_SYMBOL_IF_KUNIT added for tlob_parse_uprobe_line() and tlob_parse_remove_line() (required when CONFIG_TLOB_KUNIT_TEST=m)
- tlob_parse_remove_line() rejects negative offsets
- Unnecessary includes removed; Kconfig entry placed after the deadline monitors marker

Patch 7 (KUnit):
- Tests now call tlob_parse_uprobe_line() and tlob_parse_remove_line() directly; no tracepoint enrollment, no uprobe or filesystem state

Patch 8 (selftests):
- ftracetest walk-up algorithm (Gabriele's suggestion) incorporated; the separate verificationtest-ktap fix patch is dropped and the series shrinks from 9 to 8 patches
- All test scripts use "! cmd || false" throughout

Testing
-------
Tested on x86_64 with CONFIG_PREEMPT_RT=y (kernel 7.1 + virtme-ng):
- All KUnit tests pass
- All 7 selftests pass (uprobe_bind, uprobe_violation, uprobe_no_event, uprobe_multi, uprobe_detail_{running,sleeping,waiting})

Wen Yang (8):
  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: add tlob selftests

 Documentation/trace/rv/index.rst              |   1 + Documentation/trace/rv/monitor_tlob.rst       | 177 ++++ include/rv/da_monitor.h                       | 247 ++++- include/rv/ha_monitor.h                       |  24 +- include/rv/rv_uprobe.h                        |  93 ++ kernel/trace/rv/Kconfig                       |   8 + kernel/trace/rv/Makefile                      |   3 + kernel/trace/rv/monitors/nomiss/nomiss.c      |   6 +- kernel/trace/rv/monitors/tlob/.kunitconfig    |   8 + kernel/trace/rv/monitors/tlob/Kconfig         |  19 + kernel/trace/rv/monitors/tlob/tlob.c          | 970 ++++++++++++++++++ kernel/trace/rv/monitors/tlob/tlob.h          | 153 +++ kernel/trace/rv/monitors/tlob/tlob_kunit.c    | 139 +++ kernel/trace/rv/monitors/tlob/tlob_trace.h    |  52 + kernel/trace/rv/rv_trace.h                    |   1 + kernel/trace/rv/rv_uprobe.c                   | 104 ++ tools/testing/selftests/ftrace/ftracetest     |  18 +- .../testing/selftests/verification/.gitignore |   2 + tools/testing/selftests/verification/Makefile |  19 +- .../verification/test.d/tlob/Makefile         |  28 + .../test.d/tlob/run_tlob_tests.sh             |  90 ++ .../verification/test.d/tlob/tlob_sym.c       | 209 ++++ .../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 ++ tools/verification/models/tlob.dot            |  22 +

 31 files changed, 2839 insertions(+), 46 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 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh 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
#20 lore raw

Re: [PATCH v4 8/8] selftests/verification: add tlob selftests

In reply to [email protected]
Quoted 18 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Add seven ftrace-style test scripts for the tlob RV monitor under tools/testing/selftests/verification/test.d/tlob/.  The tests cover uprobe binding management, budget violation detection, and per-state time accounting.

Helper binaries tlob_target and tlob_sym are included in the same directory so the suite is self-contained.  tlob_sym resolves ELF symbol offsets for uprobe registration; tlob_target provides busy-spin, sleep, and preempt workloads.

ftracetest is updated to walk up the directory tree when searching for test.d/functions, so monitor subdirectories can be passed as the test directory without placing a dummy functions shim in each new directory.

---

...

Quoted 26 lines

+"$UPROBE_TARGET" 30000 &       # busy mode: tlob_busy_work fires every 200 ms +busy_pid=$!
+"$UPROBE_TARGET" 30000 sleep & # sleep mode: tlob_sleep_work fires every 200 ms

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +11 -0
+sleep_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
+
+# Binding A: 5 s budget on the busy probe - must not fire in 200 ms loops.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=5000000000" >

"$TLOB_MONITOR"

(quoted diff) +2 -0
+# Binding B: 10 us budget on the sleep probe - fires on first invocation.
+echo "p ${UPROBE_TARGET}:${sleep_offset} ${sleep_stop} threshold=10000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +6 -0
+
+# Wait up to 2 s for error_env_tlob from binding B.
+found=0; i=0
+while [ "$i" -lt 20 ]; do
+	sleep 0.1
+	grep -q "error_env_tlob" /sys/kernel/tracing/trace && { found=1;

break; }

Review

Quoted 2 lines
(quoted diff) +2 -0
+	i=$((i+1))
+done

Be careful when starting tasks and making assertions (e.g. grep) before killing them, in case those assertions fail, set -e would exit immediately and we'll skip stopping the tasks.

I'd say whenever you start tlob_target (or anything else) you can do:

  teardown() {
  	kill "$sleep_pid" 2>/dev/null || true; wait "$sleep_pid" 2>/dev/null || true
  	kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
  }
  trap teardown EXIT

just to be safe. This will kill and wait again if you need teardown before the end of the test, but that isn't an issue.

Quoted 33 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +14 -0
+
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null
+echo "-${UPROBE_TARGET}:${sleep_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$sleep_pid" 2>/dev/null || true; wait "$sleep_pid" 2>/dev/null || true
+kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+
+[ "$found" = "1" ]
+# error_env_tlob payload: clock variable must be present.
+# The event field can be "budget_exceeded" (hrtimer path) or the DA event
+# name ("sleep", "preempt") depending on which fires first; don't constrain

it.
+grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q "clk_elapsed="

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +4 -0
+# detail_env_tlob must appear alongside the error.
+grep -q "detail_env_tlob" /sys/kernel/tracing/trace
+
+echo > /sys/kernel/tracing/trace

diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file mode 100644
index 000000000000..bb2eeef17019

tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file +3 -0
@@ -0,0 +1,19 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor no spurious events without active uprobe

binding

tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc

Quoted 3 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file +3 -0
+# requires: tlob:monitor
+
+TLOB_MONITOR=monitors/tlob/monitor

This appears unused here.

Thanks,
Gabriele

author
#3 lore raw

Re: [PATCH v4 1/8] rv/da: introduce DA_MON_ALLOCATION_STRATEGY

In reply to Gabriele Monaco
Quoted 144 lines · Gabriele Monaco
Quoted 12 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Per-object DA storage allocation is currently limited to kmalloc on demand. Add a compile-time selector so monitors can choose among three strategies:

  DA_ALLOC_AUTO   (default) - kmalloc per object on the monitor path DA_ALLOC_POOL             - pre-allocated fixed-size llist pool; selected by defining DA_MON_POOL_SIZE DA_ALLOC_MANUAL           - caller pre-inserts storage; framework only links the target field

Quoted 4 lines

The pool strategy uses a lock-free llist (cmpxchg, no spinlock) so pool release is safe from RCU callback context without acquiring a lock. Moving allocation before the measurement window also prevents kmalloc latency.

Measurement window here is tlob's, remember RV isn't itself a measurement tool (yet, perhaps).
And isn't this also happening with other methods? We're trying to do allocation when the monitor starts (so before this measurement window).

I'm a bit puzzled since you're mentioning it many times, when have we done /allocations/ from RCU callbacks?
We surely do deallocations (kfree_rcu) but allocations are at most in RCU read-side critical sections and it's perfectly fine to take sleeping spinlocks there (that's a special kind of sleep under PREEMPT_RT). Besides I'm not quite sure spinlocks are that bad in RCU callbacks either (kfree surely takes them).
I'm not sure what you mean here but I don't think deallocation was ever a problem, was it?

Quoted 41 lines

nomiss is updated to DA_ALLOC_MANUAL.

---
  include/rv/da_monitor.h                  | 247 +++++++++++++++++++----
  include/rv/ha_monitor.h                  |   6 +
  kernel/trace/rv/monitors/nomiss/nomiss.c |   6 +-
  3 files changed, 221 insertions(+), 38 deletions(-)
include/rv/da_monitor.h +7 -0
index 34b8fba9ecd4..9c9acc123e3b 100644
@@ -14,7 +14,56 @@
  #ifndef _RV_DA_MONITOR_H
  #define _RV_DA_MONITOR_H
 
+/*
+ * Allocation strategies for RV_MON_PER_OBJ monitors.
+ *
+ * Select the strategy with a single define before including this header:
+ *
+ *   #define DA_MON_POOL_SIZE N          - pool mode; N pre-allocated slots.
+ *                                         Implies DA_ALLOC_POOL

automatically.
+ *   #define DA_MON_ALLOCATION_STRATEGY \
+ *           DA_ALLOC_MANUAL             - manual mode (see below).
+ *   (neither)                           - auto mode (default).
+ *
+ * Do not define both DA_MON_POOL_SIZE and DA_MON_ALLOCATION_STRATEGY.
+ *
+ * DA_ALLOC_AUTO   - lock-free kmalloc on the hot path; unbounded capacity.
+ * DA_ALLOC_POOL   - pre-allocated fixed-size pool; set by defining DA_MON_POOL_SIZE.
+ * DA_ALLOC_MANUAL - caller inserts storage before da_handle_start_event();
+ *                   the framework only links the target field.
+ */

include/rv/da_monitor.h +3 -0
+#define DA_ALLOC_AUTO   0
+#define DA_ALLOC_POOL   1
+#define DA_ALLOC_MANUAL 2

include/rv/da_monitor.h line 47

Quoted 10 lines
include/rv/da_monitor.h +10 -0
+
+#ifdef DA_MON_POOL_SIZE
+#ifdef DA_MON_ALLOCATION_STRATEGY
+#error "Define only one of DA_MON_POOL_SIZE or DA_MON_ALLOCATION_STRATEGY"
+#endif
+#if DA_MON_POOL_SIZE == 0
+#error "DA_MON_POOL_SIZE must be non-zero"
+#endif
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL
+#endif

Longer ifdefs should have comments to make them readable, like

   #endif /* DA_MON_POOL_SIZE */
Quoted 6 lines
include/rv/da_monitor.h +6 -0
+
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#endif
+
+/*

include/rv/da_monitor.h line 63

Quoted 10 lines
include/rv/da_monitor.h +10 -0
+ * Provide a zero default so da_monitor_init() can reference
+ * DA_MON_POOL_SIZE in a plain C if() without an #if guard; the
+ * compiler eliminates the dead branch.
+ */
+#ifndef DA_MON_POOL_SIZE
+#if DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL
+#error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be defined and non-zero"
+#endif
+#define DA_MON_POOL_SIZE 0
+#endif

Same here, better to have a comment.

Quoted 24 lines
include/rv/da_monitor.h +12 -0
+
  #include <rv/automata.h>
+#include <linux/llist.h>
  #include <linux/rv.h>
  #include <linux/stringify.h>
  #include <linux/bug.h>
@@ -66,6 +115,16 @@ static struct rv_monitor rv_this;
  #define da_monitor_sync_hook()
  #endif
 
+/*
+ * Per-object teardown hook, called after da_monitor_reset_all() +
+ * da_monitor_sync_hook() and before hash_del_rcu() for each entry.
+ * All HA timer callbacks have completed at this point.
+ * Define before including this header.  Default: 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

include/rv/da_monitor.h struct da_monitor_storage { · line 470

Quoted 8 lines
include/rv/da_monitor.h +5 -0
@@ -404,6 +463,12 @@ struct da_monitor_storage {
  	union rv_task_monitor rv;
  	struct hlist_node node;
  	struct rcu_head rcu;
+	/*
+	 * Mutually exclusive with rcu: rcu is live during the RCU callback
+	 * flight; free_node when the slot is in da_pool_free_list.
+	 * Present in all monitors to avoid #if-gating the pool helpers.
+	 */

I really don't understand much more about it by this comment, perhaps drop it here and make the separate usages clearer later?

Quoted 3 lines

By the way, if they are /really/ mutually exclusive and you want to save space, why not having them in an anonymous union?

Thanks.
The comment and the field are gone: the llist pool is replaced by a mempool (see below), so the pool slot no longer needs a dual-purpose field at all.

Quoted 43 lines
Quoted 26 lines
include/rv/da_monitor.h +1 -12
+	struct llist_node free_node;
  };
 
  #ifndef DA_MONITOR_HT_BITS
@@ -495,18 +560,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 *

include/rv/da_monitor.h static inline monitor_target · line 597

Quoted 8 lines
include/rv/da_monitor.h +4 -0
@@ -537,15 +590,79 @@ static inline monitor_target
 da_get_target_by_id(da_id_type id)
  	return mon_storage->target;
  }
 
+/*
+ * Lock-free llist (cmpxchg) rather than kmem_cache/mempool: on
+ * PREEMPT_RT spinlock_t becomes a sleeping lock, which is forbidden
+ * in the rcuc kthread context where RCU callbacks run.

This comment kind of implies we were using a kmem_cache, it's great for a changelog and helped me understand why you're doing this, but doesn't belong to the final version as is.

include/rv/da_monitor.h static inline monitor_target da_get_target_by_id(da_id_type id) · line 599

Quoted 3 lines
include/rv/da_monitor.h +3 -0
+ *
+ * Multiple producers (any context, any CPU) call llist_add; a single
+ * consumer (llist_del_first, serialised by the monitor's start lock)
Quoted 8 lines

Which monitor's start lock? There is no such a thing defined anywhere, maybe you wanted to say that monitors using this allocation scheme MUST lock during their start event. This by the way needs to be a global lock among all instances of the monitor (as you're indeed doing in tlob).

With that in mind, I don't really see how this is better than the original mempool: you still need to lock. There's nothing wrong in freeing stuff from RCU callbacks, that's what they're for.

Thanks, v5 drops the llist stack and uses a pre-allocated mempool:

- mempool_init_kmalloc_pool(&da_monitor_pool, DA_MON_POOL_SIZE, ...)

    eager-allocates all slots at init;

- mempool_alloc_preallocated() pops a slot without touching the allocator, bounding start latency and returning NULL when the pool is exhausted (-ENOSPC);
- mempool_free() from RCU-callback context is safe (no lock taken).

This is the mempool you pointed at in v4, with the allocation moved before the monitor start instead of on it.

Quoted 40 lines
Quoted 10 lines
include/rv/da_monitor.h +8 -0
+ * needs no additional synchronisation.
+ *
+ * Per-TU statics: each PER_OBJ monitor gets its own pool instance;
+ * da_pool_storage and da_pool_free_list are NULL/empty and the pool
+ * paths are dead code for non-pool monitors.
+ */
 
+static struct da_monitor_storage *da_pool_storage;
+static LLIST_HEAD(da_pool_free_list);

...

Quoted 15 lines

+++ b/kernel/trace/rv/monitors/nomiss/nomiss.c

include/rv/da_monitor.h +2 -2
@@ -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>

include/rv/da_monitor.h static void handle_sys_enter(void *data, struct pt_regs · line 219

Quoted 7 lines
include/rv/da_monitor.h +1 -1
@@ -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));

I'm starting to doubt this is the right thing to do. We do have the target (p) and that function doesn't check if the id already has a storage (which shouldn't happen but well, doesn't hurt checking).

Quoted 3 lines

This simplification is probably just not worth it, and doesn't look related to the rest of the change.

Reverted. nomiss.c is untouched in v5 (still DA_SKIP_AUTO_ALLOC and da_create_or_get()); patch 1 now only touches da_monitor.h.

--
Best wishes,
Wen

author
#7 lore raw

Re: [PATCH v4 2/8] rv: add generic uprobe infrastructure for RV monitors

In reply to Gabriele Monaco
Quoted 9 lines · Gabriele Monaco
Quoted 8 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

+++ b/kernel/trace/rv/Kconfig

(quoted diff) +0 -0
@@ -59,6 +59,13 @@ config RV_PER_TASK_MONITORS
  	  This option configures the maximum number of per-task RV monitors

that can run
    simultaneously.

Quoted 11 lines

kernel/trace/rv/Kconfig config RV_PER_TASK_MONITORS · line 67

Quoted 6 lines
kernel/trace/rv/Kconfig +6 -0
+config RV_UPROBE
+	bool
+	depends on RV && UPROBES
+	help
+	  Generic uprobe infrastructure for RV monitors.  Provides path
+	  resolution, registration, and safe synchronous teardown.

This isn't exposed, it's selected automatically when required, I don't even think the help text is visible (menuconfig doesn't show it), do we really need it?

Thanks, the bool config stays (it gates the obj build for monitors that need it)but the invisible help text is dropped in v5; the entry is now just:

  config RV_UPROBE
        bool
        depends on RV && UPROBES
Quoted 68 lines
Quoted 4 lines

+
 source "kernel/trace/rv/monitors/wip/Kconfig"
 source "kernel/trace/rv/monitors/wwnr/Kconfig"

...

Quoted 25 lines

+++ b/kernel/trace/rv/rv_uprobe.c

kernel/trace/rv/rv_uprobe.c new file +22 -0
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ *
+ * struct rv_uprobe embeds struct uprobe_consumer directly.  This is safe
+ * because rv_uprobe_sync() calls uprobe_unregister_sync(), which calls
+ * synchronize_rcu_tasks_trace().  handler_chain() runs under
+ * rcu_read_lock_trace(), so after synchronize_rcu_tasks_trace() returns,
+ * all in-flight handler_chain() iterations, including any pending
+ * uc->cons_node.next reads, have completed on all CPUs.  The caller may
+ * then free the struct containing rv_uprobe immediately.
+ */
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/namei.h>
+#include <linux/uprobes.h>
+#include <rv/rv_uprobe.h>
+
+/**
+ * rv_uprobe_register - initialise and register an uprobe
+ */
+int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe

*p)

kernel/trace/rv/rv_uprobe.c line 29

Quoted 7 lines
kernel/trace/rv/rv_uprobe.c new file +7 -0
+{
+	struct inode *inode;
+	struct path path;
+	int ret;
+
+	if (!p->uc.handler && !p->uc.ret_handler)
+		return -EINVAL;

uprobe_register() does this already, do we need it here too?

Quoted 10 lines
kernel/trace/rv/rv_uprobe.c new file +10 -0
+
+	ret = kern_path(binpath, LOOKUP_FOLLOW, &path);
+	if (ret)
+		return ret;
+
+	if (!d_is_reg(path.dentry)) {
+		path_put(&path);
+		return -EINVAL;
+	}
+

kernel/trace/rv/rv_uprobe.c line 49

Quoted 10 lines
kernel/trace/rv/rv_uprobe.c new file +10 -0
+	inode = d_real_inode(path.dentry);
+	p->inode = inode;
+
+	/*
+	 * uprobe_register() requires the inode (and mount) to remain
+	 * referenced across the call.  Keep the path alive until after
+	 * uprobe_register() has stored its own reference, then release it.
+	 */
+	p->uprobe = uprobe_register(inode, offset, 0, &p->uc);
+	path_put(&path);

I believe I was mistaken here, as sashiko pointed out, uprobe_register() doesn't keep a reference to the inode, (explicitly stated in it's docs:
"Caller of uprobe_register() is required to keep @inode (and the containing mount) referenced.").

Quoted 3 lines

We should probably revert back to holding path instead of inode and putting it after synchronous cleanup. That's also what BPF does.

Thanks.
Done in v5: struct rv_uprobe now keeps a struct path to the probed binary. rv_uprobe_register() resolves it via kern_path() and does not drop it after uprobe_register(); rv_uprobe_unregister() releases it with path_put() only after rv_uprobe_sync() has drained in-flight handlers, so no inode reference outlives the probe.

--
Best wishes,
Wen

Quoted 57 lines
Quoted 56 lines
kernel/trace/rv/rv_uprobe.c new file +55 -0
+	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_register);
+
+/**
+ * rv_uprobe_is_registered - test whether an uprobe is currently active
+ */
+bool rv_uprobe_is_registered(const struct rv_uprobe *p)
+{
+	return p && p->uprobe;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_is_registered);
+
+/**
+ * rv_uprobe_unregister - synchronously unregister a uprobe
+ */
+void rv_uprobe_unregister(struct rv_uprobe *p)
+{
+	if (!p || !p->uprobe)
+		return;
+
+	rv_uprobe_unregister_nosync(p);
+	rv_uprobe_sync();
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister);
+
+/**
+ * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
+ */
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p)
+{
+	if (!p || !p->uprobe)
+		return;
+
+	uprobe_unregister_nosync(p->uprobe, &p->uc);
+	p->uprobe = NULL;
+	p->inode = NULL;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister_nosync);
+
+/**
+ * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete
+ */
+void rv_uprobe_sync(void)
+{
+	uprobe_unregister_sync();
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_sync);
author
#14 lore raw

Re: [PATCH v4 6/8] rv/tlob: add tlob hybrid automaton monitor

In reply to Gabriele Monaco
Quoted 326 lines · Gabriele Monaco

kernel/trace/rv/monitors/tlob/tlob.c line 260

Quoted 11 lines · On Wed, 2026-07-08 at 23:38 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+/* Uprobe binding list; protected by tlob_uprobe_mutex. */
+static LIST_HEAD(tlob_uprobe_list);
+static DEFINE_MUTEX(tlob_uprobe_mutex);
+
+/*
+ * Serialises duplicate-check + da_handle_start_run_event() per pid.
+ * spinlock_t not raw_spinlock_t: uprobe handlers run under Tasks Trace
+ * SRCU (rcu_read_lock_trace()), which permits sleeping on PREEMPT_RT.
+ */

I don't think these comments add much value. spinlock_t is allowed in RCU critical sections anyway (it's some sort of preemption and RCU is preemptible under PREEMPT_RT).

Of course if it isn't really required we don't use a raw spinlock, you don't need a justification here.

Quoted 7 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+static DEFINE_SPINLOCK(tlob_start_lock);
+
+/* Per-uprobe-binding state: a start + stop probe pair for one binary region.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+struct tlob_uprobe_binding {
+	struct list_head	list;
+	u64			threshold_ns;

kernel/trace/rv/monitors/tlob/tlob.c line 276

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	char			binpath[TLOB_MAX_PATH];
+	loff_t			offset_start;
+	loff_t			offset_stop;
+	DECLARE_RV_UPROBE(start_probe);
+	DECLARE_RV_UPROBE(stop_probe);
+};
+
+/*
+ * Per-task teardown invoked by da_monitor_destroy() for each hash entry.
+ * CAS on stopping (0->1) claims exclusive cleanup ownership.

I find reading acronyms extremely annoying, since non-locking algorithms are already complex on their own, why don't you just say cmpxchg (which is searchable) instead of CAS. Sure CAS isn't an obscure acronym but I could find at least another 4 different definitions in the kernel tree.

Quoted 38 lines

+ *
+ * No per-entry ha_cancel_timer_sync(): da_monitor_destroy() calls
+ * da_monitor_reset_all() + synchronize_rcu() before this hook, and
+ * ha_mon_destroying prevents new timer callbacks from running.
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +33 -0
+static inline void tlob_extra_cleanup(struct da_monitor *da_mon)
+{
+	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;
+
+	put_task_struct(ws->task);
+	/*
+	 * da_monitor_destroy() has already called synchronize_rcu(); no
+	 * reader holds ws.  Return the slot directly without call_rcu.
+	 */
+	llist_add(&ws->free_node, &tlob_ws_free_list);
+}
+
+static inline bool __tlob_acc(struct task_struct *task, ktime_t now,
+			       enum tlob_acc_idx idx)
+{
+	struct tlob_task_state *ws;
+	unsigned long flags;
+
+	guard(rcu)();
+	ws = da_get_target_by_id(task->pid);
+	if (!ws)
+		return false;
+	raw_spin_lock_irqsave(&ws->entry_lock, flags);
+	ws->accs_ns[idx] += ktime_to_ns(ktime_sub(now, ws->last_ts));
+	ws->last_ts = now;
+	raw_spin_unlock_irqrestore(&ws->entry_lock, flags);

kernel/trace/rv/monitors/tlob/tlob.c line 324

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	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)
+{
+	return __tlob_acc(task, now, TLOB_ACC_RUNNING);
+}
+
+/* Accumulate waiting_ns for next; returns true if next is monitored. */

There's no next and prev here, plus you're describing __tlob_acc()'s behaviour 3 times, I'd say just document that (even if it isn't the primary facing function) and that's all.

Quoted 95 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +65 -0
+static inline bool tlob_acc_waiting(struct task_struct *task, ktime_t now)
+{
+	return __tlob_acc(task, now, TLOB_ACC_WAITING);
+}
+
+/*
+ * 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.
+ */
+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);
+
+	if (tlob_acc_running(prev, now))
+		da_handle_event(prev->pid, NULL,
+				prev_preempted ? preempt_tlob : sleep_tlob);
+	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)
+{
+	return __tlob_acc(task, now, TLOB_ACC_SLEEPING);
+}
+
+/*
+ * 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.
+ */
+static void handle_sched_wakeup(void *data, struct task_struct *p)
+{
+	ktime_t now = ktime_get();
+
+	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.
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +27 -0
+static void handle_sched_process_exit(void *data, struct task_struct *p,
+				       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, or -ENOSPC (pool at capacity).
+ */
+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 < TLOB_MIN_THRESHOLD_NS ||
+	    threshold_ns > TLOB_MAX_THRESHOLD_NS)
+		return -ERANGE;
+
+	/* Serialise duplicate-check + pool-slot claim; see tlob_start_lock.

*/

kernel/trace/rv/monitors/tlob/tlob.c line 424

Quoted 7 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+	guard(spinlock)(&tlob_start_lock);
+
+	/*
+	 * __da_get_mon_storage() uses hash_for_each_possible_rcu(), which
+	 * requires an RCU read-side critical section.  On PREEMPT_RT,
+	 * spinlock_t is an rt_mutex and does not satisfy this requirement.
+	 */

Also this is probably misleading, getting the monitor requires RCU, then the fact some in some configuration something other than read_lock_rcu() would work too (e.g. disabling preemption) is irrelevant.

Quoted 26 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +22 -0
+	scoped_guard(rcu) {
+		if (da_get_target_by_id(task->pid))
+			return -EALREADY;
+	}
+
+	/*
+	 * Both tlob_ws_alloc() and da_handle_start_run_event() pop from
+	 * pre-allocated pools of size TLOB_MAX_MONITORED; NULL return means
+	 * the pool is at capacity.
+	 */
+	ws = tlob_ws_alloc();
+	if (!ws)
+		return -ENOSPC;
+
+	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(),

(quoted diff) +2 -0
+	 * initialises the monitor, and delivers start_tlob in one step: the
+	 * generated ha_setup_invariants() resets clk_elapsed and arms the

timer.

Review

Quoted 2 lines
(quoted diff) +2 -0
+	 * Returns 0 if the da_monitor_storage pool is exhausted.
+	 */

And try not to be too specific about the internal implementation of the library, that can change without notice, we don't want to have to update all comments.

Here it is indeed non-trivial to assume no space if da_handle_start_run_event() fails, but that's only consequence of the fact that this ws is certainly new (by construction) and the monitor is
enabled: da_handle_start_run_event() can only fail if allocation failed.

da_handle_start_* functions return 1 if an event was handled, this is by the way not documented..

You could simply say something like:

da_handle_start_run_event() returns false if no event was handled, in
this case it can happen only if memory allocation failed

Quoted 54 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +40 -0
+	if (!da_handle_start_run_event(task->pid, ws, start_tlob)) {
+		put_task_struct(task);
+		tlob_ws_direct_return(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).
+ */
+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(WARN_ON_ONCE(!da_mon)))
+			return -ESRCH;
+
+		ha_mon = to_ha_monitor(da_mon);
+
+		/*
+		 * CAS (0->1) claims cleanup ownership under RCU (ws

guaranteed valid).

(quoted diff) +1 -0
+		 * _release pairs with atomic_read_acquire in

ha_setup_invariants.

kernel/trace/rv/monitors/tlob/tlob.c new file +11 -0
+		 */
+		if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
+			return -EAGAIN;
+	}
+	/*
+	 * ws and ha_mon are used below outside the RCU guard.  This is safe:
+	 * the winning CAS (stopping: 0->1) is the only path that frees ws,
+	 * and da_destroy_storage() below is the only call that returns the
+	 * pool slot.  No concurrent path can free either object.
+	 */
+

kernel/trace/rv/monitors/tlob/tlob.c line 512

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	/* Wait for in-flight timer callback before reading da_monitoring. */
+	ha_cancel_timer_sync(ha_mon);
+
+	/* Timer fired first -> budget exceeded; otherwise reset normally. */
+	scoped_guard(rcu) {
+		budget_exceeded = !da_monitoring(da_mon);
+		if (!budget_exceeded)
+			da_monitor_reset(da_mon);
+	}
+	da_destroy_storage(task->pid);

Here you're playing with the state machine, that looks fragile from a monitor code.
You could probably have budget_exceeded as part of your target and set it from tlob_reset_notify() when you know there was an expiration.

Perhaps "stop" could be yet another event in the state machine, bringing to the "stopped" state, which would automatically call reset(), you only need to grab ws->stopping before handling the event for tlob_reset_notify() to work as expected.
Though you'd probably still need ha_cancel_timer_sync() for the rare but not impossible in-flight timer not yet in RCU critical section..

Quoted 6 lines

I'd rather avoid as much as possible bringing in ha_mon/da_mon, but you probably cannot do better here, just you could avoid relying on da_monitoring().

What do you think?

Thanks.
I took this wholesale, it is exactly how v5 is structured now:

- tlob.dot gains a "stopped" state and a "stop" event (running -> stopped, only from running; stopped -> running on "start").
- tlob_stop_task() no longer resets or frees anything. It claims the window with a cmpxchg on ws->stopping, waits for the in-flight timer with ha_cancel_timer_sync() (as you predicted, still needed), then dispatches da_handle_event(pid, NULL, stop_tlob). The entry stays in the hash table, parked.
- budget_exceeded moved into the target: tlob_reset_notify() sets ws->budget_exceeded on genuine timer expiry, and tlob_stop_task() reads it back. No reliance on da_monitoring() anywhere on the stop path.
- A later "start" for the same task restarts the parked window in
place: same pool slot, hash entry and task_struct reference, no allocation. ha_setup_invariants() clears ws->stopping on the start transition, so __tlob_acc() keeps scheduler events off the parked entry (the model has no stopped self-loops).
- ws->stopping is per-window (cleared on restart); final teardown is claimed per-task through ws->destroying (tlob_extra_cleanup() on monitor disable switches to destroying too). This lets handle_sched_process_exit() call stop then destroy, and lets unbind reap tasks parked under a removed binding (tlob_unbind_reap(), closing the leak where parked tasks lingered until exit).

The stop path still touches ha_mon (to read the target and cancel the timer) but no longer touches da_mon or its monitoring flag.

--
Best wishes,
Wen