Skip to contentSkip to conversation

Add task latency over budget RV monitor

PATCH Superseded [email protected] 4 participants 27 messages Aug 20 – Aug 28

Subsystems (4)

v6 of 8 Superseded Latest is v8 →

lore
Share & tools
lore
#0 lore raw

v6 cover 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. A task may repeat the START/STOP sequence any number of times: the entry parks in the automaton's "stopped" state and later restarts in place, reusing the same pool slot (see Patch 6).

Series structure
----------------
Patch 1: rv: Introduce DA_MON_ALLOCATION_STRATEGY
Compile-time allocation strategy selector for per-object DA storage:
DA_ALLOC_AUTO (default kmalloc), DA_ALLOC_POOL (pre-allocated mempool), DA_ALLOC_MANUAL (caller-managed). DA_MON_POOL_SIZE alone selects pool mode.

Patch 2: rv: Add generic uprobe infrastructure for RV monitors
Thin wrapper over uprobe_consumer for path resolution, registration, and safe synchronous teardown. struct rv_uprobe embeds uprobe_consumer directly and holds the probed binary's path for its lifetime, avoiding a separate heap allocation per probe.

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

Patch 4: rv: Fix ha_invariant_passed_ns silent bypass of invariant check
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

Patch 5: rv: Make da_monitor_reset_hook and EVENT_NONE_LBL overridable
#ifndef guards for da_monitor_reset_hook and EVENT_NONE_LBL.

Patch 6: rv: Add tlob hybrid automaton monitor
Main tlob implementation, including the parked-window redesign described below.

Patches 7-9: Tests
KUnit tests for the uprobe-line parser; eight ftrace-style selftests for tlob; the ftracetest walk-up change needed to run them from the verification subdirectory.

Changes since v5
-----------------
Most of the fixes below address issues Sashiko ([email protected]) flagged against this series; each is described on its own merits below.

Patch 1: Reworded the DA_ALLOC_POOL comment; it could be misread as
allowing the allocating path to run from scheduling context.
Patch 3: Fixed a duplicate "running" node in the DOT file that made
rvgen mark it, instead of "stopped", as the model's final state.
Patch 6: Fixed automaton_tlob.final_states, generated before the DOT
fix above and inheriting the same wrong value (trace-only, no functional impact).
Patch 7: Reworked per Gabriele's v4 feedback. Dropped
CONFIG_TLOB_KUNIT_TEST; tlob_parse_uprobe_line()/ tlob_parse_remove_line() stay static and are reached through a new rv_tlob_kunit_ops struct, exported the same way nomiss.c/sco.c expose rv_nomiss_ops/rv_sco_ops under the shared CONFIG_RV_MONITORS_KUNIT_TEST. tlob_kunit.c now follows the monitors/*/*_kunit.c convention and is included into rv_monitors_test.c instead of building standalone.
Patch 8: Fixed tracefs paths under ftracetest --rv mode; added a
VERIFICATIONTEST_BINDIR fallback for installed kselftest trees; made uprobe_detail_waiting.tc skip on single-CPU machines instead of pinning a SCHED_FIFO-99 busy loop; guarded tlob_target.c's empty stop-probe bodies against being optimised away; added ELF program-header bounds checking to tlob_sym.c.
Patch 9: Fixed an infinite loop and a local root privilege-escalation
window (sourcing test.d/functions from a world-writable directory) in the walk-up loop.

Testing
-------
Environment: x86_64, CONFIG_PREEMPT_RT=y, kernel built with virtme-ng (vng --build), selftests built on the host and run inside the VM:
- KUnit: rv_mon suite (CONFIG_RV_MONITORS_KUNIT_TEST), rv_test_tlob case covering the uprobe-line parser
- 8 ftrace-style selftests: uprobe_bind, uprobe_violation, uprobe_no_event, uprobe_multi, uprobe_detail_{running,sleeping, waiting}, uprobe_restart
Both suites pass. The pre-existing rv_* tests in the same directory keep passing as well.

v4: https://lore.kernel.org/all/[email protected]/
v3: https://lore.kernel.org/all/[email protected]/
v2: https://lore.kernel.org/all/[email protected]/
v1: https://lore.kernel.org/all/[email protected]/

Wen Yang (9):
rv: Introduce DA_MON_ALLOCATION_STRATEGY
rv: Add generic uprobe infrastructure for RV monitors
rv: Add tlob model DOT file
rv: Fix ha_invariant_passed_ns silent bypass of invariant check
rv: Make da_monitor_reset_hook and EVENT_NONE_LBL overridable
rv: Add tlob hybrid automaton monitor
rv: Add KUnit tests for the tlob monitor selftests/verification: Add tlob selftests selftests/ftrace: Walk up to find test.d/functions when a subdirectory
is passed

 Documentation/trace/rv/index.rst              |    1 +
 Documentation/trace/rv/monitor_tlob.rst       |  194 +++
 include/rv/da_monitor.h                       |  152 ++-
 include/rv/ha_monitor.h                       |   13 +-
 include/rv/rv_uprobe.h                        |   90 ++
 kernel/trace/rv/Kconfig                       |    5 +
 kernel/trace/rv/Makefile                      |    2 +
 kernel/trace/rv/monitors/tlob/Kconfig         |   12 +
 kernel/trace/rv/monitors/tlob/tlob.c          | 1145 +++++++++++++++++
 kernel/trace/rv/monitors/tlob/tlob.h          |  149 +++
 kernel/trace/rv/monitors/tlob/tlob_kunit.c    |   87 ++
 kernel/trace/rv/monitors/tlob/tlob_kunit.h    |   17 +
 kernel/trace/rv/monitors/tlob/tlob_trace.h    |   48 +
 kernel/trace/rv/rv_monitors_test.c            |    2 +
 kernel/trace/rv/rv_trace.h                    |    1 +
 kernel/trace/rv/rv_uprobe.c                   |   91 ++
 tools/testing/selftests/ftrace/ftracetest     |   26 +-
 .../testing/selftests/verification/.gitignore |    2 +
 tools/testing/selftests/verification/Makefile |    4 +
 .../test.d/tlob/run_tlob_tests.sh             |   23 +
 .../verification/test.d/tlob/uprobe_bind.tc   |   39 +
 .../test.d/tlob/uprobe_detail_running.tc      |   53 +
 .../test.d/tlob/uprobe_detail_sleeping.tc     |   52 +
 .../test.d/tlob/uprobe_detail_waiting.tc      |   76 ++
 .../verification/test.d/tlob/uprobe_multi.tc  |   63 +
 .../test.d/tlob/uprobe_no_event.tc            |   17 +
 .../test.d/tlob/uprobe_restart.tc             |   79 ++
 .../test.d/tlob/uprobe_violation.tc           |   69 +
 .../testing/selftests/verification/tlob_sym.c |  225 ++++
 .../selftests/verification/tlob_target.c      |  138 ++
 tools/verification/models/tlob.dot            |   24 +
 31 files changed, 2884 insertions(+), 15 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/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_kunit.h
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_trace.h
 create mode 100644 kernel/trace/rv/rv_uprobe.c
 create mode 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
 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_restart.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
 create mode 100644 tools/testing/selftests/verification/tlob_sym.c
 create mode 100644 tools/testing/selftests/verification/tlob_target.c
 create mode 100644 tools/verification/models/tlob.dot

base-commit: 785095112f4198de49760552374f364043c8dbdf

Signature · 1 line
2.25.1
#1 lore raw

PATCH v6 1/9 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 mempool;
selected by defining DA_MON_POOL_SIZE
DA_ALLOC_MANUAL - caller pre-inserts storage; framework
only links the target field

---
 include/rv/da_monitor.h | 152 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 142 insertions(+), 10 deletions(-)
include/rv/da_monitor.h +142 -10
index e3cf85c9ce55..9e84f53b81cb 100644
@@ -14,7 +14,54 @@
 #ifndef _RV_DA_MONITOR_H
 #define _RV_DA_MONITOR_H
 
+/*
+ * Allocation strategies for RV_MON_PER_OBJ monitors, selected by defining
+ * one of the following before including this header (never both):
+ *
+ *   DA_MON_POOL_SIZE N           - pool mode, N pre-allocated slots
+ *   DA_MON_ALLOCATION_STRATEGY   - explicit strategy (see below)
+ *   (neither)                    - auto mode (default)
+ *
+ * DA_ALLOC_AUTO   - kmalloc on demand; unbounded.
+ * DA_ALLOC_POOL   - pre-allocated fixed-size pool; capped at DA_MON_POOL_SIZE
+ *                   entries.  Uses mempool_alloc_preallocated() (spinlock_t);
+ *                   the start-event handler must run in task context.
+ *                   Other event handlers may run in any context as they use
+ *                   da_handle_event() and never allocate new storage.
+ * DA_ALLOC_MANUAL - caller pre-inserts storage; framework links the target.
+ */
+#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 /* DA_MON_POOL_SIZE */
+
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#ifdef DA_SKIP_AUTO_ALLOC
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL
+#else
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#endif
+#endif /* DA_MON_ALLOCATION_STRATEGY */
+
+/* Zero default keeps pool-mode conditionals compile-time constant. */
+#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 /* DA_MON_POOL_SIZE */
+
 #include <rv/automata.h>
+#include <linux/mempool.h>
 #include <linux/rv.h>
 #include <rv/kunit.h>
 #include <linux/stringify.h>
@@ -67,6 +114,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
@@ -543,6 +600,59 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
 	return mon_storage->target;
 }
 
+/*
+ * Pre-allocated mempool for DA_ALLOC_POOL monitors: DA_MON_POOL_SIZE
+ * slots, eager-allocated at init.  mempool_alloc_preallocated() pops a
+ * slot without touching the allocator (bounded start latency; NULL when
+ * exhausted).  mempool_free() is safe from RCU-callback context.
+ * Non-pool monitors get a zero-initialised mempool_t; pool paths compile
+ * away.
+ */
+static mempool_t da_monitor_pool;
+
+static void da_pool_return_cb(struct rcu_head *head)
+{
+	struct da_monitor_storage *ms =
+		container_of(head, struct da_monitor_storage, rcu);
+
+	mempool_free(ms, &da_monitor_pool);
+}
+
+/*
+ * 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;
+
+	if (da_mon)
+		return da_mon;
+
+	mon_storage = mempool_alloc_preallocated(&da_monitor_pool);
+	if (!mon_storage)
+		return NULL;
+	memset(mon_storage, 0, sizeof(*mon_storage));
+
+	mon_storage->id = id;
+	mon_storage->target = target;
+
+	/* Single consumer under the caller's lock; duplicate is a double-start bug. */
+	existing = __da_get_mon_storage(id);
+	if (WARN_ON_ONCE(existing)) {
+		mempool_free(mon_storage, &da_monitor_pool);
+		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
  *
@@ -564,7 +674,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 *))
@@ -590,6 +703,9 @@ static inline void da_monitor_reset_state_all(void)
 static inline int da_monitor_init(void)
 {
 	hash_init(da_monitor_ht);
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+		return mempool_init_kmalloc_pool(&da_monitor_pool, DA_MON_POOL_SIZE,
+						 sizeof(struct da_monitor_storage));
 	return 0;
 }
 
@@ -607,21 +723,37 @@ static inline void da_monitor_destroy(void)
 	 * pending, we can safely assume no concurrent user.
 	 */
 	hash_for_each_safe(da_monitor_ht, bkt, tmp, mon_storage, node) {
+		da_extra_cleanup(&mon_storage->rv.da_mon);
 		hash_del_rcu(&mon_storage->node);
-		kfree(mon_storage);
+		if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+			mempool_free(mon_storage, &da_monitor_pool);
+		else
+			kfree(mon_storage);
+	}
+
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL) {
+		rcu_barrier();
+		mempool_exit(&da_monitor_pool);
 	}
 }
 
 /*
- * 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 link per-object monitor storage.
+ *
+ * Called only from da_handle_start_run_event(); must run in task context
+ * for DA_ALLOC_AUTO and DA_ALLOC_POOL (both take a spinlock_t internally).
+ * Subsequent event handlers use da_handle_event() and never allocate.
  */
-#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 */
 
Signature · 1 line
2.25.1
#3 lore raw

PATCH v6 2/9 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. The struct holds a struct path for the probed binary so that the inode and its mount remain referenced for the full uprobe lifetime; uprobe_register() does not take its own reference to the inode. The path is released in rv_uprobe_unregister_nosync() after the consumer has been removed.

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.

---
 include/rv/rv_uprobe.h      | 90 ++++++++++++++++++++++++++++++++++++
 kernel/trace/rv/rv_uprobe.c | 91 +++++++++++++++++++++++++++++++++++++
 2 files changed, 181 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 +90 -0
new file mode 100644
index 000000000000..d0a9079ac5be
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Wen Yang <[email protected]> */
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ *
+ */
+
+#ifndef _RV_UPROBE_H
+#define _RV_UPROBE_H
+
+#include <linux/path.h>
+#include <linux/types.h>
+#include <linux/uprobes.h>
+
+struct pt_regs;
+
+/**
+ * struct rv_uprobe - embeddable uprobe handle for RV monitors
+ *
+ * Embed via DECLARE_RV_UPROBE() and pass &name to rv_uprobe_register().
+ * The caller may free the containing struct after rv_uprobe_unregister()
+ * (or rv_uprobe_unregister_nosync() + rv_uprobe_sync()) returns.
+ *
+ * @uc:     embedded uprobe_consumer; set handler/ret_handler before registering
+ * @uprobe: registered uprobe pointer (NULL when not registered)
+ * @path:   path of the probed binary, held until unregistration
+ */
+struct rv_uprobe {
+	struct uprobe_consumer	uc;
+	struct uprobe		*uprobe;
+	struct path		path;
+};
+
+/* 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 path
+ * (p->path) is NOT released here; the caller must call rv_uprobe_sync()
+ * followed by path_put(&p->path) before freeing the containing struct.
+ * Use rv_uprobe_unregister() to handle both in one step.
+ */
+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 runs
+ * synchronize_rcu_tasks_trace() and 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/rv_uprobe.c new file +91 -0

91 lines not on this page · raw message

Signature · 1 line
2.25.1
#6 lore raw

PATCH v6 3/9 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), five transitions (switch_in, preempt, wakeup, sleep), and a single clock invariant clk_elapsed < BUDGET_NS() active in all states.

---
 tools/verification/models/tlob.dot | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)
 create mode 100644 tools/verification/models/tlob.dot
tools/verification/models/tlob.dot new file +24 -0
new file mode 100644
index 000000000000..515695599cf0
@@ -0,0 +1,24 @@
+digraph state_automaton {
+	center = true;
+	size = "7,11";
+	{node [shape = plaintext, style=invis, label=""] "__init_stopped"};
+	{node [shape = plaintext] "running"};
+	{node [shape = plaintext] "waiting"};
+	{node [shape = plaintext] "sleeping"};
+	{node [shape = plaintext] "stopped"};
+	"__init_stopped" -> "stopped";
+	"running"  [label = "running\nclk_elapsed < BUDGET_NS()",  color = green3];
+	"waiting"  [label = "waiting\nclk_elapsed < BUDGET_NS()"];
+	"sleeping" [label = "sleeping\nclk_elapsed < BUDGET_NS()"];
+	"stopped"  [label = "stopped"];
+	"running"  -> "sleeping" [ label = "sleep" ];
+	"running"  -> "waiting"  [ label = "preempt" ];
+	"waiting"  -> "running"  [ label = "switch_in" ];
+	"sleeping" -> "waiting"  [ label = "wakeup" ];
+	"running"  -> "stopped"  [ label = "stop" ];
+	"stopped"  -> "running"  [ label = "start;reset(clk_elapsed)" ];
+	{ rank = min ;
+		"__init_stopped";
+		"stopped";
+	}
+}
Signature · 1 line
2.25.1
#9 lore raw

