* [PATCH v8 00/14] rv: Add task latency over budget RV monitor
@ 2026-11-05 17:49 wen.yang
2026-11-05 17:49 ` [PATCH v8 01/14] rv: Force environment reset action on HA monitor start wen.yang
` (13 more replies)
0 siblings, 14 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
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 four states (running, waiting, sleeping, stopped)
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; which
phase consumed the budget (running, waiting or sleeping) is recovered
from event_tlob in user space (see monitor_tlob.rst).
Tasks are registered for monitoring by writing to the tracefs monitor
file:
echo "p:myregion /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 restarts in place, reusing the
same pre-allocated slot.
Changes in v8
=============
This version is a comprehensive rework addressing Gabriele Monaco's
review on v7, with substantial simplifications to both framework
support and the tlob monitor itself:
1. HA timer teardown and underflow fixes (Patches 1-5):
- Deleted ha_timer_ready() completely. Following Gabriele's
suggestion, ha_monitor_free_env() now checks da_monitoring() to
disarm the hrtimer on the storage free path (Patch 4).
- Isolated the HA underflow and timer free path KUnit tests into a
dedicated compilation unit (kernel/trace/rv/rv_ha_kunit.c) guarded
by CONFIG_RV_HA_MONITOR, avoiding polluting rv_monitors_test.c
(Patches 3, 5).
2. Framework per-object storage improvements (Patches 6-8):
- Added DA_OBJ_EMBEDDED_TYPE support based on Gabriele's PoC.
Per-object DA storage can now directly embed monitor-specific
target data inside struct da_monitor_storage, completely eliminating
secondary dynamic memory allocations and double pointer dereferences
(Patch 8).
- Added smp_store_release/smp_load_acquire memory barriers between
da_storage_set_target() and da_storage_is_empty(), and ensured
da_get_target() returns NULL if the storage slot is not yet filled.
3. Generic uprobe helper infrastructure (Patch 9):
- Extracted reusable rv_uprobe registration and synchronization
helpers into include/rv/rv_uprobe.h and kernel/trace/rv/rv_uprobe.c.
4. Major tlob simplification (Patch 11):
- Reduced tlob.c from ~1065 lines to ~660 lines (-38% code size).
- Completely removed the private mempool (tlob_record_pool) and custom
allocator; tlob now relies entirely on the framework's pre-allocated
storage via DA_MON_POOL_SIZE.
- Removed the parallel state machine (OPEN, CLOSED, RETIRED) and
tlob_binding_lock. Window lifecycle is now 100% managed by the
automaton states and framework storage lifecycle.
- Replaced complex quiesce/reap/RCU callouts with a clean RCU walk
(tlob_release_binding()) executed after rv_uprobe_unregister_sync().
- Optimized timer re-arming on the scheduler hot path.
- Refined error semantics: nested START is detected as an illegal event
(running x start) and reported via error_tlob without altering the
running window; pool exhaustion cleanly returns -ENOSPC.
5. Test coverage and selftests expansion (Patches 12-14):
- Added 3 new selftests covering critical edge cases:
* uprobe_task_exit.tc: verifies task exit/SIGKILL inside an active
window disarms the timer with no orphaned/late violations.
* uprobe_nested_start.tc: verifies nested START reporting and
window integrity.
* uprobe_pool_exhausted.tc: verifies the 64-task cap and graceful
rejection of the 65th task.
- Extended tlob_target with "nest" mode.
- All 13 selftests and all KUnit test suites pass cleanly.
Series structure
================
Part 1: Independent fixes and enhancements to the RV/HA framework
Patch 1: rv: Force environment reset action on HA monitor start
Patch 2: rv: Fix ha_monitor timer and invariant underflow
Patch 3: rv: Add a KUnit test for the HA timer underflow
Patch 4: rv: Disarm HA timers on the storage free path
Patch 5: rv: Add a KUnit test for the storage free path
Patch 6: rv: Make da_monitor_reset_hook, sync_hook and EVENT_NONE_LBL
overridable
Patch 7: rv: Introduce DA_MON_ALLOCATION_STRATEGY
Patch 8: rv: Support embedded target types in per-object DA storage
Patch 9: rv: Add generic uprobe infrastructure for RV monitors
Part 2: The tlob monitor and models
Patch 10: rv: Add tlob model DOT file
Patch 11: rv: Add tlob hybrid automaton monitor
Part 3: Testing and selftests
Patch 12: rv: Add KUnit tests for the tlob monitor
Patch 13: selftests/verification: Add tlob selftests
Patch 14: selftests/ftrace: Walk up parent dirs to find test.d/functions
v7: https://lore.kernel.org/all/cover.1791745088.git.wen.yang@linux.dev/
v6: https://lore.kernel.org/all/cover.1787243842.git.wen.yang@linux.dev/
v4: https://lore.kernel.org/all/cover.1783524627.git.wen.yang@linux.dev/
v3: https://lore.kernel.org/all/cover.1780847473.git.wen.yang@linux.dev/
v2: https://lore.kernel.org/all/cover.1778522945.git.wen.yang@linux.dev/
v1: https://lore.kernel.org/all/cover.1776020428.git.wen.yang@linux.dev/
Gabriele Monaco (1):
rv: Force environment reset action on HA monitor start
Wen Yang (13):
rv: Fix ha_monitor timer and invariant underflow
rv: Add a KUnit test for the HA timer underflow
rv: Disarm HA timers on the storage free path
rv: Add a KUnit test for the storage free path
rv: Make da_monitor_reset_hook, sync_hook and EVENT_NONE_LBL
overridable
rv: Introduce DA_MON_ALLOCATION_STRATEGY
rv: Support embedded target types in per-object DA storage
rv: Add generic uprobe infrastructure for RV monitors
rv: Add tlob model DOT file
rv: Add tlob hybrid automaton monitor
rv: Add KUnit tests for the tlob monitor
selftests/verification: Add tlob selftests
selftests/ftrace: Walk up parent dirs to find test.d/functions
Documentation/trace/rv/index.rst | 1 +
Documentation/trace/rv/monitor_tlob.rst | 162 ++++
include/rv/da_monitor.h | 226 +++++-
include/rv/ha_monitor.h | 53 +-
include/rv/rv_uprobe.h | 35 +
kernel/trace/rv/Kconfig | 5 +
kernel/trace/rv/Makefile | 3 +
kernel/trace/rv/monitors/nomiss/nomiss.c | 2 +-
kernel/trace/rv/monitors/opid/opid.h | 1 +
kernel/trace/rv/monitors/tlob/Kconfig | 12 +
kernel/trace/rv/monitors/tlob/tlob.c | 743 ++++++++++++++++++
kernel/trace/rv/monitors/tlob/tlob.h | 143 ++++
kernel/trace/rv/monitors/tlob/tlob_kunit.c | 151 ++++
kernel/trace/rv/monitors/tlob/tlob_kunit.h | 18 +
kernel/trace/rv/monitors/tlob/tlob_trace.h | 20 +
kernel/trace/rv/rv_ha_kunit.c | 201 +++++
kernel/trace/rv/rv_monitors_test.c | 2 +
kernel/trace/rv/rv_trace.h | 1 +
kernel/trace/rv/rv_uprobe.c | 56 ++
tools/testing/selftests/ftrace/ftracetest | 19 +-
.../testing/selftests/verification/.gitignore | 1 +
tools/testing/selftests/verification/Makefile | 4 +
tools/testing/selftests/verification/config | 3 +
.../selftests/verification/test.d/functions | 9 +
.../test.d/tlob/run_tlob_tests.sh | 32 +
.../verification/test.d/tlob/uprobe_bind.tc | 47 ++
.../test.d/tlob/uprobe_disable_live.tc | 79 ++
.../verification/test.d/tlob/uprobe_multi.tc | 58 ++
.../test.d/tlob/uprobe_multi_instance.tc | 65 ++
.../test.d/tlob/uprobe_nested_start.tc | 62 ++
.../test.d/tlob/uprobe_no_event.tc | 17 +
.../test.d/tlob/uprobe_overlap_register.tc | 66 ++
.../test.d/tlob/uprobe_pool_exhausted.tc | 75 ++
.../test.d/tlob/uprobe_restart.tc | 66 ++
.../test.d/tlob/uprobe_restart_violation.tc | 48 ++
.../test.d/tlob/uprobe_task_exit.tc | 65 ++
.../test.d/tlob/uprobe_unbind_live.tc | 65 ++
.../test.d/tlob/uprobe_violation.tc | 53 ++
.../selftests/verification/tlob_target.c | 156 ++++
tools/verification/models/tlob.dot | 27 +
tools/verification/rvgen/rvgen/dot2c.py | 2 +
41 files changed, 2829 insertions(+), 25 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_ha_kunit.c
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_disable_live.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_multi_instance.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_nested_start.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_overlap_register.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_pool_exhausted.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_restart_violation.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_task_exit.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_unbind_live.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
create mode 100644 tools/testing/selftests/verification/tlob_target.c
create mode 100644 tools/verification/models/tlob.dot
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 01/14] rv: Force environment reset action on HA monitor start
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 02/14] rv: Fix ha_monitor timer and invariant underflow wen.yang
` (12 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Gabriele Monaco <gmonaco@redhat.com>
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.
Signed-off-by: Gabriele Monaco <gmonaco@redhat.com>
Tested-by: Wen Yang <wen.yang@linux.dev>
---
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(-)
diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h
index 6e1c7fe5449a..93ac99ff7f1d 100644
--- a/include/rv/ha_monitor.h
+++ b/include/rv/ha_monitor.h
@@ -159,19 +159,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);
}
/*
diff --git a/kernel/trace/rv/monitors/opid/opid.h b/kernel/trace/rv/monitors/opid/opid.h
index fb0aa4c28aa6..f85b1959c2fb 100644
--- a/kernel/trace/rv/monitors/opid/opid.h
+++ b/kernel/trace/rv/monitors/opid/opid.h
@@ -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];
diff --git a/tools/verification/rvgen/rvgen/dot2c.py b/tools/verification/rvgen/rvgen/dot2c.py
index 22938ce1bf6c..1532e6b6e199 100644
--- a/tools/verification/rvgen/rvgen/dot2c.py
+++ b/tools/verification/rvgen/rvgen/dot2c.py
@@ -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
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 02/14] rv: Fix ha_monitor timer and invariant underflow
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
2026-11-05 17:49 ` [PATCH v8 01/14] rv: Force environment reset action on HA monitor start wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 03/14] rv: Add a KUnit test for the HA timer underflow wen.yang
` (11 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco
Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang, sashiko-bot
From: Wen Yang <wen.yang@linux.dev>
ha_start_timer_ns() and ha_start_timer_jiffy() compute the remaining
budget as expire - passed. Once the elapsed time passed exceeds the
budget expire (e.g. a state transition after the invariant should have
fired, before the timer callback ran), the unsigned subtraction
underflows to a huge value and the timer is scheduled far into the
future instead of expiring immediately, so the monitor stops enforcing
the budget.
ha_check_invariant_ns() computes store >= time_ns - expire_ns, which
underflows when the system uptime is still below expire_ns (a monitor
armed early in boot with a large budget), reporting a false violation.
Clamp the timer remainder to 0 (fire immediately) when passed >= expire,
and rewrite the invariant check as store + expire_ns >= time_ns so
neither side can underflow. Both timer flavours (jiffy and hrtimer) are
fixed.
Fixes: f5587d1b6ec9 ("rv: Add Hybrid Automata monitor type")
Fixes: ab2900ae252b ("rv: Simplify hybrid automata monitors's clock variables")
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Reviewed-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/ha_monitor.h | 29 ++++++++++++++++++++++++-----
1 file changed, 24 insertions(+), 5 deletions(-)
diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h
index 93ac99ff7f1d..ebb7f91e3447 100644
--- a/include/rv/ha_monitor.h
+++ b/include/rv/ha_monitor.h
@@ -351,7 +351,11 @@ static inline void ha_reset_clk_ns(struct ha_monitor *ha_mon, enum envs env, u64
static inline bool ha_check_invariant_ns(struct ha_monitor *ha_mon, enum envs env,
u64 time_ns, u64 expire_ns)
{
- return READ_ONCE(ha_mon->env_store[env]) >= time_ns - expire_ns;
+ /*
+ * Compare as store + expire >= time to avoid underflow when the
+ * system uptime is still below expire_ns (early boot, large budget).
+ */
+ return READ_ONCE(ha_mon->env_store[env]) + expire_ns >= time_ns;
}
/*
* ha_invariant_passed_ns - prepare the invariant and return the time since reset
@@ -410,6 +414,17 @@ static inline void ha_inv_to_guard(struct ha_monitor *ha_mon, enum envs env,
WRITE_ONCE(ha_mon->env_store[env], READ_ONCE(ha_mon->env_store[env]) - value);
}
+/*
+ * ha_time_left - time left before expire, 0 once it already passed
+ *
+ * A plain expire - passed underflows and would arm the timer far in the
+ * future. Unit-agnostic: callers pass ns or jiffies consistently.
+ */
+static inline u64 ha_time_left(u64 passed, u64 expire)
+{
+ return passed < expire ? expire - passed : 0;
+}
+
#if HA_TIMER_TYPE == HA_TIMER_WHEEL
/*
* Helper functions to handle the monitor timer.
@@ -439,15 +454,17 @@ static inline void ha_start_timer_jiffy(struct ha_monitor *ha_mon, enum envs env
{
u64 passed = ha_invariant_passed_jiffy(ha_mon, env, time_ns);
- mod_timer(&ha_mon->timer, get_jiffies_64() + expire - passed);
+ mod_timer(&ha_mon->timer,
+ get_jiffies_64() + ha_time_left(passed, expire));
}
static inline void ha_start_timer_ns(struct ha_monitor *ha_mon, enum envs env,
u64 expire, u64 time_ns)
{
u64 passed = ha_invariant_passed_ns(ha_mon, env, time_ns);
+ u64 delay = ha_time_left(passed, expire);
ha_start_timer_jiffy(ha_mon, ENV_MAX_STORED,
- nsecs_to_jiffies(expire - passed + TICK_NSEC - 1), time_ns);
+ nsecs_to_jiffies(delay + TICK_NSEC - 1), time_ns);
}
/*
* ha_cancel_timer - Cancel the timer
@@ -494,7 +511,8 @@ static inline void ha_start_timer_ns(struct ha_monitor *ha_mon, enum envs env,
if (RV_MON_TYPE == RV_MON_PER_CPU)
mode |= HRTIMER_MODE_PINNED;
- hrtimer_start(&ha_mon->hrtimer, ns_to_ktime(expire - passed), mode);
+ hrtimer_start(&ha_mon->hrtimer,
+ ns_to_ktime(ha_time_left(passed, expire)), mode);
}
static inline void ha_start_timer_jiffy(struct ha_monitor *ha_mon, enum envs env,
u64 expire, u64 time_ns)
@@ -502,7 +520,8 @@ static inline void ha_start_timer_jiffy(struct ha_monitor *ha_mon, enum envs env
u64 passed = ha_invariant_passed_jiffy(ha_mon, env, time_ns);
ha_start_timer_ns(ha_mon, ENV_MAX_STORED,
- jiffies_to_nsecs(expire - passed), time_ns);
+ jiffies_to_nsecs(ha_time_left(passed, expire)),
+ time_ns);
}
/*
* ha_cancel_timer - Cancel the timer
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 03/14] rv: Add a KUnit test for the HA timer underflow
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
2026-11-05 17:49 ` [PATCH v8 01/14] rv: Force environment reset action on HA monitor start wen.yang
2026-11-05 17:49 ` [PATCH v8 02/14] rv: Fix ha_monitor timer and invariant underflow wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 04/14] rv: Disarm HA timers on the storage free path wen.yang
` (10 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
Add rv_ha_kunit.c as a standalone test suite with rvtest, a minimal
per-object HA monitor, to test both underflows fixed by the previous
patch: an invariant checked while the uptime is below the budget holds,
and a timer armed after the budget elapsed expires now. Without that fix
both expectations fail.
Building the HA test in its own compilation unit keeps ha_monitor.h
isolated from non-monitor test runners and avoids macro conflicts. The
suite is guarded by IS_ENABLED(CONFIG_RV_HA_MONITOR) so kernels without
HA monitors build cleanly.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
kernel/trace/rv/Makefile | 1 +
kernel/trace/rv/rv_ha_kunit.c | 152 ++++++++++++++++++++++++++++++++++
2 files changed, 153 insertions(+)
create mode 100644 kernel/trace/rv/rv_ha_kunit.c
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index cdbf68c84f5a..4b84b83b942b 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -26,3 +26,4 @@ obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
obj-$(CONFIG_RV_REACT_PRINTK) += reactor_printk.o
obj-$(CONFIG_RV_REACT_PANIC) += reactor_panic.o
obj-$(CONFIG_RV_MONITORS_KUNIT_TEST) += rv_monitors_test.o
+obj-$(CONFIG_RV_MONITORS_KUNIT_TEST) += rv_ha_kunit.o
diff --git a/kernel/trace/rv/rv_ha_kunit.c b/kernel/trace/rv/rv_ha_kunit.c
new file mode 100644
index 000000000000..e8aaee110fae
--- /dev/null
+++ b/kernel/trace/rv/rv_ha_kunit.c
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for RV hybrid automata (HA) framework
+ */
+#include <kunit/test.h>
+#include <linux/rv.h>
+
+#if IS_ENABLED(CONFIG_RV_HA_MONITOR)
+
+#include <rv_trace.h>
+
+/* rvtest: a minimal per-object HA monitor driven directly by the tests. */
+#define MONITOR_NAME rvtest
+#define RV_MON_TYPE RV_MON_PER_OBJ
+#define HA_TIMER_TYPE HA_TIMER_HRTIMER
+typedef void *monitor_target;
+
+#define rv_monitoring_on() true
+#define trace_rv_retries_error(...) do { } while (0)
+#define trace_event_rvtest(...) do { } while (0)
+#define trace_error_rvtest(...) do { } while (0)
+#define trace_error_env_rvtest(...) do { } while (0)
+
+enum states_rvtest {
+ idle_rvtest,
+ armed_rvtest,
+ state_max_rvtest,
+};
+
+#define INVALID_STATE state_max_rvtest
+
+enum events_rvtest {
+ start_rvtest,
+ event_max_rvtest,
+};
+
+enum envs_rvtest {
+ clk_rvtest,
+ env_max_rvtest,
+ env_max_stored_rvtest = env_max_rvtest,
+};
+
+#define HA_CLK_NS
+
+struct automaton_rvtest {
+ char *state_names[state_max_rvtest];
+ char *event_names[event_max_rvtest];
+ char *env_names[env_max_rvtest];
+ unsigned char function[state_max_rvtest][event_max_rvtest];
+ unsigned char initial_state;
+ bool final_states[state_max_rvtest];
+};
+
+static const struct automaton_rvtest automaton_rvtest = {
+ .state_names = { "idle", "armed" },
+ .event_names = { "start" },
+ .env_names = { "clk" },
+ .function = {
+ { armed_rvtest }, /* idle */
+ { INVALID_STATE }, /* armed */
+ },
+ .initial_state = idle_rvtest,
+ .final_states = { 1, 0 },
+};
+
+#include <rv/ha_monitor.h>
+
+#define RVTEST_BUDGET_NS (300ULL * NSEC_PER_MSEC)
+
+static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_rvtest env,
+ u64 time_ns)
+{
+ return ha_get_clk_ns(ha_mon, env, time_ns);
+}
+
+static void ha_reset_env(struct ha_monitor *ha_mon, enum envs_rvtest env,
+ u64 time_ns)
+{
+ ha_reset_clk_ns(ha_mon, env, 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)
+{
+ return true;
+}
+
+static const struct rv_kunit_mon rvtest_mon __maybe_unused = RV_MON_OPS_INIT();
+
+/*
+ * rv_test_rvtest_underflow - the HA clock math must not underflow as u64
+ *
+ * An invariant checked while the uptime is below the budget holds, and a
+ * timer armed after the budget elapsed expires now.
+ */
+static void rv_test_rvtest_underflow(struct kunit *test)
+{
+ struct ha_monitor *ha_mon;
+ struct da_monitor *da_mon;
+ const int id = 4242;
+
+ da_monitor_init();
+ rv_this.enabled = 1;
+
+ da_handle_start_run_event(id, NULL, start_rvtest);
+
+ scoped_guard(rcu)
+ da_mon = da_get_monitor(id, NULL);
+ KUNIT_ASSERT_NOT_NULL(test, da_mon);
+ ha_mon = to_ha_monitor(da_mon);
+
+ /* Uptime below the budget: time_ns - expire_ns would underflow. */
+ ha_reset_clk_ns(ha_mon, clk_rvtest, 0);
+ KUNIT_EXPECT_TRUE(test, ha_check_invariant_ns(ha_mon, clk_rvtest, 0,
+ RVTEST_BUDGET_NS));
+
+ /*
+ * Budget already elapsed: expire - passed would underflow and queue
+ * the timer at KTIME_SEC_MAX. Reset first so the expiry does not
+ * report.
+ */
+ da_monitor_reset(&ha_mon->da_mon);
+ ha_reset_clk_ns(ha_mon, clk_rvtest, 0);
+ ha_start_timer_ns(ha_mon, clk_rvtest, RVTEST_BUDGET_NS,
+ RVTEST_BUDGET_NS + NSEC_PER_SEC);
+ KUNIT_EXPECT_TRUE(test,
+ ktime_before(hrtimer_get_expires(&ha_mon->hrtimer),
+ ktime_get() + NSEC_PER_SEC));
+
+ ha_cancel_timer_sync(ha_mon);
+ da_destroy_storage(id);
+ da_monitor_destroy();
+ rv_this.enabled = 0;
+}
+
+static struct kunit_case rv_ha_kunit_cases[] = {
+ KUNIT_CASE(rv_test_rvtest_underflow),
+ {}
+};
+
+static struct kunit_suite rv_ha_kunit_suite = {
+ .name = "rv_ha",
+ .test_cases = rv_ha_kunit_cases,
+};
+
+kunit_test_suite(rv_ha_kunit_suite);
+
+#endif /* CONFIG_RV_HA_MONITOR */
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for RV hybrid automata (HA) framework");
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 04/14] rv: Disarm HA timers on the storage free path
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (2 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 03/14] rv: Add a KUnit test for the HA timer underflow wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 05/14] rv: Add a KUnit test for " wen.yang
` (9 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
da_destroy_storage() cancels the HA timer through da_monitor_reset_hook()
and only then unlinks the storage with hash_del_rcu(). A handler on
another CPU that looked the storage up before the unlink can still call
ha_start_timer_*() after the cancel. The timer then stays queued on a
storage that kfree_rcu() frees a grace period later, and fires on freed
memory, reporting a violation for an object that no longer exists.
A monitor cannot close this from its own handlers: any cancel it issues
precedes the unlink. The first point no reader can race with is the
RCU callback that frees the storage.
Add da_monitor_free_hook(), called right before a per-object storage is
freed, from the RCU callback and from da_monitor_destroy(). HA monitors
implement it by calling ha_cancel_timer_sync() if da_monitoring() is true.
Replace kfree_rcu() with call_rcu() so there is a callback to hook.
Cancelling synchronously from the RCU callback is safe: HA hrtimers are
always armed in HARD mode, so hrtimer_cancel() only spins, and on
PREEMPT_RT, where timer_delete_sync() may sleep, RCU callbacks run in
the rcuc kthread.
Storages whose monitor never started are skipped via da_monitoring().
Fixes: 4a24127bd6cb ("rv: Add support for per-object monitors in DA/HA")
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/da_monitor.h | 27 ++++++++++++++++++++++++++-
include/rv/ha_monitor.h | 8 ++++++++
2 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/include/rv/da_monitor.h b/include/rv/da_monitor.h
index e3cf85c9ce55..c7f9733fb8a4 100644
--- a/include/rv/da_monitor.h
+++ b/include/rv/da_monitor.h
@@ -67,6 +67,17 @@ static struct rv_monitor rv_this;
#define da_monitor_sync_hook()
#endif
+/*
+ * Called on the last reference to a storage, right before it is freed.
+ * Monitors embedding a timer disarm it here: an earlier point races with a
+ * reader that resolved the storage before hash_del_rcu(), and
+ * tracepoint_synchronize_unregister() only covers tracepoint probes.
+ * Default: no-op.
+ */
+#ifndef da_monitor_free_hook
+#define da_monitor_free_hook(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 +554,15 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
return mon_storage->target;
}
+static void da_kfree_cb(struct rcu_head *head)
+{
+ struct da_monitor_storage *ms =
+ container_of(head, struct da_monitor_storage, rcu);
+
+ da_monitor_free_hook(&ms->rv.da_mon);
+ kfree(ms);
+}
+
/*
* da_destroy_storage - destroy the per-object storage
*
@@ -564,7 +584,11 @@ static inline void da_destroy_storage(da_id_type id)
return;
da_monitor_reset_hook(&mon_storage->rv.da_mon);
hash_del_rcu(&mon_storage->node);
- kfree_rcu(mon_storage, rcu);
+ /*
+ * A reader from before hash_del_rcu() may still arm; the free
+ * callback runs past it, so the hook disarms there.
+ */
+ call_rcu(&mon_storage->rcu, da_kfree_cb);
}
static void __da_monitor_reset_all(void (*reset)(struct da_monitor *))
@@ -607,6 +631,7 @@ 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_monitor_free_hook(&mon_storage->rv.da_mon);
hash_del_rcu(&mon_storage->node);
kfree(mon_storage);
}
diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h
index ebb7f91e3447..3bf1dd15544c 100644
--- a/include/rv/ha_monitor.h
+++ b/include/rv/ha_monitor.h
@@ -29,6 +29,7 @@ static inline void ha_monitor_reset_env(struct da_monitor *da_mon);
static inline void ha_setup_timer(struct ha_monitor *ha_mon);
static inline bool ha_cancel_timer(struct ha_monitor *ha_mon);
static inline void ha_cancel_timer_sync(struct ha_monitor *ha_mon);
+static inline void ha_monitor_free_env(struct da_monitor *da_mon);
static bool ha_monitor_handle_constraint(struct da_monitor *da_mon,
enum states curr_state,
enum events event,
@@ -38,6 +39,7 @@ static bool ha_monitor_handle_constraint(struct da_monitor *da_mon,
#define da_monitor_init_hook ha_monitor_init_env
#define da_monitor_reset_hook ha_monitor_reset_env
#define da_monitor_sync_hook() synchronize_rcu()
+#define da_monitor_free_hook ha_monitor_free_env
#if !defined(HA_SKIP_AUTO_CLEANUP) && RV_MON_TYPE == RV_MON_PER_TASK
/*
@@ -551,6 +553,12 @@ static inline bool ha_cancel_timer(struct ha_monitor *ha_mon)
static inline void ha_cancel_timer_sync(struct ha_monitor *ha_mon) { }
#endif
+static inline void ha_monitor_free_env(struct da_monitor *da_mon)
+{
+ if (da_monitoring(da_mon))
+ ha_cancel_timer_sync(to_ha_monitor(da_mon));
+}
+
#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
#ifdef RV_MON_OPS_INIT
#undef RV_MON_OPS_INIT
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 05/14] rv: Add a KUnit test for the storage free path
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (3 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 04/14] rv: Disarm HA timers on the storage free path wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 06/14] rv: Make da_monitor_reset_hook, sync_hook and EVENT_NONE_LBL overridable wen.yang
` (8 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
A per-object HA monitor whose timer is armed through a pointer resolved
before da_destroy_storage() outlives the storage and reports against a
gone entity. Real handlers resolve and use a storage under one
guard(rcu)(), so the fixture issues the two steps separately to reach
that window.
Add rv_test_rvtest_arm_after_destroy() to rv_ha_kunit.c to verify that
the free hook properly disarms the timer before the storage is freed.
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
kernel/trace/rv/rv_ha_kunit.c | 51 ++++++++++++++++++++++++++++++++++-
1 file changed, 50 insertions(+), 1 deletion(-)
diff --git a/kernel/trace/rv/rv_ha_kunit.c b/kernel/trace/rv/rv_ha_kunit.c
index e8aaee110fae..29ee8a739739 100644
--- a/kernel/trace/rv/rv_ha_kunit.c
+++ b/kernel/trace/rv/rv_ha_kunit.c
@@ -3,12 +3,16 @@
* KUnit tests for RV hybrid automata (HA) framework
*/
#include <kunit/test.h>
+#include <linux/atomic.h>
+#include <linux/delay.h>
#include <linux/rv.h>
#if IS_ENABLED(CONFIG_RV_HA_MONITOR)
#include <rv_trace.h>
+static atomic_t rvtest_expired;
+
/* rvtest: a minimal per-object HA monitor driven directly by the tests. */
#define MONITOR_NAME rvtest
#define RV_MON_TYPE RV_MON_PER_OBJ
@@ -19,7 +23,7 @@ typedef void *monitor_target;
#define trace_rv_retries_error(...) do { } while (0)
#define trace_event_rvtest(...) do { } while (0)
#define trace_error_rvtest(...) do { } while (0)
-#define trace_error_env_rvtest(...) do { } while (0)
+#define trace_error_env_rvtest(...) atomic_inc(&rvtest_expired)
enum states_rvtest {
idle_rvtest,
@@ -134,8 +138,53 @@ static void rv_test_rvtest_underflow(struct kunit *test)
rv_this.enabled = 0;
}
+/*
+ * rv_test_rvtest_arm_after_destroy - a storage outlived by its own timer
+ *
+ * da_destroy_storage() cancels the timer, then unlinks:
+ *
+ * da_monitor_reset_hook() cancels
+ * hash_del_rcu()
+ * call_rcu()
+ *
+ * A reader that resolved the storage before the unlink can arm past that
+ * cancel. Handlers resolve and use under one guard(rcu)(), so the two steps
+ * are issued separately here.
+ *
+ * The timer is still queued after the destroy, and it fires -- reporting a
+ * violation for an entity that is gone.
+ */
+static void rv_test_rvtest_arm_after_destroy(struct kunit *test)
+{
+ const int id = 4242;
+ struct da_monitor *stale;
+
+ atomic_set(&rvtest_expired, 0);
+ da_monitor_init();
+ rv_this.enabled = 1;
+
+ da_handle_start_run_event(id, NULL, start_rvtest);
+
+ scoped_guard(rcu) /* resolve before the unlink */
+ stale = da_get_monitor(id, NULL);
+ KUNIT_ASSERT_NOT_NULL(test, stale);
+
+ da_destroy_storage(id); /* cancel, unlink, call_rcu */
+ ha_start_timer_ns(to_ha_monitor(stale), clk_rvtest,
+ RVTEST_BUDGET_NS, ha_get_ns());
+
+ rcu_barrier();
+
+ /* A timer the free path missed expires here. */
+ msleep(600);
+ KUNIT_EXPECT_EQ(test, atomic_read(&rvtest_expired), 0);
+ da_monitor_destroy();
+ rv_this.enabled = 0;
+}
+
static struct kunit_case rv_ha_kunit_cases[] = {
KUNIT_CASE(rv_test_rvtest_underflow),
+ KUNIT_CASE(rv_test_rvtest_arm_after_destroy),
{}
};
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 06/14] rv: Make da_monitor_reset_hook, sync_hook and EVENT_NONE_LBL overridable
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (4 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 05/14] rv: Add a KUnit test for " wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 07/14] rv: Introduce DA_MON_ALLOCATION_STRATEGY wen.yang
` (7 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
Wrap da_monitor_reset_hook, da_monitor_sync_hook, and EVENT_NONE_LBL with
ifndef guards so HA-based monitors can substitute their own implementations
before including this header.
Overrides of da_monitor_reset_hook must still call ha_monitor_reset_env()
or cancel outstanding timers to avoid timer UAF.
No behaviour change for monitors that do not override these macros.
Reviewed-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/ha_monitor.h | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h
index 3bf1dd15544c..5372e5dfe3c5 100644
--- a/include/rv/ha_monitor.h
+++ b/include/rv/ha_monitor.h
@@ -37,8 +37,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
#define da_monitor_free_hook ha_monitor_free_env
#if !defined(HA_SKIP_AUTO_CLEANUP) && RV_MON_TYPE == RV_MON_PER_TASK
@@ -77,7 +83,9 @@ _Static_assert(offsetof(struct ha_monitor, da_mon) == 0,
#define ENV_INVALID_VALUE U64_MAX
/* Error with no event occurs only on timeouts */
#define EVENT_NONE EVENT_MAX
+#ifndef EVENT_NONE_LBL
#define EVENT_NONE_LBL "none"
+#endif
#define ENV_BUFFER_SIZE 64
#ifdef CONFIG_RV_REACTORS
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 07/14] rv: Introduce DA_MON_ALLOCATION_STRATEGY
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (5 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 06/14] rv: Make da_monitor_reset_hook, sync_hook and EVENT_NONE_LBL overridable wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 08/14] rv: Support embedded target types in per-object DA storage wen.yang
` (6 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
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
nomiss switches from the implicit DA_SKIP_AUTO_ALLOC override to an
explicit DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL; the behaviours are
equivalent (pre-allocated storage, no allocation on the sched_switch
start path).
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/da_monitor.h | 121 +++++++++++++++++++++--
kernel/trace/rv/monitors/nomiss/nomiss.c | 2 +-
2 files changed, 112 insertions(+), 11 deletions(-)
diff --git a/include/rv/da_monitor.h b/include/rv/da_monitor.h
index c7f9733fb8a4..76169e41d4e5 100644
--- a/include/rv/da_monitor.h
+++ b/include/rv/da_monitor.h
@@ -14,7 +14,45 @@
#ifndef _RV_DA_MONITOR_H
#define _RV_DA_MONITOR_H
+/*
+ * Allocation strategy for RV_MON_PER_OBJ monitors, selected before including
+ * this header. Defining DA_MON_POOL_SIZE N implies DA_ALLOC_POOL.
+ *
+ * DA_ALLOC_AUTO - kmalloc on demand; unbounded (default).
+ * DA_ALLOC_POOL - DA_MON_POOL_SIZE pre-allocated slots. The start handler
+ * must run in task context; other handlers only call
+ * da_handle_event() and never allocate.
+ * 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
+#if defined(DA_MON_ALLOCATION_STRATEGY) && \
+ DA_MON_ALLOCATION_STRATEGY != DA_ALLOC_POOL
+#error "DA_MON_POOL_SIZE implies DA_ALLOC_POOL"
+#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
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#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>
@@ -563,6 +601,46 @@ static void da_kfree_cb(struct rcu_head *head)
kfree(ms);
}
+/*
+ * Zero-initialised for non-pool monitors, whose 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);
+
+ da_monitor_free_hook(&ms->rv.da_mon);
+ mempool_free(ms, &da_monitor_pool);
+}
+
+/*
+ * da_create_pool_storage - pop a free pool slot and insert it into the hash.
+ * Returns NULL if the pool is exhausted. Caller holds 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;
+
+ 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;
+
+ hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+ return &mon_storage->rv.da_mon;
+}
+
/*
* da_destroy_storage - destroy the per-object storage
*
@@ -588,7 +666,10 @@ static inline void da_destroy_storage(da_id_type id)
* A reader from before hash_del_rcu() may still arm; the free
* callback runs past it, so the hook disarms there.
*/
- call_rcu(&mon_storage->rcu, da_kfree_cb);
+ if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+ call_rcu(&mon_storage->rcu, da_pool_return_cb);
+ else
+ call_rcu(&mon_storage->rcu, da_kfree_cb);
}
static void __da_monitor_reset_all(void (*reset)(struct da_monitor *))
@@ -614,6 +695,13 @@ 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) {
+ size_t storage_size = sizeof(struct da_monitor_storage);
+
+ return mempool_init_kmalloc_pool(&da_monitor_pool,
+ DA_MON_POOL_SIZE,
+ storage_size);
+ }
return 0;
}
@@ -633,20 +721,33 @@ static inline void da_monitor_destroy(void)
hash_for_each_safe(da_monitor_ht, bkt, tmp, mon_storage, node) {
da_monitor_free_hook(&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.
+ * Only the first start allocates; later starts reuse the existing da_mon.
+ * Must run in task context (see the allocation strategies above).
*/
-#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 */
diff --git a/kernel/trace/rv/monitors/nomiss/nomiss.c b/kernel/trace/rv/monitors/nomiss/nomiss.c
index 6e47d379f777..c80a2c51c514 100644
--- a/kernel/trace/rv/monitors/nomiss/nomiss.c
+++ b/kernel/trace/rv/monitors/nomiss/nomiss.c
@@ -18,7 +18,7 @@
#define RV_MON_TYPE RV_MON_PER_OBJ
#define HA_TIMER_TYPE HA_TIMER_WHEEL
/* The start condition is on sched_switch, it's dangerous to allocate there */
-#define DA_SKIP_AUTO_ALLOC
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL
typedef struct sched_dl_entity *monitor_target;
#include "nomiss.h"
#include <rv/ha_monitor.h>
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 08/14] rv: Support embedded target types in per-object DA storage
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (6 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 07/14] rv: Introduce DA_MON_ALLOCATION_STRATEGY wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 09/14] rv: Add generic uprobe infrastructure for RV monitors wen.yang
` (5 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
A PER_OBJ monitor usually typedefs monitor_target itself, as a pointer to
the monitored object (borrowed: the object exists independently of the
monitor and outlives the storage).
A monitor whose target is monitor-owned data can instead define
DA_OBJ_EMBEDDED_TYPE with a complete struct type: the framework then
derives monitor_target as a pointer to it, embeds one instance in each
per-object storage, and allocates and frees it together with the storage.
On storage creation the caller-supplied target is shallow-copied into
the storage before it is published, so it may be a fully initialised
stack object.
This allows per-object monitors to manage custom per-instance data without
maintaining private mempools, complex multi-stage reference counting, or
out-of-band teardown state machines.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/da_monitor.h | 82 ++++++++++++++++++++++++++++++++++++++---
1 file changed, 76 insertions(+), 6 deletions(-)
diff --git a/include/rv/da_monitor.h b/include/rv/da_monitor.h
index 76169e41d4e5..ed9e7e740d80 100644
--- a/include/rv/da_monitor.h
+++ b/include/rv/da_monitor.h
@@ -61,6 +61,10 @@
#include <linux/slab.h>
#include <linux/hashtable.h>
+#if defined(DA_OBJ_EMBEDDED_TYPE) && RV_MON_TYPE != RV_MON_PER_OBJ
+#error "DA_OBJ_EMBEDDED_TYPE is only supported by RV_MON_PER_OBJ monitors"
+#endif
+
/*
* Per-cpu variables require a unique name although static in some
* configurations (e.g. CONFIG_DEBUG_FORCE_WEAK_PER_CPU or alpha modules).
@@ -453,9 +457,24 @@ static inline void da_monitor_destroy(void)
* Functions to define, init and get a per-object monitor.
*/
+#ifdef DA_OBJ_EMBEDDED_TYPE
+/*
+ * Framework-owned target: one instance is embedded in each storage and
+ * allocated/freed with it. monitor_target keeps pointer semantics, but
+ * da_get_target() points into the storage rather than to a borrowed object.
+ * The header derives the typedef: do not define monitor_target by hand.
+ */
+typedef DA_OBJ_EMBEDDED_TYPE * monitor_target;
+#endif
+
struct da_monitor_storage {
da_id_type id;
+#ifdef DA_OBJ_EMBEDDED_TYPE
+ DA_OBJ_EMBEDDED_TYPE target;
+ bool filled;
+#else
monitor_target target;
+#endif
union rv_task_monitor rv;
struct hlist_node node;
struct rcu_head rcu;
@@ -466,6 +485,40 @@ struct da_monitor_storage {
#endif
static DEFINE_HASHTABLE(da_monitor_ht, DA_MONITOR_HT_BITS);
+#ifdef DA_OBJ_EMBEDDED_TYPE
+
+#define TARGET_PTR(storage) (&(storage)->target)
+
+static inline bool da_storage_is_empty(struct da_monitor_storage *mon_storage)
+{
+ /* Pairs with smp_store_release in da_storage_set_target(). */
+ return !smp_load_acquire(&mon_storage->filled);
+}
+static inline void da_storage_set_target(struct da_monitor_storage *mon_storage,
+ monitor_target target)
+{
+ if (WARN_ON_ONCE(!target))
+ return;
+
+ memcpy(&mon_storage->target, target, sizeof(mon_storage->target));
+ /* Pairs with smp_load_acquire in da_storage_is_empty(). */
+ smp_store_release(&mon_storage->filled, true);
+}
+#else
+
+#define TARGET_PTR(storage) storage->target
+
+static inline bool da_storage_is_empty(struct da_monitor_storage *mon_storage)
+{
+ return !TARGET_PTR(mon_storage);
+}
+static inline void da_storage_set_target(struct da_monitor_storage *mon_storage,
+ monitor_target target)
+{
+ mon_storage->target = target;
+}
+#endif
+
/*
* da_create_empty_storage - pre-allocate an empty storage
*/
@@ -503,7 +556,7 @@ static inline struct da_monitor *da_create_storage(da_id_type id,
if (!mon_storage)
return NULL;
- mon_storage->target = target;
+ da_storage_set_target(mon_storage, target);
return &mon_storage->rv.da_mon;
}
@@ -536,10 +589,20 @@ static struct da_monitor *da_get_monitor(da_id_type id, monitor_target target)
/*
* da_get_target - return the object associated to the monitor
+ *
+ * NULL for an embedded target whose storage was pre-allocated but not filled
+ * yet: a manual-allocation monitor publishes its storage before it has one.
*/
static inline monitor_target da_get_target(struct da_monitor *da_mon)
{
- return container_of(da_mon, struct da_monitor_storage, rv.da_mon)->target;
+ struct da_monitor_storage *mon_storage =
+ container_of(da_mon, struct da_monitor_storage, rv.da_mon);
+
+#ifdef DA_OBJ_EMBEDDED_TYPE
+ if (unlikely(da_storage_is_empty(mon_storage)))
+ return NULL;
+#endif
+ return TARGET_PTR(mon_storage);
}
/*
@@ -572,8 +635,11 @@ static inline struct da_monitor *da_fill_empty_storage(da_id_type id,
monitor_target target,
struct da_monitor *da_mon)
{
- if (unlikely(da_mon && !da_get_target(da_mon)))
- container_of(da_mon, struct da_monitor_storage, rv.da_mon)->target = target;
+ struct da_monitor_storage *mon_storage =
+ container_of(da_mon, struct da_monitor_storage, rv.da_mon);
+
+ if (unlikely(da_mon && da_storage_is_empty(mon_storage)))
+ da_storage_set_target(mon_storage, target);
return da_mon;
}
@@ -589,7 +655,11 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
if (unlikely(!mon_storage))
return NULL;
- return mon_storage->target;
+#ifdef DA_OBJ_EMBEDDED_TYPE
+ if (unlikely(da_storage_is_empty(mon_storage)))
+ return NULL;
+#endif
+ return TARGET_PTR(mon_storage);
}
static void da_kfree_cb(struct rcu_head *head)
@@ -635,7 +705,7 @@ da_create_pool_storage(da_id_type id, monitor_target target,
memset(mon_storage, 0, sizeof(*mon_storage));
mon_storage->id = id;
- mon_storage->target = target;
+ da_storage_set_target(mon_storage, target);
hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
return &mon_storage->rv.da_mon;
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 09/14] rv: Add generic uprobe infrastructure for RV monitors
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (7 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 08/14] rv: Support embedded target types in per-object DA storage wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 10/14] rv: Add tlob model DOT file wen.yang
` (4 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
Monitors that instrument user-space function boundaries need to 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 an inode reference
(acquired via igrab()) for the probed binary for the full uprobe lifetime,
as uprobe_register() does not take an inode reference. On unregister,
the inode reference is released with iput() only after
rv_uprobe_unregister_sync().
rv_uprobe_unregister_sync() calls uprobe_unregister_sync(), draining
all in-flight handlers before the containing struct may be freed.
The API provides register, nosync unregister, a global handler barrier
(rv_uprobe_unregister_sync), and an active-state predicate.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/rv_uprobe.h | 35 +++++++++++++++++++++++
kernel/trace/rv/Kconfig | 4 +++
kernel/trace/rv/Makefile | 1 +
kernel/trace/rv/rv_uprobe.c | 56 +++++++++++++++++++++++++++++++++++++
4 files changed, 96 insertions(+)
create mode 100644 include/rv/rv_uprobe.h
create mode 100644 kernel/trace/rv/rv_uprobe.c
diff --git a/include/rv/rv_uprobe.h b/include/rv/rv_uprobe.h
new file mode 100644
index 000000000000..b93c2a81bc22
--- /dev/null
+++ b/include/rv/rv_uprobe.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Wen Yang <wen.yang@linux.dev> */
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ */
+
+#ifndef _RV_UPROBE_H
+#define _RV_UPROBE_H
+
+#include <linux/types.h>
+#include <linux/uprobes.h>
+
+struct inode;
+struct pt_regs;
+
+/*
+ * Free the containing struct only after rv_uprobe_unregister_nosync() +
+ * rv_uprobe_unregister_sync() + iput(p->inode).
+ *
+ * The inode reference does not pin the dentry, so callers that re-resolve the
+ * probed path must hold a struct path: 9p cache=none remints an inode per
+ * lookup.
+ */
+struct rv_uprobe {
+ struct uprobe_consumer uc;
+ struct uprobe *uprobe;
+ struct inode *inode;
+};
+
+bool rv_uprobe_is_registered(const struct rv_uprobe *p);
+int rv_uprobe_register(struct inode *inode, loff_t offset, struct rv_uprobe *p);
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p);
+void rv_uprobe_unregister_sync(void);
+
+#endif /* _RV_UPROBE_H */
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index efa930f94ea4..2bc412304293 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -86,6 +86,10 @@ source "kernel/trace/rv/monitors/nomiss/Kconfig"
# Add new monitors here
+config RV_UPROBE
+ bool
+ depends on RV && UPROBES
+
config RV_REACTORS
bool "Runtime verification reactors"
default y
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index 4b84b83b942b..8dc14a1819b8 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -22,6 +22,7 @@ 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
# 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
diff --git a/kernel/trace/rv/rv_uprobe.c b/kernel/trace/rv/rv_uprobe.c
new file mode 100644
index 000000000000..1c7f8466aeed
--- /dev/null
+++ b/kernel/trace/rv/rv_uprobe.c
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ */
+#include <linux/fs.h>
+#include <linux/uprobes.h>
+#include <rv/rv_uprobe.h>
+
+/*
+ * uprobe_register() takes no inode reference of its own, so grab one here and
+ * drop it only after rv_uprobe_unregister_sync().
+ */
+int rv_uprobe_register(struct inode *inode, loff_t offset, struct rv_uprobe *p)
+{
+ int ret;
+
+ p->inode = igrab(inode);
+ if (!p->inode)
+ return -ESTALE;
+
+ p->uprobe = uprobe_register(inode, offset, 0, &p->uc);
+ if (IS_ERR(p->uprobe)) {
+ ret = PTR_ERR(p->uprobe);
+ p->uprobe = NULL;
+ iput(p->inode);
+ p->inode = NULL;
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_register);
+
+bool rv_uprobe_is_registered(const struct rv_uprobe *p)
+{
+ return p && p->uprobe;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_is_registered);
+
+/* The inode stays held: in-flight handlers may still reach it. */
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p)
+{
+ if (!p || !p->uprobe)
+ return;
+
+ uprobe_unregister_nosync(p->uprobe, &p->uc);
+ p->uprobe = NULL;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister_nosync);
+
+/* Global barrier over every probe dequeued so far. */
+void rv_uprobe_unregister_sync(void)
+{
+ uprobe_unregister_sync();
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister_sync);
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 10/14] rv: Add tlob model DOT file
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (8 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 09/14] rv: Add generic uprobe infrastructure for RV monitors wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 11/14] rv: Add tlob hybrid automaton monitor wen.yang
` (3 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
Add the Graphviz DOT specification of the tlob hybrid automaton to
tools/verification/models/.
The model has four states (stopped, running, waiting, sleeping) and six
events (start, stop, sleep, preempt, wakeup, switch_in). A single clock
invariant, clk_elapsed < BUDGET_NS(), holds in running, waiting and
sleeping; stopped has no invariant, hence no timer.
wakeup is a self-loop on running and waiting: a task that is already
queued can still be the target of a traced wakeup, either by waking
itself or through ttwu_runnable() while preempted, and neither case ends
the budget window. stopped takes every scheduler event as a self-loop,
so a task keeps going through the scheduler between windows.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
tools/verification/models/tlob.dot | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 tools/verification/models/tlob.dot
diff --git a/tools/verification/models/tlob.dot b/tools/verification/models/tlob.dot
new file mode 100644
index 000000000000..021a4d823548
--- /dev/null
+++ b/tools/verification/models/tlob.dot
@@ -0,0 +1,27 @@
+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 = ellipse] "stopped"};
+ "__init_stopped" -> "stopped";
+ "running" [label = "running\nclk_elapsed <= BUDGET_NS()"];
+ "waiting" [label = "waiting\nclk_elapsed <= BUDGET_NS()"];
+ "sleeping" [label = "sleeping\nclk_elapsed <= BUDGET_NS()"];
+ "stopped" [label = "stopped", color = green3];
+ "running" -> "sleeping" [ label = "sleep" ];
+ "running" -> "waiting" [ label = "preempt" ];
+ "running" -> "running" [ label = "wakeup" ];
+ "waiting" -> "running" [ label = "switch_in" ];
+ "waiting" -> "waiting" [ label = "wakeup" ];
+ "sleeping" -> "waiting" [ label = "wakeup" ];
+ "running" -> "stopped" [ label = "stop" ];
+ "stopped" -> "running" [ label = "start;reset(clk_elapsed)" ];
+ "stopped" -> "stopped" [ label = "preempt\nsleep\nswitch_in\nwakeup" ];
+ { rank = min ;
+ "__init_stopped";
+ "stopped";
+ }
+}
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 11/14] rv: Add tlob hybrid automaton monitor
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (9 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 10/14] rv: Add tlob model DOT file wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 12/14] rv: Add KUnit tests for the tlob monitor wen.yang
` (2 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
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 the elapsed time exceeds a configurable
budget.
The monitor tracks four states (running, waiting, sleeping, stopped)
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; which
phase consumed the budget (running, waiting or sleeping) is recovered
from event_tlob in user space.
Tasks are registered for monitoring by writing to the tracefs monitor
file using the ftrace uprobe_events convention:
echo "p:myregion /path/to/binary:OFFSET_START OFFSET_STOP threshold=NS" \
>> /sys/kernel/tracing/rv/monitors/tlob/monitor
The per-task monitoring record is embedded directly into struct
da_monitor_storage via DA_OBJ_EMBEDDED_TYPE, eliminating the need for a
private mempool, separate RCU reclamation, or parallel OPEN/CLOSED/RETIRED
states. Window transitions and reentrancy are managed directly by the
automaton model and framework storage lifecycle.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
Documentation/trace/rv/index.rst | 1 +
Documentation/trace/rv/monitor_tlob.rst | 162 +++++
kernel/trace/rv/Kconfig | 1 +
kernel/trace/rv/Makefile | 1 +
kernel/trace/rv/monitors/tlob/Kconfig | 12 +
kernel/trace/rv/monitors/tlob/tlob.c | 732 +++++++++++++++++++++
kernel/trace/rv/monitors/tlob/tlob.h | 143 ++++
kernel/trace/rv/monitors/tlob/tlob_trace.h | 20 +
kernel/trace/rv/rv_trace.h | 1 +
9 files changed, 1073 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
diff --git a/Documentation/trace/rv/index.rst b/Documentation/trace/rv/index.rst
index 29769f06bb0f..1501545b5f08 100644
--- a/Documentation/trace/rv/index.rst
+++ b/Documentation/trace/rv/index.rst
@@ -16,5 +16,6 @@ Runtime Verification
monitor_wwnr.rst
monitor_sched.rst
monitor_rtapp.rst
+ monitor_tlob.rst
monitor_stall.rst
monitor_deadline.rst
diff --git a/Documentation/trace/rv/monitor_tlob.rst b/Documentation/trace/rv/monitor_tlob.rst
new file mode 100644
index 000000000000..bf87b90405cd
--- /dev/null
+++ b/Documentation/trace/rv/monitor_tlob.rst
@@ -0,0 +1,162 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+Monitor tlob
+============
+
+- Name: tlob - task latency over budget
+- Type: per-object hybrid automaton (RV_MON_PER_OBJ)
+- Author: Wen Yang <wen.yang@linux.dev>
+
+Description
+-----------
+
+The tlob (task latency over budget) monitor is a per-object hybrid
+automaton that checks a task's elapsed time (CLOCK_MONOTONIC) against a
+budget: from an instrumented entry point to its matching return, the
+task must not spend more than the budget in any combination of running,
+waiting and sleeping. The clock ``clk_elapsed`` must satisfy
+``clk_elapsed <= BUDGET_NS()`` in the ``running``, ``waiting`` and
+``sleeping`` states; when it does not, the HA timer fires, the framework
+emits ``error_env_tlob`` and resets the monitor::
+
+ | (initial)
+ v
+ +--------------+ stop +----------+
+ +->| running | --------> | stopped |
+ | +--------------+ <-start-- +----------+
+ switch_in preempt sleep
+ | | |
+ | | |
+ | v v
+ +---------+ +---------+
+ | waiting | | sleeping|
+ +---------+ +---------+
+ ^ v
+ | wakeup |
+ | |
+ +------------+
+
+Not drawn above, ``running`` and ``waiting`` each take ``wakeup`` as a
+self-loop: a task woken between ``set_current_state()`` and ``schedule()``
+is still on CPU, and a wakeup never ends the budget window. ``stopped``
+takes every scheduler event (``preempt``, ``sleep``, ``switch_in``,
+``wakeup``) as a self-loop, so a task keeps going through the scheduler
+between windows without disturbing the model. See
+``tools/verification/models/tlob.dot`` for the full model.
+
+``stopped`` has no invariant and hence no timer. A ``stop``
+(``tlob_stop_task()``, window ended) does not reset the monitor: a later
+``start`` (``tlob_start_task()``) begins a new window from ``stopped``.
+Task exit and monitor disable are the paths that free the per-task
+state; removing a binding only resets the windows it opened and leaves
+the state in place. A START while a window is still open (a nested
+window) is a model violation, reported through ``error_tlob``.
+
+The non-running condition (monitor not yet started, or reset after a
+violation or monitor disable) is handled implicitly by the RV framework;
+it is not an explicit DA state.
+
+Usage
+-----
+
+tracefs interface (uprobe-based external monitoring)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``monitor`` tracefs file instruments an unmodified binary via
+uprobes, following the ftrace ``uprobe_events`` convention. Each
+binding has a NAME::
+
+ p:NAME PATH:OFFSET_START OFFSET_STOP threshold=NS
+
+The probes at ``OFFSET_START`` and ``OFFSET_STOP`` (ELF file offsets of
+entry points in ``PATH``) call ``tlob_start_task()`` and
+``tlob_stop_task()``. ``PATH`` may contain ``:``; the last ``:`` in
+``PATH:OFFSET_START`` is the separator. ``threshold`` is in
+nanoseconds and must be between 1 us and 1 hour. ``NAME`` follows the
+ftrace event-name rule (``[A-Za-z_][A-Za-z0-9_]*``, 31 characters at
+most) and must be unique among the registered bindings
+(``-EEXIST``); overlapping ranges on the same binary are rejected
+too, since a task runs one window at a time and a second range would
+shadow the first::
+
+ echo 1 > /sys/kernel/tracing/rv/monitors/tlob/enable
+
+ echo "p:myregion /usr/bin/myapp:0x12a0 0x12f0 threshold=5000000" \
+ >> /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+ # List registered bindings
+ cat /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+ # Remove the binding NAME
+ echo "-:myregion" >> /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+As with the ftrace dynamic-event files, opening ``monitor`` for write
+with ``>`` truncates: ``echo > monitor`` removes all bindings. Use
+``>>`` for every individual command, removals included.
+
+A task may hit the START/STOP pair any number of times: each pair of
+hits is one window, and the per-task record is kept between windows,
+until the task exits. A window ends at whichever comes first, the STOP
+probe or the timer; if the timer won, the STOP is a no-op and the next
+START opens the following window. Removing a binding also ends the
+windows it opened, without reporting them, so a task still running an
+instrumented section is not reported once its binding is gone; a later
+START, from that or another binding, reuses the record.
+
+A task reaching a range bound to a different binding is reported
+through ``error_tlob``: windows do not overlap, and the second would
+shadow the first.
+
+Up to 64 tasks can be monitored at once; the per-object storage comes
+from a pre-allocated pool. A task that starts past that cap is not
+monitored (the kernel log carries a one-time warning), and reuses its
+slot on a later START.
+
+Violation tracepoints
+~~~~~~~~~~~~~~~~~~~~~
+
+A budget violation emits the framework tracepoint ``error_env_tlob``:
+``id`` (task pid), ``state``, ``event`` (``"budget_exceeded"``) and
+``env`` (``clk_elapsed``).
+
+Deriving the per-state breakdown
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Which phase consumed the budget (``running``: compute, ``waiting``:
+scheduler pressure, ``sleeping``: I/O) is recovered from ``event_tlob``,
+the standard state-transition tracepoint. Each window is a run of
+``event_tlob`` lines for one pid: ``start`` opens it, ``stop`` closes it,
+and a violation closes it with ``error_env_tlob``. Attribute the time
+between consecutive lines to the ``next_state`` of the earlier one::
+
+ echo mono > /sys/kernel/tracing/trace_clock
+ echo 1 > /sys/kernel/tracing/events/rv/event_tlob/enable
+ echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+ trace-cmd record -e rv:event_tlob -e rv:error_env_tlob
+ # ... run workload ...
+ trace-cmd report
+
+The ``mono`` clock is required: ``event_tlob`` timestamps are compared
+across CPUs because a task migrates during a window. The three buckets
+sum to ``clk_elapsed`` up to the tracepoint overhead.
+
+Design notes
+------------
+
+- A fresh window always starts in ``running``: monitoring a task that
+ is already waiting or sleeping at START misattributes the first
+ interval to ``running``.
+- ``TASK_STOPPED`` and ``TASK_TRACED`` carry ``prev_state != 0`` and
+ count as ``sleeping``, indistinguishable from I/O-blocked time.
+- A task preempted between ``set_current_state()`` and ``schedule()``
+ carries ``prev_state != 0`` and counts as ``sleeping`` although it
+ stays runnable.
+- ``sched_wakeup_new`` is not hooked: a task is attached by a START
+ probe in its own context, after the initial wakeup.
+
+Specification
+-------------
+
+- Graphviz Dot file: tools/verification/models/tlob.dot
+- KUnit tests: kernel/trace/rv/monitors/tlob/tlob_kunit.c
+- User-space tests: tools/testing/selftests/verification/
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index 2bc412304293..68f0b9bc2dc3 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -82,6 +82,7 @@ source "kernel/trace/rv/monitors/wakeup/Kconfig"
source "kernel/trace/rv/monitors/stall/Kconfig"
source "kernel/trace/rv/monitors/deadline/Kconfig"
source "kernel/trace/rv/monitors/nomiss/Kconfig"
+source "kernel/trace/rv/monitors/tlob/Kconfig"
# Add new deadline monitors here
# Add new monitors here
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index 8dc14a1819b8..99a8dceb829f 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -20,6 +20,7 @@ obj-$(CONFIG_RV_MON_OPID) += monitors/opid/opid.o
obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o
obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o
obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o
+obj-$(CONFIG_RV_MON_TLOB) += monitors/tlob/tlob.o
obj-$(CONFIG_RV_MON_WAKEUP) += monitors/wakeup/wakeup.o
# Add new monitors here
obj-$(CONFIG_RV_UPROBE) += rv_uprobe.o
diff --git a/kernel/trace/rv/monitors/tlob/Kconfig b/kernel/trace/rv/monitors/tlob/Kconfig
new file mode 100644
index 000000000000..4f072a9a4da2
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/Kconfig
@@ -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 time across a
+ user-delimited code section and emits a violation when the
+ elapsed time exceeds a configurable per-invocation budget.
diff --git a/kernel/trace/rv/monitors/tlob/tlob.c b/kernel/trace/rv/monitors/tlob/tlob.c
new file mode 100644
index 000000000000..fe7d88df62f1
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob.c
@@ -0,0 +1,732 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob (task latency over budget) RV monitor.
+ *
+ * A per-task hybrid automaton that checks a task's elapsed time against
+ * a configurable budget across a user-delimited code section.
+ */
+#include <linux/ftrace.h>
+#include <linux/tracepoint.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/rv.h>
+#include <linux/fs.h>
+#include <linux/namei.h>
+#include <linux/security.h>
+#include <linux/ctype.h>
+#include <linux/seq_file.h>
+#include <rv/instrumentation.h>
+#include <rv/rv_uprobe.h>
+#include <rv.h>
+
+#define MODULE_NAME "tlob"
+
+#include <rv_trace.h>
+#include <trace/events/sched.h>
+
+struct tlob_uprobe_binding;
+
+/*
+ * Per-task monitoring record, embedded directly in struct da_monitor_storage
+ * via DA_OBJ_EMBEDDED_TYPE. Allocated and freed with the storage.
+ */
+struct tlob_record {
+ u64 threshold_ns;
+ struct tlob_uprobe_binding *binding;
+};
+
+#define DA_OBJ_EMBEDDED_TYPE struct tlob_record
+#define RV_MON_TYPE RV_MON_PER_OBJ
+#define HA_TIMER_TYPE HA_TIMER_HRTIMER
+#define DA_MON_POOL_SIZE TLOB_MAX_MONITORED
+#define EVENT_NONE_LBL "budget_exceeded"
+
+#include "tlob.h"
+#include <rv/ha_monitor.h>
+
+#define BUDGET_NS(ha_mon) (ha_get_target(ha_mon)->threshold_ns)
+
+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;
+}
+
+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);
+}
+
+/*
+ * clk_elapsed <= BUDGET_NS in running/waiting/sleeping; stopped exempt.
+ */
+static inline bool ha_verify_invariants(struct ha_monitor *ha_mon,
+ enum states curr_state,
+ enum events event,
+ 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));
+}
+
+static inline void ha_setup_invariants(struct ha_monitor *ha_mon,
+ enum events event,
+ enum states next_state,
+ u64 time_ns)
+{
+ if (next_state == stopped_tlob) {
+ ha_cancel_timer(ha_mon);
+ return;
+ }
+
+ /*
+ * Only a start re-anchors the clock. Every other transition keeps
+ * the anchor, so the timer already points at the right expiry and
+ * re-arming it would just take the hrtimer base lock on every
+ * scheduler event of a monitored task.
+ */
+ if (event != start_tlob)
+ return;
+
+ ha_reset_clk_ns(ha_mon, clk_elapsed_tlob, time_ns);
+ ha_start_timer_ns(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon),
+ 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)
+{
+ if (ha_verify_invariants(ha_mon, curr_state, event,
+ next_state, time_ns)) {
+ ha_setup_invariants(ha_mon, event, next_state, time_ns);
+ return true;
+ }
+
+ /*
+ * The event itself found the budget exceeded, so it reports the
+ * overrun and closes the window. Without the reset the transition
+ * would still be taken with the timer left armed, and the expiry
+ * would report the same overrun a second time.
+ */
+ da_monitor_reset(&ha_mon->da_mon);
+ return false;
+}
+
+/*
+ * One registered uprobe pair. Multiple bindings can be active simultaneously
+ * on different binaries or on disjoint ranges of the same binary.
+ */
+struct tlob_uprobe_binding {
+ struct list_head list;
+ char name[TLOB_MAX_NAME];
+ char binpath[TLOB_MAX_PATH];
+ loff_t offset_start;
+ loff_t offset_stop;
+ u64 threshold_ns;
+ struct path path;
+ struct rv_uprobe start_probe;
+ struct rv_uprobe stop_probe;
+};
+
+static LIST_HEAD(tlob_uprobe_list);
+static DEFINE_MUTEX(tlob_uprobe_mutex);
+
+static int tlob_start_task(struct task_struct *task, u64 threshold_ns,
+ struct tlob_uprobe_binding *binding)
+{
+ struct tlob_record *rec;
+ struct da_monitor *da_mon;
+
+ if (!da_monitor_enabled())
+ return -ENODEV;
+
+ if (threshold_ns < TLOB_MIN_THRESHOLD_NS ||
+ threshold_ns > TLOB_MAX_THRESHOLD_NS)
+ return -ERANGE;
+
+ scoped_guard(rcu) {
+ da_mon = da_get_monitor(task->pid, NULL);
+ if (da_mon) {
+ rec = da_get_target(da_mon);
+ if (WARN_ON_ONCE(!rec))
+ return -ENODEV;
+ /*
+ * A record left by a removed binding is adopted: the
+ * task is between windows and nothing owns it.
+ * Anything else starting it while it still belongs to
+ * another binding would silently shadow that window.
+ */
+ if (rec->binding && rec->binding != binding) {
+ trace_error_tlob(task->pid,
+ model_get_state_name(da_mon->curr_state),
+ model_get_event_name(start_tlob));
+ return -EALREADY;
+ }
+ /*
+ * A START while the window is still open is a model
+ * violation (running/waiting/sleeping x start). Hand it
+ * to the model for reporting, but leave the record
+ * untouched: adopting the new budget here would change
+ * the window that is already running.
+ */
+ if (da_monitoring(da_mon) &&
+ READ_ONCE(da_mon->curr_state) != stopped_tlob) {
+ da_handle_event(task->pid, NULL, start_tlob);
+ return -EALREADY;
+ }
+ rec->binding = binding;
+ rec->threshold_ns = threshold_ns;
+ /*
+ * Not da_handle_event(): a violation reset the
+ * monitor, and a plain event is dropped while
+ * !da_monitoring(). This restarts it and runs start.
+ */
+ return da_handle_start_run_event(task->pid, NULL,
+ start_tlob) ? 0 :
+ -ENODEV;
+ }
+ }
+
+ struct tlob_record init_rec = {
+ .threshold_ns = threshold_ns,
+ .binding = binding,
+ };
+
+ if (da_handle_start_run_event(task->pid, &init_rec, start_tlob))
+ return 0;
+
+ /*
+ * The framework reports a disabled monitor and an exhausted pool the
+ * same way. Storage is published only once a pool slot was claimed,
+ * so its absence means the pool is what ran out.
+ */
+ scoped_guard(rcu)
+ da_mon = da_get_monitor(task->pid, NULL);
+ if (da_mon)
+ return -ENODEV;
+
+ pr_warn_once("%s: no free record, task %d stays unmonitored\n",
+ __stringify(MODULE_NAME), task->pid);
+ return -ENOSPC;
+}
+
+static int tlob_stop_task(struct task_struct *task,
+ struct tlob_uprobe_binding *binding)
+{
+ struct tlob_record *rec;
+ struct da_monitor *da_mon;
+
+ if (!da_monitor_enabled())
+ return -ENODEV;
+
+ scoped_guard(rcu) {
+ da_mon = da_get_monitor(task->pid, NULL);
+ if (!da_mon)
+ return -ESRCH;
+
+ rec = da_get_target(da_mon);
+ if (WARN_ON_ONCE(!rec))
+ return -ENODEV;
+ if (binding && rec->binding && rec->binding != binding)
+ return -EINVAL;
+
+ /*
+ * A budget overrun already closed the window and reset the
+ * monitor; the STOP that follows must not report again.
+ */
+ if (!da_monitoring(da_mon))
+ return 0;
+
+ da_handle_event(task->pid, NULL, stop_tlob);
+ }
+ return 0;
+}
+
+static void handle_sched_switch(void *data, bool preempt,
+ struct task_struct *prev,
+ struct task_struct *next,
+ unsigned int prev_state)
+{
+ if (!preempt && prev_state != TASK_RUNNING)
+ da_handle_event(prev->pid, NULL, sleep_tlob);
+ else
+ da_handle_event(prev->pid, NULL, preempt_tlob);
+
+ da_handle_event(next->pid, NULL, switch_in_tlob);
+}
+
+static void handle_sched_wakeup(void *data, struct task_struct *p)
+{
+ da_handle_event(p->pid, NULL, wakeup_tlob);
+}
+
+static void handle_sched_process_exit(void *data, struct task_struct *p,
+ bool group_dead)
+{
+ da_destroy_storage(p->pid);
+}
+
+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);
+
+ 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);
+
+ tlob_stop_task(current, b);
+ return 0;
+}
+
+/*
+ * tlob_release_binding - stop tracking the windows opened by @binding
+ *
+ * A binding may be removed while tasks are still inside its windows. Their
+ * timers are armed and would report an overrun against a binding that no
+ * longer exists, so reset those monitors once no handler can run anymore.
+ * The record is left in place and its owner cleared: a later START, possibly
+ * from another binding, adopts and restarts it.
+ *
+ * Runs after rv_uprobe_unregister_sync(), which is what makes the walk free
+ * of concurrent writers to rec->binding.
+ */
+static void tlob_release_binding(struct tlob_uprobe_binding *binding)
+{
+ struct da_monitor_storage *mon_storage;
+ int bkt;
+
+ guard(rcu)();
+ hash_for_each_rcu(da_monitor_ht, bkt, mon_storage, node) {
+ struct tlob_record *rec = da_get_target(&mon_storage->rv.da_mon);
+
+ if (rec && rec->binding == binding) {
+ rec->binding = NULL;
+ da_monitor_reset(&mon_storage->rv.da_mon);
+ }
+ }
+}
+
+static void tlob_unbind(struct tlob_uprobe_binding *b)
+{
+ rv_uprobe_unregister_nosync(&b->start_probe);
+ rv_uprobe_unregister_nosync(&b->stop_probe);
+ rv_uprobe_unregister_sync();
+
+ tlob_release_binding(b);
+
+ iput(b->start_probe.inode);
+ iput(b->stop_probe.inode);
+ path_put(&b->path);
+}
+
+static int tlob_remove_uprobe_by_name(const char *name)
+{
+ struct tlob_uprobe_binding *b, *tmp;
+
+ guard(mutex)(&tlob_uprobe_mutex);
+
+ list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) {
+ if (strcmp(b->name, name))
+ continue;
+ list_del(&b->list);
+ tlob_unbind(b);
+ kfree(b);
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static void tlob_remove_all_uprobes(void)
+{
+ struct tlob_uprobe_binding *b, *tmp;
+ LIST_HEAD(pending);
+
+ guard(mutex)(&tlob_uprobe_mutex);
+
+ list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) {
+ list_move(&b->list, &pending);
+ rv_uprobe_unregister_nosync(&b->start_probe);
+ rv_uprobe_unregister_nosync(&b->stop_probe);
+ }
+
+ if (list_empty(&pending))
+ return;
+
+ rv_uprobe_unregister_sync();
+
+ list_for_each_entry_safe(b, tmp, &pending, list) {
+ list_del(&b->list);
+ tlob_release_binding(b);
+ iput(b->start_probe.inode);
+ iput(b->stop_probe.inode);
+ path_put(&b->path);
+ kfree(b);
+ }
+}
+
+static int tlob_add_uprobe(const char *name, const char *binpath,
+ loff_t offset_start, loff_t offset_stop,
+ u64 threshold_ns)
+{
+ struct tlob_uprobe_binding *tmp_b;
+ char pathbuf[TLOB_MAX_PATH];
+ struct inode *inode;
+ struct path path __free(path_put) = {};
+ char *canon;
+ int ret;
+
+ guard(mutex)(&tlob_uprobe_mutex);
+
+ if (binpath[0] != '/')
+ return -EINVAL;
+
+ struct tlob_uprobe_binding *b __free(kfree) =
+ kzalloc_obj(*b, GFP_KERNEL);
+ if (!b)
+ return -ENOMEM;
+
+ strscpy(b->name, name, sizeof(b->name));
+ b->threshold_ns = threshold_ns;
+ b->offset_start = offset_start;
+ b->offset_stop = offset_stop;
+
+ 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);
+
+ /*
+ * Names are unique, and so are ranges on the same binary: a task
+ * runs one window at a time, the second would silently shadow the
+ * first. Direction normalised so a reversed start/stop still matches.
+ */
+ list_for_each_entry(tmp_b, &tlob_uprobe_list, list) {
+ loff_t s1, e1, s2, e2;
+
+ if (strcmp(tmp_b->name, name) == 0)
+ return -EEXIST;
+ if (!rv_uprobe_is_registered(&tmp_b->start_probe))
+ continue;
+ if (tmp_b->start_probe.inode != inode)
+ continue;
+
+ s1 = min(offset_start, offset_stop);
+ e1 = max(offset_start, offset_stop);
+ s2 = min(tmp_b->offset_start, tmp_b->offset_stop);
+ e2 = max(tmp_b->offset_start, tmp_b->offset_stop);
+ if (s1 <= e2 && s2 <= e1)
+ 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(inode, offset_start, &b->start_probe);
+ if (ret)
+ return ret;
+
+ b->stop_probe.uc.handler = tlob_uprobe_stop_handler;
+ ret = rv_uprobe_register(inode, offset_stop, &b->stop_probe);
+ if (ret) {
+ tlob_unbind(b);
+ return ret;
+ }
+
+ b->path = path;
+ path_get(&b->path);
+
+ list_add_tail(&no_free_ptr(b)->list, &tlob_uprobe_list);
+ return 0;
+}
+
+static void *tlob_seq_start(struct seq_file *m, loff_t *pos)
+{
+ mutex_lock(&tlob_uprobe_mutex);
+ return seq_list_start(&tlob_uprobe_list, *pos);
+}
+
+static void *tlob_seq_next(struct seq_file *m, void *v, loff_t *pos)
+{
+ return seq_list_next(v, &tlob_uprobe_list, pos);
+}
+
+static void tlob_seq_stop(struct seq_file *m, void *v)
+{
+ mutex_unlock(&tlob_uprobe_mutex);
+}
+
+static int tlob_seq_show(struct seq_file *m, void *v)
+{
+ struct tlob_uprobe_binding *b =
+ list_entry(v, struct tlob_uprobe_binding, list);
+
+ seq_printf(m, "p:%s %s:0x%llx 0x%llx threshold=%llu\n",
+ b->name, b->binpath,
+ (unsigned long long)b->offset_start,
+ (unsigned long long)b->offset_stop,
+ b->threshold_ns);
+ return 0;
+}
+
+static const struct seq_operations tlob_monitor_seq_ops = {
+ .start = tlob_seq_start,
+ .next = tlob_seq_next,
+ .stop = tlob_seq_stop,
+ .show = tlob_seq_show,
+};
+
+static int tlob_parse_uprobe_line(char *buf, char **name_out, char **path_out,
+ loff_t *start_out, loff_t *stop_out,
+ u64 *thr_out)
+{
+ unsigned long long thr = 0, stop_val = 0;
+ long long start_val;
+ char *p, *name, *path_token, *token, *colon;
+ bool got_stop = false, got_thr = false;
+
+ if (buf[0] != 'p' || buf[1] != ':')
+ return -EINVAL;
+
+ p = buf + 2;
+ while (*p == ' ' || *p == '\t')
+ p++;
+
+ name = strsep(&p, " \t");
+ if (strlen(name) >= TLOB_MAX_NAME || !is_good_name(name))
+ return -EINVAL;
+
+ if (!p)
+ return -EINVAL;
+ while (*p == ' ' || *p == '\t')
+ p++;
+ path_token = strsep(&p, " \t");
+ if (!path_token || !*path_token)
+ return -EINVAL;
+
+ colon = strrchr(path_token, ':');
+ if (!colon || colon - path_token < 2)
+ return -EINVAL;
+ *colon = '\0';
+
+ if (path_token[0] != '/')
+ return -EINVAL;
+
+ if (kstrtoll(colon + 1, 0, &start_val))
+ return -EINVAL;
+ if (start_val < 0)
+ return -EINVAL;
+
+ while (p && (token = strsep(&p, " \t")) != NULL) {
+ if (!*token)
+ continue;
+ if (strncmp(token, "threshold=", 10) == 0) {
+ if (got_thr)
+ return -EINVAL;
+ if (kstrtoull(token + 10, 0, &thr))
+ return -EINVAL;
+ if (thr < TLOB_MIN_THRESHOLD_NS ||
+ thr > TLOB_MAX_THRESHOLD_NS)
+ return -ERANGE;
+ got_thr = true;
+ } else if (!got_stop) {
+ long long sv;
+
+ if (kstrtoll(token, 0, &sv))
+ 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;
+ if (start_val == (long long)stop_val)
+ return -EINVAL;
+
+ *name_out = name;
+ *thr_out = thr;
+ *path_out = path_token;
+ *start_out = (loff_t)start_val;
+ *stop_out = (loff_t)stop_val;
+ return 0;
+}
+
+static int tlob_parse_remove_line(char *buf, char **name_out)
+{
+ char *name = buf + 2;
+
+ if (buf[0] != '-' || buf[1] != ':')
+ return -EINVAL;
+ if (strlen(name) >= TLOB_MAX_NAME || !is_good_name(name))
+ return -EINVAL;
+
+ *name_out = name;
+ return 0;
+}
+
+static int tlob_create_or_delete_uprobe(char *buf)
+{
+ loff_t offset_start, offset_stop;
+ u64 threshold_ns;
+ char *name, *binpath;
+ int ret;
+
+ if (buf[0] == '-' && buf[1] == ':') {
+ ret = tlob_parse_remove_line(buf, &name);
+ if (ret)
+ return ret;
+ return tlob_remove_uprobe_by_name(name);
+ }
+ ret = tlob_parse_uprobe_line(buf, &name, &binpath, &offset_start,
+ &offset_stop, &threshold_ns);
+ if (ret)
+ return ret;
+ return tlob_add_uprobe(name, binpath, offset_start, offset_stop,
+ threshold_ns);
+}
+
+static ssize_t tlob_monitor_write(struct file *file,
+ const char __user *ubuf,
+ size_t count, loff_t *ppos)
+{
+ char buf[TLOB_MAX_PATH + TLOB_MAX_NAME + 128];
+
+ if (count >= sizeof(buf))
+ return -EINVAL;
+ if (copy_from_user(buf, ubuf, count))
+ return -EFAULT;
+ buf[count] = '\0';
+ if (count > 0 && buf[count - 1] == '\n')
+ buf[count - 1] = '\0';
+ if (!buf[0])
+ return count;
+ return tlob_create_or_delete_uprobe(buf) ?: (ssize_t)count;
+}
+
+static int tlob_monitor_open(struct inode *inode, struct file *file)
+{
+ int ret;
+
+ ret = security_locked_down(LOCKDOWN_TRACEFS);
+ if (ret)
+ return ret;
+
+ if ((file->f_mode & FMODE_WRITE) && (file->f_flags & O_TRUNC))
+ tlob_remove_all_uprobes();
+
+ return seq_open(file, &tlob_monitor_seq_ops);
+}
+
+static const struct file_operations tlob_monitor_fops = {
+ .open = tlob_monitor_open,
+ .read = seq_read,
+ .write = tlob_monitor_write,
+ .llseek = seq_lseek,
+ .release = seq_release,
+};
+
+static int tlob_enable_hooks(void)
+{
+ rv_attach_trace_probe("tlob", sched_switch, handle_sched_switch);
+ rv_attach_trace_probe("tlob", sched_wakeup, handle_sched_wakeup);
+ rv_attach_trace_probe("tlob", sched_process_exit,
+ handle_sched_process_exit);
+ return 0;
+}
+
+static void tlob_disable_hooks(void)
+{
+ rv_detach_trace_probe("tlob", sched_switch, handle_sched_switch);
+ rv_detach_trace_probe("tlob", sched_wakeup, handle_sched_wakeup);
+ rv_detach_trace_probe("tlob", sched_process_exit,
+ handle_sched_process_exit);
+}
+
+static int enable_tlob(void)
+{
+ int retval;
+
+ retval = ha_monitor_init();
+ if (retval)
+ return retval;
+
+ return tlob_enable_hooks();
+}
+
+static void disable_tlob(void)
+{
+ tlob_disable_hooks();
+ rv_this.enabled = 0;
+ tlob_remove_all_uprobes();
+ ha_monitor_destroy();
+}
+
+static struct rv_monitor rv_this = {
+ .name = "tlob",
+ .description = "Per-task latency-over-budget monitor.",
+ .enable = enable_tlob,
+ .disable = disable_tlob,
+ .reset = da_monitor_reset_all,
+ .enabled = 0,
+};
+
+static int __init register_tlob(void)
+{
+ int ret;
+
+ ret = rv_register_monitor(&rv_this, NULL);
+ if (ret)
+ return ret;
+
+ if (rv_this.root_d) {
+ if (!rv_create_file("monitor", RV_MODE_WRITE,
+ rv_this.root_d, NULL,
+ &tlob_monitor_fops)) {
+ rv_unregister_monitor(&rv_this);
+ return -ENOMEM;
+ }
+ }
+
+ return 0;
+}
+
+static void __exit unregister_tlob(void)
+{
+ rv_unregister_monitor(&rv_this);
+}
+
+module_init(register_tlob);
+module_exit(unregister_tlob);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Wen Yang <wen.yang@linux.dev>");
+MODULE_DESCRIPTION("tlob: task latency over budget per-task monitor.");
diff --git a/kernel/trace/rv/monitors/tlob/tlob.h b/kernel/trace/rv/monitors/tlob/tlob.h
new file mode 100644
index 000000000000..0a9e150436f4
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob.h
@@ -0,0 +1,143 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _RV_TLOB_H
+#define _RV_TLOB_H
+
+/*
+ * C representation of the tlob hybrid automaton (see tlob.dot).
+ * "stop" fires only from running (both callers run on CPU); running x
+ * start is INVALID: a stray re-start must not reset the budget clock.
+ * Invariant: clk_elapsed <= BUDGET_NS() in running/waiting/sleeping;
+ * stopped is exempt.
+ *
+ * Format: Documentation/trace/rv/deterministic_automata.rst
+ */
+
+#include <linux/rv.h>
+#include <linux/sched.h>
+
+#define MONITOR_NAME tlob
+
+enum states_tlob {
+ stopped_tlob,
+ running_tlob,
+ sleeping_tlob,
+ waiting_tlob,
+ state_max_tlob,
+};
+
+#define INVALID_STATE state_max_tlob
+
+enum events_tlob {
+ preempt_tlob,
+ sleep_tlob,
+ start_tlob,
+ stop_tlob,
+ switch_in_tlob,
+ wakeup_tlob,
+ event_max_tlob,
+};
+
+/*
+ * HA clock env: clk_elapsed, elapsed time since the window start; anchored in
+ * running/waiting/sleeping, cleared on stop.
+ */
+enum envs_tlob {
+ clk_elapsed_tlob,
+ env_max_tlob,
+ env_max_stored_tlob = env_max_tlob,
+};
+
+_Static_assert(env_max_stored_tlob <= MAX_HA_ENV_LEN, "Not enough slots");
+#define HA_CLK_NS
+
+struct automaton_tlob {
+ char *state_names[state_max_tlob];
+ char *event_names[event_max_tlob];
+ char *env_names[env_max_tlob];
+ unsigned char function[state_max_tlob][event_max_tlob];
+ unsigned char initial_state;
+ bool final_states[state_max_tlob];
+};
+
+static const struct automaton_tlob automaton_tlob = {
+ .state_names = {
+ "stopped",
+ "running",
+ "sleeping",
+ "waiting",
+ },
+ .event_names = {
+ "preempt",
+ "sleep",
+ "start",
+ "stop",
+ "switch_in",
+ "wakeup",
+ },
+ .env_names = {
+ "clk_elapsed",
+ },
+ .function = {
+ /* stopped: sched events self-loop */
+ {
+ stopped_tlob, /* preempt */
+ stopped_tlob, /* sleep */
+ running_tlob, /* start */
+ INVALID_STATE, /* stop */
+ stopped_tlob, /* switch_in */
+ stopped_tlob, /* wakeup */
+ },
+ /* running */
+ {
+ waiting_tlob, /* preempt (prev_state == 0) */
+ sleeping_tlob, /* sleep (prev_state != 0) */
+ INVALID_STATE, /* start (is -EALREADY) */
+ stopped_tlob, /* stop */
+ INVALID_STATE, /* switch_in */
+ running_tlob, /* wakeup */
+ },
+ /* sleeping */
+ {
+ INVALID_STATE, /* preempt */
+ INVALID_STATE, /* sleep */
+ INVALID_STATE, /* start */
+ INVALID_STATE, /* stop */
+ INVALID_STATE, /* switch_in (via waiting) */
+ waiting_tlob, /* wakeup */
+ },
+ /* waiting */
+ {
+ INVALID_STATE, /* preempt */
+ INVALID_STATE, /* sleep */
+ INVALID_STATE, /* start */
+ INVALID_STATE, /* stop */
+ running_tlob, /* switch_in */
+ waiting_tlob, /* wakeup */
+ },
+ },
+ .initial_state = stopped_tlob,
+ .final_states = { 1, 0, 0, 0 },
+};
+
+/*
+ * Hard cap on concurrently monitored tasks: the framework pre-allocates
+ * this many per-object storage slots (DA_MON_POOL_SIZE). A task keeps its
+ * slot from its first START until it exits, so a fresh start past the cap
+ * returns -ENOSPC without touching the allocator, and the monitor is not
+ * applied to that task. Restarts reuse the task's slot.
+ */
+#define TLOB_MAX_MONITORED 64U
+
+/* Maximum binary path length for uprobe binding. */
+#define TLOB_MAX_PATH 256
+
+/* Maximum binding name length (excluding NUL). */
+#define TLOB_MAX_NAME 32
+
+/* Minimum monitoring budget (1 us). */
+#define TLOB_MIN_THRESHOLD_NS 1000ULL
+
+/* Upper budget bound (1 hour). */
+#define TLOB_MAX_THRESHOLD_NS 3600000000000ULL
+
+#endif /* _RV_TLOB_H */
diff --git a/kernel/trace/rv/monitors/tlob/tlob_trace.h b/kernel/trace/rv/monitors/tlob/tlob_trace.h
new file mode 100644
index 000000000000..cda3f39fc110
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob_trace.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+/*
+ * Snippet to be included in rv_trace.h
+ */
+
+#ifdef CONFIG_RV_MON_TLOB
+DEFINE_EVENT(event_da_monitor_id, event_tlob,
+ TP_PROTO(int id, char *state, char *event,
+ char *next_state, bool final_state),
+ TP_ARGS(id, state, event, next_state, final_state));
+
+DEFINE_EVENT(error_da_monitor_id, error_tlob,
+ TP_PROTO(int id, char *state, char *event),
+ TP_ARGS(id, state, event));
+
+DEFINE_EVENT(error_env_da_monitor_id, error_env_tlob,
+ TP_PROTO(int id, char *state, char *event, char *env),
+ TP_ARGS(id, state, event, env));
+#endif /* CONFIG_RV_MON_TLOB */
diff --git a/kernel/trace/rv/rv_trace.h b/kernel/trace/rv/rv_trace.h
index 2f8a932432c9..4bfa39717cef 100644
--- a/kernel/trace/rv/rv_trace.h
+++ b/kernel/trace/rv/rv_trace.h
@@ -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
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 12/14] rv: Add KUnit tests for the tlob monitor
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (10 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 11/14] rv: Add tlob hybrid automaton monitor wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 13/14] selftests/verification: Add tlob selftests wen.yang
2026-11-05 17:49 ` [PATCH v8 14/14] selftests/ftrace: Walk up parent dirs to find test.d/functions wen.yang
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
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.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Reviewed-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
kernel/trace/rv/monitors/tlob/tlob.c | 11 ++
kernel/trace/rv/monitors/tlob/tlob_kunit.c | 151 +++++++++++++++++++++
kernel/trace/rv/monitors/tlob/tlob_kunit.h | 18 +++
kernel/trace/rv/rv_monitors_test.c | 2 +
4 files changed, 182 insertions(+)
create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c
create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.h
diff --git a/kernel/trace/rv/monitors/tlob/tlob.c b/kernel/trace/rv/monitors/tlob/tlob.c
index fe7d88df62f1..62b0d3330be8 100644
--- a/kernel/trace/rv/monitors/tlob/tlob.c
+++ b/kernel/trace/rv/monitors/tlob/tlob.c
@@ -593,6 +593,17 @@ static int tlob_parse_remove_line(char *buf, char **name_out)
return 0;
}
+#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)
{
loff_t offset_start, offset_stop;
diff --git a/kernel/trace/rv/monitors/tlob/tlob_kunit.c b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
new file mode 100644
index 000000000000..06b212d1103b
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
@@ -0,0 +1,151 @@
+// 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:NAME PATH:START STOP threshold=NS" lines and what they decode to. */
+static const struct {
+ const char *line;
+ const char *name;
+ const char *path;
+ loff_t start;
+ loff_t stop;
+ u64 thr;
+} tlob_parse_valid[] = {
+ { "p:region /usr/bin/myapp:4768 4848 threshold=5000000",
+ "region", "/usr/bin/myapp", 4768, 4848, 5000000 },
+ /* Base 0: hex offsets must not decode as decimal. */
+ { "p:my_region_2 /usr/bin/myapp:0x12a0 0x12f0 threshold=10000000",
+ "my_region_2", "/usr/bin/myapp", 0x12a0, 0x12f0, 10000000 },
+ /* The last ':' separates the offset, so a ':' in PATH is fine. */
+ { "p:_r3 /opt/my:app/bin:0x100 0x200 threshold=1000000",
+ "_r3", "/opt/my:app/bin", 0x100, 0x200, 1000000 },
+ /* TLOB_MAX_NAME - 1 characters: the longest name accepted. */
+ { "p:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa /bin/a:0x100 0x200 threshold=5000",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "/bin/a", 0x100, 0x200, 5000 },
+ /* The accepted threshold bounds themselves. */
+ { "p:lo /bin/a:0x100 0x200 threshold=1000",
+ "lo", "/bin/a", 0x100, 0x200, 1000 },
+ { "p:hi /bin/a:0x100 0x200 threshold=3600000000000",
+ "hi", "/bin/a", 0x100, 0x200, 3600000000000ULL },
+};
+
+/* Malformed "p:..." lines that must be rejected with -EINVAL. */
+static const char * const tlob_parse_invalid[] = {
+ "p /usr/bin/myapp:0x100 0x200 threshold=5000", /* no name */
+ "p:", /* empty name */
+ "p: /usr/bin/myapp:0x100 0x200 threshold=5000", /* name is a path */
+ "p:r /bin/a:0x100junk 0x200 threshold=5000", /* trailing junk */
+ "p:1region /usr/bin/myapp:0x100 0x200 threshold=5000", /* digit lead */
+ "p:my-win /usr/bin/myapp:0x100 0x200 threshold=5000", /* bad char */
+ "p:my.win /usr/bin/myapp:0x100 0x200 threshold=5000", /* bad char */
+ /* TLOB_MAX_NAME characters: one over the limit. */
+ "p:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa /bin/a:0x100 0x200 threshold=5000",
+ "p:region :0x100 0x200 threshold=5000", /* empty path */
+ "p:region /usr/bin/myapp:0x100 threshold=5000", /* missing stop */
+ "p:region /usr/bin/myapp:-1 0x200 threshold=5000", /* negative start */
+ "p:region /usr/bin/myapp:0x100 -1 threshold=5000000", /* negative stop */
+ "p:region /usr/bin/myapp:0x100 0x200", /* missing threshold */
+ "p:region /usr/bin/myapp:0x100 0x100 threshold=5000", /* start == stop */
+ /* Duplicate threshold=, like a duplicate offset, is an error. */
+ "p:region /bin/a:0x100 0x200 threshold=5000 threshold=9000",
+};
+
+/* threshold_ns out of valid range => -ERANGE. */
+static const char * const tlob_parse_out_of_range[] = {
+ "p:region /usr/bin/myapp:0x100 0x200 threshold=0",
+ "p:region /usr/bin/myapp:0x100 0x200 threshold=999",
+ "p:region /usr/bin/myapp:0x100 0x200 threshold=3600000000001",
+};
+
+/* Valid "-:NAME" remove lines. */
+static const char * const tlob_remove_valid[] = {
+ "-:region",
+ "-:my_region",
+};
+
+/* Malformed remove lines that must be rejected with -EINVAL. */
+static const char * const tlob_remove_invalid[] = {
+ "-", /* no name */
+ "-:", /* empty name */
+ "-region", /* missing ':' */
+ "-:my region", /* bad char */
+ "-:1region", /* digit lead */
+ "-:my-region", /* bad char */
+ "-:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", /* TLOB_MAX_NAME, one too many */
+};
+
+static void rv_test_tlob(struct kunit *test)
+{
+ u64 thr;
+ char *name, *path;
+ loff_t start, stop;
+ char buf[128];
+ int i, ret;
+
+ /* NAME and PATH come back NUL-terminated inside buf. */
+ strscpy(buf, "p:region /usr/bin/myapp:0x100 0x200 threshold=5000",
+ sizeof(buf));
+ ret = rv_tlob_kunit_ops.parse_uprobe_line(buf, &name, &path,
+ &start, &stop, &thr);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_STREQ(test, name, "region");
+ KUNIT_EXPECT_STREQ(test, path, "/usr/bin/myapp");
+
+ strscpy(buf, "-:region", sizeof(buf));
+ ret = rv_tlob_kunit_ops.parse_remove_line(buf, &name);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_STREQ(test, name, "region");
+
+ for (i = 0; i < ARRAY_SIZE(tlob_parse_valid); i++) {
+ strscpy(buf, tlob_parse_valid[i].line, sizeof(buf));
+ ret = rv_tlob_kunit_ops.parse_uprobe_line(buf, &name, &path,
+ &start, &stop,
+ &thr);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ if (ret)
+ continue;
+ KUNIT_EXPECT_STREQ(test, name, tlob_parse_valid[i].name);
+ KUNIT_EXPECT_STREQ(test, path, tlob_parse_valid[i].path);
+ KUNIT_EXPECT_EQ(test, start, tlob_parse_valid[i].start);
+ KUNIT_EXPECT_EQ(test, stop, tlob_parse_valid[i].stop);
+ KUNIT_EXPECT_EQ(test, thr, tlob_parse_valid[i].thr);
+ }
+
+ for (i = 0; i < ARRAY_SIZE(tlob_parse_invalid); i++) {
+ strscpy(buf, tlob_parse_invalid[i], sizeof(buf));
+ ret = rv_tlob_kunit_ops.parse_uprobe_line(buf, &name, &path,
+ &start, &stop,
+ &thr);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ }
+
+ for (i = 0; i < ARRAY_SIZE(tlob_parse_out_of_range); i++) {
+ strscpy(buf, tlob_parse_out_of_range[i], sizeof(buf));
+ ret = rv_tlob_kunit_ops.parse_uprobe_line(buf, &name, &path,
+ &start, &stop,
+ &thr);
+ KUNIT_EXPECT_EQ(test, ret, -ERANGE);
+ }
+
+ for (i = 0; i < ARRAY_SIZE(tlob_remove_valid); i++) {
+ strscpy(buf, tlob_remove_valid[i], sizeof(buf));
+ ret = rv_tlob_kunit_ops.parse_remove_line(buf, &name);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ if (!ret)
+ KUNIT_EXPECT_STREQ(test, name,
+ tlob_remove_valid[i] + 2);
+ }
+
+ for (i = 0; i < ARRAY_SIZE(tlob_remove_invalid); i++) {
+ strscpy(buf, tlob_remove_invalid[i], sizeof(buf));
+ ret = rv_tlob_kunit_ops.parse_remove_line(buf, &name);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ }
+}
+
+#else
+#define rv_test_tlob rv_test_stub
+#endif
diff --git a/kernel/trace/rv/monitors/tlob/tlob_kunit.h b/kernel/trace/rv/monitors/tlob/tlob_kunit.h
new file mode 100644
index 000000000000..c0c80c34f17b
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob_kunit.h
@@ -0,0 +1,18 @@
+/* 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, char **name_out, char **path_out,
+ loff_t *start_out, loff_t *stop_out,
+ u64 *thr_out);
+ int (*parse_remove_line)(char *buf, char **name_out);
+} rv_tlob_kunit_ops;
+
+#endif
+
+#endif /* __TLOB_KUNIT_H */
diff --git a/kernel/trace/rv/rv_monitors_test.c b/kernel/trace/rv/rv_monitors_test.c
index 3ad11195e664..548607e7c1a7 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -151,6 +151,7 @@ static void rv_test_dummy(struct kunit *test)
#include "monitors/sts/sts_kunit.c"
#include "monitors/opid/opid_kunit.c"
#include "monitors/nomiss/nomiss_kunit.c"
+#include "monitors/tlob/tlob_kunit.c"
#include "monitors/pagefault/pagefault_kunit.c"
#include "monitors/sleep/sleep_kunit.c"
@@ -161,6 +162,7 @@ static struct kunit_case rv_mon_test_cases[] = {
KUNIT_CASE(rv_test_sts),
KUNIT_CASE(rv_test_opid),
KUNIT_CASE(rv_test_nomiss),
+ KUNIT_CASE(rv_test_tlob),
KUNIT_CASE(rv_test_pagefault),
KUNIT_CASE(rv_test_sleep),
{}
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 13/14] selftests/verification: Add tlob selftests
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (11 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 12/14] rv: Add KUnit tests for the tlob monitor wen.yang
@ 2026-11-05 17:49 ` wen.yang
2026-11-05 17:49 ` [PATCH v8 14/14] selftests/ftrace: Walk up parent dirs to find test.d/functions wen.yang
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
Add thirteen 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, multi-binding,
multi-task sharing of one binding, overlap registration rejection,
unbind and disable on live tasks, start/stop restart (both normal and
after a violation), task exit inside a live window, nested START
reporting, and the 64-task pool exhaustion cap.
Helper binary tlob_target is built via TEST_GEN_FILES, following
the standard kselftests convention. run_tlob_tests.sh verifies helper
availability and delegates argument handling to ftracetest.
The multi-instance test, covering several tasks bound to the same
uprobe pair, was contributed by Gabriele Monaco.
Reviewed-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
.../testing/selftests/verification/.gitignore | 1 +
tools/testing/selftests/verification/Makefile | 4 +
tools/testing/selftests/verification/config | 3 +
.../selftests/verification/test.d/functions | 9 +
.../test.d/tlob/run_tlob_tests.sh | 32 ++++
.../verification/test.d/tlob/uprobe_bind.tc | 47 ++++++
.../test.d/tlob/uprobe_disable_live.tc | 79 +++++++++
.../verification/test.d/tlob/uprobe_multi.tc | 58 +++++++
.../test.d/tlob/uprobe_multi_instance.tc | 65 ++++++++
.../test.d/tlob/uprobe_nested_start.tc | 62 +++++++
.../test.d/tlob/uprobe_no_event.tc | 17 ++
.../test.d/tlob/uprobe_overlap_register.tc | 66 ++++++++
.../test.d/tlob/uprobe_pool_exhausted.tc | 75 +++++++++
.../test.d/tlob/uprobe_restart.tc | 66 ++++++++
.../test.d/tlob/uprobe_restart_violation.tc | 48 ++++++
.../test.d/tlob/uprobe_task_exit.tc | 65 ++++++++
.../test.d/tlob/uprobe_unbind_live.tc | 65 ++++++++
.../test.d/tlob/uprobe_violation.tc | 53 ++++++
.../selftests/verification/tlob_target.c | 156 ++++++++++++++++++
19 files changed, 971 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_disable_live.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_multi_instance.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_nested_start.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_overlap_register.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_pool_exhausted.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_restart_violation.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_task_exit.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_unbind_live.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
create mode 100644 tools/testing/selftests/verification/tlob_target.c
diff --git a/tools/testing/selftests/verification/.gitignore b/tools/testing/selftests/verification/.gitignore
index 2659417cb2c7..92d7ebdb1dd1 100644
--- a/tools/testing/selftests/verification/.gitignore
+++ b/tools/testing/selftests/verification/.gitignore
@@ -1,2 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
logs
+tlob_target
diff --git a/tools/testing/selftests/verification/Makefile b/tools/testing/selftests/verification/Makefile
index aa8790c22a71..dcdfe3ea3c06 100644
--- a/tools/testing/selftests/verification/Makefile
+++ b/tools/testing/selftests/verification/Makefile
@@ -5,4 +5,8 @@ TEST_PROGS := verificationtest-ktap
TEST_FILES := test.d settings
EXTRA_CLEAN := $(OUTPUT)/logs/*
+TEST_GEN_FILES := tlob_target
+
include ../lib.mk
+
+export VERIFICATIONTEST_BINDIR := $(OUTPUT)
diff --git a/tools/testing/selftests/verification/config b/tools/testing/selftests/verification/config
index 43072c1c38f4..5869ae4a9217 100644
--- a/tools/testing/selftests/verification/config
+++ b/tools/testing/selftests/verification/config
@@ -1 +1,4 @@
CONFIG_RV=y
+CONFIG_RV_MON_TLOB=y
+CONFIG_UPROBES=y
+CONFIG_HIGH_RES_TIMERS=y
diff --git a/tools/testing/selftests/verification/test.d/functions b/tools/testing/selftests/verification/test.d/functions
index ec36a092f56e..43c119682c1f 100644
--- a/tools/testing/selftests/verification/test.d/functions
+++ b/tools/testing/selftests/verification/test.d/functions
@@ -25,6 +25,15 @@ check_requires() { # Check required files, monitors and reactors
done
}
+sym_offset() { # <binary> <symbol>: print the ELF file offset of <symbol>
+ readelf -W -S -s "$1" | awk -v symbol="$2" '
+ { gsub(/\[ /, "[") }
+ $1 ~ /^\[[0-9]+\]$/ { sections[$1]="0x"$4; offsets[$1]="0x"$5 }
+ $1 ~ /^[0-9]+:$/ && $NF == symbol { addr="0x"$2; sec="["$7"]" }
+ END { printf "printf \"0x%%x\\n\" $((%s - %s + %s))\n", addr, sections[sec], offsets[sec] }
+ ' | sh
+}
+
initialize_system() { # Reset RV to initial-state
echo > enabled_monitors
for m in monitors/*; do
diff --git a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
new file mode 100755
index 000000000000..c87484813fc8
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
@@ -0,0 +1,32 @@
+#!/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"
+
+# The helper is built on the host by the selftests Makefile
+# (TEST_GEN_FILES in $BINDIR/Makefile); the VM (virtme-ng) mounts the
+# source read-only and may lack a compiler, so this runner never builds.
+# Build it on the host first:
+# make -C "$BINDIR" tlob_target
+if [ ! -x "$BINDIR/tlob_target" ]; then
+ echo "ERROR: tlob_target not built in $BINDIR" >&2
+ echo " build it on the host: make -C $BINDIR tlob_target" >&2
+ exit 1
+fi
+
+# Flag (not fail) the helper older than its source: git checkouts can bump
+# source mtimes, so a hard check would misfire; a host rebuild picks up
+# the new code.
+if [ -f "$BINDIR/tlob_target.c" ] &&
+ [ "$BINDIR/tlob_target" -ot "$BINDIR/tlob_target.c" ]; then
+ echo "WARNING: $BINDIR/tlob_target is older than $BINDIR/tlob_target.c;" >&2
+ echo " rebuild on the host to test the current source" >&2
+fi
+
+export VERIFICATIONTEST_BINDIR="$BINDIR"
+
+exec "$FTRACETEST" -K --rv "$SCRIPT_DIR" "$@"
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
new file mode 100644
index 000000000000..6484f37d8d2e
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
@@ -0,0 +1,47 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor uprobe binding (named, listed, removable, duplicate name/range rejected, O_TRUNC clears)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+
+"$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:busy_work ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=5000000000" >> "$TLOB_MONITOR"
+
+# Binding must appear in monitor file with canonical hex-offset format.
+grep -qE "^p:busy_work ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=[0-9]+$" "$TLOB_MONITOR"
+grep -q "threshold=5000000000" "$TLOB_MONITOR"
+
+# Duplicate range must be rejected.
+! echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=9999000" >> "$TLOB_MONITOR" 2>/dev/null || false
+
+# Duplicate name on a disjoint range must be rejected too.
+! echo "p:busy_work ${UPROBE_TARGET}:$((stop_offset + 64)) $((stop_offset + 128)) threshold=5000000" >> "$TLOB_MONITOR" 2>/dev/null || false
+
+# Remove the binding; it must no longer appear.
+echo "-:busy_work" >> "$TLOB_MONITOR"
+! grep -q "^p:busy_work " "$TLOB_MONITOR" || false
+
+# '>' opens with O_TRUNC and clears all bindings (ftrace dynamic-event
+# convention); individual commands must use '>>'.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=5000000000" >> "$TLOB_MONITOR"
+echo > "$TLOB_MONITOR"
+! grep -q "^p:" "$TLOB_MONITOR" || false
+
+echo 0 > monitors/tlob/enable
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_disable_live.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_disable_live.tc
new file mode 100644
index 000000000000..c997405d75cc
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_disable_live.tc
@@ -0,0 +1,79 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor disable with a live window (armed hrtimer cancelled, no post-cycle violation)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+sleep_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work_done)
+
+echo 1 > ../events/rv/error_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+# 300 ms budget keeps the live window far from expiry at disable; duration 0
+# loops the target so no STOP can fire while the monitor is down.
+echo "p:sleep_work ${UPROBE_TARGET}:${sleep_offset} ${stop_offset} threshold=300000000" >> "$TLOB_MONITOR"
+
+"$UPROBE_TARGET" 0 sleep &
+target_pid=$!
+teardown() {
+ kill "$target_pid" 2>/dev/null || true; wait "$target_pid" 2>/dev/null || true
+}
+trap teardown EXIT
+sleep 0.05
+
+# Disable with the window open: the armed hrtimer must be cancelled by the
+# disable path, or it fires on freed storage. Check /proc/timer_list.
+echo 0 > monitors/tlob/enable
+
+if [ ! -r /proc/timer_list ]; then
+ echo "/proc/timer_list unavailable, cannot check for leaked timers" >&2
+ exit_unresolved
+fi
+
+armed=$(grep -c ha_monitor /proc/timer_list || true)
+if [ "$armed" != "0" ]; then
+ echo "===== tlob disable-with-live-window diagnostic =====" >&2
+ echo "$armed armed ha_monitor hrtimer(s) after disable:" >&2
+ grep ha_monitor /proc/timer_list >&2 || true
+ false
+fi
+
+# A leaked timer would fire on freed storage once re-enabled.
+echo 1 > monitors/tlob/enable
+
+# Re-enabled before the 300 ms budget would have elapsed; wait past it.
+sleep 0.5
+
+if grep -q "error_env_tlob" ../trace; then
+ echo "===== tlob disable-with-live-window diagnostic =====" >&2
+ grep "error_env_tlob" ../trace >&2 || true
+ false
+fi
+
+# Disable/enable must not break detection: a 10 us budget fires on the
+# next sleep window.
+echo > ../trace
+echo "p:sleep_work ${UPROBE_TARGET}:${sleep_offset} ${stop_offset} threshold=10000" >> "$TLOB_MONITOR"
+
+found=0; i=0
+while [ "$i" -lt 20 ]; do
+ sleep 0.1
+ grep -q "error_env_tlob" ../trace && { found=1; break; }
+ i=$((i+1))
+done
+
+echo "-:sleep_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > ../events/rv/error_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
new file mode 100644
index 000000000000..709561ebc94c
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
@@ -0,0 +1,58 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor multiple uprobe bindings (different offsets fire independently)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+busy_stop=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+sleep_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work)
+sleep_stop=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work_done)
+
+"$UPROBE_TARGET" 30000 & # busy mode: tlob_busy_work fires every 200 ms
+busy_pid=$!
+"$UPROBE_TARGET" 30000 sleep & # sleep mode: tlob_sleep_work fires every 200 ms
+sleep_pid=$!
+teardown() {
+ kill "$sleep_pid" 2>/dev/null || true; wait "$sleep_pid" 2>/dev/null || true
+ kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+}
+trap teardown EXIT
+sleep 0.05
+
+echo 1 > ../events/rv/error_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+# Binding A: 5 s budget on the busy probe - must not fire in 200 ms loops.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=5000000000" >> "$TLOB_MONITOR"
+# Binding B: 10 us budget on the sleep probe - fires on first invocation.
+echo "p:sleep_work ${UPROBE_TARGET}:${sleep_offset} ${sleep_stop} threshold=10000" >> "$TLOB_MONITOR"
+
+# Wait up to 2 s for error_env_tlob from binding B.
+found=0; i=0
+while [ "$i" -lt 20 ]; do
+ sleep 0.1
+ grep -q "error_env_tlob" ../trace && { found=1; break; }
+ i=$((i+1))
+done
+
+echo "-:busy_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo "-:sleep_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/error_env_tlob/enable
+
+[ "$found" = "1" ]
+# error_env_tlob payload: clock variable must be present.
+# The event field can be "budget_exceeded" (hrtimer path) or the DA event
+# name ("sleep", "preempt") depending on which fires first; don't constrain it.
+grep "error_env_tlob" ../trace | head -n 1 | grep -q "clk_elapsed="
+
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi_instance.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi_instance.tc
new file mode 100644
index 000000000000..238ceed065b0
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi_instance.tc
@@ -0,0 +1,65 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor multiple tasks sharing one uprobe binding (concurrent instances)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+UPROBE_COMM=$(basename "$UPROBE_TARGET")
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+busy_stop=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+
+# Start 3 concurrent instances of the target; each one hits the same binding.
+"$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: no violation expected in 200 ms loops.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=5000000000" >> "$TLOB_MONITOR"
+
+# Wait up to 2 s for all three pids to register 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 binding while tasks are active must succeed cleanly.
+echo "-:busy_work" >> "$TLOB_MONITOR"
+! grep -q "^p:busy_work " "$TLOB_MONITOR" || false
+
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/event_tlob/enable
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_nested_start.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_nested_start.tc
new file mode 100644
index 000000000000..fe30db3f0134
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_nested_start.tc
@@ -0,0 +1,62 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob reporting a nested START (a window is already open)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+start_offset=$(sym_offset "$UPROBE_TARGET" tlob_nested_start)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_nested_done)
+
+echo 1 > ../events/rv/error_tlob/enable
+echo 1 > ../events/rv/event_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+
+# Generous budget: anything reported here is the nested START, not the clock.
+echo "p:nested ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=5000000000" >> "$TLOB_MONITOR"
+echo > ../trace
+
+"$UPROBE_TARGET" 0 nest 1 &
+tpid=$!
+teardown() {
+ kill "$tpid" 2>/dev/null || true; wait "$tpid" 2>/dev/null || true
+}
+trap teardown EXIT
+wait "$tpid" || true
+trap - EXIT
+
+# The second entry is a START while the window is open: running x start is an
+# invalid transition, reported through error_tlob rather than silently ignored.
+if ! grep -q "error_tlob: ${tpid}: event start not expected in the state running" ../trace; then
+ echo "ERROR: nested START was not reported" >&2
+ grep -E "event_tlob|error_tlob" ../trace | grep ":${tpid}:" >&2 || true
+ false
+fi
+
+# One START opened the window; the nested one is refused and does not open a
+# second. The violation resets the monitor, so the STOP that follows is a
+# no-op: no further transition may be recorded for this pid.
+starts=$(grep -c "event_tlob: ${tpid}: stopped x start -> running" ../trace)
+if [ "$starts" -ne 1 ]; then
+ echo "ERROR: expected exactly 1 window for ${tpid}, saw ${starts}" >&2
+ grep "event_tlob" ../trace | grep ":${tpid}:" >&2 || true
+ false
+fi
+
+if grep -q "event_tlob: ${tpid}: running x stop -> stopped" ../trace; then
+ echo "ERROR: STOP after a violation reopened/closed a window" >&2
+ grep "event_tlob" ../trace | grep ":${tpid}:" >&2 || true
+ false
+fi
+
+echo "-:nested" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/error_tlob/enable
+echo 0 > ../events/rv/event_tlob/enable
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
new file mode 100644
index 000000000000..23f8abeca090
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
@@ -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
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_overlap_register.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_overlap_register.tc
new file mode 100644
index 000000000000..eb3590fa21aa
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_overlap_register.tc
@@ -0,0 +1,66 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob rejecting overlapping uprobe bindings and reporting shadowing
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+busy_stop=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+sleep_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work)
+sleep_stop=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work_done)
+
+echo 1 > ../events/rv/error_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+# Scenario 1: overlapping ranges on the same binary are rejected at
+# registration (a task runs one window at a time; the second would shadow).
+# Covers three flavours: identical, nested, and crossing.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=1000000000" >> "$TLOB_MONITOR"
+for r in \
+ "${busy_offset} ${busy_stop}" \
+ "$((busy_offset + 8)) ${busy_stop}" \
+ "$((busy_stop - 8)) $((busy_stop + 8))"; do
+ set -- $r
+ if echo "p:dup ${UPROBE_TARGET}:$1 $2 threshold=1000000000" >> "$TLOB_MONITOR" 2>/dev/null; then
+ echo "ERROR: overlapping binding accepted: $r" >&2
+ false
+ fi
+done
+# Only the original binding survives.
+if [ "$(grep -c "^p:busy_work " "$TLOB_MONITOR")" -ne 1 ]; then
+ echo "ERROR: rejected range left a binding behind" >&2
+ false
+fi
+
+# Scenario 2: two disjoint ranges on the same binary are both accepted, but a
+# task running them sequentially is shadowed by the first binding -- reported
+# via error_tlob instead of a silent no-op.
+echo "p:sleep_work ${UPROBE_TARGET}:${sleep_offset} ${sleep_stop} threshold=1000000000" >> "$TLOB_MONITOR"
+"$UPROBE_TARGET" 0 seq 2 &
+tpid=$!
+teardown() {
+ kill "$tpid" 2>/dev/null || true; wait "$tpid" 2>/dev/null || true
+}
+trap teardown EXIT
+wait "$tpid" || true
+
+if ! grep -q "error_tlob" ../trace; then
+ echo "ERROR: no error_tlob for sequentially shadowed binding" >&2
+ echo "--- trace ---" >&2
+ grep "error_tlob\|event_tlob" ../trace >&2 || true
+ false
+fi
+
+echo "-:busy_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo "-:sleep_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/error_tlob/enable
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_pool_exhausted.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_pool_exhausted.tc
new file mode 100644
index 000000000000..2b876566936a
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_pool_exhausted.tc
@@ -0,0 +1,75 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob per-object storage pool cap (64 tasks, the 65th is not monitored)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+# TLOB_MAX_MONITORED in kernel/trace/rv/monitors/tlob/tlob.h.
+POOL_SIZE=64
+
+sleep_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work_done)
+
+echo 1 > ../events/rv/event_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+
+# 5 s budget: no window may report here, only the pool accounting matters.
+echo "p:sleep_work ${UPROBE_TARGET}:${sleep_offset} ${stop_offset} threshold=5000000000" >> "$TLOB_MONITOR"
+echo > ../trace
+
+pids=
+for i in $(seq 1 $POOL_SIZE); do
+ "$UPROBE_TARGET" 5000 sleep 0 &
+ pids="$pids $!"
+done
+teardown() {
+ for p in $pids; do kill "$p" 2>/dev/null || true; done
+ wait 2>/dev/null || true
+}
+trap teardown EXIT
+
+# Wait until every slot is taken: each of the 64 tasks opened a window once.
+n=0
+i=0
+while [ "$i" -lt 200 ]; do
+ n=$(sed -n 's/.*event_tlob: \([0-9][0-9]*\): stopped x start -> running.*/\1/p' ../trace | sort -u | wc -l)
+ [ "$n" -ge "$POOL_SIZE" ] && break
+ sleep 0.05
+ i=$((i + 1))
+done
+
+if [ "$n" -ne "$POOL_SIZE" ]; then
+ echo "ERROR: expected $POOL_SIZE monitored tasks, got $n" >&2
+ false
+fi
+
+# One more task must be left unmonitored: no START is possible without a slot.
+"$UPROBE_TARGET" 3000 sleep 0 &
+extra=$!
+pids="$pids $extra"
+sleep 0.5
+
+if grep -q "event_tlob: ${extra}: stopped x start -> running" ../trace; then
+ echo "ERROR: task ${extra} was monitored past the ${POOL_SIZE}-task cap" >&2
+ grep "event_tlob" ../trace | grep ":${extra}:" >&2 || true
+ false
+fi
+
+# A monitored task still holds its slot while the extra one is refused.
+n=$(sed -n 's/.*event_tlob: \([0-9][0-9]*\): stopped x start -> running.*/\1/p' ../trace | sort -u | wc -l)
+[ "$n" -eq "$POOL_SIZE" ] || {
+ echo "ERROR: pool accounting changed while refusing: $n" >&2
+ false
+}
+
+echo "-:sleep_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/event_tlob/enable
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_restart.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_restart.tc
new file mode 100644
index 000000000000..1584fc3d84de
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_restart.tc
@@ -0,0 +1,66 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor restarting a parked window on the same task (unbind of a repeatedly-started task works)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+# On failure: dump violations and the target's transitions, to tell a genuine
+# per-window overrun from a broken restart.
+dump_tlob_diag() {
+ TRACE=../trace
+ echo "===== tlob restart diagnostic (pid ${busy_pid}) =====" >&2
+ echo "--- error_env_tlob (violations) ---" >&2
+ grep "error_env_tlob" "$TRACE" >&2 || true
+ echo "--- event_tlob (state transitions for target pid) ---" >&2
+ grep "event_tlob" "$TRACE" | grep ":${busy_pid}:" >&2 || true
+ echo "--- last 50 trace lines (full context) ---" >&2
+ tail -50 "$TRACE" >&2 || true
+}
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+
+# tlob_target busy-loops ~200ms start/stop windows on the same task for
+# 900ms, driving tlob_start_task()'s restart path on every iteration
+# after the first (~4 windows).
+"$UPROBE_TARGET" 900 &
+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 > ../events/rv/event_tlob/enable
+echo 1 > ../events/rv/error_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+# 300ms budget covers one ~200ms window. A restart that failed to re-anchor
+# the budget clock would trip on the second window; with a correct restart,
+# no window exceeds it.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=300000000" >> "$TLOB_MONITOR"
+
+wait "$busy_pid" || true
+
+if grep -q "error_env_tlob" ../trace; then
+ dump_tlob_diag
+ false
+fi
+
+# The task has exited (tlob_destroy_task() ran via handle_sched_process_exit);
+# removing its now-stale binding must still succeed cleanly.
+echo "-:busy_work" >> "$TLOB_MONITOR"
+! grep -q "^p:busy_work " "$TLOB_MONITOR" || false
+
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/event_tlob/enable
+echo 0 > ../events/rv/error_env_tlob/enable
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_restart_violation.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_restart_violation.tc
new file mode 100644
index 000000000000..dccb4e5baa11
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_restart_violation.tc
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor restarting a window after a budget overrun (each later START on the same task starts a fresh window)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+
+echo 1 > ../events/rv/error_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+
+# 30 ms budget: every ~200 ms window overruns. Bind first, clear the trace
+# after, so the target's first window (START soon after launch) is caught.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=30000000" >> "$TLOB_MONITOR"
+echo > ../trace
+
+# 3 iterations (duration arg unused). Every window overruns; a dropped
+# restart would leave one unmonitored, dropping the count below 3.
+"$UPROBE_TARGET" 0 busy 3 &
+busy_pid=$!
+teardown() {
+ kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+}
+trap teardown EXIT
+wait "$busy_pid" || true
+
+errors=$(grep -c "error_env_tlob" ../trace || true)
+if [ "$errors" -lt 3 ]; then
+ echo "ERROR: expected >= 3 error_env_tlob (one per window), got $errors" >&2
+ echo "--- error_env_tlob ---" >&2
+ grep "error_env_tlob" ../trace >&2 || true
+ echo "--- event_tlob (state transitions for target pid) ---" >&2
+ grep "event_tlob" ../trace | grep ":${busy_pid}:" >&2 || true
+ false
+fi
+
+echo "-:busy_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/error_env_tlob/enable
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_task_exit.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_task_exit.tc
new file mode 100644
index 000000000000..b9122bc002e3
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_task_exit.tc
@@ -0,0 +1,65 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob task exiting inside a window (timer disarmed, no late violation)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+
+echo 1 > ../events/rv/error_env_tlob/enable
+echo 1 > ../events/rv/event_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+
+# 1 s budget: the task is killed well inside its 200 ms busy window, so the
+# exit path (sched_process_exit) is what has to disarm the timer.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=1000000000" >> "$TLOB_MONITOR"
+echo > ../trace
+
+"$UPROBE_TARGET" 0 busy 0 &
+tpid=$!
+teardown() {
+ kill -9 "$tpid" 2>/dev/null || true; wait "$tpid" 2>/dev/null || true
+}
+trap teardown EXIT
+
+# Wait until the target actually opened a window, so the kill below lands
+# inside it rather than before the first START.
+i=0
+while [ "$i" -lt 100 ]; do
+ grep -q "event_tlob: ${tpid}: stopped x start -> running" ../trace && break
+ sleep 0.01
+ i=$((i + 1))
+done
+if ! grep -q "event_tlob: ${tpid}: stopped x start -> running" ../trace; then
+ echo "ERROR: target never opened a window" >&2
+ grep -E "event_tlob|error_tlob" ../trace >&2 || true
+ false
+fi
+
+kill -9 "$tpid"
+wait "$tpid" 2>/dev/null || true
+trap - EXIT
+
+# Past the budget: a timer the exit path left armed fires here and reports
+# against a task that no longer exists.
+sleep 1.3
+
+if grep -q "error_env_tlob: ${tpid}:" ../trace; then
+ echo "ERROR: violation reported for a task that exited inside its window" >&2
+ grep "error_env_tlob" ../trace >&2
+ false
+fi
+
+echo "-:busy_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > monitors/tlob/enable
+echo 0 > ../events/rv/error_env_tlob/enable
+echo 0 > ../events/rv/event_tlob/enable
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_unbind_live.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_unbind_live.tc
new file mode 100644
index 000000000000..698bf907f653
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_unbind_live.tc
@@ -0,0 +1,65 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob unbind with a live window (no post-unbind violation, detection still works)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+sleep_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_sleep_work_done)
+
+echo 1 > ../events/rv/error_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+# 300 ms budget keeps the live window far from expiry at unbind; duration 0
+# loops the target.
+echo "p:sleep_work ${UPROBE_TARGET}:${sleep_offset} ${stop_offset} threshold=300000000" >> "$TLOB_MONITOR"
+
+"$UPROBE_TARGET" 0 sleep &
+target_pid=$!
+teardown() {
+ kill "$target_pid" 2>/dev/null || true; wait "$target_pid" 2>/dev/null || true
+}
+trap teardown EXIT
+sleep 0.05
+
+# Unbind while the window is open: the task can no longer reach STOP, so the
+# window must be ended and its timer cancelled before the budget expires.
+echo "-:sleep_work" >> "$TLOB_MONITOR"
+
+# Wait past the 300 ms budget.
+sleep 0.6
+
+if grep -q "error_env_tlob" ../trace; then
+ echo "===== tlob unbind-with-live-window diagnostic =====" >&2
+ grep "error_env_tlob" ../trace >&2 || true
+ false
+fi
+
+# The target survived the unbind (its window was torn down, not the task).
+kill -0 "$target_pid"
+
+# Unbind must not break detection: a 10 us budget fires on the next sleep window.
+echo > ../trace
+echo "p:sleep_work ${UPROBE_TARGET}:${sleep_offset} ${stop_offset} threshold=10000" >> "$TLOB_MONITOR"
+
+found=0; i=0
+while [ "$i" -lt 20 ]; do
+ sleep 0.1
+ grep -q "error_env_tlob" ../trace && { found=1; break; }
+ i=$((i+1))
+done
+
+echo "-:sleep_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > ../events/rv/error_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+echo > ../trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
new file mode 100644
index 000000000000..eb4e5c9bd2e3
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
@@ -0,0 +1,53 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor budget violation (error_env_tlob fires with correct fields)
+# requires: tlob:monitor readelf:program
+
+# 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_MONITOR=monitors/tlob/monitor
+
+busy_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work)
+stop_offset=$(sym_offset "$UPROBE_TARGET" tlob_busy_work_done)
+
+"$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 > ../events/rv/error_env_tlob/enable
+echo 1 > ../tracing_on
+echo 1 > monitors/tlob/enable
+echo > ../trace
+
+# 10 us budget - fires almost immediately; task is busy-spinning on-CPU.
+echo "p:busy_work ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=10000" >> "$TLOB_MONITOR"
+
+# wait up to 2 s for error_env_tlob
+found=0; i=0
+while [ "$i" -lt 20 ]; do
+ sleep 0.1
+ grep -q "error_env_tlob" ../trace && { found=1; break; }
+ i=$((i+1))
+done
+
+echo "-:busy_work" >> "$TLOB_MONITOR" 2>/dev/null
+echo 0 > ../events/rv/error_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+# error_env_tlob must carry the clk_elapsed environment field.
+# The event label is "budget_exceeded" when detected by the hrtimer callback,
+# or the triggering sched event name when detected by the constraint path on a
+# preemption that races with the timer (common on PREEMPT_RT / VM). Both are
+# valid detections; check the env field instead of the label.
+grep "error_env_tlob" ../trace | head -n 1 | grep -q "clk_elapsed="
+
+echo > ../trace
diff --git a/tools/testing/selftests/verification/tlob_target.c b/tools/testing/selftests/verification/tlob_target.c
new file mode 100644
index 000000000000..706ff5e1eeca
--- /dev/null
+++ b/tools/testing/selftests/verification/tlob_target.c
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob_target.c - uprobe target binary for tlob selftests.
+ *
+ * Provides two start/stop probe pairs:
+ *
+ * tlob_busy_work / tlob_busy_work_done - busy-spin
+ * tlob_sleep_work / tlob_sleep_work_done - nanosleep
+ *
+ * Usage: tlob_target <duration_ms> [mode] [iterations]
+ *
+ * mode is one of: busy (default), sleep, seq, nest.
+ * Loops in 200 ms iterations until <duration_ms> has elapsed
+ * (0 = run until killed, capped at TLOB_MAX_DURATION_MS), or exactly
+ * [iterations] times when > 0.
+ * seq runs busy then sleep in each iteration (two ranges back to back).
+ * nest opens a window and re-enters its own start probe once.
+ */
+#define _GNU_SOURCE
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#ifndef noinline
+#define noinline __attribute__((noinline))
+#endif
+
+/*
+ * Ceiling for duration_ms == 0. The live tests background the target and
+ * kill it from their EXIT trap; the cap bounds an orphan left behind when
+ * the trap does not run.
+ */
+#define TLOB_MAX_DURATION_MS 60000UL
+
+static inline int timespec_before(const struct timespec *a,
+ const struct timespec *b)
+{
+ return a->tv_sec < b->tv_sec ||
+ (a->tv_sec == b->tv_sec && a->tv_nsec < b->tv_nsec);
+}
+
+static void timespec_add_ms(struct timespec *ts, unsigned long ms)
+{
+ ts->tv_sec += ms / 1000;
+ ts->tv_nsec += (long)(ms % 1000) * 1000000L;
+ if (ts->tv_nsec >= 1000000000L) {
+ ts->tv_sec++;
+ ts->tv_nsec -= 1000000000L;
+ }
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_busy_work_done(void)
+{
+ asm volatile("" ::: "memory");
+}
+
+/* Busy-wait for at least duration_ms ms of wall-clock time. */
+static inline void __tlob_busy_wait(unsigned long duration_ms)
+{
+ 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);
+}
+
+/* start probe; busy-spin so the window is spent running */
+noinline void tlob_busy_work(unsigned long duration_ms)
+{
+ __tlob_busy_wait(duration_ms);
+ 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 the window is spent sleeping */
+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();
+}
+
+/*
+ * Nested window: the probe is placed on tlob_nested_start, and the function
+ * re-enters itself once, so the second entry is a START while the window is
+ * already open.
+ */
+static int tlob_nested_depth;
+
+noinline void tlob_nested_start(void)
+{
+ if (tlob_nested_depth++ < 1)
+ tlob_nested_start();
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_nested_done(void)
+{
+ asm volatile("" ::: "memory");
+}
+
+static void tlob_nested_work(void)
+{
+ tlob_nested_depth = 0;
+ tlob_nested_start();
+ tlob_nested_done();
+}
+
+int main(int argc, char *argv[])
+{
+ unsigned long duration_ms = 0, iterations = 0, i = 0;
+ const char *mode = "busy";
+ struct timespec deadline, now;
+
+ if (argc >= 2)
+ duration_ms = strtoul(argv[1], NULL, 10);
+ if (argc >= 3)
+ mode = argv[2];
+ if (argc >= 4)
+ iterations = strtoul(argv[3], NULL, 10);
+
+ clock_gettime(CLOCK_MONOTONIC, &deadline);
+ timespec_add_ms(&deadline, duration_ms ? duration_ms : TLOB_MAX_DURATION_MS);
+
+ do {
+ if (strcmp(mode, "sleep") == 0)
+ tlob_sleep_work(200);
+ else if (strcmp(mode, "seq") == 0) {
+ tlob_busy_work(200);
+ tlob_sleep_work(200);
+ } else if (strcmp(mode, "nest") == 0)
+ tlob_nested_work();
+ else
+ tlob_busy_work(200);
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ i++;
+ } while (iterations ? i < iterations : timespec_before(&now, &deadline));
+
+ return 0;
+}
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v8 14/14] selftests/ftrace: Walk up parent dirs to find test.d/functions
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
` (12 preceding siblings ...)
2026-11-05 17:49 ` [PATCH v8 13/14] selftests/verification: Add tlob selftests wen.yang
@ 2026-11-05 17:49 ` wen.yang
13 siblings, 0 replies; 15+ messages in thread
From: wen.yang @ 2026-11-05 17:49 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
When a subdirectory is passed (e.g. an RV monitor's test directory),
ftracetest fell back to its own functions file, losing the RV-specific
check_requires handling for ':monitor' and ':reactor' requirements.
Walk up from OPT_TEST_DIR until a directory containing test.d/functions
is found, stopping after five levels so a stray argument cannot reach an
unrelated functions file further up the tree. When no such directory is
found the previous default is kept.
Note that TOP_DIR also feeds LOG_TOP_DIR, so logs now follow the
directory the tests were found in: an RV run writes to
verification/logs rather than ftrace/logs.
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
tools/testing/selftests/ftrace/ftracetest | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/ftrace/ftracetest b/tools/testing/selftests/ftrace/ftracetest
index 0a56bf209f6c..3d3c3a4dfa34 100755
--- a/tools/testing/selftests/ftrace/ftracetest
+++ b/tools/testing/selftests/ftrace/ftracetest
@@ -159,9 +159,22 @@ 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
+ # A subdirectory of test.d can be given; walk up to the directory
+ # holding test.d/functions. The bound keeps a stray directory from
+ # reaching an unrelated functions file further up the tree.
+ local dir=$OPT_TEST_DIR
+ local depth=0
+ while [ "$depth" -lt 5 ]; do
+ if [ -f "$dir/test.d/functions" ]; then
+ TOP_DIR=$dir
+ TEST_DIR=$TOP_DIR/test.d
+ break
+ fi
+ case "$dir" in /|//|.) break;; esac
+ dir=$(dirname "$dir")
+ depth=$((depth + 1))
+ done
fi
}
--
2.25.1
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-11-05 17:51 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-05 17:49 [PATCH v8 00/14] rv: Add task latency over budget RV monitor wen.yang
2026-11-05 17:49 ` [PATCH v8 01/14] rv: Force environment reset action on HA monitor start wen.yang
2026-11-05 17:49 ` [PATCH v8 02/14] rv: Fix ha_monitor timer and invariant underflow wen.yang
2026-11-05 17:49 ` [PATCH v8 03/14] rv: Add a KUnit test for the HA timer underflow wen.yang
2026-11-05 17:49 ` [PATCH v8 04/14] rv: Disarm HA timers on the storage free path wen.yang
2026-11-05 17:49 ` [PATCH v8 05/14] rv: Add a KUnit test for " wen.yang
2026-11-05 17:49 ` [PATCH v8 06/14] rv: Make da_monitor_reset_hook, sync_hook and EVENT_NONE_LBL overridable wen.yang
2026-11-05 17:49 ` [PATCH v8 07/14] rv: Introduce DA_MON_ALLOCATION_STRATEGY wen.yang
2026-11-05 17:49 ` [PATCH v8 08/14] rv: Support embedded target types in per-object DA storage wen.yang
2026-11-05 17:49 ` [PATCH v8 09/14] rv: Add generic uprobe infrastructure for RV monitors wen.yang
2026-11-05 17:49 ` [PATCH v8 10/14] rv: Add tlob model DOT file wen.yang
2026-11-05 17:49 ` [PATCH v8 11/14] rv: Add tlob hybrid automaton monitor wen.yang
2026-11-05 17:49 ` [PATCH v8 12/14] rv: Add KUnit tests for the tlob monitor wen.yang
2026-11-05 17:49 ` [PATCH v8 13/14] selftests/verification: Add tlob selftests wen.yang
2026-11-05 17:49 ` [PATCH v8 14/14] selftests/ftrace: Walk up parent dirs to find test.d/functions wen.yang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®