Skip to contentSkip to conversation

rv/tlob: Add task latency over budget RV monitor

RFC PATCH Superseded [email protected] 2 participants 31 messages May 11 – May 22

Subsystems (4)

v2 of 8 Superseded Latest is v8 →

lore
Share & tools
lore
#0 lore raw

v2 cover rv/tlob: Add task latency over budget RV monitor

From: Wen Yang <[email protected]>

This series introduces tlob (task latency over budget), a per-task hybrid automaton RV monitor that measures elapsed wall-clock time across a user-delimited code section and fires when the time exceeds a configurable budget.

Background
----------
The existing wwnr monitor uses a two-state DA to detect tasks that are woken but never run. tlob extends the RV framework to a three-state hybrid automaton:

running (initial) -- on CPU
waiting -- in the scheduler runqueue, not yet on CPU
sleeping -- blocked on a lock, I/O, or similar resource

A single HA clock invariant, clk_elapsed < BUDGET_NS(), is active in all states. The framework enforces it via a per-task hrtimer. On expiry, error_env_tlob is emitted, followed by detail_env_tlob which carries a per-state time breakdown (running_ns, waiting_ns, sleeping_ns) that pinpoints whether the overrun occurred in the running, waiting, or sleeping state.

Two userspace interfaces are provided:

ioctl (/dev/rv): TLOB_IOCTL_TRACE_START / TLOB_IOCTL_TRACE_STOP for
in-process self-instrumentation; TRACE_STOP returns -EOVERFLOW if the budget was exceeded during the window.

tracefs (monitors/tlob/monitor): write "p PATH:START STOP threshold=NS"
to attach uprobes to an unmodified binary. The start uprobe calls tlob_start_task() and the stop uprobe calls tlob_stop_task().

Infrastructure additions
------------------------
The series also includes several prerequisite additions to the RV framework that are useful beyond tlob:

rv/da: two bug fixes for race conditions in da_monitor_destroy() and
the monitoring flag memory ordering that affect existing per-task monitors (patches 1-2).

rv/da: pre-allocated storage pool (da_monitor_init_prealloc) to avoid
kmalloc on the scheduler tracepoint hot path and to support PREEMPT_RT configurations (patch 4).

rv: generic uprobe infrastructure (rv_uprobe) providing a thin wrapper
around uprobe_consumer with a per-binding priv pointer and fully synchronous detach semantics (patch 5).

rvgen: support for reset() annotations on the __init arrow, enabling
monitors where a clock is reset once at object creation and active in all states (patch 6).

Testing
-------
KUnit (five suites, 19 test cases):

tlob_task_api -- start/stop lifecycle, error paths, -ENOSPC,
deadline firing in each state
tlob_sched_integration -- context-switch accounting, monitoring a
kthread other than current
tlob_uprobe_format -- add/remove format acceptance and rejection
via tlob_create_or_delete_uprobe()
tlob_trace_output -- event_tlob and error_env_tlob field values
tlob_violation_react -- error count per budget expiry; per-state
ns breakdown dominance

kselftest (12 tests, run via verificationtest-ktap on x86_64 with vng):

TAP version 13
1..12
ok 1 Test monitor enable/disable
ok 2 Test monitor reactor setting
ok 3 Check available monitors
ok 4 Test wwnr monitor with printk reactor
ok 5 Test tlob ioctl self-instrumentation (within/over-budget, error paths) ok 6 Test tlob monitor tracefs interface (enable/disable and files)
ok 7 uprobe binding: visible in monitor file, removable, duplicate offset rejected ok 8 uprobe detail sleeping: sleeping_ns dominates when task blocks between probes ok 9 uprobe detail waiting: waiting_ns dominates when task is preempted between probes ok 10 Two bindings on same binary with different offsets and budgets fire independently ok 11 Verify no spurious error_env_tlob events without an active uprobe binding ok 12 uprobe violation: error_env_tlob and detail_env_tlob fire with correct fields # Totals: pass:12 fail:0 xfail:0 xpass:0 skip:0 error:0

Changes in v2 (addressing review by Gabriele)
----------------------------------------------------------------
- Switch from a custom hash table to RV_MON_PER_OBJ with "typedef struct tlob_task_state *monitor_target"; the per-invocation threshold is carried in the target struct and accessed via ha_get_target(ha_mon)->threshold_us, following the nomiss monitor pattern.

- Replace the explicit unmonitored state and da_monitor_init_hook() timer arming with da_handle_start_event() for initial state setup, then ha_reset_clk_ns() + ha_start_timer_ns() directly. The HA clock invariant clk_elapsed < BUDGET_NS() is now expressed in the DOT model and enforced by the HA framework.

- Drop the v1 KUnit test suite that validated every DA transition matrix entry; replace with five suites that test meaningful behaviours:
start/stop lifecycle, context-switch accounting, uprobe format validation, trace event field values, and violation reaction with per-state ns breakdown.

- Move selftests from tools/testing/selftests/rv/ into the existing tools/testing/selftests/verification/ harness; test logic is expressed as ftracetest .tc scripts under test.d/tlob/, with helper binaries (tlob_ioctl, tlob_target) built by tlob/Makefile and located via PATH.

Wen Yang (10):
rv/da: fix monitor start ordering and memory ordering for monitoring
flag
rv/da: fix per-task da_monitor_destroy() ordering and sync selftests/verification: fix verificationtest-ktap for out-of-tree
execution
rv/da: add pre-allocated storage pool for per-object monitors
rv: add generic uprobe infrastructure for RV monitors
rvgen: support reset() on the __init arrow for global-window HA clocks rv/tlob: add tlob model DOT file
rv/tlob: add tlob hybrid automaton monitor
rv/tlob: add KUnit tests for the tlob monitor selftests/verification: add tlob selftests

 Documentation/trace/rv/index.rst              |    1 +
 Documentation/trace/rv/monitor_tlob.rst       |  213 +++
 MAINTAINERS                                   |    3 +
 include/linux/rv.h                            |   45 +
 include/rv/automata.h                         |   15 +
 include/rv/da_monitor.h                       |  234 ++-
 include/rv/ha_monitor.h                       |   33 +-
 include/rv/rv_uprobe.h                        |  119 ++
 include/uapi/linux/rv.h                       |   86 ++
 kernel/trace/rv/Kconfig                       |    6 +
 kernel/trace/rv/Makefile                      |    5 +-
 kernel/trace/rv/monitors/tlob/.kunitconfig    |    5 +
 kernel/trace/rv/monitors/tlob/Kconfig         |   69 +
 kernel/trace/rv/monitors/tlob/tlob.c          | 1333 +++++++++++++++++
 kernel/trace/rv/monitors/tlob/tlob.h          |  171 +++
 kernel/trace/rv/monitors/tlob/tlob_kunit.c    |  881 +++++++++++
 kernel/trace/rv/monitors/tlob/tlob_trace.h    |   58 +
 kernel/trace/rv/rv.c                          |   38 +
 kernel/trace/rv/rv.h                          |    2 +
 kernel/trace/rv/rv_chardev.c                  |  201 +++
 kernel/trace/rv/rv_trace.h                    |    1 +
 kernel/trace/rv/rv_uprobe.c                   |  189 +++
 tools/include/uapi/linux/rv.h                 |   86 ++
 tools/testing/selftests/verification/Makefile |   21 +-
 .../verification/test.d/tlob/ioctl.tc         |   36 +
 .../verification/test.d/tlob/tracefs.tc       |   17 +
 .../verification/test.d/tlob/uprobe_bind.tc   |   34 +
 .../test.d/tlob/uprobe_detail_sleeping.tc     |   47 +
 .../test.d/tlob/uprobe_detail_waiting.tc      |   60 +
 .../verification/test.d/tlob/uprobe_multi.tc  |   60 +
 .../test.d/tlob/uprobe_no_event.tc            |   19 +
 .../test.d/tlob/uprobe_violation.tc           |   60 +
 .../selftests/verification/tlob/Makefile      |   21 +
 .../selftests/verification/tlob/tlob_ioctl.c  |  626 ++++++++
 .../selftests/verification/tlob/tlob_target.c |  138 ++
 .../verification/verificationtest-ktap        |    4 +-
 tools/verification/models/tlob.dot            |   21 +
 tools/verification/rvgen/rvgen/automata.py    |   26 +
 tools/verification/rvgen/rvgen/dot2k.py       |  100 +-
 39 files changed, 5043 insertions(+), 41 deletions(-)
 create mode 100644 Documentation/trace/rv/monitor_tlob.rst
 create mode 100644 include/rv/rv_uprobe.h
 create mode 100644 include/uapi/linux/rv.h
 create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig
 create mode 100644 kernel/trace/rv/monitors/tlob/Kconfig
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob.c
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob.h
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_trace.h
 create mode 100644 kernel/trace/rv/rv_chardev.c
 create mode 100644 kernel/trace/rv/rv_uprobe.c
 create mode 100644 tools/include/uapi/linux/rv.h
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/ioctl.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/tracefs.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
 create mode 100644 tools/testing/selftests/verification/tlob/Makefile
 create mode 100644 tools/testing/selftests/verification/tlob/tlob_ioctl.c
 create mode 100644 tools/testing/selftests/verification/tlob/tlob_target.c
 create mode 100644 tools/verification/models/tlob.dot
Signature · 1 line
2.25.1
#1 lore raw

RFC PATCH v2 1/10 rv/da: fix monitor start ordering and memory ordering for monitoring flag

From: Wen Yang <[email protected]>

da_monitor_start() set monitoring=1 before calling da_monitor_init_hook(), may racing with the sched_switch handler:

  da_monitor_start()               sched_switch handler
  -------------------------        ---------------------------------
  da_mon->monitoring = 1;
                                   if (da_monitoring(da_mon))  /* true  */
                                       ha_start_timer_ns(...);
                                       /* hrtimer->base == NULL, crash */
  da_monitor_init_hook(da_mon);
  /* hrtimer_setup() sets base */

Fix the ordering and pair with release/acquire semantics:

  da_monitor_init_hook(da_mon);
  smp_store_release(&da_mon->monitoring, 1);    /* da_monitor_start()  */
  return smp_load_acquire(&da_mon->monitoring); /* da_monitoring()     */

On ARM64 a plain STR + LDR does not form a release-acquire pair, so the load can observe monitoring=1 while hrtimer->base is still NULL. The plain accesses are also data races under KCSAN.

Use WRITE_ONCE for the monitoring=0 store in da_monitor_reset() to cover the reset path.

Fixes 792575348ff7 ("rv/include: Add deterministic automata monitor definition via C macros")
---
 include/rv/da_monitor.h | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
include/rv/da_monitor.h +5 -3
index 39765ff6f098..00ded3d5ab3f 100644
@@ -82,7 +82,7 @@ static void react(enum states curr_state, enum events event)
 static inline void da_monitor_reset(struct da_monitor *da_mon)
 {
 	da_monitor_reset_hook(da_mon);
-	da_mon->monitoring = 0;
+	WRITE_ONCE(da_mon->monitoring, 0);
 	da_mon->curr_state = model_get_initial_state();
 }
 
@@ -95,8 +95,9 @@ static inline void da_monitor_reset(struct da_monitor *da_mon)
 static inline void da_monitor_start(struct da_monitor *da_mon)
 {
 	da_mon->curr_state = model_get_initial_state();
-	da_mon->monitoring = 1;
 	da_monitor_init_hook(da_mon);
+	/* Pairs with smp_load_acquire in da_monitoring(). */
+	smp_store_release(&da_mon->monitoring, 1);
 }
 
 /*
@@ -104,7 +105,8 @@ static inline void da_monitor_start(struct da_monitor *da_mon)
  */
 static inline bool da_monitoring(struct da_monitor *da_mon)
 {
-	return da_mon->monitoring;
+	/* Pairs with smp_store_release in da_monitor_start(). */
+	return smp_load_acquire(&da_mon->monitoring);
 }
 
 /*
Signature · 1 line
2.25.1
maintainer Reviewed-by on v2 1/10
#2 lore raw

Re: [RFC PATCH v2 01/10] rv/da: fix monitor start ordering and memory ordering for monitoring flag

Quoted 24 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

da_monitor_start() set monitoring=1 before calling da_monitor_init_hook(), may racing with the sched_switch handler:

  da_monitor_start()               sched_switch handler -------------------------        --------------------------------- da_mon->monitoring = 1;
                                   if (da_monitoring(da_mon))  /* true  */ ha_start_timer_ns(...); /* hrtimer->base == NULL, crash */ da_monitor_init_hook(da_mon);
  /* hrtimer_setup() sets base */

Fix the ordering and pair with release/acquire semantics:

  da_monitor_init_hook(da_mon); smp_store_release(&da_mon->monitoring, 1);    /* da_monitor_start()  */ return smp_load_acquire(&da_mon->monitoring); /* da_monitoring()     */

On ARM64 a plain STR + LDR does not form a release-acquire pair, so the load can observe monitoring=1 while hrtimer->base is still NULL. The plain accesses are also data races under KCSAN.

Quoted 6 lines

Use WRITE_ONCE for the monitoring=0 store in da_monitor_reset() to cover the reset path.

Fixes 792575348ff7 ("rv/include: Add deterministic automata monitor

definition via C macros")

Thanks for the fix!

There are probably more than a few of those bugs since most monitors are implicitly serialised because their events are serialised..

Looks good to me.

See minor comments below:

Quoted 18 lines

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

 1 file changed, 5 insertions(+), 3 deletions(-)
include/rv/da_monitor.h +1 -1
index 39765ff6f098..00ded3d5ab3f 100644
@@ -82,7 +82,7 @@ static void react(enum states curr_state, enum events event)
  static inline void da_monitor_reset(struct da_monitor *da_mon)
  {
   da_monitor_reset_hook(da_mon);
- da_mon->monitoring = 0;
+ WRITE_ONCE(da_mon->monitoring, 0);
   da_mon->curr_state = model_get_initial_state();
  }

include/rv/da_monitor.h static inline void da_monitor_reset(struct da_monitor · line 100

Quoted 7 lines
include/rv/da_monitor.h +1 -1
@@ -95,8 +95,9 @@ static inline void da_monitor_reset(struct da_monitor
 *da_mon)
  static inline void da_monitor_start(struct da_monitor *da_mon)
  {
   da_mon->curr_state = model_get_initial_state();
- da_mon->monitoring = 1;
   da_monitor_init_hook(da_mon);
+ /* Pairs with smp_load_acquire in da_monitoring(). */

I wonder if these comment are really adding value, pairing smp_load_acquire / smp_store_release is the by-the-book usage and everything is here.

But feel free to leave it if you think it's clearer.

Thanks,
Gabriele

Quoted 15 lines
include/rv/da_monitor.h +1 -0
+ smp_store_release(&da_mon->monitoring, 1);
  }


 /*

include/rv/da_monitor.h +2 -1
@@ -104,7 +105,8 @@ static inline void da_monitor_start(struct da_monitor
 *da_mon)
   */
  static inline bool da_monitoring(struct da_monitor *da_mon)
  {
- return da_mon->monitoring;
+ /* Pairs with smp_store_release in da_monitor_start(). */
+ return smp_load_acquire(&da_mon->monitoring);
  }


 /*

#3 lore raw

RFC PATCH v2 2/10 rv/da: fix per-task da_monitor_destroy() ordering and sync

In reply to [email protected]

From: Wen Yang <[email protected]>

The following two paths race:

CPU 0 (disable_stall/__rv_disable_monitor) CPU 1 (wwnr probe handler) ------------------------------------------ ----------------------------- disable_stall()

    da_monitor_destroy()
      da_monitor_reset_all()          <------ [task T: monitoring=0]
                                              da_monitor_start(&T->rv[n])
                                              /* no timer_setup */
                                               monitoring=1  <----

tracepoint_synchronize_unregister()
// CPU 1 probe has already returned; sync returns

Later, enable_stall() acquires the same slot and calls da_monitor_init():

da_monitor_reset_all()

    da_monitor_reset(&T->rv[slot])    // monitoring=1, timer.function==0
      ha_monitor_reset_env()
        ha_cancel_timer()
          timer_delete(&ha_mon->timer)  // ODEBUG: timer never initialised

ODEBUG: assert_init not available (active state 0) object type: timer_list

  Call trace: timer_delete <- da_monitor_reset_all <- enable_stall

Call tracepoint_synchronize_unregister() inside da_monitor_destroy() before da_monitor_reset_all(). The unregister_trace_xxx() calls in the monitor's disable() have already disconnected the tracepoints; the sync here drains any handler still in flight, so no new monitoring=1 can appear after da_monitor_reset_all() clears the slot.

Also fix the slot release ordering: release the slot only after reset_all() to avoid accessing rv[] with an out-of-bounds index.

Fixes f5587d1b6ec9 ("rv: Add Hybrid Automata monitor type")
---
 include/rv/da_monitor.h | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)
include/rv/da_monitor.h +16 -2
index 00ded3d5ab3f..d04bb3229c75 100644
@@ -304,6 +304,20 @@ static int da_monitor_init(void)
 
 /*
  * da_monitor_destroy - return the allocated slot
+ *
+ * Call tracepoint_synchronize_unregister() before reset_all() to close
+ * the race where an in-flight non-HA probe handler sets monitoring=1
+ * (without calling timer_setup()) after da_monitor_reset_all() has
+ * already cleared the slot but before the caller's own sync completes.
+ * Without this barrier, an HA_TIMER_WHEEL monitor that later acquires
+ * the same slot would call timer_delete() on a never-initialised
+ * timer_list, triggering ODEBUG warnings.
+ *
+ * Note: tracepoint_synchronize_unregister() is a system-wide barrier
+ * that waits for all CPUs to finish any in-flight tracepoint handlers.
+ * The caller's own __rv_disable_monitor() issues a second sync after
+ * returning from disable(); that redundant call is harmless on the
+ * infrequent admin (enable/disable) path.
  */
 static inline void da_monitor_destroy(void)
 {
@@ -311,10 +325,10 @@ static inline void da_monitor_destroy(void)
 		WARN_ONCE(1, "Disabling a disabled monitor: " __stringify(MONITOR_NAME));
 		return;
 	}
+	tracepoint_synchronize_unregister();
+	da_monitor_reset_all();
 	rv_put_task_monitor_slot(task_mon_slot);
 	task_mon_slot = RV_PER_TASK_MONITOR_INIT;
-
-	da_monitor_reset_all();
 }
 
 #elif RV_MON_TYPE == RV_MON_PER_OBJ
Signature · 1 line
2.25.1
maintainer
#4 lore raw

Re: [RFC PATCH v2 02/10] rv/da: fix per-task da_monitor_destroy() ordering and sync

Quoted 5 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

The following two paths race:

  CPU 0 (disable_stall/__rv_disable_monitor)  CPU 1 (wwnr probe handler)

^ did you mean stall?

Quoted 28 lines

  ------------------------------------------  ----------------------------- disable_stall()
    da_monitor_destroy()
      da_monitor_reset_all()          <------ [task T: monitoring=0] da_monitor_start(&T->rv[n]) /* no timer_setup */ monitoring=1  <---- tracepoint_synchronize_unregister()
  // CPU 1 probe has already returned; sync returns

Later, enable_stall() acquires the same slot and calls da_monitor_init():

  da_monitor_reset_all()
    da_monitor_reset(&T->rv[slot])    // monitoring=1, timer.function==0 ha_monitor_reset_env()
        ha_cancel_timer()
          timer_delete(&ha_mon->timer)  // ODEBUG: timer never initialised

  ODEBUG: assert_init not available (active state 0) object type: timer_list

  Call trace: timer_delete <- da_monitor_reset_all <- enable_stall

Call tracepoint_synchronize_unregister() inside da_monitor_destroy() before da_monitor_reset_all().  The unregister_trace_xxx() calls in the monitor's disable() have already disconnected the tracepoints; the sync here drains any handler still in flight, so no new monitoring=1 can appear after da_monitor_reset_all() clears the slot.

Quoted 6 lines

Also fix the slot release ordering: release the slot only after reset_all() to avoid accessing rv[] with an out-of-bounds index.

Fixes f5587d1b6ec9 ("rv: Add Hybrid Automata monitor type")

---

Thanks for the fix, I have a similar one waiting for submission.

These are technically 2 separate fixes though: the ordering with unset task_mon_slot (independent on HA) and the synchronisation with pending tracepoints. They probably deserve separate patches and visibility, the first has always been around and we're technically overwriting who knows what.

The explanation above is a bit hard to follow though, are you talking about a handler for the same (stall) monitor running after the reset, effectively undoing it by setting the monitoring flag?

Then this is indeed an issue with ha_monitor_reset_env() which expects a clean environment.

So that's basically what you'd see now much more often because in fact we don't reset the right slot (though, again, that's a different issue).

Calling tracepoint_synchronize_unregister() there too would surely fix, but it used to be kinda slow. But it's probably gotten faster since now tracepoints use SRCU, so we can wait for a dedicated grace period.

I liked the idea to wait cumulatively in the end, but that's just making things harder.. Let's do like this:

Prepare 2 separate patches as fixes, put the task slot one first (would ease backporting), mention this issue with the race condition only in the second. You can send them independently and I'll add them to the tree as urgent.

I'm soon going to send my set of fixes that will also include the task slot patch (not removing to ease my life with conflicts).

Thanks,
Gabriele

Quoted 42 lines

 include/rv/da_monitor.h | 18 ++++++++++++++++--

 1 file changed, 16 insertions(+), 2 deletions(-)
include/rv/da_monitor.h +0 -0
index 00ded3d5ab3f..d04bb3229c75 100644
@@ -304,6 +304,20 @@ static int da_monitor_init(void)


 /*
  * da_monitor_destroy - return the allocated slot
+ *
+ * Call tracepoint_synchronize_unregister() before reset_all() to close
+ * the race where an in-flight non-HA probe handler sets monitoring=1
+ * (without calling timer_setup()) after da_monitor_reset_all() has
+ * already cleared the slot but before the caller's own sync completes.
+ * Without this barrier, an HA_TIMER_WHEEL monitor that later acquires
+ * the same slot would call timer_delete() on a never-initialised
+ * timer_list, triggering ODEBUG warnings.
+ *
+ * Note: tracepoint_synchronize_unregister() is a system-wide barrier
+ * that waits for all CPUs to finish any in-flight tracepoint handlers.
+ * The caller's own __rv_disable_monitor() issues a second sync after
+ * returning from disable(); that redundant call is harmless on the
+ * infrequent admin (enable/disable) path.
  */
 static inline void da_monitor_destroy(void)
 {

include/rv/da_monitor.h +0 -0
@@ -311,10 +325,10 @@ static inline void da_monitor_destroy(void)

  WARN_ONCE(1, "Disabling a disabled monitor: " __stringify(MONITOR_NAME));
  return;
  }

include/rv/da_monitor.h +2 -0
+	tracepoint_synchronize_unregister();
+	da_monitor_reset_all();

  rv_put_task_monitor_slot(task_mon_slot);
  task_mon_slot = RV_PER_TASK_MONITOR_INIT;

include/rv/da_monitor.h +0 -2
-
-	da_monitor_reset_all();

 }

 #elif RV_MON_TYPE == RV_MON_PER_OBJ

maintainer
#5 lore raw

Re: [RFC PATCH v2 02/10] rv/da: fix per-task da_monitor_destroy() ordering and sync

Quoted 7 lines · Gabriele Monaco
Quoted 5 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

The following two paths race:

  CPU 0 (disable_stall/__rv_disable_monitor)  CPU 1 (wwnr probe handler)

^ did you mean stall?

Ok I got it now, so essentially you'd reproduce it like:

* start a DA per-task monitor (no timer)
* stop it, a handler is still running after reset, it sets monitoring back to 1
* start an HA per-task monitor

that would use the same slot that is now looking like:

{ monitoring = 1, timer.function = NULL }

because it was not initialised as HA but monitoring was reset in the race.

Thinking about this again, it isn't just an issue with per-task monitors, all monitors reusing slots would suffer from it.
Besides, relying on monitoring can be fragile when using LTL monitors on the same task (those don't even have monitoring).

Perhaps the solution isn't that trivial, I'm going to give one more thought on it, but thanks again for bringing this up!

Gabriele

Quoted 117 lines
Quoted 28 lines

  ------------------------------------------  ----------------------------- disable_stall()
    da_monitor_destroy()
      da_monitor_reset_all()          <------ [task T: monitoring=0] da_monitor_start(&T->rv[n]) /* no timer_setup */ monitoring=1  <---- tracepoint_synchronize_unregister()
  // CPU 1 probe has already returned; sync returns

Later, enable_stall() acquires the same slot and calls da_monitor_init():

  da_monitor_reset_all()
    da_monitor_reset(&T->rv[slot])    // monitoring=1, timer.function==0 ha_monitor_reset_env()
        ha_cancel_timer()
          timer_delete(&ha_mon->timer)  // ODEBUG: timer never initialised

  ODEBUG: assert_init not available (active state 0) object type: timer_list

  Call trace: timer_delete <- da_monitor_reset_all <- enable_stall

Call tracepoint_synchronize_unregister() inside da_monitor_destroy() before da_monitor_reset_all().  The unregister_trace_xxx() calls in the monitor's disable() have already disconnected the tracepoints; the sync here drains any handler still in flight, so no new monitoring=1 can appear after da_monitor_reset_all() clears the slot.

Quoted 6 lines

Also fix the slot release ordering: release the slot only after reset_all() to avoid accessing rv[] with an out-of-bounds index.

Fixes f5587d1b6ec9 ("rv: Add Hybrid Automata monitor type")

---

Thanks for the fix, I have a similar one waiting for submission.

These are technically 2 separate fixes though: the ordering with unset task_mon_slot (independent on HA) and the synchronisation with pending tracepoints. They probably deserve separate patches and visibility, the first has always been around and we're technically overwriting who knows what.

The explanation above is a bit hard to follow though, are you talking about a handler for the same (stall) monitor running after the reset, effectively undoing it by setting the monitoring flag?

Then this is indeed an issue with ha_monitor_reset_env() which expects a clean environment.

So that's basically what you'd see now much more often because in fact we don't
reset the right slot (though, again, that's a different issue).

Calling tracepoint_synchronize_unregister() there too would surely fix, but it used to be kinda slow. But it's probably gotten faster since now tracepoints use
SRCU, so we can wait for a dedicated grace period.

I liked the idea to wait cumulatively in the end, but that's just making things
harder.. Let's do like this:

Prepare 2 separate patches as fixes, put the task slot one first (would ease backporting), mention this issue with the race condition only in the second. You can send them independently and I'll add them to the tree as urgent.

I'm soon going to send my set of fixes that will also include the task slot patch (not removing to ease my life with conflicts).

Thanks,
Gabriele

Quoted 42 lines

 include/rv/da_monitor.h | 18 ++++++++++++++++--

 1 file changed, 16 insertions(+), 2 deletions(-)
include/rv/da_monitor.h +0 -0
index 00ded3d5ab3f..d04bb3229c75 100644
@@ -304,6 +304,20 @@ static int da_monitor_init(void)


 /*
  * da_monitor_destroy - return the allocated slot
+ *
+ * Call tracepoint_synchronize_unregister() before reset_all() to close
+ * the race where an in-flight non-HA probe handler sets monitoring=1
+ * (without calling timer_setup()) after da_monitor_reset_all() has
+ * already cleared the slot but before the caller's own sync completes.
+ * Without this barrier, an HA_TIMER_WHEEL monitor that later acquires
+ * the same slot would call timer_delete() on a never-initialised
+ * timer_list, triggering ODEBUG warnings.
+ *
+ * Note: tracepoint_synchronize_unregister() is a system-wide barrier
+ * that waits for all CPUs to finish any in-flight tracepoint handlers.
+ * The caller's own __rv_disable_monitor() issues a second sync after
+ * returning from disable(); that redundant call is harmless on the
+ * infrequent admin (enable/disable) path.
  */
 static inline void da_monitor_destroy(void)
 {

include/rv/da_monitor.h +0 -0
@@ -311,10 +325,10 @@ static inline void da_monitor_destroy(void)

  WARN_ONCE(1, "Disabling a disabled monitor: " __stringify(MONITOR_NAME));
  return;
  }

include/rv/da_monitor.h +2 -0
+	tracepoint_synchronize_unregister();
+	da_monitor_reset_all();

  rv_put_task_monitor_slot(task_mon_slot);
  task_mon_slot = RV_PER_TASK_MONITOR_INIT;

include/rv/da_monitor.h +0 -2
-
-	da_monitor_reset_all();

 }

 #elif RV_MON_TYPE == RV_MON_PER_OBJ

author
#6 lore raw

Re: [RFC PATCH v2 02/10] rv/da: fix per-task da_monitor_destroy() ordering and sync

Quoted 26 lines · Gabriele Monaco
Quoted 7 lines · Gabriele Monaco
Quoted 5 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

The following two paths race:

  CPU 0 (disable_stall/__rv_disable_monitor)  CPU 1 (wwnr probe handler)

^ did you mean stall?

Ok I got it now, so essentially you'd reproduce it like:

* start a DA per-task monitor (no timer)
* stop it, a handler is still running after reset, it sets monitoring back to 1
* start an HA per-task monitor

that would use the same slot that is now looking like:

{ monitoring = 1, timer.function = NULL }

because it was not initialised as HA but monitoring was reset in the race.

Thinking about this again, it isn't just an issue with per-task monitors, all monitors reusing slots would suffer from it.
Besides, relying on monitoring can be fragile when using LTL monitors on the same task (those don't even have monitoring).

Quoted 77 lines

Perhaps the solution isn't that trivial, I'm going to give one more thought on it, but thanks again for bringing this up!

Gabriele

Quoted 72 lines
Quoted 28 lines

  ------------------------------------------  ----------------------------- disable_stall()
    da_monitor_destroy()
      da_monitor_reset_all()          <------ [task T: monitoring=0] da_monitor_start(&T->rv[n]) /* no timer_setup */ monitoring=1  <---- tracepoint_synchronize_unregister()
  // CPU 1 probe has already returned; sync returns

Later, enable_stall() acquires the same slot and calls da_monitor_init():

  da_monitor_reset_all()
    da_monitor_reset(&T->rv[slot])    // monitoring=1, timer.function==0 ha_monitor_reset_env()
        ha_cancel_timer()
          timer_delete(&ha_mon->timer)  // ODEBUG: timer never initialised

  ODEBUG: assert_init not available (active state 0) object type: timer_list

   Call trace: timer_delete <- da_monitor_reset_all <- enable_stall

Call tracepoint_synchronize_unregister() inside da_monitor_destroy() before da_monitor_reset_all().  The unregister_trace_xxx() calls in the monitor's disable() have already disconnected the tracepoints; the sync here drains any handler still in flight, so no new monitoring=1 can appear after da_monitor_reset_all() clears the slot.

Quoted 6 lines

Also fix the slot release ordering: release the slot only after reset_all() to avoid accessing rv[] with an out-of-bounds index.

Fixes f5587d1b6ec9 ("rv: Add Hybrid Automata monitor type")

---

Thanks for the fix, I have a similar one waiting for submission.

These are technically 2 separate fixes though: the ordering with unset task_mon_slot (independent on HA) and the synchronisation with pending tracepoints. They probably deserve separate patches and visibility, the first has always been around and we're technically overwriting who knows what.

The explanation above is a bit hard to follow though, are you talking about a handler for the same (stall) monitor running after the reset, effectively undoing it by setting the monitoring flag?

Then this is indeed an issue with ha_monitor_reset_env() which expects a clean environment.

So that's basically what you'd see now much more often because in fact we don't
reset the right slot (though, again, that's a different issue).

Calling tracepoint_synchronize_unregister() there too would surely fix, but it used to be kinda slow. But it's probably gotten faster since now tracepoints use
SRCU, so we can wait for a dedicated grace period.

I liked the idea to wait cumulatively in the end, but that's just making things
harder.. Let's do like this:

Prepare 2 separate patches as fixes, put the task slot one first (would ease backporting), mention this issue with the race condition only in the second. You can send them independently and I'll add them to the tree as urgent.

I'm soon going to send my set of fixes that will also include the task slot patch (not removing to ease my life with conflicts).

Hi Gabriele,

Thanks for both messages. Two patches are ready; let me address your follow-up concerns before sending.

1. "all monitors reusing slots would suffer from it"

Only RV_MON_PER_TASK uses the rv_get/put_task_monitor_slot() pool. RV_MON_GLOBAL and RV_MON_PER_CPU each have dedicated storage (a single static variable and a per-cpu variable) and never share slots across monitor types. The race is exclusive to PER_TASK, so fixing that variant's da_monitor_destroy() is the correct scope.

2. "LTL monitors don't even have monitoring"

tracepoint_synchronize_unregister() does not rely on the monitoring flag at all. It is a system-wide barrier — it calls synchronize_rcu_tasks_trace() followed by synchronize_srcu(&tracepoint_srcu) — draining every in-flight tracepoint handler on every CPU regardless of which monitor dispatched it. LTL handlers are covered without any special treatment.

The slot-ordering issue (patch 1) affects all per-task DA monitors, not only HA ones — "independent on HA" — because RV_PER_TASK_MONITOR_INIT equals CONFIG_RV_PER_TASK_MONITORS (one past the end of rv[]), so da_monitor_reset_all() overwrites whatever follows rv[] in task_struct whenever any per-task monitor is disabled.

Also corrected "wwnr probe handler" to "stall probe handler" in patch 2 per your annotation.

Please let me know if the above reasoning addresses your concerns.

--
Best wishes,
Wen

Quoted 44 lines
Quoted 43 lines
Quoted 43 lines
  include/rv/da_monitor.h | 18 ++++++++++++++++--
  1 file changed, 16 insertions(+), 2 deletions(-)
include/rv/da_monitor.h +16 -2
index 00ded3d5ab3f..d04bb3229c75 100644
@@ -304,6 +304,20 @@ static int da_monitor_init(void)
 
  /*
   * da_monitor_destroy - return the allocated slot
+ *
+ * Call tracepoint_synchronize_unregister() before reset_all() to close
+ * the race where an in-flight non-HA probe handler sets monitoring=1
+ * (without calling timer_setup()) after da_monitor_reset_all() has
+ * already cleared the slot but before the caller's own sync completes.
+ * Without this barrier, an HA_TIMER_WHEEL monitor that later acquires
+ * the same slot would call timer_delete() on a never-initialised
+ * timer_list, triggering ODEBUG warnings.
+ *
+ * Note: tracepoint_synchronize_unregister() is a system-wide barrier
+ * that waits for all CPUs to finish any in-flight tracepoint handlers.
+ * The caller's own __rv_disable_monitor() issues a second sync after
+ * returning from disable(); that redundant call is harmless on the
+ * infrequent admin (enable/disable) path.
   */
  static inline void da_monitor_destroy(void)
  {
@@ -311,10 +325,10 @@ static inline void da_monitor_destroy(void)
  		WARN_ONCE(1, "Disabling a disabled monitor: "
 __stringify(MONITOR_NAME));
  		return;
  	}
+	tracepoint_synchronize_unregister();
+	da_monitor_reset_all();
  	rv_put_task_monitor_slot(task_mon_slot);
  	task_mon_slot = RV_PER_TASK_MONITOR_INIT;
-
-	da_monitor_reset_all();
  }
 

 #elif RV_MON_TYPE == RV_MON_PER_OBJ

maintainer
#7 lore raw

Re: [RFC PATCH v2 02/10] rv/da: fix per-task da_monitor_destroy() ordering and sync

Quoted 22 lines · Wen Yang

Thanks for both messages.  Two patches are ready; let me address your follow-up concerns before sending.

   1. "all monitors reusing slots would suffer from it"

      Only RV_MON_PER_TASK uses the rv_get/put_task_monitor_slot() pool.  RV_MON_GLOBAL and RV_MON_PER_CPU each have dedicated storage (a single static variable and a per-cpu variable) and never share slots across monitor types.  The race is exclusive to PER_TASK, so fixing that variant's da_monitor_destroy() is the correct scope.

   2. "LTL monitors don't even have monitoring"

      tracepoint_synchronize_unregister() does not rely on the monitoring flag at all.  It is a system-wide barrier — it calls synchronize_rcu_tasks_trace() followed by synchronize_srcu(&tracepoint_srcu) — draining every in-flight tracepoint handler on every CPU regardless of which monitor dispatched it.  LTL handlers are covered without any special treatment.

Quoted 6 lines

The slot-ordering issue (patch 1) affects all per-task DA monitors, not only HA ones — "independent on HA" — because RV_PER_TASK_MONITOR_INIT equals CONFIG_RV_PER_TASK_MONITORS (one past the end of rv[]), so da_monitor_reset_all() overwrites whatever follows rv[] in task_struct whenever any per-task monitor is disabled.

Exactly, and since whatever follows .rv is randomised on a task_struct, this can get quite nasty.

I included my version of the fix in the series in [1], but feel free to send yours, you got there first ;)

Quoted 4 lines

Also corrected "wwnr probe handler" to "stall probe handler" in patch 2 per your annotation.

While tracepoint_synchronize_unregister() does fix the race, I still see a timed bomb in the way we do ha_monitor_reset_env().

Since we reused the same slots for per-task monitors (not for the others, you're right I was brainfarting) we essentially don't know what happened before we do da_monitor_init(), the same slot could have been used by an LTL monitor which cannot even reliably clear the byte used by the monitoring flag.

Now, we either mandate all monitors to memset the entire slot (union rv_task_monitor) or we don't assume anything about the slot's state during initialisation. Any middle ground could reveal pesky bugs as soon as we refactor the structs.

The latter idea is what I did in [1]. I believe that would make the synchronisation superfluous.

What do you think?

Thanks,
Gabriele

[1] - https://lore.kernel.org/lkml/[email protected]

Quoted 51 lines

Please let me know if the above reasoning addresses your concerns.

--
Best wishes,
Wen

Quoted 44 lines
Quoted 43 lines
Quoted 43 lines

  include/rv/da_monitor.h | 18 ++++++++++++++++--

  1 file changed, 16 insertions(+), 2 deletions(-)
include/rv/da_monitor.h +0 -0
index 00ded3d5ab3f..d04bb3229c75 100644
@@ -304,6 +304,20 @@ static int da_monitor_init(void)


  /*
   * da_monitor_destroy - return the allocated slot
+ *
+ * Call tracepoint_synchronize_unregister() before reset_all() to close
+ * the race where an in-flight non-HA probe handler sets monitoring=1
+ * (without calling timer_setup()) after da_monitor_reset_all() has
+ * already cleared the slot but before the caller's own sync completes.
+ * Without this barrier, an HA_TIMER_WHEEL monitor that later acquires
+ * the same slot would call timer_delete() on a never-initialised
+ * timer_list, triggering ODEBUG warnings.
+ *
+ * Note: tracepoint_synchronize_unregister() is a system-wide barrier
+ * that waits for all CPUs to finish any in-flight tracepoint handlers.
+ * The caller's own __rv_disable_monitor() issues a second sync after
+ * returning from disable(); that redundant call is harmless on the
+ * infrequent admin (enable/disable) path.
   */
  static inline void da_monitor_destroy(void)
  {

include/rv/da_monitor.h +0 -0
@@ -311,10 +325,10 @@ static inline void da_monitor_destroy(void)

   WARN_ONCE(1, "Disabling a disabled monitor: " __stringify(MONITOR_NAME));
   return;
   }

include/rv/da_monitor.h +2 -0
+	tracepoint_synchronize_unregister();
+	da_monitor_reset_all();

   rv_put_task_monitor_slot(task_mon_slot);
   task_mon_slot = RV_PER_TASK_MONITOR_INIT;

include/rv/da_monitor.h +0 -2
-
-	da_monitor_reset_all();

  }

  #elif RV_MON_TYPE == RV_MON_PER_OBJ

#8 lore raw

RFC PATCH v2 3/10 selftests/verification: fix verificationtest-ktap for out-of-tree execution

In reply to [email protected]

From: Wen Yang <[email protected]>

verificationtest-ktap used a CWD-relative path (../ftrace/ftracetest) and a relative argument (../verification) for --rv. This works when the shell changes into the verification directory first, but breaks when the script is invoked directly - e.g. by the kselftest runner or vng - because the working directory is the kernel source root, not the script's own directory.

Fix this by computing the script's directory from $0 with cd/dirname/pwd and using absolute paths for both the ftracetest invocation and the --rv argument. Also export the directory to PATH so that check_requires in the ftracetest framework can locate helper binaries.

---
 tools/testing/selftests/verification/verificationtest-ktap | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
tools/testing/selftests/verification/verificationtest-ktap +3 -1
index 18f7fe324e2f..456b8578a307 100755
@@ -5,4 +5,6 @@
 #
 # Copyright (C) Arm Ltd., 2023
 
-../ftrace/ftracetest -K -v --rv ../verification
+dir=$(cd "$(dirname "$0")" && pwd)
+export PATH="$dir:$PATH"
+"$dir/../ftrace/ftracetest" -K -v --rv "$dir"
Signature · 1 line
2.25.1
maintainer
#9 lore raw

Re: [RFC PATCH v2 03/10] selftests/verification: fix verificationtest-ktap for out-of-tree execution

Quoted 9 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

verificationtest-ktap used a CWD-relative path (../ftrace/ftracetest) and a relative argument (../verification) for --rv.  This works when the shell changes into the verification directory first, but breaks when the script is invoked directly - e.g. by the kselftest runner or vng - because the working directory is the kernel source root, not the script's own directory.

Quoted 6 lines

Fix this by computing the script's directory from $0 with cd/dirname/pwd and using absolute paths for both the ftracetest invocation and the --rv argument.  Also export the directory to PATH so that check_requires in the ftracetest framework can locate helper binaries.

Just out of curiosity, how do you run the selftests? Are you calling the script directly just to run /some/ of them?

The officially supported way is through make [1]:

make -C tools/testing/selftests TARGETS=verification run_tests

(though I find it faster to omit TARGETS and just do make -C tools/testing/selftests/verification).

Calling with make should set up all paths as needed.

Quoted 14 lines

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

 1 file changed, 3 insertions(+), 1 deletion(-)
tools/testing/selftests/verification/verificationtest-ktap +0 -0
index 18f7fe324e2f..456b8578a307 100755
@@ -5,4 +5,6 @@
  #
  # Copyright (C) Arm Ltd., 2023


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

tools/testing/selftests/verification/verificationtest-ktap line 9

Quoted 2 lines
tools/testing/selftests/verification/verificationtest-ktap +2 -0
+dir=$(cd "$(dirname "$0")" && pwd)
+export PATH="$dir:$PATH"

Then if you really really need to call it directly, do you need to override PATH?

And isn't it clearer to do:

  dir=$(realpath "$(dirname "$0")")

Thanks,
Gabriele

[1] - https://www.kernel.org/doc/html/latest/dev-tools/kselftest.html

Quoted 1 line

+"$dir/../ftrace/ftracetest" -K -v --rv "$dir"

#10 lore raw

RFC PATCH v2 4/10 rv/da: add pre-allocated storage pool for per-object monitors

In reply to [email protected]

From: Wen Yang <[email protected]>

da_create_empty_storage() uses kmalloc_nolock(), which requires CONFIG_HAVE_ALIGNED_STRUCT_PAGE; on UML and some PREEMPT_RT configurations it always returns NULL. Calling kmalloc from scheduler tracepoint handlers also adds unwanted latency and can fail under memory pressure.

Add da_monitor_init_prealloc(N) as an opt-in alternative to da_monitor_init(). It allocates N da_monitor_storage slots with GFP_KERNEL up-front and manages them on a LIFO free-stack protected by a spinlock, so da_create_or_get() never calls kmalloc on the hot path.

Monitors that do not call da_monitor_init_prealloc() are unaffected.

---
 include/rv/da_monitor.h | 208 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 186 insertions(+), 22 deletions(-)
include/rv/da_monitor.h +186 -22
index d04bb3229c75..7d6f62766251 100644
@@ -433,18 +433,6 @@ static inline da_id_type da_get_id(struct da_monitor *da_mon)
 	return container_of(da_mon, struct da_monitor_storage, rv.da_mon)->id;
 }
 
-/*
- * da_create_or_get - create the per-object storage if not already there
- *
- * This needs a lookup so should be guarded by RCU, the condition is checked
- * directly in da_create_storage()
- */
-static inline void da_create_or_get(da_id_type id, monitor_target target)
-{
-	guard(rcu)();
-	da_create_storage(id, target, da_get_monitor(id, target));
-}
-
 /*
  * da_fill_empty_storage - store the target in a pre-allocated storage
  *
@@ -475,15 +463,121 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
 	return mon_storage->target;
 }
 
+/*
+ * Per-object pool state.
+ *
+ * Zero-initialised by default (storage == NULL ⟹ kmalloc mode).  A monitor
+ * opts into pool mode by calling da_monitor_init_prealloc(N) instead of
+ * da_monitor_init(), which sets storage to a non-NULL kcalloc'd array.
+ *
+ * Because every field is wrapped in this struct and the struct itself is a
+ * per-TU static, each monitor that includes this header gets a completely
+ * independent pool.  A kmalloc monitor (e.g. nomiss) and a pool monitor
+ * (e.g. tlob) therefore coexist without any interference.
+ *
+ * da_pool_return_cb runs from softirq on non-PREEMPT_RT, so irqsave is
+ * required to prevent deadlock with task-context callers.  On PREEMPT_RT
+ * it runs from an rcuc kthread where spinlock_t is a sleeping lock.
+ */
+struct da_per_obj_pool {
+	struct da_monitor_storage  *storage;  /* non-NULL ⟹ pool mode */
+	struct da_monitor_storage **free;     /* kmalloc'd pointer stack */
+	unsigned int                free_top;
+	spinlock_t                  lock;
+};
+
+static struct da_per_obj_pool da_pool = {
+	.lock = __SPIN_LOCK_UNLOCKED(da_pool.lock),
+};
+
+static void da_pool_return_cb(struct rcu_head *head)
+{
+	struct da_monitor_storage *ms =
+		container_of(head, struct da_monitor_storage, rcu);
+	unsigned long flags;
+
+	spin_lock_irqsave(&da_pool.lock, flags);
+	da_pool.free[da_pool.free_top++] = ms;
+	spin_unlock_irqrestore(&da_pool.lock, flags);
+}
+
+/* Pops a slot from the pre-allocated pool; returns -ENOSPC if exhausted. */
+static inline int da_create_or_get_pool(da_id_type id, monitor_target target)
+{
+	struct da_monitor_storage *mon_storage;
+	unsigned long flags;
+
+	spin_lock_irqsave(&da_pool.lock, flags);
+	if (!da_pool.free_top) {
+		spin_unlock_irqrestore(&da_pool.lock, flags);
+		return -ENOSPC;
+	}
+	mon_storage = da_pool.free[--da_pool.free_top];
+	spin_unlock_irqrestore(&da_pool.lock, flags);
+
+	mon_storage->id = id;
+	mon_storage->target = target;
+	guard(rcu)();
+	hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+	return 0;
+}
+
+/*
+ * Tries da_create_storage() first (lock-free via kmalloc_nolock); falls back
+ * to kmalloc(GFP_KERNEL).  Must be called from task context.
+ */
+static inline int da_create_or_get_kmalloc(da_id_type id, monitor_target target)
+{
+	struct da_monitor_storage *mon_storage;
+
+	scoped_guard(rcu) {
+		if (da_create_storage(id, target, da_get_monitor(id, target)))
+			return 0;
+	}
+
+	/*
+	 * da_create_storage() failed because kmalloc_nolock() returned NULL.
+	 * Allocate with GFP_KERNEL outside the RCU read section: GFP_KERNEL
+	 * may sleep for memory reclaim, which is illegal while the RCU read
+	 * lock is held (preemption disabled on !PREEMPT_RT).
+	 */
+	mon_storage = kmalloc_obj(*mon_storage, GFP_KERNEL | __GFP_ZERO);
+	if (!mon_storage)
+		return -ENOMEM;
+	mon_storage->id = id;
+	mon_storage->target = target;
+
+	/*
+	 * Re-check for a concurrent insertion before linking: another
+	 * caller may have succeeded while we slept in kmalloc().
+	 * Discard our allocation and let the winner's entry stand.
+	 */
+	scoped_guard(rcu) {
+		if (da_get_monitor(id, target)) {
+			kfree(mon_storage);
+			return 0;
+		}
+		hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+	}
+	return 0;
+}
+
+/* Create the per-object storage if not already there. */
+static inline int da_create_or_get(da_id_type id, monitor_target target)
+{
+	if (da_pool.storage)
+		return da_create_or_get_pool(id, target);
+	return da_create_or_get_kmalloc(id, target);
+}
+
 /*
  * da_destroy_storage - destroy the per-object storage
  *
- * The caller is responsible to synchronise writers, either with locks or
- * implicitly. For instance, if da_destroy_storage is called at sched_exit and
- * da_create_storage can never occur after that, it's safe to call this without
- * locks.
- * This function includes an RCU read-side critical section to synchronise
- * against da_monitor_destroy().
+ * Pool mode: removes from hash and returns the slot via call_rcu().
+ * Kmalloc mode: removes from hash and frees via kfree_rcu().
+ *
+ * Includes an RCU read-side critical section to synchronise against
+ * da_monitor_destroy().
  */
 static inline void da_destroy_storage(da_id_type id)
 {
@@ -491,15 +585,17 @@ static inline void da_destroy_storage(da_id_type id)
 
 	guard(rcu)();
 	mon_storage = __da_get_mon_storage(id);
-
 	if (!mon_storage)
 		return;
 	da_monitor_reset_hook(&mon_storage->rv.da_mon);
 	hash_del_rcu(&mon_storage->node);
-	kfree_rcu(mon_storage, rcu);
+	if (da_pool.storage)
+		call_rcu(&mon_storage->rcu, da_pool_return_cb);
+	else
+		kfree_rcu(mon_storage, rcu);
 }
 
-static void da_monitor_reset_all(void)
+static __maybe_unused void da_monitor_reset_all(void)
 {
 	struct da_monitor_storage *mon_storage;
 	int bkt;
@@ -510,13 +606,65 @@ static void da_monitor_reset_all(void)
 	rcu_read_unlock();
 }
 
+/*
+ * da_monitor_init_prealloc - initialise with a pre-allocated storage pool
+ *
+ * Allocates @prealloc_count storage slots up-front so that da_create_or_get()
+ * and da_destroy_storage() never call kmalloc/kfree.  Must be called instead
+ * of da_monitor_init() for monitors that require pool mode.
+ */
+static inline int da_monitor_init_prealloc(unsigned int prealloc_count)
+{
+	hash_init(da_monitor_ht);
+
+	da_pool.storage = kcalloc(prealloc_count, sizeof(*da_pool.storage),
+				  GFP_KERNEL);
+	if (!da_pool.storage)
+		return -ENOMEM;
+
+	da_pool.free = kmalloc_array(prealloc_count, sizeof(*da_pool.free),
+				     GFP_KERNEL);
+	if (!da_pool.free) {
+		kfree(da_pool.storage);
+		da_pool.storage = NULL;
+		return -ENOMEM;
+	}
+
+	da_pool.free_top = 0;
+	for (unsigned int i = 0; i < prealloc_count; i++)
+		da_pool.free[da_pool.free_top++] = &da_pool.storage[i];
+	return 0;
+}
+
+/*
+ * da_monitor_init - initialise in kmalloc mode (no pre-allocation)
+ */
 static inline int da_monitor_init(void)
 {
 	hash_init(da_monitor_ht);
 	return 0;
 }
 
-static inline void da_monitor_destroy(void)
+static inline void da_monitor_destroy_pool(void)
+{
+	WARN_ON_ONCE(!hash_empty(da_monitor_ht));
+	/*
+	 * Wait for all in-flight da_pool_return_cb() callbacks to
+	 * complete before freeing da_pool.free.  synchronize_rcu() is
+	 * not sufficient: it only waits for callbacks registered before
+	 * it was called, but call_rcu() from concurrent da_destroy_storage()
+	 * calls may have been enqueued later.  rcu_barrier() drains every
+	 * pending callback.
+	 */
+	rcu_barrier();
+	kfree(da_pool.storage);
+	da_pool.storage = NULL;
+	kfree(da_pool.free);
+	da_pool.free = NULL;
+	da_pool.free_top = 0;
+}
+
+static inline void da_monitor_destroy_kmalloc(void)
 {
 	struct da_monitor_storage *mon_storage;
 	struct hlist_node *tmp;
@@ -534,6 +682,22 @@ static inline void da_monitor_destroy(void)
 	}
 }
 
+/*
+ * da_monitor_destroy - tear down the per-object monitor
+ *
+ * Pool mode: the hash must already be empty (caller must have drained all
+ * tasks first); calls rcu_barrier() to drain all pending da_pool_return_cb()
+ * callbacks before freeing pool arrays.
+ * Kmalloc mode: drains any remaining entries after synchronize_rcu().
+ */
+static inline void da_monitor_destroy(void)
+{
+	if (da_pool.storage)
+		da_monitor_destroy_pool();
+	else
+		da_monitor_destroy_kmalloc();
+}
+
 /*
  * Allow the per-object monitors to run allocation manually, necessary if the
  * start condition is in a context problematic for allocation (e.g. scheduling).
Signature · 1 line
2.25.1
maintainer
#11 lore raw

Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

Quoted 8 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

da_create_empty_storage() uses kmalloc_nolock(), which requires CONFIG_HAVE_ALIGNED_STRUCT_PAGE; on UML and some PREEMPT_RT configurations it always returns NULL.  Calling kmalloc from scheduler tracepoint handlers also adds unwanted latency and can fail under memory pressure.

Quoted 7 lines

Add da_monitor_init_prealloc(N) as an opt-in alternative to da_monitor_init().  It allocates N da_monitor_storage slots with GFP_KERNEL up-front and manages them on a LIFO free-stack protected by a spinlock, so da_create_or_get() never calls kmalloc on the hot path.

Monitors that do not call da_monitor_init_prealloc() are unaffected.

That's definitely a good addition, kmalloc_nolock was not that good already so I tried some way have a preallocation, though I realise it isn't really flexible.

Since you're using spinlocks, isn't that going to sleep on PREEMPT_RT?

Isn't this similar to what you'd do with a kmem_cache. That was my original idea although that uses spinlocks too.

I quickly tried an implementation like yours using mempool_create_slab_pool(prealloc_count) and mempool_alloc_preallocated() and it still explodes with my monitors, but perhaps now that tracepoints no longer disable preemption it could play well with some monitors.

The selftests with tlob seem to work just the same with this kmem_cache (up to the unrelated RCU stall). To be fair since you only allocate from the uprobe handler, you'd probably be just fine with kmalloc_nolock, but let's ignore this for now.

That said, the API is starting to get complex (well, not that it wasn't already).
We have essentially 3 ways to allocate:
* fully automatic with kmalloc_nolock
* semi-automatic with pool preallocation
* manual with direct storage preallocation

Quoted 275 lines

---
 include/rv/da_monitor.h | 208 +++++++++++++++++++++++++++++++++++-----

 1 file changed, 186 insertions(+), 22 deletions(-)
include/rv/da_monitor.h +0 -0
index d04bb3229c75..7d6f62766251 100644
@@ -433,18 +433,6 @@ static inline da_id_type da_get_id(struct da_monitor
 *da_mon)
   return container_of(da_mon, struct da_monitor_storage, rv.da_mon)->id;
  }

include/rv/da_monitor.h +0 -12
-/*
- * da_create_or_get - create the per-object storage if not already there
- *
- * This needs a lookup so should be guarded by RCU, the condition is checked
- * directly in da_create_storage()
- */
-static inline void da_create_or_get(da_id_type id, monitor_target target)
-{
- guard(rcu)();
- da_create_storage(id, target, da_get_monitor(id, target));
-}
-

 /*
  * da_fill_empty_storage - store the target in a pre-allocated storage *

include/rv/da_monitor.h +0 -0
@@ -475,15 +463,121 @@ static inline monitor_target
 da_get_target_by_id(da_id_type id)
   return mon_storage->target;
  }

include/rv/da_monitor.h +64 -0
+/*
+ * Per-object pool state.
+ *
+ * Zero-initialised by default (storage == NULL ⟹ kmalloc mode).  A monitor
+ * opts into pool mode by calling da_monitor_init_prealloc(N) instead of
+ * da_monitor_init(), which sets storage to a non-NULL kcalloc'd array.
+ *
+ * Because every field is wrapped in this struct and the struct itself is a
+ * per-TU static, each monitor that includes this header gets a completely
+ * independent pool.  A kmalloc monitor (e.g. nomiss) and a pool monitor
+ * (e.g. tlob) therefore coexist without any interference.
+ *
+ * da_pool_return_cb runs from softirq on non-PREEMPT_RT, so irqsave is
+ * required to prevent deadlock with task-context callers.  On PREEMPT_RT
+ * it runs from an rcuc kthread where spinlock_t is a sleeping lock.
+ */
+struct da_per_obj_pool {
+ struct da_monitor_storage  *storage;  /* non-NULL ⟹ pool mode */
+ struct da_monitor_storage **free;     /* kmalloc'd pointer stack */
+ unsigned int                free_top;
+ spinlock_t                  lock;
+};
+
+static struct da_per_obj_pool da_pool = {
+ .lock = __SPIN_LOCK_UNLOCKED(da_pool.lock),
+};
+
+static void da_pool_return_cb(struct rcu_head *head)
+{
+ struct da_monitor_storage *ms =
+ container_of(head, struct da_monitor_storage, rcu);
+ unsigned long flags;
+
+ spin_lock_irqsave(&da_pool.lock, flags);
+ da_pool.free[da_pool.free_top++] = ms;
+ spin_unlock_irqrestore(&da_pool.lock, flags);
+}
+
+/* Pops a slot from the pre-allocated pool; returns -ENOSPC if exhausted. */
+static inline int da_create_or_get_pool(da_id_type id, monitor_target target)
+{
+ struct da_monitor_storage *mon_storage;
+ unsigned long flags;
+
+ spin_lock_irqsave(&da_pool.lock, flags);
+ if (!da_pool.free_top) {
+ spin_unlock_irqrestore(&da_pool.lock, flags);
+ return -ENOSPC;
+ }
+ mon_storage = da_pool.free[--da_pool.free_top];
+ spin_unlock_irqrestore(&da_pool.lock, flags);
+
+ mon_storage->id = id;
+ mon_storage->target = target;
+ guard(rcu)();
+ hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+ return 0;
+}
+
+/*
+ * Tries da_create_storage() first (lock-free via kmalloc_nolock); falls back
+ * to kmalloc(GFP_KERNEL).  Must be called from task context.
+ */
+static inline int da_create_or_get_kmalloc(da_id_type id, monitor_target

target)

include/rv/da_monitor.h +43 -0
+{
+ struct da_monitor_storage *mon_storage;
+
+ scoped_guard(rcu) {
+ if (da_create_storage(id, target, da_get_monitor(id, target)))
+ return 0;
+ }
+
+ /*
+ * da_create_storage() failed because kmalloc_nolock() returned NULL.
+ * Allocate with GFP_KERNEL outside the RCU read section: GFP_KERNEL
+ * may sleep for memory reclaim, which is illegal while the RCU read
+ * lock is held (preemption disabled on !PREEMPT_RT).
+ */
+ mon_storage = kmalloc_obj(*mon_storage, GFP_KERNEL | __GFP_ZERO);
+ if (!mon_storage)
+ return -ENOMEM;
+ mon_storage->id = id;
+ mon_storage->target = target;
+
+ /*
+ * Re-check for a concurrent insertion before linking: another
+ * caller may have succeeded while we slept in kmalloc().
+ * Discard our allocation and let the winner's entry stand.
+ */
+ scoped_guard(rcu) {
+ if (da_get_monitor(id, target)) {
+ kfree(mon_storage);
+ return 0;
+ }
+ hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+ }
+ return 0;
+}
+
+/* Create the per-object storage if not already there. */
+static inline int da_create_or_get(da_id_type id, monitor_target target)
+{
+ if (da_pool.storage)
+ return da_create_or_get_pool(id, target);
+ return da_create_or_get_kmalloc(id, target);
+}
+

 /*
  * da_destroy_storage - destroy the per-object storage
  *
- * The caller is responsible to synchronise writers, either with locks or
- * implicitly. For instance, if da_destroy_storage is called at sched_exit and
- * da_create_storage can never occur after that, it's safe to call this without
- * locks.
- * This function includes an RCU read-side critical section to synchronise
- * against da_monitor_destroy().
+ * Pool mode: removes from hash and returns the slot via call_rcu().
+ * Kmalloc mode: removes from hash and frees via kfree_rcu().
+ *
+ * Includes an RCU read-side critical section to synchronise against
+ * da_monitor_destroy().
  */
 static inline void da_destroy_storage(da_id_type id)
 {

include/rv/da_monitor.h +0 -0
@@ -491,15 +585,17 @@ static inline void da_destroy_storage(da_id_type id)


  guard(rcu)();
  mon_storage = __da_get_mon_storage(id);
-
  if (!mon_storage)
  return;
  da_monitor_reset_hook(&mon_storage->rv.da_mon);
  hash_del_rcu(&mon_storage->node);
- kfree_rcu(mon_storage, rcu);
+ if (da_pool.storage)
+ call_rcu(&mon_storage->rcu, da_pool_return_cb);
+ else
+ kfree_rcu(mon_storage, rcu);
 }

include/rv/da_monitor.h +1 -1
-static void da_monitor_reset_all(void)
+static __maybe_unused void da_monitor_reset_all(void)

 {
  struct da_monitor_storage *mon_storage;
  int bkt;

include/rv/da_monitor.h +0 -0
@@ -510,13 +606,65 @@ static void da_monitor_reset_all(void)
   rcu_read_unlock();
  }


+/*
+ * da_monitor_init_prealloc - initialise with a pre-allocated storage pool
+ *
+ * Allocates @prealloc_count storage slots up-front so that da_create_or_get()
+ * and da_destroy_storage() never call kmalloc/kfree.  Must be called instead
+ * of da_monitor_init() for monitors that require pool mode.
+ */

include/rv/da_monitor.h +26 -0
+static inline int da_monitor_init_prealloc(unsigned int prealloc_count)
+{
+ hash_init(da_monitor_ht);
+
+ da_pool.storage = kcalloc(prealloc_count, sizeof(*da_pool.storage),
+   GFP_KERNEL);
+ if (!da_pool.storage)
+ return -ENOMEM;
+
+ da_pool.free = kmalloc_array(prealloc_count, sizeof(*da_pool.free),
+      GFP_KERNEL);
+ if (!da_pool.free) {
+ kfree(da_pool.storage);
+ da_pool.storage = NULL;
+ return -ENOMEM;
+ }
+
+ da_pool.free_top = 0;
+ for (unsigned int i = 0; i < prealloc_count; i++)
+ da_pool.free[da_pool.free_top++] = &da_pool.storage[i];
+ return 0;
+}
+
+/*
+ * da_monitor_init - initialise in kmalloc mode (no pre-allocation)
+ */

 static inline int da_monitor_init(void)
 {
  hash_init(da_monitor_ht);
  return 0;
 }

include/rv/da_monitor.h +20 -1
-static inline void da_monitor_destroy(void)
+static inline void da_monitor_destroy_pool(void)
+{
+ WARN_ON_ONCE(!hash_empty(da_monitor_ht));
+ /*
+ * Wait for all in-flight da_pool_return_cb() callbacks to
+ * complete before freeing da_pool.free.  synchronize_rcu() is
+ * not sufficient: it only waits for callbacks registered before
+ * it was called, but call_rcu() from concurrent da_destroy_storage()
+ * calls may have been enqueued later.  rcu_barrier() drains every
+ * pending callback.
+ */
+ rcu_barrier();
+ kfree(da_pool.storage);
+ da_pool.storage = NULL;
+ kfree(da_pool.free);
+ da_pool.free = NULL;
+ da_pool.free_top = 0;
+}
+
+static inline void da_monitor_destroy_kmalloc(void)

 {
  struct da_monitor_storage *mon_storage;
  struct hlist_node *tmp;

include/rv/da_monitor.h +0 -0
@@ -534,6 +682,22 @@ static inline void da_monitor_destroy(void)
   }
  }

include/rv/da_monitor.h +16 -0
+/*
+ * da_monitor_destroy - tear down the per-object monitor
+ *
+ * Pool mode: the hash must already be empty (caller must have drained all
+ * tasks first); calls rcu_barrier() to drain all pending da_pool_return_cb()
+ * callbacks before freeing pool arrays.
+ * Kmalloc mode: drains any remaining entries after synchronize_rcu().
+ */
+static inline void da_monitor_destroy(void)
+{
+ if (da_pool.storage)
+ da_monitor_destroy_pool();
+ else
+ da_monitor_destroy_kmalloc();
+}
+

 /*
  * Allow the per-object monitors to run allocation manually, necessary if the
  * start condition is in a context problematic for allocation (e.g. scheduling).

maintainer
#12 lore raw

Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

In reply to [email protected]
Quoted 8 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

da_create_empty_storage() uses kmalloc_nolock(), which requires CONFIG_HAVE_ALIGNED_STRUCT_PAGE; on UML and some PREEMPT_RT configurations it always returns NULL.  Calling kmalloc from scheduler tracepoint handlers also adds unwanted latency and can fail under memory pressure.

Quoted 7 lines

Add da_monitor_init_prealloc(N) as an opt-in alternative to da_monitor_init().  It allocates N da_monitor_storage slots with GFP_KERNEL up-front and manages them on a LIFO free-stack protected by a spinlock, so da_create_or_get() never calls kmalloc on the hot path.

Monitors that do not call da_monitor_init_prealloc() are unaffected.

That's definitely a good addition, kmalloc_nolock was not that good already so I tried some way have a preallocation, though I realise it isn't really flexible.

Since you're using spinlocks, isn't that going to sleep on PREEMPT_RT?

Isn't this similar to what you'd do with a kmem_cache. That was my original idea although that uses spinlocks too.

I quickly tried an implementation like yours using mempool_create_slab_pool(prealloc_count) and mempool_alloc_preallocated() and it still explodes with my monitors, but perhaps now that tracepoints no longer disable preemption it could play well with some monitors.

The selftests with tlob seem to work just the same with this kmem_cache (up to the unrelated RCU stall). To be fair since you only allocate from the uprobe handler, you'd probably be just fine with kmalloc_nolock, but let's continue with the preallocation logic.

The API is starting to get complex (well, not that it wasn't already). We have essentially 3 ways to allocate:
* fully automatic with kmalloc_nolock
* semi-automatic with pool preallocation
* manual with direct storage preallocation

We can have a macro DA_MON_ALLOCATION_STRATEGY = {DA_MON_AUTO, DA_MON_POOL, DA_MON_MANUAL} where DA_MON_POOL also requires DA_MON_POOL_SIZE to be define (force that with an #error).

Quoted 275 lines

---
 include/rv/da_monitor.h | 208 +++++++++++++++++++++++++++++++++++-----

 1 file changed, 186 insertions(+), 22 deletions(-)
include/rv/da_monitor.h +0 -0
index d04bb3229c75..7d6f62766251 100644
@@ -433,18 +433,6 @@ static inline da_id_type da_get_id(struct da_monitor
 *da_mon)
   return container_of(da_mon, struct da_monitor_storage, rv.da_mon)->id;
  }

include/rv/da_monitor.h +0 -12
-/*
- * da_create_or_get - create the per-object storage if not already there
- *
- * This needs a lookup so should be guarded by RCU, the condition is checked
- * directly in da_create_storage()
- */
-static inline void da_create_or_get(da_id_type id, monitor_target target)
-{
- guard(rcu)();
- da_create_storage(id, target, da_get_monitor(id, target));
-}
-

 /*
  * da_fill_empty_storage - store the target in a pre-allocated storage *

include/rv/da_monitor.h +0 -0
@@ -475,15 +463,121 @@ static inline monitor_target
 da_get_target_by_id(da_id_type id)
   return mon_storage->target;
  }

include/rv/da_monitor.h +64 -0
+/*
+ * Per-object pool state.
+ *
+ * Zero-initialised by default (storage == NULL ⟹ kmalloc mode).  A monitor
+ * opts into pool mode by calling da_monitor_init_prealloc(N) instead of
+ * da_monitor_init(), which sets storage to a non-NULL kcalloc'd array.
+ *
+ * Because every field is wrapped in this struct and the struct itself is a
+ * per-TU static, each monitor that includes this header gets a completely
+ * independent pool.  A kmalloc monitor (e.g. nomiss) and a pool monitor
+ * (e.g. tlob) therefore coexist without any interference.
+ *
+ * da_pool_return_cb runs from softirq on non-PREEMPT_RT, so irqsave is
+ * required to prevent deadlock with task-context callers.  On PREEMPT_RT
+ * it runs from an rcuc kthread where spinlock_t is a sleeping lock.
+ */
+struct da_per_obj_pool {
+ struct da_monitor_storage  *storage;  /* non-NULL ⟹ pool mode */
+ struct da_monitor_storage **free;     /* kmalloc'd pointer stack */
+ unsigned int                free_top;
+ spinlock_t                  lock;
+};
+
+static struct da_per_obj_pool da_pool = {
+ .lock = __SPIN_LOCK_UNLOCKED(da_pool.lock),
+};
+
+static void da_pool_return_cb(struct rcu_head *head)
+{
+ struct da_monitor_storage *ms =
+ container_of(head, struct da_monitor_storage, rcu);
+ unsigned long flags;
+
+ spin_lock_irqsave(&da_pool.lock, flags);
+ da_pool.free[da_pool.free_top++] = ms;
+ spin_unlock_irqrestore(&da_pool.lock, flags);
+}
+
+/* Pops a slot from the pre-allocated pool; returns -ENOSPC if exhausted. */
+static inline int da_create_or_get_pool(da_id_type id, monitor_target target)
+{
+ struct da_monitor_storage *mon_storage;
+ unsigned long flags;
+
+ spin_lock_irqsave(&da_pool.lock, flags);
+ if (!da_pool.free_top) {
+ spin_unlock_irqrestore(&da_pool.lock, flags);
+ return -ENOSPC;
+ }
+ mon_storage = da_pool.free[--da_pool.free_top];
+ spin_unlock_irqrestore(&da_pool.lock, flags);
+
+ mon_storage->id = id;
+ mon_storage->target = target;
+ guard(rcu)();
+ hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+ return 0;
+}
+
+/*
+ * Tries da_create_storage() first (lock-free via kmalloc_nolock); falls back
+ * to kmalloc(GFP_KERNEL).  Must be called from task context.
+ */
+static inline int da_create_or_get_kmalloc(da_id_type id, monitor_target

target)

include/rv/da_monitor.h +43 -0
+{
+ struct da_monitor_storage *mon_storage;
+
+ scoped_guard(rcu) {
+ if (da_create_storage(id, target, da_get_monitor(id, target)))
+ return 0;
+ }
+
+ /*
+ * da_create_storage() failed because kmalloc_nolock() returned NULL.
+ * Allocate with GFP_KERNEL outside the RCU read section: GFP_KERNEL
+ * may sleep for memory reclaim, which is illegal while the RCU read
+ * lock is held (preemption disabled on !PREEMPT_RT).
+ */
+ mon_storage = kmalloc_obj(*mon_storage, GFP_KERNEL | __GFP_ZERO);
+ if (!mon_storage)
+ return -ENOMEM;
+ mon_storage->id = id;
+ mon_storage->target = target;
+
+ /*
+ * Re-check for a concurrent insertion before linking: another
+ * caller may have succeeded while we slept in kmalloc().
+ * Discard our allocation and let the winner's entry stand.
+ */
+ scoped_guard(rcu) {
+ if (da_get_monitor(id, target)) {
+ kfree(mon_storage);
+ return 0;
+ }
+ hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+ }
+ return 0;
+}
+
+/* Create the per-object storage if not already there. */
+static inline int da_create_or_get(da_id_type id, monitor_target target)
+{
+ if (da_pool.storage)
+ return da_create_or_get_pool(id, target);
+ return da_create_or_get_kmalloc(id, target);
+}
+

 /*
  * da_destroy_storage - destroy the per-object storage
  *
- * The caller is responsible to synchronise writers, either with locks or
- * implicitly. For instance, if da_destroy_storage is called at sched_exit and
- * da_create_storage can never occur after that, it's safe to call this without
- * locks.
- * This function includes an RCU read-side critical section to synchronise
- * against da_monitor_destroy().
+ * Pool mode: removes from hash and returns the slot via call_rcu().
+ * Kmalloc mode: removes from hash and frees via kfree_rcu().
+ *
+ * Includes an RCU read-side critical section to synchronise against
+ * da_monitor_destroy().
  */
 static inline void da_destroy_storage(da_id_type id)
 {

include/rv/da_monitor.h +0 -0
@@ -491,15 +585,17 @@ static inline void da_destroy_storage(da_id_type id)


  guard(rcu)();
  mon_storage = __da_get_mon_storage(id);
-
  if (!mon_storage)
  return;
  da_monitor_reset_hook(&mon_storage->rv.da_mon);
  hash_del_rcu(&mon_storage->node);
- kfree_rcu(mon_storage, rcu);
+ if (da_pool.storage)
+ call_rcu(&mon_storage->rcu, da_pool_return_cb);
+ else
+ kfree_rcu(mon_storage, rcu);
 }

include/rv/da_monitor.h +1 -1
-static void da_monitor_reset_all(void)
+static __maybe_unused void da_monitor_reset_all(void)

 {
  struct da_monitor_storage *mon_storage;
  int bkt;

include/rv/da_monitor.h +0 -0
@@ -510,13 +606,65 @@ static void da_monitor_reset_all(void)
   rcu_read_unlock();
  }


+/*
+ * da_monitor_init_prealloc - initialise with a pre-allocated storage pool
+ *
+ * Allocates @prealloc_count storage slots up-front so that da_create_or_get()
+ * and da_destroy_storage() never call kmalloc/kfree.  Must be called instead
+ * of da_monitor_init() for monitors that require pool mode.
+ */

include/rv/da_monitor.h +26 -0
+static inline int da_monitor_init_prealloc(unsigned int prealloc_count)
+{
+ hash_init(da_monitor_ht);
+
+ da_pool.storage = kcalloc(prealloc_count, sizeof(*da_pool.storage),
+   GFP_KERNEL);
+ if (!da_pool.storage)
+ return -ENOMEM;
+
+ da_pool.free = kmalloc_array(prealloc_count, sizeof(*da_pool.free),
+      GFP_KERNEL);
+ if (!da_pool.free) {
+ kfree(da_pool.storage);
+ da_pool.storage = NULL;
+ return -ENOMEM;
+ }
+
+ da_pool.free_top = 0;
+ for (unsigned int i = 0; i < prealloc_count; i++)
+ da_pool.free[da_pool.free_top++] = &da_pool.storage[i];
+ return 0;
+}
+
+/*
+ * da_monitor_init - initialise in kmalloc mode (no pre-allocation)
+ */

 static inline int da_monitor_init(void)
 {
  hash_init(da_monitor_ht);
  return 0;
 }

include/rv/da_monitor.h +20 -1
-static inline void da_monitor_destroy(void)
+static inline void da_monitor_destroy_pool(void)
+{
+ WARN_ON_ONCE(!hash_empty(da_monitor_ht));
+ /*
+ * Wait for all in-flight da_pool_return_cb() callbacks to
+ * complete before freeing da_pool.free.  synchronize_rcu() is
+ * not sufficient: it only waits for callbacks registered before
+ * it was called, but call_rcu() from concurrent da_destroy_storage()
+ * calls may have been enqueued later.  rcu_barrier() drains every
+ * pending callback.
+ */
+ rcu_barrier();
+ kfree(da_pool.storage);
+ da_pool.storage = NULL;
+ kfree(da_pool.free);
+ da_pool.free = NULL;
+ da_pool.free_top = 0;
+}
+
+static inline void da_monitor_destroy_kmalloc(void)

 {
  struct da_monitor_storage *mon_storage;
  struct hlist_node *tmp;

include/rv/da_monitor.h +0 -0
@@ -534,6 +682,22 @@ static inline void da_monitor_destroy(void)
   }
  }

include/rv/da_monitor.h +16 -0
+/*
+ * da_monitor_destroy - tear down the per-object monitor
+ *
+ * Pool mode: the hash must already be empty (caller must have drained all
+ * tasks first); calls rcu_barrier() to drain all pending da_pool_return_cb()
+ * callbacks before freeing pool arrays.
+ * Kmalloc mode: drains any remaining entries after synchronize_rcu().
+ */
+static inline void da_monitor_destroy(void)
+{
+ if (da_pool.storage)
+ da_monitor_destroy_pool();
+ else
+ da_monitor_destroy_kmalloc();
+}
+

 /*
  * Allow the per-object monitors to run allocation manually, necessary if the
  * start condition is in a context problematic for allocation (e.g. scheduling).

maintainer
#13 lore raw

Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

In reply to [email protected]

Sorry for the spam, I accidentally mapped ctrl+Enter to send without confirmation and can't teach my fingers not to press it instead of ctrl+delete..

Quoted 26 lines

That's definitely a good addition, kmalloc_nolock was not that good already so I tried some way have a preallocation, though I realise it isn't really flexible.

Since you're using spinlocks, isn't that going to sleep on PREEMPT_RT?

Isn't this similar to what you'd do with a kmem_cache. That was my original idea although that uses spinlocks too.

I quickly tried an implementation like yours using mempool_create_slab_pool(prealloc_count) and mempool_alloc_preallocated() and it still explodes with my monitors, but perhaps now that tracepoints no longer disable preemption it could play well with some monitors.

The selftests with tlob seem to work just the same with this kmem_cache (up to the unrelated RCU stall). To be fair since you only allocate from the uprobe handler, you'd probably be just fine with kmalloc_nolock, but let's continue with the preallocation logic.

The API is starting to get complex (well, not that it wasn't already). We have essentially 3 ways to allocate:
* fully automatic with kmalloc_nolock
* semi-automatic with pool preallocation
* manual with direct storage preallocation

Quoted 3 lines

We can have a macro DA_MON_ALLOCATION_STRATEGY = {DA_ALLOC_AUTO, DA_ALLOC_POOL, DA_ALLOC_MANUAL} where DA_MON_POOL also requires DA_MON_POOL_SIZE to be define (force that with an #error).

Anyway, this way you probably wouldn't need to define a different init function and let everything handled more transparently.

Also you don't need to call da_create_or_get() explicitly, da_handle_start_event() should do it for you.

The only manual step required is da_create_storage() when you explicitly cannot lock when calling da_handle_start_event() (that would be DA_ALLOC_MANUAL, you don't need that).

Hope I didn't create too much confusion.

Thanks,
Gabriele

maintainer
#14 lore raw

[PATCH] Re: Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

So this is what I meant. It's quick and dirty but seems to work as far as I could test it.

I didn't change too much around to avoid confusing more, but it probably needs a refactor for the functions positions and names. Some AI can do that later after we agree on how it should look.

The main idea is (using current function names):

da_handle_start_[run_]_event() calls da_prepare_storage(), this makes sure the storage is there and usable based on the strategy:
1. da_create_storage() plain allocation with kmalloc_nolock
2. da_create_or_get_pool() get a slot from the pool
3. da_fill_empty_storage() only set the target in a storage manually allocated before

The reason why you'd need 3. is that since da_handle_start_event() is called from a tracepoint, you may in no way be able to allocate from there, then you use manually somewhere else with da_create_empty_storage() if you don't have the target and da_create_or_get() if you do (this one is misleading, we should probably simplify further).

The newly created 2. might be useful if you aren't on preempt-rt and cannot sleep but also don't want a manual allocation (beware of lock dependencies, it doesn't always work).

Now, I left your da_create_or_get_kmalloc() unwired because I don't really see the use case (you use kmalloc_nolock because you cannot lock, so if it fails you don't try a kmalloc). But if we really want to offer a possibility to allocate with GFP_KERNEL, we can make 1. more configurable.

Does this make sense to you?

Thanks,
Gabriele

---
 include/rv/da_monitor.h                  | 160 ++++++++++-------------
 kernel/trace/rv/monitors/nomiss/nomiss.c |   2 +-
 kernel/trace/rv/monitors/tlob/tlob.c     |  15 +--
 3 files changed, 74 insertions(+), 103 deletions(-)
include/rv/da_monitor.h +70 -90
index 74aa95d9a284..3b4a36245531 100644
@@ -21,6 +21,7 @@
 #include <linux/sched.h>
 #include <linux/slab.h>
 #include <linux/hashtable.h>
+#include <linux/mempool.h>
 
 /*
  * Per-cpu variables require a unique name although static in some
@@ -67,6 +68,35 @@ static struct rv_monitor rv_this;
 #define da_id_type int
 #endif
 
+#define DA_ALLOC_AUTO 0
+#define DA_ALLOC_POOL 1
+#define DA_ALLOC_MANUAL 2
+
+/*
+ * 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.
+ */
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#endif
+#ifndef DA_MON_POOL_SIZE
+#define DA_MON_POOL_SIZE 0
+#endif
+#if DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_MANUAL
+#define da_prepare_storage da_fill_empty_storage
+
+#elif DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL
+#define da_prepare_storage da_create_or_get_pool
+#if DA_MON_POOL_SIZE == 0
+#error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be non-zero"
+#endif
+
+#else
+#define da_prepare_storage da_create_storage
+#endif /* DA_MON_ALLOCATION_STRATEGY */
+
+
 static void react(enum states curr_state, enum events event)
 {
 	rv_react(&rv_this,
@@ -448,62 +478,38 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
 }
 
 /*
- * Per-object pool state.
- *
- * Zero-initialised by default (storage == NULL ⟹ kmalloc mode).  A monitor
- * opts into pool mode by calling da_monitor_init_prealloc(N) instead of
- * da_monitor_init(), which sets storage to a non-NULL kcalloc'd array.
- *
- * Because every field is wrapped in this struct and the struct itself is a
- * per-TU static, each monitor that includes this header gets a completely
- * independent pool.  A kmalloc monitor (e.g. nomiss) and a pool monitor
- * (e.g. tlob) therefore coexist without any interference.
- *
- * da_pool_return_cb runs from softirq on non-PREEMPT_RT, so irqsave is
- * required to prevent deadlock with task-context callers.  On PREEMPT_RT
- * it runs from an rcuc kthread where spinlock_t is a sleeping lock.
- */
-struct da_per_obj_pool {
-	struct da_monitor_storage  *storage;  /* non-NULL ⟹ pool mode */
-	struct da_monitor_storage **free;     /* kmalloc'd pointer stack */
-	unsigned int                free_top;
-	spinlock_t                  lock;
-};
-
-static struct da_per_obj_pool da_pool = {
-	.lock = __SPIN_LOCK_UNLOCKED(da_pool.lock),
-};
+ * Per-object pool state using kmem_cache and mempool.
+ */
+static struct kmem_cache *da_mon_cache;
+static mempool_t *da_mon_pool;
 
 static void da_pool_return_cb(struct rcu_head *head)
 {
 	struct da_monitor_storage *ms =
 		container_of(head, struct da_monitor_storage, rcu);
-	unsigned long flags;
-
-	spin_lock_irqsave(&da_pool.lock, flags);
-	da_pool.free[da_pool.free_top++] = ms;
-	spin_unlock_irqrestore(&da_pool.lock, flags);
+	mempool_free(ms, da_mon_pool);
 }
 
-/* Pops a slot from the pre-allocated pool; returns -ENOSPC if exhausted. */
-static inline int da_create_or_get_pool(da_id_type id, monitor_target target)
+/* Pops a slot from the pre-allocated pool; returns NULL if exhausted. */
+static inline struct da_monitor *da_create_or_get_pool(da_id_type id,
+						       monitor_target target,
+						       struct da_monitor *da_mon)
 {
 	struct da_monitor_storage *mon_storage;
-	unsigned long flags;
 
-	spin_lock_irqsave(&da_pool.lock, flags);
-	if (!da_pool.free_top) {
-		spin_unlock_irqrestore(&da_pool.lock, flags);
-		return -ENOSPC;
-	}
-	mon_storage = da_pool.free[--da_pool.free_top];
-	spin_unlock_irqrestore(&da_pool.lock, flags);
+	if (da_mon)
+		return da_mon;
 
+	mon_storage = mempool_alloc_preallocated(da_mon_pool);
+	if (!mon_storage)
+		return NULL;
+
+	memset(mon_storage, 0, sizeof(*mon_storage));
 	mon_storage->id = id;
 	mon_storage->target = target;
 	guard(rcu)();
 	hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
-	return 0;
+	return &mon_storage->rv.da_mon;
 }
 
 /*
@@ -547,11 +553,12 @@ static inline int da_create_or_get_kmalloc(da_id_type id, monitor_target target)
 }
 
 /* Create the per-object storage if not already there. */
-static inline int da_create_or_get(da_id_type id, monitor_target target)
+// NOTE: this is only needed for manual allocation!
+// we can refactor to have it only defined there, leaving it for now
+static inline void da_create_or_get(da_id_type id, monitor_target target)
 {
-	if (da_pool.storage)
-		return da_create_or_get_pool(id, target);
-	return da_create_or_get_kmalloc(id, target);
+	guard(rcu)();
+	da_create_storage(id, target, da_get_monitor(id, target));
 }
 
 /*
@@ -573,7 +580,7 @@ static inline void da_destroy_storage(da_id_type id)
 		return;
 	da_monitor_reset_hook(&mon_storage->rv.da_mon);
 	hash_del_rcu(&mon_storage->node);
-	if (da_pool.storage)
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
 		call_rcu(&mon_storage->rcu, da_pool_return_cb);
 	else
 		kfree_rcu(mon_storage, rcu);
@@ -591,41 +598,26 @@ static __maybe_unused void da_monitor_reset_all(void)
 }
 
 /*
- * da_monitor_init_prealloc - initialise with a pre-allocated storage pool
- *
- * Allocates @prealloc_count storage slots up-front so that da_create_or_get()
- * and da_destroy_storage() never call kmalloc/kfree.  Must be called instead
- * of da_monitor_init() for monitors that require pool mode.
+ * da_monitor_init - initialise in kmalloc mode (no pre-allocation)
  */
-static inline int da_monitor_init_prealloc(unsigned int prealloc_count)
+static inline int da_monitor_init(void)
 {
 	hash_init(da_monitor_ht);
+	if (DA_MON_ALLOCATION_STRATEGY != DA_ALLOC_POOL)
+		return 0;
 
-	da_pool.storage = kcalloc(prealloc_count, sizeof(*da_pool.storage),
-				  GFP_KERNEL);
-	if (!da_pool.storage)
+	da_mon_cache = kmem_cache_create(__stringify(DA_MON_NAME) "_cache",
+					 sizeof(struct da_monitor_storage),
+					 0, 0, NULL);
+	if (!da_mon_cache)
 		return -ENOMEM;
 
-	da_pool.free = kmalloc_array(prealloc_count, sizeof(*da_pool.free),
-				     GFP_KERNEL);
-	if (!da_pool.free) {
-		kfree(da_pool.storage);
-		da_pool.storage = NULL;
+	da_mon_pool = mempool_create_slab_pool(DA_MON_POOL_SIZE, da_mon_cache);
+	if (!da_mon_pool) {
+		kmem_cache_destroy(da_mon_cache);
+		da_mon_cache = NULL;
 		return -ENOMEM;
 	}
-
-	da_pool.free_top = 0;
-	for (unsigned int i = 0; i < prealloc_count; i++)
-		da_pool.free[da_pool.free_top++] = &da_pool.storage[i];
-	return 0;
-}
-
-/*
- * da_monitor_init - initialise in kmalloc mode (no pre-allocation)
- */
-static inline int da_monitor_init(void)
-{
-	hash_init(da_monitor_ht);
 	return 0;
 }
 
@@ -641,11 +633,10 @@ static inline void da_monitor_destroy_pool(void)
 	 * pending callback.
 	 */
 	rcu_barrier();
-	kfree(da_pool.storage);
-	da_pool.storage = NULL;
-	kfree(da_pool.free);
-	da_pool.free = NULL;
-	da_pool.free_top = 0;
+	mempool_destroy(da_mon_pool);
+	da_mon_pool = NULL;
+	kmem_cache_destroy(da_mon_cache);
+	da_mon_cache = NULL;
 }
 
 static inline void da_monitor_destroy_kmalloc(void)
@@ -676,23 +667,12 @@ static inline void da_monitor_destroy_kmalloc(void)
  */
 static inline void da_monitor_destroy(void)
 {
-	if (da_pool.storage)
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
 		da_monitor_destroy_pool();
 	else
 		da_monitor_destroy_kmalloc();
 }
 
-/*
- * 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.
- */
-#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 */
-
 #endif /* RV_MON_TYPE */
 
 #if RV_MON_TYPE == RV_MON_GLOBAL || RV_MON_TYPE == RV_MON_PER_CPU
kernel/trace/rv/monitors/nomiss/nomiss.c +1 -1
index 31f90f3638d8..f089cc0e2f10 100644
@@ -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>
kernel/trace/rv/monitors/tlob/tlob.c +3 -12
index 90e7035a0b55..486a6bac5cf9 100644
@@ -88,8 +88,8 @@ struct tlob_task_state {
 
 #define RV_MON_TYPE RV_MON_PER_OBJ
 #define HA_TIMER_TYPE HA_TIMER_HRTIMER
-/* Pool mode: da_handle_start_event uses da_fill_empty_storage, not kmalloc. */
-#define DA_SKIP_AUTO_ALLOC
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL
+#define DA_MON_POOL_SIZE TLOB_MAX_MONITORED
 
 /* Type for da_monitor_storage.target; must be defined before the includes. */
 typedef struct tlob_task_state *monitor_target;
@@ -428,7 +428,6 @@ int tlob_start_task(struct task_struct *task, u64 threshold_us)
 	struct da_monitor *da_mon;
 	struct ha_monitor *ha_mon;
 	u64 now_ns;
-	int ret;
 
 	if (!da_monitor_enabled())
 		return -ENODEV;
@@ -457,14 +456,6 @@ int tlob_start_task(struct task_struct *task, u64 threshold_us)
 	ws->last_ts = ktime_get();
 	raw_spin_lock_init(&ws->entry_lock);
 
-	/* Claim a pool slot (no kmalloc; DA_SKIP_AUTO_ALLOC + prealloc). */
-	ret = da_create_or_get(task->pid, ws);
-	if (ret) {
-		put_task_struct(task);
-		kmem_cache_free(tlob_state_cache, ws);
-		return ret;
-	}
-
 	atomic_inc(&tlob_num_monitored);
 
 	/* Hold RCU across handle + timer setup to keep da_mon valid. */
@@ -955,7 +946,7 @@ static int __tlob_init_monitor(void)
 
 	atomic_set(&tlob_num_monitored, 0);
 
-	retval = da_monitor_init_prealloc(TLOB_MAX_MONITORED);
+	retval = da_monitor_init();
 	if (retval) {
 		kmem_cache_destroy(tlob_state_cache);
 		tlob_state_cache = NULL;
Signature · 1 line
2.54.0
author
#15 lore raw

Re: [PATCH] Re: Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

Hi Gabriele,

Thanks again for the thorough review and for the design sketch. Below is a consolidated reply grouped by patch, describing what has been implemented in v3.

-- Patch 02: per-task slot ordering / ha_monitor_reset_env

Dropped from v3; v3 is rebased on top of your series [1].

[1] - https://lore.kernel.org/lkml/[email protected]

-- Patch 03: verificationtest-ktap

Quoted 2 lines

And isn't it clearer to do:
dir=$(realpath "$(dirname "$0")")

Agreed and applied:

  dir=$(realpath "$(dirname "$0")")
  export PATH="$dir:$PATH"
  "$dir/../ftrace/ftracetest" -K -v --rv "$dir"
Quoted 2 lines

Then if you really really need to call it directly, do you need to override PATH?

Yes. The ftracetest check_requires logic calls command -v <binary> to satisfy requires: <name>:program directives. Without the script's directory in PATH those checks evaluate to exit_unsupported and test cases are skipped rather than run. The make path avoids this only because make sets OUTDIR and the runner appends it to PATH internally.

-- Patch 04: pre-allocated storage pool

Quoted 1 line

Since you're using spinlocks, isn't that going to sleep on PREEMPT_RT?

User-mode uprobe handlers run with preempt_count == 0, fully preemptible on both PREEMPT_RT and non-PREEMPT_RT. The strongest evidence is in tlob
itself: tlob_start_task() takes a mutex and calls kmem_cache_zalloc(..., GFP_KERNEL) from the uprobe entry handler; both are illegal in atomic context and would trigger lockdep splats immediately.

On PREEMPT_RT, spinlock_t becoming a sleeping lock in the uprobe handler iscfine: both call sites (da_create_or_get_pool() from the handler and da_pool_return_cb() from the rcuc kthread) are in sleepable task context.

Quoted 9 lines

We can have a macro DA_MON_ALLOCATION_STRATEGY = {DA_ALLOC_AUTO, DA_ALLOC_POOL, DA_ALLOC_MANUAL} where DA_MON_POOL also requires DA_MON_POOL_SIZE to be defined (force that with an #error).

Anyway, this way you probably wouldn't need to define a different init function and let everything handled more transparently.

Also you don't need to call da_create_or_get() explicitly, da_handle_start_event() should do it for you.

Agreed on all counts. We plan to implement this in v3 as follows. The three strategies would be a compile-time selection in da_monitor.h:

  DA_ALLOC_AUTO   (default) - lock-free kmalloc_nolock on the hot path;
                              unbounded capacity.

DA_ALLOC_POOL - pre-allocated fixed-size pool;
DA_MON_POOL_SIZE
required, enforced with #error if missing.

DA_ALLOC_MANUAL - caller pre-inserts storage via
da_create_empty_storage() before the first da_handle_start_event(); the framework only links the target field.

da_monitor_init_prealloc() would be removed; da_monitor_init() would select pool or kmalloc initialisation internally based on the strategy.

da_handle_start_event() and da_handle_start_run_event() would both call da_prepare_storage() at compile time:

DA_ALLOC_AUTO -> da_create_storage() (kmalloc_nolock)
DA_ALLOC_POOL -> da_create_or_get_pool()
DA_ALLOC_MANUAL -> da_fill_empty_storage() (link target into pre-
allocated slot; no allocation on the hot path)

No explicit da_create_or_get() call would be needed in any monitor.

da_create_or_get_kmalloc() would be removed: as you noted, a caller that uses kmalloc_nolock does so because locking is forbidden; a GFP_KERNEL fallback is equally forbidden if the lockless attempt fails, so the function has no viable use case.

tlob would define:
#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL
#define DA_MON_POOL_SIZE TLOB_MAX_MONITORED

nomiss would define:
#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL

and call da_create_empty_storage() from handle_sys_enter() (the sched_setscheduler syscall path), which runs in safe task context; da_fill_empty_storage() would then link the sched_dl_entity target on the first da_handle_start_run_event() call in handle_sched_switch().

-- Patch 05: generic uprobe infrastructure

Carried unchanged into v3 (as part of the 08-b split described below).

-- Patch 06: rvgen __init arrow reset

Thanks, carried unchanged into v3.

Quoted 2 lines

Why don't you make it a separate event (e.g. "start_tlob") [...] then you also wouldn't need to call reset() and start_timer() manually.

Good suggestion. We plan to use a dedicated start_tlob event instead, with a self-loop in tlob.dot:

"running" -> "running" [ label = "start;reset(clk_elapsed)" ]

da_handle_start_run_event(task->pid, ws, start_tlob) would put the monitor into running and deliver start_tlob, which resets clk_elapsed and arms the budget hrtimer via the generated ha_setup_invariants() — no manual reset() or start_timer() calls needed.

One guard would be added in tlob's ha_setup_invariants() to make the self-loop work correctly:

  if (next_state == curr_state && event != start_tlob)
      return;

Without this, the start_tlob self-loop would be treated the same as any repeated switch_in (already running) and ha_setup_invariants() would return early, leaving the timer unarmed. Does this look right to you?

-- Patch 08: tlob monitor

--- Patch structure ---

Quoted 2 lines

Could you have everything that isn't strictly tlob-related in another patch.

Agreed. With the ioctl interface deferred (see below), v3 would keep patch 08 as the tlob monitor only:

05-b: rv: extend uprobe API with three-phase detach helpers
(rv_uprobe.c, rv_uprobe.h, rv_uprobe_detach refactoring) — extension of patch 05, independent of tlob

08: rv/tlob: add the tlob monitor itself
(tlob.c, tlob.h, tlob_trace.h, Kconfig/Makefile, Documentation,
rv_trace.h include; ha_monitor.h EVENT_NONE_LBL override bundled here as it is only needed by tlob)

The chardev infrastructure (rv_chardev.c, rv.h additions) and the UAPI header (include/uapi/linux/rv.h) would move to a follow-up series together with the ioctl self-instrumentation feature.

--- ioctl interface design ---

Quoted 6 lines

I'm not particularly fond of ioctls, they aren't that flexible and in this way I don't really see an added value.
[...] cannot the same thing be achieved using uprobes alone, e.g. by registering a function address or the current instruction pointer? [...] wouldn't a sysfs/tracefs file achieve a similar purpose without much of the boilerplate code?

Fair point. We plan to ship v3 with the tracefs/uprobe interface only and defer the ioctl (/dev/rv chardev) to a follow-up series once there is a concrete in-tree user that requires it.

The unique value of the ioctl is that TLOB_IOCTL_TRACE_STOP returns a synchronous per-call result (-EOVERFLOW or 0) to the calling thread, which neither uprobes nor tracefs writes can provide. We want to keep that option open for later, but agree it should not block the initial tlob submission.

Does this approach work for you?

What is your preference?

--- Handler simplification ---

Quoted 2 lines

Perhaps keep the handler simpler by moving this reporting to a helper function and use guard(rcu)() there.

Done. The accumulation logic is extracted into three inline helpers, each using scoped_guard(rcu) and returning bool (true if the task is monitored):

  tlob_acc_running(prev)   - accumulate running_ns on sched-out
  tlob_acc_waiting(next)   - accumulate waiting_ns on sched-in
  tlob_acc_sleeping(task)  - accumulate sleeping_ns on wakeup

handle_sched_switch() and handle_sched_wakeup() become one-liners:

  static void handle_sched_switch(...)
  {
      bool prev_preempted = (prev_state == 0);
      if (tlob_acc_running(prev))
          da_handle_event(prev->pid, NULL,
                          prev_preempted ? preempt_tlob : sleep_tlob);
      if (tlob_acc_waiting(next))
          da_handle_event(next->pid, NULL, switch_in_tlob);

}

Quoted 2 lines

You probably don't need these. da_handle_event should skip tasks without a monitor.

Agreed; the do_prev/do_next flags are gone. The helpers return false for unmonitored tasks, and da_handle_event() skips them too — both paths are no-ops for tasks with no pool entry.

--- scoped_guard(rcu) ---

Quoted 2 lines

That should be a scoped_guard(rcu), definitely use guards if you have return paths, the compiler is going to clean up (unlock) for you.

Applied to all RCU-protected sections in tlob_start_task() and tlob_stop_task(). tlob_start_task() now uses guard(mutex) for the serialised duplicate-check (replacing the explicit mutex_lock/unlock), and tlob_stop_task() uses scoped_guard(rcu) for the atomic CAS section:

  scoped_guard(rcu) {
      ws = da_get_target_by_id(task->pid);
      if (!ws)
          return -ESRCH;
      ...
      if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
          return -EAGAIN;
  }

--- tlob_stop_all removal ---

Quoted 5 lines

All this function does should be done by da_monitor_destroy. We could add a way to pass some additional deallocation for all the other cleanup you're doing on each storage. Something like a da_extra_cleanup() you can define as whatever you need and gets called in all per-obj destruction paths.

Agreed. tlob_stop_all() (~50 lines) has been removed entirely.

A da_extra_cleanup() hook macro is introduced in da_monitor.h: the default is a no-op; a monitor may override it before including the header. tlob defines:

  static inline void tlob_extra_cleanup(struct da_monitor *da_mon)
  {
      struct ha_monitor *ha_mon = to_ha_monitor(da_mon);
      struct tlob_task_state *ws = da_get_target(ha_mon);
      if (!ws)
          return;
      if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
          return;
      ha_cancel_timer_sync(ha_mon);
      atomic_dec(&tlob_num_monitored);
      put_task_struct(ws->task);
      call_rcu(&ws->rcu, tlob_free_rcu);

}
#define da_extra_cleanup tlob_extra_cleanup

da_monitor_destroy() iterates remaining entries via da_extra_cleanup + hash_del_rcu + call_rcu, then waits for all callbacks via rcu_barrier(). tlob's disable path is now simply:

  static void __tlob_destroy_monitor(void)
  {
      da_monitor_destroy();
  }

--- EVENT_NONE_LBL ---

Quoted 2 lines

Why don't you just override EVENT_NONE_LBL (and if you prefer call it MONITOR_TIMER_EVENT_NAME) without the need for another function?

Done. model_get_timer_event_name() has been removed from automata.h. In ha_monitor.h, EVENT_NONE_LBL is now overridable:

  #ifndef EVENT_NONE_LBL
  #define EVENT_NONE_LBL "none"
  #endif

tlob.c defines it before including the model header:

  #define EVENT_NONE_LBL "budget_exceeded"

The two call sites in ha_monitor.h that previously called model_get_timer_event_name() now use EVENT_NONE_LBL directly.

--- KUnit config / tristate ---

Quoted 3 lines

Do you need to add this here? Since you have a patch adding KUnit tests to tlob, cannot you put everything kunit-related there?
I couldn't build it as module.

Agreed on moving the Kconfig entry to patch 09.

The module build issue is fixed by exporting the symbols needed by the test via EXPORT_SYMBOL_IF_KUNIT (EXPORTED_FOR_KUNIT_TESTING namespace); tlob_kunit.c imports the namespace with MODULE_IMPORT_NS. We plan to keep tristate rather than changing to bool. Does that work for you?

--- detail_env_tlob tracepoint ---

Quoted 2 lines

Since you are not documenting the detail_env_tlob tracepoint, is it something really required? I would at the very least document its usage.

Fair point. detail_env_tlob emits (running_ns, waiting_ns, sleeping_ns) so the user can see which phase consumed the budget: high sleeping_ns indicates I/O latency, high waiting_ns indicates scheduler pressure, high running_ns indicates a compute overrun. Without this breakdown the user only knows the total elapsed time exceeded the threshold, not why.

--- Documentation ---

Quoted 4 lines

This is standard tracepoints usage, there's nothing about tlob we should document here.
Same here, standard RV [for enable/desc tracefs files].
And this is duplicating what mentioned above about uprobes, isn't it?

Agreed. The following have been removed:

- "Violation events" section: generic trace-cmd examples and cat-trace instructions (standard tracepoints usage).
- tracefs files: "enable (rw)" and "desc (ro)" entries (standard RV).
- tracefs files: "monitor (rw)" description condensed to one line with a cross-reference to the uprobes section above.

In their place, a new "Violation tracepoints" subsection documents both tlob-specific tracepoints with fields and a worked example:

error_env_tlob: id, state, event ("budget_exceeded"), env ("clk_elapsed")

detail_env_tlob: id, threshold_us, running_ns, waiting_ns, sleeping_ns
Use sleeping_ns to diagnose I/O latency, waiting_ns for scheduler pressure, running_ns for compute overruns.

Example:
trace-cmd record -e error_env_tlob -e detail_env_tlob & # ... run workload ...
trace-cmd report

Quoted 2 lines

Is kernel code going to use this API? RV monitors are meant to be enabled by userspace. What's the use-case here?

Agreed. The uprobe interface is driven from userspace; tlob_start_task() and tlob_stop_task() are the internal implementation functions it calls, not a public API for external kernel modules. The hypothetical kernel-module use case would be removed from the documentation; the kernel-doc block is retained for code maintainers.

Quoted 2 lines

That's probably a bit too detailed for this page. If you really want this information somewhere couldn't it stay in the code?

Agreed; moved to comments in handle_sched_switch() and handle_sched_wakeup(). The "Limitations" subsection is retained.

-- Patch 09: KUnit tests

Quoted 3 lines

What caught my eyes are tests enrolling tracepoints handlers. If you go there you're no longer doing unit testing, what's the advantage of testing the entire monitor here over doing that in selftests?

Agreed. The three suites that register tracepoint handlers or create kthreads (tlob_sched_integration, tlob_trace_output, tlob_violation_react) have been removed from KUnit and will be added to selftests in v3.

Two pure unit test suites remain in KUnit:

tlob_task_api:
Tests tlob_start_task / tlob_stop_task return values (-ENODEV, -EALREADY, -ESRCH, -EOVERFLOW, -ENOSPC, -ERANGE) via direct calls (these functions are the internal implementation used by both the uprobe and, in future, the ioctl interface).
No tracepoints, no scheduling.

tlob_uprobe_format:
Tests the uprobe line parser (tlob_parse_uprobe_line, tlob_parse_remove_line) against valid and invalid input strings. Pure string parsing; no scheduling, no tracepoints.

This also resolves the tristate-vs-bool issue: with only pure unit tests there is no dependency on sched_setscheduler_nocheck, so bool is correct.

-- Patch 10: selftests

--- PREEMPT_RT RCU stall ---

Quoted 2 lines

I run it on a VM and have it hanging at step 9 [...] rcu_preempt stall. Did you see that? Am I doing something wrong?

Thanks for reporting. The patch changed ha_monitor.h from HRTIMER_MODE_REL_HARD (the existing upstream value) to REL_SOFT; the stall appeared on PREEMPT_RT after that change. We have not fully confirmed whether REL_SOFT is the root cause — REL_SOFT defers the callback to the ktimers kthread, which could starve rcu_preempt under certain PREEMPT_RT configurations, but other factors may be involved.

We plan to revert to HRTIMER_MODE_REL_HARD at both sites in ha_monitor.h as the conservative choice:

ha_setup_timer(): HRTIMER_MODE_REL_SOFT -> HRTIMER_MODE_REL_HARD
ha_start_timer_ns(): HRTIMER_MODE_REL_SOFT -> HRTIMER_MODE_REL_HARD

Do you have more insight into the stall, or does REL_HARD resolve it on your setup?

--- Selftest structure ---

Quoted 3 lines

This should be tested together with the other monitors (enable/disable), we could at most expand those with the check_requires.
Let's focus on tlob-only features in this patch.

Agreed. In v3 we plan to drop tracefs.tc (covered by the generic rv_monitor_enable_disable.tc) and keep only the six uprobe-specific test cases under test.d/tlob/

ioctl.tc is deferred with the ioctl interface to the follow-up series. The KUnit integration tests (sched_switch accounting, budget-expiry tracepoint) would be moved to selftests as additional test cases.

--
Thanks,
Wen

Quoted 354 lines · Gabriele Monaco

So this is what I meant. It's quick and dirty but seems to work as far as I could test it.

I didn't change too much around to avoid confusing more, but it probably needs a refactor for the functions positions and names. Some AI can do that later after we agree on how it should look.

The main idea is (using current function names):

da_handle_start_[run_]_event() calls da_prepare_storage(), this makes sure the storage is there and usable based on the strategy:
1. da_create_storage() plain allocation with kmalloc_nolock
2. da_create_or_get_pool() get a slot from the pool
3. da_fill_empty_storage() only set the target in a storage manually allocated before

The reason why you'd need 3. is that since da_handle_start_event() is called from a tracepoint, you may in no way be able to allocate from there, then you use manually somewhere else with da_create_empty_storage() if you don't have the target and da_create_or_get() if you do (this one is misleading, we should probably simplify further).

The newly created 2. might be useful if you aren't on preempt-rt and cannot sleep but also don't want a manual allocation (beware of lock dependencies, it doesn't always work).

Now, I left your da_create_or_get_kmalloc() unwired because I don't really see the use case (you use kmalloc_nolock because you cannot lock, so if it fails you don't try a kmalloc). But if we really want to offer a possibility to allocate with GFP_KERNEL, we can make 1. more configurable.

Does this make sense to you?

Thanks,
Gabriele

---
  include/rv/da_monitor.h                  | 160 ++++++++++-------------
  kernel/trace/rv/monitors/nomiss/nomiss.c |   2 +-
  kernel/trace/rv/monitors/tlob/tlob.c     |  15 +--
  3 files changed, 74 insertions(+), 103 deletions(-)
include/rv/da_monitor.h +70 -90
index 74aa95d9a284..3b4a36245531 100644
@@ -21,6 +21,7 @@
  #include <linux/sched.h>
  #include <linux/slab.h>
  #include <linux/hashtable.h>
+#include <linux/mempool.h>
 
  /*
   * Per-cpu variables require a unique name although static in some
@@ -67,6 +68,35 @@ static struct rv_monitor rv_this;
  #define da_id_type int
  #endif
 
+#define DA_ALLOC_AUTO 0
+#define DA_ALLOC_POOL 1
+#define DA_ALLOC_MANUAL 2
+
+/*
+ * 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.
+ */
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#endif
+#ifndef DA_MON_POOL_SIZE
+#define DA_MON_POOL_SIZE 0
+#endif
+#if DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_MANUAL
+#define da_prepare_storage da_fill_empty_storage
+
+#elif DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL
+#define da_prepare_storage da_create_or_get_pool
+#if DA_MON_POOL_SIZE == 0
+#error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be non-zero"
+#endif
+
+#else
+#define da_prepare_storage da_create_storage
+#endif /* DA_MON_ALLOCATION_STRATEGY */
+
+
  static void react(enum states curr_state, enum events event)
  {
  	rv_react(&rv_this,
@@ -448,62 +478,38 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
  }
 
  /*
- * Per-object pool state.
- *
- * Zero-initialised by default (storage == NULL ⟹ kmalloc mode).  A monitor
- * opts into pool mode by calling da_monitor_init_prealloc(N) instead of
- * da_monitor_init(), which sets storage to a non-NULL kcalloc'd array.
- *
- * Because every field is wrapped in this struct and the struct itself is a
- * per-TU static, each monitor that includes this header gets a completely
- * independent pool.  A kmalloc monitor (e.g. nomiss) and a pool monitor
- * (e.g. tlob) therefore coexist without any interference.
- *
- * da_pool_return_cb runs from softirq on non-PREEMPT_RT, so irqsave is
- * required to prevent deadlock with task-context callers.  On PREEMPT_RT
- * it runs from an rcuc kthread where spinlock_t is a sleeping lock.
- */
-struct da_per_obj_pool {
-	struct da_monitor_storage  *storage;  /* non-NULL ⟹ pool mode */
-	struct da_monitor_storage **free;     /* kmalloc'd pointer stack */
-	unsigned int                free_top;
-	spinlock_t                  lock;
-};
-
-static struct da_per_obj_pool da_pool = {
-	.lock = __SPIN_LOCK_UNLOCKED(da_pool.lock),
-};
+ * Per-object pool state using kmem_cache and mempool.
+ */
+static struct kmem_cache *da_mon_cache;
+static mempool_t *da_mon_pool;
 
  static void da_pool_return_cb(struct rcu_head *head)
  {
  	struct da_monitor_storage *ms =
  		container_of(head, struct da_monitor_storage, rcu);
-	unsigned long flags;
-
-	spin_lock_irqsave(&da_pool.lock, flags);
-	da_pool.free[da_pool.free_top++] = ms;
-	spin_unlock_irqrestore(&da_pool.lock, flags);
+	mempool_free(ms, da_mon_pool);
  }
 
-/* Pops a slot from the pre-allocated pool; returns -ENOSPC if exhausted. */
-static inline int da_create_or_get_pool(da_id_type id, monitor_target target)
+/* Pops a slot from the pre-allocated pool; returns NULL if exhausted. */
+static inline struct da_monitor *da_create_or_get_pool(da_id_type id,
+						       monitor_target target,
+						       struct da_monitor *da_mon)
  {
  	struct da_monitor_storage *mon_storage;
-	unsigned long flags;
 
-	spin_lock_irqsave(&da_pool.lock, flags);
-	if (!da_pool.free_top) {
-		spin_unlock_irqrestore(&da_pool.lock, flags);
-		return -ENOSPC;
-	}
-	mon_storage = da_pool.free[--da_pool.free_top];
-	spin_unlock_irqrestore(&da_pool.lock, flags);
+	if (da_mon)
+		return da_mon;
 
+	mon_storage = mempool_alloc_preallocated(da_mon_pool);
+	if (!mon_storage)
+		return NULL;
+
+	memset(mon_storage, 0, sizeof(*mon_storage));
  	mon_storage->id = id;
  	mon_storage->target = target;
  	guard(rcu)();
  	hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
-	return 0;
+	return &mon_storage->rv.da_mon;
  }
 
  /*
@@ -547,11 +553,12 @@ static inline int da_create_or_get_kmalloc(da_id_type id, monitor_target target)
  }
 
  /* Create the per-object storage if not already there. */
-static inline int da_create_or_get(da_id_type id, monitor_target target)
+// NOTE: this is only needed for manual allocation!
+// we can refactor to have it only defined there, leaving it for now
+static inline void da_create_or_get(da_id_type id, monitor_target target)
  {
-	if (da_pool.storage)
-		return da_create_or_get_pool(id, target);
-	return da_create_or_get_kmalloc(id, target);
+	guard(rcu)();
+	da_create_storage(id, target, da_get_monitor(id, target));
  }
 
  /*
@@ -573,7 +580,7 @@ static inline void da_destroy_storage(da_id_type id)
  		return;
  	da_monitor_reset_hook(&mon_storage->rv.da_mon);
  	hash_del_rcu(&mon_storage->node);
-	if (da_pool.storage)
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
  		call_rcu(&mon_storage->rcu, da_pool_return_cb);
  	else
  		kfree_rcu(mon_storage, rcu);
@@ -591,41 +598,26 @@ static __maybe_unused void da_monitor_reset_all(void)
  }
 
  /*
- * da_monitor_init_prealloc - initialise with a pre-allocated storage pool
- *
- * Allocates @prealloc_count storage slots up-front so that da_create_or_get()
- * and da_destroy_storage() never call kmalloc/kfree.  Must be called instead
- * of da_monitor_init() for monitors that require pool mode.
+ * da_monitor_init - initialise in kmalloc mode (no pre-allocation)
   */
-static inline int da_monitor_init_prealloc(unsigned int prealloc_count)
+static inline int da_monitor_init(void)
  {
  	hash_init(da_monitor_ht);
+	if (DA_MON_ALLOCATION_STRATEGY != DA_ALLOC_POOL)
+		return 0;
 
-	da_pool.storage = kcalloc(prealloc_count, sizeof(*da_pool.storage),
-				  GFP_KERNEL);
-	if (!da_pool.storage)
+	da_mon_cache = kmem_cache_create(__stringify(DA_MON_NAME) "_cache",
+					 sizeof(struct da_monitor_storage),
+					 0, 0, NULL);
+	if (!da_mon_cache)
  		return -ENOMEM;
 
-	da_pool.free = kmalloc_array(prealloc_count, sizeof(*da_pool.free),
-				     GFP_KERNEL);
-	if (!da_pool.free) {
-		kfree(da_pool.storage);
-		da_pool.storage = NULL;
+	da_mon_pool = mempool_create_slab_pool(DA_MON_POOL_SIZE, da_mon_cache);
+	if (!da_mon_pool) {
+		kmem_cache_destroy(da_mon_cache);
+		da_mon_cache = NULL;
  		return -ENOMEM;
  	}
-
-	da_pool.free_top = 0;
-	for (unsigned int i = 0; i < prealloc_count; i++)
-		da_pool.free[da_pool.free_top++] = &da_pool.storage[i];
-	return 0;
-}
-
-/*
- * da_monitor_init - initialise in kmalloc mode (no pre-allocation)
- */
-static inline int da_monitor_init(void)
-{
-	hash_init(da_monitor_ht);
  	return 0;
  }
 
@@ -641,11 +633,10 @@ static inline void da_monitor_destroy_pool(void)
  	 * pending callback.
  	 */
  	rcu_barrier();
-	kfree(da_pool.storage);
-	da_pool.storage = NULL;
-	kfree(da_pool.free);
-	da_pool.free = NULL;
-	da_pool.free_top = 0;
+	mempool_destroy(da_mon_pool);
+	da_mon_pool = NULL;
+	kmem_cache_destroy(da_mon_cache);
+	da_mon_cache = NULL;
  }
 
  static inline void da_monitor_destroy_kmalloc(void)
@@ -676,23 +667,12 @@ static inline void da_monitor_destroy_kmalloc(void)
   */
  static inline void da_monitor_destroy(void)
  {
-	if (da_pool.storage)
+	if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
  		da_monitor_destroy_pool();
  	else
  		da_monitor_destroy_kmalloc();
  }
 
-/*
- * 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.
- */
-#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 */
-
  #endif /* RV_MON_TYPE */
 
  #if RV_MON_TYPE == RV_MON_GLOBAL || RV_MON_TYPE == RV_MON_PER_CPU
kernel/trace/rv/monitors/nomiss/nomiss.c +1 -1
index 31f90f3638d8..f089cc0e2f10 100644
@@ -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>
kernel/trace/rv/monitors/tlob/tlob.c +3 -12
index 90e7035a0b55..486a6bac5cf9 100644
@@ -88,8 +88,8 @@ struct tlob_task_state {
 
  #define RV_MON_TYPE RV_MON_PER_OBJ
  #define HA_TIMER_TYPE HA_TIMER_HRTIMER
-/* Pool mode: da_handle_start_event uses da_fill_empty_storage, not kmalloc. */
-#define DA_SKIP_AUTO_ALLOC
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL
+#define DA_MON_POOL_SIZE TLOB_MAX_MONITORED
 
  /* Type for da_monitor_storage.target; must be defined before the includes. */
  typedef struct tlob_task_state *monitor_target;
@@ -428,7 +428,6 @@ int tlob_start_task(struct task_struct *task, u64 threshold_us)
  	struct da_monitor *da_mon;
  	struct ha_monitor *ha_mon;
  	u64 now_ns;
-	int ret;
 
  	if (!da_monitor_enabled())
  		return -ENODEV;
@@ -457,14 +456,6 @@ int tlob_start_task(struct task_struct *task, u64 threshold_us)
  	ws->last_ts = ktime_get();
  	raw_spin_lock_init(&ws->entry_lock);
 
-	/* Claim a pool slot (no kmalloc; DA_SKIP_AUTO_ALLOC + prealloc). */
-	ret = da_create_or_get(task->pid, ws);
-	if (ret) {
-		put_task_struct(task);
-		kmem_cache_free(tlob_state_cache, ws);
-		return ret;
-	}
-
  	atomic_inc(&tlob_num_monitored);
 
  	/* Hold RCU across handle + timer setup to keep da_mon valid. */
@@ -955,7 +946,7 @@ static int __tlob_init_monitor(void)
 
  	atomic_set(&tlob_num_monitored, 0);
 
-	retval = da_monitor_init_prealloc(TLOB_MAX_MONITORED);
+	retval = da_monitor_init();
  	if (retval) {
  		kmem_cache_destroy(tlob_state_cache);
  		tlob_state_cache = NULL;
maintainer
#16 lore raw

Re: [PATCH] Re: Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

Hi Wen,

Quoted 7 lines · Wen Yang

Yes.  The ftracetest check_requires logic calls command -v <binary> to satisfy requires: <name>:program directives.  Without the script's directory in PATH those checks evaluate to exit_unsupported and test cases are skipped rather than run.  The make path avoids this only because make sets OUTDIR and the runner appends it to PATH internally.

So you're overriding PATH so the selftest's binaries can be found from the test, right?

Wouldn't it be simpler to just put the absolute paths in the tests and don't touch PATH.

If the selftests are run via makefile, it ensures the required binaries are built and available, so there's no need to go through the `requires:
<name>:program` infrastructure (that's more about what's installed on the system.

Or if you don't want anything hardcoded, you could pass the $OUTDIR from the environment and use that in scripts, whatever looks cleaner.

Does it make sense to you?

Quoted 11 lines

-- Patch 04: pre-allocated storage pool

 > Since you're using spinlocks, isn't that going to sleep on PREEMPT_RT?

User-mode uprobe handlers run with preempt_count == 0, fully preemptible on both PREEMPT_RT and non-PREEMPT_RT.  The strongest evidence is in tlob
itself: tlob_start_task() takes a mutex and calls kmem_cache_zalloc(..., GFP_KERNEL) from the uprobe entry handler; both are illegal in atomic context and would trigger lockdep splats immediately.

Quoted 4 lines

On PREEMPT_RT, spinlock_t becoming a sleeping lock in the uprobe handler
iscfine: both call sites (da_create_or_get_pool() from the handler and da_pool_return_cb() from the rcuc kthread) are in sleepable task context.

Yeah exactly, the uprobe is fine with anything (even the automatic kmalloc_nolock), but sure preallocation at least guarantees the slots are there.

Quoted 47 lines

 > We can have a macro DA_MON_ALLOCATION_STRATEGY = {DA_ALLOC_AUTO, > DA_ALLOC_POOL, DA_ALLOC_MANUAL} where DA_MON_POOL also requires > DA_MON_POOL_SIZE to be defined (force that with an #error). >
 > Anyway, this way you probably wouldn't need to define a different init > function and let everything handled more transparently.
 >
 > Also you don't need to call da_create_or_get() explicitly, > da_handle_start_event() should do it for you.

Agreed on all counts.  We plan to implement this in v3 as follows. The three strategies would be a compile-time selection in da_monitor.h:

   DA_ALLOC_AUTO   (default) - lock-free kmalloc_nolock on the hot path; unbounded capacity.

   DA_ALLOC_POOL             - pre-allocated fixed-size pool; DA_MON_POOL_SIZE
                               required, enforced with #error if missing.

   DA_ALLOC_MANUAL           - caller pre-inserts storage via da_create_empty_storage() before the first da_handle_start_event(); the framework only links the target field.

da_monitor_init_prealloc() would be removed; da_monitor_init() would select pool or kmalloc initialisation internally based on the strategy.

da_handle_start_event() and da_handle_start_run_event() would both call da_prepare_storage() at compile time:

   DA_ALLOC_AUTO   -> da_create_storage()        (kmalloc_nolock) DA_ALLOC_POOL   -> da_create_or_get_pool() DA_ALLOC_MANUAL -> da_fill_empty_storage()    (link target into pre- allocated slot; no allocation on the hot path)

No explicit da_create_or_get() call would be needed in any monitor.

da_create_or_get_kmalloc() would be removed: as you noted, a caller that uses kmalloc_nolock does so because locking is forbidden; a GFP_KERNEL fallback is equally forbidden if the lockless attempt fails, so the function has no viable use case.

tlob would define:
   #define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL #define DA_MON_POOL_SIZE            TLOB_MAX_MONITORED

Quoted 7 lines

nomiss would define:
   #define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL

and call da_create_empty_storage() from handle_sys_enter() (the sched_setscheduler syscall path), which runs in safe task context; da_fill_empty_storage() would then link the sched_dl_entity target on the first da_handle_start_run_event() call in handle_sched_switch().

Yeah good point, there's no need to make it a special path even if we have the target ready, da_handle_start_run_event() can do it just fine.

Quoted 11 lines

-- Patch 05: generic uprobe infrastructure

Carried unchanged into v3 (as part of the 08-b split described below).

-- Patch 06: rvgen __init arrow reset

Thanks, carried unchanged into v3.

Well, if you don't need reset() on the __init arrow we can drop this, right? Also it doesn't seem fully wired with the rest and requires a separate event to do handle_monitor_start(), which can be only just another handler for tlob, nothing general.

Quoted 17 lines

 > Why don't you make it a separate event (e.g. "start_tlob") [...] then > you also wouldn't need to call reset() and start_timer() manually.

Good suggestion.  We plan to use a dedicated start_tlob event instead, with a self-loop in tlob.dot:

   "running" -> "running" [ label = "start;reset(clk_elapsed)" ]

da_handle_start_run_event(task->pid, ws, start_tlob) would put the monitor into running and deliver start_tlob, which resets clk_elapsed and arms the budget hrtimer via the generated ha_setup_invariants() — no manual reset() or start_timer() calls needed.

One guard would be added in tlob's ha_setup_invariants() to make the self-loop work correctly:

Quoted 7 lines

   if (next_state == curr_state && event != start_tlob) return;

Without this, the start_tlob self-loop would be treated the same as any repeated switch_in (already running) and ha_setup_invariants() would return early, leaving the timer unarmed.  Does this look right to you?

If you just add a separate event rvgen should take care of everything, you should be able to take ha_verify_constraint() and friends as-is from the generated code.

But yeah, that's what it would end up doing.

Quoted 43 lines

-- Patch 08: tlob monitor

--- Patch structure ---

 > Could you have everything that isn't strictly tlob-related in another > patch.

Agreed.  With the ioctl interface deferred (see below), v3 would keep patch 08 as the tlob monitor only:

   05-b: rv: extend uprobe API with three-phase detach helpers (rv_uprobe.c, rv_uprobe.h, rv_uprobe_detach refactoring) — extension of patch 05, independent of tlob

   08:   rv/tlob: add the tlob monitor itself
         (tlob.c, tlob.h, tlob_trace.h, Kconfig/Makefile, Documentation, rv_trace.h include; ha_monitor.h EVENT_NONE_LBL override bundled here as it is only needed by tlob)

The chardev infrastructure (rv_chardev.c, rv.h additions) and the UAPI header (include/uapi/linux/rv.h) would move to a follow-up series together with the ioctl self-instrumentation feature.

--- ioctl interface design ---

 > I'm not particularly fond of ioctls, they aren't that flexible and in > this way I don't really see an added value.
 > [...] cannot the same thing be achieved using uprobes alone, e.g. by > registering a function address or the current instruction pointer? > [...] wouldn't a sysfs/tracefs file achieve a similar purpose without > much of the boilerplate code?

Fair point.  We plan to ship v3 with the tracefs/uprobe interface only and defer the ioctl (/dev/rv chardev) to a follow-up series once there is a concrete in-tree user that requires it.

The unique value of the ioctl is that TLOB_IOCTL_TRACE_STOP returns a synchronous per-call result (-EOVERFLOW or 0) to the calling thread, which neither uprobes nor tracefs writes can provide.  We want to keep that option open for later, but agree it should not block the initial tlob submission.

Quoted 3 lines

Does this approach work for you?

What is your preference?

Yeah looks good to me.
Ioctls are cumbersome to set up also for the user, perhaps another sysfs file in the monitor directory would keep the control entirely in tlob.c and give you roughly the same value with easier setup.

Heck we might even think of an RV reactor that does that: e.g. creates a file where reads sleep until the first reaction (-EOVERFLOW) and returns 0 in other scenarios. I'm gonna have a thought on that, but anyway I don't see why a sysfs file cannot do this.

Let's defer it for now.

Quoted 19 lines

--- Handler simplification ---

 > Perhaps keep the handler simpler by moving this reporting to a helper > function and use guard(rcu)() there.

Done.  The accumulation logic is extracted into three inline helpers, each using scoped_guard(rcu) and returning bool (true if the task is monitored):

   tlob_acc_running(prev)   - accumulate running_ns on sched-out tlob_acc_waiting(next)   - accumulate waiting_ns on sched-in tlob_acc_sleeping(task)  - accumulate sleeping_ns on wakeup

handle_sched_switch() and handle_sched_wakeup() become one-liners:

   static void handle_sched_switch(...)
   {
       bool prev_preempted = (prev_state == 0);

Quoted 6 lines

       if (tlob_acc_running(prev)) da_handle_event(prev->pid, NULL,
                           prev_preempted ? preempt_tlob : sleep_tlob); if (tlob_acc_waiting(next)) da_handle_event(next->pid, NULL, switch_in_tlob); }

Yeah sounds good.

Quoted 18 lines

 > You probably don't need these. da_handle_event should skip tasks without > a monitor.

Agreed; the do_prev/do_next flags are gone.  The helpers return false for unmonitored tasks, and da_handle_event() skips them too — both paths are no-ops for tasks with no pool entry.

--- scoped_guard(rcu) ---

 > That should be a scoped_guard(rcu), definitely use guards if you have > return paths, the compiler is going to clean up (unlock) for you.

Applied to all RCU-protected sections in tlob_start_task() and tlob_stop_task().  tlob_start_task() now uses guard(mutex) for the serialised duplicate-check (replacing the explicit mutex_lock/unlock), and tlob_stop_task() uses scoped_guard(rcu) for the atomic CAS section:

Quoted 8 lines

   scoped_guard(rcu) {
       ws = da_get_target_by_id(task->pid);
       if (!ws)
           return -ESRCH;
       ...
       if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0) return -EAGAIN;
   }

Perfect.

Quoted 20 lines

--- tlob_stop_all removal ---

 > All this function does should be done by da_monitor_destroy. We could > add a way to pass some additional deallocation for all the other cleanup > you're doing on each storage.  Something like a da_extra_cleanup() you > can define as whatever you need and gets called in all per-obj > destruction paths.

Agreed.  tlob_stop_all() (~50 lines) has been removed entirely.

A da_extra_cleanup() hook macro is introduced in da_monitor.h: the default is a no-op; a monitor may override it before including the header.  tlob defines:

   static inline void tlob_extra_cleanup(struct da_monitor *da_mon) {
       struct ha_monitor *ha_mon = to_ha_monitor(da_mon); struct tlob_task_state *ws = da_get_target(ha_mon);

Quoted 6 lines

       if (!ws)
           return;
       if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0) return;

       ha_cancel_timer_sync(ha_mon);

After my patch making timer callbacks RCU read-side critical section, you won't need that, just let the usual reset asynchronously stop the timer and put everything that needs it stopped in your RCU callback.

Of course make sure the timer was stopped before this extra cleanup, so put the macro accordingly.

I don't think da_extra_cleanup in general should be expected to sleep and call_rcu should do the heavy lifting (it may run from any tracepoint).

Anyway we can see it later after that's merged.

Quoted 6 lines

       atomic_dec(&tlob_num_monitored);
       put_task_struct(ws->task);
       call_rcu(&ws->rcu, tlob_free_rcu);
   }
   #define da_extra_cleanup tlob_extra_cleanup

Quoted 8 lines

da_monitor_destroy() iterates remaining entries via da_extra_cleanup + hash_del_rcu + call_rcu, then waits for all callbacks via rcu_barrier(). tlob's disable path is now simply:

   static void __tlob_destroy_monitor(void)
   {
       da_monitor_destroy();
   }

Looks good, let's see the full picture.

Quoted 25 lines

--- EVENT_NONE_LBL ---

 > Why don't you just override EVENT_NONE_LBL (and if you prefer call it > MONITOR_TIMER_EVENT_NAME) without the need for another function?

Done.  model_get_timer_event_name() has been removed from automata.h. In ha_monitor.h, EVENT_NONE_LBL is now overridable:

   #ifndef EVENT_NONE_LBL
   #define EVENT_NONE_LBL "none"
   #endif

tlob.c defines it before including the model header:

   #define EVENT_NONE_LBL "budget_exceeded"

The two call sites in ha_monitor.h that previously called model_get_timer_event_name() now use EVENT_NONE_LBL directly.

--- KUnit config / tristate ---

 > Do you need to add this here? Since you have a patch adding KUnit tests > to tlob, cannot you put everything kunit-related there?
 > I couldn't build it as module.

Quoted 6 lines

Agreed on moving the Kconfig entry to patch 09.

The module build issue is fixed by exporting the symbols needed by the test via EXPORT_SYMBOL_IF_KUNIT (EXPORTED_FOR_KUNIT_TESTING namespace); tlob_kunit.c imports the namespace with MODULE_IMPORT_NS.  We plan to keep tristate rather than changing to bool.  Does that work for you?

Yeah it's good as long as it works as module too.

I might have a look at making my patch module-ready, for now it just can't work but I wonder if we can do something nicer to allow it (like in your case a bunch of exports, a separate file and a standalone testcase, perhaps all wrapped in some helper).

Quoted 12 lines

--- detail_env_tlob tracepoint ---

 > Since you are not documenting the detail_env_tlob tracepoint, is it > something really required? I would at the very least document its usage.

Fair point.  detail_env_tlob emits (running_ns, waiting_ns, sleeping_ns) so the user can see which phase consumed the budget: high sleeping_ns indicates I/O latency, high waiting_ns indicates scheduler pressure, high running_ns indicates a compute overrun.  Without this breakdown the user only knows the total elapsed time exceeded the threshold, not why.

Alright, then this can go into the docs.

Quoted 21 lines

--- Documentation ---

 > This is standard tracepoints usage, there's nothing about tlob we should > document here.
 > Same here, standard RV [for enable/desc tracefs files].
 > And this is duplicating what mentioned above about uprobes, isn't it?

Agreed.  The following have been removed:

   - "Violation events" section: generic trace-cmd examples and cat-trace instructions (standard tracepoints usage).
   - tracefs files: "enable (rw)" and "desc (ro)" entries (standard RV).
   - tracefs files: "monitor (rw)" description condensed to one line with a cross-reference to the uprobes section above.

In their place, a new "Violation tracepoints" subsection documents both tlob-specific tracepoints with fields and a worked example:

   error_env_tlob: id, state, event ("budget_exceeded"), env ("clk_elapsed")

Quoted 8 lines

   detail_env_tlob: id, threshold_us, running_ns, waiting_ns, sleeping_ns Use sleeping_ns to diagnose I/O latency, waiting_ns for scheduler pressure, running_ns for compute overruns.

   Example:
     trace-cmd record -e error_env_tlob -e detail_env_tlob &

     # ... run workload ...
     trace-cmd report

Yeah sounds good, also pointing out to enable the monitor. We might think of a general way to do this kind of thing in tools/rv, although detail_env_tlob is non-standard.

Quoted 34 lines

 > Is kernel code going to use this API? RV monitors are meant to be > enabled by userspace. What's the use-case here?

Agreed.  The uprobe interface is driven from userspace; tlob_start_task() and tlob_stop_task() are the internal implementation functions it calls, not a public API for external kernel modules.  The hypothetical kernel-module use case would be removed from the documentation; the kernel-doc block is retained for code maintainers.

 > That's probably a bit too detailed for this page. If you really want > this information somewhere couldn't it stay in the code?

Agreed; moved to comments in handle_sched_switch() and handle_sched_wakeup().  The "Limitations" subsection is retained.

-- Patch 09: KUnit tests

 > What caught my eyes are tests enrolling tracepoints handlers. If you > go there you're no longer doing unit testing, what's the advantage of > testing the entire monitor here over doing that in selftests?

Agreed.  The three suites that register tracepoint handlers or create kthreads (tlob_sched_integration, tlob_trace_output, tlob_violation_react) have been removed from KUnit and will be added to selftests in v3.

Two pure unit test suites remain in KUnit:

   tlob_task_api:
     Tests tlob_start_task / tlob_stop_task return values (-ENODEV, -EALREADY, -ESRCH, -EOVERFLOW, -ENOSPC, -ERANGE) via direct calls (these functions are the internal implementation used by both the uprobe and, in future, the ioctl interface).
     No tracepoints, no scheduling.

Quoted 8 lines

   tlob_uprobe_format:
     Tests the uprobe line parser (tlob_parse_uprobe_line, tlob_parse_remove_line) against valid and invalid input strings. Pure string parsing; no scheduling, no tracepoints.

This also resolves the tristate-vs-bool issue: with only pure unit tests there is no dependency on sched_setscheduler_nocheck, so bool is correct.

Yeah looks good.

Quoted 15 lines

--  Patch 10: selftests

--- PREEMPT_RT RCU stall ---

 > I run it on a VM and have it hanging at step 9 [...] rcu_preempt stall. > Did you see that? Am I doing something wrong?

Thanks for reporting.  The patch changed ha_monitor.h from HRTIMER_MODE_REL_HARD (the existing upstream value) to REL_SOFT; the stall appeared on PREEMPT_RT after that change.  We have not fully confirmed whether REL_SOFT is the root cause — REL_SOFT defers the callback to the ktimers kthread, which could starve rcu_preempt under certain PREEMPT_RT configurations, but other factors may be involved.

Quoted 8 lines

We plan to revert to HRTIMER_MODE_REL_HARD at both sites in ha_monitor.h as the conservative choice:

   ha_setup_timer():     HRTIMER_MODE_REL_SOFT -> HRTIMER_MODE_REL_HARD ha_start_timer_ns():  HRTIMER_MODE_REL_SOFT -> HRTIMER_MODE_REL_HARD

Do you have more insight into the stall, or does REL_HARD resolve it on your setup?

Right, good point, any specific reason why you wanted REL_SOFT?

I indeed always test under PREEMPT_RT but I still see the same splat also after reverting REL_HARD..
Could you reproduce it on your setup?

My config is nothing special: what vng gives you adding PREEMPT_RT/RCU_PREEMPT and lockdep (PROVE_LOCKING/PROVE_RCU).

Quoted 7 lines

--- Selftest structure ---

 > This should be tested together with the other monitors (enable/disable), > we could at most expand those with the check_requires.
 > Let's focus on tlob-only features in this patch.

Quoted 8 lines

Agreed.  In v3 we plan to drop tracefs.tc (covered by the generic rv_monitor_enable_disable.tc) and keep only the six uprobe-specific test cases under test.d/tlob/

ioctl.tc is deferred with the ioctl interface to the follow-up series. The KUnit integration tests (sched_switch accounting, budget-expiry tracepoint) would be moved to selftests as additional test cases.

Thanks,
Gabriele

author
#17 lore raw

Re: [PATCH] Re: Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

Quoted 481 lines · Gabriele Monaco

Hi Wen,

Quoted 7 lines · Wen Yang

Yes.  The ftracetest check_requires logic calls command -v <binary> to satisfy requires: <name>:program directives.  Without the script's directory in PATH those checks evaluate to exit_unsupported and test cases are skipped rather than run.  The make path avoids this only because make sets OUTDIR and the runner appends it to PATH internally.

So you're overriding PATH so the selftest's binaries can be found from the test, right?

Wouldn't it be simpler to just put the absolute paths in the tests and don't touch PATH.

If the selftests are run via makefile, it ensures the required binaries are built and available, so there's no need to go through the `requires:
<name>:program` infrastructure (that's more about what's installed on the system.

Or if you don't want anything hardcoded, you could pass the $OUTDIR from the environment and use that in scripts, whatever looks cleaner.

Does it make sense to you?

Quoted 11 lines

-- Patch 04: pre-allocated storage pool

 > Since you're using spinlocks, isn't that going to sleep on PREEMPT_RT?

User-mode uprobe handlers run with preempt_count == 0, fully preemptible on both PREEMPT_RT and non-PREEMPT_RT.  The strongest evidence is in tlob
itself: tlob_start_task() takes a mutex and calls kmem_cache_zalloc(..., GFP_KERNEL) from the uprobe entry handler; both are illegal in atomic context and would trigger lockdep splats immediately.

Quoted 4 lines

On PREEMPT_RT, spinlock_t becoming a sleeping lock in the uprobe handler
iscfine: both call sites (da_create_or_get_pool() from the handler and da_pool_return_cb() from the rcuc kthread) are in sleepable task context.

Yeah exactly, the uprobe is fine with anything (even the automatic kmalloc_nolock), but sure preallocation at least guarantees the slots are there.

Quoted 47 lines

 > We can have a macro DA_MON_ALLOCATION_STRATEGY = {DA_ALLOC_AUTO, > DA_ALLOC_POOL, DA_ALLOC_MANUAL} where DA_MON_POOL also requires > DA_MON_POOL_SIZE to be defined (force that with an #error). >
 > Anyway, this way you probably wouldn't need to define a different init > function and let everything handled more transparently.
 >
 > Also you don't need to call da_create_or_get() explicitly, > da_handle_start_event() should do it for you.

Agreed on all counts.  We plan to implement this in v3 as follows. The three strategies would be a compile-time selection in da_monitor.h:

   DA_ALLOC_AUTO   (default) - lock-free kmalloc_nolock on the hot path; unbounded capacity.

   DA_ALLOC_POOL             - pre-allocated fixed-size pool;
DA_MON_POOL_SIZE
                               required, enforced with #error if missing.

   DA_ALLOC_MANUAL           - caller pre-inserts storage via da_create_empty_storage() before the first da_handle_start_event(); the framework only links the target field.

da_monitor_init_prealloc() would be removed; da_monitor_init() would select pool or kmalloc initialisation internally based on the strategy.

da_handle_start_event() and da_handle_start_run_event() would both call da_prepare_storage() at compile time:

   DA_ALLOC_AUTO   -> da_create_storage()        (kmalloc_nolock) DA_ALLOC_POOL   -> da_create_or_get_pool() DA_ALLOC_MANUAL -> da_fill_empty_storage()    (link target into pre- allocated slot; no allocation on the hot path)

No explicit da_create_or_get() call would be needed in any monitor.

da_create_or_get_kmalloc() would be removed: as you noted, a caller that uses kmalloc_nolock does so because locking is forbidden; a GFP_KERNEL fallback is equally forbidden if the lockless attempt fails, so the function has no viable use case.

tlob would define:
   #define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL #define DA_MON_POOL_SIZE            TLOB_MAX_MONITORED

Quoted 7 lines

nomiss would define:
   #define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL

and call da_create_empty_storage() from handle_sys_enter() (the sched_setscheduler syscall path), which runs in safe task context; da_fill_empty_storage() would then link the sched_dl_entity target on the first da_handle_start_run_event() call in handle_sched_switch().

Yeah good point, there's no need to make it a special path even if we have the target ready, da_handle_start_run_event() can do it just fine.

Quoted 11 lines

-- Patch 05: generic uprobe infrastructure

Carried unchanged into v3 (as part of the 08-b split described below).

-- Patch 06: rvgen __init arrow reset

Thanks, carried unchanged into v3.

Well, if you don't need reset() on the __init arrow we can drop this, right? Also it doesn't seem fully wired with the rest and requires a separate event to do handle_monitor_start(), which can be only just another handler for tlob, nothing general.

Quoted 17 lines

 > Why don't you make it a separate event (e.g. "start_tlob") [...] then > you also wouldn't need to call reset() and start_timer() manually.

Good suggestion.  We plan to use a dedicated start_tlob event instead, with a self-loop in tlob.dot:

   "running" -> "running" [ label = "start;reset(clk_elapsed)" ]

da_handle_start_run_event(task->pid, ws, start_tlob) would put the monitor into running and deliver start_tlob, which resets clk_elapsed and arms the budget hrtimer via the generated ha_setup_invariants() — no manual reset() or start_timer() calls needed.

One guard would be added in tlob's ha_setup_invariants() to make the self-loop work correctly:

Quoted 7 lines

   if (next_state == curr_state && event != start_tlob) return;

Without this, the start_tlob self-loop would be treated the same as any repeated switch_in (already running) and ha_setup_invariants() would return early, leaving the timer unarmed.  Does this look right to you?

If you just add a separate event rvgen should take care of everything, you should be able to take ha_verify_constraint() and friends as-is from the generated code.

But yeah, that's what it would end up doing.

Quoted 43 lines

-- Patch 08: tlob monitor

--- Patch structure ---

 > Could you have everything that isn't strictly tlob-related in another > patch.

Agreed.  With the ioctl interface deferred (see below), v3 would keep patch 08 as the tlob monitor only:

   05-b: rv: extend uprobe API with three-phase detach helpers (rv_uprobe.c, rv_uprobe.h, rv_uprobe_detach refactoring) — extension of patch 05, independent of tlob

   08:   rv/tlob: add the tlob monitor itself
         (tlob.c, tlob.h, tlob_trace.h, Kconfig/Makefile, Documentation, rv_trace.h include; ha_monitor.h EVENT_NONE_LBL override bundled here as it is only needed by tlob)

The chardev infrastructure (rv_chardev.c, rv.h additions) and the UAPI header (include/uapi/linux/rv.h) would move to a follow-up series together with the ioctl self-instrumentation feature.

--- ioctl interface design ---

 > I'm not particularly fond of ioctls, they aren't that flexible and in > this way I don't really see an added value.
 > [...] cannot the same thing be achieved using uprobes alone, e.g. by > registering a function address or the current instruction pointer? > [...] wouldn't a sysfs/tracefs file achieve a similar purpose without > much of the boilerplate code?

Fair point.  We plan to ship v3 with the tracefs/uprobe interface only and defer the ioctl (/dev/rv chardev) to a follow-up series once there is a concrete in-tree user that requires it.

The unique value of the ioctl is that TLOB_IOCTL_TRACE_STOP returns a synchronous per-call result (-EOVERFLOW or 0) to the calling thread, which neither uprobes nor tracefs writes can provide.  We want to keep that option open for later, but agree it should not block the initial tlob submission.

Quoted 3 lines

Does this approach work for you?

What is your preference?

Yeah looks good to me.
Ioctls are cumbersome to set up also for the user, perhaps another sysfs file in the monitor directory would keep the control entirely in tlob.c and give you roughly the same value with easier setup.

Heck we might even think of an RV reactor that does that: e.g. creates a file where reads sleep until the first reaction (-EOVERFLOW) and returns 0 in other scenarios. I'm gonna have a thought on that, but anyway I don't see why a sysfs file cannot do this.

Let's defer it for now.

Quoted 19 lines

--- Handler simplification ---

 > Perhaps keep the handler simpler by moving this reporting to a helper > function and use guard(rcu)() there.

Done.  The accumulation logic is extracted into three inline helpers, each using scoped_guard(rcu) and returning bool (true if the task is monitored):

   tlob_acc_running(prev)   - accumulate running_ns on sched-out tlob_acc_waiting(next)   - accumulate waiting_ns on sched-in tlob_acc_sleeping(task)  - accumulate sleeping_ns on wakeup

handle_sched_switch() and handle_sched_wakeup() become one-liners:

   static void handle_sched_switch(...)
   {
       bool prev_preempted = (prev_state == 0);

Quoted 6 lines

       if (tlob_acc_running(prev)) da_handle_event(prev->pid, NULL,
                           prev_preempted ? preempt_tlob : sleep_tlob); if (tlob_acc_waiting(next)) da_handle_event(next->pid, NULL, switch_in_tlob); }

Yeah sounds good.

Quoted 18 lines

 > You probably don't need these. da_handle_event should skip tasks without > a monitor.

Agreed; the do_prev/do_next flags are gone.  The helpers return false for unmonitored tasks, and da_handle_event() skips them too — both paths are no-ops for tasks with no pool entry.

--- scoped_guard(rcu) ---

 > That should be a scoped_guard(rcu), definitely use guards if you have > return paths, the compiler is going to clean up (unlock) for you.

Applied to all RCU-protected sections in tlob_start_task() and tlob_stop_task().  tlob_start_task() now uses guard(mutex) for the serialised duplicate-check (replacing the explicit mutex_lock/unlock), and tlob_stop_task() uses scoped_guard(rcu) for the atomic CAS section:

Quoted 8 lines

   scoped_guard(rcu) {
       ws = da_get_target_by_id(task->pid);
       if (!ws)
           return -ESRCH;
       ...
       if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0) return -EAGAIN;
   }

Perfect.

Quoted 20 lines

--- tlob_stop_all removal ---

 > All this function does should be done by da_monitor_destroy. We could > add a way to pass some additional deallocation for all the other cleanup > you're doing on each storage.  Something like a da_extra_cleanup() you > can define as whatever you need and gets called in all per-obj > destruction paths.

Agreed.  tlob_stop_all() (~50 lines) has been removed entirely.

A da_extra_cleanup() hook macro is introduced in da_monitor.h: the default is a no-op; a monitor may override it before including the header.  tlob defines:

   static inline void tlob_extra_cleanup(struct da_monitor *da_mon) {
       struct ha_monitor *ha_mon = to_ha_monitor(da_mon); struct tlob_task_state *ws = da_get_target(ha_mon);

Quoted 6 lines

       if (!ws)
           return;
       if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0) return;

       ha_cancel_timer_sync(ha_mon);

After my patch making timer callbacks RCU read-side critical section, you won't need that, just let the usual reset asynchronously stop the timer and put everything that needs it stopped in your RCU callback.

Of course make sure the timer was stopped before this extra cleanup, so put the macro accordingly.

I don't think da_extra_cleanup in general should be expected to sleep and call_rcu should do the heavy lifting (it may run from any tracepoint).

Anyway we can see it later after that's merged.

Quoted 6 lines

       atomic_dec(&tlob_num_monitored);
       put_task_struct(ws->task);
       call_rcu(&ws->rcu, tlob_free_rcu);
   }
   #define da_extra_cleanup tlob_extra_cleanup

Quoted 8 lines

da_monitor_destroy() iterates remaining entries via da_extra_cleanup + hash_del_rcu + call_rcu, then waits for all callbacks via rcu_barrier(). tlob's disable path is now simply:

   static void __tlob_destroy_monitor(void)
   {
       da_monitor_destroy();
   }

Looks good, let's see the full picture.

Quoted 25 lines

--- EVENT_NONE_LBL ---

 > Why don't you just override EVENT_NONE_LBL (and if you prefer call it > MONITOR_TIMER_EVENT_NAME) without the need for another function?

Done.  model_get_timer_event_name() has been removed from automata.h. In ha_monitor.h, EVENT_NONE_LBL is now overridable:

   #ifndef EVENT_NONE_LBL
   #define EVENT_NONE_LBL "none"
   #endif

tlob.c defines it before including the model header:

   #define EVENT_NONE_LBL "budget_exceeded"

The two call sites in ha_monitor.h that previously called model_get_timer_event_name() now use EVENT_NONE_LBL directly.

--- KUnit config / tristate ---

 > Do you need to add this here? Since you have a patch adding KUnit tests > to tlob, cannot you put everything kunit-related there?
 > I couldn't build it as module.

Quoted 6 lines

Agreed on moving the Kconfig entry to patch 09.

The module build issue is fixed by exporting the symbols needed by the test via EXPORT_SYMBOL_IF_KUNIT (EXPORTED_FOR_KUNIT_TESTING namespace); tlob_kunit.c imports the namespace with MODULE_IMPORT_NS.  We plan to keep tristate rather than changing to bool.  Does that work for you?

Yeah it's good as long as it works as module too.

I might have a look at making my patch module-ready, for now it just can't work but I wonder if we can do something nicer to allow it (like in your case a bunch of exports, a separate file and a standalone testcase, perhaps all wrapped in some helper).

Quoted 12 lines

--- detail_env_tlob tracepoint ---

 > Since you are not documenting the detail_env_tlob tracepoint, is it > something really required? I would at the very least document its usage.

Fair point.  detail_env_tlob emits (running_ns, waiting_ns, sleeping_ns) so the user can see which phase consumed the budget: high sleeping_ns indicates I/O latency, high waiting_ns indicates scheduler pressure, high running_ns indicates a compute overrun.  Without this breakdown the user only knows the total elapsed time exceeded the threshold, not why.

Alright, then this can go into the docs.

Quoted 21 lines

--- Documentation ---

 > This is standard tracepoints usage, there's nothing about tlob we should > document here.
 > Same here, standard RV [for enable/desc tracefs files].
 > And this is duplicating what mentioned above about uprobes, isn't it?

Agreed.  The following have been removed:

   - "Violation events" section: generic trace-cmd examples and cat-trace instructions (standard tracepoints usage).
   - tracefs files: "enable (rw)" and "desc (ro)" entries (standard RV).
   - tracefs files: "monitor (rw)" description condensed to one line with a cross-reference to the uprobes section above.

In their place, a new "Violation tracepoints" subsection documents both tlob-specific tracepoints with fields and a worked example:

   error_env_tlob: id, state, event ("budget_exceeded"), env ("clk_elapsed")

Quoted 8 lines

   detail_env_tlob: id, threshold_us, running_ns, waiting_ns, sleeping_ns Use sleeping_ns to diagnose I/O latency, waiting_ns for scheduler pressure, running_ns for compute overruns.

   Example:
     trace-cmd record -e error_env_tlob -e detail_env_tlob &

      # ... run workload ...
      trace-cmd report

Yeah sounds good, also pointing out to enable the monitor. We might think of a general way to do this kind of thing in tools/rv, although detail_env_tlob is non-standard.

Quoted 34 lines

 > Is kernel code going to use this API? RV monitors are meant to be > enabled by userspace. What's the use-case here?

Agreed.  The uprobe interface is driven from userspace; tlob_start_task() and tlob_stop_task() are the internal implementation functions it calls, not a public API for external kernel modules.  The hypothetical kernel-module use case would be removed from the documentation; the kernel-doc block is retained for code maintainers.

 > That's probably a bit too detailed for this page. If you really want > this information somewhere couldn't it stay in the code?

Agreed; moved to comments in handle_sched_switch() and handle_sched_wakeup().  The "Limitations" subsection is retained.

-- Patch 09: KUnit tests

 > What caught my eyes are tests enrolling tracepoints handlers. If you > go there you're no longer doing unit testing, what's the advantage of > testing the entire monitor here over doing that in selftests?

Agreed.  The three suites that register tracepoint handlers or create kthreads (tlob_sched_integration, tlob_trace_output, tlob_violation_react) have been removed from KUnit and will be added to selftests in v3.

Two pure unit test suites remain in KUnit:

   tlob_task_api:
     Tests tlob_start_task / tlob_stop_task return values (-ENODEV, -EALREADY, -ESRCH, -EOVERFLOW, -ENOSPC, -ERANGE) via direct calls (these functions are the internal implementation used by both the uprobe and, in future, the ioctl interface).
     No tracepoints, no scheduling.

Quoted 8 lines

   tlob_uprobe_format:
     Tests the uprobe line parser (tlob_parse_uprobe_line, tlob_parse_remove_line) against valid and invalid input strings. Pure string parsing; no scheduling, no tracepoints.

This also resolves the tristate-vs-bool issue: with only pure unit tests there is no dependency on sched_setscheduler_nocheck, so bool is correct.

Yeah looks good.

Quoted 15 lines

--  Patch 10: selftests

--- PREEMPT_RT RCU stall ---

 > I run it on a VM and have it hanging at step 9 [...] rcu_preempt stall. > Did you see that? Am I doing something wrong?

Thanks for reporting.  The patch changed ha_monitor.h from HRTIMER_MODE_REL_HARD (the existing upstream value) to REL_SOFT; the stall appeared on PREEMPT_RT after that change.  We have not fully confirmed whether REL_SOFT is the root cause — REL_SOFT defers the callback to the ktimers kthread, which could starve rcu_preempt under certain PREEMPT_RT configurations, but other factors may be involved.

Quoted 8 lines

We plan to revert to HRTIMER_MODE_REL_HARD at both sites in ha_monitor.h as the conservative choice:

   ha_setup_timer():     HRTIMER_MODE_REL_SOFT -> HRTIMER_MODE_REL_HARD ha_start_timer_ns():  HRTIMER_MODE_REL_SOFT -> HRTIMER_MODE_REL_HARD

Do you have more insight into the stall, or does REL_HARD resolve it on your setup?

Quoted 9 lines

Right, good point, any specific reason why you wanted REL_SOFT?

I indeed always test under PREEMPT_RT but I still see the same splat also after reverting REL_HARD..
Could you reproduce it on your setup?

My config is nothing special: what vng gives you adding PREEMPT_RT/RCU_PREEMPT and lockdep (PROVE_LOCKING/PROVE_RCU).

Hi Gabriele,

No specific reason for REL_SOFT — not intentional, reverting to REL_HARD.

Reproduced the stall on the same config (PREEMPT_RT + PROVE_LOCKING/PROVE_RCU).

Root cause is a cleanup ordering bug in uprobe_detail_waiting.tc, unrelated to REL_SOFT/REL_HARD:

  # original cleanup — wrong order 
  echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR"  # (A) 

kill "$hog_pid" # (B)

                                                             (A) 

triggers synchronize_srcu() in the kernel. But tlob_target is stuck mid-uprobe_notify_resume holding an SRCU read lock, preempted by the FIFO-99 hog -> so the reader never finishes and (B) is never reached. rcuc/0 (a kthread on PREEMPT_RT) is also starved by the hog -> RCU stall.

Fix: kill the hog first:

kill "$hog_pid"; wait "$hog_pid"

  echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 

On the PREEMPT_RT it is more reliably triggered there because rcuc/0 runs as a preemptible kthread rather than in softirq, making it easier for the hog to monopolise the CPU long enough to hit the stall.

Thank you for the thorough review and valuable suggestions. We are working through all of them and running the full test suite. We expect to post v3 within the next two days.

--
Best wishes,
Wen

Quoted 19 lines
Quoted 7 lines

--- Selftest structure ---

 > This should be tested together with the other monitors (enable/disable), > we could at most expand those with the check_requires.
 > Let's focus on tlob-only features in this patch.

Quoted 8 lines

Agreed.  In v3 we plan to drop tracefs.tc (covered by the generic rv_monitor_enable_disable.tc) and keep only the six uprobe-specific test cases under test.d/tlob/

ioctl.tc is deferred with the ioctl interface to the follow-up series. The KUnit integration tests (sched_switch accounting, budget-expiry tracepoint) would be moved to selftests as additional test cases.

Thanks,
Gabriele

maintainer
#18 lore raw

Re: [PATCH] Re: Re: [RFC PATCH v2 04/10] rv/da: add pre-allocated storage pool for per-object monitors

Hi Wen,

Quoted 16 lines · Wen Yang

Hi Gabriele,

No specific reason for REL_SOFT — not intentional, reverting to REL_HARD.

Reproduced the stall on the same config (PREEMPT_RT + PROVE_LOCKING/PROVE_RCU).

Root cause is a cleanup ordering bug in uprobe_detail_waiting.tc, unrelated to REL_SOFT/REL_HARD:

   # original cleanup — wrong order 

   echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR"  # (A)

Quoted 8 lines

   kill "$hog_pid"                                              # (B)


                                                              (A) triggers synchronize_srcu() in the kernel. But tlob_target is stuck mid-uprobe_notify_resume holding an SRCU read lock, preempted by the FIFO-99 hog -> so the reader never finishes and (B) is never reached. rcuc/0 (a kthread on PREEMPT_RT) is also starved by the hog -> RCU stall.

great you found the issue and solution. Wonder why lockdep wasn't more informative, but probably the issue was so frequent to hog that too.

Quoted 8 lines

   Fix: kill the hog first:

   kill "$hog_pid"; wait "$hog_pid"

   echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR"

Quoted 7 lines

On the PREEMPT_RT it is more reliably triggered there because rcuc/0 runs as a preemptible kthread rather than in softirq, making it easier for the hog to monopolise the CPU long enough to hit the stall.

Thank you for the thorough review and valuable suggestions. We are working through all of them and running the full test suite. We expect to post v3 within the next two days.

Alright, sounds good.

Thanks,
Gabriele

#19 lore raw

RFC PATCH v2 5/10 add generic uprobe infrastructure for RV monitors

In reply to [email protected]

From: Wen Yang <[email protected]>

Introduce rv_uprobe, a thin wrapper around uprobe_consumer providing rv_uprobe_attach_path(), rv_uprobe_attach(), and rv_uprobe_detach() for RV monitors. An opaque priv pointer is forwarded unchanged to entry/return handlers so monitors can carry per-binding state (e.g. a latency threshold) to the hot path without any global lookup.

rv_uprobe_detach() is fully synchronous (nosync + sync + path_put + kfree), closing the use-after-free window present in open-coded patterns where kfree() precedes uprobe_unregister_sync().

---
 include/rv/rv_uprobe.h      |  87 ++++++++++++++++++++
 kernel/trace/rv/Kconfig     |   4 +
 kernel/trace/rv/Makefile    |   1 +
 kernel/trace/rv/rv_uprobe.c | 153 ++++++++++++++++++++++++++++++++++++
 4 files changed, 245 insertions(+)
 create mode 100644 include/rv/rv_uprobe.h
 create mode 100644 kernel/trace/rv/rv_uprobe.c
include/rv/rv_uprobe.h new file +87 -0
new file mode 100644
index 000000000000..084cdb36a2ff
@@ -0,0 +1,87 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ *
+ */
+
+#ifndef _RV_UPROBE_H
+#define _RV_UPROBE_H
+
+#include <linux/path.h>
+#include <linux/types.h>
+
+struct pt_regs;
+
+/**
+ * struct rv_uprobe - a single uprobe registered on behalf of an RV monitor
+ *
+ * @offset:   byte offset within the ELF binary where the probe is installed
+ * @priv:     monitor-private pointer; set at attach time, never touched by
+ *            this layer; passed unchanged to entry_fn / ret_fn
+ * @path:     resolved path of the probed binary (read-only after attach);
+ *            callers may use path.dentry for identity comparisons
+ *
+ * The implementation fields (uprobe_consumer, uprobe handle, callbacks) are
+ * private to rv_uprobe.c and are not exposed here; monitors must not access
+ * them directly.
+ */
+struct rv_uprobe {
+	/* public: read-only after rv_uprobe_attach*() */
+	loff_t		 offset;
+	void		*priv;
+	struct path	 path;
+};
+
+/**
+ * rv_uprobe_attach_path - register an uprobe given an already-resolved path
+ * @path:     path of the target binary; rv_uprobe takes its own reference
+ * @offset:   byte offset within the binary
+ * @entry_fn: called on probe hit (entry); may be NULL
+ * @ret_fn:   called on function return (uretprobe); may be NULL
+ * @priv:     opaque pointer forwarded to callbacks unchanged
+ *
+ * Use this variant when the caller has already resolved the path (e.g. to
+ * register multiple probes on the same binary with a single kern_path call).
+ * The inode is derived internally via d_real_inode(), so inode and path are
+ * always consistent.
+ *
+ * Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure.
+ */
+struct rv_uprobe *rv_uprobe_attach_path(struct path *path, loff_t offset,
+	int (*entry_fn)(struct rv_uprobe *p, struct pt_regs *regs, __u64 *data),
+	int (*ret_fn)(struct rv_uprobe *p, unsigned long func,
+			struct pt_regs *regs, __u64 *data),
+	void *priv);
+
+/**
+ * rv_uprobe_attach - resolve binpath and register an uprobe
+ * @binpath:  absolute path to the target binary
+ * @offset:   byte offset within the binary
+ * @entry_fn: called on probe hit (entry); may be NULL
+ * @ret_fn:   called on function return (uretprobe); may be NULL
+ * @priv:     opaque pointer forwarded to callbacks unchanged
+ *
+ * Resolves binpath via kern_path(), then delegates to rv_uprobe_attach_path().
+ *
+ * Returns a pointer to the new rv_uprobe on success, ERR_PTR on failure.
+ */
+struct rv_uprobe *rv_uprobe_attach(const char *binpath, loff_t offset,
+	int (*entry_fn)(struct rv_uprobe *p, struct pt_regs *regs, __u64 *data),
+	int (*ret_fn)(struct rv_uprobe *p, unsigned long func,
+			struct pt_regs *regs, __u64 *data),
+	void *priv);
+
+/**
+ * rv_uprobe_detach - synchronously unregister an uprobe and free it
+ * @p:  probe to detach; may be NULL (no-op)
+ *
+ * Calls uprobe_unregister_nosync(), then uprobe_unregister_sync() to wait
+ * for any in-progress handler to finish, then releases the path reference
+ * and frees the rv_uprobe struct.  The caller's priv data is NOT freed.
+ *
+ * Safe to call from process context only (uprobe_unregister_sync() may
+ * schedule).
+ */
+void rv_uprobe_detach(struct rv_uprobe *p);
+
+#endif /* _RV_UPROBE_H */
kernel/trace/rv/Kconfig +4 -0
index 3884b14df375..e2e0033a00b9 100644
@@ -59,6 +59,10 @@ config RV_PER_TASK_MONITORS
 	  This option configures the maximum number of per-task RV monitors that can run
 	  simultaneously.
 
+config RV_UPROBE
+	bool
+	depends on RV && UPROBES
+
 source "kernel/trace/rv/monitors/wip/Kconfig"
 source "kernel/trace/rv/monitors/wwnr/Kconfig"
 
kernel/trace/rv/Makefile +1 -0
index 94498da35b37..f139b904bea3 100644
@@ -21,6 +21,7 @@ obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o
 obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o
 obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o
 # Add new monitors here
+obj-$(CONFIG_RV_UPROBE) += rv_uprobe.o
 obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
 obj-$(CONFIG_RV_REACT_PRINTK) += reactor_printk.o
 obj-$(CONFIG_RV_REACT_PANIC) += reactor_panic.o
kernel/trace/rv/rv_uprobe.c new file +153 -0

153 lines not on this page · raw message

Signature · 1 line
2.25.1
#20 lore raw

RFC PATCH v2 6/10 rvgen: support reset() on the __init arrow for global-window HA clocks

In reply to [email protected]

From: Wen Yang <[email protected]>

rvgen rejects a state invariant when its env is never reset on any state-transition edge. This prevents expressing monitors where a clock tracks the full monitoring window — reset once at object creation, active in all states.

Allow reset() annotations on the __init_STATE -> STATE arrow. automata.py adds listed envs to the new env_init_started set (and to env_stored so the HA framework allocates per-object storage). dot2k.py uses env_init_started for three purposes:

- Generate a handle_monitor_start() skeleton that resets the env and arms the timer after the caller sets up DA storage and initial state.

- Guard ha_inv_to_guard calls with !ha_monitor_env_invalid() for these
envs: a concurrent DA event between da_handle_start_event() and ha_reset_env() would otherwise store U64_MAX - BUDGET as the guard anchor, silently disabling enforcement.

- Always generate ha_verify_guards() for monitors with invariants, providing a stable extension point for future per-event guards.

Models without __init resets (e.g. stall.dot) are unaffected.

---
 tools/verification/rvgen/rvgen/automata.py |  26 ++++++
 tools/verification/rvgen/rvgen/dot2k.py    | 100 +++++++++++++++++++--
 2 files changed, 119 insertions(+), 7 deletions(-)
tools/verification/rvgen/rvgen/automata.py +26 -0
index b9f8149f7118..178a1a4ffd8a 100644
@@ -69,15 +69,41 @@ class Automata:
         self.states, self.initial_state, self.final_states = self.__get_state_variables()
         self.env_types = {}
         self.env_stored = set()
+        self.env_init_started = set()
         self.constraint_vars = set()
         self.self_loop_reset_events = set()
         self.events, self.envs = self.__get_event_variables()
+        self.__parse_init_resets()
         self.function, self.constraints = self.__create_matrix()
         self.events_start, self.events_start_run = self.__store_init_events()
         self.env_stored = sorted(self.env_stored)
+        self.env_init_started = sorted(self.env_init_started)
         self.constraint_vars = sorted(self.constraint_vars)
         self.self_loop_reset_events = sorted(self.self_loop_reset_events)
 
+    def __parse_init_resets(self) -> None:
+        """Parse reset() annotations on the __init_STATE -> STATE arrow.
+
+        Adds each listed env to env_stored (HA framework allocates per-object
+        storage) and env_init_started (ha2k generates handle_monitor_start()).
+        """
+        init_prefix = f'"{self.init_marker}'
+        for line in map(str.lstrip, self.__dot_lines):
+            if not line.startswith(init_prefix):
+                continue
+            split_line = line.split()
+            if len(split_line) < 3 or split_line[1] != "->":
+                continue
+            if "label" not in line:
+                continue
+            label = "".join(split_line[split_line.index("label") + 2:-1]).replace('"', '')
+            for part in label.split(";"):
+                reset_m = self.constraint_reset.search(part.strip())
+                if reset_m:
+                    env = reset_m["env"]
+                    self.env_stored.add(env)
+                    self.env_init_started.add(env)
+
     def __get_model_name(self) -> str:
         basename = ntpath.basename(self.__dot_path)
         if not basename.endswith(".dot") and not basename.endswith(".gv"):
tools/verification/rvgen/rvgen/dot2k.py +93 -7

131 lines not on this page · raw message

Signature · 1 line
2.25.1
maintainer
#21 lore raw

Re: [RFC PATCH v2 06/10] rvgen: support reset() on the __init arrow for global-window HA clocks

Quoted 20 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

rvgen rejects a state invariant when its env is never reset on any state-transition edge.  This prevents expressing monitors where a clock tracks the full monitoring window — reset once at object creation, active in all states.

Allow reset() annotations on the __init_STATE -> STATE arrow. automata.py adds listed envs to the new env_init_started set (and to env_stored so the HA framework allocates per-object storage).  dot2k.py uses env_init_started for three purposes:

- Generate a handle_monitor_start() skeleton that resets the env and arms the timer after the caller sets up DA storage and initial state.

- Guard ha_inv_to_guard calls with !ha_monitor_env_invalid() for these
  envs: a concurrent DA event between da_handle_start_event() and ha_reset_env() would otherwise store U64_MAX - BUDGET as the guard anchor, silently disabling enforcement.

Quoted 6 lines

- Always generate ha_verify_guards() for monitors with invariants, providing a stable extension point for future per-event guards.

Models without __init resets (e.g. stall.dot) are unaffected.

Mmh, I see that's an issue, but technically the init arrow isn't a real state transition. In your case, you have a start condition that you labelled "switch_in_tlob" although it isn't a switch in.

Why don't you make it a separate event (e.g. "start_tlob"), it seems to me you cannot really have that occur multiple times, so doing the following wouldn't harm and you wouldn't need to change how HA monitors work:

                |
                |
                v
              +-----------------------------+   start;reset(clk)
              |           running           | -------------------+
 switch_in    |      clk < BUDGET_NS()      |                    |
  +---------> |                             | <------------------+
  |           +-----------------------------+
  |             |                  |
  |             | sleep            |
  |             v                  |
  |                                |
  |          sleeping              |
  |      clk < BUDGET_NS()         |
  |                                | preempt
  |             |                  |
  |             | wakeup           |
  |             v                  |
  |                                |
  |                 waiting        |
  +----------  clk < BUDGET_NS()  <+

Then you also wouldn't need to call reset() and start_timer() manually. Isn't that feasible?

Thanks,
Gabriele

Quoted 213 lines

---
 tools/verification/rvgen/rvgen/automata.py |  26 ++++++ tools/verification/rvgen/rvgen/dot2k.py    | 100 +++++++++++++++++++--

 2 files changed, 119 insertions(+), 7 deletions(-)
tools/verification/rvgen/rvgen/automata.py +0 -0
index b9f8149f7118..178a1a4ffd8a 100644
@@ -69,15 +69,41 @@ class Automata:

         self.states, self.initial_state, self.final_states = self.__get_state_variables()
         self.env_types = {}
         self.env_stored = set()
+        self.env_init_started = set()
         self.constraint_vars = set()
         self.self_loop_reset_events = set()
         self.events, self.envs = self.__get_event_variables()
+        self.__parse_init_resets()
         self.function, self.constraints = self.__create_matrix() self.events_start, self.events_start_run = self.__store_init_events() self.env_stored = sorted(self.env_stored)
+        self.env_init_started = sorted(self.env_init_started) self.constraint_vars = sorted(self.constraint_vars) self.self_loop_reset_events = sorted(self.self_loop_reset_events)
+    def __parse_init_resets(self) -> None:
+        """Parse reset() annotations on the __init_STATE -> STATE arrow. +
+        Adds each listed env to env_stored (HA framework allocates per-object +        storage) and env_init_started (ha2k generates handle_monitor_start()).
+        """
+        init_prefix = f'"{self.init_marker}'
+        for line in map(str.lstrip, self.__dot_lines):
+            if not line.startswith(init_prefix):
+                continue
+            split_line = line.split()
+            if len(split_line) < 3 or split_line[1] != "->":
+                continue
+            if "label" not in line:
+                continue
+            label = "".join(split_line[split_line.index("label") + 2:- 1]).replace('"', '')
+            for part in label.split(";"):
+                reset_m = self.constraint_reset.search(part.strip()) +                if reset_m:
+                    env = reset_m["env"]
+                    self.env_stored.add(env)
+                    self.env_init_started.add(env)
+
     def __get_model_name(self) -> str:
         basename = ntpath.basename(self.__dot_path)
         if not basename.endswith(".dot") and not basename.endswith(".gv"):

tools/verification/rvgen/rvgen/dot2k.py +0 -0
index e6f476b903b0..e8066260c0af 100644
@@ -366,7 +366,18 @@ f"""static inline void ha_convert_inv_guard(struct
 ha_monitor *ha_mon,

             conf_g = [e for s, e in conflict_guards if s == state] if not conf_i and not conf_g:
                 continue
-            buff.append(f"\t{_else}if (curr_state == {self.states[state]}{self.enum_suffix})")
+
+            state_name = f"{self.states[state]}{self.enum_suffix}" +            env_full = self.__get_constraint_env(constr)
+            env_bare = env_full[:-len(self.enum_suffix)]
+            if env_bare in self.env_init_started:
+                # env_store is ENV_INVALID_VALUE until handle_monitor_start();
+                # skip ha_inv_to_guard during the init race window. +                cont = "\t\t " if _else else "\t    "
+                buff.append(f"\t{_else}if (curr_state == {state_name} &&") +                buff.append(f"{cont}!ha_monitor_env_invalid(ha_mon, {env_full}))")
+            else:
+                buff.append(f"\t{_else}if (curr_state == {state_name})")
             buff.append(f"\t\t{self.__start_to_conv(constr)};") _else = "else "

tools/verification/rvgen/rvgen/dot2k.py +0 -0
@@ -376,16 +387,22 @@ f"""static inline void ha_convert_inv_guard(struct
 ha_monitor *ha_mon,


     def __fill_verify_guards_func(self) -> list[str]:
         buff = []
-        if not self.guards:
+        # Always generate for monitors with invariants: stable extension +        # point for future guard conditions.
+        if not self.guards and not self.invariants:
             return []

         buff.append(
 f"""static inline bool ha_verify_guards(struct ha_monitor *ha_mon, \t\t\t\t    enum {self.enum_states_def} curr_state, enum {self.enum_events_def} event,
 \t\t\t\t    enum {self.enum_states_def} next_state, u64 time_ns)

tools/verification/rvgen/rvgen/dot2k.py +7 -3
-{{
-\tbool res = true;
-""")
+{{""")
+
+        if not self.guards:
+            buff.append("\treturn true;\n}\n")
+            return buff
+
+        buff.append("\tbool res = true;\n")


         _else = ""
         for edge, constr in sorted(self.guards.items()):

tools/verification/rvgen/rvgen/dot2k.py +0 -0
@@ -522,7 +539,7 @@ f"""static bool ha_verify_constraint(struct ha_monitor
 *ha_mon,

             buff.append("\tha_convert_inv_guard(ha_mon, curr_state, event, " "next_state, time_ns);\n")

-        if self.guards:
+        if self.guards or self.invariants:
             buff.append("\tif (!ha_verify_guards(ha_mon, curr_state, event, " "next_state, time_ns))\n\t\treturn false;\n")

tools/verification/rvgen/rvgen/dot2k.py +0 -0
@@ -599,8 +616,77 @@ f"""static bool ha_verify_constraint(struct ha_monitor
 *ha_mon,
                  buff.append("}\n")

         return buff

+    def __fill_init_start_helper(self) -> list[str]:
+        """Generate handle_monitor_start() for envs reset on the __init arrow.

tools/verification/rvgen/rvgen/dot2k.py +30 -0
+
+        env_store is invalid inside da_handle_start_event(); this helper must
+        be called after DA storage is allocated and initial state is set.
+        """
+        if not self.env_init_started:
+            return []
+
+        # Collect the ha_start_timer call for each init-started env from the
+        # first state invariant that references it.
+        timer_calls: dict[str, str] = {}
+        for env in self.env_init_started:
+            env_full = f"{env}{self.enum_suffix}"
+            for constr in self.invariants.values():
+                if env_full in constr:
+                    timer_calls[env] = constr
+                    break
+
+        buff = []
+        buff.append(
+"""/*
+ * handle_monitor_start - reset per-object clock(s) and arm the timer.
+ *
+ * env_store is invalid inside da_handle_start_event(); call this helper
+ * after allocating DA storage and setting the initial DA state.
+ *
+ * XXX: replace the placeholders with the actual logic for your monitor.
+ */""")
+
+        if self.monitor_type == "per_obj":
+            buff.append("static int handle_monitor_start(int id,

monitor_target t)")
+            buff.append("{")
+            buff.append("\tstruct ha_monitor *ha_mon;")
+            buff.append("\tu64 time_ns = ktime_get_ns();\n")
+            buff.append("\t/* XXX: allocate DA storage, e.g. da_create_or_get(id, t) */")
+            buff.append("\t/* XXX: set initial DA state, e.g. da_handle_start_event(id, t, <event>) */")
+            buff.append("\tha_mon = /* XXX: retrieve ha_monitor for (id, t) */;")
+        elif self.monitor_type == "per_task":
+            buff.append("static int handle_monitor_start(struct task_struct *p)")
+            buff.append("{")
+            buff.append("\tstruct ha_monitor *ha_mon;")
+            buff.append("\tu64 time_ns = ktime_get_ns();\n")
+            buff.append("\t/* XXX: allocate DA storage, e.g. da_create_or_get(p->pid, p) */")
+            buff.append("\t/* XXX: set initial DA state, e.g. da_handle_start_event(p->pid, p, <event>) */")
+            buff.append("\tha_mon = /* XXX: retrieve ha_monitor for p */;") +        else:
+            buff.append("static int handle_monitor_start(void)") +            buff.append("{")
+            buff.append("\tstruct ha_monitor *ha_mon;")
+            buff.append("\tu64 time_ns = ktime_get_ns();\n")
+            buff.append("\tha_mon = /* XXX: retrieve global ha_monitor */;")

tools/verification/rvgen/rvgen/dot2k.py +6 -0
+
+        buff.append("\tif (!ha_mon)")
+        buff.append("\t\treturn -ENOENT;")
+
+        for env in self.env_init_started:
+            buff.append(f"\tha_reset_env(ha_mon, {env}{self.enum_suffix},

time_ns);")
+            if env in timer_calls:
+                buff.append(f"\t{timer_calls[env]};") +            else:
+                buff.append(f"\t/* XXX: arm timer for {env} */")

tools/verification/rvgen/rvgen/dot2k.py +5 -0
+
+        buff.append("\treturn 0;")
+        buff.append("}\n")
+        return buff
+

     def _fill_hybrid_definitions(self) -> list[str]:
-        return self.__fill_hybrid_get_reset_functions() + self.__fill_constr_func()
+        return (self.__fill_hybrid_get_reset_functions() + +                self.__fill_init_start_helper() + +                self.__fill_constr_func())

     def _fill_timer_type(self) -> list:
         if self.invariants:

#22 lore raw

RFC PATCH v2 7/10 rv/tlob: add tlob model DOT file

In reply to [email protected]

From: Wen Yang <[email protected]>

Add the Graphviz DOT specification for the tlob (task latency over budget) hybrid automaton.

The model defines three states: running (initial), waiting (in the scheduler runqueue), and sleeping (blocked on a resource), with the transitions:

running --(sleep)-------> sleeping
running --(preempt)-----> waiting
sleeping --(wakeup)------> waiting
waiting --(switch_in)--> running

A single clock invariant clk_elapsed < BUDGET_NS() is active in all three states. The HA framework enforces it via a per-task hrtimer; expiry emits error_env_tlob and resets the monitor automatically.

---
 MAINTAINERS                        |  3 +++
 tools/verification/models/tlob.dot | 21 +++++++++++++++++++++
 2 files changed, 24 insertions(+)
 create mode 100644 tools/verification/models/tlob.dot
MAINTAINERS +3 -0
index 74c86cf9bc65..beb7224d08ef 100644
@@ -23317,7 +23317,10 @@ S:	Maintained
 F:	Documentation/trace/rv/
 F:	include/linux/rv.h
 F:	include/rv/
+F:	include/uapi/linux/rv.h
 F:	kernel/trace/rv/
+F:	samples/rv/
+F:	tools/testing/selftests/rv/
 F:	tools/testing/selftests/verification/
 F:	tools/verification/
 
tools/verification/models/tlob.dot new file +21 -0
new file mode 100644
index 000000000000..8421b1120e80
@@ -0,0 +1,21 @@
+digraph state_automaton {
+	center = true;
+	size = "7,11";
+	{node [shape = plaintext, style=invis, label=""] "__init_running"};
+	{node [shape = ellipse] "running"};
+	{node [shape = plaintext] "running"};
+	{node [shape = plaintext] "waiting"};
+	{node [shape = plaintext] "sleeping"};
+	"__init_running" -> "running" [ label = "reset(clk_elapsed)" ];
+	"running"  [label = "running\nclk_elapsed < BUDGET_NS()",  color = green3];
+	"waiting"  [label = "waiting\nclk_elapsed < BUDGET_NS()"];
+	"sleeping" [label = "sleeping\nclk_elapsed < BUDGET_NS()"];
+	"running"  -> "sleeping" [ label = "sleep" ];
+	"running"  -> "waiting"  [ label = "preempt" ];
+	"waiting"  -> "running"  [ label = "switch_in" ];
+	"sleeping" -> "waiting"  [ label = "wakeup" ];
+	{ rank = min ;
+		"__init_running";
+		"running";
+	}
+}
Signature · 1 line
2.25.1
#23 lore raw

RFC PATCH v2 8/10 rv/tlob: add tlob hybrid automaton monitor

In reply to [email protected]

From: Wen Yang <[email protected]>

Introduce tlob (task latency over budget), a per-task hybrid-automaton RV monitor that measures elapsed time (CLOCK_MONOTONIC) across a user-delimited code section and fires an error_env_tlob tracepoint when the elapsed time exceeds a configurable per-invocation budget.

The monitor is built on RV_MON_PER_OBJ with HA_TIMER_HRTIMER. Three states track the scheduler status of the monitored task:

running --(sleep)-------> sleeping
running --(preempt)-----> waiting
sleeping --(wakeup)------> waiting
waiting --(switch_in)--> running

A single clock invariant clk_elapsed < BUDGET_NS() is active in all three states. The budget hrtimer is rearmed on each DA transition for the remaining budget, keeping the absolute deadline fixed at start_time + BUDGET_NS.

Per-task state is stored in the DA framework's hash table keyed by task->pid. Storage is pre-allocated by tlob_start_task() with GFP_KERNEL via da_create_or_get() before the scheduler tracepoints can fire, using DA_SKIP_AUTO_ALLOC so that no kmalloc occurs on the tracepoint hot path. This avoids both the kmalloc_nolock() restriction (requires HAVE_ALIGNED_STRUCT_PAGE) and latency issues under PREEMPT_RT.

Nested monitoring is handled by nest_depth: tlob_start_task() on an already-monitored pid returns -EEXIST and increments nest_depth without disturbing the outer window; only the outermost tlob_stop_task() performs real cleanup.

Two userspace interfaces are provided. The ioctl interface exposes in-process self-instrumentation via /dev/rv with TLOB_IOCTL_TRACE_START and TLOB_IOCTL_TRACE_STOP. The uprobe interface enables external monitoring of unmodified binaries via tracefs:

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

Violations are reported via error_env_tlob (HA clock-invariant) regardless of which interface triggered them.

---
 Documentation/trace/rv/index.rst           |    1 +
 Documentation/trace/rv/monitor_tlob.rst    |  213 ++++
 include/linux/rv.h                         |   45 +
 include/rv/automata.h                      |   15 +
 include/rv/ha_monitor.h                    |   33 +-
 include/rv/rv_uprobe.h                     |   32 +
 include/uapi/linux/rv.h                    |   86 ++
 kernel/trace/rv/Kconfig                    |    2 +
 kernel/trace/rv/Makefile                   |    4 +-
 kernel/trace/rv/monitors/tlob/Kconfig      |   69 ++
 kernel/trace/rv/monitors/tlob/tlob.c       | 1307 ++++++++++++++++++++
 kernel/trace/rv/monitors/tlob/tlob.h       |  171 +++
 kernel/trace/rv/monitors/tlob/tlob_trace.h |   58 +
 kernel/trace/rv/rv.c                       |   38 +
 kernel/trace/rv/rv.h                       |    2 +
 kernel/trace/rv/rv_chardev.c               |  201 +++
 kernel/trace/rv/rv_trace.h                 |    1 +
 kernel/trace/rv/rv_uprobe.c                |   46 +-
 tools/include/uapi/linux/rv.h              |   86 ++
 19 files changed, 2400 insertions(+), 10 deletions(-)
 create mode 100644 Documentation/trace/rv/monitor_tlob.rst
 create mode 100644 include/uapi/linux/rv.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_trace.h
 create mode 100644 kernel/trace/rv/rv_chardev.c
 create mode 100644 tools/include/uapi/linux/rv.h
Documentation/trace/rv/index.rst +1 -0
index 29769f06bb0f..1501545b5f08 100644
@@ -16,5 +16,6 @@ Runtime Verification
    monitor_wwnr.rst
    monitor_sched.rst
    monitor_rtapp.rst
+   monitor_tlob.rst
    monitor_stall.rst
    monitor_deadline.rst
Documentation/trace/rv/monitor_tlob.rst new file +213 -0

213 lines not on this page · raw message

include/linux/rv.h +45 -0

57 lines not on this page · raw message

include/rv/automata.h +15 -0
index 4a4eb40cf09a..ae819638d85a 100644
@@ -41,6 +41,21 @@ static char *model_get_event_name(enum events event)
 	return RV_AUTOMATON_NAME.event_names[event];
 }
 
+/*
+ * model_get_timer_event_name - label used when the HA timer fires (no event).
+ *
+ * Monitors may define MONITOR_TIMER_EVENT_NAME before including the model
+ * header to give the timer-fired violation a semantically meaningful label
+ * (e.g. "budget_exceeded" for tlob).  Defaults to "none".
+ */
+#ifndef MONITOR_TIMER_EVENT_NAME
+#define MONITOR_TIMER_EVENT_NAME "none"
+#endif
+static inline char *model_get_timer_event_name(void)
+{
+	return MONITOR_TIMER_EVENT_NAME;
+}
+
 /*
  * model_get_initial_state - return the automaton's initial state
  */
include/rv/ha_monitor.h +29 -4
index d59507e8cb30..dfc993774089 100644
@@ -28,6 +28,7 @@ static inline void ha_monitor_init_env(struct da_monitor *da_mon);
 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 bool ha_monitor_handle_constraint(struct da_monitor *da_mon,
 					 enum states curr_state,
 					 enum events event,
@@ -35,7 +36,10 @@ static bool ha_monitor_handle_constraint(struct da_monitor *da_mon,
 					 da_id_type id);
 #define da_monitor_event_hook ha_monitor_handle_constraint
 #define da_monitor_init_hook ha_monitor_init_env
+/* Allow monitors to override da_monitor_reset_hook before including this header. */
+#ifndef da_monitor_reset_hook
 #define da_monitor_reset_hook ha_monitor_reset_env
+#endif
 
 #include <rv/da_monitor.h>
 #include <linux/seq_buf.h>
@@ -70,7 +74,7 @@ static void ha_react(enum states curr_state, enum events event, char *env)
 	rv_react(&rv_this,
 		 "rv: monitor %s does not allow event %s on state %s with env %s\n",
 		 __stringify(MONITOR_NAME),
-		 event == EVENT_NONE ? EVENT_NONE_LBL : model_get_event_name(event),
+		 event == EVENT_NONE ? model_get_timer_event_name() : model_get_event_name(event),
 		 model_get_state_name(curr_state), env);
 }
 
@@ -246,7 +250,7 @@ static inline void __ha_monitor_timer_callback(struct ha_monitor *ha_mon)
 	ha_get_env_string(&env_string, ha_mon, time_ns);
 	ha_react(curr_state, EVENT_NONE, env_string.buffer);
 	ha_trace_error_env(ha_mon, model_get_state_name(curr_state),
-			   EVENT_NONE_LBL, env_string.buffer,
+			   model_get_timer_event_name(), env_string.buffer,
 			   da_get_id(&ha_mon->da_mon));
 
 	da_monitor_reset(&ha_mon->da_mon);
@@ -412,6 +416,14 @@ static inline bool ha_cancel_timer(struct ha_monitor *ha_mon)
 {
 	return timer_delete(&ha_mon->timer);
 }
+/*
+ * ha_cancel_timer_sync - Cancel the timer, blocking until any running
+ * callback has completed.
+ */
+static inline void ha_cancel_timer_sync(struct ha_monitor *ha_mon)
+{
+	timer_delete_sync(&ha_mon->timer);
+}
 #elif HA_TIMER_TYPE == HA_TIMER_HRTIMER
 /*
  * Helper functions to handle the monitor timer.
@@ -432,12 +444,12 @@ static enum hrtimer_restart ha_monitor_timer_callback(struct hrtimer *hrtimer)
 static inline void ha_setup_timer(struct ha_monitor *ha_mon)
 {
 	hrtimer_setup(&ha_mon->hrtimer, ha_monitor_timer_callback,
-		      CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
+		      CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
 }
 static inline void ha_start_timer_ns(struct ha_monitor *ha_mon, enum envs env,
 				     u64 expire, u64 time_ns)
 {
-	int mode = HRTIMER_MODE_REL_HARD;
+	int mode = HRTIMER_MODE_REL_SOFT;
 	u64 passed = ha_invariant_passed_ns(ha_mon, env, expire, time_ns);
 
 	if (RV_MON_TYPE == RV_MON_PER_CPU)
@@ -463,6 +475,18 @@ static inline bool ha_cancel_timer(struct ha_monitor *ha_mon)
 {
 	return hrtimer_try_to_cancel(&ha_mon->hrtimer) == 1;
 }
+/*
+ * ha_cancel_timer_sync - Cancel the timer, blocking until any running
+ * callback has completed.
+ *
+ * Use in teardown paths (e.g. stop_task) where the caller must know the
+ * callback has finished before inspecting or freeing monitor state.
+ * Must not be called from atomic context or within the timer callback.
+ */
+static inline void ha_cancel_timer_sync(struct ha_monitor *ha_mon)
+{
+	hrtimer_cancel(&ha_mon->hrtimer);
+}
 #else /* HA_TIMER_NONE */
 /*
  * Start function is intentionally not defined, monitors using timers must
@@ -473,6 +497,7 @@ static inline bool ha_cancel_timer(struct ha_monitor *ha_mon)
 {
 	return false;
 }
+static inline void ha_cancel_timer_sync(struct ha_monitor *ha_mon) { }
 #endif
 
 #endif
include/rv/rv_uprobe.h +32 -0
index 084cdb36a2ff..9106c5c9275e 100644
@@ -79,9 +79,41 @@ struct rv_uprobe *rv_uprobe_attach(const char *binpath, loff_t offset,
  * for any in-progress handler to finish, then releases the path reference
  * and frees the rv_uprobe struct.  The caller's priv data is NOT freed.
  *
+ * When removing a single probe, prefer this over the three-phase API.
  * Safe to call from process context only (uprobe_unregister_sync() may
  * schedule).
  */
 void rv_uprobe_detach(struct rv_uprobe *p);
 
+/**
+ * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
+ * @p:  probe to dequeue; may be NULL (no-op)
+ *
+ * Removes the uprobe from the uprobe subsystem but does NOT wait for
+ * in-flight handlers to complete.  The caller must call rv_uprobe_sync()
+ * before calling rv_uprobe_free() on the same probe.
+ *
+ * Use this to batch multiple deregistrations before a single rv_uprobe_sync().
+ */
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete
+ *
+ * Global barrier: waits for every in-flight uprobe handler across the system
+ * to finish.  Call once after a batch of rv_uprobe_unregister_nosync() calls
+ * and before any rv_uprobe_free() call.
+ */
+void rv_uprobe_sync(void);
+
+/**
+ * rv_uprobe_free - release resources of a previously deregistered probe
+ * @p:  probe to free; may be NULL (no-op)
+ *
+ * Releases the path reference and frees the rv_uprobe struct.  Must only
+ * be called after rv_uprobe_sync() has returned.  The caller's priv data
+ * is NOT freed.
+ */
+void rv_uprobe_free(struct rv_uprobe *p);
+
 #endif /* _RV_UPROBE_H */
include/uapi/linux/rv.h new file +86 -0

86 lines not on this page · raw message

kernel/trace/rv/Kconfig +2 -0
index e2e0033a00b9..1c36939db8e5 100644
@@ -87,6 +87,8 @@ source "kernel/trace/rv/monitors/deadline/Kconfig"
 source "kernel/trace/rv/monitors/nomiss/Kconfig"
 # Add new deadline monitors here
 
+source "kernel/trace/rv/monitors/tlob/Kconfig"
+
 # Add new monitors here
 
 config RV_REACTORS
kernel/trace/rv/Makefile +3 -1
index f139b904bea3..8a5b5c84aff9 100644
@@ -2,7 +2,7 @@
 
 ccflags-y += -I $(src)		# needed for trace events
 
-obj-$(CONFIG_RV) += rv.o
+obj-$(CONFIG_RV) += rv.o rv_chardev.o
 obj-$(CONFIG_RV_MON_WIP) += monitors/wip/wip.o
 obj-$(CONFIG_RV_MON_WWNR) += monitors/wwnr/wwnr.o
 obj-$(CONFIG_RV_MON_SCHED) += monitors/sched/sched.o
@@ -17,6 +17,8 @@ obj-$(CONFIG_RV_MON_STS) += monitors/sts/sts.o
 obj-$(CONFIG_RV_MON_NRP) += monitors/nrp/nrp.o
 obj-$(CONFIG_RV_MON_SSSW) += monitors/sssw/sssw.o
 obj-$(CONFIG_RV_MON_OPID) += monitors/opid/opid.o
+obj-$(CONFIG_RV_MON_TLOB) += monitors/tlob/tlob.o
+obj-$(CONFIG_TLOB_KUNIT_TEST) += monitors/tlob/tlob_kunit.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
kernel/trace/rv/monitors/tlob/Kconfig new file +69 -0

69 lines not on this page · raw message

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

1,307 lines not on this page · raw message

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

171 lines not on this page · raw message

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

58 lines not on this page · raw message

kernel/trace/rv/rv.c +38 -0
index ee4e68102f17..a45c4763dbe5 100644
@@ -142,10 +142,17 @@
 #include <linux/module.h>
 #include <linux/init.h>
 #include <linux/slab.h>
+#include <kunit/visibility.h>
 
 #ifdef CONFIG_RV_MON_EVENTS
 #define CREATE_TRACE_POINTS
 #include <rv_trace.h>
+
+#ifdef CONFIG_RV_MON_TLOB
+EXPORT_TRACEPOINT_SYMBOL_GPL(error_tlob);
+EXPORT_TRACEPOINT_SYMBOL_GPL(event_tlob);
+EXPORT_TRACEPOINT_SYMBOL_GPL(error_env_tlob);
+#endif
 #endif
 
 #include "rv.h"
@@ -696,6 +703,33 @@ static void turn_monitoring_on(void)
 	WRITE_ONCE(monitoring_on, true);
 }
 
+#if IS_ENABLED(CONFIG_KUNIT)
+/**
+ * rv_kunit_monitoring_on - enable the global monitoring_on flag for KUnit tests.
+ *
+ * KUnit test suite_init functions must call this before initialising any
+ * monitor, mirroring the turn_monitoring_on() call in rv_init_interface().
+ * The matching rv_kunit_monitoring_off() must be called in suite_exit to
+ * restore the flag so that test suites do not interfere with each other.
+ */
+void rv_kunit_monitoring_on(void)
+{
+	turn_monitoring_on();
+}
+EXPORT_SYMBOL_IF_KUNIT(rv_kunit_monitoring_on);
+
+/**
+ * rv_kunit_monitoring_off - disable the global monitoring_on flag for KUnit tests.
+ *
+ * Must be called in suite_exit to restore global state after rv_kunit_monitoring_on().
+ */
+void rv_kunit_monitoring_off(void)
+{
+	turn_monitoring_off();
+}
+EXPORT_SYMBOL_IF_KUNIT(rv_kunit_monitoring_off);
+#endif /* CONFIG_KUNIT */
+
 static void turn_monitoring_on_with_reset(void)
 {
 	lockdep_assert_held(&rv_interface_lock);
@@ -846,6 +880,10 @@ int __init rv_init_interface(void)
 	if (retval)
 		return 1;
 
+	retval = rv_chardev_init();
+	if (retval)
+		return 1;
+
 	turn_monitoring_on();
 
 	rv_root.root_dir = no_free_ptr(root_dir);
kernel/trace/rv/rv.h +2 -0
index 2c0f51ff9d5c..82c9a2b57596 100644
@@ -31,6 +31,8 @@ int rv_enable_monitor(struct rv_monitor *mon);
 bool rv_is_container_monitor(struct rv_monitor *mon);
 bool rv_is_nested_monitor(struct rv_monitor *mon);
 
+int rv_chardev_init(void);
+
 #ifdef CONFIG_RV_REACTORS
 int reactor_populate_monitor(struct rv_monitor *mon, struct dentry *root);
 int init_rv_reactors(struct dentry *root_dir);
kernel/trace/rv/rv_chardev.c new file +201 -0

201 lines not on this page · raw message

kernel/trace/rv/rv_trace.h +1 -0
index 9622c269789c..a4bc215c1f15 100644
@@ -189,6 +189,7 @@ DECLARE_EVENT_CLASS(error_env_da_monitor_id,
 
 #include <monitors/stall/stall_trace.h>
 #include <monitors/nomiss/nomiss_trace.h>
+#include <monitors/tlob/tlob_trace.h>
 // Add new monitors based on CONFIG_HA_MON_EVENTS_ID here
 
 #endif
kernel/trace/rv/rv_uprobe.c +41 -5

62 lines not on this page · raw message

tools/include/uapi/linux/rv.h new file +86 -0

86 lines not on this page · raw message

Signature · 1 line
2.25.1
maintainer
#24 lore raw

Re: [RFC PATCH v2 08/10] rv/tlob: add tlob hybrid automaton monitor

Quoted 46 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Introduce tlob (task latency over budget), a per-task hybrid-automaton RV monitor that measures elapsed time (CLOCK_MONOTONIC) across a user-delimited code section and fires an error_env_tlob tracepoint when the elapsed time exceeds a configurable per-invocation budget.

The monitor is built on RV_MON_PER_OBJ with HA_TIMER_HRTIMER.  Three states track the scheduler status of the monitored task:

  running  --(sleep)-------> sleeping
  running  --(preempt)-----> waiting
  sleeping --(wakeup)------> waiting
  waiting  --(switch_in)--> running

A single clock invariant clk_elapsed < BUDGET_NS() is active in all three states.  The budget hrtimer is rearmed on each DA transition for the remaining budget, keeping the absolute deadline fixed at start_time + BUDGET_NS.

Per-task state is stored in the DA framework's hash table keyed by task->pid.  Storage is pre-allocated by tlob_start_task() with GFP_KERNEL via da_create_or_get() before the scheduler tracepoints can fire, using DA_SKIP_AUTO_ALLOC so that no kmalloc occurs on the tracepoint hot path.  This avoids both the kmalloc_nolock() restriction (requires HAVE_ALIGNED_STRUCT_PAGE) and latency issues under PREEMPT_RT.

Nested monitoring is handled by nest_depth: tlob_start_task() on an already-monitored pid returns -EEXIST and increments nest_depth without disturbing the outer window; only the outermost tlob_stop_task() performs real cleanup.

Two userspace interfaces are provided.  The ioctl interface exposes in-process self-instrumentation via /dev/rv with TLOB_IOCTL_TRACE_START and TLOB_IOCTL_TRACE_STOP.  The uprobe interface enables external monitoring of unmodified binaries via tracefs:

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

Violations are reported via error_env_tlob (HA clock-invariant) regardless of which interface triggered them.

---

[...]

Quoted 19 lines
include/linux/rv.h +0 -0
index 541ba404926a..1ea91bb3f1c2 100644
@@ -21,6 +21,13 @@
  #include <linux/list.h>
  #include <linux/types.h>

include/linux/rv.h +7 -0
+/* Forward declaration: poll_table is only needed by rv_chardev_ops::poll.
+ * Avoid pulling in <linux/poll.h> from rv.h — that header is included by
+ * sched.h, and poll.h → fs.h → rcupdate.h creates a header-ordering cycle
+ * with migrate_disable() on UML/non-SMP targets.
+ */
+struct poll_table_struct;
+

 /*
  * Deterministic automaton per-object variables.
  */

include/linux/rv.h int rv_register_monitor(struct rv_monitor *monitor, · line 167

Quoted 3 lines
include/linux/rv.h +0 -0
@@ -158,6 +165,44 @@ int rv_register_monitor(struct rv_monitor *monitor,
 struct rv_monitor *parent);
  int rv_get_task_monitor_slot(void);
  void rv_put_task_monitor_slot(int slot);

Could you have everything that isn't strictly tlob-related in another patch. This adds the ioctl functionality, can it stay on its own until you wire it with tlob?

[...]

Quoted 16 lines
include/rv/automata.h +0 -0
index 4a4eb40cf09a..ae819638d85a 100644
@@ -41,6 +41,21 @@ static char *model_get_event_name(enum events event)
  	return RV_AUTOMATON_NAME.event_names[event];
  }


+/*
+ * model_get_timer_event_name - label used when the HA timer fires (no event).
+ *
+ * Monitors may define MONITOR_TIMER_EVENT_NAME before including the model
+ * header to give the timer-fired violation a semantically meaningful label
+ * (e.g. "budget_exceeded" for tlob).  Defaults to "none".
+ */

include/rv/automata.h static char *model_get_event_name(enum events event) · line 53

Quoted 3 lines
include/rv/automata.h +3 -0
+#ifndef MONITOR_TIMER_EVENT_NAME
+#define MONITOR_TIMER_EVENT_NAME "none"
+#endif

Why don't you just override EVENT_NONE_LBL (and if you prefer call it MONITOR_TIMER_EVENT_NAME) without the need for another function?

Quoted 5 lines
include/rv/automata.h +5 -0
+static inline char *model_get_timer_event_name(void)
+{
+	return MONITOR_TIMER_EVENT_NAME;
+}
+

[...]

Quoted 5 lines
include/rv/rv_uprobe.h +0 -0
index 084cdb36a2ff..9106c5c9275e 100644

include/rv/rv_uprobe.h struct rv_uprobe *rv_uprobe_attach(const char *binpath, · line 87

Quoted 9 lines
include/rv/rv_uprobe.h +1 -0
@@ -79,9 +79,41 @@ struct rv_uprobe *rv_uprobe_attach(const char *binpath,
 loff_t offset,
   * for any in-progress handler to finish, then releases the path reference
   * and frees the rv_uprobe struct.  The caller's priv data is NOT freed.
   *
+ * When removing a single probe, prefer this over the three-phase API.
   * Safe to call from process context only (uprobe_unregister_sync() may
   * schedule).
   */
  void rv_uprobe_detach(struct rv_uprobe *p);

Why don't you put all this in the patch about uprobes?

Quoted 40 lines


+/**
+ * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
+ * @p:  probe to dequeue; may be NULL (no-op)
+ *
+ * Removes the uprobe from the uprobe subsystem but does NOT wait for
+ * in-flight handlers to complete.  The caller must call rv_uprobe_sync()
+ * before calling rv_uprobe_free() on the same probe.
+ *
+ * Use this to batch multiple deregistrations before a single rv_uprobe_sync().
+ */

include/rv/rv_uprobe.h +21 -0
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete
+ *
+ * Global barrier: waits for every in-flight uprobe handler across the system
+ * to finish.  Call once after a batch of rv_uprobe_unregister_nosync() calls
+ * and before any rv_uprobe_free() call.
+ */
+void rv_uprobe_sync(void);
+
+/**
+ * rv_uprobe_free - release resources of a previously deregistered probe
+ * @p:  probe to free; may be NULL (no-op)
+ *
+ * Releases the path reference and frees the rv_uprobe struct.  Must only
+ * be called after rv_uprobe_sync() has returned.  The caller's priv data
+ * is NOT freed.
+ */
+void rv_uprobe_free(struct rv_uprobe *p);
+

 #endif /* _RV_UPROBE_H */

include/uapi/linux/rv.h new file +0 -0
new file mode 100644
index 000000000000..a34e5426393b

include/uapi/linux/rv.h line 10

Quoted 10 lines
include/uapi/linux/rv.h new file +10 -0
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * UAPI definitions for Runtime Verification (RV) monitors.
+ *
+ * All RV monitors that expose an ioctl self-instrumentation interface
+ * share the magic byte RV_IOC_MAGIC ('r').
+ *
+ * Usage examples and design rationale are in:
+ *   Documentation/trace/rv/monitor_tlob.rst
+ */

Same as above, this could be in a separate patch.

Quoted 7 lines
include/uapi/linux/rv.h new file +7 -0
+
+#ifndef _UAPI_LINUX_RV_H
+#define _UAPI_LINUX_RV_H
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+

[...]

kernel/trace/rv/Makefile line 5

Quoted 10 lines
kernel/trace/rv/Makefile +0 -0
index f139b904bea3..8a5b5c84aff9 100644
@@ -2,7 +2,7 @@


 ccflags-y += -I $(src) # needed for trace events

kernel/trace/rv/Makefile +1 -1
-obj-$(CONFIG_RV) += rv.o
+obj-$(CONFIG_RV) += rv.o rv_chardev.o

Same here.

Quoted 54 lines

 obj-$(CONFIG_RV_MON_WIP) += monitors/wip/wip.o obj-$(CONFIG_RV_MON_WWNR) += monitors/wwnr/wwnr.o obj-$(CONFIG_RV_MON_SCHED) += monitors/sched/sched.o

kernel/trace/rv/monitors/tlob/Kconfig new file +48 -0
@@ -0,0 +1,69 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+config RV_MON_TLOB
+	depends on RV
+	select RV_UPROBE
+	select HA_MON_EVENTS_ID
+	bool "tlob monitor"
+	help
+	  Enable the tlob (task latency over budget) monitor.  This monitor
+	  tracks the elapsed time (CLOCK_MONOTONIC) of a marked code path
+	  within a task (including both on-CPU and off-CPU time) and reports
+	  a violation when the elapsed time exceeds a configurable budget.
+
+	  The monitor uses a three-state hybrid automaton (running, waiting,
+	  sleeping) stored per object using RV_MON_PER_OBJ.  A single HA
+	  clock invariant (clk_elapsed < BUDGET_NS) is enforced in all three
+	  states via a per-task hrtimer.
+
+	  States: running (initial, on-CPU), waiting (in runqueue, off-CPU),
+	          sleeping (blocked on resource, off-CPU).
+	  Key transitions:
+	    running  --(sleep)------> sleeping
+	    running  --(preempt)----> waiting
+	    sleeping --(wakeup)-----> waiting
+	    waiting  --(switch_in)--> running
+	  task_start calls da_handle_start_event() to set the initial state,
+	  then arms the budget timer directly via ha_reset_clk_ns() +
+	  ha_start_timer_ns().  task_stop cancels the timer synchronously via
+	  ha_cancel_timer_sync() then calls da_monitor_reset().
+
+	  Two userspace interfaces are provided:
+
+	  tracefs uprobe binding (external, unmodified binaries):
+	    echo "p PATH:OFFSET_START OFFSET_STOP threshold=NS" \
+	        > /sys/kernel/tracing/rv/monitors/tlob/monitor
+	  The uprobe at offset_start fires tlob_start_task(); the uprobe at
+	  offset_stop fires tlob_stop_task().  Both are plain entry uprobes
+	  so a mistyped offset cannot corrupt the call stack.
+
+	  /dev/rv ioctl (in-process self-instrumentation):
+	    ioctl(fd, TLOB_IOCTL_TRACE_START, &args);
+	    do_critical_work();
+	    ret = ioctl(fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	    /* ret == -EOVERFLOW when budget exceeded */
+	  Allows conditional monitoring, sub-function granularity, and
+	  inline reaction to violations without polling the trace buffer.
+
+	  Up to TLOB_MAX_MONITORED tasks may be monitored simultaneously.

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/Kconfig new file +10 -0
+
+	  Violations are always reported via the standard error_env_tlob RV
+	  tracepoint regardless of which interface triggered them.  The
+	  tracefs interface requires only tracefs write permissions, avoiding
+	  the CAP_BPF privilege needed for equivalent eBPF-based approaches.
+
+	  For further information, see:
+	    Documentation/trace/rv/monitor_tlob.rst
+
+config TLOB_KUNIT_TEST

Do you need to add this here? Since you have a patch adding KUnit tests to tlob, cannot you put everything kunit-related there?

That's also going to simplify things since RV KUnits aren't stable right now.

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

Quoted 1 line
kernel/trace/rv/monitors/tlob/Kconfig new file +1 -0
+	tristate "KUnit tests for tlob monitor" if !KUNIT_ALL_TESTS

I couldn't build it as module, do we need it that way?

ERROR: modpost: "sched_setscheduler_nocheck" [kernel/trace/rv/monitors/tlob/tlob_kunit.ko] undefined!

Quoted 370 lines
kernel/trace/rv/monitors/tlob/Kconfig new file +10 -0
+	depends on RV_MON_TLOB && KUNIT
+	default KUNIT_ALL_TESTS
+	help
+	  Enable KUnit in-kernel unit tests for the tlob RV monitor.
+
+	  Tests cover automaton state transitions, the start/stop task
+	  interface, scheduler context-switch accounting, and the uprobe
+	  format string parser.
+
+	  Say Y or M here to run the tlob KUnit test suite; otherwise say N.
kernel/trace/rv/monitors/tlob/tlob.c new file +12 -0
new file mode 100644
index 000000000000..475e972ae9aa
@@ -0,0 +1,1307 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob: task latency over budget monitor
+ *
+ * Track the elapsed wall-clock time of a marked code path and detect when
+ * a monitored task exceeds its per-task latency budget.  CLOCK_MONOTONIC
+ * is used so both on-CPU and off-CPU time count toward the budget.
+ *
+ * On a budget violation, two tracepoints are emitted from the hrtimer
+ * callback: error_env_tlob signals the violation, and detail_env_tlob
+ * provides a per-state time breakdown (running_ns, waiting_ns, sleeping_ns)
+ * that pinpoints whether the overrun occurred in running, waiting, or

sleeping state.
+ *
+ * The monitor uses RV_MON_PER_OBJ: per-task state (struct tlob_task_state)
+ * is stored as monitor_target in the framework's hash table.
+ *
+ * One HA clock invariant is enforced:
+ *   clk_elapsed < BUDGET_NS()   (active in all states)
+ *
+ * task_start uses da_handle_start_event() to set the initial state, then
+ * calls ha_reset_clk_ns() + ha_start_timer_ns() directly to initialise the
+ * clock and arm the budget timer.  No synthetic event is needed.
+ * The HA timer is cancelled synchronously by ha_cancel_timer_sync() in
+ * tlob_stop_task().
+ *
+ * Copyright (C) 2026 Wen Yang <[email protected]>
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +46 -0
+#include <linux/completion.h>
+#include <linux/hrtimer.h>
+#include <linux/kernel.h>
+#include <linux/ktime.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/namei.h>
+#include <linux/refcount.h>
+#include <linux/rv.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/tracefs.h>
+#include <linux/uaccess.h>
+#include <kunit/visibility.h>
+#include <rv/instrumentation.h>
+#include <rv/rv_uprobe.h>
+#include <uapi/linux/rv.h>
+#include "../../rv.h"
+
+#define MODULE_NAME "tlob"
+
+#include <trace/events/sched.h>
+#include <rv_trace.h>
+
+/*
+ * Per-fd private data; one instance per open /dev/rv fd.
+ * monitoring: set while TRACE_START is active; cleared at TRACE_STOP.
+ * budget_exceeded: set by hrtimer callback; read at TRACE_STOP to report
+ * -EOVERFLOW even when cleanup was claimed by a concurrent stop_all or
+ * a task-exit handler.
+ */
+struct tlob_fpriv {
+	struct task_struct	*task;
+	bool			monitoring;
+	bool			budget_exceeded;
+};
+
+/*
+ * Per-task latency monitoring state.  One instance per monitoring window.
+ * Stored as monitor_target in da_monitor_storage; freed via call_rcu.
+ */
+struct tlob_task_state {
+	struct task_struct	*task;		/* via get_task_struct */
+	u64			threshold_us;	/* budget in microseconds */
+
+	/* 1 = cleanup claimed; ha_setup_invariants won't restart the timer.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	atomic_t		stopping;
+
+	/* Serialises the ns accumulators; held briefly (hardirq-safe). */
+	raw_spinlock_t		entry_lock;
+	u64			running_ns;	/* time in running state  */
+	u64			waiting_ns;	/* time in waiting state  */
+	u64			sleeping_ns;	/* time in sleeping state */
+	ktime_t			last_ts;
+
+	/* store-release in TRACE_START ioctl, load-acquire in reset_notify.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+	struct tlob_fpriv	*fpriv;
+
+	struct rcu_head		rcu;		/* for call_rcu()

teardown */

kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+};
+
+#define RV_MON_TYPE RV_MON_PER_OBJ
+#define HA_TIMER_TYPE HA_TIMER_HRTIMER
+/* Pool mode: da_handle_start_event uses da_fill_empty_storage, not kmalloc.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+#define DA_SKIP_AUTO_ALLOC
+
+/* Type for da_monitor_storage.target; must be defined before the includes.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +49 -0
+typedef struct tlob_task_state *monitor_target;
+
+/* Forward-declared so da_monitor_reset_hook works before ha_monitor.h. */
+static inline void tlob_reset_notify(struct da_monitor *da_mon);
+#define da_monitor_reset_hook tlob_reset_notify
+
+/*
+ * When the hrtimer fires (budget elapsed), the HA framework emits
+ * error_env_tlob with this label instead of the generic "none".
+ */
+#define MONITOR_TIMER_EVENT_NAME "budget_exceeded"
+
+#include "tlob.h"
+#include <rv/ha_monitor.h>
+
+/*
+ * Called from da_monitor_reset() on both normal stop and hrtimer expiry.
+ * On violation (stopping==0), emits detail_env_tlob.
+ */
+static inline void tlob_reset_notify(struct da_monitor *da_mon)
+{
+	struct ha_monitor *ha_mon = to_ha_monitor(da_mon);
+	struct tlob_task_state *ws;
+
+	ha_monitor_reset_env(da_mon);
+
+	ws = ha_get_target(ha_mon);
+	if (!ws)
+		return;
+
+	/*
+	 * Emit per-state breakdown on budget violation only.
+	 * stopping==0: timer callback owns this path (genuine overrun).
+	 * stopping==1: normal stop claimed ownership first; skip.
+	 */
+	if (!atomic_read(&ws->stopping)) {
+		unsigned int curr_state = READ_ONCE(da_mon->curr_state);
+		u64 running_ns, waiting_ns, sleeping_ns, partial_ns;
+		struct tlob_fpriv *fp;
+		unsigned long flags;
+
+		/*
+		 * Snapshot accumulators; partial_ns covers curr_state time
+		 * not yet folded in (transition-out pending).
+		 */
+		raw_spin_lock_irqsave(&ws->entry_lock, flags);
+		partial_ns   = ktime_get_ns() - ktime_to_ns(ws->last_ts);
+		running_ns   = ws->running_ns  +
+			       (curr_state == running_tlob  ? partial_ns :

0);

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+		waiting_ns   = ws->waiting_ns  +
+			       (curr_state == waiting_tlob  ? partial_ns :

0);

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+		sleeping_ns  = ws->sleeping_ns +
+			       (curr_state == sleeping_tlob ? partial_ns :

0);

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+		raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
+
+		trace_detail_env_tlob(da_get_id(da_mon), ws->threshold_us,
+				      running_ns, waiting_ns, sleeping_ns);
+
+		/*
+		 * Latch violation in the fd so TRACE_STOP can return -

EOVERFLOW

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+		 * even if a concurrent stop_all or task-exit handler claims
+		 * cleanup first.  Pairs with smp_store_release in

TRACE_START.

kernel/trace/rv/monitors/tlob/tlob.c new file +13 -0
+		 */
+		fp = smp_load_acquire(&ws->fpriv);
+		if (fp)
+			WRITE_ONCE(fp->budget_exceeded, true);
+	}
+}
+
+#define BUDGET_US(ha_mon) (ha_get_target(ha_mon)->threshold_us)
+#define BUDGET_NS(ha_mon) (BUDGET_US(ha_mon) * 1000ULL)
+
+/* HA constraint functions (called by ha_monitor_handle_constraint) */
+
+static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_tlob env, u64

time_ns)

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+{
+	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)

kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+{
+	if (env == clk_elapsed_tlob)
+		ha_reset_clk_ns(ha_mon, env, time_ns);
+}
+
+/*
+ * ha_verify_invariants - clk_elapsed < BUDGET_NS must hold in all states.
+ */
+static inline bool ha_verify_invariants(struct ha_monitor *ha_mon,
+					enum states curr_state, enum events

event,

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

time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+	else if (curr_state == sleeping_tlob)
+		return ha_check_invariant_ns(ha_mon, clk_elapsed_tlob,

time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+	else if (curr_state == waiting_tlob)
+		return ha_check_invariant_ns(ha_mon, clk_elapsed_tlob,

time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+	return true;
+}
+
+/*
+ * Convert invariant (deadline) to guard (reset anchor) on state transitions.
+ * Skip if uninitialised (ENV_INVALID_VALUE): the race between
+ * da_handle_start_event() and ha_reset_clk_ns() would give U64_MAX -

BUDGET_NS.
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+static inline void ha_convert_inv_guard(struct ha_monitor *ha_mon,
+					enum states curr_state, enum events

event,

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+					enum states next_state, u64 time_ns)
+{
+	if (curr_state == next_state)
+		return;
+	if (curr_state == running_tlob &&
+	    !ha_monitor_env_invalid(ha_mon, clk_elapsed_tlob))
+		ha_inv_to_guard(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon),

time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+	else if (curr_state == sleeping_tlob &&
+		 !ha_monitor_env_invalid(ha_mon, clk_elapsed_tlob))
+		ha_inv_to_guard(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon),

time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+	else if (curr_state == waiting_tlob &&
+		 !ha_monitor_env_invalid(ha_mon, clk_elapsed_tlob))
+		ha_inv_to_guard(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon),

time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+}
+
+/* No per-event guard conditions for tlob; invariants suffice. */
+static inline bool ha_verify_guards(struct ha_monitor *ha_mon,
+				    enum states curr_state, enum events

event,

kernel/trace/rv/monitors/tlob/tlob.c new file +8 -0
+				    enum states next_state, u64 time_ns)
+{
+	return true;
+}
+
+/*
+ * Arm or cancel the HA budget timer on state transitions.
+ * Guard on stopping: sched_switch events can arrive after

ha_cancel_timer_sync,
+ * restarting the timer and triggering an ODEBUG "activate active" splat.
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+static inline void ha_setup_invariants(struct ha_monitor *ha_mon,
+				       enum states curr_state, enum events

event,

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+				       enum states next_state, u64 time_ns)
+{
+	if (next_state == curr_state)
+		return;
+	if (next_state == running_tlob) {
+		if (!atomic_read_acquire(&ha_get_target(ha_mon)->stopping))
+			ha_start_timer_ns(ha_mon, clk_elapsed_tlob,

BUDGET_NS(ha_mon), time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+	} else if (next_state == sleeping_tlob) {
+		if (!atomic_read_acquire(&ha_get_target(ha_mon)->stopping))
+			ha_start_timer_ns(ha_mon, clk_elapsed_tlob,

BUDGET_NS(ha_mon), time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+	} else if (next_state == waiting_tlob) {
+		if (!atomic_read_acquire(&ha_get_target(ha_mon)->stopping))
+			ha_start_timer_ns(ha_mon, clk_elapsed_tlob,

BUDGET_NS(ha_mon), time_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +13 -0
+	} else if (curr_state == running_tlob)
+		ha_cancel_timer(ha_mon);
+	else if (curr_state == waiting_tlob)
+		ha_cancel_timer(ha_mon);
+	else if (curr_state == sleeping_tlob)
+		ha_cancel_timer(ha_mon);
+}
+
+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))

kernel/trace/rv/monitors/tlob/tlob.c new file +5 -0
+		return false;
+
+	ha_convert_inv_guard(ha_mon, curr_state, event, next_state, time_ns);
+
+	if (!ha_verify_guards(ha_mon, curr_state, event, next_state,

time_ns))

kernel/trace/rv/monitors/tlob/tlob.c new file +30 -0
+		return false;
+
+	ha_setup_invariants(ha_mon, curr_state, event, next_state, time_ns);
+
+	return true;
+}
+
+static struct kmem_cache *tlob_state_cache;
+
+static atomic_t tlob_num_monitored = ATOMIC_INIT(0);
+
+/* Uprobe binding list; protected by tlob_uprobe_mutex. */
+static LIST_HEAD(tlob_uprobe_list);
+static DEFINE_MUTEX(tlob_uprobe_mutex);
+
+/*
+ * Serialises duplicate-check + da_create_or_get() to prevent two concurrent
+ * callers for the same pid from both inserting into the hash table.
+ */
+static DEFINE_MUTEX(tlob_start_mutex);
+
+/*
+ * Counts open /dev/rv fds plus one synthetic ref held while enabled.
+ * __tlob_destroy_monitor() drops the synthetic ref and waits for zero
+ * before teardown, preventing kmem_cache_zalloc() on a destroyed cache.
+ */
+static refcount_t tlob_fd_refcount = REFCOUNT_INIT(0);
+static DECLARE_COMPLETION(tlob_fd_released);
+
+/* Per-uprobe-binding state: a start + stop probe pair for one binary region.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +27 -0
+struct tlob_uprobe_binding {
+	struct list_head	list;
+	u64			threshold_us;
+	char			binpath[TLOB_MAX_PATH];
+	loff_t			offset_start;
+	loff_t			offset_stop;
+	struct rv_uprobe	*start_probe;
+	struct rv_uprobe	*stop_probe;
+};
+
+/* RCU callback: free the slab once no readers remain. */
+static void tlob_free_rcu(struct rcu_head *head)
+{
+	struct tlob_task_state *ws =
+		container_of(head, struct tlob_task_state, rcu);
+	kmem_cache_free(tlob_state_cache, ws);
+}
+
+/*
+ * handle_sched_switch - advance the DA on every context switch.
+ *
+ * Generates three DA events:
+ *   prev, prev_state != 0  -> sleep_tlob    (running -> sleeping)
+ *   prev, prev_state == 0  -> preempt_tlob  (running -> waiting)
+ *   next                   -> switch_in_tlob (waiting -> running)
+ */
+static void handle_sched_switch(void *data, bool preempt_unused,

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+				struct task_struct *prev,
+				struct task_struct *next,
+				unsigned int prev_state)
+{
+	struct tlob_task_state *ws;
+	unsigned long flags;
+	bool do_prev = false, do_next = false;
+	bool prev_preempted;
+	ktime_t now;
+

Perhaps keep the handler simpler by moving this reporting to a helper function and use guard(rcu)() there.

Quoted 17 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+	rcu_read_lock();
+
+	ws = da_get_target_by_id(prev->pid);
+	if (ws) {
+		raw_spin_lock_irqsave(&ws->entry_lock, flags);
+		now = ktime_get();
+		ws->running_ns += ktime_to_ns(ktime_sub(now, ws->last_ts));
+		ws->last_ts = now;
+		/* prev_state == 0: TASK_RUNNING (preempted); != 0: sleeping.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +7 -0
+		prev_preempted = (prev_state == 0);
+		do_prev = true;
+		raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
+	}
+
+	ws = da_get_target_by_id(next->pid);
+	if (ws) {

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+		raw_spin_lock_irqsave(&ws->entry_lock, flags);
+		now = ktime_get();
+		ws->waiting_ns += ktime_to_ns(ktime_sub(now, ws->last_ts));
+		ws->last_ts = now;
+		do_next = true;
+		raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
+	}
+
+	rcu_read_unlock();
+

You probably don't need these. da_handle_event should skip tasks without a monitor.

Quoted 9 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+	if (do_prev)
+		da_handle_event(prev->pid, NULL,
+				prev_preempted ? preempt_tlob : sleep_tlob);
+	if (do_next)
+		da_handle_event(next->pid, NULL, switch_in_tlob);
+}
+
+/*
+ * handle_sched_wakeup - sleeping -> waiting transition.

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+ *
+ * try_to_wake_up() skips TASK_RUNNING tasks, so this never fires for a
+ * task already in running or waiting state.
+ */
+static void handle_sched_wakeup(void *data, struct task_struct *p)
+{
+	struct tlob_task_state *ws;
+	unsigned long flags;
+	bool found = false;
+

Same as above to keep the handler simple.

Quoted 4 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+	rcu_read_lock();
+	ws = da_get_target_by_id(p->pid);
+	if (ws) {
+		ktime_t now = ktime_get();

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+
+		raw_spin_lock_irqsave(&ws->entry_lock, flags);
+		ws->sleeping_ns += ktime_to_ns(ktime_sub(now, ws->last_ts));
+		ws->last_ts = now;
+		raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
+		found = true;
+	}
+	rcu_read_unlock();
+
+	if (found)

You probably don't need this. da_handle_event should skip tasks without a monitor.

Quoted 11 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +11 -0
+		da_handle_event(p->pid, NULL, wakeup_tlob);
+}
+
+/*
+ * handle_sched_process_exit - clean up if a task exits without TRACE_STOP.
+ *
+ * Called in do_exit() context; the task still has a valid pid here.
+ */
+static void handle_sched_process_exit(void *data, struct task_struct *p,
+				       bool group_dead)
+{

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+	struct tlob_task_state *ws;
+	bool found = false;
+
 
+	rcu_read_lock();
+	ws = da_get_target_by_id(p->pid);
+	found = !!ws;
+	rcu_read_unlock();
+
+	if (found)

You can skip all this here.

Quoted 22 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+		tlob_stop_task(p);
+}
+
+
+
+/**
+ * tlob_start_task - begin monitoring @task with budget @threshold_us us.
+ * @task:         Task to monitor; may be current or another task.
+ * @threshold_us: Latency budget in microseconds (wall-clock; running +

waiting + sleeping). > 0.
+ *
+ * Returns 0, -ENODEV, -EALREADY, -ENOSPC, or -ENOMEM.
+ */

kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+int tlob_start_task(struct task_struct *task, u64 threshold_us)
+{
+	struct tlob_task_state *ws_existing;
+	struct tlob_task_state *ws;
+	struct da_monitor *da_mon;
+	struct ha_monitor *ha_mon;
+	u64 now_ns;
+	int ret;
+

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	if (!da_monitor_enabled())
+		return -ENODEV;
+
+	if (threshold_us == 0)
+		return -ERANGE;
+
+	/* Serialise duplicate-check + da_create_or_get for the same pid. */
+	guard(mutex)(&tlob_start_mutex);
+
+	rcu_read_lock();

That should be a scoped_guard(rcu), definitely use guards if you have return paths, the compiler is going to clean up (unlock) for you.

Quoted 19 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +19 -0
+	ws_existing = da_get_target_by_id(task->pid);
+	if (ws_existing) {
+		rcu_read_unlock();
+		return -EALREADY;
+	}
+	rcu_read_unlock();
+
+	ws = kmem_cache_zalloc(tlob_state_cache, GFP_KERNEL);
+	if (!ws)
+		return -ENOMEM;
+
+	ws->task = task;
+	get_task_struct(task);
+	ws->threshold_us = threshold_us;
+	ws->last_ts = ktime_get();
+	raw_spin_lock_init(&ws->entry_lock);
+
+	/* Claim a pool slot (no kmalloc; DA_SKIP_AUTO_ALLOC + prealloc). */
+	ret = da_create_or_get(task->pid, ws);

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	if (ret) {
+		put_task_struct(task);
+		kmem_cache_free(tlob_state_cache, ws);
+		return ret;
+	}
+
+	atomic_inc(&tlob_num_monitored);
+
+	/* Hold RCU across handle + timer setup to keep da_mon valid. */
+	rcu_read_lock();

Same here about guards.
Sadly there doesn't seem to be a cleanup helper for kmem_cache_free, would be worth adding one. You have also a lot of other things to do here so it isn't a big deal.

Quoted 79 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+	da_handle_start_event(task->pid, ws, switch_in_tlob);
+	da_mon = da_get_monitor(task->pid, NULL);
+	if (unlikely(!da_mon)) {
+		/* Slot registered; missing da_mon means concurrent destroy.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +11 -0
+		rcu_read_unlock();
+		da_destroy_storage(task->pid);
+		atomic_dec(&tlob_num_monitored);
+		put_task_struct(task);
+		kmem_cache_free(tlob_state_cache, ws);
+		return -ENOMEM;
+	}
+	ha_mon = to_ha_monitor(da_mon);
+	now_ns = ktime_get_ns();
+	ha_reset_env(ha_mon, clk_elapsed_tlob, now_ns);
+	ha_start_timer_ns(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon),

now_ns);

kernel/trace/rv/monitors/tlob/tlob.c new file +42 -0
+	rcu_read_unlock();
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(tlob_start_task);
+
+/**
+ * tlob_stop_task - stop monitoring @task.
+ * @task: Task to stop.
+ *
+ * CAS on ws->stopping (0->1) under RCU claims cleanup ownership;
+ * the winner cancels the timer synchronously and frees all resources.
+ *
+ * Returns 0, -EOVERFLOW (budget exceeded), -ESRCH (not monitored),
+ * or -EAGAIN (concurrent caller claimed cleanup).
+ */
+int tlob_stop_task(struct task_struct *task)
+{
+	struct da_monitor *da_mon;
+	struct ha_monitor *ha_mon;
+	struct tlob_task_state *ws;
+	bool budget_exceeded;
+
+	rcu_read_lock();
+	ws = da_get_target_by_id(task->pid);
+	if (!ws) {
+		rcu_read_unlock();
+		return -ESRCH;
+	}
+
+	da_mon = da_get_monitor(task->pid, NULL);
+	if (unlikely(!da_mon)) {
+		/* ws in hash but da_mon gone; internal inconsistency. */
+		rcu_read_unlock();
+		WARN_ON_ONCE(1);
+		return -ESRCH;
+	}
+
+	ha_mon = to_ha_monitor(da_mon);
+
+	/*
+	 * CAS (0->1) claims cleanup ownership under RCU (ws guaranteed

valid).

kernel/trace/rv/monitors/tlob/tlob.c new file +19 -0
+	 * _release pairs with atomic_read_acquire in ha_setup_invariants.
+	 */
+	if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0) {
+		rcu_read_unlock();
+		return -EAGAIN;
+	}
+
+	rcu_read_unlock();
+
+	/* Wait for in-flight timer callback before reading da_monitoring. */
+	ha_cancel_timer_sync(ha_mon);
+
+	/* Timer fired first -> budget exceeded; otherwise reset normally. */
+	rcu_read_lock();
+	budget_exceeded = !da_monitoring(da_mon);
+	if (!budget_exceeded)
+		da_monitor_reset(da_mon);
+	rcu_read_unlock();
+	da_destroy_storage(task->pid);

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

Quoted 10 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +10 -0
+	atomic_dec(&tlob_num_monitored);
+
+	put_task_struct(ws->task);
+	call_rcu(&ws->rcu, tlob_free_rcu);
+	return budget_exceeded ? -EOVERFLOW : 0;
+}
+EXPORT_SYMBOL_GPL(tlob_stop_task);
+
+static void tlob_stop_all(void)
+{

All this function does should be done by da_monitor_destroy. It does have some concurrency issues I'm trying to fix, but there's no reason not to use it.

We could add a way to pass some additional deallocation for all the other cleanup you're doing on each storage.

Something like a da_extra_cleanup() you can define as whatever you need and gets called in all per-obj destruction paths.

In general, let's try to use/extend as much as possible in the RV API rather then re-implementing things.

Quoted 780 lines
kernel/trace/rv/monitors/tlob/tlob.c new file +30 -0
+	struct da_monitor_storage *ms;
+	pid_t pids[TLOB_MAX_MONITORED];
+	int bkt, n = 0;
+
+	/* Snapshot pids under RCU; re-derive ws under a fresh lock below. */
+	rcu_read_lock();
+	hash_for_each_rcu(da_monitor_ht, bkt, ms, node) {
+		if (ms->target && n < TLOB_MAX_MONITORED)
+			pids[n++] = ms->id;
+	}
+	rcu_read_unlock();
+
+	for (int i = 0; i < n; i++) {
+		pid_t pid = pids[i];
+		struct da_monitor *da_mon;
+		struct ha_monitor *ha_mon;
+		struct tlob_task_state *ws;
+
+		rcu_read_lock();
+		da_mon = da_get_monitor(pid, NULL);
+		if (!da_mon) {
+			/* Cleaned up by tlob_stop_task or exit handler. */
+			rcu_read_unlock();
+			continue;
+		}
+
+		ws = da_get_target(da_mon);
+		ha_mon = to_ha_monitor(da_mon);
+
+		/* CAS (0->1) claims ownership; skip if another caller won.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +19 -0
+		if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0) {
+			rcu_read_unlock();
+			continue;
+		}
+		rcu_read_unlock();
+
+		ha_cancel_timer_sync(ha_mon);
+
+		scoped_guard(rcu) {
+			da_monitor_reset(da_mon);
+		}
+		da_destroy_storage(pid);
+		atomic_dec(&tlob_num_monitored);
+		put_task_struct(ws->task);
+		call_rcu(&ws->rcu, tlob_free_rcu);
+	}
+}
+
+static int tlob_uprobe_entry_handler(struct rv_uprobe *p, struct pt_regs

*regs,

kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+				     __u64 *data)
+{
+	struct tlob_uprobe_binding *b = p->priv;
+
+	tlob_start_task(current, b->threshold_us);
+	return 0;
+}
+
+static int tlob_uprobe_stop_handler(struct rv_uprobe *p, struct pt_regs

*regs,

kernel/trace/rv/monitors/tlob/tlob.c new file +56 -0
+				    __u64 *data)
+{
+	tlob_stop_task(current);
+	return 0;
+}
+
+/*
+ * Register start + stop entry uprobes for a binding.
+ * Called with tlob_uprobe_mutex held.
+ */
+static int tlob_add_uprobe(u64 threshold_us, const char *binpath,
+			   loff_t offset_start, loff_t offset_stop)
+{
+	struct tlob_uprobe_binding *b, *tmp_b;
+	char pathbuf[TLOB_MAX_PATH];
+	struct path path;
+	char *canon;
+	int ret;
+
+	if (binpath[0] != '/')
+		return -EINVAL;
+
+	b = kzalloc_obj(*b, GFP_KERNEL);
+	if (!b)
+		return -ENOMEM;
+
+	b->threshold_us = threshold_us;
+	b->offset_start = offset_start;
+	b->offset_stop  = offset_stop;
+
+	ret = kern_path(binpath, LOOKUP_FOLLOW, &path);
+	if (ret)
+		goto err_free;
+
+	if (!d_is_reg(path.dentry)) {
+		ret = -EINVAL;
+		goto err_path;
+	}
+
+	/* Reject duplicate start offset for the same binary. */
+	list_for_each_entry(tmp_b, &tlob_uprobe_list, list) {
+		if (tmp_b->offset_start == offset_start &&
+		    tmp_b->start_probe->path.dentry == path.dentry) {
+			ret = -EEXIST;
+			goto err_path;
+		}
+	}
+
+	canon = d_path(&path, pathbuf, sizeof(pathbuf));
+	if (IS_ERR(canon)) {
+		ret = PTR_ERR(canon);
+		goto err_path;
+	}
+	strscpy(b->binpath, canon, sizeof(b->binpath));
+
+	/* Both probes share b (priv) and path; attach_path refs path itself.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+	b->start_probe = rv_uprobe_attach_path(&path, offset_start,
+					       tlob_uprobe_entry_handler,

NULL, b);

kernel/trace/rv/monitors/tlob/tlob.c new file +8 -0
+	if (IS_ERR(b->start_probe)) {
+		ret = PTR_ERR(b->start_probe);
+		b->start_probe = NULL;
+		goto err_path;
+	}
+
+	b->stop_probe = rv_uprobe_attach_path(&path, offset_stop,
+					      tlob_uprobe_stop_handler, NULL,

b);

kernel/trace/rv/monitors/tlob/tlob.c new file +20 -0
+	if (IS_ERR(b->stop_probe)) {
+		ret = PTR_ERR(b->stop_probe);
+		b->stop_probe = NULL;
+		goto err_start;
+	}
+
+	path_put(&path);
+	list_add_tail(&b->list, &tlob_uprobe_list);
+	return 0;
+
+err_start:
+	rv_uprobe_detach(b->start_probe);
+err_path:
+	path_put(&path);
+err_free:
+	kfree(b);
+	return ret;
+}
+
+static int tlob_remove_uprobe_by_key(loff_t offset_start, const char

*binpath)

kernel/trace/rv/monitors/tlob/tlob.c new file +173 -0
+{
+	struct tlob_uprobe_binding *b, *tmp;
+	struct path remove_path;
+	int ret;
+
+	ret = kern_path(binpath, LOOKUP_FOLLOW, &remove_path);
+	if (ret)
+		return ret;
+
+	ret = -ENOENT;
+	list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) {
+		if (b->offset_start != offset_start)
+			continue;
+		if (b->start_probe->path.dentry != remove_path.dentry)
+			continue;
+		list_del(&b->list);
+		rv_uprobe_detach(b->start_probe);
+		rv_uprobe_detach(b->stop_probe);
+		kfree(b);
+		ret = 0;
+		break;
+	}
+
+	path_put(&remove_path);
+	return ret;
+}
+
+static void tlob_remove_all_uprobes(void)
+{
+	struct tlob_uprobe_binding *b, *tmp;
+	LIST_HEAD(pending);
+
+	mutex_lock(&tlob_uprobe_mutex);
+	list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) {
+		list_move(&b->list, &pending);
+		rv_uprobe_unregister_nosync(b->start_probe);
+		rv_uprobe_unregister_nosync(b->stop_probe);
+	}
+	mutex_unlock(&tlob_uprobe_mutex);
+
+	if (list_empty(&pending))
+		return;
+
+	/*
+	 * One global barrier for all probes dequeued above; no new handlers
+	 * for any of them can fire after this returns.
+	 */
+	rv_uprobe_sync();
+
+	list_for_each_entry_safe(b, tmp, &pending, list) {
+		rv_uprobe_free(b->start_probe);
+		rv_uprobe_free(b->stop_probe);
+		kfree(b);
+	}
+}
+
+static ssize_t tlob_monitor_read(struct file *file,
+				 char __user *ubuf,
+				 size_t count, loff_t *ppos)
+{
+	const int line_sz = TLOB_MAX_PATH + 128;
+	struct tlob_uprobe_binding *b;
+	char *buf, *p;
+	int n = 0, buf_sz, pos = 0;
+	ssize_t ret;
+
+	mutex_lock(&tlob_uprobe_mutex);
+	list_for_each_entry(b, &tlob_uprobe_list, list)
+		n++;
+
+	buf_sz = (n ? n : 1) * line_sz + 1;
+	buf = kmalloc(buf_sz, GFP_KERNEL);
+	if (!buf) {
+		mutex_unlock(&tlob_uprobe_mutex);
+		return -ENOMEM;
+	}
+
+	list_for_each_entry(b, &tlob_uprobe_list, list) {
+		p = b->binpath;
+		pos += scnprintf(buf + pos, buf_sz - pos,
+				 "p %s:0x%llx 0x%llx threshold=%llu\n",
+				 p,
+				 (unsigned long long)b->offset_start,
+				 (unsigned long long)b->offset_stop,
+				 b->threshold_us);
+	}
+	mutex_unlock(&tlob_uprobe_mutex);
+
+	ret = simple_read_from_buffer(ubuf, count, ppos, buf, pos);
+	kfree(buf);
+	return ret;
+}
+
+/*
+ * Parse "p PATH:OFFSET_START OFFSET_STOP threshold=US".
+ * PATH may contain ':'; the last ':' separates path from offset.
+ * Returns 0 or -EINVAL.
+ */
+static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+				  char **path_out,
+				  loff_t *start_out, loff_t *stop_out)
+{
+	unsigned long long thr = 0, stop_val = 0;
+	long long start_val;
+	char *p, *path_token, *token, *colon;
+	bool got_stop = false, got_thr = false;
+	int n;
+
+	/* Must start with "p " */
+	if (buf[0] != 'p' || buf[1] != ' ')
+		return -EINVAL;
+
+	p = buf + 2;
+	while (*p == ' ')
+		p++;
+
+	/* First space-delimited token is PATH:OFFSET_START */
+	path_token = strsep(&p, " \t");
+	if (!path_token || !*path_token)
+		return -EINVAL;
+
+	/* Split at last ':' to handle paths that contain ':'. */
+	colon = strrchr(path_token, ':');
+	if (!colon || colon - path_token < 2)
+		return -EINVAL;
+	*colon = '\0';
+
+	if (path_token[0] != '/')
+		return -EINVAL;
+
+	n = 0;
+	if (sscanf(colon + 1, "%lli%n", &start_val, &n) != 1 || n == 0)
+		return -EINVAL;
+	if (start_val < 0)
+		return -EINVAL;
+
+	/* Remaining tokens: OFFSET_STOP threshold=US */
+	while (p && (token = strsep(&p, " \t")) != NULL) {
+		if (!*token)
+			continue;
+		if (strncmp(token, "threshold=", 10) == 0) {
+			if (kstrtoull(token + 10, 0, &thr))
+				return -EINVAL;
+			got_thr = true;
+		} else if (!got_stop) {
+			long long sv;
+
+			n = 0;
+			if (sscanf(token, "%lli%n", &sv, &n) != 1 || n == 0)
+				return -EINVAL;
+			if (sv < 0)
+				return -EINVAL;
+			stop_val = (unsigned long long)sv;
+			got_stop = true;
+		} else {
+			return -EINVAL;
+		}
+	}
+
+	if (!got_stop || !got_thr || thr == 0)
+		return -EINVAL;
+	if (start_val == (long long)stop_val)
+		return -EINVAL;
+
+	*thr_out   = thr;
+	*path_out  = path_token;
+	*start_out = (loff_t)start_val;
+	*stop_out  = (loff_t)stop_val;
+	return 0;
+}
+
+/* Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention). */
+static int tlob_parse_remove_line(char *buf, char **path_out, loff_t

*start_out)

kernel/trace/rv/monitors/tlob/tlob.c new file +43 -0
+{
+	char *binpath, *colon;
+	long long off;
+	int n = 0;
+
+	if (buf[0] != '-')
+		return -EINVAL;
+	binpath = buf + 1;
+	if (binpath[0] != '/')
+		return -EINVAL;
+	colon = strrchr(binpath, ':');
+	if (!colon || colon - binpath < 2)
+		return -EINVAL;
+	*colon = '\0';
+	if (sscanf(colon + 1, "%lli%n", &off, &n) != 1 || n == 0)
+		return -EINVAL;
+	*path_out  = binpath;
+	*start_out = (loff_t)off;
+	return 0;
+}
+
+VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf)
+{
+	loff_t offset_start, offset_stop;
+	u64 threshold_us;
+	char *binpath;
+	int ret;
+
+	if (buf[0] == '-') {
+		ret = tlob_parse_remove_line(buf, &binpath, &offset_start);
+		if (ret)
+			return ret;
+		mutex_lock(&tlob_uprobe_mutex);
+		ret = tlob_remove_uprobe_by_key(offset_start, binpath);
+		mutex_unlock(&tlob_uprobe_mutex);
+		return ret;
+	}
+	ret = tlob_parse_uprobe_line(buf, &threshold_us, &binpath,
+				     &offset_start, &offset_stop);
+	if (ret)
+		return ret;
+	mutex_lock(&tlob_uprobe_mutex);
+	ret = tlob_add_uprobe(threshold_us, binpath, offset_start,

offset_stop);

kernel/trace/rv/monitors/tlob/tlob.c new file +61 -0
+	mutex_unlock(&tlob_uprobe_mutex);
+	return ret;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_create_or_delete_uprobe);
+
+static ssize_t tlob_monitor_write(struct file *file,
+				  const char __user *ubuf,
+				  size_t count, loff_t *ppos)
+{
+	char buf[TLOB_MAX_PATH + 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';
+	return tlob_create_or_delete_uprobe(buf) ?: (ssize_t)count;
+}
+
+static const struct file_operations tlob_monitor_fops = {
+	.open	= simple_open,
+	.read	= tlob_monitor_read,
+	.write	= tlob_monitor_write,
+	.llseek	= noop_llseek,
+};
+
+static int __tlob_init_monitor(void)
+{
+	int retval;
+
+	tlob_state_cache = kmem_cache_create("tlob_task_state",
+					     sizeof(struct tlob_task_state),
+					     0, 0, NULL);
+	if (!tlob_state_cache)
+		return -ENOMEM;
+
+	atomic_set(&tlob_num_monitored, 0);
+
+	retval = da_monitor_init_prealloc(TLOB_MAX_MONITORED);
+	if (retval) {
+		kmem_cache_destroy(tlob_state_cache);
+		tlob_state_cache = NULL;
+		return retval;
+	}
+
+	/* Synthetic reference: held while the monitor is enabled. */
+	reinit_completion(&tlob_fd_released);
+	refcount_set(&tlob_fd_refcount, 1);
+
+	rv_this.enabled = 1;
+	return 0;
+}
+
+static void __tlob_destroy_monitor(void)
+{
+	rv_this.enabled = 0;
+	/*
+	 * Remove uprobes first so stop_task can't race with tlob_stop_all().
+	 * rv_uprobe_sync() inside ensures all in-flight handlers have

finished.

kernel/trace/rv/monitors/tlob/tlob.c new file +4 -0
+	 */
+	tlob_remove_all_uprobes();
+	tlob_stop_all();
+	/* Wait for tlob_free_rcu and da_pool_return_cb before pool teardown.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +15 -0
+	synchronize_rcu();
+
+	/*
+	 * Drop the synthetic ref and wait for all open fds to close before
+	 * teardown; prevents kmem_cache_zalloc() on the destroyed cache.
+	 */
+	if (!refcount_dec_and_test(&tlob_fd_refcount))
+		wait_for_completion(&tlob_fd_released);
+
+	da_monitor_destroy();
+	kmem_cache_destroy(tlob_state_cache);
+	tlob_state_cache = NULL;
+}
+
+/* KUnit wrappers that acquire rv_interface_lock around monitor init/destroy.

*/

kernel/trace/rv/monitors/tlob/tlob.c new file +33 -0
+#if IS_ENABLED(CONFIG_KUNIT)
+int tlob_init_monitor(void)
+{
+	int ret;
+
+	mutex_lock(&rv_interface_lock);
+	ret = __tlob_init_monitor();
+	mutex_unlock(&rv_interface_lock);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(tlob_init_monitor);
+
+void tlob_destroy_monitor(void)
+{
+	mutex_lock(&rv_interface_lock);
+	__tlob_destroy_monitor();
+	mutex_unlock(&rv_interface_lock);
+}
+EXPORT_SYMBOL_GPL(tlob_destroy_monitor);
+
+int tlob_num_monitored_read(void)
+{
+	return atomic_read(&tlob_num_monitored);
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_num_monitored_read);
+
+/* Tracepoint probes for KUnit; rv_trace.h is only included here. */
+static struct tlob_captured_event     tlob_kunit_last_event;
+static struct tlob_captured_error_env tlob_kunit_last_error_env;
+static atomic_t tlob_kunit_event_cnt    = ATOMIC_INIT(0);
+static atomic_t tlob_kunit_error_env_cnt = ATOMIC_INIT(0);
+
+static void tlob_kunit_event_probe(void *data, int id, char *state, char

*event,

kernel/trace/rv/monitors/tlob/tlob.c new file +37 -0
+				   char *next_state, bool final_state)
+{
+	tlob_kunit_last_event.id = id;
+	strscpy(tlob_kunit_last_event.state, state,
+		sizeof(tlob_kunit_last_event.state));
+	strscpy(tlob_kunit_last_event.event, event,
+		sizeof(tlob_kunit_last_event.event));
+	strscpy(tlob_kunit_last_event.next_state, next_state,
+		sizeof(tlob_kunit_last_event.next_state));
+	tlob_kunit_last_event.final_state = final_state;
+	atomic_inc(&tlob_kunit_event_cnt);
+}
+
+static void tlob_kunit_error_env_probe(void *data, int id, char *state,
+				       char *event, char *env)
+{
+	tlob_kunit_last_error_env.id = id;
+	strscpy(tlob_kunit_last_error_env.state, state,
+		sizeof(tlob_kunit_last_error_env.state));
+	strscpy(tlob_kunit_last_error_env.event, event,
+		sizeof(tlob_kunit_last_error_env.event));
+	strscpy(tlob_kunit_last_error_env.env, env,
+		sizeof(tlob_kunit_last_error_env.env));
+	atomic_inc(&tlob_kunit_error_env_cnt);
+}
+
+int tlob_register_kunit_probes(void)
+{
+	int ret;
+
+	atomic_set(&tlob_kunit_event_cnt, 0);
+	atomic_set(&tlob_kunit_error_env_cnt, 0);
+
+	ret = register_trace_event_tlob(tlob_kunit_event_probe, NULL);
+	if (ret)
+		return ret;
+	ret = register_trace_error_env_tlob(tlob_kunit_error_env_probe,

NULL);

kernel/trace/rv/monitors/tlob/tlob.c new file +59 -0
+	if (ret) {
+		unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
+		return ret;
+	}
+	return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_register_kunit_probes);
+
+void tlob_unregister_kunit_probes(void)
+{
+	unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
+	unregister_trace_error_env_tlob(tlob_kunit_error_env_probe, NULL);
+	tracepoint_synchronize_unregister();
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_unregister_kunit_probes);
+
+int tlob_event_count_read(void)
+{
+	return atomic_read(&tlob_kunit_event_cnt);
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_event_count_read);
+
+void tlob_event_count_reset(void)
+{
+	atomic_set(&tlob_kunit_event_cnt, 0);
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_event_count_reset);
+
+int tlob_error_env_count_read(void)
+{
+	return atomic_read(&tlob_kunit_error_env_cnt);
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_error_env_count_read);
+
+void tlob_error_env_count_reset(void)
+{
+	atomic_set(&tlob_kunit_error_env_cnt, 0);
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_error_env_count_reset);
+
+const struct tlob_captured_event *tlob_last_event_read(void)
+{
+	return &tlob_kunit_last_event;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_last_event_read);
+
+const struct tlob_captured_error_env *tlob_last_error_env_read(void)
+{
+	return &tlob_kunit_last_error_env;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_last_error_env_read);
+
+#endif /* CONFIG_KUNIT */
+
+VISIBLE_IF_KUNIT 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);

kernel/trace/rv/monitors/tlob/tlob.c new file +9 -0
+	return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_enable_hooks);
+
+VISIBLE_IF_KUNIT 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);

kernel/trace/rv/monitors/tlob/tlob.c new file +39 -0
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_disable_hooks);
+
+static int enable_tlob(void)
+{
+	int retval;
+
+	retval = __tlob_init_monitor();
+	if (retval)
+		return retval;
+
+	return tlob_enable_hooks();
+}
+
+static void disable_tlob(void)
+{
+	tlob_disable_hooks();
+	__tlob_destroy_monitor();
+}
+
+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 void *tlob_chardev_bind(void)
+{
+	struct tlob_fpriv *fp;
+
+	fp = kzalloc_obj(*fp, GFP_KERNEL);
+	if (!fp)
+		return ERR_PTR(-ENOMEM);
+
+	/* Pin cache/pool for fd lifetime; balanced in tlob_chardev_release.
+	 * If the synthetic ref has already been dropped

(__tlob_destroy_monitor

kernel/trace/rv/monitors/tlob/tlob.c new file +1 -0
+	 * ran to completion), reject the bind so the caller gets ENODEV

instead

kernel/trace/rv/monitors/tlob/tlob.c new file +27 -0
+	 * of corrupting a zero refcount.
+	 */
+	if (!refcount_inc_not_zero(&tlob_fd_refcount)) {
+		kfree(fp);
+		return ERR_PTR(-ENODEV);
+	}
+	return fp;
+}
+
+static void tlob_chardev_release(void *priv)
+{
+	struct tlob_fpriv *fp = priv;
+
+	if (fp->monitoring) {
+		/* All return values are safe on close. */
+		(void)tlob_stop_task(fp->task);
+		put_task_struct(fp->task);
+	}
+
+	kfree(fp);
+
+	/* Release fd's pin; if last, wake __tlob_destroy_monitor. */
+	if (refcount_dec_and_test(&tlob_fd_refcount))
+		complete(&tlob_fd_released);
+}
+
+static long tlob_chardev_ioctl(void *priv, unsigned int cmd, unsigned long

arg)

kernel/trace/rv/monitors/tlob/tlob.c new file +25 -0
+{
+	struct tlob_fpriv *fp = priv;
+	struct tlob_start_args args;
+	struct task_struct *task;
+	int ret;
+
+	switch (cmd) {
+	case TLOB_IOCTL_TRACE_START:
+		if (fp->monitoring)
+			return -EALREADY;
+
+		if (copy_from_user(&args, (void __user *)arg, sizeof(args)))
+			return -EFAULT;
+
+		ret = tlob_start_task(current, args.threshold_us);
+		if (ret)
+			return ret;
+
+		fp->task = current;
+		get_task_struct(current);
+		fp->budget_exceeded = false;
+
+		/* Link fd so hrtimer callback can latch budget_exceeded. */
+		scoped_guard(rcu) {
+			struct tlob_task_state *ws =

da_get_target_by_id(current->pid);

kernel/trace/rv/monitors/tlob/tlob.c new file +22 -0
+
+			if (ws)
+				smp_store_release(&ws->fpriv, fp);
+		}
+
+		fp->monitoring = true;
+		return 0;
+
+	case TLOB_IOCTL_TRACE_STOP:
+		if (!fp->monitoring)
+			return -EINVAL;
+
+		task = fp->task;
+		fp->monitoring = false;
+		fp->task = NULL;
+
+		ret = tlob_stop_task(task);
+		put_task_struct(task);
+
+		/*
+		 * -EOVERFLOW: budget exceeded; propagate to caller.
+		 * -EAGAIN: concurrent stop_all claimed cleanup; fall through

to

kernel/trace/rv/monitors/tlob/tlob.c new file +2 -0
+		 *   budget_exceeded latch set by the hrtimer callback.
+		 * -ESRCH: task exited before TRACE_STOP (process-exit

handler

kernel/trace/rv/monitors/tlob/tlob.c new file +1 -0
+		 *   claimed cleanup); same latch applies.  Not an internal

error.

kernel/trace/rv/monitors/tlob/tlob.c new file +3 -0
+		 */
+		if (ret == -EAGAIN || ret == -ESRCH)
+			return READ_ONCE(fp->budget_exceeded) ? -EOVERFLOW :

0;

kernel/trace/rv/monitors/tlob/tlob.c new file +30 -0
+		return ret;
+
+	default:
+		return -ENOTTY;
+	}
+}
+
+static const struct rv_chardev_ops tlob_chardev_ops = {
+	.owner   = THIS_MODULE,
+	.bind    = tlob_chardev_bind,
+	.ioctl   = tlob_chardev_ioctl,
+	.release = tlob_chardev_release,
+};
+
+static int __init register_tlob(void)
+{
+	int ret;
+
+	ret = rv_chardev_register_monitor("tlob", &tlob_chardev_ops);
+	if (ret)
+		return ret;
+
+	ret = rv_register_monitor(&rv_this, NULL);
+	if (ret) {
+		rv_chardev_unregister_monitor("tlob");
+		return ret;
+	}
+
+	if (rv_this.root_d) {
+		if (!tracefs_create_file("monitor", 0644, rv_this.root_d,

NULL,

kernel/trace/rv/monitors/tlob/tlob.c new file +22 -0
+					 &tlob_monitor_fops)) {
+			rv_unregister_monitor(&rv_this);
+			rv_chardev_unregister_monitor("tlob");
+			return -ENOMEM;
+		}
+	}
+
+	return 0;
+}
+
+static void __exit unregister_tlob(void)
+{
+	rv_chardev_unregister_monitor("tlob");
+	rv_unregister_monitor(&rv_this);
+}
+
+module_init(register_tlob);
+module_exit(unregister_tlob);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Wen Yang <[email protected]>");
+MODULE_DESCRIPTION("tlob: task latency over budget per-task monitor.");
kernel/trace/rv/monitors/tlob/tlob.h new file +0 -1
new file mode 100644
index 000000000000..71c1735d27d2
--- /dev/null

[...]

Quoted 13 lines
kernel/trace/rv/rv.c +1 -0
index ee4e68102f17..a45c4763dbe5 100644
@@ -142,10 +142,17 @@
  #include <linux/module.h>
  #include <linux/init.h>
  #include <linux/slab.h>
+#include <kunit/visibility.h>


 #ifdef CONFIG_RV_MON_EVENTS
 #define CREATE_TRACE_POINTS
 #include <rv_trace.h>

kernel/trace/rv/rv.c line 155

Quoted 6 lines
kernel/trace/rv/rv.c +6 -0
+
+#ifdef CONFIG_RV_MON_TLOB
+EXPORT_TRACEPOINT_SYMBOL_GPL(error_tlob);
+EXPORT_TRACEPOINT_SYMBOL_GPL(event_tlob);
+EXPORT_TRACEPOINT_SYMBOL_GPL(error_env_tlob);
+#endif

Cannot this stay in tlob.c ? So you keep the shared file clean and skip the ifdeffery.

Quoted 44 lines

 #endif

 #include "rv.h"

kernel/trace/rv/rv.c +0 -0
@@ -696,6 +703,33 @@ static void turn_monitoring_on(void)
  	WRITE_ONCE(monitoring_on, true);
  }

kernel/trace/rv/rv.c +3 -0
+#if IS_ENABLED(CONFIG_KUNIT)
+/**
+ * rv_kunit_monitoring_on - enable the global monitoring_on flag for KUnit

tests.
+ *
+ * KUnit test suite_init functions must call this before initialising any
+ * monitor, mirroring the turn_monitoring_on() call in rv_init_interface().
+ * The matching rv_kunit_monitoring_off() must be called in suite_exit to
+ * restore the flag so that test suites do not interfere with each other.
+ */

kernel/trace/rv/rv.c +8 -0
+void rv_kunit_monitoring_on(void)
+{
+	turn_monitoring_on();
+}
+EXPORT_SYMBOL_IF_KUNIT(rv_kunit_monitoring_on);
+
+/**
+ * rv_kunit_monitoring_off - disable the global monitoring_on flag for KUnit

tests.
+ *
+ * Must be called in suite_exit to restore global state after rv_kunit_monitoring_on().
+ */

kernel/trace/rv/rv.c +7 -0
+void rv_kunit_monitoring_off(void)
+{
+	turn_monitoring_off();
+}
+EXPORT_SYMBOL_IF_KUNIT(rv_kunit_monitoring_off);
+#endif /* CONFIG_KUNIT */
+

 static void turn_monitoring_on_with_reset(void)
 {
  lockdep_assert_held(&rv_interface_lock);

kernel/trace/rv/rv.c +0 -0
@@ -846,6 +880,10 @@ int __init rv_init_interface(void)
  	if (retval)
  		return 1;

kernel/trace/rv/rv.c int __init rv_init_interface(void) · line 886

Quoted 4 lines
kernel/trace/rv/rv.c +4 -0
+	retval = rv_chardev_init();
+	if (retval)
+		return 1;
+

Both of those can stay in separate patches as mentioned above.

kernel/trace/rv/rv.h int rv_enable_monitor(struct rv_monitor *mon); · line 35

Quoted 13 lines

  turn_monitoring_on();

  rv_root.root_dir = no_free_ptr(root_dir);

kernel/trace/rv/rv.h +0 -0
index 2c0f51ff9d5c..82c9a2b57596 100644
@@ -31,6 +31,8 @@ int rv_enable_monitor(struct rv_monitor *mon);
  bool rv_is_container_monitor(struct rv_monitor *mon);
  bool rv_is_nested_monitor(struct rv_monitor *mon);

kernel/trace/rv/rv.h +2 -0
+int rv_chardev_init(void);
+

Same here.

kernel/trace/rv/rv_chardev.c line 2

Quoted 11 lines

 #ifdef CONFIG_RV_REACTORS
 int reactor_populate_monitor(struct rv_monitor *mon, struct dentry *root); int init_rv_reactors(struct dentry *root_dir);

kernel/trace/rv/rv_chardev.c new file +2 -0
new file mode 100644
index 000000000000..1fba1642ebc1
@@ -0,0 +1,201 @@
+// SPDX-License-Identifier: GPL-2.0
+

And here.

kernel/trace/rv/rv_uprobe.c

Quoted 4 lines
kernel/trace/rv/rv_uprobe.c +0 -0
index bc28399cfd4b..1ba7b80c1d87 100644

Also this probably belongs in the uprobes patch.

Quoted 23 lines
kernel/trace/rv/rv_uprobe.c +0 -2
@@ -132,13 +132,10 @@ EXPORT_SYMBOL_GPL(rv_uprobe_attach);
   */
  void rv_uprobe_detach(struct rv_uprobe *p)
  {
-	struct rv_uprobe_impl *impl;
-
  	if (!p)
  		return;

kernel/trace/rv/rv_uprobe.c +1 -2
-	impl = container_of(p, struct rv_uprobe_impl, pub);
-	uprobe_unregister_nosync(impl->uprobe, &impl->uc);
+	rv_uprobe_unregister_nosync(p);

  /*
  * uprobe_unregister_sync() is a global barrier: it waits for all
  * in-flight uprobe handlers across the entire system to complete,

kernel/trace/rv/rv_uprobe.c +4 -0
@@ -146,8 +143,47 @@ void rv_uprobe_detach(struct rv_uprobe *p)
  	 * guarantees that no handler touching impl->pub.priv is running by
  	 * the time we return, even if the caller immediately frees priv.
  	 */
+	rv_uprobe_sync();
+	rv_uprobe_free(p);
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_detach);

[...]

Quoted 6 lines
tools/include/uapi/linux/rv.h new file +0 -0
new file mode 100644
index 000000000000..a34e5426393b

tools/include/uapi/linux/rv.h line 10

Quoted 10 lines
tools/include/uapi/linux/rv.h new file +10 -0
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * UAPI definitions for Runtime Verification (RV) monitors.
+ *
+ * All RV monitors that expose an ioctl self-instrumentation interface
+ * share the magic byte RV_IOC_MAGIC ('r').
+ *
+ * Usage examples and design rationale are in:
+ *   Documentation/trace/rv/monitor_tlob.rst
+ */

And this in a new ioctl patch.

tools/include/uapi/linux/rv.h line 13

Quoted 3 lines
tools/include/uapi/linux/rv.h new file +3 -0
+
+#ifndef _UAPI_LINUX_RV_H
+#define _UAPI_LINUX_RV_H

Thanks,
Gabriele

maintainer
#25 lore raw

Re: [RFC PATCH v2 08/10] rv/tlob: add tlob hybrid automaton monitor

In reply to [email protected]
Quoted 35 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Documentation/trace/rv/monitor_tlob.rst new file +26 -0
new file mode 100644
index 000000000000..91b592630b3f
+Usage
+-----
+
+tracefs interface (uprobe-based external monitoring)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``monitor`` tracefs file instruments an unmodified binary via uprobes.
+The format follows the ftrace ``uprobe_events`` convention (``PATH:OFFSET``
+for the probe location, ``key=value`` for configuration parameters)::
+
+  p PATH:OFFSET_START OFFSET_STOP threshold=US
+
+The uprobe at ``OFFSET_START`` fires ``tlob_start_task()``; the uprobe at
+``OFFSET_STOP`` fires ``tlob_stop_task()``.  Both offsets are ELF file
+offsets of entry points in ``PATH``.  ``PATH`` may contain ``:``; the last
+``:`` in the ``PATH:OFFSET_START`` token is the separator.
+
+To remove a binding, use ``-PATH:OFFSET_START``::
+
+  echo 1 > /sys/kernel/tracing/rv/monitors/tlob/enable
+
+  echo "p /usr/bin/myapp:0x12a0 0x12f0 threshold=5000" \
+      > /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+  # Remove a binding
+  echo "-/usr/bin/myapp:0x12a0" >

/sys/kernel/tracing/rv/monitors/tlob/monitor

Documentation/trace/rv/monitor_tlob.rst line 95

Quoted 9 lines
Documentation/trace/rv/monitor_tlob.rst new file +9 -0
+
+  # List registered bindings
+  cat /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+  # Read violations from the trace buffer
+  cat /sys/kernel/tracing/trace
+
+ioctl self-instrumentation (/dev/rv)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I'm not particularly fond of ioctls, they aren't that flexible and in this way I don't really see an added value.

In short, you're adding this so a program could instrument itself using ioctls instead of using uprobes, cannot the same thing be achieved using uprobes alone, e.g. by registering a function address or the current instruction pointer?

If you really cannot do it with uprobes alone, wouldn't a sysfs/tracefs file achieve a similar purpose without much of the boilerplate code?

Quoted 22 lines

+
+``/dev/rv`` is a shared RV character device.  Before using any monitor- specific

Documentation/trace/rv/monitor_tlob.rst new file +19 -0
+ioctl, the fd must be bound to a monitor via ``RV_IOCTL_BIND_MONITOR``.  Each
+open fd has independent per-fd monitoring state::
+
+  int fd = open("/dev/rv", O_RDWR);
+
+  /* Bind this fd to the tlob monitor. */
+  struct rv_bind_args bind = { .monitor_name = "tlob" };
+  ioctl(fd, RV_IOCTL_BIND_MONITOR, &bind);
+
+  struct tlob_start_args args = {
+      .threshold_us = 50000,   /* 50 ms in microseconds */
+  };
+  ioctl(fd, TLOB_IOCTL_TRACE_START, &args);
+
+  /* ... code path under observation ... */
+
+  int ret = ioctl(fd, TLOB_IOCTL_TRACE_STOP, NULL);
+  /* ret == 0:          within budget  */
+  /* ret == -EOVERFLOW: budget exceeded */

Documentation/trace/rv/monitor_tlob.rst line 126

Quoted 10 lines
Documentation/trace/rv/monitor_tlob.rst new file +10 -0
+
+  close(fd);
+
+``TRACE_STOP`` returns ``-EOVERFLOW`` whenever the budget was exceeded.
+The HA timer calls ``da_monitor_reset()`` (storage remains); the
+synchronous ``ha_cancel_timer_sync()`` in ``tlob_stop_task()`` ensures the
+callback has completed before checking ``da_monitoring()``.
+
+Violation events
+~~~~~~~~~~~~~~~~

Since you are not documenting the detail_env_tlob tracepoint, is it something really required?
It's deviating from the original RV purpose (run a model and spot violations) by adding further accounting, I'm fine with that if there is a documented need.
In such case I would at the very least document its usage (thought I'd really like to be rid of it and let the curious user implement the accounting themselves).

Documentation/trace/rv/monitor_tlob.rst line 139

Quoted 13 lines
Documentation/trace/rv/monitor_tlob.rst new file +13 -0
+
+Budget violations are always reported via the ``error_env_tlob`` RV
+tracepoint (HA clock-invariant violation), regardless of which interface
+triggered them::
+
+  cat /sys/kernel/tracing/trace
+
+To capture violations in a file::
+
+  trace-cmd record -e error_env_tlob &
+  # ... run workload ...
+  trace-cmd report
+

This is standard tracepoints usage, there's nothing about tlob we should document here. If you feel the existing RV documentation should expand this subject, feel free to contribute there.

Documentation/trace/rv/monitor_tlob.rst line 150

Quoted 11 lines
Documentation/trace/rv/monitor_tlob.rst new file +11 -0
+tracefs files
+-------------
+
+The following files are created under
+``/sys/kernel/tracing/rv/monitors/tlob/``:
+
+``enable`` (rw)
+  Write ``1`` to enable the monitor; write ``0`` to disable it.
+
+``desc`` (ro)
+  Human-readable description of the monitor.

Same here, standard RV.

Quoted 5 lines

+
+``monitor`` (rw)
+  Write ``p PATH:OFFSET_START OFFSET_STOP threshold=US``
+  to bind two entry uprobes.  Write ``-PATH:OFFSET_START`` to remove a +  binding.  Read to list registered bindings in the same format.

And this is duplicating what mentioned above about uprobes, isn't it?

Quoted 17 lines
Documentation/trace/rv/monitor_tlob.rst new file +17 -0
+
+Kernel API
+----------
+
+.. kernel-doc:: kernel/trace/rv/monitors/tlob/tlob.c
+   :functions: tlob_start_task tlob_stop_task
+
+``tlob_start_task(task, threshold_us)``
+  Begin monitoring *task* with a total latency budget of *threshold_us*
+  microseconds.  Allocates per-task state, sets initial DA state to
+  ``running``, resets ``clk_elapsed``, and arms the HA budget timer.
+  Returns 0, -ENODEV (monitor disabled), -ERANGE (zero threshold),
+  -EALREADY (already monitoring), -ENOSPC (at capacity), or -ENOMEM.
+
+``tlob_stop_task(task)``
+  Stop monitoring *task*.  Synchronously cancels the HA timer via
+  ``ha_cancel_timer_sync()``, checks ``da_monitoring()`` to determine
Quoted 4 lines

outcome.
+  Returns 0 (clean stop, within budget), -EOVERFLOW (budget was exceeded), +  -ESRCH (not monitored), or -EAGAIN (concurrent stop racing). +

Is kernel code going to use this API? RV monitors are meant to be enabled by userspace. What's the use-case here?

Quoted 5 lines
Documentation/trace/rv/monitor_tlob.rst new file +5 -0
+Design notes
+------------
+
+State transitions are driven by two tracepoints:
+

Documentation/trace/rv/monitor_tlob.rst line 190

Quoted 10 lines
Documentation/trace/rv/monitor_tlob.rst new file +10 -0
+- ``sched_switch``: ``prev_state == 0`` (``TASK_RUNNING``, preempted,
+  stays on runqueue) → running→waiting; ``prev_state != 0`` (voluntarily
+  blocked, leaves runqueue) → running→sleeping; ``next`` pointer →
+  waiting→running.
+- ``sched_wakeup``: task moves back onto the runqueue → sleeping→waiting.
+
+No ``waiting → sleeping`` edge exists because a task can only block
+itself while executing on CPU.  ``try_to_wake_up()`` is also a no-op
+when ``__state == TASK_RUNNING``, so ``sched_wakeup`` never fires while
+the task is in ``waiting`` state.

That's probably a bit too detailed for this page.
If you really want this information somewhere couldn't it stay in the code?

Thanks,
Gabriele

#26 lore raw

RFC PATCH v2 9/10 rv/tlob: add KUnit tests for the tlob monitor

In reply to [email protected]

From: Wen Yang <[email protected]>

Add five KUnit test suites gated behind CONFIG_TLOB_KUNIT_TEST (depends on RV_MON_TLOB && KUNIT; default KUNIT_ALL_TESTS) with a .kunitconfig fragment for the kunit.py runner.

tlob_task_api tests the start/stop API, error returns (-EEXIST, -ESRCH, -EOVERFLOW, -ENOSPC, -ERANGE). tlob_sched_integration covers context-switch accounting and monitoring a kthread. tlob_parse_uprobe exercises the uprobe line parser. tlob_trace_output checks sched_switch and error_env_tlob field layout. tlob_violation_react verifies error_env_tlob fires once on budget expiry and zero times when the budget is not exceeded.

---
 kernel/trace/rv/monitors/tlob/.kunitconfig |   5 +
 kernel/trace/rv/monitors/tlob/tlob.c       |  26 +
 kernel/trace/rv/monitors/tlob/tlob_kunit.c | 881 +++++++++++++++++++++
 3 files changed, 912 insertions(+)
 create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c
kernel/trace/rv/monitors/tlob/.kunitconfig new file +5 -0
new file mode 100644
index 000000000000..977c58601ab7
@@ -0,0 +1,5 @@
+CONFIG_FTRACE=y
+CONFIG_KUNIT=y
+CONFIG_RV=y
+CONFIG_RV_MON_TLOB=y
+CONFIG_TLOB_KUNIT_TEST=y
kernel/trace/rv/monitors/tlob/tlob.c +26 -0
index 475e972ae9aa..90e7035a0b55 100644
@@ -1024,6 +1024,7 @@ EXPORT_SYMBOL_IF_KUNIT(tlob_num_monitored_read);
 /* Tracepoint probes for KUnit; rv_trace.h is only included here. */
 static struct tlob_captured_event     tlob_kunit_last_event;
 static struct tlob_captured_error_env tlob_kunit_last_error_env;
+static struct tlob_captured_detail    tlob_kunit_last_detail;
 static atomic_t tlob_kunit_event_cnt    = ATOMIC_INIT(0);
 static atomic_t tlob_kunit_error_env_cnt = ATOMIC_INIT(0);
 
@@ -1054,6 +1055,17 @@ static void tlob_kunit_error_env_probe(void *data, int id, char *state,
 	atomic_inc(&tlob_kunit_error_env_cnt);
 }
 
+static void tlob_kunit_detail_probe(void *data, int pid, u64 threshold_us,
+				    u64 running_ns, u64 waiting_ns,
+				    u64 sleeping_ns)
+{
+	tlob_kunit_last_detail.pid		= pid;
+	tlob_kunit_last_detail.threshold_us	= threshold_us;
+	tlob_kunit_last_detail.running_ns	= running_ns;
+	tlob_kunit_last_detail.waiting_ns	= waiting_ns;
+	tlob_kunit_last_detail.sleeping_ns	= sleeping_ns;
+}
+
 int tlob_register_kunit_probes(void)
 {
 	int ret;
@@ -1069,6 +1081,12 @@ int tlob_register_kunit_probes(void)
 		unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
 		return ret;
 	}
+	ret = register_trace_detail_env_tlob(tlob_kunit_detail_probe, NULL);
+	if (ret) {
+		unregister_trace_error_env_tlob(tlob_kunit_error_env_probe, NULL);
+		unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
+		return ret;
+	}
 	return 0;
 }
 EXPORT_SYMBOL_IF_KUNIT(tlob_register_kunit_probes);
@@ -1077,6 +1095,7 @@ void tlob_unregister_kunit_probes(void)
 {
 	unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
 	unregister_trace_error_env_tlob(tlob_kunit_error_env_probe, NULL);
+	unregister_trace_detail_env_tlob(tlob_kunit_detail_probe, NULL);
 	tracepoint_synchronize_unregister();
 }
 EXPORT_SYMBOL_IF_KUNIT(tlob_unregister_kunit_probes);
@@ -1105,6 +1124,7 @@ void tlob_error_env_count_reset(void)
 }
 EXPORT_SYMBOL_IF_KUNIT(tlob_error_env_count_reset);
 
+
 const struct tlob_captured_event *tlob_last_event_read(void)
 {
 	return &tlob_kunit_last_event;
@@ -1117,6 +1137,12 @@ const struct tlob_captured_error_env *tlob_last_error_env_read(void)
 }
 EXPORT_SYMBOL_IF_KUNIT(tlob_last_error_env_read);
 
+const struct tlob_captured_detail *tlob_last_detail_read(void)
+{
+	return &tlob_kunit_last_detail;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_last_detail_read);
+
 #endif /* CONFIG_KUNIT */
 
 VISIBLE_IF_KUNIT int tlob_enable_hooks(void)
kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +881 -0

881 lines not on this page · raw message

Signature · 1 line
2.25.1
maintainer
#27 lore raw

Re: [RFC PATCH v2 09/10] rv/tlob: add KUnit tests for the tlob monitor

Quoted 14 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Add five KUnit test suites gated behind CONFIG_TLOB_KUNIT_TEST (depends on RV_MON_TLOB && KUNIT; default KUNIT_ALL_TESTS) with a .kunitconfig fragment for the kunit.py runner.

tlob_task_api tests the start/stop API, error returns (-EEXIST, -ESRCH, -EOVERFLOW, -ENOSPC, -ERANGE). tlob_sched_integration covers context-switch accounting and monitoring a kthread.  tlob_parse_uprobe exercises the uprobe line parser. tlob_trace_output checks sched_switch and error_env_tlob field layout. tlob_violation_react verifies error_env_tlob fires once on budget expiry and zero times when the budget is not exceeded.

Quoted 2 lines

That's quite extensive, but what caught my eyes are tests enrolling tracepoints handlers. If you go there you're no longer doing unit testing, what's the advantage of testing the entire monitor here over doing that in selftests?

Thanks,
Gabriele

Quoted 998 lines

---
 kernel/trace/rv/monitors/tlob/.kunitconfig |   5 + kernel/trace/rv/monitors/tlob/tlob.c       |  26 + kernel/trace/rv/monitors/tlob/tlob_kunit.c | 881 +++++++++++++++++++++

 3 files changed, 912 insertions(+)

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

kernel/trace/rv/monitors/tlob/.kunitconfig new file +5 -0
new file mode 100644
index 000000000000..977c58601ab7
@@ -0,0 +1,5 @@
+CONFIG_FTRACE=y
+CONFIG_KUNIT=y
+CONFIG_RV=y
+CONFIG_RV_MON_TLOB=y
+CONFIG_TLOB_KUNIT_TEST=y
kernel/trace/rv/monitors/tlob/tlob.c +1 -0
index 475e972ae9aa..90e7035a0b55 100644
@@ -1024,6 +1024,7 @@ EXPORT_SYMBOL_IF_KUNIT(tlob_num_monitored_read);
  /* Tracepoint probes for KUnit; rv_trace.h is only included here. */
  static struct tlob_captured_event     tlob_kunit_last_event;
  static struct tlob_captured_error_env tlob_kunit_last_error_env;
+static struct tlob_captured_detail    tlob_kunit_last_detail;
  static atomic_t tlob_kunit_event_cnt    = ATOMIC_INIT(0);
  static atomic_t tlob_kunit_error_env_cnt = ATOMIC_INIT(0);

kernel/trace/rv/monitors/tlob/tlob.c +0 -0
@@ -1054,6 +1055,17 @@ static void tlob_kunit_error_env_probe(void *data, int
 id, char *state,
  	atomic_inc(&tlob_kunit_error_env_cnt);
  }

kernel/trace/rv/monitors/tlob/tlob.c +11 -0
+static void tlob_kunit_detail_probe(void *data, int pid, u64 threshold_us,
+				    u64 running_ns, u64 waiting_ns,
+				    u64 sleeping_ns)
+{
+	tlob_kunit_last_detail.pid		= pid;
+	tlob_kunit_last_detail.threshold_us	= threshold_us;
+	tlob_kunit_last_detail.running_ns	= running_ns;
+	tlob_kunit_last_detail.waiting_ns	= waiting_ns;
+	tlob_kunit_last_detail.sleeping_ns	= sleeping_ns;
+}
+

 int tlob_register_kunit_probes(void)
 {
  int ret;

kernel/trace/rv/monitors/tlob/tlob.c +6 -0
@@ -1069,6 +1081,12 @@ int tlob_register_kunit_probes(void)
  		unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
  		return ret;
  	}
+	ret = register_trace_detail_env_tlob(tlob_kunit_detail_probe, NULL);
+	if (ret) {
+		unregister_trace_error_env_tlob(tlob_kunit_error_env_probe,
 NULL);
+		unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
+		return ret;
+	}
  	return 0;
  }

 EXPORT_SYMBOL_IF_KUNIT(tlob_register_kunit_probes);

kernel/trace/rv/monitors/tlob/tlob.c +1 -0
@@ -1077,6 +1095,7 @@ void tlob_unregister_kunit_probes(void)
  {
  	unregister_trace_event_tlob(tlob_kunit_event_probe, NULL);
  	unregister_trace_error_env_tlob(tlob_kunit_error_env_probe, NULL);
+	unregister_trace_detail_env_tlob(tlob_kunit_detail_probe, NULL);
  	tracepoint_synchronize_unregister();
  }
  EXPORT_SYMBOL_IF_KUNIT(tlob_unregister_kunit_probes);
@@ -1105,6 +1124,7 @@ void tlob_error_env_count_reset(void)
  }
  EXPORT_SYMBOL_IF_KUNIT(tlob_error_env_count_reset);


+
 const struct tlob_captured_event *tlob_last_event_read(void) {
  return &tlob_kunit_last_event;

kernel/trace/rv/monitors/tlob/tlob.c +0 -0
@@ -1117,6 +1137,12 @@ const struct tlob_captured_error_env
 *tlob_last_error_env_read(void)
  }
  EXPORT_SYMBOL_IF_KUNIT(tlob_last_error_env_read);

kernel/trace/rv/monitors/tlob/tlob.c +6 -0
+const struct tlob_captured_detail *tlob_last_detail_read(void)
+{
+	return &tlob_kunit_last_detail;
+}
+EXPORT_SYMBOL_IF_KUNIT(tlob_last_detail_read);
+

 #endif /* CONFIG_KUNIT */

 VISIBLE_IF_KUNIT int tlob_enable_hooks(void)

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +7 -0
new file mode 100644
index 000000000000..ed2e7c7abaf8
@@ -0,0 +1,881 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the tlob RV monitor.
+ *
+ * tlob_task_api:          start/stop lifecycle, error paths, violations.
+ * tlob_sched_integration: per-state accounting across real context switches.
+ * tlob_uprobe_format:     uprobe binding format; add/remove acceptance and

rejection.
+ * tlob_trace_output:      trace event format for event_tlob, error_env_tlob.
+ * tlob_violation_react:   error count per budget expiry; per-state breakdown.
+ *
+ * tlob_add_uprobe() duplicate-(binary, offset_start) constraint is not covered
+ * here: kern_path() requires a real filesystem; see selftests instead.
+ */

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +18 -0
+#include <kunit/test.h>
+#include <linux/atomic.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/kthread.h>
+#include <linux/ktime.h>
+#include <linux/mutex.h>
+#include <linux/sched.h>
+#include <linux/sched/rt.h>
+#include <linux/sched/task.h>
+
+#include "tlob.h"
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+
+/*
+ * Kthread cleanup guard: registers a kunit action that stops a kthread on
+ * test exit, even when a KUNIT_ASSERT fires before normal teardown code

runs.
+ *
+ * Caller must call get_task_struct() before registering the guard.
+ * Set guard->task = NULL before normal-path teardown to prevent double-stop.
+ * Pass the completion to unblock on early exit, or NULL if not needed.
+ */

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +51 -0
+struct tlob_kthread_guard {
+	struct task_struct	*task;
+	struct completion	*unblock;
+};
+
+static void kthread_guard_fn(void *arg)
+{
+	struct tlob_kthread_guard *g = arg;
+
+	if (!g->task)
+		return;
+	if (g->unblock)
+		complete(g->unblock);
+	kthread_stop(g->task);
+	put_task_struct(g->task);
+}
+
+static struct tlob_kthread_guard *
+tlob_guard_kthread(struct kunit *test, struct task_struct *task,
+		   struct completion *unblock)
+{
+	struct tlob_kthread_guard *g;
+
+	g = kunit_kzalloc(test, sizeof(*g), GFP_KERNEL);
+	if (!g)
+		return NULL;
+	g->task = task;
+	g->unblock = unblock;
+	if (kunit_add_action_or_reset(test, kthread_guard_fn, g))
+		return NULL;
+	return g;
+}
+
+/* Suite 1: task API - lifecycle, error paths, violations. */
+
+/* Basic start/stop cycle */
+static void tlob_start_stop_ok(struct kunit *test)
+{
+	int ret;
+
+	ret = tlob_start_task(current, 10000000ULL);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), 0);
+	KUNIT_EXPECT_EQ(test, tlob_num_monitored_read(), 0);
+}
+
+/* Double start must return -EALREADY; double stop must return -ESRCH. */
+static void tlob_double_start(struct kunit *test)
+{
+	KUNIT_ASSERT_EQ(test, tlob_start_task(current, 10000000ULL), 0);
+	KUNIT_EXPECT_EQ(test, tlob_start_task(current, 10000000ULL), -

EALREADY);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +82 -0
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), 0);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -ESRCH);
+	KUNIT_EXPECT_EQ(test, tlob_num_monitored_read(), 0);
+}
+
+/* Stop without start must return -ESRCH. */
+static void tlob_stop_without_start(struct kunit *test)
+{
+	tlob_stop_task(current);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -ESRCH);
+	KUNIT_EXPECT_EQ(test, tlob_num_monitored_read(), 0);
+}
+
+/* threshold_us == 0 is invalid and must return -ERANGE. */
+static void tlob_zero_threshold(struct kunit *test)
+{
+	KUNIT_EXPECT_EQ(test, tlob_start_task(current, 0), -ERANGE);
+}
+
+/* 1 ns budget: timer almost certainly fires before tlob_stop_task(). */
+static void tlob_immediate_deadline(struct kunit *test)
+{
+	int ret = tlob_start_task(current, 1);
+
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	udelay(100);
+	/* timer fired -> -EOVERFLOW; if we won the race, 0 is also valid */
+	ret = tlob_stop_task(current);
+	KUNIT_EXPECT_TRUE(test, ret == 0 || ret == -EOVERFLOW);
+	KUNIT_EXPECT_EQ(test, tlob_num_monitored_read(), 0);
+}
+
+/*
+ * kthreads provide distinct task_structs; fill to TLOB_MAX_MONITORED,
+ * then verify -ENOSPC.
+ */
+struct tlob_waiter_ctx {
+	struct completion start;
+	struct completion done;
+};
+
+static int tlob_waiter_fn(void *arg)
+{
+	struct tlob_waiter_ctx *ctx = arg;
+
+	wait_for_completion(&ctx->start);
+	complete(&ctx->done);
+	return 0;
+}
+
+static void tlob_enospc(struct kunit *test)
+{
+	struct tlob_waiter_ctx *ctxs;
+	struct task_struct **threads;
+	int i, ret;
+
+	ctxs = kunit_kcalloc(test, TLOB_MAX_MONITORED,
+			     sizeof(*ctxs), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, ctxs);
+
+	threads = kunit_kcalloc(test, TLOB_MAX_MONITORED,
+				sizeof(*threads), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, threads);
+
+	KUNIT_ASSERT_EQ(test, tlob_num_monitored_read(), 0);
+
+	for (i = 0; i < TLOB_MAX_MONITORED; i++) {
+		init_completion(&ctxs[i].start);
+		init_completion(&ctxs[i].done);
+
+		threads[i] = kthread_run(tlob_waiter_fn, &ctxs[i],
+					 "tlob_waiter_%d", i);
+		if (IS_ERR(threads[i])) {
+			KUNIT_FAIL(test, "kthread_run failed at i=%d", i);
+			threads[i] = NULL;
+			goto cleanup;
+		}
+		get_task_struct(threads[i]);
+
+		ret = tlob_start_task(threads[i], 10000000ULL);
+		if (ret != 0) {
+			KUNIT_FAIL(test, "tlob_start_task failed at i=%d:

%d",

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +111 -0
+				   i, ret);
+			put_task_struct(threads[i]);
+			complete(&ctxs[i].start);
+			threads[i] = NULL;
+			goto cleanup;
+		}
+	}
+
+	ret = tlob_start_task(current, 10000000ULL);
+	KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+
+cleanup:
+	/* cancel monitoring and unblock first, then wait for full exit */
+	for (i = 0; i < TLOB_MAX_MONITORED; i++) {
+		if (!threads[i])
+			break;
+		tlob_stop_task(threads[i]);
+		complete(&ctxs[i].start);
+	}
+	for (i = 0; i < TLOB_MAX_MONITORED; i++) {
+		if (!threads[i])
+			break;
+		kthread_stop(threads[i]);
+		put_task_struct(threads[i]);
+	}
+}
+
+/*
+ * Holder kthread holds a mutex for 80 ms; arm a 10 ms budget, burn ~1 ms
+ * on-CPU, then block on the mutex; timer fires while sleeping -> -EOVERFLOW.
+ */
+struct tlob_holder_ctx {
+	struct mutex		lock;
+	struct completion	ready;
+	unsigned int		hold_ms;
+};
+
+static int tlob_holder_fn(void *arg)
+{
+	struct tlob_holder_ctx *ctx = arg;
+
+	mutex_lock(&ctx->lock);
+	complete(&ctx->ready);
+	msleep(ctx->hold_ms);
+	mutex_unlock(&ctx->lock);
+	return 0;
+}
+
+static void tlob_deadline_fires_sleeping(struct kunit *test)
+{
+	struct tlob_holder_ctx *ctx;
+	struct tlob_kthread_guard *guard;
+	struct task_struct *holder;
+	ktime_t t0;
+	int ret;
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	ctx->hold_ms = 80;
+	mutex_init(&ctx->lock);
+	init_completion(&ctx->ready);
+
+	holder = kthread_run(tlob_holder_fn, ctx, "tlob_holder_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, holder);
+	get_task_struct(holder);
+
+	guard = tlob_guard_kthread(test, holder, NULL);
+	KUNIT_ASSERT_NOT_NULL(test, guard);
+
+	wait_for_completion(&ctx->ready);
+
+	ret = tlob_start_task(current, 10000);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 1000)
+		cpu_relax();
+
+	/* block on mutex: running->sleeping; timer fires while sleeping */
+	mutex_lock(&ctx->lock);
+	mutex_unlock(&ctx->lock);
+
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -EOVERFLOW);
+
+	guard->task = NULL;
+	kthread_stop(holder);
+	put_task_struct(holder);
+}
+
+/*
+ * yield() triggers a preempt sched_switch (prev_state==0): running->waiting.
+ * Busy-spin 50 ms so the 2 ms budget fires regardless of scheduler timing.
+ */
+static void tlob_deadline_fires_waiting(struct kunit *test)
+{
+	ktime_t t0;
+	int ret;
+
+	ret = tlob_start_task(current, 2000);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	yield();
+
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 50000)
+		cpu_relax();
+
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -EOVERFLOW);
+}
+
+/* Arm a 1 ms budget and busy-spin for 50 ms; timer fires in running state.

*/

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +88 -0
+static void tlob_deadline_fires_running(struct kunit *test)
+{
+	ktime_t t0;
+	int ret;
+
+	ret = tlob_start_task(current, 1000);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 50000)
+		cpu_relax();
+
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -EOVERFLOW);
+}
+
+/* Start three tasks, reinit monitor, verify all entries are gone. */
+static int tlob_dummy_fn(void *arg)
+{
+	wait_for_completion((struct completion *)arg);
+	return 0;
+}
+
+static void tlob_reinit_clears_all(struct kunit *test)
+{
+	struct completion *done1, *done2;
+	struct tlob_kthread_guard *guard1, *guard2;
+	struct task_struct *t1, *t2;
+	int ret;
+
+	done1 = kunit_kzalloc(test, sizeof(*done1), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, done1);
+	done2 = kunit_kzalloc(test, sizeof(*done2), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, done2);
+
+	init_completion(done1);
+	init_completion(done2);
+
+	t1 = kthread_run(tlob_dummy_fn, done1, "tlob_dummy1");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t1);
+	get_task_struct(t1);
+	guard1 = tlob_guard_kthread(test, t1, done1);
+	KUNIT_ASSERT_NOT_NULL(test, guard1);
+
+	t2 = kthread_run(tlob_dummy_fn, done2, "tlob_dummy2");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t2);
+	get_task_struct(t2);
+	guard2 = tlob_guard_kthread(test, t2, done2);
+	KUNIT_ASSERT_NOT_NULL(test, guard2);
+
+	KUNIT_ASSERT_EQ(test, tlob_start_task(current, 10000000ULL), 0);
+	KUNIT_ASSERT_EQ(test, tlob_start_task(t1, 10000000ULL), 0);
+	KUNIT_ASSERT_EQ(test, tlob_start_task(t2, 10000000ULL), 0);
+
+	tlob_destroy_monitor();
+	ret = tlob_init_monitor();
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -ESRCH);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(t1), -ESRCH);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(t2), -ESRCH);
+
+	/* null guards before teardown to prevent double-stop */
+	guard1->task = NULL;
+	guard2->task = NULL;
+	complete(done1);
+	complete(done2);
+	kthread_stop(t1);
+	kthread_stop(t2);
+	put_task_struct(t1);
+	put_task_struct(t2);
+}
+
+static int tlob_task_api_suite_init(struct kunit_suite *suite)
+{
+	rv_kunit_monitoring_on();
+	return tlob_init_monitor();
+}
+
+static void tlob_task_api_suite_exit(struct kunit_suite *suite)
+{
+	tlob_destroy_monitor();
+	rv_kunit_monitoring_off();
+}
+
+static void tlob_task_api_exit(struct kunit *test)
+{
+	/*
+	 * tlob_stop_task() returns pool slots via call_rcu

(da_pool_return_cb).

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +1 -0
+	 * Wait for all pending callbacks so each test starts with a full

pool.

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +66 -0
+	 */
+	rcu_barrier();
+}
+
+static struct kunit_case tlob_task_api_cases[] = {
+	KUNIT_CASE(tlob_start_stop_ok),
+	KUNIT_CASE(tlob_double_start),
+	KUNIT_CASE(tlob_stop_without_start),
+	KUNIT_CASE(tlob_zero_threshold),
+	KUNIT_CASE(tlob_immediate_deadline),
+	KUNIT_CASE(tlob_enospc),
+	KUNIT_CASE(tlob_deadline_fires_sleeping),
+	KUNIT_CASE(tlob_deadline_fires_waiting),
+	KUNIT_CASE(tlob_deadline_fires_running),
+	KUNIT_CASE(tlob_reinit_clears_all),
+	{}
+};
+
+static struct kunit_suite tlob_task_api_suite = {
+	.name       = "tlob_task_api",
+	.suite_init = tlob_task_api_suite_init,
+	.suite_exit = tlob_task_api_suite_exit,
+	.exit       = tlob_task_api_exit,
+	.test_cases = tlob_task_api_cases,
+};
+
+/* Suite 2: sched integration - per-state ns accounting. */
+
+struct tlob_ping_ctx {
+	struct completion ping;
+	struct completion pong;
+};
+
+static int tlob_ping_fn(void *arg)
+{
+	struct tlob_ping_ctx *ctx = arg;
+
+	wait_for_completion(&ctx->ping);
+	complete(&ctx->pong);
+	return 0;
+}
+
+/* Force two context switches and verify stop returns 0 (within budget). */
+static void tlob_sched_switch_accounting(struct kunit *test)
+{
+	struct tlob_ping_ctx *ctx;
+	struct tlob_kthread_guard *guard;
+	struct task_struct *peer;
+	int ret;
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	init_completion(&ctx->ping);
+	init_completion(&ctx->pong);
+
+	peer = kthread_run(tlob_ping_fn, ctx, "tlob_ping_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, peer);
+	get_task_struct(peer);
+
+	guard = tlob_guard_kthread(test, peer, &ctx->ping);
+	KUNIT_ASSERT_NOT_NULL(test, guard);
+
+	ret = tlob_start_task(current, 5000000ULL);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/* complete(ping) -> peer runs, forcing a context switch out and back

*/

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +103 -0
+	complete(&ctx->ping);
+	wait_for_completion(&ctx->pong);
+
+	ret = tlob_stop_task(current);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+
+	guard->task = NULL;
+	kthread_stop(peer);
+	put_task_struct(peer);
+}
+
+/* start/stop monitoring a kthread other than current */
+static int tlob_block_fn(void *arg)
+{
+	struct completion *done = arg;
+
+	msleep(20);
+	complete(done);
+	return 0;
+}
+
+static void tlob_monitor_other_task(struct kunit *test)
+{
+	struct completion *done;
+	struct tlob_kthread_guard *guard;
+	struct task_struct *target;
+	int ret;
+
+	done = kunit_kzalloc(test, sizeof(*done), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, done);
+	init_completion(done);
+
+	target = kthread_run(tlob_block_fn, done, "tlob_target_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, target);
+	get_task_struct(target);
+
+	guard = tlob_guard_kthread(test, target, NULL);
+	KUNIT_ASSERT_NOT_NULL(test, guard);
+
+	ret = tlob_start_task(target, 5000000ULL);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	wait_for_completion(done);
+
+	/* 5 s budget won't fire in 20 ms; 0 or -EOVERFLOW are both valid */
+	ret = tlob_stop_task(target);
+	KUNIT_EXPECT_TRUE(test, ret == 0 || ret == -EOVERFLOW);
+
+	guard->task = NULL;
+	kthread_stop(target);
+	put_task_struct(target);
+}
+
+static int tlob_sched_suite_init(struct kunit_suite *suite)
+{
+	rv_kunit_monitoring_on();
+	return tlob_init_monitor();
+}
+
+static void tlob_sched_suite_exit(struct kunit_suite *suite)
+{
+	tlob_destroy_monitor();
+	rv_kunit_monitoring_off();
+}
+
+static struct kunit_case tlob_sched_integration_cases[] = {
+	KUNIT_CASE(tlob_sched_switch_accounting),
+	KUNIT_CASE(tlob_monitor_other_task),
+	{}
+};
+
+static struct kunit_suite tlob_sched_integration_suite = {
+	.name       = "tlob_sched_integration",
+	.suite_init = tlob_sched_suite_init,
+	.suite_exit = tlob_sched_suite_exit,
+	.test_cases = tlob_sched_integration_cases,
+};
+
+/* Suite 3: uprobe binding format - add/remove acceptance and rejection. */
+
+static const char * const tlob_format_valid[] = {
+	"p /usr/bin/myapp:4768 4848 threshold=5000",
+	"p /usr/bin/myapp:0x12a0 0x12f0 threshold=10000",
+	"p /opt/my:app/bin:0x100 0x200 threshold=1000",
+};
+
+static const char * const tlob_format_invalid[] = {
+	/* add: malformed */
+	"p /usr/bin/myapp:0x100 0x200 threshold=0",
+	"p :0x100 0x200 threshold=5000",
+	"p /usr/bin/myapp:0x100 threshold=5000",
+	"p /usr/bin/myapp:-1 0x200 threshold=5000",
+	"p /usr/bin/myapp:0x100 0x200",
+	"p /usr/bin/myapp:0x100 0x100 threshold=5000",
+	/* remove: malformed */
+	"-usr/bin/myapp:0x100",
+	"-/usr/bin/myapp",
+	"-/:0x100",
+	"-/usr/bin/myapp:abc",
+};
+
+/*
+ * Valid add lines return -ENOENT (path does not exist in the test

environment)
+ * rather than 0; a non-(-EINVAL) return confirms the format was accepted.
+ */

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +8 -0
+static void tlob_format_accepted(struct kunit *test)
+{
+	char buf[128];
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(tlob_format_valid); i++) {
+		strscpy(buf, tlob_format_valid[i], sizeof(buf));
+		KUNIT_EXPECT_NE(test, tlob_create_or_delete_uprobe(buf), -

EINVAL);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +11 -0
+	}
+}
+
+static void tlob_format_rejected(struct kunit *test)
+{
+	char buf[128];
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(tlob_format_invalid); i++) {
+		strscpy(buf, tlob_format_invalid[i], sizeof(buf));
+		KUNIT_EXPECT_EQ(test, tlob_create_or_delete_uprobe(buf), -

EINVAL);

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +15 -0
+	}
+}
+
+static struct kunit_case tlob_uprobe_format_cases[] = {
+	KUNIT_CASE(tlob_format_accepted),
+	KUNIT_CASE(tlob_format_rejected),
+	{}
+};
+
+static struct kunit_suite tlob_uprobe_format_suite = {
+	.name       = "tlob_uprobe_format",
+	.test_cases = tlob_uprobe_format_cases,
+};
+
+/* Suite 4: trace output - verify event_tlob and error_env_tlob field values.

*/

kernel/trace/rv/monitors/tlob/tlob_kunit.c new file +307 -0
+
+static void tlob_trace_event_format(struct kunit *test)
+{
+	const struct tlob_captured_event *ev;
+	int pid = current->pid;
+	int ret;
+
+	tlob_event_count_reset();
+	ret = tlob_start_task(current, 5000000ULL);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/* sleep/wakeup/switch_in: running->sleeping->waiting->running */
+	msleep(20);
+
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), 0);
+
+	KUNIT_EXPECT_GE(test, tlob_event_count_read(), 3);
+
+	ev = tlob_last_event_read();
+	KUNIT_EXPECT_EQ(test,    ev->id,          pid);
+	KUNIT_EXPECT_STREQ(test, ev->state,       "waiting");
+	KUNIT_EXPECT_STREQ(test, ev->event,       "switch_in");
+	KUNIT_EXPECT_STREQ(test, ev->next_state,  "running");
+	KUNIT_EXPECT_TRUE(test,  ev->final_state);
+}
+
+static void tlob_trace_error_env_format(struct kunit *test)
+{
+	const struct tlob_captured_error_env *err;
+	ktime_t t0;
+	int pid = current->pid;
+	int ret;
+
+	tlob_error_env_count_reset();
+	ret = tlob_start_task(current, 1000);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 50000)
+		cpu_relax();
+
+	tlob_stop_task(current);
+
+	KUNIT_ASSERT_GE(test, tlob_error_env_count_read(), 1);
+
+	err = tlob_last_error_env_read();
+	KUNIT_EXPECT_EQ(test,    err->id,    pid);
+	KUNIT_EXPECT_STREQ(test, err->state, "running");
+	KUNIT_EXPECT_STREQ(test, err->event, "budget_exceeded");
+	KUNIT_EXPECT_TRUE(test, strncmp(err->env, "clk_elapsed=", 12) == 0);
+}
+
+static int tlob_trace_suite_init(struct kunit_suite *suite)
+{
+	int ret;
+
+	rv_kunit_monitoring_on();
+	ret = tlob_init_monitor();
+	if (ret)
+		goto err_mon_off;
+	ret = tlob_register_kunit_probes();
+	if (ret)
+		goto err_destroy;
+	ret = tlob_enable_hooks();
+	if (ret)
+		goto err_probes;
+	return 0;
+
+err_probes:
+	tlob_unregister_kunit_probes();
+err_destroy:
+	tlob_destroy_monitor();
+err_mon_off:
+	rv_kunit_monitoring_off();
+	return ret;
+}
+
+static void tlob_trace_suite_exit(struct kunit_suite *suite)
+{
+	tlob_disable_hooks();
+	tlob_unregister_kunit_probes();
+	tlob_destroy_monitor();
+	rv_kunit_monitoring_off();
+}
+
+static struct kunit_case tlob_trace_output_cases[] = {
+	KUNIT_CASE(tlob_trace_event_format),
+	KUNIT_CASE(tlob_trace_error_env_format),
+	{}
+};
+
+static struct kunit_suite tlob_trace_output_suite = {
+	.name       = "tlob_trace_output",
+	.suite_init = tlob_trace_suite_init,
+	.suite_exit = tlob_trace_suite_exit,
+	.test_cases = tlob_trace_output_cases,
+};
+
+/*
+ * Suite 5: violation reaction - complement to Suite 4.
+ * Suite 4 checks trace field values; Suite 5 checks semantics:
+ * error count per budget expiry and per-state ns breakdown.
+ */
+
+/* generous budget; usleep forces state transitions; no error must fire */
+static void tlob_no_error_within_budget(struct kunit *test)
+{
+	tlob_error_env_count_reset();
+	tlob_event_count_reset();
+
+	KUNIT_ASSERT_EQ(test, tlob_start_task(current, 10000000ULL), 0);
+	usleep_range(5000, 10000);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), 0);
+	KUNIT_EXPECT_EQ(test, tlob_error_env_count_read(), 0);
+	KUNIT_EXPECT_GE(test, tlob_event_count_read(), 2);
+}
+
+/* busy-spin 50 ms >> 1 ms budget; running_ns must dominate */
+static void tlob_detail_running_dominates(struct kunit *test)
+{
+	const struct tlob_captured_detail *d;
+	u64 total_ns;
+	ktime_t t0;
+	int ret;
+
+	tlob_error_env_count_reset();
+
+	ret = tlob_start_task(current, 1000);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 50000)
+		cpu_relax();
+
+	tlob_stop_task(current);
+
+	KUNIT_EXPECT_EQ(test, tlob_error_env_count_read(), 1);
+	d = tlob_last_detail_read();
+	KUNIT_EXPECT_EQ(test, d->pid, current->pid);
+	KUNIT_EXPECT_EQ(test, d->threshold_us, 1000ULL);
+	total_ns = d->running_ns + d->waiting_ns + d->sleeping_ns;
+	KUNIT_EXPECT_GE(test, total_ns, 1000ULL * 1000);
+	KUNIT_EXPECT_GT(test, d->running_ns, d->sleeping_ns + d->waiting_ns);
+}
+
+struct tlob_hog_ctx {
+	int spin_ms;
+};
+
+static int tlob_hog_fn(void *arg)
+{
+	struct tlob_hog_ctx *ctx = arg;
+	ktime_t t0 = ktime_get();
+
+	while (!kthread_should_stop() &&
+	       ktime_ms_delta(ktime_get(), t0) < ctx->spin_ms)
+		cpu_relax();
+	return 0;
+}
+
+/*
+ * SCHED_FIFO kthread bound to the same CPU preempts the monitored task
+ * (sched_switch prev_state == 0: running->waiting) and holds the CPU for
+ * 80 ms >> 10 ms budget, guaranteeing the timer fires in waiting state.
+ */
+static void tlob_detail_waiting_dominates(struct kunit *test)
+{
+	struct tlob_hog_ctx *ctx;
+	struct task_struct *hog;
+	struct tlob_kthread_guard *guard;
+	const struct tlob_captured_detail *d;
+	struct sched_param param = { .sched_priority = MAX_RT_PRIO - 1 };
+	int ret;
+
+	tlob_error_env_count_reset();
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	ctx->spin_ms = 80;
+
+	hog = kthread_create(tlob_hog_fn, ctx, "tlob_s5_hog");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, hog);
+	get_task_struct(hog);
+
+	kthread_bind(hog, smp_processor_id());
+	sched_setscheduler_nocheck(hog, SCHED_FIFO, &param);
+
+	guard = tlob_guard_kthread(test, hog, NULL);
+	KUNIT_ASSERT_NOT_NULL(test, guard);
+
+	ret = tlob_start_task(current, 10000); /* 10 ms budget */
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	wake_up_process(hog);
+	yield(); /* sched_switch prev_state == 0: running->waiting */
+
+	tlob_stop_task(current);
+
+	KUNIT_EXPECT_EQ(test, tlob_error_env_count_read(), 1);
+	d = tlob_last_detail_read();
+	KUNIT_EXPECT_EQ(test, d->sleeping_ns, 0ULL);
+	KUNIT_EXPECT_GT(test, d->waiting_ns, d->running_ns + d->sleeping_ns);
+
+	guard->task = NULL;
+	kthread_stop(hog);
+	put_task_struct(hog);
+}
+
+/* block on mutex for 80 ms >> 10 ms budget; sleeping_ns must dominate */
+static void tlob_detail_sleeping_dominates(struct kunit *test)
+{
+	struct tlob_holder_ctx *ctx;
+	struct tlob_kthread_guard *guard;
+	struct task_struct *holder;
+	const struct tlob_captured_detail *d;
+	int ret;
+
+	tlob_error_env_count_reset();
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	ctx->hold_ms = 80;
+	mutex_init(&ctx->lock);
+	init_completion(&ctx->ready);
+
+	holder = kthread_run(tlob_holder_fn, ctx, "tlob_s5_detail");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, holder);
+	get_task_struct(holder);
+
+	guard = tlob_guard_kthread(test, holder, NULL);
+	KUNIT_ASSERT_NOT_NULL(test, guard);
+
+	wait_for_completion(&ctx->ready);
+
+	ret = tlob_start_task(current, 10000);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	mutex_lock(&ctx->lock);
+	mutex_unlock(&ctx->lock);
+
+	tlob_stop_task(current);
+
+	KUNIT_EXPECT_EQ(test, tlob_error_env_count_read(), 1);
+	d = tlob_last_detail_read();
+	KUNIT_EXPECT_GT(test, d->sleeping_ns, d->running_ns + d->waiting_ns);
+
+	guard->task = NULL;
+	kthread_stop(holder);
+	put_task_struct(holder);
+}
+
+static int tlob_violation_suite_init(struct kunit_suite *suite)
+{
+	int ret;
+
+	rv_kunit_monitoring_on();
+	ret = tlob_init_monitor();
+	if (ret)
+		goto err_mon_off;
+	ret = tlob_register_kunit_probes();
+	if (ret)
+		goto err_destroy;
+	ret = tlob_enable_hooks();
+	if (ret)
+		goto err_probes;
+	return 0;
+
+err_probes:
+	tlob_unregister_kunit_probes();
+err_destroy:
+	tlob_destroy_monitor();
+err_mon_off:
+	rv_kunit_monitoring_off();
+	return ret;
+}
+
+static void tlob_violation_suite_exit(struct kunit_suite *suite)
+{
+	tlob_disable_hooks();
+	tlob_unregister_kunit_probes();
+	tlob_destroy_monitor();
+	rv_kunit_monitoring_off();
+}
+
+static struct kunit_case tlob_violation_react_cases[] = {
+	KUNIT_CASE(tlob_no_error_within_budget),
+	KUNIT_CASE(tlob_detail_running_dominates),
+	KUNIT_CASE(tlob_detail_sleeping_dominates),
+	KUNIT_CASE(tlob_detail_waiting_dominates),
+	{}
+};
+
+static struct kunit_suite tlob_violation_react_suite = {
+	.name       = "tlob_violation_react",
+	.suite_init = tlob_violation_suite_init,
+	.suite_exit = tlob_violation_suite_exit,
+	.test_cases = tlob_violation_react_cases,
+};
+
+kunit_test_suites(&tlob_task_api_suite,
+		  &tlob_sched_integration_suite,
+		  &tlob_uprobe_format_suite,
+		  &tlob_trace_output_suite,
+		  &tlob_violation_react_suite);
+
+MODULE_DESCRIPTION("KUnit tests for the tlob RV monitor");
+MODULE_LICENSE("GPL");
#28 lore raw

RFC PATCH v2 10/10 selftests/verification: add tlob selftests

In reply to [email protected]

From: Wen Yang <[email protected]>

Add selftest coverage for the tlob RV monitor in
tools/testing/selftests/verification/.

Two helper binaries are built by tlob/Makefile: tlob_helper for the ioctl interface (/dev/rv) and tlob_uprobe_target for the uprobe tests. The top-level Makefile delegates to tlob/ via a generic MONITOR_SUBDIRS pattern so monitor-specific build details stay within each monitor's own subdirectory.

Eight test files cover the tracefs control interface (tracefs.tc), the ioctl self-instrumentation interface (ioctl.tc, 8 scenarios), and the uprobe external monitoring interface (uprobe_bind.tc, uprobe_violation.tc, uprobe_no_event.tc, uprobe_multi.tc, uprobe_detail_sleeping.tc, uprobe_detail_waiting.tc).

Tested on x86_64 with vng (virtme-ng):

TAP version 13
1..12
ok 1 Test monitor enable/disable
ok 2 Test monitor reactor setting
ok 3 Check available monitors
ok 4 Test wwnr monitor with printk reactor
ok 5 Test tlob ioctl self-instrumentation (within/over-budget, error paths) ok 6 Test tlob monitor tracefs interface (enable/disable and files)
ok 7 uprobe binding: visible in monitor file, removable, duplicate offset rejected ok 8 uprobe detail sleeping: sleeping_ns dominates when task blocks between probes ok 9 uprobe detail waiting: waiting_ns dominates when task is preempted between probes ok 10 Two bindings on same binary with different offsets and budgets fire independently ok 11 Verify no spurious error_env_tlob events without an active uprobe binding ok 12 uprobe violation: error_env_tlob and detail_env_tlob fire with correct fields # Totals: pass:12 fail:0 xfail:0 xpass:0 skip:0 error:0

---
 tools/testing/selftests/verification/Makefile |  21 +-
 .../verification/test.d/tlob/ioctl.tc         |  36 +
 .../verification/test.d/tlob/tracefs.tc       |  17 +
 .../verification/test.d/tlob/uprobe_bind.tc   |  34 +
 .../test.d/tlob/uprobe_detail_sleeping.tc     |  47 ++
 .../test.d/tlob/uprobe_detail_waiting.tc      |  60 ++
 .../verification/test.d/tlob/uprobe_multi.tc  |  60 ++
 .../test.d/tlob/uprobe_no_event.tc            |  19 +
 .../test.d/tlob/uprobe_violation.tc           |  60 ++
 .../selftests/verification/tlob/Makefile      |  21 +
 .../selftests/verification/tlob/tlob_ioctl.c  | 626 ++++++++++++++++++
 .../selftests/verification/tlob/tlob_target.c | 138 ++++
 12 files changed, 1138 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/ioctl.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/tracefs.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
 create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
 create mode 100644 tools/testing/selftests/verification/tlob/Makefile
 create mode 100644 tools/testing/selftests/verification/tlob/tlob_ioctl.c
 create mode 100644 tools/testing/selftests/verification/tlob/tlob_target.c
tools/testing/selftests/verification/Makefile +20 -1
index aa8790c22a71..b5584fd3762d 100644
@@ -1,8 +1,27 @@
 # SPDX-License-Identifier: GPL-2.0
-all:
 
 TEST_PROGS := verificationtest-ktap
 TEST_FILES := test.d settings
 EXTRA_CLEAN := $(OUTPUT)/logs/*
 
+# Subdirectories that provide helper binaries for the test runner.
+# Each entry must contain a Makefile that accepts OUTDIR= and deposits
+# its binaries there; verificationtest-ktap adds OUTDIR to PATH so
+# the ftracetest require-checks resolve the binaries by name.
+MONITOR_SUBDIRS := tlob
+
 include ../lib.mk
+
+# Build and clean each monitor subdirectory.
+all: $(patsubst %,_build_%,$(MONITOR_SUBDIRS))
+
+clean: $(patsubst %,_clean_%,$(MONITOR_SUBDIRS))
+
+.PHONY: $(patsubst %,_build_%,$(MONITOR_SUBDIRS)) \
+        $(patsubst %,_clean_%,$(MONITOR_SUBDIRS))
+
+$(patsubst %,_build_%,$(MONITOR_SUBDIRS)): _build_%:
+	$(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)"
+
+$(patsubst %,_clean_%,$(MONITOR_SUBDIRS)): _clean_%:
+	$(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
tools/testing/selftests/verification/test.d/tlob/ioctl.tc new file +36 -0
new file mode 100644
index 000000000000..54ae249af9a6
@@ -0,0 +1,36 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob ioctl self-instrumentation (within/over-budget, error paths)
+# requires: tlob:monitor tlob_ioctl:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+
+[ -c /dev/rv ] || exit_unsupported
+
+echo 1 > monitors/tlob/enable
+
+# within budget: 50 ms threshold, 10 ms workload
+"$TLOB_HELPER" within_budget
+
+# over budget in running state: 1 ms threshold, 100 ms busy-spin
+"$TLOB_HELPER" over_budget_running
+
+# over budget in sleeping state: 3 ms threshold, 50 ms sleep
+"$TLOB_HELPER" over_budget_sleeping
+
+# over budget in waiting state: 1 us threshold, sched_yield
+"$TLOB_HELPER" over_budget_waiting
+
+# error paths
+"$TLOB_HELPER" double_start
+"$TLOB_HELPER" stop_no_start
+
+# per-thread isolation
+"$TLOB_HELPER" multi_thread
+
+# bind against disabled monitor must return ENODEV, not crash
+echo 0 > monitors/tlob/enable
+"$TLOB_HELPER" not_enabled
+echo 1 > monitors/tlob/enable
+
+echo 0 > monitors/tlob/enable
tools/testing/selftests/verification/test.d/tlob/tracefs.tc new file +17 -0
new file mode 100644
index 000000000000..5d1e7cc02498
@@ -0,0 +1,17 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor tracefs interface (enable/disable and files)
+# requires: tlob:monitor
+
+check_requires monitors/tlob/enable monitors/tlob/desc monitors/tlob/monitor
+
+# enable / disable via the enable file
+echo 1 > monitors/tlob/enable
+grep -q 1 monitors/tlob/enable
+echo "tlob" >> enabled_monitors
+grep -q tlob enabled_monitors
+
+echo 0 > monitors/tlob/enable
+grep -q 0 monitors/tlob/enable
+echo "!tlob" >> enabled_monitors
+! grep -q "^tlob$" enabled_monitors
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +34 -0
new file mode 100644
index 000000000000..41e20d593855
@@ -0,0 +1,34 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe binding (visible in monitor file, removable, duplicate rejected)
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+[ -n "$busy_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > monitors/tlob/enable
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=5000000" > "$TLOB_MONITOR"
+
+# Binding must appear in monitor file with canonical hex-offset format.
+grep -qE "^p ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=[0-9]+$" "$TLOB_MONITOR"
+grep -q "threshold=5000000" "$TLOB_MONITOR"
+
+# Duplicate offset_start must be rejected.
+! echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=9999" > "$TLOB_MONITOR" 2>/dev/null
+
+# Remove the binding; it must no longer appear.
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR"
+! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR"
+
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > monitors/tlob/enable
tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc new file +47 -0

47 lines not on this page · raw message

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

60 lines not on this page · raw message

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

60 lines not on this page · raw message

tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file +19 -0
new file mode 100644
index 000000000000..4a74853346e3
@@ -0,0 +1,19 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test no spurious error_env_tlob events without an active uprobe binding
+# requires: tlob:monitor tlob_ioctl:program
+
+TLOB_MONITOR=monitors/tlob/monitor
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+sleep 0.5
+
+! grep -q "error_env_tlob" /sys/kernel/tracing/trace
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo > /sys/kernel/tracing/trace
tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +60 -0

60 lines not on this page · raw message

tools/testing/selftests/verification/tlob/Makefile new file +21 -0
new file mode 100644
index 000000000000..1bedf946cb34
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0
+# Builds tlob selftest helper binaries.
+#
+# Invoked by ../Makefile; pass OUTDIR to control the output directory
+# and TOOLS_INCLUDES for the in-tree UAPI -isystem flag.
+
+OUTDIR ?= $(CURDIR)/..
+CFLAGS += $(TOOLS_INCLUDES)
+
+.PHONY: all
+all: $(OUTDIR)/tlob_ioctl $(OUTDIR)/tlob_target
+
+$(OUTDIR)/tlob_ioctl: tlob_ioctl.c
+	$(CC) $(CFLAGS) -o $@ $< -lpthread
+
+$(OUTDIR)/tlob_target: tlob_target.c
+	$(CC) $(CFLAGS) -o $@ $<
+
+.PHONY: clean
+clean:
+	$(RM) $(OUTDIR)/tlob_ioctl $(OUTDIR)/tlob_target
tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +626 -0

626 lines not on this page · raw message

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

138 lines not on this page · raw message

Signature · 1 line
2.25.1
maintainer
#29 lore raw

Re: [RFC PATCH v2 10/10] selftests/verification: add tlob selftests

Quoted 11 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Add selftest coverage for the tlob RV monitor in
tools/testing/selftests/verification/.

Two helper binaries are built by tlob/Makefile: tlob_helper for the ioctl interface (/dev/rv) and tlob_uprobe_target for the uprobe tests. The top-level Makefile delegates to tlob/ via a generic MONITOR_SUBDIRS pattern so monitor-specific build details stay within each monitor's own subdirectory.

Quoted 5 lines

Eight test files cover the tracefs control interface (tracefs.tc), the ioctl self-instrumentation interface (ioctl.tc, 8 scenarios), and the uprobe external monitoring interface (uprobe_bind.tc, uprobe_violation.tc, uprobe_no_event.tc, uprobe_multi.tc, uprobe_detail_sleeping.tc, uprobe_detail_waiting.tc).

Thanks for the deep test suite!

I run it on a VM (virtme-ng on my x86 16 core fedora box) and have it hanging at step 9 (you see 8 is ok and after I get an RCU splat):

$ sudo vng -v -- make -C tools/testing/selftests/verification run_tests
...
# ok 5 Test tlob ioctl self-instrumentation (within/over-budget, error paths)
# ok 6 Test tlob monitor tracefs interface (enable/disable and files)
# ok 7 Test uprobe binding (visible in monitor file, removable, duplicate rejected)
# ok 8 Test uprobe detail sleeping (sleeping_ns dominates when task blocks between probes)
[   53.989561] tlob_target (1756) used greatest stack depth: 11792 bytes left
[   75.100818] rcu: INFO: rcu_preempt self-detected stall on CPU
[   75.100825] rcu: 	0-...!: (26082 ticks this GP) idle=a8e4/1/0x4000000000000000 softirq=0/0 fqs=13 rcuc=26078 jiffies(starved)
[   75.100833] rcu: 	(t=26000 jiffies g=17333 q=146 ncpus=16)
[   75.100836] rcu: rcu_preempt kthread timer wakeup didn't happen for 24040 jiffies! g17333 f0x0 RCU_GP_WAIT_FQS(5) ->state=0x402
[   75.100839] rcu: 	Possible timer handling issue on cpu=7 timer-softirq=317
[   75.100840] rcu: rcu_preempt kthread starved for 24043 jiffies! g17333 f0x0 RCU_GP_WAIT_FQS(5) ->state=0x402 ->cpu=7
[   75.100843] rcu: 	Unless rcu_preempt kthread gets sufficient CPU time, OOM is now expected behavior.
[   75.100843] rcu: RCU grace-period kthread stack dump:
[   75.100845] task:rcu_preempt     state:I stack:14104 pid:17    tgid:17    ppid:2      task_flags:0x208040 flags:0x00080000
[   75.100856] Call Trace:
[   75.100859]  <TASK>
[   75.100870]  __schedule+0x4f1/0x1490
[   75.100890]  ? __pfx_rcu_gp_kthread+0x10/0x10
[   75.100898]  schedule+0x5b/0x210
[   75.100901]  ? schedule_timeout+0xae/0x130
[   75.100905]  schedule_timeout+0xae/0x130
[   75.100911]  ? __pfx_process_timeout+0x10/0x10
[   75.100925]  rcu_gp_fqs_loop+0x114/0x880
[   75.100933]  ? lock_release+0x2ea/0x4a0
[   75.100945]  ? __pfx_rcu_gp_kthread+0x10/0x10
[   75.100948]  rcu_gp_kthread+0x26b/0x320
[   75.100951]  ? preempt_count_sub+0x5f/0x80
[   75.100963]  ? __pfx_rcu_gp_kthread+0x10/0x10
[   75.100966]  kthread+0xf3/0x130
[   75.100970]  ? __pfx_kthread+0x10/0x10
[   75.100978]  ret_from_fork+0x3b4/0x420
[   75.100984]  ? __pfx_kthread+0x10/0x10
[   75.100989]  ret_from_fork_asm+0x1a/0x30
[   75.101018]  </TASK>
[   75.101019] rcu: Stack dump where RCU GP kthread last ran:
[   75.101021] Sending NMI from CPU 0 to CPUs 7:
[   75.101106] NMI backtrace for cpu 7
[   75.101118] CPU: 7 UID: 0 PID: 0 Comm: swapper/7 Not tainted 7.1.0-rc2+ #160 PREEMPT_{RT,(lazy)} 
[   75.101124] Hardware name: Bochs Bochs, BIOS Bochs 01/01/2011
[   75.101128] RIP: 0010:pv_native_safe_halt+0xf/0x20
[   75.101139] Code: 75 70 00 c3 cc cc cc cc 0f 1f 00 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 f3 0f 1e fa eb 07 0f 00 2d 25 6e 1c 00 fb f4 <c3> cc cc cc cc 66 2e 0f 1f 84 00 00 00 00 00 66 90 90 90 90 90 90
[   75.101142] RSP: 0018:ffffd22ec0103eb8 EFLAGS: 00000296
[   75.101147] RAX: 00000000000529f3 RBX: 0000000000000000 RCX: ffffffff8ca56131
[   75.101170] RDX: ffff8de4c185c280 RSI: 0000000000000000 RDI: ffffffff8ca56131
[   75.101172] RBP: ffff8de4c185c280 R08: 0000000000000000 R09: 0000000000000000
[   75.101174] R10: 0000000000000000 R11: 0000000000000001 R12: 0000000000000007
[   75.101176] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
[   75.101373] FS:  0000000000000000(0000) GS:ffff8de56a091000(0000) knlGS:0000000000000000
[   75.101379] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   75.101381] CR2: 00007fb886a53f98 CR3: 000000003be5c002 CR4: 0000000000770ef0
[   75.101383] PKRU: 55555554
[   75.101384] Call Trace:
[   75.101388]  <TASK>
[   75.101389]  default_idle+0x9/0x10
[   75.101397]  default_idle_call+0x85/0x240
[   75.101404]  do_idle+0x291/0x300
[   75.101412]  ? schedule_idle+0x22/0x40
[   75.101415]  cpu_startup_entry+0x29/0x30
[   75.101418]  start_secondary+0xf8/0x100
[   75.101424]  common_startup_64+0x12c/0x138
[   75.101435]  </TASK>
[   75.102036] CPU: 0 UID: 0 PID: 1758 Comm: sh Not tainted 7.1.0-rc2+ #160 PREEMPT_{RT,(lazy)} 
[   75.102040] Hardware name: Bochs Bochs, BIOS Bochs 01/01/2011
[   75.102042] RIP: 0033:0x556458604e3f
[   75.102049] Code: 3c 18 4e 8d 04 3f 42 c6 04 21 00 0f b6 01 4c 89 7d b0 4c 89 c3 e9 bf ed ff ff 90 41 0f b6 c1 48 8d 15 c5 3f 11 00 80 3c 02 00 <0f> 84 a9 f0 ff ff 48 8b 45 80 f6 40 08 50 0f 85 9b f0 ff ff e9 78
[   75.102051] RSP: 002b:00007ffc7ac46e30 EFLAGS: 00000246
[   75.102054] RAX: 0000000000000074 RBX: 0000000000000074 RCX: 000055646adb8a60
[   75.102056] RDX: 0000556458718e00 RSI: 0000000000000018 RDI: 0000000000000000
[   75.102057] RBP: 00007ffc7ac46f20 R08: 000055646adc3100 R09: 0000000000000074
[   75.102058] R10: 0000000000000021 R11: 0000000000000001 R12: 0000000000000000
[   75.102059] R13: 0000000000000070 R14: 000055646adb9cf0 R15: 0000000000000000
[   75.102061] FS:  00007f832822b740 GS:  0000000000000000

Did you see that? Am I doing something wrong?

Thanks,
Gabriele

Quoted 1,367 lines

Tested on x86_64 with vng (virtme-ng):

  TAP version 13
  1..12
  ok 1 Test monitor enable/disable
  ok 2 Test monitor reactor setting
  ok 3 Check available monitors
  ok 4 Test wwnr monitor with printk reactor
  ok 5 Test tlob ioctl self-instrumentation (within/over-budget, error paths) ok 6 Test tlob monitor tracefs interface (enable/disable and files) ok 7 uprobe binding: visible in monitor file, removable, duplicate offset rejected
  ok 8 uprobe detail sleeping: sleeping_ns dominates when task blocks between probes
  ok 9 uprobe detail waiting: waiting_ns dominates when task is preempted between probes
  ok 10 Two bindings on same binary with different offsets and budgets fire independently
  ok 11 Verify no spurious error_env_tlob events without an active uprobe binding
  ok 12 uprobe violation: error_env_tlob and detail_env_tlob fire with correct fields
  # Totals: pass:12 fail:0 xfail:0 xpass:0 skip:0 error:0

---
 tools/testing/selftests/verification/Makefile |  21 +- .../verification/test.d/tlob/ioctl.tc         |  36 + .../verification/test.d/tlob/tracefs.tc       |  17 + .../verification/test.d/tlob/uprobe_bind.tc   |  34 + .../test.d/tlob/uprobe_detail_sleeping.tc     |  47 ++ .../test.d/tlob/uprobe_detail_waiting.tc      |  60 ++ .../verification/test.d/tlob/uprobe_multi.tc  |  60 ++ .../test.d/tlob/uprobe_no_event.tc            |  19 + .../test.d/tlob/uprobe_violation.tc           |  60 ++ .../selftests/verification/tlob/Makefile      |  21 + .../selftests/verification/tlob/tlob_ioctl.c  | 626 ++++++++++++++++++ .../selftests/verification/tlob/tlob_target.c | 138 ++++

 12 files changed, 1138 insertions(+), 1 deletion(-)

 create mode 100644 tools/testing/selftests/verification/test.d/tlob/ioctl.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/tracefs.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc create mode 100644 tools/testing/selftests/verification/tlob/Makefile create mode 100644 tools/testing/selftests/verification/tlob/tlob_ioctl.c create mode 100644 tools/testing/selftests/verification/tlob/tlob_target.c

tools/testing/selftests/verification/Makefile +0 -1
index aa8790c22a71..b5584fd3762d 100644
@@ -1,8 +1,27 @@
  # SPDX-License-Identifier: GPL-2.0
-all:


 TEST_PROGS := verificationtest-ktap
 TEST_FILES := test.d settings
 EXTRA_CLEAN := $(OUTPUT)/logs/*

tools/testing/selftests/verification/Makefile +6 -0
+# Subdirectories that provide helper binaries for the test runner.
+# Each entry must contain a Makefile that accepts OUTDIR= and deposits
+# its binaries there; verificationtest-ktap adds OUTDIR to PATH so
+# the ftracetest require-checks resolve the binaries by name.
+MONITOR_SUBDIRS := tlob
+

 include ../lib.mk

tools/testing/selftests/verification/Makefile +14 -0
+
+# Build and clean each monitor subdirectory.
+all: $(patsubst %,_build_%,$(MONITOR_SUBDIRS))
+
+clean: $(patsubst %,_clean_%,$(MONITOR_SUBDIRS))
+
+.PHONY: $(patsubst %,_build_%,$(MONITOR_SUBDIRS)) \
+        $(patsubst %,_clean_%,$(MONITOR_SUBDIRS))
+
+$(patsubst %,_build_%,$(MONITOR_SUBDIRS)): _build_%:
+	$(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)"
+
+$(patsubst %,_clean_%,$(MONITOR_SUBDIRS)): _clean_%:
+	$(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
tools/testing/selftests/verification/test.d/tlob/ioctl.tc new file +3 -0
new file mode 100644
index 000000000000..54ae249af9a6
@@ -0,0 +1,36 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob ioctl self-instrumentation (within/over-budget,

error paths)

tools/testing/selftests/verification/test.d/tlob/ioctl.tc new file +33 -0
+# requires: tlob:monitor tlob_ioctl:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+
+[ -c /dev/rv ] || exit_unsupported
+
+echo 1 > monitors/tlob/enable
+
+# within budget: 50 ms threshold, 10 ms workload
+"$TLOB_HELPER" within_budget
+
+# over budget in running state: 1 ms threshold, 100 ms busy-spin
+"$TLOB_HELPER" over_budget_running
+
+# over budget in sleeping state: 3 ms threshold, 50 ms sleep
+"$TLOB_HELPER" over_budget_sleeping
+
+# over budget in waiting state: 1 us threshold, sched_yield
+"$TLOB_HELPER" over_budget_waiting
+
+# error paths
+"$TLOB_HELPER" double_start
+"$TLOB_HELPER" stop_no_start
+
+# per-thread isolation
+"$TLOB_HELPER" multi_thread
+
+# bind against disabled monitor must return ENODEV, not crash
+echo 0 > monitors/tlob/enable
+"$TLOB_HELPER" not_enabled
+echo 1 > monitors/tlob/enable
+
+echo 0 > monitors/tlob/enable
tools/testing/selftests/verification/test.d/tlob/tracefs.tc new file +17 -0
new file mode 100644
index 000000000000..5d1e7cc02498
@@ -0,0 +1,17 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor tracefs interface (enable/disable and files)
+# requires: tlob:monitor
+
+check_requires monitors/tlob/enable monitors/tlob/desc monitors/tlob/monitor
+
+# enable / disable via the enable file
+echo 1 > monitors/tlob/enable
+grep -q 1 monitors/tlob/enable
+echo "tlob" >> enabled_monitors
+grep -q tlob enabled_monitors
+
+echo 0 > monitors/tlob/enable
+grep -q 0 monitors/tlob/enable
+echo "!tlob" >> enabled_monitors
+! grep -q "^tlob$" enabled_monitors
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
new file mode 100644
index 000000000000..41e20d593855
@@ -0,0 +1,34 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe binding (visible in monitor file, removable,

duplicate rejected)

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)

+[ -n "$busy_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > monitors/tlob/enable
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=5000000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
+
+# Binding must appear in monitor file with canonical hex-offset format.
+grep -qE "^p ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=[0-9]+$"

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +4 -0
+grep -q "threshold=5000000" "$TLOB_MONITOR"
+
+# Duplicate offset_start must be rejected.
+! echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=9999" >

"$TLOB_MONITOR" 2>/dev/null

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+
+# Remove the binding; it must no longer appear.
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR"
+! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR"
+
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > monitors/tlob/enable

diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc new file mode 100644
index 000000000000..2b8656e0fef1
--- /dev/null
+++
b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
@@ -0,0 +1,47 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe detail sleeping (sleeping_ns dominates when task

blocks between probes)

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+start_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work_done 2>/dev/null)
+[ -n "$start_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +12 -0
+
+"$UPROBE_TARGET" 5000 sleep &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# 50 ms budget; task sleeps 200 ms per iteration -> sleeping_ns dominates.
+echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=50000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +5 -0
+
+found=0; i=0
+while [ "$i" -lt 30 ]; do
+	sleep 0.1
+	grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=1;

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +17 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+line=$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1)
+running=$(echo "$line" | sed 's/.*running_ns=\([0-9]*\).*/\1/')
+waiting=$(echo "$line" | sed 's/.*waiting_ns=\([0-9]*\).*/\1/')
+sleeping=$(echo "$line" | sed 's/.*sleeping_ns=\([0-9]*\).*/\1/')
+[ "$sleeping" -gt "$((running + waiting))" ]
+
+echo > /sys/kernel/tracing/trace

diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc new file mode 100644
index 000000000000..0705854f24df
--- /dev/null
+++
b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
@@ -0,0 +1,60 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe detail waiting (waiting_ns dominates when task is

preempted between probes)

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +10 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+command -v chrt    > /dev/null || exit_unsupported
+command -v taskset > /dev/null || exit_unsupported
+
+start_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_preempt_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_preempt_work_done 2>/dev/null)
+[ -n "$start_offset" ] || exit_unsupported
+[ -n "$stop_offset" ]  || exit_unsupported

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +11 -0
+
+cpu=0
+
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# Register probe before the target starts so the start uprobe fires on the
+# first entry to tlob_preempt_work. Budget: 500 ms.
+echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=500000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +15 -0
+
+# Target starts; start probe fires on tlob_preempt_work entry.
+taskset -c "$cpu" "$UPROBE_TARGET" 5000 preempt &
+busy_pid=$!
+sleep 0.05
+
+# RT hog on the same CPU preempts the target; target stays in waiting state
+# (runnable, off-CPU) until the budget expires -> waiting_ns dominates.
+chrt -f 99 taskset -c "$cpu" sh -c 'while true; do :; done' 2>/dev/null &
+hog_pid=$!
+
+found=0; i=0
+while [ "$i" -lt 30 ]; do
+	sleep 0.1
+	grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=1;

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +18 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$hog_pid" 2>/dev/null; wait "$hog_pid" 2>/dev/null || true
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+line=$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1)
+running=$(echo "$line" | sed 's/.*running_ns=\([0-9]*\).*/\1/')
+sleeping=$(echo "$line" | sed 's/.*sleeping_ns=\([0-9]*\).*/\1/')
+waiting=$(echo "$line" | sed 's/.*waiting_ns=\([0-9]*\).*/\1/')
+[ "$waiting" -gt "$((running + sleeping))" ]
+
+echo > /sys/kernel/tracing/trace
tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +3 -0
new file mode 100644
index 000000000000..c4b8f7108ae9
@@ -0,0 +1,60 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test two uprobe bindings on same binary (different offsets

fire independently)

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work

2>/dev/null)
+busy_stop=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+sleep_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2>/dev/null)
+sleep_stop=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work_done 2>/dev/null)
+[ -n "$busy_offset" ]  || exit_unsupported
+[ -n "$busy_stop" ]    || exit_unsupported
+[ -n "$sleep_offset" ] || exit_unsupported
+[ -n "$sleep_stop" ]   || exit_unsupported

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +4 -0
+
+"$UPROBE_TARGET" 30000 &       # busy mode: tlob_busy_work fires every 200 ms
+busy_pid=$!
+"$UPROBE_TARGET" 30000 sleep & # sleep mode: tlob_sleep_work fires every 200

ms

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +11 -0
+sleep_pid=$!
+sleep 0.05
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# Binding A: 5 s budget on the busy probe - must not fire in 200 ms loops.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=5000000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +2 -0
+# Binding B: 10 ns budget on the sleep probe - fires on first invocation.
+echo "p ${UPROBE_TARGET}:${sleep_offset} ${sleep_stop} threshold=10" >

"$TLOB_MONITOR"

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

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +15 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null
+echo "-${UPROBE_TARGET}:${sleep_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$sleep_pid" 2>/dev/null; wait "$sleep_pid" 2>/dev/null || true
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+
+[ "$found" = "1" ]
+# error_env_tlob payload: label and clock variable must be present.
+grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q

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

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

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

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

uprobe binding

tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file +16 -0
+# requires: tlob:monitor tlob_ioctl:program
+
+TLOB_MONITOR=monitors/tlob/monitor
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+sleep 0.5
+
+! grep -q "error_env_tlob" /sys/kernel/tracing/trace
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo > /sys/kernel/tracing/trace

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..624fdb950f6b

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +3 -0
@@ -0,0 +1,60 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe violation (error_env_tlob and detail_env_tlob fire

with correct fields)

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)

+[ -n "$busy_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +13 -0
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# 10 ns budget - fires almost immediately; task is busy-spinning on-CPU.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=10" >

"$TLOB_MONITOR"

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

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +13 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+# error_env_tlob event label must be budget_exceeded
+grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q

"budget_exceeded"

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +15 -0
+
+# detail_env_tlob must have all five fields with the correct threshold
+line=$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1)
+echo "$line" | grep -q "pid="
+echo "$line" | grep -q "threshold_us=10"
+echo "$line" | grep -q "running_ns="
+echo "$line" | grep -q "waiting_ns="
+echo "$line" | grep -q "sleeping_ns="
+
+# Busy-spin keeps the task on-CPU: running_ns must exceed sleeping_ns.
+running=$(echo "$line" | sed 's/.*running_ns=\([0-9]*\).*/\1/')
+sleeping=$(echo "$line" | sed 's/.*sleeping_ns=\([0-9]*\).*/\1/')
+[ "$running" -gt "$sleeping" ]
+
+echo > /sys/kernel/tracing/trace
tools/testing/selftests/verification/tlob/Makefile new file +21 -0
new file mode 100644
index 000000000000..1bedf946cb34
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0
+# Builds tlob selftest helper binaries.
+#
+# Invoked by ../Makefile; pass OUTDIR to control the output directory
+# and TOOLS_INCLUDES for the in-tree UAPI -isystem flag.
+
+OUTDIR ?= $(CURDIR)/..
+CFLAGS += $(TOOLS_INCLUDES)
+
+.PHONY: all
+all: $(OUTDIR)/tlob_ioctl $(OUTDIR)/tlob_target
+
+$(OUTDIR)/tlob_ioctl: tlob_ioctl.c
+	$(CC) $(CFLAGS) -o $@ $< -lpthread
+
+$(OUTDIR)/tlob_target: tlob_target.c
+	$(CC) $(CFLAGS) -o $@ $<
+
+.PHONY: clean
+clean:
+	$(RM) $(OUTDIR)/tlob_ioctl $(OUTDIR)/tlob_target
tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +15 -0
new file mode 100644
index 000000000000..abb4e2e80a2c
@@ -0,0 +1,626 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob_ioctl.c - ioctl test driver and ELF utility for tlob selftests
+ *
+ * Usage: tlob_ioctl <subcommand> [args...]
+ *
+ *   not_enabled          - TRACE_START without monitor enabled -> ENODEV
+ *   within_budget        - sleep within budget -> 0
+ *   over_budget_running  - busy-spin past budget -> EOVERFLOW
+ *   over_budget_sleeping - sleep past budget -> EOVERFLOW
+ *   over_budget_waiting  - sched_yield into waiting state -> EOVERFLOW
+ *   double_start         - two starts without stop -> EALREADY
+ *   stop_no_start        - stop without start -> EINVAL
+ *   multi_thread         - two fds: thread A within budget, thread B over
+ *   bench                - TRACE_START/STOP latency (TAP output, always

passes)
+ *   sym_offset <binary> <symbol> - print ELF file offset of symbol
+ *
+ * Exit: 0 = pass, 1 = fail, 2 = skip (device not available).
+ */

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +86 -0
+#define _GNU_SOURCE
+#include <elf.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/rv.h>
+
+static int rv_fd = -1;
+
+static int open_rv(void)
+{
+	struct rv_bind_args bind = { .monitor_name = "tlob" };
+
+	rv_fd = open("/dev/rv", O_RDWR);
+	if (rv_fd < 0) {
+		fprintf(stderr, "open /dev/rv: %s\n", strerror(errno));
+		return -1;
+	}
+	if (ioctl(rv_fd, RV_IOCTL_BIND_MONITOR, &bind) < 0) {
+		fprintf(stderr, "bind tlob: %s\n", strerror(errno));
+		close(rv_fd);
+		rv_fd = -1;
+		return -1;
+	}
+	return 0;
+}
+
+static void busy_spin_us(unsigned long us)
+{
+	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 < us * 1000UL);
+}
+
+static int trace_start(uint64_t threshold_us)
+{
+	struct tlob_start_args args = {
+		.threshold_us = threshold_us,
+	};
+
+	return ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+}
+
+static int trace_stop(void)
+{
+	return ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+}
+
+/* Synchronous TRACE_START / TRACE_STOP tests */
+
+/* Bind to a disabled monitor must return ENODEV without crashing */
+static int test_not_enabled(void)
+{
+	struct rv_bind_args bind = { .monitor_name = "tlob" };
+	int fd;
+	int ret;
+
+	fd = open("/dev/rv", O_RDWR);
+	if (fd < 0) {
+		fprintf(stderr, "open /dev/rv: %s\n", strerror(errno));
+		return 2; /* skip */
+	}
+
+	ret = ioctl(fd, RV_IOCTL_BIND_MONITOR, &bind);
+	close(fd);
+
+	if (ret == 0) {
+		fprintf(stderr, "RV_IOCTL_BIND_MONITOR: expected ENODEV, got

success\n");

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +4 -0
+		return 1;
+	}
+	if (errno != ENODEV) {
+		fprintf(stderr, "RV_IOCTL_BIND_MONITOR: expected ENODEV, got

%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +82 -0
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_within_budget(void)
+{
+	int ret;
+
+	/* 50 ms budget */
+	if (trace_start(50000) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	usleep(10000); /* 10 ms */
+	ret = trace_stop();
+	if (ret != 0) {
+		fprintf(stderr, "TRACE_STOP: expected 0, got %d errno=%s\n",
+			ret, strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_over_budget_running(void)
+{
+	int ret;
+
+	/* 1 ms budget */
+	if (trace_start(1000) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	busy_spin_us(100000); /* 100 ms */
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n");
+		return 1;
+	}
+	if (errno != EOVERFLOW) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_over_budget_sleeping(void)
+{
+	int ret;
+
+	/* 3 ms budget */
+	if (trace_start(3000) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	usleep(50000); /* 50 ms; sleeping time counts toward budget */
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n");
+		return 1;
+	}
+	if (errno != EOVERFLOW) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_over_budget_waiting(void)
+{
+	int ret;
+
+	/* 1 us budget */
+	if (trace_start(1) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	sched_yield(); /* running -> waiting -> running */
+	busy_spin_us(10); /* 10 us >> 1 us budget; hrtimer fires during spin

*/

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +27 -0
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n");
+		return 1;
+	}
+	if (errno != EOVERFLOW) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+/* Error-handling tests */
+
+static int test_double_start(void)
+{
+	int ret;
+
+	/* 10 s: large enough the hrtimer won't fire during the test */
+	if (trace_start(10000000ULL) < 0) {
+		fprintf(stderr, "first TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	ret = trace_start(10000000);
+	if (ret == 0) {
+		fprintf(stderr, "second TRACE_START: expected EALREADY, got

0\n");

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +5 -0
+		trace_stop();
+		return 1;
+	}
+	if (errno != EALREADY) {
+		fprintf(stderr, "second TRACE_START: expected EALREADY, got

%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +49 -0
+			strerror(errno));
+		trace_stop();
+		return 1;
+	}
+	trace_stop();
+	return 0;
+}
+
+static int test_stop_no_start(void)
+{
+	int ret;
+
+	/* Ensure clean state: ignore error from a stale entry */
+	trace_stop();
+
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EINVAL, got 0\n");
+		return 1;
+	}
+	if (errno != EINVAL) {
+		fprintf(stderr, "TRACE_STOP: expected EINVAL, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+/* Two threads, each with its own fd: A within budget, B over budget. */
+
+struct mt_thread_args {
+	uint64_t      threshold_us;
+	unsigned long workload_us;
+	int           busy;
+	int           expect_eoverflow;
+	int           result;
+};
+
+static void *mt_thread_fn(void *arg)
+{
+	struct mt_thread_args *a = arg;
+	struct tlob_start_args args = { .threshold_us = a->threshold_us };
+	struct rv_bind_args bind = { .monitor_name = "tlob" };
+	int fd;
+	int ret;
+
+	fd = open("/dev/rv", O_RDWR);
+	if (fd < 0) {
+		fprintf(stderr, "thread open /dev/rv: %s\n",

strerror(errno));

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +27 -0
+		a->result = 1;
+		return NULL;
+	}
+	if (ioctl(fd, RV_IOCTL_BIND_MONITOR, &bind) < 0) {
+		fprintf(stderr, "thread bind tlob: %s\n", strerror(errno));
+		close(fd);
+		a->result = 1;
+		return NULL;
+	}
+
+	ret = ioctl(fd, TLOB_IOCTL_TRACE_START, &args);
+	if (ret < 0) {
+		fprintf(stderr, "thread TRACE_START: %s\n", strerror(errno));
+		close(fd);
+		a->result = 1;
+		return NULL;
+	}
+
+	if (a->busy)
+		busy_spin_us(a->workload_us);
+	else
+		usleep(a->workload_us);
+
+	ret = ioctl(fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	if (a->expect_eoverflow) {
+		if (ret == 0 || errno != EOVERFLOW) {
+			fprintf(stderr, "thread: expected EOVERFLOW, got

ret=%d errno=%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +8 -0
+				ret, strerror(errno));
+			close(fd);
+			a->result = 1;
+			return NULL;
+		}
+	} else {
+		if (ret != 0) {
+			fprintf(stderr, "thread: expected 0, got ret=%d

errno=%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +17 -0
+				ret, strerror(errno));
+			close(fd);
+			a->result = 1;
+			return NULL;
+		}
+	}
+	close(fd);
+	a->result = 0;
+	return NULL;
+}
+
+static int test_multi_thread(void)
+{
+	pthread_t ta, tb;
+	struct mt_thread_args a = {
+		.threshold_us     = 20000,   /* 20 ms */
+		.workload_us      = 5000,    /* 5 ms sleep -> within budget

*/

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +75 -0
+		.busy             = 0,
+		.expect_eoverflow = 0,
+	};
+	struct mt_thread_args b = {
+		.threshold_us     = 3000,    /* 3 ms */
+		.workload_us      = 30000,   /* 30 ms spin -> over budget */
+		.busy             = 1,
+		.expect_eoverflow = 1,
+	};
+
+	pthread_create(&ta, NULL, mt_thread_fn, &a);
+	pthread_create(&tb, NULL, mt_thread_fn, &b);
+	pthread_join(ta, NULL);
+	pthread_join(tb, NULL);
+
+	return (a.result || b.result) ? 1 : 0;
+}
+
+/*
+ * Benchmark TRACE_START, TRACE_STOP, and round-trip ioctls.
+ * Output uses TAP '#' prefix; always returns 0.
+ */
+#define BENCH_WARMUP  32
+#define BENCH_N      1000
+
+static long long timespec_diff_ns(const struct timespec *a,
+				   const struct timespec *b)
+{
+	return (long long)(b->tv_sec - a->tv_sec) * 1000000000LL
+		+ (b->tv_nsec - a->tv_nsec);
+}
+
+static int test_bench(void)
+{
+	struct tlob_start_args args = {
+		.threshold_us = 10000000ULL, /* 10 s */
+	};
+	struct timespec t0, t1;
+	long long total_start_ns = 0, total_stop_ns = 0, total_rt_ns = 0;
+	int i;
+
+	/* warm up */
+	for (i = 0; i < BENCH_WARMUP; i++) {
+		if (ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args) == 0)
+			ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	}
+
+	/* start only */
+	for (i = 0; i < BENCH_N; i++) {
+		clock_gettime(CLOCK_MONOTONIC, &t0);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+		clock_gettime(CLOCK_MONOTONIC, &t1);
+		total_start_ns += timespec_diff_ns(&t0, &t1);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	}
+
+	/* stop only */
+	for (i = 0; i < BENCH_N; i++) {
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+		clock_gettime(CLOCK_MONOTONIC, &t0);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+		clock_gettime(CLOCK_MONOTONIC, &t1);
+		total_stop_ns += timespec_diff_ns(&t0, &t1);
+	}
+
+	/* round-trip */
+	clock_gettime(CLOCK_MONOTONIC, &t0);
+	for (i = 0; i < BENCH_N; i++) {
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	}
+	clock_gettime(CLOCK_MONOTONIC, &t1);
+	total_rt_ns = timespec_diff_ns(&t0, &t1);
+
+	printf("# start ioctl only:      %lld ns/iter (N=%d, includes

syscall)\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +2 -0
+	       total_start_ns / BENCH_N, BENCH_N);
+	printf("# stop ioctl only:       %lld ns/iter (N=%d, includes

syscall)\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +2 -0
+	       total_stop_ns / BENCH_N, BENCH_N);
+	printf("# start+stop roundtrip:  %lld ns/iter (N=%d, includes 2

syscalls)\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +52 -0
+	       total_rt_ns / BENCH_N, BENCH_N);
+	return 0;
+}
+
+/*
+ * Print the ELF file offset of <symname> in <binary>.  Walks .symtab
+ * (falling back to .dynsym) and converts vaddr to file offset via PT_LOAD.
+ * Supports 32- and 64-bit ELF.
+ */
+static int sym_offset(const char *binary, const char *symname)
+{
+	int fd;
+	struct stat st;
+	void *map;
+	Elf64_Ehdr *ehdr;
+	Elf32_Ehdr *ehdr32;
+	int is64;
+	uint64_t sym_vaddr = 0;
+	int found = 0;
+	uint64_t file_offset = 0;
+
+	fd = open(binary, O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "open %s: %s\n", binary, strerror(errno));
+		return 1;
+	}
+	if (fstat(fd, &st) < 0) {
+		close(fd);
+		return 1;
+	}
+	map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+	close(fd);
+	if (map == MAP_FAILED) {
+		fprintf(stderr, "mmap: %s\n", strerror(errno));
+		return 1;
+	}
+
+	ehdr = (Elf64_Ehdr *)map;
+	ehdr32 = (Elf32_Ehdr *)map;
+	if (st.st_size < 4 ||
+	    ehdr->e_ident[EI_MAG0] != ELFMAG0 ||
+	    ehdr->e_ident[EI_MAG1] != ELFMAG1 ||
+	    ehdr->e_ident[EI_MAG2] != ELFMAG2 ||
+	    ehdr->e_ident[EI_MAG3] != ELFMAG3) {
+		fprintf(stderr, "%s: not an ELF file\n", binary);
+		munmap(map, (size_t)st.st_size);
+		return 1;
+	}
+	is64 = (ehdr->e_ident[EI_CLASS] == ELFCLASS64);
+
+	if (is64) {
+		Elf64_Shdr *shdrs = (Elf64_Shdr *)((char *)map + ehdr-
Quoted 1 line

e_shoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +17 -0
+		Elf64_Shdr *shstrtab_hdr = &shdrs[ehdr->e_shstrndx];
+		const char *shstrtab = (char *)map + shstrtab_hdr->sh_offset;
+		int si;
+
+		/* prefer .symtab; fall back to .dynsym */
+		for (int pass = 0; pass < 2 && !found; pass++) {
+			const char *target = pass ? ".dynsym" : ".symtab";
+
+			for (si = 0; si < ehdr->e_shnum && !found; si++) {
+				Elf64_Shdr *sh = &shdrs[si];
+				const char *name = shstrtab + sh->sh_name;
+
+				if (strcmp(name, target) != 0)
+					continue;
+
+				Elf64_Shdr *strtab_sh = &shdrs[sh->sh_link];
+				const char *strtab = (char *)map + strtab_sh-
Quoted 1 line

sh_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				Elf64_Sym *syms = (Elf64_Sym *)((char *)map +

sh->sh_offset);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				uint64_t nsyms = sh->sh_size /

sizeof(Elf64_Sym);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +4 -0
+				uint64_t j;
+
+				for (j = 0; j < nsyms; j++) {
+					if (strcmp(strtab + syms[j].st_name,

symname) == 0) {

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +10 -0
+						sym_vaddr = syms[j].st_value;
+						found = 1;
+						break;
+					}
+				}
+			}
+		}
+
+		if (!found) {
+			fprintf(stderr, "symbol '%s' not found in %s\n",

symname, binary);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +6 -0
+			munmap(map, (size_t)st.st_size);
+			return 1;
+		}
+
+		/* Convert vaddr to file offset via PT_LOAD segments */
+		Elf64_Phdr *phdrs = (Elf64_Phdr *)((char *)map + ehdr-
Quoted 1 line

e_phoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +10 -0
+		int pi;
+
+		for (pi = 0; pi < ehdr->e_phnum; pi++) {
+			Elf64_Phdr *ph = &phdrs[pi];
+
+			if (ph->p_type != PT_LOAD)
+				continue;
+			if (sym_vaddr >= ph->p_vaddr &&
+			    sym_vaddr < ph->p_vaddr + ph->p_filesz) {
+				file_offset = sym_vaddr - ph->p_vaddr + ph-
Quoted 1 line

p_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +6 -0
+				break;
+			}
+		}
+	} else {
+		/* 32-bit ELF */
+		Elf32_Shdr *shdrs = (Elf32_Shdr *)((char *)map + ehdr32-
Quoted 1 line

e_shoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +17 -0
+		Elf32_Shdr *shstrtab_hdr = &shdrs[ehdr32->e_shstrndx];
+		const char *shstrtab = (char *)map + shstrtab_hdr->sh_offset;
+		int si;
+		uint32_t sym_vaddr32 = 0;
+
+		for (int pass = 0; pass < 2 && !found; pass++) {
+			const char *target = pass ? ".dynsym" : ".symtab";
+
+			for (si = 0; si < ehdr32->e_shnum && !found; si++) {
+				Elf32_Shdr *sh = &shdrs[si];
+				const char *name = shstrtab + sh->sh_name;
+
+				if (strcmp(name, target) != 0)
+					continue;
+
+				Elf32_Shdr *strtab_sh = &shdrs[sh->sh_link];
+				const char *strtab = (char *)map + strtab_sh-
Quoted 1 line

sh_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				Elf32_Sym *syms = (Elf32_Sym *)((char *)map +

sh->sh_offset);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				uint32_t nsyms = sh->sh_size /

sizeof(Elf32_Sym);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +4 -0
+				uint32_t j;
+
+				for (j = 0; j < nsyms; j++) {
+					if (strcmp(strtab + syms[j].st_name,

symname) == 0) {

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+						sym_vaddr32 =

syms[j].st_value;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +9 -0
+						found = 1;
+						break;
+					}
+				}
+			}
+		}
+
+		if (!found) {
+			fprintf(stderr, "symbol '%s' not found in %s\n",

symname, binary);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +5 -0
+			munmap(map, (size_t)st.st_size);
+			return 1;
+		}
+
+		Elf32_Phdr *phdrs = (Elf32_Phdr *)((char *)map + ehdr32-
Quoted 1 line

e_phoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +10 -0
+		int pi;
+
+		for (pi = 0; pi < ehdr32->e_phnum; pi++) {
+			Elf32_Phdr *ph = &phdrs[pi];
+
+			if (ph->p_type != PT_LOAD)
+				continue;
+			if (sym_vaddr32 >= ph->p_vaddr &&
+			    sym_vaddr32 < ph->p_vaddr + ph->p_filesz) {
+				file_offset = sym_vaddr32 - ph->p_vaddr + ph-
Quoted 1 line

p_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +24 -0
+				break;
+			}
+		}
+		sym_vaddr = sym_vaddr32;
+	}
+
+	munmap(map, (size_t)st.st_size);
+
+	if (!file_offset && sym_vaddr) {
+		fprintf(stderr, "could not map vaddr 0x%lx to file offset\n",
+			(unsigned long)sym_vaddr);
+		return 1;
+	}
+
+	printf("0x%lx\n", (unsigned long)file_offset);
+	return 0;
+}
+
+int main(int argc, char *argv[])
+{
+	int rc;
+
+	if (argc < 2) {
+		fprintf(stderr, "Usage: %s <subcommand> [args...]\n",

argv[0]);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +7 -0
+		return 1;
+	}
+
+	/* sym_offset does not need /dev/rv */
+	if (strcmp(argv[1], "sym_offset") == 0) {
+		if (argc < 4) {
+			fprintf(stderr, "Usage: %s sym_offset <binary>

<symbol>\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +7 -0
+				argv[0]);
+			return 1;
+		}
+		return sym_offset(argv[2], argv[3]);
+	}
+
+	/* not_enabled: monitor is disabled; bind must return ENODEV without

open_rv() */

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +30 -0
+	if (strcmp(argv[1], "not_enabled") == 0)
+		return test_not_enabled();
+
+	if (open_rv() < 0)
+		return 2; /* skip */
+
+	if (strcmp(argv[1], "bench") == 0)
+		rc = test_bench();
+	else if (strcmp(argv[1], "within_budget") == 0)
+		rc = test_within_budget();
+	else if (strcmp(argv[1], "over_budget_running") == 0)
+		rc = test_over_budget_running();
+	else if (strcmp(argv[1], "over_budget_sleeping") == 0)
+		rc = test_over_budget_sleeping();
+	else if (strcmp(argv[1], "over_budget_waiting") == 0)
+		rc = test_over_budget_waiting();
+	else if (strcmp(argv[1], "double_start") == 0)
+		rc = test_double_start();
+	else if (strcmp(argv[1], "stop_no_start") == 0)
+		rc = test_stop_no_start();
+	else if (strcmp(argv[1], "multi_thread") == 0)
+		rc = test_multi_thread();
+	else {
+		fprintf(stderr, "Unknown test: %s\n", argv[1]);
+		rc = 1;
+	}
+
+	close(rv_fd);
+	return rc;
+}
tools/testing/selftests/verification/tlob/tlob_target.c new file +8 -0
new file mode 100644
index 000000000000..0fdbc575d71d
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob_target.c - uprobe target binary for tlob selftests.
+ *
+ * Provides three start/stop probe pairs, each designed to exercise a
+ * different dominant component of the detail_env_tlob ns breakdown:
+ *
+ *   tlob_busy_work    / tlob_busy_work_done    - busy-spin: running_ns

dominates
+ *   tlob_sleep_work   / tlob_sleep_work_done   - nanosleep: sleeping_ns dominates
+ *   tlob_preempt_work / tlob_preempt_work_done - busy-spin: waiting_ns dominates
+ *                                                (needs an RT competitor on the same CPU)
+ *
+ * Usage: tlob_target <duration_ms> [mode]
+ *
+ * mode is one of: busy (default), sleep, preempt.
+ * Loops in 200 ms iterations until <duration_ms> has elapsed
+ * (0 = run for ~24 hours).
+ */

tools/testing/selftests/verification/tlob/tlob_target.c new file +120 -0
+#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
+
+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)
+{
+	/* empty: uprobe fires on entry */
+}
+
+/* start probe; busy-spin so running_ns dominates */
+noinline void tlob_busy_work(unsigned long duration_ns)
+{
+	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_ns);
+
+	tlob_busy_work_done();
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_sleep_work_done(void)
+{
+	/* empty: uprobe fires on entry */
+}
+
+/* start probe; nanosleep so sleeping_ns dominates */
+noinline void tlob_sleep_work(unsigned long duration_ms)
+{
+	struct timespec ts = {
+		.tv_sec  = duration_ms / 1000,
+		.tv_nsec = (long)(duration_ms % 1000) * 1000000L,
+	};
+	nanosleep(&ts, NULL);
+	tlob_sleep_work_done();
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_preempt_work_done(void)
+{
+	/* empty: uprobe fires on entry */
+}
+
+/*
+ * start probe; busy-spin so an RT competitor on the same CPU drives
+ * waiting_ns (prev_state==0 -> preempt event, task stays runnable off-CPU).
+ */
+noinline void tlob_preempt_work(unsigned long duration_ms)
+{
+	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);
+
+	tlob_preempt_work_done();
+}
+
+int main(int argc, char *argv[])
+{
+	unsigned long duration_ms = 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];
+
+	clock_gettime(CLOCK_MONOTONIC, &deadline);
+	timespec_add_ms(&deadline, duration_ms ? duration_ms : 86400000UL);
+
+	do {
+		if (strcmp(mode, "sleep") == 0)
+			tlob_sleep_work(200);
+		else if (strcmp(mode, "preempt") == 0)
+			tlob_preempt_work(200);
+		else
+			tlob_busy_work(200 * 1000000UL);
+		clock_gettime(CLOCK_MONOTONIC, &now);
+	} while (timespec_before(&now, &deadline));
+
+	return 0;
+}
maintainer
#30 lore raw

Re: [RFC PATCH v2 10/10] selftests/verification: add tlob selftests

In reply to [email protected]
Quoted 19 lines · On Tue, 2026-05-12 at 02:24 +0800, [email protected] wrote:

From: Wen Yang <[email protected]>

Add selftest coverage for the tlob RV monitor in
tools/testing/selftests/verification/.

Two helper binaries are built by tlob/Makefile: tlob_helper for the ioctl interface (/dev/rv) and tlob_uprobe_target for the uprobe tests. The top-level Makefile delegates to tlob/ via a generic MONITOR_SUBDIRS pattern so monitor-specific build details stay within each monitor's own subdirectory.

Eight test files cover the tracefs control interface (tracefs.tc), the ioctl self-instrumentation interface (ioctl.tc, 8 scenarios), and the uprobe external monitoring interface (uprobe_bind.tc, uprobe_violation.tc, uprobe_no_event.tc, uprobe_multi.tc, uprobe_detail_sleeping.tc, uprobe_detail_waiting.tc).

Tested on x86_64 with vng (virtme-ng):

Quoted 8 lines

  TAP version 13
  1..12
  ok 1 Test monitor enable/disable
  ok 2 Test monitor reactor setting
  ok 3 Check available monitors
  ok 4 Test wwnr monitor with printk reactor
  ok 5 Test tlob ioctl self-instrumentation (within/over-budget, error paths) ok 6 Test tlob monitor tracefs interface (enable/disable and files)

This should be tested together with the other monitors (enable/disable), we could at most expand those with the check_requires, though that seems to be meant for ftracetest's internals.

Let's focus on tlob-only features in this patch.

Thanks,
Gabriele

Quoted 1,356 lines

  ok 7 uprobe binding: visible in monitor file, removable, duplicate offset rejected
  ok 8 uprobe detail sleeping: sleeping_ns dominates when task blocks between probes
  ok 9 uprobe detail waiting: waiting_ns dominates when task is preempted between probes
  ok 10 Two bindings on same binary with different offsets and budgets fire independently
  ok 11 Verify no spurious error_env_tlob events without an active uprobe binding
  ok 12 uprobe violation: error_env_tlob and detail_env_tlob fire with correct fields
  # Totals: pass:12 fail:0 xfail:0 xpass:0 skip:0 error:0

---
 tools/testing/selftests/verification/Makefile |  21 +- .../verification/test.d/tlob/ioctl.tc         |  36 + .../verification/test.d/tlob/tracefs.tc       |  17 + .../verification/test.d/tlob/uprobe_bind.tc   |  34 + .../test.d/tlob/uprobe_detail_sleeping.tc     |  47 ++ .../test.d/tlob/uprobe_detail_waiting.tc      |  60 ++ .../verification/test.d/tlob/uprobe_multi.tc  |  60 ++ .../test.d/tlob/uprobe_no_event.tc            |  19 + .../test.d/tlob/uprobe_violation.tc           |  60 ++ .../selftests/verification/tlob/Makefile      |  21 + .../selftests/verification/tlob/tlob_ioctl.c  | 626 ++++++++++++++++++ .../selftests/verification/tlob/tlob_target.c | 138 ++++

 12 files changed, 1138 insertions(+), 1 deletion(-)

 create mode 100644 tools/testing/selftests/verification/test.d/tlob/ioctl.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/tracefs.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc create mode 100644 tools/testing/selftests/verification/tlob/Makefile create mode 100644 tools/testing/selftests/verification/tlob/tlob_ioctl.c create mode 100644 tools/testing/selftests/verification/tlob/tlob_target.c

tools/testing/selftests/verification/Makefile +0 -1
index aa8790c22a71..b5584fd3762d 100644
@@ -1,8 +1,27 @@
  # SPDX-License-Identifier: GPL-2.0
-all:


 TEST_PROGS := verificationtest-ktap
 TEST_FILES := test.d settings
 EXTRA_CLEAN := $(OUTPUT)/logs/*

tools/testing/selftests/verification/Makefile +6 -0
+# Subdirectories that provide helper binaries for the test runner.
+# Each entry must contain a Makefile that accepts OUTDIR= and deposits
+# its binaries there; verificationtest-ktap adds OUTDIR to PATH so
+# the ftracetest require-checks resolve the binaries by name.
+MONITOR_SUBDIRS := tlob
+

 include ../lib.mk

tools/testing/selftests/verification/Makefile +14 -0
+
+# Build and clean each monitor subdirectory.
+all: $(patsubst %,_build_%,$(MONITOR_SUBDIRS))
+
+clean: $(patsubst %,_clean_%,$(MONITOR_SUBDIRS))
+
+.PHONY: $(patsubst %,_build_%,$(MONITOR_SUBDIRS)) \
+        $(patsubst %,_clean_%,$(MONITOR_SUBDIRS))
+
+$(patsubst %,_build_%,$(MONITOR_SUBDIRS)): _build_%:
+	$(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)"
+
+$(patsubst %,_clean_%,$(MONITOR_SUBDIRS)): _clean_%:
+	$(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
tools/testing/selftests/verification/test.d/tlob/ioctl.tc new file +3 -0
new file mode 100644
index 000000000000..54ae249af9a6
@@ -0,0 +1,36 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob ioctl self-instrumentation (within/over-budget,

error paths)

tools/testing/selftests/verification/test.d/tlob/ioctl.tc new file +33 -0
+# requires: tlob:monitor tlob_ioctl:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+
+[ -c /dev/rv ] || exit_unsupported
+
+echo 1 > monitors/tlob/enable
+
+# within budget: 50 ms threshold, 10 ms workload
+"$TLOB_HELPER" within_budget
+
+# over budget in running state: 1 ms threshold, 100 ms busy-spin
+"$TLOB_HELPER" over_budget_running
+
+# over budget in sleeping state: 3 ms threshold, 50 ms sleep
+"$TLOB_HELPER" over_budget_sleeping
+
+# over budget in waiting state: 1 us threshold, sched_yield
+"$TLOB_HELPER" over_budget_waiting
+
+# error paths
+"$TLOB_HELPER" double_start
+"$TLOB_HELPER" stop_no_start
+
+# per-thread isolation
+"$TLOB_HELPER" multi_thread
+
+# bind against disabled monitor must return ENODEV, not crash
+echo 0 > monitors/tlob/enable
+"$TLOB_HELPER" not_enabled
+echo 1 > monitors/tlob/enable
+
+echo 0 > monitors/tlob/enable
tools/testing/selftests/verification/test.d/tlob/tracefs.tc new file +17 -0
new file mode 100644
index 000000000000..5d1e7cc02498
@@ -0,0 +1,17 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor tracefs interface (enable/disable and files)
+# requires: tlob:monitor
+
+check_requires monitors/tlob/enable monitors/tlob/desc monitors/tlob/monitor
+
+# enable / disable via the enable file
+echo 1 > monitors/tlob/enable
+grep -q 1 monitors/tlob/enable
+echo "tlob" >> enabled_monitors
+grep -q tlob enabled_monitors
+
+echo 0 > monitors/tlob/enable
+grep -q 0 monitors/tlob/enable
+echo "!tlob" >> enabled_monitors
+! grep -q "^tlob$" enabled_monitors
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
new file mode 100644
index 000000000000..41e20d593855
@@ -0,0 +1,34 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe binding (visible in monitor file, removable,

duplicate rejected)

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)

+[ -n "$busy_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > monitors/tlob/enable
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=5000000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
+
+# Binding must appear in monitor file with canonical hex-offset format.
+grep -qE "^p ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=[0-9]+$"

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +4 -0
+grep -q "threshold=5000000" "$TLOB_MONITOR"
+
+# Duplicate offset_start must be rejected.
+! echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=9999" >

"$TLOB_MONITOR" 2>/dev/null

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+
+# Remove the binding; it must no longer appear.
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR"
+! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR"
+
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > monitors/tlob/enable

diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc new file mode 100644
index 000000000000..2b8656e0fef1
--- /dev/null
+++
b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
@@ -0,0 +1,47 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe detail sleeping (sleeping_ns dominates when task

blocks between probes)

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+start_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work_done 2>/dev/null)
+[ -n "$start_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +12 -0
+
+"$UPROBE_TARGET" 5000 sleep &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# 50 ms budget; task sleeps 200 ms per iteration -> sleeping_ns dominates.
+echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=50000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +5 -0
+
+found=0; i=0
+while [ "$i" -lt 30 ]; do
+	sleep 0.1
+	grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=1;

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +17 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+line=$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1)
+running=$(echo "$line" | sed 's/.*running_ns=\([0-9]*\).*/\1/')
+waiting=$(echo "$line" | sed 's/.*waiting_ns=\([0-9]*\).*/\1/')
+sleeping=$(echo "$line" | sed 's/.*sleeping_ns=\([0-9]*\).*/\1/')
+[ "$sleeping" -gt "$((running + waiting))" ]
+
+echo > /sys/kernel/tracing/trace

diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc new file mode 100644
index 000000000000..0705854f24df
--- /dev/null
+++
b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +3 -0
@@ -0,0 +1,60 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe detail waiting (waiting_ns dominates when task is

preempted between probes)

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +10 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+command -v chrt    > /dev/null || exit_unsupported
+command -v taskset > /dev/null || exit_unsupported
+
+start_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_preempt_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_preempt_work_done 2>/dev/null)
+[ -n "$start_offset" ] || exit_unsupported
+[ -n "$stop_offset" ]  || exit_unsupported

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +11 -0
+
+cpu=0
+
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# Register probe before the target starts so the start uprobe fires on the
+# first entry to tlob_preempt_work. Budget: 500 ms.
+echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=500000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +15 -0
+
+# Target starts; start probe fires on tlob_preempt_work entry.
+taskset -c "$cpu" "$UPROBE_TARGET" 5000 preempt &
+busy_pid=$!
+sleep 0.05
+
+# RT hog on the same CPU preempts the target; target stays in waiting state
+# (runnable, off-CPU) until the budget expires -> waiting_ns dominates.
+chrt -f 99 taskset -c "$cpu" sh -c 'while true; do :; done' 2>/dev/null &
+hog_pid=$!
+
+found=0; i=0
+while [ "$i" -lt 30 ]; do
+	sleep 0.1
+	grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=1;

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file +18 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$hog_pid" 2>/dev/null; wait "$hog_pid" 2>/dev/null || true
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+line=$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1)
+running=$(echo "$line" | sed 's/.*running_ns=\([0-9]*\).*/\1/')
+sleeping=$(echo "$line" | sed 's/.*sleeping_ns=\([0-9]*\).*/\1/')
+waiting=$(echo "$line" | sed 's/.*waiting_ns=\([0-9]*\).*/\1/')
+[ "$waiting" -gt "$((running + sleeping))" ]
+
+echo > /sys/kernel/tracing/trace
tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +3 -0
new file mode 100644
index 000000000000..c4b8f7108ae9
@@ -0,0 +1,60 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test two uprobe bindings on same binary (different offsets

fire independently)

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work

2>/dev/null)
+busy_stop=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+sleep_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2>/dev/null)
+sleep_stop=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_sleep_work_done 2>/dev/null)
+[ -n "$busy_offset" ]  || exit_unsupported
+[ -n "$busy_stop" ]    || exit_unsupported
+[ -n "$sleep_offset" ] || exit_unsupported
+[ -n "$sleep_stop" ]   || exit_unsupported

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +4 -0
+
+"$UPROBE_TARGET" 30000 &       # busy mode: tlob_busy_work fires every 200 ms
+busy_pid=$!
+"$UPROBE_TARGET" 30000 sleep & # sleep mode: tlob_sleep_work fires every 200

ms

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +11 -0
+sleep_pid=$!
+sleep 0.05
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# Binding A: 5 s budget on the busy probe - must not fire in 200 ms loops.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=5000000" >

"$TLOB_MONITOR"

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +2 -0
+# Binding B: 10 ns budget on the sleep probe - fires on first invocation.
+echo "p ${UPROBE_TARGET}:${sleep_offset} ${sleep_stop} threshold=10" >

"$TLOB_MONITOR"

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

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file +15 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null
+echo "-${UPROBE_TARGET}:${sleep_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$sleep_pid" 2>/dev/null; wait "$sleep_pid" 2>/dev/null || true
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+
+[ "$found" = "1" ]
+# error_env_tlob payload: label and clock variable must be present.
+grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q

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

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

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

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

uprobe binding

tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file +16 -0
+# requires: tlob:monitor tlob_ioctl:program
+
+TLOB_MONITOR=monitors/tlob/monitor
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+sleep 0.5
+
+! grep -q "error_env_tlob" /sys/kernel/tracing/trace
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo > /sys/kernel/tracing/trace

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..624fdb950f6b

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +3 -0
@@ -0,0 +1,60 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test uprobe violation (error_env_tlob and detail_env_tlob fire

with correct fields)

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +7 -0
+# requires: tlob:monitor tlob_ioctl:program tlob_target:program
+
+TLOB_HELPER=$(command -v tlob_ioctl)
+UPROBE_TARGET=$(command -v tlob_target)
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work

2>/dev/null)
+stop_offset=$("$TLOB_HELPER" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)

+[ -n "$busy_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +13 -0
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# 10 ns budget - fires almost immediately; task is busy-spinning on-CPU.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=10" >

"$TLOB_MONITOR"

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

break; }

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +13 -0
+	i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$busy_pid" 2>/dev/null; wait "$busy_pid" 2>/dev/null || true
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+# error_env_tlob event label must be budget_exceeded
+grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q

"budget_exceeded"

tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc new file +15 -0
+
+# detail_env_tlob must have all five fields with the correct threshold
+line=$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1)
+echo "$line" | grep -q "pid="
+echo "$line" | grep -q "threshold_us=10"
+echo "$line" | grep -q "running_ns="
+echo "$line" | grep -q "waiting_ns="
+echo "$line" | grep -q "sleeping_ns="
+
+# Busy-spin keeps the task on-CPU: running_ns must exceed sleeping_ns.
+running=$(echo "$line" | sed 's/.*running_ns=\([0-9]*\).*/\1/')
+sleeping=$(echo "$line" | sed 's/.*sleeping_ns=\([0-9]*\).*/\1/')
+[ "$running" -gt "$sleeping" ]
+
+echo > /sys/kernel/tracing/trace
tools/testing/selftests/verification/tlob/Makefile new file +21 -0
new file mode 100644
index 000000000000..1bedf946cb34
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0
+# Builds tlob selftest helper binaries.
+#
+# Invoked by ../Makefile; pass OUTDIR to control the output directory
+# and TOOLS_INCLUDES for the in-tree UAPI -isystem flag.
+
+OUTDIR ?= $(CURDIR)/..
+CFLAGS += $(TOOLS_INCLUDES)
+
+.PHONY: all
+all: $(OUTDIR)/tlob_ioctl $(OUTDIR)/tlob_target
+
+$(OUTDIR)/tlob_ioctl: tlob_ioctl.c
+	$(CC) $(CFLAGS) -o $@ $< -lpthread
+
+$(OUTDIR)/tlob_target: tlob_target.c
+	$(CC) $(CFLAGS) -o $@ $<
+
+.PHONY: clean
+clean:
+	$(RM) $(OUTDIR)/tlob_ioctl $(OUTDIR)/tlob_target
tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +15 -0
new file mode 100644
index 000000000000..abb4e2e80a2c
@@ -0,0 +1,626 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob_ioctl.c - ioctl test driver and ELF utility for tlob selftests
+ *
+ * Usage: tlob_ioctl <subcommand> [args...]
+ *
+ *   not_enabled          - TRACE_START without monitor enabled -> ENODEV
+ *   within_budget        - sleep within budget -> 0
+ *   over_budget_running  - busy-spin past budget -> EOVERFLOW
+ *   over_budget_sleeping - sleep past budget -> EOVERFLOW
+ *   over_budget_waiting  - sched_yield into waiting state -> EOVERFLOW
+ *   double_start         - two starts without stop -> EALREADY
+ *   stop_no_start        - stop without start -> EINVAL
+ *   multi_thread         - two fds: thread A within budget, thread B over
+ *   bench                - TRACE_START/STOP latency (TAP output, always

passes)
+ *   sym_offset <binary> <symbol> - print ELF file offset of symbol
+ *
+ * Exit: 0 = pass, 1 = fail, 2 = skip (device not available).
+ */

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +86 -0
+#define _GNU_SOURCE
+#include <elf.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/rv.h>
+
+static int rv_fd = -1;
+
+static int open_rv(void)
+{
+	struct rv_bind_args bind = { .monitor_name = "tlob" };
+
+	rv_fd = open("/dev/rv", O_RDWR);
+	if (rv_fd < 0) {
+		fprintf(stderr, "open /dev/rv: %s\n", strerror(errno));
+		return -1;
+	}
+	if (ioctl(rv_fd, RV_IOCTL_BIND_MONITOR, &bind) < 0) {
+		fprintf(stderr, "bind tlob: %s\n", strerror(errno));
+		close(rv_fd);
+		rv_fd = -1;
+		return -1;
+	}
+	return 0;
+}
+
+static void busy_spin_us(unsigned long us)
+{
+	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 < us * 1000UL);
+}
+
+static int trace_start(uint64_t threshold_us)
+{
+	struct tlob_start_args args = {
+		.threshold_us = threshold_us,
+	};
+
+	return ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+}
+
+static int trace_stop(void)
+{
+	return ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+}
+
+/* Synchronous TRACE_START / TRACE_STOP tests */
+
+/* Bind to a disabled monitor must return ENODEV without crashing */
+static int test_not_enabled(void)
+{
+	struct rv_bind_args bind = { .monitor_name = "tlob" };
+	int fd;
+	int ret;
+
+	fd = open("/dev/rv", O_RDWR);
+	if (fd < 0) {
+		fprintf(stderr, "open /dev/rv: %s\n", strerror(errno));
+		return 2; /* skip */
+	}
+
+	ret = ioctl(fd, RV_IOCTL_BIND_MONITOR, &bind);
+	close(fd);
+
+	if (ret == 0) {
+		fprintf(stderr, "RV_IOCTL_BIND_MONITOR: expected ENODEV, got

success\n");

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +4 -0
+		return 1;
+	}
+	if (errno != ENODEV) {
+		fprintf(stderr, "RV_IOCTL_BIND_MONITOR: expected ENODEV, got

%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +82 -0
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_within_budget(void)
+{
+	int ret;
+
+	/* 50 ms budget */
+	if (trace_start(50000) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	usleep(10000); /* 10 ms */
+	ret = trace_stop();
+	if (ret != 0) {
+		fprintf(stderr, "TRACE_STOP: expected 0, got %d errno=%s\n",
+			ret, strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_over_budget_running(void)
+{
+	int ret;
+
+	/* 1 ms budget */
+	if (trace_start(1000) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	busy_spin_us(100000); /* 100 ms */
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n");
+		return 1;
+	}
+	if (errno != EOVERFLOW) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_over_budget_sleeping(void)
+{
+	int ret;
+
+	/* 3 ms budget */
+	if (trace_start(3000) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	usleep(50000); /* 50 ms; sleeping time counts toward budget */
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n");
+		return 1;
+	}
+	if (errno != EOVERFLOW) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+static int test_over_budget_waiting(void)
+{
+	int ret;
+
+	/* 1 us budget */
+	if (trace_start(1) < 0) {
+		fprintf(stderr, "TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	sched_yield(); /* running -> waiting -> running */
+	busy_spin_us(10); /* 10 us >> 1 us budget; hrtimer fires during spin

*/

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +27 -0
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n");
+		return 1;
+	}
+	if (errno != EOVERFLOW) {
+		fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+/* Error-handling tests */
+
+static int test_double_start(void)
+{
+	int ret;
+
+	/* 10 s: large enough the hrtimer won't fire during the test */
+	if (trace_start(10000000ULL) < 0) {
+		fprintf(stderr, "first TRACE_START: %s\n", strerror(errno));
+		return 1;
+	}
+	ret = trace_start(10000000);
+	if (ret == 0) {
+		fprintf(stderr, "second TRACE_START: expected EALREADY, got

0\n");

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +5 -0
+		trace_stop();
+		return 1;
+	}
+	if (errno != EALREADY) {
+		fprintf(stderr, "second TRACE_START: expected EALREADY, got

%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +49 -0
+			strerror(errno));
+		trace_stop();
+		return 1;
+	}
+	trace_stop();
+	return 0;
+}
+
+static int test_stop_no_start(void)
+{
+	int ret;
+
+	/* Ensure clean state: ignore error from a stale entry */
+	trace_stop();
+
+	ret = trace_stop();
+	if (ret == 0) {
+		fprintf(stderr, "TRACE_STOP: expected EINVAL, got 0\n");
+		return 1;
+	}
+	if (errno != EINVAL) {
+		fprintf(stderr, "TRACE_STOP: expected EINVAL, got %s\n",
+			strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+/* Two threads, each with its own fd: A within budget, B over budget. */
+
+struct mt_thread_args {
+	uint64_t      threshold_us;
+	unsigned long workload_us;
+	int           busy;
+	int           expect_eoverflow;
+	int           result;
+};
+
+static void *mt_thread_fn(void *arg)
+{
+	struct mt_thread_args *a = arg;
+	struct tlob_start_args args = { .threshold_us = a->threshold_us };
+	struct rv_bind_args bind = { .monitor_name = "tlob" };
+	int fd;
+	int ret;
+
+	fd = open("/dev/rv", O_RDWR);
+	if (fd < 0) {
+		fprintf(stderr, "thread open /dev/rv: %s\n",

strerror(errno));

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +27 -0
+		a->result = 1;
+		return NULL;
+	}
+	if (ioctl(fd, RV_IOCTL_BIND_MONITOR, &bind) < 0) {
+		fprintf(stderr, "thread bind tlob: %s\n", strerror(errno));
+		close(fd);
+		a->result = 1;
+		return NULL;
+	}
+
+	ret = ioctl(fd, TLOB_IOCTL_TRACE_START, &args);
+	if (ret < 0) {
+		fprintf(stderr, "thread TRACE_START: %s\n", strerror(errno));
+		close(fd);
+		a->result = 1;
+		return NULL;
+	}
+
+	if (a->busy)
+		busy_spin_us(a->workload_us);
+	else
+		usleep(a->workload_us);
+
+	ret = ioctl(fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	if (a->expect_eoverflow) {
+		if (ret == 0 || errno != EOVERFLOW) {
+			fprintf(stderr, "thread: expected EOVERFLOW, got

ret=%d errno=%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +8 -0
+				ret, strerror(errno));
+			close(fd);
+			a->result = 1;
+			return NULL;
+		}
+	} else {
+		if (ret != 0) {
+			fprintf(stderr, "thread: expected 0, got ret=%d

errno=%s\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +17 -0
+				ret, strerror(errno));
+			close(fd);
+			a->result = 1;
+			return NULL;
+		}
+	}
+	close(fd);
+	a->result = 0;
+	return NULL;
+}
+
+static int test_multi_thread(void)
+{
+	pthread_t ta, tb;
+	struct mt_thread_args a = {
+		.threshold_us     = 20000,   /* 20 ms */
+		.workload_us      = 5000,    /* 5 ms sleep -> within budget

*/

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +75 -0
+		.busy             = 0,
+		.expect_eoverflow = 0,
+	};
+	struct mt_thread_args b = {
+		.threshold_us     = 3000,    /* 3 ms */
+		.workload_us      = 30000,   /* 30 ms spin -> over budget */
+		.busy             = 1,
+		.expect_eoverflow = 1,
+	};
+
+	pthread_create(&ta, NULL, mt_thread_fn, &a);
+	pthread_create(&tb, NULL, mt_thread_fn, &b);
+	pthread_join(ta, NULL);
+	pthread_join(tb, NULL);
+
+	return (a.result || b.result) ? 1 : 0;
+}
+
+/*
+ * Benchmark TRACE_START, TRACE_STOP, and round-trip ioctls.
+ * Output uses TAP '#' prefix; always returns 0.
+ */
+#define BENCH_WARMUP  32
+#define BENCH_N      1000
+
+static long long timespec_diff_ns(const struct timespec *a,
+				   const struct timespec *b)
+{
+	return (long long)(b->tv_sec - a->tv_sec) * 1000000000LL
+		+ (b->tv_nsec - a->tv_nsec);
+}
+
+static int test_bench(void)
+{
+	struct tlob_start_args args = {
+		.threshold_us = 10000000ULL, /* 10 s */
+	};
+	struct timespec t0, t1;
+	long long total_start_ns = 0, total_stop_ns = 0, total_rt_ns = 0;
+	int i;
+
+	/* warm up */
+	for (i = 0; i < BENCH_WARMUP; i++) {
+		if (ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args) == 0)
+			ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	}
+
+	/* start only */
+	for (i = 0; i < BENCH_N; i++) {
+		clock_gettime(CLOCK_MONOTONIC, &t0);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+		clock_gettime(CLOCK_MONOTONIC, &t1);
+		total_start_ns += timespec_diff_ns(&t0, &t1);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	}
+
+	/* stop only */
+	for (i = 0; i < BENCH_N; i++) {
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+		clock_gettime(CLOCK_MONOTONIC, &t0);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+		clock_gettime(CLOCK_MONOTONIC, &t1);
+		total_stop_ns += timespec_diff_ns(&t0, &t1);
+	}
+
+	/* round-trip */
+	clock_gettime(CLOCK_MONOTONIC, &t0);
+	for (i = 0; i < BENCH_N; i++) {
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args);
+		ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL);
+	}
+	clock_gettime(CLOCK_MONOTONIC, &t1);
+	total_rt_ns = timespec_diff_ns(&t0, &t1);
+
+	printf("# start ioctl only:      %lld ns/iter (N=%d, includes

syscall)\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +2 -0
+	       total_start_ns / BENCH_N, BENCH_N);
+	printf("# stop ioctl only:       %lld ns/iter (N=%d, includes

syscall)\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +2 -0
+	       total_stop_ns / BENCH_N, BENCH_N);
+	printf("# start+stop roundtrip:  %lld ns/iter (N=%d, includes 2

syscalls)\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +52 -0
+	       total_rt_ns / BENCH_N, BENCH_N);
+	return 0;
+}
+
+/*
+ * Print the ELF file offset of <symname> in <binary>.  Walks .symtab
+ * (falling back to .dynsym) and converts vaddr to file offset via PT_LOAD.
+ * Supports 32- and 64-bit ELF.
+ */
+static int sym_offset(const char *binary, const char *symname)
+{
+	int fd;
+	struct stat st;
+	void *map;
+	Elf64_Ehdr *ehdr;
+	Elf32_Ehdr *ehdr32;
+	int is64;
+	uint64_t sym_vaddr = 0;
+	int found = 0;
+	uint64_t file_offset = 0;
+
+	fd = open(binary, O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "open %s: %s\n", binary, strerror(errno));
+		return 1;
+	}
+	if (fstat(fd, &st) < 0) {
+		close(fd);
+		return 1;
+	}
+	map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+	close(fd);
+	if (map == MAP_FAILED) {
+		fprintf(stderr, "mmap: %s\n", strerror(errno));
+		return 1;
+	}
+
+	ehdr = (Elf64_Ehdr *)map;
+	ehdr32 = (Elf32_Ehdr *)map;
+	if (st.st_size < 4 ||
+	    ehdr->e_ident[EI_MAG0] != ELFMAG0 ||
+	    ehdr->e_ident[EI_MAG1] != ELFMAG1 ||
+	    ehdr->e_ident[EI_MAG2] != ELFMAG2 ||
+	    ehdr->e_ident[EI_MAG3] != ELFMAG3) {
+		fprintf(stderr, "%s: not an ELF file\n", binary);
+		munmap(map, (size_t)st.st_size);
+		return 1;
+	}
+	is64 = (ehdr->e_ident[EI_CLASS] == ELFCLASS64);
+
+	if (is64) {
+		Elf64_Shdr *shdrs = (Elf64_Shdr *)((char *)map + ehdr-
Quoted 1 line

e_shoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +17 -0
+		Elf64_Shdr *shstrtab_hdr = &shdrs[ehdr->e_shstrndx];
+		const char *shstrtab = (char *)map + shstrtab_hdr->sh_offset;
+		int si;
+
+		/* prefer .symtab; fall back to .dynsym */
+		for (int pass = 0; pass < 2 && !found; pass++) {
+			const char *target = pass ? ".dynsym" : ".symtab";
+
+			for (si = 0; si < ehdr->e_shnum && !found; si++) {
+				Elf64_Shdr *sh = &shdrs[si];
+				const char *name = shstrtab + sh->sh_name;
+
+				if (strcmp(name, target) != 0)
+					continue;
+
+				Elf64_Shdr *strtab_sh = &shdrs[sh->sh_link];
+				const char *strtab = (char *)map + strtab_sh-
Quoted 1 line

sh_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				Elf64_Sym *syms = (Elf64_Sym *)((char *)map +

sh->sh_offset);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				uint64_t nsyms = sh->sh_size /

sizeof(Elf64_Sym);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +4 -0
+				uint64_t j;
+
+				for (j = 0; j < nsyms; j++) {
+					if (strcmp(strtab + syms[j].st_name,

symname) == 0) {

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +10 -0
+						sym_vaddr = syms[j].st_value;
+						found = 1;
+						break;
+					}
+				}
+			}
+		}
+
+		if (!found) {
+			fprintf(stderr, "symbol '%s' not found in %s\n",

symname, binary);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +6 -0
+			munmap(map, (size_t)st.st_size);
+			return 1;
+		}
+
+		/* Convert vaddr to file offset via PT_LOAD segments */
+		Elf64_Phdr *phdrs = (Elf64_Phdr *)((char *)map + ehdr-
Quoted 1 line

e_phoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +10 -0
+		int pi;
+
+		for (pi = 0; pi < ehdr->e_phnum; pi++) {
+			Elf64_Phdr *ph = &phdrs[pi];
+
+			if (ph->p_type != PT_LOAD)
+				continue;
+			if (sym_vaddr >= ph->p_vaddr &&
+			    sym_vaddr < ph->p_vaddr + ph->p_filesz) {
+				file_offset = sym_vaddr - ph->p_vaddr + ph-
Quoted 1 line

p_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +6 -0
+				break;
+			}
+		}
+	} else {
+		/* 32-bit ELF */
+		Elf32_Shdr *shdrs = (Elf32_Shdr *)((char *)map + ehdr32-
Quoted 1 line

e_shoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +17 -0
+		Elf32_Shdr *shstrtab_hdr = &shdrs[ehdr32->e_shstrndx];
+		const char *shstrtab = (char *)map + shstrtab_hdr->sh_offset;
+		int si;
+		uint32_t sym_vaddr32 = 0;
+
+		for (int pass = 0; pass < 2 && !found; pass++) {
+			const char *target = pass ? ".dynsym" : ".symtab";
+
+			for (si = 0; si < ehdr32->e_shnum && !found; si++) {
+				Elf32_Shdr *sh = &shdrs[si];
+				const char *name = shstrtab + sh->sh_name;
+
+				if (strcmp(name, target) != 0)
+					continue;
+
+				Elf32_Shdr *strtab_sh = &shdrs[sh->sh_link];
+				const char *strtab = (char *)map + strtab_sh-
Quoted 1 line

sh_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				Elf32_Sym *syms = (Elf32_Sym *)((char *)map +

sh->sh_offset);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+				uint32_t nsyms = sh->sh_size /

sizeof(Elf32_Sym);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +4 -0
+				uint32_t j;
+
+				for (j = 0; j < nsyms; j++) {
+					if (strcmp(strtab + syms[j].st_name,

symname) == 0) {

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +1 -0
+						sym_vaddr32 =

syms[j].st_value;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +9 -0
+						found = 1;
+						break;
+					}
+				}
+			}
+		}
+
+		if (!found) {
+			fprintf(stderr, "symbol '%s' not found in %s\n",

symname, binary);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +5 -0
+			munmap(map, (size_t)st.st_size);
+			return 1;
+		}
+
+		Elf32_Phdr *phdrs = (Elf32_Phdr *)((char *)map + ehdr32-
Quoted 1 line

e_phoff);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +10 -0
+		int pi;
+
+		for (pi = 0; pi < ehdr32->e_phnum; pi++) {
+			Elf32_Phdr *ph = &phdrs[pi];
+
+			if (ph->p_type != PT_LOAD)
+				continue;
+			if (sym_vaddr32 >= ph->p_vaddr &&
+			    sym_vaddr32 < ph->p_vaddr + ph->p_filesz) {
+				file_offset = sym_vaddr32 - ph->p_vaddr + ph-
Quoted 1 line

p_offset;

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +24 -0
+				break;
+			}
+		}
+		sym_vaddr = sym_vaddr32;
+	}
+
+	munmap(map, (size_t)st.st_size);
+
+	if (!file_offset && sym_vaddr) {
+		fprintf(stderr, "could not map vaddr 0x%lx to file offset\n",
+			(unsigned long)sym_vaddr);
+		return 1;
+	}
+
+	printf("0x%lx\n", (unsigned long)file_offset);
+	return 0;
+}
+
+int main(int argc, char *argv[])
+{
+	int rc;
+
+	if (argc < 2) {
+		fprintf(stderr, "Usage: %s <subcommand> [args...]\n",

argv[0]);

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +7 -0
+		return 1;
+	}
+
+	/* sym_offset does not need /dev/rv */
+	if (strcmp(argv[1], "sym_offset") == 0) {
+		if (argc < 4) {
+			fprintf(stderr, "Usage: %s sym_offset <binary>

<symbol>\n",

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +7 -0
+				argv[0]);
+			return 1;
+		}
+		return sym_offset(argv[2], argv[3]);
+	}
+
+	/* not_enabled: monitor is disabled; bind must return ENODEV without

open_rv() */

tools/testing/selftests/verification/tlob/tlob_ioctl.c new file +30 -0
+	if (strcmp(argv[1], "not_enabled") == 0)
+		return test_not_enabled();
+
+	if (open_rv() < 0)
+		return 2; /* skip */
+
+	if (strcmp(argv[1], "bench") == 0)
+		rc = test_bench();
+	else if (strcmp(argv[1], "within_budget") == 0)
+		rc = test_within_budget();
+	else if (strcmp(argv[1], "over_budget_running") == 0)
+		rc = test_over_budget_running();
+	else if (strcmp(argv[1], "over_budget_sleeping") == 0)
+		rc = test_over_budget_sleeping();
+	else if (strcmp(argv[1], "over_budget_waiting") == 0)
+		rc = test_over_budget_waiting();
+	else if (strcmp(argv[1], "double_start") == 0)
+		rc = test_double_start();
+	else if (strcmp(argv[1], "stop_no_start") == 0)
+		rc = test_stop_no_start();
+	else if (strcmp(argv[1], "multi_thread") == 0)
+		rc = test_multi_thread();
+	else {
+		fprintf(stderr, "Unknown test: %s\n", argv[1]);
+		rc = 1;
+	}
+
+	close(rv_fd);
+	return rc;
+}
tools/testing/selftests/verification/tlob/tlob_target.c new file +8 -0
new file mode 100644
index 000000000000..0fdbc575d71d
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob_target.c - uprobe target binary for tlob selftests.
+ *
+ * Provides three start/stop probe pairs, each designed to exercise a
+ * different dominant component of the detail_env_tlob ns breakdown:
+ *
+ *   tlob_busy_work    / tlob_busy_work_done    - busy-spin: running_ns

dominates
+ *   tlob_sleep_work   / tlob_sleep_work_done   - nanosleep: sleeping_ns dominates
+ *   tlob_preempt_work / tlob_preempt_work_done - busy-spin: waiting_ns dominates
+ *                                                (needs an RT competitor on the same CPU)
+ *
+ * Usage: tlob_target <duration_ms> [mode]
+ *
+ * mode is one of: busy (default), sleep, preempt.
+ * Loops in 200 ms iterations until <duration_ms> has elapsed
+ * (0 = run for ~24 hours).
+ */

tools/testing/selftests/verification/tlob/tlob_target.c new file +120 -0
+#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
+
+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)
+{
+	/* empty: uprobe fires on entry */
+}
+
+/* start probe; busy-spin so running_ns dominates */
+noinline void tlob_busy_work(unsigned long duration_ns)
+{
+	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_ns);
+
+	tlob_busy_work_done();
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_sleep_work_done(void)
+{
+	/* empty: uprobe fires on entry */
+}
+
+/* start probe; nanosleep so sleeping_ns dominates */
+noinline void tlob_sleep_work(unsigned long duration_ms)
+{
+	struct timespec ts = {
+		.tv_sec  = duration_ms / 1000,
+		.tv_nsec = (long)(duration_ms % 1000) * 1000000L,
+	};
+	nanosleep(&ts, NULL);
+	tlob_sleep_work_done();
+}
+
+/* stop probe; noinline keeps the entry point visible to uprobes */
+noinline void tlob_preempt_work_done(void)
+{
+	/* empty: uprobe fires on entry */
+}
+
+/*
+ * start probe; busy-spin so an RT competitor on the same CPU drives
+ * waiting_ns (prev_state==0 -> preempt event, task stays runnable off-CPU).
+ */
+noinline void tlob_preempt_work(unsigned long duration_ms)
+{
+	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);
+
+	tlob_preempt_work_done();
+}
+
+int main(int argc, char *argv[])
+{
+	unsigned long duration_ms = 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];
+
+	clock_gettime(CLOCK_MONOTONIC, &deadline);
+	timespec_add_ms(&deadline, duration_ms ? duration_ms : 86400000UL);
+
+	do {
+		if (strcmp(mode, "sleep") == 0)
+			tlob_sleep_work(200);
+		else if (strcmp(mode, "preempt") == 0)
+			tlob_preempt_work(200);
+		else
+			tlob_busy_work(200 * 1000000UL);
+		clock_gettime(CLOCK_MONOTONIC, &now);
+	} while (timespec_before(&now, &deadline));
+
+	return 0;
+}