PATCH v6 4/9 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().

This is a stopgap: once the RV framework reworks the per-env clock guard, this first-invocation reset should be subsumed.

---
 include/rv/ha_monitor.h | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)
include/rv/ha_monitor.h +3 -2
index 6e1c7fe5449a..e1738d199b28 100644
@@ -355,7 +355,7 @@ static inline u64 ha_invariant_passed_ns(struct ha_monitor *ha_mon, enum envs en
 	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);
 	return ha_get_env(ha_mon, env, time_ns);
 }
 
@@ -375,6 +375,7 @@ static inline bool ha_check_invariant_jiffy(struct ha_monitor *ha_mon, enum envs
 {
 	return time_after64(READ_ONCE(ha_mon->env_store[env]), get_jiffies_64() - expire_jiffy);
 }
+
 /*
  * ha_invariant_passed_jiffy - prepare the invariant and return the time since reset
  */
@@ -383,7 +384,7 @@ static inline u64 ha_invariant_passed_jiffy(struct ha_monitor *ha_mon, enum envs
 	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);
 	return ha_get_env(ha_mon, env, time_ns);
 }
 
Signature · 1 line
2.25.1
#12 lore raw

PATCH v6 5/9 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 | 8 ++++++++
 1 file changed, 8 insertions(+)
include/rv/ha_monitor.h +8 -0
index e1738d199b28..807b981eb548 100644
@@ -36,8 +36,14 @@ 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
+
+/* Overrides must still call ha_monitor_reset_env() or cancel the timer. */
+#ifndef da_monitor_reset_hook
 #define da_monitor_reset_hook ha_monitor_reset_env
+#endif
+#ifndef da_monitor_sync_hook
 #define da_monitor_sync_hook() synchronize_rcu()
+#endif
 
 #if !defined(HA_SKIP_AUTO_CLEANUP) && RV_MON_TYPE == RV_MON_PER_TASK
 /*
@@ -75,7 +81,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
#14 lore raw

PATCH v6 6/9 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.

Four-state automaton (running, waiting, sleeping, stopped) driven by sched_switch/sched_wakeup tracepoints and a user-visible start/stop
pair: "stop" only fires from running and parks the window in stopped, where a later "start" restarts it in place (same pool slot, no re-registration); both callers of stop run while the task is on CPU. A single clk_elapsed < BUDGET_NS() invariant is enforced by a per-task HRTIMER_MODE_REL_HARD timer; on expiry the monitor records a per-state 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". A pre-allocated mempool hard-caps concurrently monitored tasks at TLOB_MAX_MONITORED (past it, fresh starts return -ENOSPC) with allocation-free start/stop on the uprobe hot path.

---
 Documentation/trace/rv/index.rst           |    1 +
 Documentation/trace/rv/monitor_tlob.rst    |  194 ++++
 kernel/trace/rv/Kconfig                    |    5 +
 kernel/trace/rv/Makefile                   |    2 +
 kernel/trace/rv/monitors/tlob/Kconfig      |   12 +
 kernel/trace/rv/monitors/tlob/tlob.c       | 1137 ++++++++++++++++++++
 kernel/trace/rv/monitors/tlob/tlob.h       |  149 +++
 kernel/trace/rv/monitors/tlob/tlob_trace.h |   48 +
 kernel/trace/rv/rv_trace.h                 |    1 +
 9 files changed, 1549 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 +194 -0

194 lines not on this page · raw message

kernel/trace/rv/Kconfig +5 -0
index efa930f94ea4..222b3bea8079 100644
@@ -84,8 +84,13 @@ 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_UPROBE
+	bool
+	depends on RV && UPROBES
+
 config RV_REACTORS
 	bool "Runtime verification reactors"
 	default y
kernel/trace/rv/Makefile +2 -0
index cdbf68c84f5a..cd0ec11f0e05 100644
@@ -21,7 +21,9 @@ 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_WAKEUP) += monitors/wakeup/wakeup.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
 obj-$(CONFIG_RV_REACT_PRINTK) += reactor_printk.o
 obj-$(CONFIG_RV_REACT_PANIC) += reactor_panic.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 +1137 -0

1,137 lines not on this page · raw message

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

149 lines not on this page · raw message

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

48 lines not on this page · raw message

kernel/trace/rv/rv_trace.h +1 -0
index 2f8a932432c9..4bfa39717cef 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
#18 lore raw

PATCH v6 7/9 Add KUnit tests for the tlob monitor

In reply to [email protected]

From: Wen Yang <[email protected]>

Add a test case to the shared rv_monitors_test.c suite (gated by the existing CONFIG_RV_MONITORS_KUNIT_TEST, same as nomiss/sco/etc.) covering the uprobe-line parser.

tlob_parse_uprobe_line() and tlob_parse_remove_line() stay static and
unconditional: production code (tlob_create_or_delete_uprobe()) always needs them, so they can't be compiled out, and un-hiding them via VISIBLE_IF_KUNIT would tie their linkage to CONFIG_KUNIT while nothing else in tlob.c depends on it. Instead, expose them to the test only through a const rv_tlob_kunit_ops struct of function pointers, built and exported the same way nomiss.c/sco.c expose their rv_<mon>_ops:
the struct itself, its declaration in tlob_kunit.h, and its use in tlob_kunit.c are all gated on the single CONFIG_RV_MONITORS_KUNIT_TEST symbol, so there is no separate prototype to go out of sync with a visibility macro.

tlob_kunit.c follows the monitors/*/*_kunit.c convention: one rv_test_tlob() case textually included into rv_monitors_test.c, guarded by IS_REACHABLE(CONFIG_RV_MON_TLOB) with an rv_test_stub() fallback so the shared suite still builds when RV_MON_TLOB=n (that symbol is independent of CONFIG_RV_MONITORS_KUNIT_TEST). Drop the per-monitor TLOB_KUNIT_TEST Kconfig entry, .kunitconfig, and Makefile line that a standalone test module would have needed.

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

---
 kernel/trace/rv/monitors/tlob/tlob.c       | 24 ++++--
 kernel/trace/rv/monitors/tlob/tlob_kunit.c | 87 ++++++++++++++++++++++
 kernel/trace/rv/monitors/tlob/tlob_kunit.h | 17 +++++
 kernel/trace/rv/rv_monitors_test.c         |  2 +
 4 files changed, 122 insertions(+), 8 deletions(-)
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.h
kernel/trace/rv/monitors/tlob/tlob.c +16 -8
index 08b1bee884cc..18150cbf57a5 100644
@@ -20,7 +20,6 @@
 #include <linux/namei.h>
 #include <linux/rv.h>
 #include <linux/slab.h>
-#include <kunit/visibility.h>
 #include <rv/instrumentation.h>
 #include <rv/rv_uprobe.h>
 #include <rv.h>
@@ -877,9 +876,9 @@ static ssize_t tlob_monitor_read(struct file *file,
  * PATH may contain ':'; the last ':' separates path from offset.
  * Returns 0, -EINVAL, or -ERANGE.
  */
-VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
-					    char **path_out,
-					    loff_t *start_out, loff_t *stop_out)
+static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+				  char **path_out,
+				  loff_t *start_out, loff_t *stop_out)
 {
 	unsigned long long thr = 0, stop_val = 0;
 	long long start_val;
@@ -951,13 +950,12 @@ VISIBLE_IF_KUNIT 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).
  */
-VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out,
-					    loff_t *start_out)
+static int tlob_parse_remove_line(char *buf, char **path_out,
+				  loff_t *start_out)
 {
 	char *binpath, *colon;
 	long long off;
@@ -980,7 +978,17 @@ 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);
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+#include <kunit/visibility.h>
+#include "tlob_kunit.h"
+
+const struct rv_tlob_kunit_ops rv_tlob_kunit_ops = {
+	.parse_uprobe_line = tlob_parse_uprobe_line,
+	.parse_remove_line = tlob_parse_remove_line,
+};
+EXPORT_SYMBOL_IF_KUNIT(rv_tlob_kunit_ops);
+#endif
 
 static int tlob_create_or_delete_uprobe(char *buf)
 {
kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +87 -0

87 lines not on this page · raw message

kernel/trace/rv/monitors/tlob/tlob_kunit.h new file +17 -0
new file mode 100644
index 000000000000..4c1081871ea3
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __TLOB_KUNIT_H
+#define __TLOB_KUNIT_H
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+
+#include <linux/types.h>
+
+extern const struct rv_tlob_kunit_ops {
+	int (*parse_uprobe_line)(char *buf, u64 *thr_out, char **path_out,
+				 loff_t *start_out, loff_t *stop_out);
+	int (*parse_remove_line)(char *buf, char **path_out, loff_t *start_out);
+} rv_tlob_kunit_ops;
+
+#endif
+
+#endif /* __TLOB_KUNIT_H */
kernel/trace/rv/rv_monitors_test.c +2 -0
index 3ad11195e664..791df0fe03e3 100644
@@ -153,6 +153,7 @@ static void rv_test_dummy(struct kunit *test)
 #include "monitors/nomiss/nomiss_kunit.c"
 #include "monitors/pagefault/pagefault_kunit.c"
 #include "monitors/sleep/sleep_kunit.c"
+#include "monitors/tlob/tlob_kunit.c"
 
 static struct kunit_case rv_mon_test_cases[] = {
 	KUNIT_CASE(rv_test_dummy),
@@ -163,6 +164,7 @@ static struct kunit_case rv_mon_test_cases[] = {
 	KUNIT_CASE(rv_test_nomiss),
 	KUNIT_CASE(rv_test_pagefault),
 	KUNIT_CASE(rv_test_sleep),
+	KUNIT_CASE(rv_test_tlob),
 	{}
 };
 
Signature · 1 line
2.25.1
#20 lore raw

PATCH v6 8/9 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.

run_tlob_tests.sh is a thin wrapper: it builds the helpers and delegates all argument handling to ftracetest via exec "$FTRACETEST" -K --rv "$@".

All .tc files that start background processes set up a trap teardown EXIT immediately after launch so background tasks are cleaned up even when a subsequent assertion fails under set -e.

---
 .../testing/selftests/verification/.gitignore |   2 +
 tools/testing/selftests/verification/Makefile |   4 +
 .../test.d/tlob/run_tlob_tests.sh             |  23 ++
 .../verification/test.d/tlob/uprobe_bind.tc   |  39 +++
 .../test.d/tlob/uprobe_detail_running.tc      |  53 +++++
 .../test.d/tlob/uprobe_detail_sleeping.tc     |  52 ++++
 .../test.d/tlob/uprobe_detail_waiting.tc      |  76 ++++++
 .../verification/test.d/tlob/uprobe_multi.tc  |  63 +++++
 .../test.d/tlob/uprobe_no_event.tc            |  17 ++
 .../test.d/tlob/uprobe_restart.tc             |  79 ++++++
 .../test.d/tlob/uprobe_violation.tc           |  69 ++++++
 .../testing/selftests/verification/tlob_sym.c | 225 ++++++++++++++++++
 .../selftests/verification/tlob_target.c      | 138 +++++++++++
 13 files changed, 840 insertions(+)
 create mode 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
 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_restart.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
 create mode 100644 tools/testing/selftests/verification/tlob_sym.c
 create mode 100644 tools/testing/selftests/verification/tlob_target.c
tools/testing/selftests/verification/.gitignore +2 -0
index 2659417cb2c7..d2f231f1bacb 100644
@@ -1,2 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 logs
+tlob_sym
+tlob_target
tools/testing/selftests/verification/Makefile +4 -0
index aa8790c22a71..41445d15b86a 100644
@@ -5,4 +5,8 @@ TEST_PROGS := verificationtest-ktap
 TEST_FILES := test.d settings
 EXTRA_CLEAN := $(OUTPUT)/logs/*
 
+TEST_GEN_FILES := tlob_sym tlob_target
+
 include ../lib.mk
+
+export VERIFICATIONTEST_BINDIR := $(OUTPUT)
tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file +23 -0
new file mode 100755
index 000000000000..d8f96c4ecc2c
@@ -0,0 +1,23 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# Standalone runner for tlob selftests
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+BINDIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+FTRACETEST="$(cd "$SCRIPT_DIR/../../../ftrace" && pwd)/ftracetest"
+
+# Rebuild helpers when the Makefile is present (source-tree run).
+# Skip silently in installed kselftest environments where source is absent.
+if [ -f "$BINDIR/Makefile" ]; then
+	make -C "$BINDIR" tlob_target tlob_sym
+fi
+
+if [ ! -x "$BINDIR/tlob_target" ] || [ ! -x "$BINDIR/tlob_sym" ]; then
+	echo "ERROR: tlob_target or tlob_sym not found in $BINDIR" >&2
+	exit 1
+fi
+
+export VERIFICATIONTEST_BINDIR="$BINDIR"
+
+exec "$FTRACETEST" -K --rv "$SCRIPT_DIR" "$@"
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +39 -0
new file mode 100644
index 000000000000..950ef0497542
@@ -0,0 +1,39 @@
+#!/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
+
+# Fall back to the verification directory relative to FTRACETEST_ROOT when not
+# set by make (e.g. in installed kselftest environments).
+: "${VERIFICATIONTEST_BINDIR:="$FTRACETEST_ROOT/../verification"}"
+
+UPROBE_TARGET="${VERIFICATIONTEST_BINDIR}/tlob_target"
+TLOB_SYM="${VERIFICATIONTEST_BINDIR}/tlob_sym"
+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)
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+teardown() {
+    kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+}
+trap teardown EXIT
+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
+
+echo 0 > monitors/tlob/enable
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc new file +53 -0

53 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc new file +52 -0

52 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc new file +76 -0

76 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +63 -0

63 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file +17 -0
new file mode 100644
index 000000000000..23f8abeca090
@@ -0,0 +1,17 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor no spurious events without active uprobe binding
+# requires: tlob:monitor
+
+echo 1 > ../events/rv/error_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+sleep 0.5
+
+! grep -q "error_env_tlob" ../trace || false
+
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/error_env_tlob/enable
+echo > ../trace
tools/testing/selftests/verification/test.d/tlob/uprobe_restart.tc new file +79 -0

79 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +69 -0

69 lines not on this page · raw message

tools/testing/selftests/verification/tlob_sym.c new file +225 -0

225 lines not on this page · raw message

tools/testing/selftests/verification/tlob_target.c new file +138 -0

138 lines not on this page · raw message

Signature · 1 line
2.25.1
#23 lore raw

PATCH v6 9/9 selftests/ftrace: Walk up to find test.d/functions when a subdirectory is passed

In reply to [email protected]

From: Wen Yang <[email protected]>

When a test directory that does not itself contain test.d/functions is passed to ftracetest (e.g. verification/test.d/tlob/), ftracetest fell back to its own functions file and lost the rv-specific check_requires handling for ':monitor' and ':reactor' requirements.

Walk up the directory tree from OPT_TEST_DIR until a directory containing test.d/functions is found. This allows monitor subdirectories to be passed directly as the test root without placing a functions shim in each one.

The RV verification suite uses this so that

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

can pass test.d/tlob/ to ftracetest and have it source verification/test.d/functions (which understands ':monitor'/':reactor').

---
 tools/testing/selftests/ftrace/ftracetest | 26 ++++++++++++++++++++---
 1 file changed, 23 insertions(+), 3 deletions(-)
tools/testing/selftests/ftrace/ftracetest +23 -3
index 0a56bf209f6c..8f9d9291bf4c 100755
@@ -159,9 +159,29 @@ 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 that contains
+    # test.d/functions.  This allows a monitor subdirectory (e.g.
+    # verification/test.d/tlob/) to be passed directly without placing a
+    # dummy functions shim in each new subdirectory.
+    # Security: only walk within directories owned by root or the current user
+    # to prevent sourcing a functions file planted in a world-writable ancestor.
+    dir=$(realpath "$OPT_TEST_DIR" 2>/dev/null || echo "$OPT_TEST_DIR")
+    while [ "$dir" != "/" ] && [ "$dir" != "//" ]; do
+      if [ -f "$dir/test.d/functions" ]; then
+        # Verify the directory is not world-writable to prevent LPE.
+        dirperms=$(stat -c "%a" "$dir" 2>/dev/null || stat -f "%Lp" "$dir" 2>/dev/null)
+        case "$dirperms" in
+          *2|*3|*6|*7) ;; # world-writable: skip this candidate
+          *)
+            TOP_DIR=$dir
+            TEST_DIR=$TOP_DIR/test.d
+            break
+            ;;
+        esac
+      fi
+      dir=$(dirname "$dir")
+    done
   fi
 }
 
Signature · 1 line
2.25.1
#7 lore raw

Re: [PATCH v6 3/9] rv: Add tlob model DOT file

In reply to [email protected]
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] The implemented DOT model contradicts explicit claims made in the commit message regarding…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The implemented DOT model contradicts explicit claims made in the commit message regarding states, transitions, and invariants. --

--- Patch [3]: [PATCH v6 3/9] rv: Add tlob model DOT file --- commit 8a3ea61817d2834335f8a9230c5ffaf2a08353b7
Author: Wen Yang <[email protected]>

rv: Add tlob model DOT file

This commit adds the Graphviz DOT specification of the tlob hybrid automaton to the kernel's verification models.

Quoted 5 lines

Add the Graphviz DOT specification of the tlob hybrid automaton to tools/verification/models/. The model has three states (running, waiting, sleeping), five transitions (switch_in, preempt, wakeup, sleep), and a single clock invariant clk_elapsed < BUDGET_NS() active in all states.

[Severity: Medium]
Does this description accurately reflect the implemented model?

The text mentions three states, five transitions, and an invariant active in all states. However, the included DOT file defines a fourth "stopped" state, two additional transitions ("stop" and "start"), and omits the invariant from the "stopped" state:

tools/verification/models/tlob.dot:

	"running"  [label = "running\nclk_elapsed < BUDGET_NS()",  color = green3];
	"waiting"  [label = "waiting\nclk_elapsed < BUDGET_NS()"];
	"sleeping" [label = "sleeping\nclk_elapsed < BUDGET_NS()"];
	"stopped"  [label = "stopped"];
	"running"  -> "sleeping" [ label = "sleep" ];
	"running"  -> "waiting"  [ label = "preempt" ];
	"waiting"  -> "running"  [ label = "switch_in" ];
	"sleeping" -> "waiting"  [ label = "wakeup" ];
	"running"  -> "stopped"  [ label = "stop" ];
	"stopped"  -> "running"  [ label = "start;reset(clk_elapsed)" ];

Could the commit message be updated to match the actual automaton implemented in the code?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
#21 lore raw

Re: [PATCH v6 8/9] selftests/verification: Add tlob selftests

In reply to [email protected]
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] Using `$(nproc) - 1` to determine a target CPU ID assumes that CPU IDs are contiguous and start…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Using $(nproc) - 1 to determine a target CPU ID assumes that CPU IDs are contiguous and start from 0, which breaks on sparse affinity masks or offline CPUs.
- [Medium] The ELF parser in tlob_sym.c lacks sufficient bounds checks when validating headers and iterating over sections, leading to out-of-bounds memory accesses. --

--- Patch [8]: [PATCH v6 8/9] selftests/verification: Add tlob selftests --- commit cb49807af49abc34cb32dee7d7e1d06575cff80a
Author: Wen Yang <[email protected]>

selftests/verification: Add tlob selftests

This commit adds seven ftrace-style test scripts for the tlob RV monitor, covering uprobe binding management, budget violation detection, and per-state time accounting. It also includes a standalone runner and C helper programs for target execution and ELF symbol parsing.

Quoted 5 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc new file +0 -0
new file mode 100644
index 0000000000000..798a04012b6ea

[ ... ]

tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc line 23

Quoted 6 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc new file +6 -0
+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)
+
+# Pick the last CPU to avoid cpu0 which is used by vng infrastructure.
+cpu=$(($(nproc) - 1))
+

[Severity: Medium]
Does this CPU ID calculation safely handle systems with sparse CPU affinity masks or offline CPUs?

If a system has non-contiguous CPU IDs (for example, if CPU 1 is offline but CPUs 0 and 2 are online), nproc will evaluate to 2. This calculation would yield CPU 1, which might cause the subsequent taskset commands to fail with an invalid argument error since CPU 1 is unavailable.

Quoted 4 lines
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc new file +4 -0
+echo 1 > ../events/rv/detail_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace

[ ... ]

Quoted 5 lines
tools/testing/selftests/verification/tlob_sym.c new file +0 -0
new file mode 100644
index 0000000000000..2d9561331d2f9

[ ... ]

tools/testing/selftests/verification/tlob_sym.c line 83

Quoted 6 lines
tools/testing/selftests/verification/tlob_sym.c new file +6 -0
+		for (int pass = 0; pass < 2 && !found; pass++) {
+			const char *target = pass ? ".dynsym" : ".symtab";
+
+			for (si = 0; si < ehdr->e_shnum && !found; si++) {
+				Elf64_Shdr *sh = &shdrs[si];
+				const char *name = shstrtab + sh->sh_name;

[Severity: Medium]
Is there a risk of an out-of-bounds read when accessing the string table here?

It appears sh->sh_name is used as an offset into shstrtab and passed to strcmp() without bounds checking against the actual string table size. Could a malformed binary cause a segmentation fault when the tool attempts to read beyond the mapped memory?

tools/testing/selftests/verification/tlob_sym.c line 88

Quoted 5 lines
tools/testing/selftests/verification/tlob_sym.c new file +5 -0
+
+				if (strcmp(name, target) != 0)
+					continue;
+
+				Elf64_Shdr *strtab_sh = &shdrs[sh->sh_link];

[Severity: Medium]
Could this array indexing access memory outside the section header table?

The sh->sh_link field is used directly as an index into shdrs. Since there is no verification that sh->sh_link is less than ehdr->e_shnum, a malformed ELF file could cause the tool to read arbitrary memory when mapping headers.

Quoted 4 lines
tools/testing/selftests/verification/tlob_sym.c new file +4 -0
+				const char *strtab = (char *)map + strtab_sh->sh_offset;
+				Elf64_Sym *syms = (Elf64_Sym *)((char *)map + sh->sh_offset);
+				uint64_t nsyms = sh->sh_size / sizeof(Elf64_Sym);
+				uint64_t j;

[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
#24 lore raw

Re: [PATCH v6 9/9] selftests/ftrace: Walk up to find test.d/functions when a subdirectory is passed

In reply to [email protected]
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] The directory traversal logic fails to check directory ownership and fails open on `stat` error…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The directory traversal logic fails to check directory ownership and fails open on stat error, allowing arbitrary code execution as root via an attacker-controlled ancestor directory. --

--- Patch [9]: [PATCH v6 9/9] selftests/ftrace: Walk up to find test.d/functions when a subdirectory is passed --- commit 71305ea3fc4b8a7294d098d4bcdd8624fd21ca70
Author: Wen Yang <[email protected]>

selftests/ftrace: Walk up to find test.d/functions when a subdirectory is passed

This commit modifies ftracetest to walk up the directory tree from a passed test directory until it finds a test.d/functions file. This allows monitor subdirectories to be used directly as the test root without needing dummy functions shims.

Quoted 4 lines
tools/testing/selftests/ftrace/ftracetest +0 -0
index 0a56bf209f6c0..8f9d9291bf4ce 100755

[ ... ]

Quoted 13 lines
tools/testing/selftests/ftrace/ftracetest +6 -3
@@ -159,9 +159,29 @@ 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 that contains
+    # test.d/functions.  This allows a monitor subdirectory (e.g.
+    # verification/test.d/tlob/) to be passed directly without placing a
+    # dummy functions shim in each new subdirectory.
+    # Security: only walk within directories owned by root or the current user

tools/testing/selftests/ftrace/ftracetest parse_opts() { # opts · line 177

Quoted 10 lines
tools/testing/selftests/ftrace/ftracetest +10 -0
+    # to prevent sourcing a functions file planted in a world-writable ancestor.
+    dir=$(realpath "$OPT_TEST_DIR" 2>/dev/null || echo "$OPT_TEST_DIR")
+    while [ "$dir" != "/" ] && [ "$dir" != "//" ]; do
+      if [ -f "$dir/test.d/functions" ]; then
+        # Verify the directory is not world-writable to prevent LPE.
+        dirperms=$(stat -c "%a" "$dir" 2>/dev/null || stat -f "%Lp" "$dir" 2>/dev/null)
+        case "$dirperms" in
+          *2|*3|*6|*7) ;; # world-writable: skip this candidate
+          *)
+            TOP_DIR=$dir

[Severity: High]
Does this logic actually check directory ownership as mentioned in the comments?

It appears the code only checks if the directory is world-writable, but omits validating that it is owned by root or the current user. An attacker could potentially create a directory with standard 0755 permissions, plant a malicious test.d/functions script, and if a privileged user or automated test harness runs ftracetest against a subdirectory, the script would traverse up and source the payload.

Additionally, if the stat command fails for any reason, dirperms will be empty. Would an empty string match the *) fallback case and cause the script to silently fail open, accepting even a world-writable directory?

Quoted 9 lines
tools/testing/selftests/ftrace/ftracetest +7 -0
+            TEST_DIR=$TOP_DIR/test.d
+            break
+            ;;
+        esac
+      fi
+      dir=$(dirname "$dir")
+    done
   fi
 }
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=9
#10 lore raw

Re: [PATCH v6 4/9] rv: Fix ha_invariant_passed_ns silent bypass of invariant check

In reply to [email protected]
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:

New issues:
- [High] Check-then-act (TOCTOU) race condition in first-invocation guard reset allows concurrent thread execution to overwrite an initialized invariant timestamp.

Pre-existing issues:
- [High] ha_check_invariant_ns calculates time_ns - expire_ns, which can underflow and cause valid invariants to falsely fail when the system uptime is less than expire_ns.
- [High] ha_start_timer_ns and ha_start_timer_jiffy suffer from integer underflow when elapsed time (passed) exceeds the timeout (expire), causing monitor timers to be scheduled far into the future (or at a random time) instead of expiring immediately. --

--- Patch [4]: [PATCH v6 4/9] rv: Fix ha_invariant_passed_ns silent bypass of invariant check --- commit 1e15107a6865e6fddc264988e2a5c771a535486a
Author: Wen Yang <[email protected]>

rv: Fix ha_invariant_passed_ns silent bypass of invariant check

This patch fixes a silent bypass in ha_invariant_passed_ns() and ha_invariant_passed_jiffy() by initializing env_store to the current clock on the first invocation. It replaces the early return with a call to ha_reset_clk_ns() to establish the invariant guard.

include/rv/ha_monitor.h static inline u64 ha_invariant_passed_ns(struct ha_monitor *ha_mon, enum envs en · line 358

Quoted 10 lines
include/rv/ha_monitor.h +1 -1
index 6e1c7fe5449a3..e1738d199b289 100644
@@ -355,7 +355,7 @@ static inline u64 ha_invariant_passed_ns(struct ha_monitor *ha_mon, enum envs en
 	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);

[Severity: High]
Does this lockless state check and modification allow a check-then-act race?

Looking at ha_invariant_passed_ns() in include/rv/ha_monitor.h, if concurrent threads execute this path for the same multi-state monitor, could Thread A and Thread B both evaluate ha_monitor_env_invalid() as true?

This could lead to one thread blindly overwriting the invariant start time initialized by the other, inappropriately pushing the epoch forward and masking violations.

include/rv/ha_monitor.h static inline bool ha_check_invariant_jiffy(struct ha_monitor *ha_mon, enum envs · line 378

Quoted 8 lines
include/rv/ha_monitor.h +1 -0
 	return ha_get_env(ha_mon, env, time_ns);
 }
 
@@ -375,6 +375,7 @@ static inline bool ha_check_invariant_jiffy(struct ha_monitor *ha_mon, enum envs
 {
 	return time_after64(READ_ONCE(ha_mon->env_store[env]), get_jiffies_64() - expire_jiffy);
 }
+

[Severity: High]
This isn't a bug introduced by this patch, but is there a risk of integer underflow in the related ns check function?

include/rv/ha_monitor.h:ha_check_invariant_ns() {
    return READ_ONCE(ha_mon->env_store[env]) >= time_ns - expire_ns;
}

If the system uptime (time_ns) is smaller than expire_ns (e.g. for a monitor started early during boot with a large timeout budget), could time_ns - expire_ns underflow?

This would immediately trigger a false positive invariant violation that could cause a kernel panic if the panic reactor is configured.

[Severity: High]
This is a pre-existing issue, but could integer underflow occur in the timer setup functions when the elapsed time exceeds the timeout?

include/rv/ha_monitor.h:ha_start_timer_ns() {
    u64 passed = ha_invariant_passed_ns(ha_mon, env, time_ns);
    ha_start_timer_jiffy(ha_mon, ENV_MAX_STORED,
                         nsecs_to_jiffies(expire - passed + TICK_NSEC - 1), time_ns);
}

If passed is greater than expire, expire - passed underflows to a massive unsigned 64-bit value.

When passed to nsecs_to_jiffies() (or jiffies_to_nsecs() in ha_start_timer_jiffy), this can result in timers being scheduled far into the future (or at a random time) instead of expiring immediately, nullifying the monitor's ability to enforce latency budgets.

include/rv/ha_monitor.h static inline u64 ha_invariant_passed_jiffy(struct ha_monitor *ha_mon, enum envs · line 387

Quoted 9 lines
include/rv/ha_monitor.h +1 -1
 /*
  * ha_invariant_passed_jiffy - prepare the invariant and return the time since reset
  */
@@ -383,7 +384,7 @@ static inline u64 ha_invariant_passed_jiffy(struct ha_monitor *ha_mon, enum envs
 	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);

[Severity: High]
Can concurrent threads race when resetting the jiffy clock here as well?

Similarly to ha_invariant_passed_ns(), could multiple CPUs evaluating ha_monitor_env_invalid() at the same time in ha_invariant_passed_jiffy() cause an initialized invariant timestamp to be silently overwritten?

Quoted 3 lines
include/rv/ha_monitor.h +0 -0
 	return ha_get_env(ha_mon, env, time_ns);
 }
 
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
#4 lore raw

Re: [PATCH v6 2/9] rv: Add generic uprobe infrastructure for RV monitors

In reply to [email protected]
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] API state tracking design in `rv_uprobe` leaves stale pointers on error and breaks state…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] API state tracking design in rv_uprobe leaves stale pointers on error and breaks state validation for batch cleanup, trapping callers into a double-free or memory leak.
- [Medium] The commit message explicitly states a factually incorrect resource lifecycle that contradicts the code and creates an RCU use-after-free hazard if implemented as described. --

--- Patch [2]: [PATCH v6 2/9] rv: Add generic uprobe infrastructure for RV monitors --- commit be3e5812586424192ba2d5028d63b1a9b959099b
Author: Wen Yang <[email protected]>

rv: Add generic uprobe infrastructure for RV monitors

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.

Quoted 6 lines

struct rv_uprobe embeds struct uprobe_consumer directly, avoiding a separate heap allocation per probe. The struct holds a struct path for the probed binary so that the inode and its mount remain referenced for the full uprobe lifetime; uprobe_register() does not take its own reference to the inode. The path is released in rv_uprobe_unregister_nosync() after the consumer has been removed.

[Severity: Medium]
This isn't a bug in the code, but this statement contradicts the actual implementation. Looking at the code, rv_uprobe_unregister_nosync() explicitly leaves the path held to prevent RCU use-after-free hazards, relying on the caller to release it after rv_uprobe_sync(). Could this description be updated to match the code?

Quoted 6 lines
kernel/trace/rv/rv_uprobe.c new file +0 -0
new file mode 100644
index 0000000000000..b412a8e28a6ec
@@ -0,0 +1,91 @@

[ ... ]

kernel/trace/rv/rv_uprobe.c line 29

Quoted 13 lines
kernel/trace/rv/rv_uprobe.c new file +13 -0
+int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe *p)
+{
+	struct inode *inode;
+	int ret;
+
+	ret = kern_path(binpath, LOOKUP_FOLLOW, &p->path);
+	if (ret)
+		return ret;
+
+	if (!d_is_reg(p->path.dentry)) {
+		path_put(&p->path);
+		return -EINVAL;
+	}

[Severity: Medium]
Do these error paths leave a stale pointer in the caller's struct?

When path_put() is called, p->path.dentry is not cleared. If a caller is using an array of struct rv_uprobe and tries to batch clean up failed registrations, could they accidentally use this stale pointer and cause a double-free?

Quoted 14 lines
kernel/trace/rv/rv_uprobe.c new file +14 -0
+
+	inode = d_real_inode(p->path.dentry);
+
+	/* uprobe_register() takes no inode reference; the path is held in p->path */
+	p->uprobe = uprobe_register(inode, offset, 0, &p->uc);
+	if (IS_ERR(p->uprobe)) {
+		ret = PTR_ERR(p->uprobe);
+		p->uprobe = NULL;
+		path_put(&p->path);
+		return ret;
+	}
+
+	return 0;
+}

[ ... ]

kernel/trace/rv/rv_uprobe.c line 61

Quoted 4 lines
kernel/trace/rv/rv_uprobe.c new file +4 -0
+void rv_uprobe_unregister(struct rv_uprobe *p)
+{
+	if (!p || !p->uprobe)
+		return;

[Severity: Medium]
Does this early return cause a memory leak if the caller previously used rv_uprobe_unregister_nosync()?

Since rv_uprobe_unregister_nosync() clears p->uprobe to NULL, a caller attempting to use rv_uprobe_unregister() on it later will hit this early return. This skips path_put(&p->path), permanently leaking the path reference.

Quoted 8 lines
kernel/trace/rv/rv_uprobe.c new file +8 -0
+
+	uprobe_unregister_nosync(p->uprobe, &p->uc);
+	p->uprobe = NULL;
+	rv_uprobe_sync();
+	path_put(&p->path);
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister);
+

kernel/trace/rv/rv_uprobe.c line 79

Quoted 10 lines
kernel/trace/rv/rv_uprobe.c new file +10 -0
+/**
+ * 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;

[Severity: Medium]
If p->uprobe is cleared here, how does the caller know which paths need to be released later?

The documentation suggests batching deregistrations. However, if p->uprobe is set to NULL here, any subsequent iteration over an array to conditionally call path_put() has no reliable state indicator left to determine if the path was actually held, potentially leading to incorrect cleanup.

Quoted 2 lines
kernel/trace/rv/rv_uprobe.c new file +2 -0
+	/* path held; caller must call rv_uprobe_sync() then path_put(&p->path) */
+}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#13 lore raw

Re: [PATCH v6 5/9] rv: Make da_monitor_reset_hook and EVENT_NONE_LBL overridable

In reply to [email protected]
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Non-synchronous timer cancellation combined with synchronize_rcu() or kfree_rcu() leads to a Use-After-Free during monitor teardown. --

--- Patch [5]: [PATCH v6 5/9] rv: Make da_monitor_reset_hook and EVENT_NONE_LBL overridable --- commit 693c5f5465506e090a4b8b44d5e3c72769ec0685
Author: Wen Yang <[email protected]>

rv: Make da_monitor_reset_hook and EVENT_NONE_LBL overridable

This commit wraps da_monitor_reset_hook and EVENT_NONE_LBL with ifndef guards to allow HA-based monitors to provide their own implementations. It enables monitors like tlob to define custom reset hooks for tasks such as cancelling per-task hrtimers.

Quoted 6 lines
include/rv/ha_monitor.h +0 -0
index e1738d199b289..807b981eb548a 100644
@@ -36,8 +36,14 @@ static bool ha_monitor_handle_constraint(struct da_monitor *da_mon,
 					 da_id_type id);

include/rv/ha_monitor.h static bool ha_monitor_handle_constraint(struct da_monitor *da_mon, · line 46

Quoted 10 lines
include/rv/ha_monitor.h +6 -0
 #define da_monitor_event_hook ha_monitor_handle_constraint
 #define da_monitor_init_hook ha_monitor_init_env
+
+/* Overrides must still call ha_monitor_reset_env() or cancel the timer. */
+#ifndef da_monitor_reset_hook
 #define da_monitor_reset_hook ha_monitor_reset_env
+#endif
+#ifndef da_monitor_sync_hook
 #define da_monitor_sync_hook() synchronize_rcu()
+#endif

[Severity: High]
This isn't a bug introduced by this patch, but is it safe to rely on synchronize_rcu() to wait for cancelled timers during monitor teardown?

Looking at the global teardown path:

include/rv/da_monitor.h:da_monitor_destroy() {
    ...
    da_monitor_reset_all();
    da_monitor_sync_hook();
    ...
}

And per-object teardown:

include/rv/da_monitor.h:da_destroy_storage() {
    ...
    da_monitor_reset_hook(&mon_storage->rv.da_mon);
    hash_del_rcu(&mon_storage->node);
    if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
        call_rcu(&mon_storage->rcu, da_pool_return_cb);
    else
        kfree_rcu(mon_storage, rcu);
    ...
}

Both paths cancel timers non-synchronously in ha_monitor_reset_env():

include/rv/ha_monitor.h:ha_monitor_reset_env() {
    ...
    ha_cancel_timer(ha_mon);
    ...
}

They then rely on RCU grace periods to wait for executing callbacks, assuming the callbacks execute within an RCU read-side critical section:

include/rv/ha_monitor.h:__ha_monitor_timer_callback() {
    ...
    guard(rcu)();
    ...
}

If the timer callback is dispatched but preempted or delayed before entering its RCU read-side critical section, synchronize_rcu() will not wait for it. When the callback resumes, couldn't it access the freed monitor storage and cause a use-after-free?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=5
#15 lore raw

Re: [PATCH v6 6/9] rv: Add tlob hybrid automaton monitor

In reply to [email protected]
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:

New issues:
- [Critical] Concurrent task exit causes Use-After-Free and list corruption in tlob_unbind_reap.
- [High] Race between tlob_stop_task claiming the window and the HA timer callback suppresses the detail trace and hides budget violations.
- [High] tlob_ws_lock is a spinlock_t acquired in tracepoint and uprobe exception contexts, causing sleeping in atomic context on PREEMPT_RT.

Pre-existing issues:
- [Critical] Concurrent lockless calls to hash_add_rcu and hash_del_rcu on da_monitor_ht corrupt the hash bucket lists. --

--- Patch [6]: [PATCH v6 6/9] rv: Add tlob hybrid automaton monitor --- commit ecef387f80f071ac66f0456fa83acfd88043286c
Author: Wen Yang <[email protected]>
Subject: rv: Add tlob hybrid automaton monitor

This patch introduces tlob (task latency over budget), a per-task hybrid automaton RV monitor. It tracks wall-clock time across a user-delimited code section using uprobes and emits warnings when the elapsed time exceeds a configured threshold.

Quoted 5 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +0 -0
new file mode 100644
index 0000000000000..08b1bee884cc4

[ ... ]

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

Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+/* Serialises tlob_task_state ownership: restart, detach, unbind reap. */
+static DEFINE_SPINLOCK(tlob_ws_lock);

[Severity: High]
Could this spinlock cause sleeping in atomic context warnings on PREEMPT_RT kernels?

On PREEMPT_RT, spinlock_t maps to a sleepable rt_mutex. However, this lock is acquired in atomic contexts. For example, in the tracepoint callback path:

handle_sched_process_exit()
tlob_destroy_task()
tlob_detach_from_binding()

And in the uprobe exception path:

tlob_uprobe_entry_handler()
tlob_start_task()

Should this use a raw_spinlock_t instead to avoid violating PREEMPT_RT rules?

[ ... ]

Quoted 15 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +15 -0
+static int tlob_stop_task(struct task_struct *task, void *binding)
+{
+	struct ha_monitor *ha_mon;
+	struct tlob_task_state *ws;
+	bool budget_exceeded;
+
+	scoped_guard(rcu) {
+		ha_mon = ha_get_monitor(task->pid, NULL);
+		if (!ha_mon)
+			return -ESRCH;
+
+		ws = ha_get_target(ha_mon);
+		if (WARN_ON_ONCE(!ws))
+			return -ESRCH;
+

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+		/* Only the binding that opened the window may end it; NULL
+		 * (task exit) skips the check.  Symmetric with the restart
+		 * check in tlob_start_task(). */
+		if (binding && ws->binding != binding)
+			return -EALREADY;
+
+		/* cmpxchg (0->1) claims the window under RCU; _release pairs
+		 * with the acquire in ha_setup_invariants(). */
+		if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
+			return -EAGAIN;

[Severity: High]
Is there a race condition here if the latency budget expires at the exact same time the task hits the stop uprobe?

If tlob_stop_task() wins the cmpxchg, it claims the window. The hrtimer callback could then run concurrently and call tlob_reset_notify(), which checks the stopping flag and returns early. This would skip emitting the detail tracepoint and skip setting budget_exceeded to true.

Since tlob_stop_task() then reads budget_exceeded as false and returns 0, could this silently hide genuine budget violations?

[ ... ]

Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+static void tlob_destroy_task(struct task_struct *task)
+{

[ ... ]

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	tlob_detach_from_binding(ws);
+
+	/* Force the window ended: @task may never have reached STOP or a timer. */
+	atomic_set(&ws->stopping, 1);
+	ha_cancel_timer_sync(ha_mon);
+
+	scoped_guard(rcu) {
+		da_monitor_reset(&ha_mon->da_mon);
+	}
+	da_destroy_storage(task->pid);

[Severity: Critical]
This is a pre-existing issue, but is it safe to call da_destroy_storage() here without holding a lock?

Looking at da_destroy_storage() in include/rv/da_monitor.h, it calls hash_del_rcu(&mon_storage->node). If multiple monitored tasks whose PIDs map to the same hash bucket exit concurrently, or one exits while another starts, calling hash_del_rcu() and hash_add_rcu() concurrently without mutual exclusion on the same bucket can corrupt the hlist pointers.

[ ... ]

Quoted 5 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+static void tlob_unbind_reap(struct tlob_uprobe_binding *b)
+{
+	struct tlob_task_state *ws, *tmp;
+	LIST_HEAD(to_destroy);
+

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	scoped_guard(spinlock, &tlob_ws_lock) {
+		list_for_each_entry_safe(ws, tmp, &b->started_list, started_node) {
+			list_del_init(&ws->started_node);
+			ws->binding = NULL;
+			if (atomic_read(&ws->stopping))
+				list_add_tail(&ws->started_node, &to_destroy);
+		}
+	}
+
+	list_for_each_entry_safe(ws, tmp, &to_destroy, started_node) {

[Severity: Critical]
Can this iteration trigger a use-after-free if a parked task exits concurrently?

When tlob_unbind_reap() moves parked windows to the local to_destroy list and drops the spinlock, it iterates the list without holding rcu_read_lock().

If handle_sched_process_exit() invokes tlob_destroy_task() concurrently, it skips detaching from the binding list (since ws->binding is already NULL) and calls call_rcu() to free the task state. Without RCU read protection here, the grace period could expire and free the memory while it is still linked on the local to_destroy list.

Quoted 4 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+		list_del_init(&ws->started_node);
+		tlob_destroy_task(ws->task);
+	}
+}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
maintainer
#25 lore raw

Re: [PATCH v6 9/9] selftests/ftrace: Walk up to find test.d/functions when a subdirectory is passed

In reply to [email protected]
Quoted 24 lines · On Thu, 2026-08-20 at 16:58 +0000, [email protected] wrote:
Quoted 11 lines

+  if [ -n "$OPT_TEST_DIR" ]; then
+    # Walk up from OPT_TEST_DIR to find the nearest ancestor that contains +    # test.d/functions.  This allows a monitor subdirectory (e.g. +    # verification/test.d/tlob/) to be passed directly without placing a +    # dummy functions shim in each new subdirectory.
+    # Security: only walk within directories owned by root or the current user
+    # to prevent sourcing a functions file planted in a world-writable ancestor.
+    dir=$(realpath "$OPT_TEST_DIR" 2>/dev/null || echo "$OPT_TEST_DIR") +    while [ "$dir" != "/" ] && [ "$dir" != "//" ]; do

Quoted 8 lines

+      if [ -f "$dir/test.d/functions" ]; then
+        # Verify the directory is not world-writable to prevent LPE. +        dirperms=$(stat -c "%a" "$dir" 2>/dev/null || stat -f "%Lp" "$dir" 2>/dev/null)
+        case "$dirperms" in
+          *2|*3|*6|*7) ;; # world-writable: skip this candidate +          *)
+            TOP_DIR=$dir

[Severity: High]
Does this logic actually check directory ownership as mentioned in the comments?

Quoted 6 lines

It appears the code only checks if the directory is world-writable, but omits validating that it is owned by root or the current user. An attacker could potentially create a directory with standard 0755 permissions, plant a malicious test.d/functions script, and if a privileged user or automated test harness runs ftracetest against a subdirectory, the script would traverse up and source the payload.

I'm wondering if this is a broad example of root can shoot themselves in the foot.
This is a selftest run by a root user, I believe they are responsible to validate what's running isn't malicious.

I cannot picture a case where a path is commonly world writable here. I would keep things simple and perhaps walk up at a maximum of 4-5 levels (tools/testing/selftests/verification/test.d), to kinda make sure we never go out of the repository.

You cannot really rely on git (may run from a tarball) or other static methods, but I believe 4-5 levels up will cover us just fine.

In my opinion all this stat/dirperms thing is rarely useful, you could just write the concern in a *brief* comment, saying we trust 4-5 level ups are still in the repo and we trust the repo, just to make AIs happy.

What do you think?

Gabriele

Quoted 15 lines

Additionally, if the stat command fails for any reason, dirperms will be empty.
Would an empty string match the *) fallback case and cause the script to silently
fail open, accepting even a world-writable directory?

Quoted 9 lines

+            TEST_DIR=$TOP_DIR/test.d
+            break
+            ;;
+        esac
+      fi
+      dir=$(dirname "$dir")
+    done
   fi
 }

maintainer
#26 lore raw

Re: [PATCH v6 9/9] selftests/ftrace: Walk up to find test.d/functions when a subdirectory is passed

Quoted 5 lines · On Mon, 24 Aug 2026 12:08:25 +0200 Gabriele Monaco <[email protected]> wrote:

In my opinion all this stat/dirperms thing is rarely useful, you could just write the concern in a *brief* comment, saying we trust 4-5 level ups are still in the repo and we trust the repo, just to make AIs happy.

What do you think?

I personally think we can ignore this comment from Sashiko. The only ones that should be running this code is those that are running it on a test box/VM. That is, there should be nobody else on the box, and the system can be blown away at any time and nobody should care.

-- Steve

maintainer
#16 lore raw

Re: [PATCH v6 6/9] rv: Add tlob hybrid automaton monitor

In reply to [email protected]

Hi Wen,

thanks for the series, I'm going with the first chunk, I still need to review the unbind_reap logic here.

Quoted 8 lines · On Fri, 2026-08-21 at 00:45 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+static inline void tlob_reset_notify(struct da_monitor *da_mon)
+{
+	struct ha_monitor *ha_mon = to_ha_monitor(da_mon);
+	struct tlob_task_state *ws;
+
+	ha_monitor_reset_env(da_mon);
+

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	ws = ha_get_target(ha_mon);
+	if (!ws)
+		return;
+
+	/*
+	 * stopping==1 means tlob_stop_task() ended this window already.
+	 * acquire pairs with the _release clear in ha_setup_invariants().
+	 */
+	if (atomic_read_acquire(&ws->stopping))
+		return;

Maybe I'm getting confused by the "acquire" in here, but are you aiming to "acquire" the stopping here by reading and then setting it later? That's obviously not atomic so you could read 0 and a concurring handler could set it to 1 before you do. Wouldn't it be better to cmpxchg?

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

Quoted 9 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+
+	/*
+	 * Monitor disable (ha_mon_destroying set) is not a violation: the
+	 * teardown paths free ws regardless.  Couples to an HA-layer flag
+	 * with no public contract; a framework-level equivalent would be
+	 * cleaner.
+	 */
+	if (unlikely(READ_ONCE(ha_mon_destroying)))
+		return;

This should be the first check in this function, there's no need to do anything else and you cannot trust the da_mon pointer.
Put it before ha_monitor_reset_env() (to_ha_monitor is pointer arithmetic, it can stay where it is for better readability).

Quoted 78 lines
(quoted diff) +2 -0
+
+	/* Genuine expiry: end the window so a later start takes the restart

path. */

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+	atomic_set(&ws->stopping, 1);
+
+	/* Stamped regardless of the tracepoint; tlob_stop_task() reads it.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	WRITE_ONCE(ws->budget_exceeded, true);
+
+	if (!trace_detail_env_tlob_enabled())
+		return;
+
+	unsigned int curr_state = READ_ONCE(da_mon->curr_state);
+	u64 accs[TLOB_ACC_MAX], partial_ns;
+	unsigned long flags;
+
+	/* Snapshot accumulators; partial_ns covers curr_state time not yet

folded in. */

kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+	raw_spin_lock_irqsave(&ws->entry_lock, flags);
+	partial_ns = ktime_get_ns() - ktime_to_ns(ws->last_ts);
+	accs[TLOB_ACC_RUNNING]  = ws->accs_ns[TLOB_ACC_RUNNING]  +
+				  (curr_state == running_tlob  ? partial_ns :

0);

(quoted diff) +2 -0
+	accs[TLOB_ACC_WAITING]  = ws->accs_ns[TLOB_ACC_WAITING]  +
+				  (curr_state == waiting_tlob  ? partial_ns :

0);

(quoted diff) +2 -0
+	accs[TLOB_ACC_SLEEPING] = ws->accs_ns[TLOB_ACC_SLEEPING] +
+				  (curr_state == sleeping_tlob ? partial_ns :

0);

kernel/trace/rv/monitors/tlob/tlob.c new file +27 -0
+	raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
+
+	trace_detail_env_tlob(da_get_id(da_mon), ws->threshold_ns,
+			      accs[TLOB_ACC_RUNNING],
+			      accs[TLOB_ACC_WAITING],
+			      accs[TLOB_ACC_SLEEPING]);
+}
+
+#define BUDGET_NS(ha_mon) (ha_get_target(ha_mon)->threshold_ns)
+
+/* HA constraint functions (called by ha_monitor_handle_constraint) */
+
+static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_tlob env,
+		      u64 time_ns)
+{
+	if (env == clk_elapsed_tlob)
+		return ha_get_clk_ns(ha_mon, env, time_ns);
+	return ENV_INVALID_VALUE;
+}
+
+/*
+ * Invariant: clk_elapsed < BUDGET_NS in running/waiting/sleeping.  "stopped"
+ * is exempt: the parked period must not be measured against the old window's
+ * clock anchor (restart from "stopped" would otherwise spuriously overrun).
+ */
+static inline bool ha_verify_invariants(struct ha_monitor *ha_mon,
+					enum states curr_state, enum events

event,

kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+					enum states next_state, u64 time_ns)
+{
+	if (curr_state == stopped_tlob)
+		return true;
+	return ha_check_invariant_ns(ha_mon, clk_elapsed_tlob, time_ns,

BUDGET_NS(ha_mon));

kernel/trace/rv/monitors/tlob/tlob.c new file +14 -0
+}
+
+/*
+ * The clock stays in guard (anchor) representation all window: env_store
+ * holds the window-start timestamp, re-anchored on start/restart.
+ * ha_invariant_passed_ns() never stores the deadline representation (the
+ * framework dropped ha_set_invariant_ns(), commit ab2900ae252b), so calling
+ * ha_inv_to_guard() here would subtract BUDGET_NS from the anchor and skew
+ * every check by one budget.  nomiss likewise never converts.
+ */
+
+/* No per-event guard conditions for tlob; invariants suffice. */
+static inline bool ha_verify_guards(struct ha_monitor *ha_mon,
+				    enum states curr_state, enum events

event,

Review

Quoted 2 lines
(quoted diff) +2 -0
+				    enum states next_state, u64 time_ns)
+{

Mmh, I just realised you don't use the guards for resets, is the order of actions a problem for this monitor?

Quoted 24 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +14 -0
+	return true;
+}
+
+/*
+ * Guard on stopping: a sched_switch after ha_cancel_timer_sync() would
+ * re-arm the timer (ODEBUG splat).  _acquire pairs with cmpxchg_release in
+ * tlob_stop_task.
+ *
+ * Entering stopped_tlob also resets env_store to the invalid sentinel, so a
+ * restart re-anchors the clock; a stale anchor would wrap the restart's
+ * timer delay to ~U64_MAX.
+ */
+static inline void ha_setup_invariants(struct ha_monitor *ha_mon,
+				       enum states curr_state, enum events

event,

kernel/trace/rv/monitors/tlob/tlob.c new file +6 -0
+				       enum states next_state, u64 time_ns)
+{
+	if (next_state == stopped_tlob) {
+		/*
+		 * Window ending: reset env_store to the invalid sentinel so
+		 * the next window gets a fresh clock anchor.  Keep

stopping==1

(quoted diff) +1 -0
+		 * so __tlob_acc() continues to block sched events while

parked.

Review

Quoted 2 lines
(quoted diff) +2 -0
+		 */
+		ha_monitor_reset_all_stored(ha_mon);

Is this really necessary since you have a reset() in the start edge? Isn't that anyway equivalent to reset() on the stop edge too?

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

Quoted 4 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+		return;
+	}
+
+	if (atomic_read_acquire(&ha_get_target(ha_mon)->stopping)) {

Same as above, do you mean to cmpxchg?

Quoted 12 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +8 -0
+		/*
+		 * Restart (stopped -> running): arm the timer, then clear
+		 * stopping so __tlob_acc() admits sched events only once the
+		 * state is already running_tlob.  _release pairs with the
+		 * acquires in __tlob_acc/tlob_reset_notify.
+		 */
+		if (next_state < state_max_tlob)
+			ha_start_timer_ns(ha_mon, clk_elapsed_tlob,

BUDGET_NS(ha_mon), time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+		atomic_set_release(&ha_get_target(ha_mon)->stopping, 0);
+		return;
+	}

...

Quoted 8 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +8 -0
+
+/*
+ * Accumulate elapsed ns into accs_ns[idx] since last_ts and advance it.
+ * Returns true if monitored with an active window.  The stopping gate is
+ * what keeps scheduler events from reaching a parked task (no "stopped"
+ * self-loops, see tlob.h) and keeps accs_ns[] from growing while parked.
+ */
+static inline bool __tlob_acc(struct task_struct *task, ktime_t now,

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+			       enum tlob_acc_idx idx)
+{
+	struct tlob_task_state *ws;
+	unsigned long flags;
+
+	guard(rcu)();
+	ws = da_get_target_by_id(task->pid);
+	/* acquire pairs with the _release clear in ha_setup_invariants(). */
+	if (!ws || atomic_read_acquire(&ws->stopping))
+		return false;

Mmh, returning false on !ws is kind of a shortcut to avoid another hashtable lookup that would return nothing, but skipping events on stopping is actually changing how the model behaves.

It is usually clearer to allow all events in a stopped state as self-loops (the /task/ can go through those events also when stopped). I usually prefer to keep logic in the model rather than in the source file.

It wouldn't be too wrong to say that the task can do anything but the tlob instance is stopped and other events cannot occur there, like you're doing, but that should be well documented.
You're vaguely mentioning this in some changelog, if you want to keep it this way, please explain it in the documentation file.
Word it like a description of the model rather than how you implement it, something like (adapt it accordingly): "although tasks can enter the scheduler when tlob is in the stopped state, those events are explicitly ignored by the tlob instance, hence there is no self-loop in the stopped state".

Thanks,
Gabriele

maintainer
#8 lore raw

Re: [PATCH v6 3/9] rv: Add tlob model DOT file

In reply to [email protected]

tools/verification/models/tlob.dot line 8

Quoted 9 lines · On Fri, 2026-08-21 at 00:45 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

tools/verification/models/tlob.dot new file +8 -0
+digraph state_automaton {
+	center = true;
+	size = "7,11";
+	{node [shape = plaintext, style=invis, label=""] "__init_stopped"};
+	{node [shape = plaintext] "running"};
+	{node [shape = plaintext] "waiting"};
+	{node [shape = plaintext] "sleeping"};
+	{node [shape = plaintext] "stopped"};

There's a convention to mark the final, which is usually also the initial, state differently. Please do:

	{node [shape = ellipse] "stopped"};

Somehow rvgen still gets it right, but it doesn't hurt to follow this (undocumented, I know) convention.

Thanks,
Gabriele

maintainer
#2 lore raw

Re: [PATCH v6 1/9] rv: Introduce DA_MON_ALLOCATION_STRATEGY

In reply to [email protected]

include/rv/da_monitor.h line 38

Quoted 8 lines · On Fri, 2026-08-21 at 00:45 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

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

That's correct, but technically it wouldn't be wrong to also define DA_ALLOC_POOL. We could do:

  #if defined(DA_MON_ALLOCATION_STRATEGY) && DA_MON_ALLOCATION_STRATEGY != DA_ALLOC_POOL
  #error "DA_MON_POOL_SIZE implies DA_ALLOC_POOL"
  #endif

Then the next define shouldn't be an issue because the preprocessor doesn't complain on multiple /equivalent/ definitions.

No big deal if you prefer it like this though.

include/rv/da_monitor.h line 48

Quoted 10 lines
include/rv/da_monitor.h +10 -0
+#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 /* DA_MON_POOL_SIZE */
+
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#ifdef DA_SKIP_AUTO_ALLOC

Right, I told you not to touch nomiss, but there's no need to maintain DA_SKIP_AUTO_ALLOC, we can have the monitor do

  #define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL

(without any other change), so we can simplify the logic.

...

include/rv/da_monitor.h static struct rv_monitor rv_this; · line 125

Quoted 9 lines
include/rv/da_monitor.h +9 -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

da_extra_cleanup() doesn't belong in this patch, does it? You could have a separate patch for this.

...

Quoted 27 lines
include/rv/da_monitor.h +26 -0
+/*
+ * 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;
+
+	if (da_mon)
+		return da_mon;
+
+	mon_storage = mempool_alloc_preallocated(&da_monitor_pool);
+	if (!mon_storage)
+		return NULL;
+	memset(mon_storage, 0, sizeof(*mon_storage));
+
+	mon_storage->id = id;
+	mon_storage->target = target;
+
+	/* Single consumer under the caller's lock; duplicate is a double-

start bug. */

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

Quoted 5 lines
include/rv/da_monitor.h +5 -0
+	existing = __da_get_mon_storage(id);
+	if (WARN_ON_ONCE(existing)) {
+		mempool_free(mon_storage, &da_monitor_pool);
+		return NULL;
+	}

Isn't this check for existing redundant? da_prepare_storage() is always called after a da_get_monitor(), so the first check for da_mon is in fact validating that we do not already have this entry.

It doesn't return NULL because the entire machine doesn't expect two different targets with the same id. This doesn't seem to be expected in your case either (you WARN), so I think you can easily drop this check.

Quoted 4 lines
include/rv/da_monitor.h +4 -0
+	hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+	return &mon_storage->rv.da_mon;
+}
+

...

include/rv/da_monitor.h static inline void da_monitor_destroy(void) · line 726

Quoted 5 lines
include/rv/da_monitor.h +1 -0
@@ -607,21 +723,37 @@ static inline void da_monitor_destroy(void)
  	 * pending, we can safely assume no concurrent user.
  	 */
  	hash_for_each_safe(da_monitor_ht, bkt, tmp, mon_storage, node) {
+		da_extra_cleanup(&mon_storage->rv.da_mon);

This one line belongs to another patch, see the comment about da_extra_cleanup() above.

Quoted 15 lines
include/rv/da_monitor.h +9 -1
  		hash_del_rcu(&mon_storage->node);
-		kfree(mon_storage);
+		if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+			mempool_free(mon_storage, &da_monitor_pool);
+		else
+			kfree(mon_storage);
+	}
+
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL) {
+		rcu_barrier();
+		mempool_exit(&da_monitor_pool);
  	}
  }


 /*

Quoted 8 lines

- * 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 link per-object monitor storage.
+ *
+ * Called only from da_handle_start_run_event(); must run in task context

Also from da_handle_start_event(), you could use da_handle_start*_event() in the comment or even better say "only when the monitor is started for the first time", because we still call da_handle_start_event() and friends when the monitor is running on models where the start event can occur again, but obviously do not reallocate (that's what the check for da_mon was for).

Quoted 8 lines

+ * for DA_ALLOC_AUTO and DA_ALLOC_POOL (both take a spinlock_t internally).
+ * Subsequent event handlers use da_handle_event() and never allocate. */

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

include/rv/da_monitor.h static inline void da_monitor_destroy(void) · line 756

Quoted 10 lines
include/rv/da_monitor.h +10 -0
+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);
+}

The rest of the implementation looks good.

Thanks,
Gabriele

maintainer
#5 lore raw

Re: [PATCH v6 2/9] rv: Add generic uprobe infrastructure for RV monitors

In reply to [email protected]
Quoted 16 lines · On Fri, 2026-08-21 at 00:45 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>
+/**
+ * struct rv_uprobe - embeddable uprobe handle for RV monitors
+ *
+ * Embed via DECLARE_RV_UPROBE() and pass &name to rv_uprobe_register().
+ * The caller may free the containing struct after rv_uprobe_unregister()
+ * (or rv_uprobe_unregister_nosync() + rv_uprobe_sync()) returns.
+ *
+ * @uc:     embedded uprobe_consumer; set handler/ret_handler before registering
+ * @uprobe: registered uprobe pointer (NULL when not registered)
+ * @path:   path of the probed binary, held until unregistration
+ */

include/rv/rv_uprobe.h new file +3 -0
+struct rv_uprobe {
+	struct uprobe_consumer	uc;
+	struct uprobe		*uprobe;

include/rv/rv_uprobe.h line 40

Quoted 10 lines
include/rv/rv_uprobe.h new file +10 -0
+	struct path		path;
+};
+
+/* 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
+ */

I think kernel-docs for functions should stay with their definitions (in rv_uprobes.c). This header should have kernel-docs only for what is defined here (structs or inline functions) and the source should have complete kernel-docs fo r what's defined there.
Use /** only for complete kernel-docs. You can validate it with tools/docs/kernel-doc .

Quoted 17 lines
include/rv/rv_uprobe.h new file +8 -0
+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);

...

Quoted 11 lines
kernel/trace/rv/rv_uprobe.c new file +11 -0
+/**
+ * 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;
+	/* path held; caller must call rv_uprobe_sync() then path_put(&p-
Quoted 1 line
Quoted 1 line

path) */

This comment isn't necessary here if you have the kernel-doc up-to-date.

Review

Quoted 2 lines
(quoted diff) +2 -0
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister_nosync);

The implementation looks alright, but I still need to test it.

Thanks,
Gabriele

maintainer
#17 lore raw

Re: [PATCH v6 6/9] rv: Add tlob hybrid automaton monitor

In reply to [email protected]
Quoted 13 lines · On Fri, 2026-08-21 at 00:45 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Documentation/trace/rv/monitor_tlob.rst new file +11 -0
+
+Description
+-----------
+
+The tlob monitor tracks per-task elapsed wall-clock time (CLOCK_MONOTONIC,
+spanning running, waiting, and sleeping states) and reports a violation when
+the monitored task exceeds a configurable per-invocation budget threshold.
+
+The monitor implements a four-state hybrid automaton with a single clock
+environment variable ``clk_elapsed``.  The clock invariant
+``clk_elapsed < BUDGET_NS()`` is active in the ``running``, ``waiting``, and

Documentation/trace/rv/monitor_tlob.rst line 29

Quoted 10 lines
Documentation/trace/rv/monitor_tlob.rst new file +10 -0
+``sleeping`` states (``stopped`` has no invariant, hence no timer); when it
+is violated the HA timer fires and the framework emits ``error_env_tlob``
+then calls ``da_monitor_reset()`` automatically::
+
+                                           | (initial)
+                                           v
+           +--------------+           +----------+
+           |   running    | --------> |  stopped |
+        |->+--------------+ <-------- +----------+
+   switch_in  preempt  sleep

This triggered my OCD ;)
Please fix the arrow waiting -> running:

+           +--------------+           +----------+ +        +->|   running    | --------> |  stopped | +        | +--------------+ <-------- +----------+ +   switch_in  preempt  sleep

Quoted 16 lines
+        |        |       |
+        |        |       |
+        |        v       v

+        +---------+  +---------+
+        | waiting |  | sleeping|
+        +---------+  +---------+
+             ^            v
+             |   wakeup   |
+             |            |
+             +------------+
+
+  A fourth state, ``stopped``, has no clock invariant (hence no timer). +  ``running`` reaches it on ``stop`` (``tlob_stop_task()``, window ended, +  per-task state parked rather than freed) and returns to ``running`` on +  ``start`` (``tlob_start_task()`` restarting the same task's parked +  window).

So you define a pseudo-state "parked" that is essentially stopped but after monitoring started (we allocated) and before the task exits (we deallocate), is that right?
It looks kind of like an implementation detail rather than something related to your model: there isn't any parked state in the model and you don't need one.
If you really want the concept of parked to explain how you handle allocation, perhaps you could make it clear in the code only. For instance (if I got it right) when describing the tlob_task_state->stopping you could say: tasks with this flag set are "parked" until deallocation.

Quoted 20 lines

+
+  Key transitions:
+    running  --(sleep)------> sleeping   (task blocks waiting for a resource) +    running  --(preempt)----> waiting    (task preempted, back in runqueue) +    sleeping --(wakeup)-----> waiting    (resource available, enters runqueue)
+    waiting  --(switch_in)--> running    (scheduler picks task, back on CPU) +    running  --(stop)-------> stopped    (tlob_stop_task(): window ended, parked)
+    stopped  --(start)------> running    (tlob_start_task(): window restarted)
+
+  ``tlob_start_task()`` calls ``da_handle_start_run_event(task->pid, ws, start_tlob)``.
+  The ``start_tlob`` edge goes ``stopped`` -> ``running`` for both a fresh +  allocation (the initial state is ``stopped``) and a parked window's restart;
+  there is no ``start`` self-loop on ``running`` (a running task's START is +  rejected with ``-EALREADY``).  The transition triggers ``ha_setup_invariants()``,

Quoted 8 lines

+  which anchors ``clk_elapsed`` and arms the budget timer automatically. +  ``tlob_stop_task()`` cancels the HA timer synchronously
+  via ``ha_cancel_timer_sync()``, then dispatches the ``stop_tlob`` event +  (running -> stopped) instead of resetting the monitor: the per-task state +  is parked, not freed, so a later ``tlob_start_task()`` call for the same +  task can restart it without reallocating.  Final teardown (task exit, +  uprobe unbind, or monitor disable) is what actually calls
+  ``da_monitor_reset()`` and frees the state.

All allocation or broadly implementation details don't belong here. I believe it's already clear from the model, but you may still stress that a task can start another measuring window after the previous was stopped.

Being general about implementation in your documentation saves you some headache while keeping that in sync (and I believe AIs make this problem worse, by the way).

...

Documentation/trace/rv/monitor_tlob.rst line 167

Quoted 9 lines
Documentation/trace/rv/monitor_tlob.rst new file +9 -0
+Kernel API
+----------
+
+``tlob_start_task`` and ``tlob_stop_task`` are the implementation-level
+functions called by the uprobe entry/exit handlers; the interface is
+driven from userspace.
+
+.. kernel-doc:: kernel/trace/rv/monitors/tlob/tlob.c
+   :functions: tlob_start_task tlob_stop_task

I remember mentioning this, there's no real kernel API, those functions aren't exported nor meant to be called by anyone besides the model. I would remove this section altogether.

...

kernel/trace/rv/monitors/tlob/Kconfig line 11

Quoted 13 lines

+++ b/kernel/trace/rv/monitors/tlob/Kconfig

kernel/trace/rv/monitors/tlob/Kconfig new file +11 -0
@@ -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

emits a violation when...

Quoted 7 lines
kernel/trace/rv/monitors/tlob/Kconfig new file +1 -0
+	  elapsed time exceeds a configurable per-invocation budget.
kernel/trace/rv/monitors/tlob/tlob.c new file +0 -0
new file mode 100644
index 000000000000..08b1bee884cc

...

Quoted 16 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +16 -0
+struct tlob_task_state {
+	struct task_struct	*task;		/* via get_task_struct */
+	u64			threshold_ns;	/* budget in nanoseconds */
+
+	/*
+	 * Per-window: 1 = this window ended (stop or timer expiry).  Blocks
+	 * timer re-arm in ha_setup_invariants(); cleared on restart.
+	 */
+	atomic_t		stopping;
+
+	/*
+	 * Per-task, one-shot: final teardown has claimed this slot; never
+	 * reset (a window can end and restart, the task cannot).  atomic_t
+	 * so cmpxchg is well-defined on every arch.
+	 */
+	atomic_t		destroying;

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+
+	bool			budget_exceeded;
+
+	/*
+	 * Opaque owner: the binding that started this task.  Set once on
+	 * fresh allocation (NULL for callers with no binding), cleared by
+	 * tlob_unbind_reap() for an active task whose binding is removed.
+	 * Immutable elsewhere.  Protected by tlob_ws_lock.
+	 */
+	void			*binding;

Does this really need to be opaque? You're casting it anyway so I don't see why it can't be a struct tlob_uprobe_binding * to begin with.

Quoted 15 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	/*
+	 * Linked into binding->started_list for the whole lifetime (not just
+	 * while parked) so unbind reaping finds parked and active tasks.
+	 * Protected by tlob_ws_lock.
+	 */
+	struct list_head	started_node;
+
+	/* Serialises accs_ns[]; held briefly (hardirq-safe). */
+	raw_spinlock_t		entry_lock;
+	u64			accs_ns[TLOB_ACC_MAX]; /* per-state elapsed

ns */

kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+	ktime_t			last_ts;
+
+	struct rcu_head		rcu;
+};

...

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+
+/*
+ * Unlink ws from its binding's started_list before returning it to the pool.
+ * ws->binding is left stale: the next tlob_ws_alloc() memsets it, and the
+ * restart path checks destroying first.  Idempotent (list_del_init no-op).
+ */
+static inline void tlob_detach_from_binding(struct tlob_task_state *ws)
+{
+	if (!ws->binding)
+		return;

Should you access also the binding field under the lock? And perhaps be set to NULL also here?

Quoted 3 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+	guard(spinlock)(&tlob_ws_lock);
+	list_del_init(&ws->started_node);
+}

...

Quoted 89 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +17 -0
+
+/**
+ * tlob_start_task - begin monitoring @task with budget @threshold_ns ns.
+ * @task:         Task to monitor; may be current or another task.
+ * @threshold_ns: Budget in ns, in [1000, TLOB_MAX_THRESHOLD_NS].
+ * @binding:      Opaque owner, recorded on fresh allocation and checked for
+ *                an exact match on restart; NULL for callers that never
+ *                restart a parked window.
+ *
+ * Allocates a fresh entry if @task has none, or restarts a parked entry in
+ * place when @binding matches (see tlob.dot: "start" fires from both
+ * running and stopped).
+ *
+ * Returns 0, -ENODEV, -ERANGE, -EALREADY, -ESRCH, or -ENOSPC (fresh start
+ * past pool capacity).
+ */
+static int tlob_start_task(struct task_struct *task, u64 threshold_ns, void

*binding)

kernel/trace/rv/monitors/tlob/tlob.c new file +35 -0
+{
+	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_ws_lock. */
+	guard(spinlock)(&tlob_ws_lock);
+
+	/*
+	 * da_get_target_by_id() uses hash_for_each_possible_rcu(), which
+	 * requires an RCU read-side critical section.
+	 */
+	scoped_guard(rcu) {
+		ws = da_get_target_by_id(task->pid);
+		if (ws) {
+			if (!atomic_read(&ws->stopping))
+				return -EALREADY;
+			if (atomic_read(&ws->destroying))
+				return -ESRCH;
+			/*
+			 * Exact match only.  An orphaned parked ws (binding
+			 * cleared while active, then parked) is not adopted:
+			 * that would need re-linking into the new binding's
+			 * started_list.  Accepted gap; the slot is reclaimed
+			 * at task exit.
+			 */
+			if (ws->binding != binding)
+				return -EALREADY;
+
+			/* Restart in place: same slot, hash entry, task ref,

list node. */

kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+			ws->threshold_ns = threshold_ns;
+			WRITE_ONCE(ws->budget_exceeded, false);
+			memset(ws->accs_ns, 0, sizeof(ws->accs_ns));
+			ws->last_ts = ktime_get();
+
+			/*
+			 * Keep stopping set: __tlob_acc() gates out sched
+			 * events until ha_setup_invariants() clears it after
+			 * the state is running_tlob.  Clearing here would

let

kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+			 * events hit stopped_tlob (INVALID transitions).
+			 */
+
+			/* Only failure here: monitor disabled since the

check above. */

kernel/trace/rv/monitors/tlob/tlob.c new file +1 -0
+			if (!da_handle_start_run_event(task->pid, ws,

start_tlob))

kernel/trace/rv/monitors/tlob/tlob.c new file +18 -0
+				return -ENODEV;
+			return 0;
+		}
+	}
+
+	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);
+	ws->binding = binding;
+	if (binding)
+		list_add_tail(&ws->started_node,
+			      &((struct tlob_uprobe_binding *)binding)-
Quoted 1 line
Quoted 1 line

started_list);

Why do you add to the list and then remove if start failed? Is the binding needed when the model does it's job?
Cannot you just do all that after only if the start passed?

Also, can binding really be NULL ?

Quoted 42 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+
+	/* Dispatch failed (pool exhausted or monitor disabled): unwind the

slot. */

kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+	if (!da_handle_start_run_event(task->pid, ws, start_tlob)) {
+		if (binding)
+			list_del_init(&ws->started_node);
+		/* stopping=1 short-circuits the reset hook; destroy before

freeing ws. */

kernel/trace/rv/monitors/tlob/tlob.c new file +34 -0
+		atomic_set(&ws->stopping, 1);
+		da_destroy_storage(task->pid);
+		put_task_struct(task);
+		tlob_ws_direct_return(ws);
+		return -ENOSPC;
+	}
+
+	return 0;
+}
+
+/**
+ * tlob_stop_task - end the current monitoring window for @task.
+ * @task: Task to stop.
+ * @binding: Opaque owner; must match ws->binding to end a normal (uprobe)
+ *           window.  NULL (task exit) skips the check.
+ *
+ * Ends the window (dispatches "stop") but does NOT free the entry: it stays
+ * parked so a later tlob_start_task() can restart it.  Call
+ * tlob_destroy_task() once @task will never restart.
+ *
+ * cmpxchg on stopping (0->1) under RCU claims ownership; the winner cancels
+ * the timer synchronously.
+ *
+ * Returns 0, -EOVERFLOW (budget exceeded), -ESRCH (not monitored),
+ * -EAGAIN (window already ended), or -EALREADY (owned by another binding).
+ */
+static int tlob_stop_task(struct task_struct *task, void *binding)
+{
+	struct ha_monitor *ha_mon;
+	struct tlob_task_state *ws;
+	bool budget_exceeded;
+
+	scoped_guard(rcu) {
+		ha_mon = ha_get_monitor(task->pid, NULL);

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+		if (!ha_mon)
+			return -ESRCH;
+
+		ws = ha_get_target(ha_mon);
+		if (WARN_ON_ONCE(!ws))
+			return -ESRCH;
+
+		/* Only the binding that opened the window may end it; NULL
+		 * (task exit) skips the check.  Symmetric with the restart
+		 * check in tlob_start_task(). */

The format of this comment is wrong (break the line after /* on multi-line comments).

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

Quoted 5 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+		if (binding && ws->binding != binding)
+			return -EALREADY;
+
+		/* cmpxchg (0->1) claims the window under RCU; _release pairs
+		 * with the acquire in ha_setup_invariants(). */

Same here and probably somewhere else, please check around.

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

Quoted 7 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+		if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
+			return -EAGAIN;
+
+		/*
+		 * ws may be destroyed concurrently (unbind -> call_rcu), so
+		 * keep its access under RCU; dispatch re-looks-up under RCU.
+		 */

This is the correct format.
Although I wonder: if we need a multi-line comment on each line, aren't we perhaps overdoing it? cmpxchg (0->1) is documented at the function level, it's probably enough to leave it there.

Also this specific comment has little to do with the lines that come after. Prefer function level documentation where possible. If a function is very large (and you're convinced that's fine), you can document some non-trivial steps as brief as possible.

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

Quoted 13 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+		ha_cancel_timer_sync(ha_mon);
+		budget_exceeded = READ_ONCE(ws->budget_exceeded);
+	}
+
+	/* running -> stopped: no reset or destroy, the entry stays parked.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+	da_handle_event(task->pid, NULL, stop_tlob);
+
+	return budget_exceeded ? -EOVERFLOW : 0;
+}
+
+/*
+ * tlob_destroy_task - final teardown for @task's entry: frees the pool slot,

Double line break after the : and continue with the long description. It's fine not writing a full-blown kernel-doc, but you can do better here (exactly like you do in tlob_unbind_reap).

Quoted 122 lines

+ * drops the task_struct ref, removes the hash entry, whether active or parked.
+ * Idempotent via the destroying cmpxchg (same pattern as tlob_extra_cleanup()).
+ * Callers must end the window first (see handle_sched_process_exit()).
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +19 -0
+static void tlob_destroy_task(struct task_struct *task)
+{
+	struct ha_monitor *ha_mon;
+	struct tlob_task_state *ws;
+
+	scoped_guard(rcu) {
+		ha_mon = ha_get_monitor(task->pid, NULL);
+		if (!ha_mon)
+			return;
+		ws = ha_get_target(ha_mon);
+		if (WARN_ON_ONCE(!ws))
+			return;
+		if (atomic_cmpxchg_release(&ws->destroying, 0, 1) != 0)
+			return;
+	}
+
+	tlob_detach_from_binding(ws);
+
+	/* Force the window ended: @task may never have reached STOP or a

timer. */

kernel/trace/rv/monitors/tlob/tlob.c new file +17 -0
+	atomic_set(&ws->stopping, 1);
+	ha_cancel_timer_sync(ha_mon);
+
+	scoped_guard(rcu) {
+		da_monitor_reset(&ha_mon->da_mon);
+	}
+	da_destroy_storage(task->pid);
+
+	put_task_struct(ws->task);
+	call_rcu(&ws->rcu, tlob_ws_return_cb);
+}
+
+static int tlob_uprobe_entry_handler(struct uprobe_consumer *self,
+				     struct pt_regs *regs, __u64 *data)
+{
+	struct tlob_uprobe_binding *b =
+		container_of(self, struct tlob_uprobe_binding,

start_probe.uc);

kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+
+	tlob_start_task(current, b->threshold_ns, b);
+	return 0;
+}
+
+static int tlob_uprobe_stop_handler(struct uprobe_consumer *self,
+				    struct pt_regs *regs, __u64 *data)
+{
+	struct tlob_uprobe_binding *b =
+		container_of(self, struct tlob_uprobe_binding,

stop_probe.uc);

kernel/trace/rv/monitors/tlob/tlob.c new file +23 -0
+
+	tlob_stop_task(current, b);
+	return 0;
+}
+
+/*
+ * Register start + stop entry uprobes for a binding.
+ * Called with tlob_uprobe_mutex held.
+ */
+static int tlob_add_uprobe(u64 threshold_ns, const char *binpath,
+			   loff_t offset_start, loff_t offset_stop)
+{
+	struct tlob_uprobe_binding *tmp_b;
+	char pathbuf[TLOB_MAX_PATH];
+	struct inode *inode;
+	struct path path __free(path_put) = {};
+	char *canon;
+	int ret;
+
+	if (binpath[0] != '/')
+		return -EINVAL;
+
+	struct tlob_uprobe_binding *b __free(kfree) = kzalloc_obj(*b,

GFP_KERNEL);

kernel/trace/rv/monitors/tlob/tlob.c new file +43 -0
+	if (!b)
+		return -ENOMEM;
+
+	b->threshold_ns = threshold_ns;
+	b->offset_start = offset_start;
+	b->offset_stop  = offset_stop;
+	INIT_LIST_HEAD(&b->started_list);
+
+	ret = kern_path(binpath, LOOKUP_FOLLOW, &path);
+	if (ret)
+		return ret;
+
+	if (!d_is_reg(path.dentry))
+		return -EINVAL;
+
+	inode = d_real_inode(path.dentry);
+
+	/* Reject duplicate start offset for the same binary inode. */
+	list_for_each_entry(tmp_b, &tlob_uprobe_list, list) {
+		if (tmp_b->offset_start == offset_start &&
+		    rv_uprobe_is_registered(&tmp_b->start_probe) &&
+		    d_real_inode(tmp_b->start_probe.path.dentry) == inode)
+			return -EEXIST;
+	}
+
+	canon = d_path(&path, pathbuf, sizeof(pathbuf));
+	if (IS_ERR(canon))
+		return PTR_ERR(canon);
+	strscpy(b->binpath, canon, sizeof(b->binpath));
+
+	b->start_probe.uc.handler = tlob_uprobe_entry_handler;
+	ret = rv_uprobe_register(b->binpath, offset_start, &b->start_probe);
+	if (ret)
+		return ret;
+
+	b->stop_probe.uc.handler = tlob_uprobe_stop_handler;
+	ret = rv_uprobe_register(b->binpath, offset_stop, &b->stop_probe);
+	if (ret) {
+		rv_uprobe_unregister(&b->start_probe);
+		return ret;
+	}
+
+	/* NOT "b = no_free_ptr(b)": the re-assignment would free the live
Quoted 1 line

node. */

This comment feels like an AI tried the wrong way to use no_free_ptr and left it not to make the same mistake again, we don't need it.

Quoted 62 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +20 -0
+	list_add_tail(&no_free_ptr(b)->list, &tlob_uprobe_list);
+	return 0;
+}
+
+/*
+ * tlob_unbind_reap - detach every task @b started, destroy the parked ones.
+ *
+ * Caller must have unregistered @b's uprobes and called rv_uprobe_sync():
+ * no start/stop can then be in flight for @b, so started_list is safe to
+ * walk.  Active tasks are detached (binding cleared) and left running,
+ * matching unbind behaviour today; parked tasks are destroyed, or their
+ * pool slot leaks until the task next exits.
+ */
+static void tlob_unbind_reap(struct tlob_uprobe_binding *b)
+{
+	struct tlob_task_state *ws, *tmp;
+	LIST_HEAD(to_destroy);
+
+	scoped_guard(spinlock, &tlob_ws_lock) {
+		list_for_each_entry_safe(ws, tmp, &b->started_list,

started_node) {

kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+			list_del_init(&ws->started_node);
+			ws->binding = NULL;
+			if (atomic_read(&ws->stopping))
+				list_add_tail(&ws->started_node,

&to_destroy);

kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+		}
+	}
+
+	list_for_each_entry_safe(ws, tmp, &to_destroy, started_node) {
+		list_del_init(&ws->started_node);
+		tlob_destroy_task(ws->task);
+	}
+}
+
+static int tlob_remove_uprobe_by_key(loff_t offset_start, const char

*binpath)

kernel/trace/rv/monitors/tlob/tlob.c new file +25 -0
+{
+	struct tlob_uprobe_binding *b, *tmp;
+	struct path remove_path;
+	struct inode *inode;
+	int ret;
+
+	ret = kern_path(binpath, LOOKUP_FOLLOW, &remove_path);
+	if (ret)
+		return ret;
+
+	inode = d_real_inode(remove_path.dentry);
+
+	ret = -ENOENT;
+	list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) {
+		if (b->offset_start != offset_start)
+			continue;
+		if (d_real_inode(b->start_probe.path.dentry) != inode)
+			continue;
+		list_del(&b->list);
+		/*
+		 * rv_uprobe_sync() may sleep; list_del() already made the
+		 * binding invisible to new readers.
+		 */
+		rv_uprobe_unregister_nosync(&b->start_probe);
+		rv_uprobe_unregister_nosync(&b->stop_probe);

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+		rv_uprobe_sync();
+		tlob_unbind_reap(b);
+		path_put(&b->start_probe.path);
+		path_put(&b->stop_probe.path);
+		kfree(b);
+		ret = 0;
+		break;
+	}
+
+	path_put(&remove_path);

Just for consistency I would use __free(path_put) also for this. But you don't have to if you prefer this way.

Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+	return ret;
+}

...

Quoted 9 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +8 -0
+/*
+ * Parse "p PATH:OFFSET_START OFFSET_STOP threshold=NS".
+ * PATH may contain ':'; the last ':' separates path from offset.
+ * Returns 0, -EINVAL, or -ERANGE.
+ */
+VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+					    char **path_out,
+					    loff_t *start_out, loff_t

*stop_out)

These VISIBLE_IF_KUNIT stuff are left from the previous implementation and removed from the KUnit patch, they shouldn't be here.

Quoted 63 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +43 -0
+{
+	unsigned long long thr = 0, stop_val = 0;
+	long long start_val;
+	char *p, *path_token, *token, *colon;
+	bool got_stop = false, got_thr = false;
+	int n;
+
+	/* Must start with "p " */
+	if (buf[0] != 'p' || buf[1] != ' ')
+		return -EINVAL;
+
+	p = buf + 2;
+	while (*p == ' ')
+		p++;
+
+	/* First space-delimited token is PATH:OFFSET_START */
+	path_token = strsep(&p, " \t");
+	if (!path_token || !*path_token)
+		return -EINVAL;
+
+	/* Split at last ':' to handle paths that contain ':'. */
+	colon = strrchr(path_token, ':');
+	if (!colon || colon - path_token < 2)
+		return -EINVAL;
+	*colon = '\0';
+
+	if (path_token[0] != '/')
+		return -EINVAL;
+
+	n = 0;
+	if (sscanf(colon + 1, "%lli%n", &start_val, &n) != 1 || n == 0)
+		return -EINVAL;
+	if (start_val < 0)
+		return -EINVAL;
+
+	/* Remaining tokens: OFFSET_STOP threshold=NS */
+	while (p && (token = strsep(&p, " \t")) != NULL) {
+		if (!*token)
+			continue;
+		if (strncmp(token, "threshold=", 10) == 0) {
+			if (kstrtoull(token + 10, 0, &thr))
+				return -EINVAL;
+			if (thr < TLOB_MIN_THRESHOLD_NS || thr >

TLOB_MAX_THRESHOLD_NS)

kernel/trace/rv/monitors/tlob/tlob.c new file +19 -0
+				return -ERANGE;
+			got_thr = true;
+		} else if (!got_stop) {
+			long long sv;
+
+			n = 0;
+			if (sscanf(token, "%lli%n", &sv, &n) != 1 || n == 0)
+				return -EINVAL;
+			if (sv < 0)
+				return -EINVAL;
+			stop_val = (unsigned long long)sv;
+			got_stop = true;
+		} else {
+			return -EINVAL;
+		}
+	}
+
+	if (!got_stop || !got_thr)
+		return -EINVAL;

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	if (start_val == (long long)stop_val)
+		return -EINVAL;
+
+	*thr_out   = thr;
+	*path_out  = path_token;
+	*start_out = (loff_t)start_val;
+	*stop_out  = (loff_t)stop_val;
+	return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line);

Same with these.

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

Quoted 6 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +6 -0
+
+/*
+ * Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention).
+ */
+VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out,
+					    loff_t *start_out)

And here.

Quoted 13 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +13 -0
+{
+	char *binpath, *colon;
+	long long off;
+	int n = 0;
+
+	if (buf[0] != '-')
+		return -EINVAL;
+	binpath = buf + 1;
+	if (binpath[0] != '/')
+		return -EINVAL;
+	colon = strrchr(binpath, ':');
+	if (!colon || colon - binpath < 2)
+		return -EINVAL;

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	*colon = '\0';
+	if (sscanf(colon + 1, "%lli%n", &off, &n) != 1 || n == 0)
+		return -EINVAL;
+	if (off < 0)
+		return -EINVAL;
+	*path_out  = binpath;
+	*start_out = (loff_t)off;
+	return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_remove_line);

And here.

Quoted 5 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+
+static int tlob_create_or_delete_uprobe(char *buf)
+{
+	loff_t offset_start, offset_stop;
+	u64 threshold_ns;

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	char *binpath;
+	int ret;
+
+	if (buf[0] == '-') {
+		ret = tlob_parse_remove_line(buf, &binpath, &offset_start);
+		if (ret)
+			return ret;
+		mutex_lock(&tlob_uprobe_mutex);
+		ret = tlob_remove_uprobe_by_key(offset_start, binpath);
+		mutex_unlock(&tlob_uprobe_mutex);

It's probably more readable if you take this locks inside the functions.

I'd just put a guard (not scoped) from the first point where it seems needed and let it be (e.g. just before list_for_each_entry_safe). You don't need to be overly precise at the cost of readability since this isn't a hot path.

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +8 -0
+		return ret;
+	}
+	ret = tlob_parse_uprobe_line(buf, &threshold_ns, &binpath,
+				     &offset_start, &offset_stop);
+	if (ret)
+		return ret;
+	mutex_lock(&tlob_uprobe_mutex);
+	ret = tlob_add_uprobe(threshold_ns, binpath, offset_start,

offset_stop);

kernel/trace/rv/monitors/tlob/tlob.c new file +1 -0
+	mutex_unlock(&tlob_uprobe_mutex);

Same here, you can guard-lock before list_for_each_entry.

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

Quoted 2 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+	return ret;
+}

Implementation looks good otherwise and seems to work as far as I could test.

Thanks,
Gabriele

maintainer Reviewed-by on v6 7/9
#19 lore raw

Re: [PATCH v6 7/9] rv: Add KUnit tests for the tlob monitor

In reply to [email protected]
Quoted 26 lines · On Fri, 2026-08-21 at 00:45 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Add a test case to the shared rv_monitors_test.c suite (gated by the existing CONFIG_RV_MONITORS_KUNIT_TEST, same as nomiss/sco/etc.) covering the uprobe-line parser.

tlob_parse_uprobe_line() and tlob_parse_remove_line() stay static and
unconditional: production code (tlob_create_or_delete_uprobe()) always needs them, so they can't be compiled out, and un-hiding them via VISIBLE_IF_KUNIT would tie their linkage to CONFIG_KUNIT while nothing else in tlob.c depends on it. Instead, expose them to the test only through a const rv_tlob_kunit_ops struct of function pointers, built and exported the same way nomiss.c/sco.c expose their rv_<mon>_ops:
the struct itself, its declaration in tlob_kunit.h, and its use in tlob_kunit.c are all gated on the single CONFIG_RV_MONITORS_KUNIT_TEST symbol, so there is no separate prototype to go out of sync with a visibility macro.

tlob_kunit.c follows the monitors/*/*_kunit.c convention: one rv_test_tlob() case textually included into rv_monitors_test.c, guarded by IS_REACHABLE(CONFIG_RV_MON_TLOB) with an rv_test_stub() fallback so the shared suite still builds when RV_MON_TLOB=n (that symbol is independent of CONFIG_RV_MONITORS_KUNIT_TEST). Drop the per-monitor TLOB_KUNIT_TEST Kconfig entry, .kunitconfig, and Makefile line that a standalone test module would have needed.

Quoted 6 lines

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

---

Tests look good, however this commit message is overdoing it.
Try to avoid mentioning implementation details in commit messages, especially those referring to older versions of the same patch (you do it often but this really stands out).

Good commit messages usually state the problem and delineate how the solution addresses it, without repeating what's done in the patch but explaining the ideas behind it. Here you aren't really addressing a problem (you're adding a /feature/), so you can even be shorter. Remember to focus on /what/ is done rather than /how/. This is sufficient:

rv: Add KUnit tests for the tlob monitor

Add a test case to the shared rv_monitors_test.c suite covering the uprobe-line parser in the tlob monitor.

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

I suggest you find some AI skills to write commit messages more tailored to the kernel if that's how you write commit messages. caveman-commit [1] usually does a decent job, though it's more generic and needs some changes. You can even get one written by your agent based on some (arguably) good commit messages you find with git log (e.g. filter by author or subsystem).

Anyway this is minor (but address it in v7!), the patch looks good:

Thanks,
Gabriele

[1] - https://github.com/JuliusBrussee/caveman

Quoted 221 lines

 kernel/trace/rv/monitors/tlob/tlob.c       | 24 ++++-- kernel/trace/rv/monitors/tlob/tlob_kunit.c | 87 ++++++++++++++++++++++ kernel/trace/rv/monitors/tlob/tlob_kunit.h | 17 +++++ kernel/trace/rv/rv_monitors_test.c         |  2 +

 4 files changed, 122 insertions(+), 8 deletions(-)

 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.h

kernel/trace/rv/monitors/tlob/tlob.c +3 -4
index 08b1bee884cc..18150cbf57a5 100644
@@ -20,7 +20,6 @@
  #include <linux/namei.h>
  #include <linux/rv.h>
  #include <linux/slab.h>
-#include <kunit/visibility.h>
  #include <rv/instrumentation.h>
  #include <rv/rv_uprobe.h>
  #include <rv.h>
@@ -877,9 +876,9 @@ static ssize_t tlob_monitor_read(struct file *file,
   * PATH may contain ':'; the last ':' separates path from offset.
   * Returns 0, -EINVAL, or -ERANGE.
   */
-VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
-					    char **path_out,
-					    loff_t *start_out, loff_t
 *stop_out)
+static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+				  char **path_out,
+				  loff_t *start_out, loff_t *stop_out)
  {
  	unsigned long long thr = 0, stop_val = 0;

  long long start_val;

kernel/trace/rv/monitors/tlob/tlob.c +0 -1
@@ -951,13 +950,12 @@ VISIBLE_IF_KUNIT 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 +2 -2
-VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out,
-					    loff_t *start_out)
+static int tlob_parse_remove_line(char *buf, char **path_out,
+				  loff_t *start_out)

 {
  char *binpath, *colon;
  long long off;

kernel/trace/rv/monitors/tlob/tlob.c +11 -1
@@ -980,7 +978,17 @@ 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);
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+#include <kunit/visibility.h>
+#include "tlob_kunit.h"
+
+const struct rv_tlob_kunit_ops rv_tlob_kunit_ops = {
+	.parse_uprobe_line = tlob_parse_uprobe_line,
+	.parse_remove_line = tlob_parse_remove_line,
+};
+EXPORT_SYMBOL_IF_KUNIT(rv_tlob_kunit_ops);
+#endif


 static int tlob_create_or_delete_uprobe(char *buf) {

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +20 -0
new file mode 100644
index 000000000000..a8987fba2533
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include "tlob_kunit.h"
+
+#if IS_REACHABLE(CONFIG_RV_MON_TLOB)
+
+/* 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 rv_test_tlob(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,

rv_tlob_kunit_ops.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 +5 -0
+	}
+
+	for (i = 0; i < ARRAY_SIZE(tlob_parse_invalid); i++) {
+		strscpy(buf, tlob_parse_invalid[i], sizeof(buf));
+		KUNIT_EXPECT_EQ(test,

rv_tlob_kunit_ops.parse_uprobe_line(buf, &thr, &path,

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +1 -0
+									 

&start, &stop), -EINVAL);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +5 -0
+	}
+
+	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,

rv_tlob_kunit_ops.parse_uprobe_line(buf, &thr, &path,

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +1 -0
+									 

&start, &stop), -ERANGE);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +5 -0
+	}
+
+	for (i = 0; i < ARRAY_SIZE(tlob_remove_valid); i++) {
+		strscpy(buf, tlob_remove_valid[i], sizeof(buf));
+		KUNIT_EXPECT_EQ(test,

rv_tlob_kunit_ops.parse_remove_line(buf, &path, &start), 0);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +5 -0
+	}
+
+	for (i = 0; i < ARRAY_SIZE(tlob_remove_invalid); i++) {
+		strscpy(buf, tlob_remove_invalid[i], sizeof(buf));
+		KUNIT_EXPECT_EQ(test,

rv_tlob_kunit_ops.parse_remove_line(buf, &path, &start),

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +7 -0
+				-EINVAL);
+	}
+}
+
+#else
+#define rv_test_tlob rv_test_stub
+#endif
kernel/trace/rv/monitors/tlob/tlob_kunit.h new file +12 -0
new file mode 100644
index 000000000000..4c1081871ea3
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __TLOB_KUNIT_H
+#define __TLOB_KUNIT_H
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+
+#include <linux/types.h>
+
+extern const struct rv_tlob_kunit_ops {
+	int (*parse_uprobe_line)(char *buf, u64 *thr_out, char **path_out,
+				 loff_t *start_out, loff_t *stop_out);
+	int (*parse_remove_line)(char *buf, char **path_out, loff_t

*start_out);

kernel/trace/rv/monitors/tlob/tlob_kunit.h new file +5 -0
+} rv_tlob_kunit_ops;
+
+#endif
+
+#endif /* __TLOB_KUNIT_H */
kernel/trace/rv/rv_monitors_test.c +1 -0
index 3ad11195e664..791df0fe03e3 100644
@@ -153,6 +153,7 @@ static void rv_test_dummy(struct kunit *test)
  #include "monitors/nomiss/nomiss_kunit.c"
  #include "monitors/pagefault/pagefault_kunit.c"
  #include "monitors/sleep/sleep_kunit.c"
+#include "monitors/tlob/tlob_kunit.c"


 static struct kunit_case rv_mon_test_cases[] = {
  KUNIT_CASE(rv_test_dummy),

kernel/trace/rv/rv_monitors_test.c +1 -0
@@ -163,6 +164,7 @@ static struct kunit_case rv_mon_test_cases[] = {
  	KUNIT_CASE(rv_test_nomiss),
  	KUNIT_CASE(rv_test_pagefault),
  	KUNIT_CASE(rv_test_sleep),
+	KUNIT_CASE(rv_test_tlob),
  	{}
  };

maintainer Reviewed-by on v6 8/9
#22 lore raw

Re: [PATCH v6 8/9] selftests/verification: Add tlob selftests

In reply to [email protected]
Quoted 5 lines · On Fri, 2026-08-21 at 00:45 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

(quoted diff) +4 -0
+
+/* start probe; busy-spin so running_ns dominates */
+noinline void tlob_busy_work(unsigned long duration_ms)
+{

Review

Quoted 10 lines
(quoted diff) +10 -0
+	struct timespec start, now;
+	unsigned long elapsed;
+
+	clock_gettime(CLOCK_MONOTONIC, &start);
+	do {
+		clock_gettime(CLOCK_MONOTONIC, &now);
+		elapsed = (unsigned long)(now.tv_sec - start.tv_sec)
+			  * 1000000000UL
+			+ (unsigned long)(now.tv_nsec - start.tv_nsec);
+	} while (elapsed < duration_ms * 1000000UL);

I really don't like repeated code, cannot this go to a static inline __tlob_busy_wait(duration_ms) and you call that from tlob_busy_work() and tlob_preempt_work() ?

Quoted 33 lines
(quoted diff) +33 -0
+
+	tlob_busy_work_done();
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_sleep_work_done(void)
+{
+	asm volatile("" ::: "memory");
+}
+
+/* start probe; nanosleep so sleeping_ns dominates */
+noinline void tlob_sleep_work(unsigned long duration_ms)
+{
+	struct timespec ts = {
+		.tv_sec  = duration_ms / 1000,
+		.tv_nsec = (long)(duration_ms % 1000) * 1000000L,
+	};
+	nanosleep(&ts, NULL);
+	tlob_sleep_work_done();
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_preempt_work_done(void)
+{
+	asm volatile("" ::: "memory");
+}
+
+/*
+ * start probe; busy-spin so an RT competitor on the same CPU drives
+ * waiting_ns (prev_state==0 -> preempt event, task stays runnable off-CPU).
+ */
+noinline void tlob_preempt_work(unsigned long duration_ms)
+{

Review

Quoted 10 lines
(quoted diff) +10 -0
+	struct timespec start, now;
+	unsigned long elapsed;
+
+	clock_gettime(CLOCK_MONOTONIC, &start);
+	do {
+		clock_gettime(CLOCK_MONOTONIC, &now);
+		elapsed = (unsigned long)(now.tv_sec - start.tv_sec)
+			  * 1000000000UL
+			+ (unsigned long)(now.tv_nsec - start.tv_nsec);
+	} while (elapsed < duration_ms * 1000000UL);

So here you'd just call __tlob_busy_wait(duration_ms).

Review

Quoted 3 lines
(quoted diff) +3 -0
+
+	tlob_preempt_work_done();
+}

Tests look good. Apparently all that started_list/started_node thing in tlob.c is for multiple tasks sharing the same binding right. Shouldn't that be tested? I just got a simple test for that, it's generated and manually fixed but you may want to double check if it's really relevant for this.

Anyway that isn't too important, for now:

Thanks,
Gabriele

tools/testing/selftests/verification/test.d/tlob/uprobe_multi_instance.tc new file +68 -0
new file mode 100644
index 000000000000..33404683d29a
@@ -0,0 +1,68 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor multiple instances of same uprobe binding (concurrent tasks on same binding)
+# requires: tlob:monitor
+
+# Fall back to the verification directory relative to FTRACETEST_ROOT when not
+# set by make (e.g. in installed kselftest environments).
+: "${VERIFICATIONTEST_BINDIR:="$FTRACETEST_ROOT/../verification"}"
+
+UPROBE_TARGET="${VERIFICATIONTEST_BINDIR}/tlob_target"
+TLOB_SYM="${VERIFICATIONTEST_BINDIR}/tlob_sym"
+TLOB_MONITOR=monitors/tlob/monitor
+UPROBE_COMM=$(basename ${UPROBE_TARGET})
+
+busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
+busy_stop=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+
+# Start 3 concurrent instances of the target running the same binary probe
+"$UPROBE_TARGET" 30000 &
+pid1=$!
+"$UPROBE_TARGET" 30000 &
+pid2=$!
+"$UPROBE_TARGET" 30000 &
+pid3=$!
+
+teardown() {
+	kill "$pid1" 2>/dev/null || true; wait "$pid1" 2>/dev/null || true
+	kill "$pid2" 2>/dev/null || true; wait "$pid2" 2>/dev/null || true
+	kill "$pid3" 2>/dev/null || true; wait "$pid3" 2>/dev/null || true
+}
+trap teardown EXIT
+sleep 0.05
+
+echo 1 > ../events/rv/event_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+# 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"
+
+# Wait up to 2 s for all three pids to be registered and transition through start.
+# This proves multiple tasks can hit the same uprobe binding concurrently.
+found1=0; found2=0; found3=0
+i=0
+while [ "$i" -lt 20 ]; do
+	sleep 0.1
+	grep "event_tlob" ../trace | grep -wq "${UPROBE_COMM}-${pid1}" && found1=1
+	grep "event_tlob" ../trace | grep -wq "${UPROBE_COMM}-${pid2}" && found2=1
+	grep "event_tlob" ../trace | grep -wq "${UPROBE_COMM}-${pid3}" && found3=1
+	if [ "$found1" = "1" ] && [ "$found2" = "1" ] && [ "$found3" = "1" ]; then
+		break
+	fi
+	i=$((i+1))
+done
+
+[ "$found1" = "1" ]
+[ "$found2" = "1" ]
+[ "$found3" = "1" ]
+
+# Removing the uprobe while tasks are active must succeed cleanly and unbind them.
+# Active tasks will be detached (binding set to NULL), parked tasks will be destroyed.
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR"
+! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR" || false
+
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/event_tlob/enable
+echo > ../trace
maintainer
#11 lore raw

Re: [PATCH v6 4/9] rv: Fix ha_invariant_passed_ns silent bypass of invariant check

In reply to [email protected]
Quoted 13 lines · On Fri, 2026-08-21 at 00:45 +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.

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

Quoted 5 lines

This is a stopgap: once the RV framework reworks the per-env clock guard, this first-invocation reset should be subsumed.

---

I would like to solve this a bit differently. I have this patch hanging around to reset() when the monitor starts, which makes the whole invalid thing obsolete.

I was planning on sending it together with other changes when I have time to test them, but it seems the patch works fine on this series as well.

I reverted your fix, applied that and added the ha_reset_env() which you were not defining:

(quoted diff) +0 -0

b/kernel/trace/rv/monitors/tlob/tlob.c
index 18150cbf57a5..934eea34021a 100644

kernel/trace/rv/monitors/tlob/tlob.c +0 -0
@@ -175,6 +175,12 @@ static u64 ha_get_env(struct ha_monitor *ha_mon, enum

envs_tlob env,

        return ENV_INVALID_VALUE;

}

+static void ha_reset_env(struct ha_monitor *ha_mon, enum envs_tlob env, u64 time_ns)
+{
+ if (env == clk_elapsed_tlob)
+ ha_reset_clk_ns(ha_mon, env, time_ns);
+}
+
/*
* Invariant: clk_elapsed < BUDGET_NS in running/waiting/sleeping. "stopped"
* is exempt: the parked period must not be measured against the old window's

---

Applying the appended patch too and running the selftests seems to work fine on my setup.

If you want, you can add this patch to your series for now as it doesn't have dependencies, I can sort the rest out.

Thanks,
Gabriele

---

From 29bdf58fc8f108d63b06051b415b97a45b27a976 Mon Sep 17 00:00:00 2001
From: Gabriele Monaco <[email protected]>
Date: Mon, 25 May 2026 10:23:25 +0200
Subject: [PATCH] rv: Force environment reset action on HA monitor start

Currently the monitor variables with a storage (e.g. clocks) are initialised as invalid, then the first explicit reset() sets them as valid for constraint. This only adds extra complexity to handle the invalid case in constraints check.

Apply a reset() by default when starting monitors instead of resetting to an invalid state. If monitors have no stored variable (hence no reset function) add a macro HA_NO_RESET to stub it, this is all transparently handled by rvgen.

The reset() action on ns-granularity clocks needs the (possibly cached) current time, read it directly there. This might cause a double call to ktime_get_ns() within the same da_handle_start_run_event() but will occur only the first time and is harmless.

---
 include/rv/ha_monitor.h                 | 8 +++++++-
 kernel/trace/rv/monitors/opid/opid.h    | 1 +
 tools/verification/rvgen/rvgen/dot2c.py | 2 ++
 3 files changed, 10 insertions(+), 1 deletion(-)
include/rv/ha_monitor.h +7 -1
index c2e200234e67..4fc5dadb4240 100644
@@ -167,19 +167,25 @@ static void ha_monitor_destroy(void)
 
 /* Should be supplied by the monitor */
 static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs env, u64 time_ns);
+static void ha_reset_env(struct ha_monitor *ha_mon, enum envs env, u64 time_ns);
 static bool ha_verify_constraint(struct ha_monitor *ha_mon,
 				 enum states curr_state,
 				 enum events event,
 				 enum states next_state,
 				 u64 time_ns);
+#ifdef HA_NO_RESET
+static void ha_reset_env(struct ha_monitor *ha_mon, enum envs env, u64 time_ns) { }
+#endif
 
 /*
  * ha_monitor_reset_all_stored - reset all environment variables in the monitor
  */
 static inline void ha_monitor_reset_all_stored(struct ha_monitor *ha_mon)
 {
+	u64 time_ns = ha_get_ns();
+
 	for (int i = 0; i < ENV_MAX_STORED; i++)
-		WRITE_ONCE(ha_mon->env_store[i], ENV_INVALID_VALUE);
+		ha_reset_env(ha_mon, i, time_ns);
 }
 
 /*
kernel/trace/rv/monitors/opid/opid.h +1 -0
index fb0aa4c28aa6..f85b1959c2fb 100644
@@ -28,6 +28,7 @@ enum envs_opid {
 };
 
 _Static_assert(env_max_stored_opid <= MAX_HA_ENV_LEN, "Not enough slots");
+#define HA_NO_RESET
 
 struct automaton_opid {
 	char *state_names[state_max_opid];
tools/verification/rvgen/rvgen/dot2c.py +2 -0
index 22938ce1bf6c..1532e6b6e199 100644
@@ -90,6 +90,8 @@ class Dot2c(Automata):
                         ' "Not enough slots");')
             if {"ns", "us", "ms", "s"}.intersection(self.env_types.values()):
                 buff.append("#define HA_CLK_NS")
+            if len(self.env_stored) == 0:
+                buff.append("#define HA_NO_RESET")
             buff.append("")
         return buff
 
Signature · 1 line
2.55.0