* Re: [PATCH v4 RESEND 2/7] riscv: stacktrace: disable KASAN and KCOV instrumentation for stacktrace.o
From: Shuai Xue @ 2026-07-08 8:07 UTC (permalink / raw)
To: Wang Han, Paul Walmsley, Palmer Dabbelt, Albert Ou
Cc: Alexandre Ghiti, linux-riscv, Oleg Nesterov, Steven Rostedt,
Masami Hiramatsu, Mark Rutland, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Alexander Shishkin,
Jiri Olsa, Ian Rogers, Adrian Hunter, James Clark, Josh Poimboeuf,
Jiri Kosina, Miroslav Benes, Petr Mladek, Joe Lawrence,
Shuah Khan, oliver.yang, zhuo.song, jkchen, Marcos Paulo de Souza,
linux-kernel, linux-trace-kernel, linux-perf-users, live-patching,
linux-kselftest
In-Reply-To: <20260629072713.3273743-3-wanghan@linux.alibaba.com>
On 6/29/26 3:27 PM, Wang Han wrote:
> KASAN records stack traces for every alloc/free, which means it walks
> the unwinder very frequently. Instrumenting the stack trace collection
> code itself adds substantial overhead and makes the traces themselves
> noisier.
>
> KCOV instruments every basic-block edge. The unwinder is a hot path,
> especially with KASAN enabled, so KCOV instrumentation has the same kind
> of cost and noise problem here.
>
> Mark stacktrace.o as not KASAN- or KCOV-instrumented, matching the x86
> treatment of its stack unwinding code. RISC-V keeps the relevant unwinder
> code in stacktrace.o, so a single translation-unit annotation covers the
> equivalent scope. This is a prerequisite preference for the upcoming
> reliable unwinder, but the change is valid on its own.
>
> Signed-off-by: Wang Han <wanghan@linux.alibaba.com>
> ---
> arch/riscv/kernel/Makefile | 6 ++++++
> 1 file changed, 6 insertions(+)
>
> diff --git a/arch/riscv/kernel/Makefile b/arch/riscv/kernel/Makefile
> index cabb99cadfb6..c565a72a36f3 100644
> --- a/arch/riscv/kernel/Makefile
> +++ b/arch/riscv/kernel/Makefile
> @@ -44,6 +44,12 @@ CFLAGS_REMOVE_return_address.o = $(CC_FLAGS_FTRACE)
> CFLAGS_REMOVE_sbi_ecall.o = $(CC_FLAGS_FTRACE)
> endif
>
> +# When KASAN is enabled, a stack trace is recorded for every alloc/free, which
> +# can significantly impact performance. Avoid instrumenting the stack trace
> +# collection code to minimize this impact.
> +KASAN_SANITIZE_stacktrace.o := n
> +KCOV_INSTRUMENT_stacktrace.o := n
> +
> always-$(KBUILD_BUILTIN) += vmlinux.lds
>
> obj-y += head.o
Reviewed-by: Shuai Xue <xueshuai@linux.alibaba.com>
Thanks.
Shuai
^ permalink raw reply
* Re: [PATCH v4 RESEND 5/7] riscv: stacktrace: switch to frame-pointer based unwinder
From: Shuai Xue @ 2026-07-08 8:21 UTC (permalink / raw)
To: Wang Han, Paul Walmsley, Palmer Dabbelt, Albert Ou
Cc: Alexandre Ghiti, linux-riscv, Oleg Nesterov, Steven Rostedt,
Masami Hiramatsu, Mark Rutland, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Alexander Shishkin,
Jiri Olsa, Ian Rogers, Adrian Hunter, James Clark, Josh Poimboeuf,
Jiri Kosina, Miroslav Benes, Petr Mladek, Joe Lawrence,
Shuah Khan, oliver.yang, zhuo.song, jkchen, Marcos Paulo de Souza,
linux-kernel, linux-trace-kernel, linux-perf-users, live-patching,
linux-kselftest
In-Reply-To: <20260629072713.3273743-6-wanghan@linux.alibaba.com>
On 6/29/26 3:27 PM, Wang Han wrote:
> Replace the open-coded frame-pointer walker in arch_stack_walk() with a
> robust kunwind state machine, modelled on arch/arm64/kernel/stacktrace.c
> and retargeted to the RISC-V {fp, ra} frame record convention. The new
> walker tracks stack bounds, consumes frame records monotonically,
> understands the metadata pt_regs records added in the previous frame
> record metadata patch, and recovers return addresses replaced by
> function graph tracing and kretprobes.
>
> This commit introduces arch_stack_walk_reliable() but does not yet
> select HAVE_RELIABLE_STACKTRACE; that is done in a follow-up Kconfig
> patch so this commit can be reviewed and bisected as a pure unwinder
> replacement. Until that Kconfig change lands, livepatch is not yet
> enabled and arch_stack_walk_reliable() has no in-tree caller.
>
> Three related callers are updated to keep the same frame-record
> assumptions everywhere:
>
> * Function graph tracing: the old RISC-V unwinder matched function
> graph return-stack entries by the saved return-address slot. That
> was consistent with the static mcount path, but not with the dynamic
> ftrace path where the parent slot is ftrace_regs::ra. Use the
> architectural frame pointer as the function graph return-address
> cookie, matching the kunwind walker.
>
> * Perf callchains: route kernel callchain collection through
> arch_stack_walk() so perf sees the same frame-pointer unwind
> behaviour as dump_stack() and the upcoming livepatch path.
>
> * dump_backtrace() / __get_wchan() / show_stack(): these now go
> through arch_stack_walk(); the explicit "Call Trace:" header is
> moved into dump_backtrace() to preserve the original output.
>
> The non-frame-pointer fallback walker is kept untouched for
> !CONFIG_FRAME_POINTER builds.
>
> Signed-off-by: Wang Han <wanghan@linux.alibaba.com>
> ---
> arch/riscv/kernel/ftrace.c | 6 +-
> arch/riscv/kernel/perf_callchain.c | 2 +-
> arch/riscv/kernel/stacktrace.c | 559 ++++++++++++++++++++++++-----
> 3 files changed, 471 insertions(+), 96 deletions(-)
>
> diff --git a/arch/riscv/kernel/ftrace.c b/arch/riscv/kernel/ftrace.c
> index b430edfb83f4..5d55199a9230 100644
> --- a/arch/riscv/kernel/ftrace.c
> +++ b/arch/riscv/kernel/ftrace.c
> @@ -242,7 +242,8 @@ void prepare_ftrace_return(unsigned long *parent, unsigned long self_addr,
> */
> old = *parent;
>
> - if (!function_graph_enter(old, self_addr, frame_pointer, parent))
> + if (!function_graph_enter(old, self_addr, frame_pointer,
> + (void *)frame_pointer))
> *parent = return_hooker;
> }
>
> @@ -264,7 +265,8 @@ void ftrace_graph_func(unsigned long ip, unsigned long parent_ip,
> */
> old = *parent;
>
> - if (!function_graph_enter_regs(old, ip, frame_pointer, parent, fregs))
> + if (!function_graph_enter_regs(old, ip, frame_pointer,
> + (void *)frame_pointer, fregs))
> *parent = return_hooker;
> }
> #endif /* CONFIG_DYNAMIC_FTRACE */
> diff --git a/arch/riscv/kernel/perf_callchain.c b/arch/riscv/kernel/perf_callchain.c
> index b465bc9eb870..436af96ea59c 100644
> --- a/arch/riscv/kernel/perf_callchain.c
> +++ b/arch/riscv/kernel/perf_callchain.c
> @@ -44,5 +44,5 @@ void perf_callchain_kernel(struct perf_callchain_entry_ctx *entry,
> return;
> }
>
> - walk_stackframe(NULL, regs, fill_callchain, entry);
> + arch_stack_walk(fill_callchain, entry, NULL, regs);
> }
> diff --git a/arch/riscv/kernel/stacktrace.c b/arch/riscv/kernel/stacktrace.c
> index c7555447149b..c43bf9a84207 100644
> --- a/arch/riscv/kernel/stacktrace.c
> +++ b/arch/riscv/kernel/stacktrace.c
> @@ -11,98 +11,16 @@
> #include <linux/sched/task_stack.h>
> #include <linux/stacktrace.h>
> #include <linux/ftrace.h>
> +#include <linux/kprobes.h>
> +#include <linux/llist.h>
>
> #include <asm/stacktrace.h>
>
> -#ifdef CONFIG_FRAME_POINTER
> -
> /*
> - * This disables KASAN checking when reading a value from another task's stack,
> - * since the other task could be running on another CPU and could have poisoned
> - * the stack in the meantime.
> + * Non-frame-pointer fallback unwinder.
> + * Only compiled when CONFIG_FRAME_POINTER is not enabled.
> */
> -#define READ_ONCE_TASK_STACK(task, x) \
> -({ \
> - unsigned long val; \
> - unsigned long addr = x; \
> - if ((task) == current) \
> - val = READ_ONCE(addr); \
> - else \
> - val = READ_ONCE_NOCHECK(addr); \
> - val; \
> -})
> -
> -extern asmlinkage void handle_exception(void);
> -extern unsigned long ret_from_exception_end;
> -
> -static inline int fp_is_valid(unsigned long fp, unsigned long sp)
> -{
> - unsigned long low, high;
> -
> - low = sp + sizeof(struct stackframe);
> - high = ALIGN(sp, THREAD_SIZE);
> -
> - return !(fp < low || fp > high || fp & 0x07);
> -}
> -
> -void notrace walk_stackframe(struct task_struct *task, struct pt_regs *regs,
> - bool (*fn)(void *, unsigned long), void *arg)
> -{
> - unsigned long fp, sp, pc;
> - int graph_idx = 0;
> - int level = 0;
> -
> - if (regs) {
> - fp = frame_pointer(regs);
> - sp = user_stack_pointer(regs);
> - pc = instruction_pointer(regs);
> - } else if (task == NULL || task == current) {
> - fp = (unsigned long)__builtin_frame_address(0);
> - sp = current_stack_pointer;
> - pc = (unsigned long)walk_stackframe;
> - level = -1;
> - } else {
> - /* task blocked in __switch_to */
> - fp = task->thread.s[0];
> - sp = task->thread.sp;
> - pc = task->thread.ra;
> - }
> -
> - for (;;) {
> - struct stackframe *frame;
> -
> - if (unlikely(!__kernel_text_address(pc) || (level++ >= 0 && !fn(arg, pc))))
> - break;
> -
> - if (unlikely(!fp_is_valid(fp, sp)))
> - break;
> -
> - /* Unwind stack frame */
> - frame = (struct stackframe *)fp - 1;
> - sp = fp;
> - if (regs && (regs->epc == pc) && fp_is_valid(frame->ra, sp)) {
> - /* We hit function where ra is not saved on the stack */
> - fp = frame->ra;
> - pc = regs->ra;
> - } else {
> - fp = READ_ONCE_TASK_STACK(task, frame->fp);
> - pc = READ_ONCE_TASK_STACK(task, frame->ra);
> - pc = ftrace_graph_ret_addr(task, &graph_idx, pc,
> - &frame->ra);
> - if (pc >= (unsigned long)handle_exception &&
> - pc < (unsigned long)&ret_from_exception_end) {
> - if (unlikely(!fn(arg, pc)))
> - break;
> -
> - pc = ((struct pt_regs *)sp)->epc;
> - fp = ((struct pt_regs *)sp)->s0;
> - }
> - }
> -
> - }
> -}
> -
> -#else /* !CONFIG_FRAME_POINTER */
> +#ifndef CONFIG_FRAME_POINTER
>
> void notrace walk_stackframe(struct task_struct *task,
> struct pt_regs *regs, bool (*fn)(void *, unsigned long), void *arg)
> @@ -133,7 +51,12 @@ void notrace walk_stackframe(struct task_struct *task,
> }
> }
>
> -#endif /* CONFIG_FRAME_POINTER */
> +#endif /* !CONFIG_FRAME_POINTER */
> +
> +/*
> + * Common trace helpers.
> + * These are used by both the FP (kunwind) and non-FP (walk_stackframe) paths.
> + */
>
> static bool print_trace_address(void *arg, unsigned long pc)
> {
> @@ -146,12 +69,12 @@ static bool print_trace_address(void *arg, unsigned long pc)
> noinline void dump_backtrace(struct pt_regs *regs, struct task_struct *task,
> const char *loglvl)
> {
> - walk_stackframe(task, regs, print_trace_address, (void *)loglvl);
> + printk("%sCall Trace:\n", loglvl);
> + arch_stack_walk(print_trace_address, (void *)loglvl, task, regs);
> }
>
> void show_stack(struct task_struct *task, unsigned long *sp, const char *loglvl)
> {
> - pr_cont("%sCall Trace:\n", loglvl);
> dump_backtrace(NULL, task, loglvl);
> }
>
> @@ -171,17 +94,467 @@ unsigned long __get_wchan(struct task_struct *task)
>
> if (!try_get_task_stack(task))
> return 0;
> - walk_stackframe(task, NULL, save_wchan, &pc);
> + arch_stack_walk(save_wchan, &pc, task, NULL);
> put_task_stack(task);
> return pc;
> }
>
> -noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry, void *cookie,
> - struct task_struct *task, struct pt_regs *regs)
> +/*
> + * Frame-pointer-based kernel unwind infrastructure.
> + * Only compiled when CONFIG_FRAME_POINTER is enabled.
> + *
> + * See: arch/arm64/kernel/stacktrace.c for the reference implementation.
> + */
> +#ifdef CONFIG_FRAME_POINTER
> +
> +/*
> + * Per-cpu stacks are only accessible when unwinding the current task in a
> + * non-preemptible context.
> + */
> +#define STACKINFO_CPU(task, name) \
> + ({ \
> + (((task) == current) && !preemptible()) \
> + ? stackinfo_get_##name() \
> + : stackinfo_get_unknown(); \
> + })
> +
> +enum kunwind_source {
> + KUNWIND_SOURCE_UNKNOWN,
> + KUNWIND_SOURCE_FRAME,
> + KUNWIND_SOURCE_CALLER,
> + KUNWIND_SOURCE_TASK,
> + KUNWIND_SOURCE_REGS_PC,
> +};
> +
> +union unwind_flags {
> + unsigned long all;
> + struct {
> + unsigned long fgraph : 1,
> + kretprobe : 1;
> + };
> +};
> +
> +/*
> + * Kernel unwind state
> + *
> + * @common: Common unwind state.
> + * @task: The task being unwound.
> + * @graph_idx: Used by ftrace_graph_ret_addr() for optimized stack unwinding.
> + * @kr_cur: When KRETPROBES is selected, holds the kretprobe instance
> + * associated with the most recently encountered replacement ra
> + * value.
> + */
> +struct kunwind_state {
> + struct unwind_state common;
> + struct task_struct *task;
> + int graph_idx;
> +#ifdef CONFIG_KRETPROBES
> + struct llist_node *kr_cur;
> +#endif
> + enum kunwind_source source;
> + union unwind_flags flags;
> + struct pt_regs *regs;
> +};
> +
> +static __always_inline void
> +kunwind_init(struct kunwind_state *state,
> + struct task_struct *task)
> +{
> + unwind_init_common(&state->common);
> + state->task = task;
> + state->source = KUNWIND_SOURCE_UNKNOWN;
> + state->flags.all = 0;
> + state->regs = NULL;
> +}
> +
> +/*
> + * Start an unwind from a pt_regs.
> + *
> + * The unwind will begin at the PC within the regs.
> + *
> + * The regs must be on a stack currently owned by the calling task.
> + */
> +static __always_inline void
> +kunwind_init_from_regs(struct kunwind_state *state,
> + struct pt_regs *regs)
> +{
> + kunwind_init(state, current);
> +
> + state->regs = regs;
> + state->common.fp = frame_pointer(regs);
> + state->common.pc = instruction_pointer(regs);
> + state->source = KUNWIND_SOURCE_REGS_PC;
> +}
> +
> +/*
> + * Start an unwind from a caller.
> + *
> + * The unwind will begin at the caller of whichever function this is inlined
> + * into.
> + *
> + * The function which invokes this must be noinline.
> + */
> +static __always_inline void
> +kunwind_init_from_caller(struct kunwind_state *state)
> +{
> + unsigned long fp = (unsigned long)__builtin_frame_address(0);
> + struct frame_record *record = (struct frame_record *)fp - 1;
> +
> + kunwind_init(state, current);
> +
> + state->common.fp = READ_ONCE(record->fp);
> + state->common.pc = READ_ONCE(record->ra);
> + state->source = KUNWIND_SOURCE_CALLER;
> +}
> +
> +/*
> + * Start an unwind from a blocked task.
> + *
> + * The unwind will begin at the blocked task's saved PC (i.e. the caller of
> + * __switch_to).
> + *
> + * The caller should ensure the task is blocked in __switch_to for the
> + * duration of the unwind, or the unwind will be bogus. It is never valid to
> + * call this for the current task.
> + */
> +static __always_inline void
> +kunwind_init_from_task(struct kunwind_state *state,
> + struct task_struct *task)
> +{
> + kunwind_init(state, task);
> +
> + state->common.fp = task->thread.s[0];
> + state->common.pc = task->thread.ra;
> + state->source = KUNWIND_SOURCE_TASK;
> +}
> +
> +static __always_inline int
> +kunwind_recover_return_address(struct kunwind_state *state)
> +{
> +#ifdef CONFIG_FUNCTION_GRAPH_TRACER
> + if (state->task->ret_stack &&
> + state->common.pc == (unsigned long)return_to_handler) {
> + unsigned long orig_pc;
> +
> + orig_pc = ftrace_graph_ret_addr(state->task, &state->graph_idx,
> + state->common.pc,
> + (void *)state->common.fp);
> + if (state->common.pc == orig_pc) {
> + WARN_ON_ONCE(state->task == current);
> + return -EINVAL;
> + }
> + state->common.pc = orig_pc;
> + state->flags.fgraph = 1;
> + }
> +#endif /* CONFIG_FUNCTION_GRAPH_TRACER */
> +
> +#ifdef CONFIG_KRETPROBES
> + if (is_kretprobe_trampoline(state->common.pc)) {
> + unsigned long orig_pc;
> +
> + orig_pc = kretprobe_find_ret_addr(state->task,
> + (void *)state->common.fp,
> + &state->kr_cur);
> + if (!orig_pc)
> + return -EINVAL;
> + state->common.pc = orig_pc;
> + state->flags.kretprobe = 1;
> + }
> +#endif /* CONFIG_KRETPROBES */
> +
> + return 0;
> +}
> +
> +/*
> + * When we reach an exception boundary marked by a metadata frame record,
> + * extract pt_regs from the stack and continue unwinding from the saved
> + * context (epc and s0/fp).
> + *
> + * On RISC-V, fp points above the metadata record, so the record's
> + * frame_record portion is at fp - sizeof(struct frame_record).
> + */
> +static __always_inline int
> +kunwind_next_regs_pc(struct kunwind_state *state)
> +{
> + struct stack_info *info;
> + unsigned long fp = state->common.fp;
> + struct pt_regs *regs;
> +
> + regs = container_of((unsigned long *)(fp - sizeof(struct frame_record)),
> + struct pt_regs, stackframe.record.fp);
> +
> + info = unwind_find_stack(&state->common, (unsigned long)regs,
> + sizeof(*regs));
> + if (!info)
> + return -EINVAL;
> +
> + unwind_consume_stack(&state->common, info, (unsigned long)regs,
> + sizeof(*regs));
> +
> + state->regs = regs;
> + state->common.pc = regs->epc;
> + state->common.fp = frame_pointer(regs);
> + state->source = KUNWIND_SOURCE_REGS_PC;
> + return 0;
> +}
> +
> +/*
> + * Handle a metadata frame record embedded in pt_regs.
> + *
> + * On RISC-V, fp points above the record (fp = metadata + 16), so the
> + * frame_record_meta starts at fp - sizeof(struct frame_record).
> + *
> + * FRAME_META_TYPE_FINAL: This is the outermost exception entry
> + * (user -> kernel). Unwinding terminates successfully.
> + * FRAME_META_TYPE_PT_REGS: This is a nested exception entry
> + * (kernel -> kernel). Continue unwinding from the saved context.
> + */
> +static __always_inline int
> +kunwind_next_frame_record_meta(struct kunwind_state *state)
> +{
> + struct task_struct *tsk = state->task;
> + unsigned long fp = state->common.fp;
> + unsigned long meta_base = fp - sizeof(struct frame_record);
> + struct frame_record_meta *meta;
> + struct stack_info *info;
> +
> + info = unwind_find_stack(&state->common, meta_base, sizeof(*meta));
> + if (!info)
> + return -EINVAL;
> +
> + meta = (struct frame_record_meta *)meta_base;
> + switch (READ_ONCE(meta->type)) {
> + case FRAME_META_TYPE_FINAL:
> + if (meta == &task_pt_regs(tsk)->stackframe)
> + return -ENOENT;
> + WARN_ON_ONCE(tsk == current);
> + return -EINVAL;
> + case FRAME_META_TYPE_PT_REGS:
> + return kunwind_next_regs_pc(state);
> + default:
> + WARN_ON_ONCE(tsk == current);
> + return -EINVAL;
> + }
> +}
> +
> +/*
> + * Unwind from one frame record to the next.
> + *
> + * On RISC-V, the frame record sits at fp - sizeof(struct frame_record),
> + * immediately below the address pointed to by fp/s0. This applies to both
> + * normal frame records and metadata frame records (embedded in pt_regs).
> + *
> + * A metadata record is identified by both fp and ra being zero in the
> + * frame_record portion, with a type value following at fp + 16.
> + */
> +static __always_inline int
> +kunwind_next_frame_record(struct kunwind_state *state)
> +{
> + unsigned long fp = state->common.fp;
> + struct frame_record *record;
> + struct stack_info *info;
> + unsigned long new_fp, new_pc;
> + unsigned long record_base;
> +
> + if (fp & 0x7)
> + return -EINVAL;
> +
> + record_base = fp - sizeof(*record);
> +
> + info = unwind_find_stack(&state->common, record_base, sizeof(*record));
> + if (!info)
> + return -EINVAL;
> +
> + record = (struct frame_record *)record_base;
> + new_fp = READ_ONCE(record->fp);
> + new_pc = READ_ONCE(record->ra);
> +
> + if (!new_fp && !new_pc)
> + return kunwind_next_frame_record_meta(state);
> +
> + unwind_consume_stack(&state->common, info, record_base,
> + sizeof(*record));
> +
> + state->common.fp = new_fp;
> + state->common.pc = new_pc;
> + state->source = KUNWIND_SOURCE_FRAME;
> +
> + return 0;
> +}
> +
> +/*
> + * Unwind from one frame record (A) to the next frame record (B).
> + *
> + * We terminate early if the location of B indicates a malformed chain of frame
> + * records (e.g. a cycle), determined based on the location and fp value of A
> + * and the location (but not the fp value) of B.
> + */
> +static __always_inline int
> +kunwind_next(struct kunwind_state *state)
> +{
> + int err;
> +
> + state->flags.all = 0;
> +
> + switch (state->source) {
> + case KUNWIND_SOURCE_FRAME:
> + case KUNWIND_SOURCE_CALLER:
> + case KUNWIND_SOURCE_TASK:
> + case KUNWIND_SOURCE_REGS_PC:
> + err = kunwind_next_frame_record(state);
> + break;
> + default:
> + err = -EINVAL;
> + }
> +
> + if (err)
> + return err;
> +
> + return kunwind_recover_return_address(state);
> +}
> +
> +typedef bool (*kunwind_consume_fn)(const struct kunwind_state *state, void *cookie);
> +
> +static __always_inline int
> +do_kunwind(struct kunwind_state *state, kunwind_consume_fn consume_state,
> + void *cookie)
> +{
> + int ret;
> +
> + ret = kunwind_recover_return_address(state);
> + if (ret)
> + return ret;
> +
> + while (1) {
> + if (!consume_state(state, cookie))
> + return -EINVAL;
> + ret = kunwind_next(state);
> + if (ret == -ENOENT)
> + return 0;
> + if (ret < 0)
> + return ret;
> + }
> +}
> +
> +static __always_inline int
> +kunwind_stack_walk(kunwind_consume_fn consume_state,
> + void *cookie, struct task_struct *task,
> + struct pt_regs *regs)
> +{
> + struct task_struct *tsk = task ?: current;
> + struct stack_info stacks[] = {
> + stackinfo_get_task(tsk),
> + STACKINFO_CPU(tsk, irq),
> +#ifdef CONFIG_VMAP_STACK
> + STACKINFO_CPU(tsk, overflow),
> +#endif
> + };
> + struct kunwind_state state = {
> + .common = {
> + .stacks = stacks,
> + .nr_stacks = ARRAY_SIZE(stacks),
> + },
> + };
> +
> + if (regs) {
> + if (tsk != current)
> + return -EINVAL;
> + kunwind_init_from_regs(&state, regs);
> + } else if (tsk == current) {
> + kunwind_init_from_caller(&state);
> + } else {
> + kunwind_init_from_task(&state, tsk);
> + }
> +
> + return do_kunwind(&state, consume_state, cookie);
> +}
> +
> +struct kunwind_consume_entry_data {
> + stack_trace_consume_fn consume_entry;
> + void *cookie;
> +};
> +
> +static __always_inline bool
> +arch_kunwind_consume_entry(const struct kunwind_state *state, void *cookie)
> +{
> + struct kunwind_consume_entry_data *data = cookie;
> +
> + return data->consume_entry(data->cookie, state->common.pc);
> +}
> +
> +static __always_inline bool
> +arch_reliable_kunwind_consume_entry(const struct kunwind_state *state, void *cookie)
> +{
> + /*
> + * At an exception boundary we can reliably consume the saved PC. We do
> + * not know whether ra was live when the exception was taken, and
> + * so we cannot perform the next unwind step reliably.
> + *
> + * All that matters is whether the *entire* unwind is reliable, so give
> + * up as soon as we hit an exception boundary.
> + */
> + if (state->source == KUNWIND_SOURCE_REGS_PC)
> + return false;
> +
> + return arch_kunwind_consume_entry(state, cookie);
> +}
> +
> +#endif /* CONFIG_FRAME_POINTER */
> +
> +/*
> + * arch_stack_walk - dual implementation.
> + *
> + * When CONFIG_FRAME_POINTER is enabled, uses the kunwind infrastructure for
> + * robust frame-pointer-based unwinding, consistent with arch_stack_walk_reliable.
> + *
> + * When CONFIG_FRAME_POINTER is disabled, falls back to the simple stack scan
> + * in walk_stackframe().
> + */
> +#ifdef CONFIG_FRAME_POINTER
> +
> +noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry,
> + void *cookie, struct task_struct *task,
> + struct pt_regs *regs)
> +{
> + struct kunwind_consume_entry_data data = {
> + .consume_entry = consume_entry,
> + .cookie = cookie,
> + };
> +
> + kunwind_stack_walk(arch_kunwind_consume_entry, &data, task, regs);
> +}
> +
> +#else
> +
> +noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry,
> + void *cookie, struct task_struct *task,
> + struct pt_regs *regs)
> {
> walk_stackframe(task, regs, consume_entry, cookie);
> }
>
> +#endif /* CONFIG_FRAME_POINTER */
> +
> +/*
> + * Reliable stack walk for livepatch (CONFIG_FRAME_POINTER only).
> + */
> +#ifdef CONFIG_FRAME_POINTER
> +
> +noinline noinstr int arch_stack_walk_reliable(stack_trace_consume_fn consume_entry,
> + void *cookie,
> + struct task_struct *task)
> +{
> + struct kunwind_consume_entry_data data = {
> + .consume_entry = consume_entry,
> + .cookie = cookie,
> + };
> +
> + return kunwind_stack_walk(arch_reliable_kunwind_consume_entry, &data,
> + task, NULL);
> +}
> +
> +#endif /* CONFIG_FRAME_POINTER */
> +
> /*
> * Get the return address for a single stackframe and return a pointer to the
> * next frame tail.
LGTM.
Reviewed-by: Shuai Xue <xueshuai@linux.alibaba.com>
Thanks.
Shuai
^ permalink raw reply
* [PATCH v2] rtla: Simplify osnoise tracer option setting code
From: Tomas Glozar @ 2026-07-08 8:30 UTC (permalink / raw)
To: Steven Rostedt, Tomas Glozar
Cc: John Kacur, Luis Goncalves, Crystal Wood, Costa Shulyupin,
Wander Lairson Costa, LKML, linux-trace-kernel
Each osnoise tracer option (in /sys/kernel/tracing/osnoise) used by RTLA
requires four functions to be defined:
- static osnoise_get_<opt>() - to get the current value of the option
and save it into struct osnoise_context's orig_<opt> field,
- osnoise_set_<opt>() - to set the value of the option requested by the
user after reading and saving the original with osnoise_get_<opt>(),
and save it into <opt> field of struct osnoise_context,
- osnoise_restore_<opt>() - restore the value recorded in orig_<opt>,
- static osnoise_put_<opt>() - restore the value recorded in orig_<opt>
and update <opt> to reflect that.
The logic is duplicated for all the options, except for cpus (which is
the only string option) and period/runtime (which are handled together
and feature extra checks).
Deduplicate by moving the common code in get/set/restore/put into common
helper functions local to osnoise.c. The original set/restore/put
functions are reduced to wrappers generated using a set of X macros:
- OSNOISE_LL_OPTIONS, which invokes the OSNOISE_LL_OPTION macro for all
"long long" options,
- OSNOISE_FLAG_OPTIONS, which invokes the OSNOISE_FLAG_OPTION macro for
all flag options (boolean values in osnoise/options file).
Those are also used to auto-generate the corresponding fields in struct
osnoise_context, plus initialization and restoration code for them.
There are a few minor changes in the refactored code:
- osnoise_set_<opt>() now distinguishes between -2 (option cannot be
set) and -1 (option not present in kernel) for all options.
- OSNOISE_OPTION_INIT_VAL and OSNOISE_TIME_INIT_VAL are removed and
replaced with a plain -1; all unsigned long long fields are changed to
signed long long, consistent with pre-existing "%lld" format string in
osnoise_write_ll_config().
- osnoise_restore_<opt>() now correctly resets the <opt> field (not
orig_<opt>) for all options.
The change is intended to simplify adding and modifying tracer options.
It also makes the code shorter by a bit over 500 lines.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Tomas Glozar <tglozar@redhat.com>
---
v1: https://lore.kernel.org/linux-trace-kernel/20260612115121.54862-1-tglozar@redhat.com/T
v2 changes (mostly suggestions from Crystal Wood):
- Reduce code inside macros by moving as much code as possible into helper
functions (new or existing) that take the struct field pointers as arguments.
- Remove "get" functions from the macro, as they are no longer used anywhere.
- Replace OSNOISE_OPTION_INIT_VAL and OSNOISE_TIME_INIT_VAL with a unified -1
value for "unset" (values > INT64_MAX never worked anyway, as
osnoise_write_ll_config() always used "%lld" to format the value).
- Add detailed comments to the helper functions, as one of the issues of the
code was the logic being hard to understand.
- Move some details about the macros from the commit message into a comment.
tools/tracing/rtla/src/common.h | 96 +--
tools/tracing/rtla/src/osnoise.c | 1093 +++++++++---------------------
tools/tracing/rtla/src/osnoise.h | 33 +-
3 files changed, 381 insertions(+), 841 deletions(-)
diff --git a/tools/tracing/rtla/src/common.h b/tools/tracing/rtla/src/common.h
index 04b287a03f6d4..0de02347a3138 100644
--- a/tools/tracing/rtla/src/common.h
+++ b/tools/tracing/rtla/src/common.h
@@ -6,9 +6,42 @@
#include "trace.h"
#include "utils.h"
+/*
+ * OSNOISE_LL_OPTIONS - list of long long options backed by tracefs files.
+ * OSNOISE_LL_OPTION(field_name, tracefs_path)
+ *
+ * OSNOISE_FLAG_OPTIONS - list of boolean options backed by osnoise/options.
+ * OSNOISE_FLAG_OPTION(field_name, option_string)
+ *
+ * These X-macro lists are invoked in four places:
+ * - struct osnoise_context field declarations (common.h),
+ * - function declarations for osnoise_set_<opt>/osnoise_restore_<opt> (common.h),
+ * - function definitions for set/restore/put (osnoise.c),
+ * - context initialization and teardown in osnoise_context_alloc() and
+ * osnoise_put_context() (osnoise.c).
+ */
+#define OSNOISE_LL_OPTIONS \
+ OSNOISE_LL_OPTION(stop_us, "osnoise/stop_tracing_us") \
+ OSNOISE_LL_OPTION(stop_total_us, "osnoise/stop_tracing_total_us") \
+ OSNOISE_LL_OPTION(print_stack, "osnoise/print_stack") \
+ OSNOISE_LL_OPTION(tracing_thresh, "tracing_thresh") \
+ OSNOISE_LL_OPTION(timerlat_period_us, "osnoise/timerlat_period_us") \
+ OSNOISE_LL_OPTION(timerlat_align_us, "osnoise/timerlat_align_us")
+
+#define OSNOISE_FLAG_OPTIONS \
+ OSNOISE_FLAG_OPTION(irq_disable, "OSNOISE_IRQ_DISABLE") \
+ OSNOISE_FLAG_OPTION(workload, "OSNOISE_WORKLOAD") \
+ OSNOISE_FLAG_OPTION(timerlat_align, "TIMERLAT_ALIGN")
+
/*
* osnoise_context - read, store, write, restore osnoise configs.
*/
+#define OSNOISE_LL_OPTION(name, path) \
+ long long orig_##name; \
+ long long name;
+#define OSNOISE_FLAG_OPTION(name, option_str) \
+ int orig_opt_##name; \
+ int opt_##name;
struct osnoise_context {
int flags;
int ref;
@@ -16,50 +49,17 @@ struct osnoise_context {
char *curr_cpus;
char *orig_cpus;
- /* 0 as init value */
- unsigned long long orig_runtime_us;
- unsigned long long runtime_us;
-
- /* 0 as init value */
- unsigned long long orig_period_us;
- unsigned long long period_us;
-
- /* 0 as init value */
- long long orig_timerlat_period_us;
- long long timerlat_period_us;
-
- /* 0 as init value */
- long long orig_tracing_thresh;
- long long tracing_thresh;
-
- /* -1 as init value because 0 is disabled */
- long long orig_stop_us;
- long long stop_us;
-
- /* -1 as init value because 0 is disabled */
- long long orig_stop_total_us;
- long long stop_total_us;
-
- /* -1 as init value because 0 is disabled */
- long long orig_print_stack;
- long long print_stack;
-
- /* -1 as init value because 0 is off */
- int orig_opt_irq_disable;
- int opt_irq_disable;
-
- /* -1 as init value because 0 is off */
- int orig_opt_workload;
- int opt_workload;
+ long long orig_runtime_us;
+ long long runtime_us;
- /* -1 as init value because 0 is off */
- int orig_opt_timerlat_align;
- int opt_timerlat_align;
+ long long orig_period_us;
+ long long period_us;
- /* 0 as init value */
- unsigned long long orig_timerlat_align_us;
- unsigned long long timerlat_align_us;
+ OSNOISE_LL_OPTIONS
+ OSNOISE_FLAG_OPTIONS
};
+#undef OSNOISE_LL_OPTION
+#undef OSNOISE_FLAG_OPTION
extern volatile int stop_tracing;
@@ -173,15 +173,21 @@ common_threshold_handler(const struct osnoise_tool *tool);
int osnoise_set_cpus(struct osnoise_context *context, char *cpus);
void osnoise_restore_cpus(struct osnoise_context *context);
-int osnoise_set_workload(struct osnoise_context *context, bool onoff);
+#define OSNOISE_LL_OPTION(name, path) \
+ int osnoise_set_##name(struct osnoise_context *context, long long name); \
+ void osnoise_restore_##name(struct osnoise_context *context);
+#define OSNOISE_FLAG_OPTION(name, option_str) \
+ int osnoise_set_##name(struct osnoise_context *context, bool onoff); \
+ void osnoise_restore_##name(struct osnoise_context *context);
+OSNOISE_LL_OPTIONS
+OSNOISE_FLAG_OPTIONS
+#undef OSNOISE_LL_OPTION
+#undef OSNOISE_FLAG_OPTION
void osnoise_destroy_tool(struct osnoise_tool *top);
struct osnoise_tool *osnoise_init_tool(char *tool_name);
struct osnoise_tool *osnoise_init_trace_tool(const char *tracer);
bool osnoise_trace_is_off(struct osnoise_tool *tool, struct osnoise_tool *record);
-int osnoise_set_stop_us(struct osnoise_context *context, long long stop_us);
-int osnoise_set_stop_total_us(struct osnoise_context *context,
- long long stop_total_us);
int common_apply_config(struct osnoise_tool *tool, struct common_params *params);
int top_main_loop(struct osnoise_tool *tool);
diff --git a/tools/tracing/rtla/src/osnoise.c b/tools/tracing/rtla/src/osnoise.c
index 4ff5dad013b10..7e935b575feed 100644
--- a/tools/tracing/rtla/src/osnoise.c
+++ b/tools/tracing/rtla/src/osnoise.c
@@ -124,16 +124,40 @@ void osnoise_put_cpus(struct osnoise_context *context)
context->orig_cpus = NULL;
}
-/*
- * osnoise_read_ll_config - read a long long value from a config
+/**
+ * osnoise_read_ll_config - read a long long value from a tracefs config
+ * @rel_path: tracefs-relative path to the config file
+ * @set_value: pointer to the cached value set by RTLA, or -1 if unset
+ * @orig_value: pointer to the cached original value read from tracefs, or -1 if unread
+ *
+ * Returns the current effective value for the config at @rel_path. If RTLA has
+ * already written a value (@set_value != -1), that value is returned. If the
+ * original has already been read (@orig_value != -1), that cached value is
+ * returned. Otherwise, reads the value from tracefs, caches it in @orig_value
+ * (so it can later be restored), and returns it.
*
- * returns -1 on error.
+ * This is the shared read primitive used by both the manually implemented
+ * get functions (osnoise_get_runtime, osnoise_get_period) and the
+ * OSNOISE_LL_OPTION-generated set/restore/put functions.
+ *
+ * Returns the config value on success, -1 on error.
*/
-static long long osnoise_read_ll_config(char *rel_path)
+static long long osnoise_read_ll_config(char *rel_path,
+ long long *set_value,
+ long long *orig_value)
{
long long retval;
char *buffer;
+ if (*set_value != -1)
+ /* option has been set by RTLA already */
+ return *set_value;
+
+ if (*orig_value != -1)
+ /* RTLA has already read the option */
+ return *orig_value;
+
+ /* current value is not known to RTLA yet, read it from tracefs */
buffer = tracefs_instance_file_read(NULL, rel_path, NULL);
if (!buffer)
return -1;
@@ -145,51 +169,110 @@ static long long osnoise_read_ll_config(char *rel_path)
free(buffer);
+ if (retval < 0)
+ goto out_err;
+
+ /* save the value and return it */
+ *orig_value = retval;
return retval;
+
+out_err:
+ return -1;
}
-/*
- * osnoise_write_ll_config - write a long long value to a config in rel_path
+/**
+ * osnoise_write_ll_config - write a long long value to a tracefs config
+ * @rel_path: tracefs-relative path to the config file
+ * @value: the value to write
+ * @set_value: pointer to the cached value set by RTLA, or -1 if unset
+ * @orig_value: pointer to the cached original value read from tracefs, or -1 if unread
*
- * returns -1 on error.
+ * Writes @value to the config at @rel_path. Before writing, calls
+ * osnoise_read_ll_config() to ensure the original value is cached in
+ * @orig_value (enabling later restoration). On successful write, records
+ * the new value in @set_value.
+ *
+ * This is the shared write primitive used by both the manually implemented
+ * write functions (__osnoise_write_runtime, __osnoise_write_period) and the
+ * OSNOISE_LL_OPTION-generated set/restore/put functions.
+ *
+ * Returns 0 on success, -1 on read error (option likely unknown to kernel),
+ * or -2 on write error.
*/
-static long long osnoise_write_ll_config(char *rel_path, long long value)
+static int osnoise_write_ll_config(char *rel_path,
+ long long value,
+ long long *set_value,
+ long long *orig_value)
{
- char buffer[BUFF_U64_STR_SIZE];
+ long long curr = osnoise_read_ll_config(rel_path, set_value, orig_value);
long long retval;
+ char buffer[BUFF_U64_STR_SIZE];
+
+ if (curr == -1)
+ /* read failed, option likely unknown to kernel */
+ return -1;
snprintf(buffer, sizeof(buffer), "%lld\n", value);
debug_msg("setting %s to %lld\n", rel_path, value);
retval = tracefs_instance_file_write(NULL, rel_path, buffer);
- return retval;
+
+ if (retval < 0)
+ /* write failed, hard error */
+ return -2;
+
+ /* record the set value and return success */
+ *set_value = value;
+ return 0;
}
-/*
- * osnoise_get_runtime - return the original "osnoise/runtime_us" value
+/**
+ * osnoise_restore_ll_config - restore a long long config to its original value
+ * @rel_path: tracefs-relative path to the config file
+ * @set_value: pointer to the cached value set by RTLA, or -1 if unset
+ * @orig_value: pointer to the cached original value read from tracefs, or -1 if unread
*
- * It also saves the value to be restored.
+ * Restores the config at @rel_path to the value cached in @orig_value (which
+ * was saved by a prior osnoise_read_ll_config() or osnoise_write_ll_config()
+ * call). If the original was never read, or if the current set value already
+ * matches the original, no write is performed. After restoring, clears
+ * @set_value to -1 to indicate RTLA no longer overrides this config.
+ *
+ * This is the shared restore primitive used by both the manually implemented
+ * restore function (osnoise_restore_runtime_period) and the
+ * OSNOISE_LL_OPTION-generated restore/put functions.
*/
-unsigned long long osnoise_get_runtime(struct osnoise_context *context)
+static void osnoise_restore_ll_config(char *rel_path,
+ long long *set_value,
+ long long *orig_value)
{
- long long runtime_us;
+ int retval;
- if (context->runtime_us != OSNOISE_TIME_INIT_VAL)
- return context->runtime_us;
+ if (*orig_value == -1)
+ return;
- if (context->orig_runtime_us != OSNOISE_TIME_INIT_VAL)
- return context->orig_runtime_us;
+ if (*orig_value == *set_value)
+ goto out_done;
- runtime_us = osnoise_read_ll_config("osnoise/runtime_us");
- if (runtime_us < 0)
- goto out_err;
+ retval = osnoise_write_ll_config(rel_path, *orig_value, set_value, orig_value);
+ if (retval < 0)
+ err_msg("Could not restore original value for %s\n", rel_path);
- context->orig_runtime_us = runtime_us;
- return runtime_us;
+out_done:
+ *set_value = -1;
+}
-out_err:
- return OSNOISE_TIME_INIT_VAL;
+/*
+ * osnoise_get_runtime - return the original "osnoise/runtime_us" value
+ *
+ * It also saves the value to be restored.
+ */
+long long osnoise_get_runtime(struct osnoise_context *context)
+{
+ return osnoise_read_ll_config("osnoise/runtime_us",
+ &context->runtime_us,
+ &context->orig_runtime_us);
}
/*
@@ -197,57 +280,29 @@ unsigned long long osnoise_get_runtime(struct osnoise_context *context)
*
* It also saves the value to be restored.
*/
-unsigned long long osnoise_get_period(struct osnoise_context *context)
+long long osnoise_get_period(struct osnoise_context *context)
{
- long long period_us;
-
- if (context->period_us != OSNOISE_TIME_INIT_VAL)
- return context->period_us;
-
- if (context->orig_period_us != OSNOISE_TIME_INIT_VAL)
- return context->orig_period_us;
-
- period_us = osnoise_read_ll_config("osnoise/period_us");
- if (period_us < 0)
- goto out_err;
-
- context->orig_period_us = period_us;
- return period_us;
-
-out_err:
- return OSNOISE_TIME_INIT_VAL;
+ return osnoise_read_ll_config("osnoise/period_us",
+ &context->period_us,
+ &context->orig_period_us);
}
static int __osnoise_write_runtime(struct osnoise_context *context,
- unsigned long long runtime)
+ long long runtime)
{
- int retval;
-
- if (context->orig_runtime_us == OSNOISE_TIME_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("osnoise/runtime_us", runtime);
- if (retval < 0)
- return -1;
-
- context->runtime_us = runtime;
- return 0;
+ return osnoise_write_ll_config("osnoise/runtime_us",
+ runtime,
+ &context->runtime_us,
+ &context->orig_runtime_us);
}
static int __osnoise_write_period(struct osnoise_context *context,
- unsigned long long period)
+ long long period)
{
- int retval;
-
- if (context->orig_period_us == OSNOISE_TIME_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("osnoise/period_us", period);
- if (retval < 0)
- return -1;
-
- context->period_us = period;
- return 0;
+ return osnoise_write_ll_config("osnoise/period_us",
+ period,
+ &context->period_us,
+ &context->orig_period_us);
}
/*
@@ -258,11 +313,11 @@ static int __osnoise_write_period(struct osnoise_context *context,
* to set the runtime and period if they are != 0.
*/
int osnoise_set_runtime_period(struct osnoise_context *context,
- unsigned long long runtime,
- unsigned long long period)
+ long long runtime,
+ long long period)
{
- unsigned long long curr_runtime_us;
- unsigned long long curr_period_us;
+ long long curr_runtime_us;
+ long long curr_period_us;
int retval;
if (!period && !runtime)
@@ -272,7 +327,7 @@ int osnoise_set_runtime_period(struct osnoise_context *context,
curr_period_us = osnoise_get_period(context);
/* error getting any value? */
- if (curr_period_us == OSNOISE_TIME_INIT_VAL || curr_runtime_us == OSNOISE_TIME_INIT_VAL)
+ if (curr_period_us == -1 || curr_runtime_us == -1)
return -1;
if (!period) {
@@ -309,13 +364,13 @@ int osnoise_set_runtime_period(struct osnoise_context *context,
*/
void osnoise_restore_runtime_period(struct osnoise_context *context)
{
- unsigned long long orig_runtime = context->orig_runtime_us;
- unsigned long long orig_period = context->orig_period_us;
- unsigned long long curr_runtime = context->runtime_us;
- unsigned long long curr_period = context->period_us;
+ long long orig_runtime = context->orig_runtime_us;
+ long long orig_period = context->orig_period_us;
+ long long curr_runtime = context->runtime_us;
+ long long curr_period = context->period_us;
int retval;
- if ((orig_runtime == OSNOISE_TIME_INIT_VAL) && (orig_period == OSNOISE_TIME_INIT_VAL))
+ if ((orig_runtime == -1) && (orig_period == -1))
return;
if ((orig_period == curr_period) && (orig_runtime == curr_runtime))
@@ -326,8 +381,8 @@ void osnoise_restore_runtime_period(struct osnoise_context *context)
err_msg("Could not restore original osnoise runtime/period\n");
out_done:
- context->runtime_us = OSNOISE_TIME_INIT_VAL;
- context->period_us = OSNOISE_TIME_INIT_VAL;
+ context->runtime_us = -1;
+ context->period_us = -1;
}
/*
@@ -337,498 +392,81 @@ void osnoise_put_runtime_period(struct osnoise_context *context)
{
osnoise_restore_runtime_period(context);
- if (context->orig_runtime_us != OSNOISE_TIME_INIT_VAL)
- context->orig_runtime_us = OSNOISE_TIME_INIT_VAL;
-
- if (context->orig_period_us != OSNOISE_TIME_INIT_VAL)
- context->orig_period_us = OSNOISE_TIME_INIT_VAL;
-}
-
-/*
- * osnoise_get_timerlat_period_us - read and save the original "timerlat_period_us"
- */
-static long long
-osnoise_get_timerlat_period_us(struct osnoise_context *context)
-{
- long long timerlat_period_us;
-
- if (context->timerlat_period_us != OSNOISE_TIME_INIT_VAL)
- return context->timerlat_period_us;
-
- if (context->orig_timerlat_period_us != OSNOISE_TIME_INIT_VAL)
- return context->orig_timerlat_period_us;
-
- timerlat_period_us = osnoise_read_ll_config("osnoise/timerlat_period_us");
- if (timerlat_period_us < 0)
- goto out_err;
-
- context->orig_timerlat_period_us = timerlat_period_us;
- return timerlat_period_us;
-
-out_err:
- return OSNOISE_TIME_INIT_VAL;
-}
-
-/*
- * osnoise_set_timerlat_period_us - set "timerlat_period_us"
- */
-int osnoise_set_timerlat_period_us(struct osnoise_context *context, long long timerlat_period_us)
-{
- long long curr_timerlat_period_us = osnoise_get_timerlat_period_us(context);
- int retval;
-
- if (curr_timerlat_period_us == OSNOISE_TIME_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("osnoise/timerlat_period_us", timerlat_period_us);
- if (retval < 0)
- return -1;
-
- context->timerlat_period_us = timerlat_period_us;
-
- return 0;
-}
-
-/*
- * osnoise_restore_timerlat_period_us - restore "timerlat_period_us"
- */
-void osnoise_restore_timerlat_period_us(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_timerlat_period_us == OSNOISE_TIME_INIT_VAL)
- return;
-
- if (context->orig_timerlat_period_us == context->timerlat_period_us)
- goto out_done;
-
- retval = osnoise_write_ll_config("osnoise/timerlat_period_us", context->orig_timerlat_period_us);
- if (retval < 0)
- err_msg("Could not restore original osnoise timerlat_period_us\n");
-
-out_done:
- context->timerlat_period_us = OSNOISE_TIME_INIT_VAL;
-}
-
-/*
- * osnoise_put_timerlat_period_us - restore original values and cleanup data
- */
-void osnoise_put_timerlat_period_us(struct osnoise_context *context)
-{
- osnoise_restore_timerlat_period_us(context);
-
- if (context->orig_timerlat_period_us == OSNOISE_TIME_INIT_VAL)
- return;
-
- context->orig_timerlat_period_us = OSNOISE_TIME_INIT_VAL;
-}
-
-/*
- * osnoise_get_timerlat_align_us - read and save the original "timerlat_align_us"
- */
-static long long
-osnoise_get_timerlat_align_us(struct osnoise_context *context)
-{
- long long timerlat_align_us;
-
- if (context->timerlat_align_us != OSNOISE_OPTION_INIT_VAL)
- return context->timerlat_align_us;
-
- if (context->orig_timerlat_align_us != OSNOISE_OPTION_INIT_VAL)
- return context->orig_timerlat_align_us;
-
- timerlat_align_us = osnoise_read_ll_config("osnoise/timerlat_align_us");
- if (timerlat_align_us < 0)
- goto out_err;
-
- context->orig_timerlat_align_us = timerlat_align_us;
- return timerlat_align_us;
-
-out_err:
- return OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_set_timerlat_align_us - set "timerlat_align_us"
- */
-int osnoise_set_timerlat_align_us(struct osnoise_context *context, long long timerlat_align_us)
-{
- long long curr_timerlat_align_us = osnoise_get_timerlat_align_us(context);
- int retval;
-
- if (curr_timerlat_align_us == OSNOISE_OPTION_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("osnoise/timerlat_align_us", timerlat_align_us);
- if (retval < 0)
- return -1;
-
- context->timerlat_align_us = timerlat_align_us;
-
- return 0;
-}
-
-/*
- * osnoise_restore_timerlat_align_us - restore "timerlat_align_us"
- */
-void osnoise_restore_timerlat_align_us(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_timerlat_align_us == OSNOISE_OPTION_INIT_VAL)
- return;
-
- if (context->orig_timerlat_align_us == context->timerlat_align_us)
- goto out_done;
-
- retval = osnoise_write_ll_config("osnoise/timerlat_align_us",
- context->orig_timerlat_align_us);
- if (retval < 0)
- err_msg("Could not restore original osnoise timerlat_align_us\n");
-
-out_done:
- context->timerlat_align_us = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_put_timerlat_align_us - restore original values and cleanup data
- */
-void osnoise_put_timerlat_align_us(struct osnoise_context *context)
-{
- osnoise_restore_timerlat_align_us(context);
-
- if (context->orig_timerlat_align_us == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_timerlat_align_us = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_get_stop_us - read and save the original "stop_tracing_us"
- */
-static long long
-osnoise_get_stop_us(struct osnoise_context *context)
-{
- long long stop_us;
-
- if (context->stop_us != OSNOISE_OPTION_INIT_VAL)
- return context->stop_us;
-
- if (context->orig_stop_us != OSNOISE_OPTION_INIT_VAL)
- return context->orig_stop_us;
-
- stop_us = osnoise_read_ll_config("osnoise/stop_tracing_us");
- if (stop_us < 0)
- goto out_err;
-
- context->orig_stop_us = stop_us;
- return stop_us;
-
-out_err:
- return OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_set_stop_us - set "stop_tracing_us"
- */
-int osnoise_set_stop_us(struct osnoise_context *context, long long stop_us)
-{
- long long curr_stop_us = osnoise_get_stop_us(context);
- int retval;
-
- if (curr_stop_us == OSNOISE_OPTION_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("osnoise/stop_tracing_us", stop_us);
- if (retval < 0)
- return -1;
-
- context->stop_us = stop_us;
-
- return 0;
-}
-
-/*
- * osnoise_restore_stop_us - restore the original "stop_tracing_us"
- */
-void osnoise_restore_stop_us(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_stop_us == OSNOISE_OPTION_INIT_VAL)
- return;
-
- if (context->orig_stop_us == context->stop_us)
- goto out_done;
-
- retval = osnoise_write_ll_config("osnoise/stop_tracing_us", context->orig_stop_us);
- if (retval < 0)
- err_msg("Could not restore original osnoise stop_us\n");
-
-out_done:
- context->stop_us = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_put_stop_us - restore original values and cleanup data
- */
-void osnoise_put_stop_us(struct osnoise_context *context)
-{
- osnoise_restore_stop_us(context);
-
- if (context->orig_stop_us == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_stop_us = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_get_stop_total_us - read and save the original "stop_tracing_total_us"
- */
-static long long
-osnoise_get_stop_total_us(struct osnoise_context *context)
-{
- long long stop_total_us;
-
- if (context->stop_total_us != OSNOISE_OPTION_INIT_VAL)
- return context->stop_total_us;
-
- if (context->orig_stop_total_us != OSNOISE_OPTION_INIT_VAL)
- return context->orig_stop_total_us;
-
- stop_total_us = osnoise_read_ll_config("osnoise/stop_tracing_total_us");
- if (stop_total_us < 0)
- goto out_err;
-
- context->orig_stop_total_us = stop_total_us;
- return stop_total_us;
-
-out_err:
- return OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_set_stop_total_us - set "stop_tracing_total_us"
- */
-int osnoise_set_stop_total_us(struct osnoise_context *context, long long stop_total_us)
-{
- long long curr_stop_total_us = osnoise_get_stop_total_us(context);
- int retval;
-
- if (curr_stop_total_us == OSNOISE_OPTION_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("osnoise/stop_tracing_total_us", stop_total_us);
- if (retval < 0)
- return -1;
-
- context->stop_total_us = stop_total_us;
-
- return 0;
-}
-
-/*
- * osnoise_restore_stop_total_us - restore the original "stop_tracing_total_us"
- */
-void osnoise_restore_stop_total_us(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_stop_total_us == OSNOISE_OPTION_INIT_VAL)
- return;
-
- if (context->orig_stop_total_us == context->stop_total_us)
- goto out_done;
-
- retval = osnoise_write_ll_config("osnoise/stop_tracing_total_us",
- context->orig_stop_total_us);
- if (retval < 0)
- err_msg("Could not restore original osnoise stop_total_us\n");
-
-out_done:
- context->stop_total_us = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_put_stop_total_us - restore original values and cleanup data
- */
-void osnoise_put_stop_total_us(struct osnoise_context *context)
-{
- osnoise_restore_stop_total_us(context);
-
- if (context->orig_stop_total_us == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_stop_total_us = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_get_print_stack - read and save the original "print_stack"
- */
-static long long
-osnoise_get_print_stack(struct osnoise_context *context)
-{
- long long print_stack;
-
- if (context->print_stack != OSNOISE_OPTION_INIT_VAL)
- return context->print_stack;
-
- if (context->orig_print_stack != OSNOISE_OPTION_INIT_VAL)
- return context->orig_print_stack;
-
- print_stack = osnoise_read_ll_config("osnoise/print_stack");
- if (print_stack < 0)
- goto out_err;
-
- context->orig_print_stack = print_stack;
- return print_stack;
-
-out_err:
- return OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_set_print_stack - set "print_stack"
- */
-int osnoise_set_print_stack(struct osnoise_context *context, long long print_stack)
-{
- long long curr_print_stack = osnoise_get_print_stack(context);
- int retval;
-
- if (curr_print_stack == OSNOISE_OPTION_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("osnoise/print_stack", print_stack);
- if (retval < 0)
- return -1;
-
- context->print_stack = print_stack;
-
- return 0;
-}
-
-/*
- * osnoise_restore_print_stack - restore the original "print_stack"
- */
-void osnoise_restore_print_stack(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_print_stack == OSNOISE_OPTION_INIT_VAL)
- return;
-
- if (context->orig_print_stack == context->print_stack)
- goto out_done;
-
- retval = osnoise_write_ll_config("osnoise/print_stack", context->orig_print_stack);
- if (retval < 0)
- err_msg("Could not restore original osnoise print_stack\n");
-
-out_done:
- context->print_stack = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_put_print_stack - restore original values and cleanup data
- */
-void osnoise_put_print_stack(struct osnoise_context *context)
-{
- osnoise_restore_print_stack(context);
-
- if (context->orig_print_stack == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_print_stack = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_get_tracing_thresh - read and save the original "tracing_thresh"
- */
-static long long
-osnoise_get_tracing_thresh(struct osnoise_context *context)
-{
- long long tracing_thresh;
-
- if (context->tracing_thresh != OSNOISE_OPTION_INIT_VAL)
- return context->tracing_thresh;
-
- if (context->orig_tracing_thresh != OSNOISE_OPTION_INIT_VAL)
- return context->orig_tracing_thresh;
-
- tracing_thresh = osnoise_read_ll_config("tracing_thresh");
- if (tracing_thresh < 0)
- goto out_err;
-
- context->orig_tracing_thresh = tracing_thresh;
- return tracing_thresh;
-
-out_err:
- return OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_set_tracing_thresh - set "tracing_thresh"
- */
-int osnoise_set_tracing_thresh(struct osnoise_context *context, long long tracing_thresh)
-{
- long long curr_tracing_thresh = osnoise_get_tracing_thresh(context);
- int retval;
-
- if (curr_tracing_thresh == OSNOISE_OPTION_INIT_VAL)
- return -1;
-
- retval = osnoise_write_ll_config("tracing_thresh", tracing_thresh);
- if (retval < 0)
- return -1;
-
- context->tracing_thresh = tracing_thresh;
-
- return 0;
-}
-
-/*
- * osnoise_restore_tracing_thresh - restore the original "tracing_thresh"
- */
-void osnoise_restore_tracing_thresh(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_tracing_thresh == OSNOISE_OPTION_INIT_VAL)
- return;
-
- if (context->orig_tracing_thresh == context->tracing_thresh)
- goto out_done;
-
- retval = osnoise_write_ll_config("tracing_thresh", context->orig_tracing_thresh);
- if (retval < 0)
- err_msg("Could not restore original tracing_thresh\n");
-
-out_done:
- context->tracing_thresh = OSNOISE_OPTION_INIT_VAL;
-}
-
-/*
- * osnoise_put_tracing_thresh - restore original values and cleanup data
+ if (context->orig_runtime_us != -1)
+ context->orig_runtime_us = -1;
+
+ if (context->orig_period_us != -1)
+ context->orig_period_us = -1;
+}
+
+/*
+ * Long long option set/restore/put functions, generated from OSNOISE_LL_OPTIONS.
+ */
+#define OSNOISE_LL_OPTION(name, path) \
+int osnoise_set_##name(struct osnoise_context *context, long long name) \
+{ \
+ return osnoise_write_ll_config(path, \
+ name, \
+ &context->name, \
+ &context->orig_##name); \
+} \
+ \
+void osnoise_restore_##name(struct osnoise_context *context) \
+{ \
+ osnoise_restore_ll_config(path, &context->name, &context->orig_##name); \
+} \
+ \
+static void osnoise_put_##name(struct osnoise_context *context) \
+{ \
+ osnoise_restore_##name(context); \
+ \
+ if (context->orig_##name == -1) \
+ return; \
+ \
+ context->orig_##name = -1; \
+}
+OSNOISE_LL_OPTIONS
+#undef OSNOISE_LL_OPTION
+
+/**
+ * osnoise_get_option - read a boolean flag from osnoise/options
+ * @option: the option name string to look for (e.g. "OSNOISE_IRQ_DISABLE")
+ * @set_value: pointer to the cached value set by RTLA, or -1 if unset
+ * @orig_value: pointer to the cached original value read from tracefs, or -1 if unread
+ *
+ * Returns the current state of the flag @option. If RTLA has already written a
+ * value (@set_value != -1), that value is returned. If the original has already
+ * been read (@orig_value != -1), that cached value is returned. Otherwise, reads
+ * the "osnoise/options" file from tracefs, checks whether @option appears with or
+ * without a "NO_" prefix, caches the result in @orig_value (so it can later be
+ * restored), and returns it.
+ *
+ * This is the shared read primitive used by the OSNOISE_FLAG_OPTION-generated
+ * set/restore/put functions.
+ *
+ * Returns 1 if enabled, 0 if disabled, or -1 on error (option unknown to kernel).
*/
-void osnoise_put_tracing_thresh(struct osnoise_context *context)
-{
- osnoise_restore_tracing_thresh(context);
-
- if (context->orig_tracing_thresh == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_tracing_thresh = OSNOISE_OPTION_INIT_VAL;
-}
-
-static int osnoise_options_get_option(char *option)
+static int osnoise_get_option(char *option,
+ int *set_value,
+ int *orig_value)
{
- char *options = tracefs_instance_file_read(NULL, "osnoise/options", NULL);
+ char *options;
char no_option[128];
int retval = 0;
char *opt;
+ if (*set_value != -1)
+ /* option has been set by RTLA already */
+ return *set_value;
+
+ if (*orig_value != -1)
+ /* RTLA has already read the option */
+ return *orig_value;
+
+ /* current value is not known to RTLA yet, read it from tracefs */
+ options = tracefs_instance_file_read(NULL, "osnoise/options", NULL);
if (!options)
- return OSNOISE_OPTION_INIT_VAL;
+ return -1;
/*
* Check first if the option is disabled.
@@ -847,207 +485,138 @@ static int osnoise_options_get_option(char *option)
if (opt)
retval = 1;
else
- retval = OSNOISE_OPTION_INIT_VAL;
+ retval = -1;
out_free:
free(options);
- return retval;
-}
-
-static int osnoise_options_set_option(char *option, bool onoff)
-{
- char no_option[128];
-
- if (onoff)
- return tracefs_instance_file_write(NULL, "osnoise/options", option);
-
- snprintf(no_option, sizeof(no_option), "NO_%s", option);
-
- return tracefs_instance_file_write(NULL, "osnoise/options", no_option);
-}
-
-static int osnoise_get_irq_disable(struct osnoise_context *context)
-{
- if (context->opt_irq_disable != OSNOISE_OPTION_INIT_VAL)
- return context->opt_irq_disable;
-
- if (context->orig_opt_irq_disable != OSNOISE_OPTION_INIT_VAL)
- return context->orig_opt_irq_disable;
-
- context->orig_opt_irq_disable = osnoise_options_get_option("OSNOISE_IRQ_DISABLE");
-
- return context->orig_opt_irq_disable;
-}
-
-int osnoise_set_irq_disable(struct osnoise_context *context, bool onoff)
-{
- int opt_irq_disable = osnoise_get_irq_disable(context);
- int retval;
-
- if (opt_irq_disable == OSNOISE_OPTION_INIT_VAL)
- return -1;
-
- if (opt_irq_disable == onoff)
- return 0;
-
- retval = osnoise_options_set_option("OSNOISE_IRQ_DISABLE", onoff);
- if (retval < 0)
- return -1;
-
- context->opt_irq_disable = onoff;
- return 0;
-}
-
-static void osnoise_restore_irq_disable(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_opt_irq_disable == OSNOISE_OPTION_INIT_VAL)
- return;
-
- if (context->orig_opt_irq_disable == context->opt_irq_disable)
- goto out_done;
-
- retval = osnoise_options_set_option("OSNOISE_IRQ_DISABLE", context->orig_opt_irq_disable);
if (retval < 0)
- err_msg("Could not restore original OSNOISE_IRQ_DISABLE option\n");
-
-out_done:
- context->orig_opt_irq_disable = OSNOISE_OPTION_INIT_VAL;
-}
-
-static void osnoise_put_irq_disable(struct osnoise_context *context)
-{
- osnoise_restore_irq_disable(context);
-
- if (context->orig_opt_irq_disable == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_opt_irq_disable = OSNOISE_OPTION_INIT_VAL;
-}
-
-static int osnoise_get_workload(struct osnoise_context *context)
-{
- if (context->opt_workload != OSNOISE_OPTION_INIT_VAL)
- return context->opt_workload;
-
- if (context->orig_opt_workload != OSNOISE_OPTION_INIT_VAL)
- return context->orig_opt_workload;
+ goto out_err;
- context->orig_opt_workload = osnoise_options_get_option("OSNOISE_WORKLOAD");
+ /* save the value and return it */
+ *orig_value = retval;
+ return retval;
- return context->orig_opt_workload;
+out_err:
+ return -1;
}
-int osnoise_set_workload(struct osnoise_context *context, bool onoff)
+/**
+ * osnoise_set_option - write a boolean flag to osnoise/options
+ * @option: the option name string (e.g. "OSNOISE_IRQ_DISABLE")
+ * @onoff: the desired state (true to enable, false to disable)
+ * @set_value: pointer to the cached value set by RTLA, or -1 if unset
+ * @orig_value: pointer to the cached original value read from tracefs, or -1 if unread
+ *
+ * Sets the flag @option to the state @onoff. Before writing, calls
+ * osnoise_get_option() to ensure the original value is cached in @orig_value
+ * (enabling later restoration). If the current value already matches @onoff,
+ * no write is performed. On successful write, records the new state in
+ * @set_value.
+ *
+ * This is the shared write primitive used by the OSNOISE_FLAG_OPTION-generated
+ * set/restore/put functions.
+ *
+ * Returns 0 on success, -1 on read error (option likely unknown to kernel),
+ * or -2 on write error.
+ */
+static int osnoise_set_option(char *option,
+ bool onoff,
+ int *set_value,
+ int *orig_value)
{
- int opt_workload = osnoise_get_workload(context);
+ int curr = osnoise_get_option(option, set_value, orig_value);
+ char no_option[128];
int retval;
- if (opt_workload == OSNOISE_OPTION_INIT_VAL)
+ if (curr == -1)
+ /* read failed, option likely unknown to kernel */
return -1;
- if (opt_workload == onoff)
+ if (curr == onoff)
return 0;
- retval = osnoise_options_set_option("OSNOISE_WORKLOAD", onoff);
- if (retval < 0)
- return -2;
-
- context->opt_workload = onoff;
-
- return 0;
-}
-
-static void osnoise_restore_workload(struct osnoise_context *context)
-{
- int retval;
-
- if (context->orig_opt_workload == OSNOISE_OPTION_INIT_VAL)
- return;
-
- if (context->orig_opt_workload == context->opt_workload)
- goto out_done;
-
- retval = osnoise_options_set_option("OSNOISE_WORKLOAD", context->orig_opt_workload);
- if (retval < 0)
- err_msg("Could not restore original OSNOISE_WORKLOAD option\n");
-
-out_done:
- context->orig_opt_workload = OSNOISE_OPTION_INIT_VAL;
-}
-
-static void osnoise_put_workload(struct osnoise_context *context)
-{
- osnoise_restore_workload(context);
-
- if (context->orig_opt_workload == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_opt_workload = OSNOISE_OPTION_INIT_VAL;
-}
-
-static int osnoise_get_timerlat_align(struct osnoise_context *context)
-{
- if (context->opt_timerlat_align != OSNOISE_OPTION_INIT_VAL)
- return context->opt_timerlat_align;
-
- if (context->orig_opt_timerlat_align != OSNOISE_OPTION_INIT_VAL)
- return context->orig_opt_timerlat_align;
-
- context->orig_opt_timerlat_align = osnoise_options_get_option("TIMERLAT_ALIGN");
-
- return context->orig_opt_timerlat_align;
-}
-
-int osnoise_set_timerlat_align(struct osnoise_context *context, bool onoff)
-{
- int opt_timerlat_align = osnoise_get_timerlat_align(context);
- int retval;
-
- if (opt_timerlat_align == OSNOISE_OPTION_INIT_VAL)
- return -1;
-
- if (opt_timerlat_align == onoff)
- return 0;
+ if (onoff) {
+ retval = tracefs_instance_file_write(NULL, "osnoise/options", option);
+ } else {
+ snprintf(no_option, sizeof(no_option), "NO_%s", option);
+ retval = tracefs_instance_file_write(NULL, "osnoise/options", no_option);
+ }
- retval = osnoise_options_set_option("TIMERLAT_ALIGN", onoff);
if (retval < 0)
+ /* write failed, hard error */
return -2;
- context->opt_timerlat_align = onoff;
-
+ /* record the set value and return success */
+ *set_value = onoff;
return 0;
}
-static void osnoise_restore_timerlat_align(struct osnoise_context *context)
+/**
+ * osnoise_restore_option - restore a boolean flag to its original value
+ * @option: the option name string (e.g. "OSNOISE_IRQ_DISABLE")
+ * @set_value: pointer to the cached value set by RTLA, or -1 if unset
+ * @orig_value: pointer to the cached original value read from tracefs, or -1 if unread
+ *
+ * Restores the flag @option to the state cached in @orig_value (which was saved
+ * by a prior osnoise_get_option() or osnoise_set_option() call). If the original
+ * was never read, or if the current set value already matches the original, no
+ * write is performed. After restoring, clears @set_value to -1 to indicate RTLA
+ * no longer overrides this option.
+ *
+ * This is the shared restore primitive used by the OSNOISE_FLAG_OPTION-generated
+ * restore/put functions.
+ */
+static void osnoise_restore_option(char *option,
+ int *set_value,
+ int *orig_value)
{
int retval;
- if (context->orig_opt_timerlat_align == OSNOISE_OPTION_INIT_VAL)
+ if (*orig_value == -1)
return;
- if (context->orig_opt_timerlat_align == context->opt_timerlat_align)
+ if (*orig_value == *set_value)
goto out_done;
- retval = osnoise_options_set_option("TIMERLAT_ALIGN", context->orig_opt_timerlat_align);
+ retval = osnoise_set_option(option, *orig_value, set_value, orig_value);
if (retval < 0)
- err_msg("Could not restore original TIMERLAT_ALIGN option\n");
+ err_msg("Could not restore original %s option\n", option);
out_done:
- context->orig_opt_timerlat_align = OSNOISE_OPTION_INIT_VAL;
-}
-
-static void osnoise_put_timerlat_align(struct osnoise_context *context)
-{
- osnoise_restore_timerlat_align(context);
-
- if (context->orig_opt_timerlat_align == OSNOISE_OPTION_INIT_VAL)
- return;
-
- context->orig_opt_timerlat_align = OSNOISE_OPTION_INIT_VAL;
-}
+ *set_value = -1;
+}
+
+/*
+ * Flag option set/restore/put functions, generated from OSNOISE_FLAG_OPTIONS.
+ */
+#define OSNOISE_FLAG_OPTION(name, option_str) \
+int osnoise_set_##name(struct osnoise_context *context, bool onoff) \
+{ \
+ return osnoise_set_option(option_str, \
+ onoff, \
+ &context->opt_##name, \
+ &context->orig_opt_##name); \
+} \
+ \
+void osnoise_restore_##name(struct osnoise_context *context) \
+{ \
+ osnoise_restore_option(option_str, \
+ &context->opt_##name, \
+ &context->orig_opt_##name); \
+} \
+ \
+static void osnoise_put_##name(struct osnoise_context *context) \
+{ \
+ osnoise_restore_##name(context); \
+ \
+ if (context->orig_opt_##name == -1) \
+ return; \
+ \
+ context->orig_opt_##name = -1; \
+}
+OSNOISE_FLAG_OPTIONS
+#undef OSNOISE_FLAG_OPTION
enum {
FLAG_CONTEXT_NEWLY_CREATED = (1 << 0),
@@ -1083,29 +652,23 @@ struct osnoise_context *osnoise_context_alloc(void)
context = calloc_fatal(1, sizeof(*context));
- context->orig_stop_us = OSNOISE_OPTION_INIT_VAL;
- context->stop_us = OSNOISE_OPTION_INIT_VAL;
-
- context->orig_stop_total_us = OSNOISE_OPTION_INIT_VAL;
- context->stop_total_us = OSNOISE_OPTION_INIT_VAL;
-
- context->orig_print_stack = OSNOISE_OPTION_INIT_VAL;
- context->print_stack = OSNOISE_OPTION_INIT_VAL;
-
- context->orig_tracing_thresh = OSNOISE_OPTION_INIT_VAL;
- context->tracing_thresh = OSNOISE_OPTION_INIT_VAL;
-
- context->orig_opt_irq_disable = OSNOISE_OPTION_INIT_VAL;
- context->opt_irq_disable = OSNOISE_OPTION_INIT_VAL;
-
- context->orig_opt_workload = OSNOISE_OPTION_INIT_VAL;
- context->opt_workload = OSNOISE_OPTION_INIT_VAL;
-
- context->orig_opt_timerlat_align = OSNOISE_OPTION_INIT_VAL;
- context->opt_timerlat_align = OSNOISE_OPTION_INIT_VAL;
-
- context->orig_timerlat_align_us = OSNOISE_OPTION_INIT_VAL;
- context->timerlat_align_us = OSNOISE_OPTION_INIT_VAL;
+ /* First allocate manually implemented options... */
+ context->orig_runtime_us = -1;
+ context->runtime_us = -1;
+ context->orig_period_us = -1;
+ context->period_us = -1;
+
+ /* ...then the automatically generated ones. */
+#define OSNOISE_LL_OPTION(name, path) \
+ context->orig_##name = -1; \
+ context->name = -1;
+#define OSNOISE_FLAG_OPTION(name, option_str) \
+ context->orig_opt_##name = -1; \
+ context->opt_##name = -1;
+ OSNOISE_LL_OPTIONS
+ OSNOISE_FLAG_OPTIONS
+#undef OSNOISE_LL_OPTION
+#undef OSNOISE_FLAG_OPTION
osnoise_get_context(context);
@@ -1126,17 +689,17 @@ void osnoise_put_context(struct osnoise_context *context)
if (!(context->flags & FLAG_CONTEXT_DELETED))
return;
+ /* First restore the original values of the options... */
osnoise_put_cpus(context);
osnoise_put_runtime_period(context);
- osnoise_put_stop_us(context);
- osnoise_put_stop_total_us(context);
- osnoise_put_timerlat_period_us(context);
- osnoise_put_print_stack(context);
- osnoise_put_tracing_thresh(context);
- osnoise_put_irq_disable(context);
- osnoise_put_workload(context);
- osnoise_put_timerlat_align(context);
- osnoise_put_timerlat_align_us(context);
+
+ /* ...then the automatically generated ones. */
+#define OSNOISE_LL_OPTION(name, path) osnoise_put_##name(context);
+#define OSNOISE_FLAG_OPTION(name, option_str) osnoise_put_##name(context);
+ OSNOISE_LL_OPTIONS
+ OSNOISE_FLAG_OPTIONS
+#undef OSNOISE_LL_OPTION
+#undef OSNOISE_FLAG_OPTION
free(context);
}
diff --git a/tools/tracing/rtla/src/osnoise.h b/tools/tracing/rtla/src/osnoise.h
index 340ff5a64e6e4..6d94fc0e29b21 100644
--- a/tools/tracing/rtla/src/osnoise.h
+++ b/tools/tracing/rtla/src/osnoise.h
@@ -18,44 +18,15 @@ struct osnoise_params {
#define to_osnoise_params(ptr) container_of(ptr, struct osnoise_params, common)
-/*
- * *_INIT_VALs are also invalid values, they are used to
- * communicate errors.
- */
-#define OSNOISE_OPTION_INIT_VAL (-1)
-#define OSNOISE_TIME_INIT_VAL (0)
-
struct osnoise_context *osnoise_context_alloc(void);
int osnoise_get_context(struct osnoise_context *context);
void osnoise_put_context(struct osnoise_context *context);
int osnoise_set_runtime_period(struct osnoise_context *context,
- unsigned long long runtime,
- unsigned long long period);
+ long long runtime,
+ long long period);
void osnoise_restore_runtime_period(struct osnoise_context *context);
-void osnoise_restore_stop_us(struct osnoise_context *context);
-void osnoise_restore_stop_total_us(struct osnoise_context *context);
-
-int osnoise_set_timerlat_period_us(struct osnoise_context *context,
- long long timerlat_period_us);
-void osnoise_restore_timerlat_period_us(struct osnoise_context *context);
-
-int osnoise_set_tracing_thresh(struct osnoise_context *context,
- long long tracing_thresh);
-void osnoise_restore_tracing_thresh(struct osnoise_context *context);
-
-void osnoise_restore_print_stack(struct osnoise_context *context);
-int osnoise_set_print_stack(struct osnoise_context *context,
- long long print_stack);
-
-int osnoise_set_timerlat_align_us(struct osnoise_context *context,
- long long timerlat_align_us);
-void osnoise_restore_timerlat_align_us(struct osnoise_context *context);
-
-int osnoise_set_timerlat_align(struct osnoise_context *context, bool onoff);
-
-int osnoise_set_irq_disable(struct osnoise_context *context, bool onoff);
void osnoise_report_missed_events(struct osnoise_tool *tool);
int osnoise_apply_config(struct osnoise_tool *tool, struct osnoise_params *params);
--
2.55.0
^ permalink raw reply related
* Re: [PATCH v3 06/11] mm/cma: Allow dynamically creating CMA areas
From: David Hildenbrand (Arm) @ 2026-07-08 8:35 UTC (permalink / raw)
To: Marek Szyprowski, Thierry Reding, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jonathan Hunter,
Mikko Perttunen, Yury Norov, Rasmus Villemoes, Russell King,
Alexander Gordeev, Gerald Schaefer, Heiko Carstens, Vasily Gorbik,
Christian Borntraeger, Sven Schnelle, Andrew Morton,
Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Robin Murphy, Sumit Semwal,
Benjamin Gaignard, Brian Starkey, John Stultz, T.J. Mercier,
Christian König, Steven Rostedt, Masami Hiramatsu,
Mathieu Desnoyers, Catalin Marinas, Will Deacon
Cc: devicetree, linux-tegra, linux-kernel, dri-devel, linux-media,
linux-arm-kernel, linux-s390, linux-mm, iommu, linaro-mm-sig,
linux-trace-kernel
In-Reply-To: <3f47aeab-33b1-4966-a5ce-5d6d5261e0e2@samsung.com>
On 7/7/26 12:02, Marek Szyprowski wrote:
> On 01.07.2026 18:08, Thierry Reding wrote:
>> From: Thierry Reding <treding@nvidia.com>
>>
>> There is no technical reason why there should be a limited number of CMA
>> regions, so extract some code into helpers and use them to create extra
>> functions (cma_create() and cma_free()) that allow creating and freeing,
>> respectively, CMA regions dynamically at runtime.
>
>
> Well, the technical reason for not creating cma regions dynamically at
> runtime is that on some architectures (like 32bit ARM) the early fixup
> for the region is needed to make it functional for DMA.
Can you point me at the code that does that? Thanks!
--
Cheers,
David
^ permalink raw reply
* Re: [RFC 1/3] arm64: kprobes: Do not handle non-XOL faults as kprobe faults
From: Hongyan Xia @ 2026-07-08 8:55 UTC (permalink / raw)
To: Masami Hiramatsu (Google)
Cc: Pu Hu, ada.coupriediaz@arm.com, catalin.marinas@arm.com,
davem@davemloft.net, Jiazi Li,
linux-arm-kernel@lists.infradead.org,
linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org,
naveen@kernel.org, will@kernel.org, yang@os.amperecomputing.com
In-Reply-To: <20260708164224.c080a1e834f02ac953007e5c@kernel.org>
On 7/8/2026 3:42 PM, Masami Hiramatsu wrote:
> On Wed, 8 Jul 2026 05:57:24 +0000
> Hongyan Xia <hongyan.xia@transsion.com> wrote:
>
>> Hi Masami,
>>
>> On 7/8/2026 8:46 AM, Masami Hiramatsu wrote:
>>> On Mon, 6 Jul 2026 08:36:48 +0000
>>> Pu Hu <hupu@transsion.com> wrote:
>>>
>>>> From: Pu Hu <hupu@transsion.com>
>>>>
>>>> kprobe_fault_handler() handles faults taken while kprobes is in
>>>> KPROBE_HIT_SS or KPROBE_REENTER state as faults caused by the
>>>> single-stepped instruction.
>>>>
>>>> That assumption is not always true. While a kprobe is preparing or
>>>> executing the out-of-line single-step instruction, other code may run
>>>> in that window. For example, perf or trace code can be invoked from the
>>>> debug exception path and may take a fault of its own. In that case the
>>>> fault did not happen on the kprobe XOL instruction, but the kprobe fault
>>>> handler may still try to recover it as a kprobe single-step fault.
>>>>
>>>> This can corrupt the exception recovery flow and leave the real fault to
>>>> be handled with a wrong PC. A typical reproducer is running simpleperf
>>>> with preemptirq tracepoints and dwarf callchains while a kprobe is
>>>> installed on a frequently executed kernel function.
>>>>
>>>> Fix this by handling faults in KPROBE_HIT_SS/KPROBE_REENTER only when
>>>> the faulting PC points at the current kprobe's XOL instruction. Faults
>>>> from any other PC are left to the normal fault handling path.
>>>>
>>>> This follows the same idea as the x86 fix in commit 6381c24cd6d5
>>>> ("kprobes/x86: Fix page-fault handling logic").
>>>>
>>>> Signed-off-by: Pu Hu <hupu@transsion.com>
>>>> Signed-off-by: Hongyan Xia <hongyan.xia@transsion.com>
>>>> ---
>>>> arch/arm64/kernel/probes/kprobes.c | 14 ++++++++++++++
>>>> 1 file changed, 14 insertions(+)
>>>>
>>>> diff --git a/arch/arm64/kernel/probes/kprobes.c b/arch/arm64/kernel/probes/kprobes.c
>>>> index 43a0361a8bf0..e4d2852ce2fb 100644
>>>> --- a/arch/arm64/kernel/probes/kprobes.c
>>>> +++ b/arch/arm64/kernel/probes/kprobes.c
>>>> @@ -285,6 +285,20 @@ int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)
>>>> switch (kcb->kprobe_status) {
>>>> case KPROBE_HIT_SS:
>>>> case KPROBE_REENTER:
>>>> + /*
>>>> + * A fault taken while a kprobe is single-stepping is not
>>>> + * necessarily caused by the instruction in the XOL slot. For
>>>> + * example, tracing or perf code running in this window may take
>>>> + * an unrelated fault.
>>>> + *
>>>> + * Handle the fault here only when the faulting PC is the XOL
>>>> + * instruction of the current kprobe. Otherwise let the normal
>>>> + * fault handling path deal with it.
>>>> + */
>>>> + if (cur->ainsn.xol_insn &&
>>>> + instruction_pointer(regs) != (unsigned long)cur->ainsn.xol_insn)
>>>> + break;
>>>
>>> Can you check Sashiko's comments[1]?
>>>
>>> [1] https://sashiko.dev/#/patchset/20260706083636.159883-1-hupu%40transsion.com?part=1
>>>
>>> It seems that it complains about simulated kprobe's case.
>>> In that case, cur->ainsn.xol_insn == NULL. The simulation should be done
>>> in the kprobe context (which is a debug trap). I'm not sure the arm64
>>> can cause NMI in that context, but if it happens and causes a fault,
>>> it may cause a problem.
>>>
>>> So I think we can just ignore the fault on the simulated kprobes.
>>>
>>> To ensure that, you can just add:
>>>
>>> if (cur && !cur->ainsn.xol_insn)
>>> return 0;
>>>
>>> at the entry of this function. (and remove redundant cur->ainsn.xol_insn check)
>>
>> Right, both cases:
>>
>> 1. single-step XOL
>> 2. simulated
>>
>> have this problem and this patch fixed 1. 2 remains unchanged.
>>
>> Ideally we should fix both, but the simulated case seems more
>> complicated, and at least we didn't make things worse for 2. So I wonder
>> if we can analyze 2 more thoroughly and fix it in a separate patch.
>
> OK, but basically this fault handler is only for the SS XOL,
> not for simulated one (as same as x86). So just skip the
> simulated case is enough in this patch.
> (Note that x86 also have simulated path, and that is not handled
> by the fault handler)
Hmm, what happens if the original PC is a simulated instruction that has
a recoverable ex_table entry that handles potential page fault, and this
PC also has an attached kprobe? Then the simulated case should be able
to enter kprobe_fault_handler()?
.ex_table:
insn foo, handler bar
foo:
LDR symbol # Load PC-relative. Simulated. Kprobe attached.
ret
When foo is called and the kprobe fires, it enters simulated path but
then LDR triggers a page fault. It then enters kprobe_fault_handler() to
fixup the PC. Then the fixup_exception() of the normal page fault finds
the ex_table entry and this case is successfully handled?
Looks like this handler can be entered by simulated instructions? Or
this is only theoretical and whoever arrange code like this should be
fired immediately?
>
> Thank you,
>
>>
>>> Thank you,
>>>
>>>> +
>>>> /*
>>>> * We are here because the instruction being single
>>>> * stepped caused a page fault. We reset the current
>>>> --
>>>> 2.43.0
>>>>
>>> --
>>> Masami Hiramatsu (Google) <mhiramat@kernel.org>
>>
>
>
^ permalink raw reply
* Re: [PATCH v3 06/11] mm/cma: Allow dynamically creating CMA areas
From: David Hildenbrand (Arm) @ 2026-07-08 8:59 UTC (permalink / raw)
To: Thierry Reding, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Hunter, David Airlie, Simona Vetter, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, Sowjanya Komatineni,
Luca Ceresoli, Mikko Perttunen, Yury Norov, Rasmus Villemoes,
Russell King, Alexander Gordeev, Gerald Schaefer, Heiko Carstens,
Vasily Gorbik, Christian Borntraeger, Sven Schnelle,
Andrew Morton, Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka,
Mike Rapoport, Suren Baghdasaryan, Michal Hocko, Marek Szyprowski,
Robin Murphy, Sumit Semwal, Benjamin Gaignard, Brian Starkey,
John Stultz, T.J. Mercier, Christian König, Steven Rostedt,
Masami Hiramatsu, Mathieu Desnoyers, Catalin Marinas, Will Deacon
Cc: Thierry Reding, devicetree, linux-tegra, linux-kernel, dri-devel,
linux-media, linux-arm-kernel, linux-s390, linux-mm, iommu,
linaro-mm-sig, linux-trace-kernel, Thierry Reding
In-Reply-To: <20260701-tegra-vpr-v3-6-d80f7b871bb4@nvidia.com>
On 7/1/26 18:08, Thierry Reding wrote:
> From: Thierry Reding <treding@nvidia.com>
>
> There is no technical reason why there should be a limited number of CMA
> regions, so extract some code into helpers and use them to create extra
> functions (cma_create() and cma_free()) that allow creating and freeing,
> respectively, CMA regions dynamically at runtime.
I'm confused. We still allow cma_create() only during __init, right?
Would we expect callers of cma_free() after __init? Or at which point?
>
> The static array of CMA areas cannot be replaced by dynamically created
> areas because for many of them, allocation must not fail and some cases
> may need to initialize them before the slab allocator is even available.
We can start with a memblock array of an initial size (like we do today).
Then, when you need more space, we can double the size (copying content and
exchanging the pointer). Either allocate from memblock or from slab, if
available (slab_is_available).
memblock does something similar, see memblock_double_array().
--
Cheers,
David
^ permalink raw reply
* Re: [PATCH 2/2] spi: qcom-geni: add GENI SE registers trace event on error paths
From: Mukesh Savaliya @ 2026-07-08 10:06 UTC (permalink / raw)
To: Praveen Talari, Bjorn Andersson, Konrad Dybcio, Steven Rostedt,
Masami Hiramatsu, Mathieu Desnoyers, Mark Brown
Cc: linux-kernel, linux-arm-msm, linux-trace-kernel, linux-spi,
aniket.randive, chandana.chiluveru
In-Reply-To: <20260706-add-tracepoints-for-se-reg-dump-v1-2-48bd08e28cf2@oss.qualcomm.com>
On 7/6/2026 4:38 PM, Praveen Talari wrote:
> The GENI SPI driver reports various transfer failures such as command
> timeouts, DMA reset timeouts, DMA transaction errors, and unexpected
> interrupt conditions. However, diagnosing the root cause of these
> failures is difficult as the hardware state is not captured when the
> error occurs.
>
> Add trace_geni_se_regs() calls at critical SPI error handling paths to
> automatically capture GENI serial engine debug registers when failures
> are detected. This includes:
>
> - M_CMD abort/cancel timeout
> - DMA TX/RX FSM reset timeout
> - DMA transaction failures and pending residue conditions
> - Unexpected interrupt error status
> - Premature transfer completion with pending TX/RX data
>
> Dumping the SE debug registers at the time of failure provides
> additional hardware context and significantly improves post-mortem
> analysis of SPI transfer issues without affecting normal operation.
>
> Signed-off-by: Praveen Talari <praveen.talari@oss.qualcomm.com>
> ---
> drivers/spi/spi-geni-qcom.c | 22 ++++++++++++++++++----
> 1 file changed, 18 insertions(+), 4 deletions(-)
>
Acked-by: Mukesh Kumar Savaliya <mukesh.savaliya@oss.qualcomm.com>
^ permalink raw reply
* [GIT PULL] RTLA fixes for v7.2-rc3
From: Tomas Glozar @ 2026-07-08 11:24 UTC (permalink / raw)
To: Steven Rostedt
Cc: Andreas Ziegler, Bastian Blank, LKML, linux-trace-kernel,
Tomas Glozar
Steven,
The following changes since commit 8cdeaa50eae8dad34885515f62559ee83e7e8dda:
Linux 7.2-rc2 (2026-07-05 14:44:06 -1000)
are available in the Git repository at:
https://git.kernel.org/pub/scm/linux/kernel/git/tglozar/linux.git tags/rtla-fixes-v7.2-rc3
for you to fetch changes up to cd9993d22a577339a93dcb401574e874ea29b143:
rtla: Also link in ctype.c (2026-07-08 10:38:18 +0200)
----------------------------------------------------------------
RTLA fixes for v7.2-rc3
- Fix missing unistd include
A missing #include <unistd.h> broke build on uclibc systems.
Add the include to fix it.
- Fix missing tools/lib/ctype.c dependency
RTLA links tools/lib/string.c as a dependency of libsubcmd, some of its
functions require _ctype. Link tools/lib/ctype.c as well, to fix build
without GCC LTO.
Signed-off-by: Tomas Glozar <tglozar@redhat.com>
----------------------------------------------------------------
Andreas Ziegler (1):
rtla: Fix missing unistd include
Bastian Blank (1):
rtla: Also link in ctype.c
tools/tracing/rtla/Makefile | 14 ++++++++++----
tools/tracing/rtla/src/common.c | 1 +
2 files changed, 11 insertions(+), 4 deletions(-)
^ permalink raw reply
* Re: [RFC 1/3] arm64: kprobes: Do not handle non-XOL faults as kprobe faults
From: Masami Hiramatsu @ 2026-07-08 11:59 UTC (permalink / raw)
To: Hongyan Xia
Cc: Pu Hu, ada.coupriediaz@arm.com, catalin.marinas@arm.com,
davem@davemloft.net, Jiazi Li,
linux-arm-kernel@lists.infradead.org,
linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org,
naveen@kernel.org, will@kernel.org, yang@os.amperecomputing.com
In-Reply-To: <c33043c2-f931-4687-ad16-2d60ab12faca@transsion.com>
On Wed, 8 Jul 2026 08:55:37 +0000
Hongyan Xia <hongyan.xia@transsion.com> wrote:
> On 7/8/2026 3:42 PM, Masami Hiramatsu wrote:
> > On Wed, 8 Jul 2026 05:57:24 +0000
> > Hongyan Xia <hongyan.xia@transsion.com> wrote:
> >
> >> Hi Masami,
> >>
> >> On 7/8/2026 8:46 AM, Masami Hiramatsu wrote:
> >>> On Mon, 6 Jul 2026 08:36:48 +0000
> >>> Pu Hu <hupu@transsion.com> wrote:
> >>>
> >>>> From: Pu Hu <hupu@transsion.com>
> >>>>
> >>>> kprobe_fault_handler() handles faults taken while kprobes is in
> >>>> KPROBE_HIT_SS or KPROBE_REENTER state as faults caused by the
> >>>> single-stepped instruction.
> >>>>
> >>>> That assumption is not always true. While a kprobe is preparing or
> >>>> executing the out-of-line single-step instruction, other code may run
> >>>> in that window. For example, perf or trace code can be invoked from the
> >>>> debug exception path and may take a fault of its own. In that case the
> >>>> fault did not happen on the kprobe XOL instruction, but the kprobe fault
> >>>> handler may still try to recover it as a kprobe single-step fault.
> >>>>
> >>>> This can corrupt the exception recovery flow and leave the real fault to
> >>>> be handled with a wrong PC. A typical reproducer is running simpleperf
> >>>> with preemptirq tracepoints and dwarf callchains while a kprobe is
> >>>> installed on a frequently executed kernel function.
> >>>>
> >>>> Fix this by handling faults in KPROBE_HIT_SS/KPROBE_REENTER only when
> >>>> the faulting PC points at the current kprobe's XOL instruction. Faults
> >>>> from any other PC are left to the normal fault handling path.
> >>>>
> >>>> This follows the same idea as the x86 fix in commit 6381c24cd6d5
> >>>> ("kprobes/x86: Fix page-fault handling logic").
> >>>>
> >>>> Signed-off-by: Pu Hu <hupu@transsion.com>
> >>>> Signed-off-by: Hongyan Xia <hongyan.xia@transsion.com>
> >>>> ---
> >>>> arch/arm64/kernel/probes/kprobes.c | 14 ++++++++++++++
> >>>> 1 file changed, 14 insertions(+)
> >>>>
> >>>> diff --git a/arch/arm64/kernel/probes/kprobes.c b/arch/arm64/kernel/probes/kprobes.c
> >>>> index 43a0361a8bf0..e4d2852ce2fb 100644
> >>>> --- a/arch/arm64/kernel/probes/kprobes.c
> >>>> +++ b/arch/arm64/kernel/probes/kprobes.c
> >>>> @@ -285,6 +285,20 @@ int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)
> >>>> switch (kcb->kprobe_status) {
> >>>> case KPROBE_HIT_SS:
> >>>> case KPROBE_REENTER:
> >>>> + /*
> >>>> + * A fault taken while a kprobe is single-stepping is not
> >>>> + * necessarily caused by the instruction in the XOL slot. For
> >>>> + * example, tracing or perf code running in this window may take
> >>>> + * an unrelated fault.
> >>>> + *
> >>>> + * Handle the fault here only when the faulting PC is the XOL
> >>>> + * instruction of the current kprobe. Otherwise let the normal
> >>>> + * fault handling path deal with it.
> >>>> + */
> >>>> + if (cur->ainsn.xol_insn &&
> >>>> + instruction_pointer(regs) != (unsigned long)cur->ainsn.xol_insn)
> >>>> + break;
> >>>
> >>> Can you check Sashiko's comments[1]?
> >>>
> >>> [1] https://sashiko.dev/#/patchset/20260706083636.159883-1-hupu%40transsion.com?part=1
> >>>
> >>> It seems that it complains about simulated kprobe's case.
> >>> In that case, cur->ainsn.xol_insn == NULL. The simulation should be done
> >>> in the kprobe context (which is a debug trap). I'm not sure the arm64
> >>> can cause NMI in that context, but if it happens and causes a fault,
> >>> it may cause a problem.
> >>>
> >>> So I think we can just ignore the fault on the simulated kprobes.
> >>>
> >>> To ensure that, you can just add:
> >>>
> >>> if (cur && !cur->ainsn.xol_insn)
> >>> return 0;
> >>>
> >>> at the entry of this function. (and remove redundant cur->ainsn.xol_insn check)
> >>
> >> Right, both cases:
> >>
> >> 1. single-step XOL
> >> 2. simulated
> >>
> >> have this problem and this patch fixed 1. 2 remains unchanged.
> >>
> >> Ideally we should fix both, but the simulated case seems more
> >> complicated, and at least we didn't make things worse for 2. So I wonder
> >> if we can analyze 2 more thoroughly and fix it in a separate patch.
> >
> > OK, but basically this fault handler is only for the SS XOL,
> > not for simulated one (as same as x86). So just skip the
> > simulated case is enough in this patch.
> > (Note that x86 also have simulated path, and that is not handled
> > by the fault handler)
>
> Hmm, what happens if the original PC is a simulated instruction that has
> a recoverable ex_table entry that handles potential page fault,
That should never happen. BPF trampoline code may populate extable
entries, but kprobe doesn't. For the simulated instruction, as far
as I can see, there are 2 operations
- simulate_ldr_literal
- simulate_ldrsw_literal
will involve the memory access and both are accessing PC-relative
kernel data, which should be mapped.
load_addr = addr + ldr_displacement(opcode);
...
set_x_reg(regs, xn, READ_ONCE(*(u64 *)load_addr));
this directly accessing the memory without using extable.
> and this
> PC also has an attached kprobe?
You meant that putting kprobes in simulate_* functions?
Those have __kprobes attribute, so kprobe can not probe it.
(It should use NOKPROBE_SYMBOL...)
> Then the simulated case should be able
> to enter kprobe_fault_handler()?
>
> .ex_table:
> insn foo, handler bar
>
> foo:
> LDR symbol # Load PC-relative. Simulated. Kprobe attached.
> ret
>
> When foo is called and the kprobe fires, it enters simulated path but
> then LDR triggers a page fault. It then enters kprobe_fault_handler() to
> fixup the PC. Then the fixup_exception() of the normal page fault finds
> the ex_table entry and this case is successfully handled?
To do that, you need to update the simulate_ldr* to use uaccess API
(_ASM_EXTABLE_UACCESS* macros) AND allow kprobes to probe those functions.
>
> Looks like this handler can be entered by simulated instructions? Or
> this is only theoretical and whoever arrange code like this should be
> fired immediately?
So the simulated instructions never cause fault, or if it causes a fault
that means the original code has a bug. (Of course there is room to
improve the bug message so that it decodes the address probed by kprobe
when a bug occurs.)
Thank you,
--
Masami Hiramatsu (Google) <mhiramat@kernel.org>
^ permalink raw reply
* Re: [RFC 1/3] arm64: kprobes: Do not handle non-XOL faults as kprobe faults
From: Hongyan Xia @ 2026-07-08 12:12 UTC (permalink / raw)
To: Masami Hiramatsu (Google)
Cc: Pu Hu, ada.coupriediaz@arm.com, catalin.marinas@arm.com,
davem@davemloft.net, Jiazi Li,
linux-arm-kernel@lists.infradead.org,
linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org,
naveen@kernel.org, will@kernel.org, yang@os.amperecomputing.com
In-Reply-To: <20260708205947.4af54d7638f115e915e9c7ee@kernel.org>
On 7/8/2026 7:59 PM, Masami Hiramatsu wrote:
> On Wed, 8 Jul 2026 08:55:37 +0000
> Hongyan Xia <hongyan.xia@transsion.com> wrote:
>
>> On 7/8/2026 3:42 PM, Masami Hiramatsu wrote:
>>> On Wed, 8 Jul 2026 05:57:24 +0000
>>> Hongyan Xia <hongyan.xia@transsion.com> wrote:
>>>
>>>> Hi Masami,
>>>>
>>>> On 7/8/2026 8:46 AM, Masami Hiramatsu wrote:
>>>>> On Mon, 6 Jul 2026 08:36:48 +0000
>>>>> Pu Hu <hupu@transsion.com> wrote:
>>>>>
>>>>>> From: Pu Hu <hupu@transsion.com>
>>>>>>
>>>>>> kprobe_fault_handler() handles faults taken while kprobes is in
>>>>>> KPROBE_HIT_SS or KPROBE_REENTER state as faults caused by the
>>>>>> single-stepped instruction.
>>>>>>
>>>>>> That assumption is not always true. While a kprobe is preparing or
>>>>>> executing the out-of-line single-step instruction, other code may run
>>>>>> in that window. For example, perf or trace code can be invoked from the
>>>>>> debug exception path and may take a fault of its own. In that case the
>>>>>> fault did not happen on the kprobe XOL instruction, but the kprobe fault
>>>>>> handler may still try to recover it as a kprobe single-step fault.
>>>>>>
>>>>>> This can corrupt the exception recovery flow and leave the real fault to
>>>>>> be handled with a wrong PC. A typical reproducer is running simpleperf
>>>>>> with preemptirq tracepoints and dwarf callchains while a kprobe is
>>>>>> installed on a frequently executed kernel function.
>>>>>>
>>>>>> Fix this by handling faults in KPROBE_HIT_SS/KPROBE_REENTER only when
>>>>>> the faulting PC points at the current kprobe's XOL instruction. Faults
>>>>>> from any other PC are left to the normal fault handling path.
>>>>>>
>>>>>> This follows the same idea as the x86 fix in commit 6381c24cd6d5
>>>>>> ("kprobes/x86: Fix page-fault handling logic").
>>>>>>
>>>>>> Signed-off-by: Pu Hu <hupu@transsion.com>
>>>>>> Signed-off-by: Hongyan Xia <hongyan.xia@transsion.com>
>>>>>> ---
>>>>>> arch/arm64/kernel/probes/kprobes.c | 14 ++++++++++++++
>>>>>> 1 file changed, 14 insertions(+)
>>>>>>
>>>>>> diff --git a/arch/arm64/kernel/probes/kprobes.c b/arch/arm64/kernel/probes/kprobes.c
>>>>>> index 43a0361a8bf0..e4d2852ce2fb 100644
>>>>>> --- a/arch/arm64/kernel/probes/kprobes.c
>>>>>> +++ b/arch/arm64/kernel/probes/kprobes.c
>>>>>> @@ -285,6 +285,20 @@ int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)
>>>>>> switch (kcb->kprobe_status) {
>>>>>> case KPROBE_HIT_SS:
>>>>>> case KPROBE_REENTER:
>>>>>> + /*
>>>>>> + * A fault taken while a kprobe is single-stepping is not
>>>>>> + * necessarily caused by the instruction in the XOL slot. For
>>>>>> + * example, tracing or perf code running in this window may take
>>>>>> + * an unrelated fault.
>>>>>> + *
>>>>>> + * Handle the fault here only when the faulting PC is the XOL
>>>>>> + * instruction of the current kprobe. Otherwise let the normal
>>>>>> + * fault handling path deal with it.
>>>>>> + */
>>>>>> + if (cur->ainsn.xol_insn &&
>>>>>> + instruction_pointer(regs) != (unsigned long)cur->ainsn.xol_insn)
>>>>>> + break;
>>>>>
>>>>> Can you check Sashiko's comments[1]?
>>>>>
>>>>> [1] https://sashiko.dev/#/patchset/20260706083636.159883-1-hupu%40transsion.com?part=1
>>>>>
>>>>> It seems that it complains about simulated kprobe's case.
>>>>> In that case, cur->ainsn.xol_insn == NULL. The simulation should be done
>>>>> in the kprobe context (which is a debug trap). I'm not sure the arm64
>>>>> can cause NMI in that context, but if it happens and causes a fault,
>>>>> it may cause a problem.
>>>>>
>>>>> So I think we can just ignore the fault on the simulated kprobes.
>>>>>
>>>>> To ensure that, you can just add:
>>>>>
>>>>> if (cur && !cur->ainsn.xol_insn)
>>>>> return 0;
>>>>>
>>>>> at the entry of this function. (and remove redundant cur->ainsn.xol_insn check)
>>>>
>>>> Right, both cases:
>>>>
>>>> 1. single-step XOL
>>>> 2. simulated
>>>>
>>>> have this problem and this patch fixed 1. 2 remains unchanged.
>>>>
>>>> Ideally we should fix both, but the simulated case seems more
>>>> complicated, and at least we didn't make things worse for 2. So I wonder
>>>> if we can analyze 2 more thoroughly and fix it in a separate patch.
>>>
>>> OK, but basically this fault handler is only for the SS XOL,
>>> not for simulated one (as same as x86). So just skip the
>>> simulated case is enough in this patch.
>>> (Note that x86 also have simulated path, and that is not handled
>>> by the fault handler)
>>
>> Hmm, what happens if the original PC is a simulated instruction that has
>> a recoverable ex_table entry that handles potential page fault,
>
> That should never happen. BPF trampoline code may populate extable
> entries, but kprobe doesn't. For the simulated instruction, as far
> as I can see, there are 2 operations
> - simulate_ldr_literal
> - simulate_ldrsw_literal
> will involve the memory access and both are accessing PC-relative
> kernel data, which should be mapped.
>
> load_addr = addr + ldr_displacement(opcode);
> ...
> set_x_reg(regs, xn, READ_ONCE(*(u64 *)load_addr));
>
> this directly accessing the memory without using extable.
>
>
>> and this
>> PC also has an attached kprobe?
>
> You meant that putting kprobes in simulate_* functions?
> Those have __kprobes attribute, so kprobe can not probe it.
> (It should use NOKPROBE_SYMBOL...)
>
>> Then the simulated case should be able
>> to enter kprobe_fault_handler()?
>>
>> .ex_table:
>> insn foo, handler bar
>>
>> foo:
>> LDR symbol # Load PC-relative. Simulated. Kprobe attached.
>> ret
>>
>> When foo is called and the kprobe fires, it enters simulated path but
>> then LDR triggers a page fault. It then enters kprobe_fault_handler() to
>> fixup the PC. Then the fixup_exception() of the normal page fault finds
>> the ex_table entry and this case is successfully handled?
>
> To do that, you need to update the simulate_ldr* to use uaccess API
> (_ASM_EXTABLE_UACCESS* macros) AND allow kprobes to probe those functions.
I see. The kprobe code actually does not even allow this combination.
The arm64 arch_prepare_kprobe() will reject extable PCs. Sorry for the
noise.
>>
>> Looks like this handler can be entered by simulated instructions? Or
>> this is only theoretical and whoever arrange code like this should be
>> fired immediately?
>
> So the simulated instructions never cause fault, or if it causes a fault
> that means the original code has a bug. (Of course there is room to
> improve the bug message so that it decodes the address probed by kprobe
> when a bug occurs.)
If we rule out the edge case I came up with (which arm64 kprobe does not
even allow to happen), then other cases are indeed kernel bugs.
Thanks for walking us through some of the logic. Will fix things in v2.
^ permalink raw reply
* [BUG] tracing: Too many tries to read user space
From: Jeongho Choi @ 2026-07-08 12:37 UTC (permalink / raw)
To: linux-trace-kernel, linux-kernel, rostedt, mhiramat
Cc: ji2yoon.jo, minki.jang, hajun.sung
In-Reply-To: <CGME20260708123754epcas2p1f15cc305ddb09f97164491d750769ef7@epcas2p1.samsung.com>
[-- Attachment #1: Type: text/plain, Size: 3535 bytes --]
Hello,
We are seeing a reproducible kernel panic related to the tracing code
when it fails to read user-space memory.
The issue was originally reported through the Android/Google Issue
Tracker, and we were advised to report it to the upstream trace mailing
list because the affected code is upstream.
Environment:
Architecture: arm64
Kernel: Linux 6.18.21
Base: Android Common Kernel (android17-6.18)
Affected area: kernel/trace/
The relevant error/panic log is:
[48916.569148] [9: lmkd: 536] Error: Too many tries to read
user space
[48916.569156] [9: lmkd: 536] WARNING: CPU: 9 PID: 536 at
kernel/trace/trace.c:7374 trace_user_fault_read+0x334/0x360
[48916.569443] [9: lmkd: 536] CPU: 9 UID: 1069 PID: 536 Comm:
lmkd Tainted: G OE 6.18.21-android17-5-ga1a8e8cab9ec-4k
#1 PREEMPT 25372cd4750dcac3c0fe86b57d47c665f97a6046
[48916.569450] [9: lmkd: 536] pstate: 63402005 (nZCv daif
+PAN -UAO +TCO +DIT -SSBS BTYPE=--)
[48916.569452] [9: lmkd: 536] pc :
trace_user_fault_read+0x334/0x360
[48916.569454] [9: lmkd: 536] lr :
trace_user_fault_read+0x330/0x360
[48916.569456] [9: lmkd: 536] sp : ffffffc096993cc0
[48916.569457] [9: lmkd: 536] x29: ffffffc096993cd0 x28:
0000000000000065 x27: ffffff8817e45200
[48916.569461] [9: lmkd: 536] x26: 0000000000000000 x25:
000000011616c082 x24: 0000000000000000
[48916.569463] [9: lmkd: 536] x23: 0000000000000009 x22:
000000000000002b x21: ffffff880597f740
[48916.569466] [9: lmkd: 536] x20: 0000007fc42dd940 x19:
ffffff8817e45770 x18: ffffffd5947ce240
[48916.569468] [9: lmkd: 536] x17: 2073656972742079 x16:
6e616d206f6f5420 x15: 3a726f727245205d
[48916.569471] [9: lmkd: 536] x14: 36333520203a646b x13:
6563617073207265 x12: 0000000000000001
[48916.569473] [9: lmkd: 536] x11: 6f74207365697274 x10:
0000000000000001 x9 : 4bd9ad4516d75100
[48916.569476] [9: lmkd: 536] x8 : 4bd9ad4516d75100 x7 :
205d383431393635 x6 : 2e36313938345b0a
[48916.569479] [9: lmkd: 536] x5 : ffffffc080fa5998 x4 :
ffffffd591707202 x3 : 0001360a00000000
[48916.569481] [9: lmkd: 536] x2 : ffffffc096993af4 x1 :
00000000000000c0 x0 : 0000000000000028
[48916.569484] [9: lmkd: 536] Call trace:
[48916.569486] [9: lmkd: 536]
trace_user_fault_read+0x334/0x360 (P)
[48916.569488] [9: lmkd: 536] tracing_mark_write+0x84/0x174
[48916.569491] [9: lmkd: 536] __arm64_sys_write+0x2a0/0x5c0
[48916.569494] [9: lmkd: 536] invoke_syscall+0x58/0xe4
[48916.569498] [9: lmkd: 536] do_el0_svc+0x48/0xdc
[48916.569500] [9: lmkd: 536] el0_svc+0x3c/0x98
[48916.569503] [9: lmkd: 536]
el0t_64_sync_handler+0x20/0x130
[48916.569505] [9: lmkd: 536] el0t_64_sync+0x1c4/0x1c8
[48916.569508] [9: lmkd: 536] Kernel panic - not syncing:
kernel: panic_on_warn set ...
The code at the WARN location mentioned in the log above is as follows.
7374 if (WARN_ONCE(trys++ > 100, "Error: Too many
tries to read user space"))
7375 return NULL;
Our current analysis is as follows:
In the Gmail process, during a low memory situation, LMKD writes strings
to /sys/kernel/tracing/trace_marker for systrace recording. At the same
time, it broadcasts a sigkill due to low memory, which is causing the
LMKD trace marker operation to stall.
Please review this issue.
Thanks,
Jeongho Choi
[-- Attachment #2: Type: text/plain, Size: 0 bytes --]
^ permalink raw reply
* Re: [PATCH v3 04/11] arm64/mm: Add set_memory_device() and set_memory_normal()
From: Thierry Reding @ 2026-07-08 12:50 UTC (permalink / raw)
To: Will Deacon
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Jonathan Hunter,
David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Sowjanya Komatineni, Luca Ceresoli,
Mikko Perttunen, Yury Norov, Rasmus Villemoes, Russell King,
Alexander Gordeev, Gerald Schaefer, Heiko Carstens, Vasily Gorbik,
Christian Borntraeger, Sven Schnelle, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Marek Szyprowski, Robin Murphy, Sumit Semwal, Benjamin Gaignard,
Brian Starkey, John Stultz, T.J. Mercier, Christian König,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Catalin Marinas, Thierry Reding, devicetree, linux-tegra,
linux-kernel, dri-devel, linux-media, linux-arm-kernel,
linux-s390, linux-mm, iommu, linaro-mm-sig, linux-trace-kernel,
Thierry Reding, Chun Ng
In-Reply-To: <akzikTrmhMsvkNVY@willie-the-truck>
[-- Attachment #1: Type: text/plain, Size: 6397 bytes --]
On Tue, Jul 07, 2026 at 12:27:13PM +0100, Will Deacon wrote:
> On Mon, Jul 06, 2026 at 03:49:24PM +0200, Thierry Reding wrote:
> > On Fri, Jul 03, 2026 at 06:13:31PM +0100, Will Deacon wrote:
> > > On Thu, Jul 02, 2026 at 06:41:23PM +0200, Thierry Reding wrote:
> > > > On Thu, Jul 02, 2026 at 03:46:44PM +0200, Thierry Reding wrote:
> > > > > On Thu, Jul 02, 2026 at 10:18:47AM +0100, Will Deacon wrote:
> > > > > > On Wed, Jul 01, 2026 at 06:08:15PM +0200, Thierry Reding wrote:
> > > > > > > From: Chun Ng <chunn@nvidia.com>
> > > > > > >
> > > > > > > Add helpers to swap PROT_NORMAL and PROT_DEVICE_nGnRnE protection bits
> > > > > > > on a kernel-linear-map range.
> > > > > >
> > > > > > That sounds like a really terrible idea. Why is this necessary and how
> > > > > > does it interact with things like load_unaligned_zeropad()?
> > > > >
> > > > > This is necessary because once the memory controller has walled off the
> > > > > new memory region the CPU must not access it under any circumstances or
> > > > > it'll cause the CPU to lock up (I think technically it'll hit an SError
> > > > > but in practice that just means it'll freeze, as far as I can tell).
> > > > >
> > > > > Probably doesn't interact well at all with load_unaligned_zeropad().
> > > > >
> > > > > > I think you should unmap the memory from the linear map and memremap()
> > > > > > it instead.
> > > > >
> > > > > Given that the memory can never be accessed by the CPU after the memory
> > > > > controller locks it down, I don't think we'll even need memremap(). The
> > > > > only thing we really need is the sg_table we hand out via the DMA BUFs
> > > > > so that they can be used by device drivers to program their DMA engines
> > > > > internally.
> > > > >
> > > > > Looking through some of the architecture code around this, shouldn't we
> > > > > simply be using set_memory_encrypted() and set_memory_decrypted() for
> > > > > this? While they might've been created for slightly other use-cases,
> > > > > they seem to be doing exactly what we want (i.e. remove the page range
> > > > > from the linear mapping and flushing it, or restoring the valid bit and
> > > > > standard permissions, respectively).
> > > >
> > > > Ah... I guess we can't do it because we're not in a realm world and so
> > > > the early checks in __set_memory_enc_dec() would return early and turn
> > > > it into a no-op.
> > > >
> > > > How about if I extract a common helper and provide set_memory_p() and
> > > > set_memory_np() in terms of those. Those are available on x86 and
> > > > PowerPC as well, so fairly standard. I suppose at that point we're
> > > > closer to set_memory_valid().
> > >
> > > Why not just call set_direct_map_invalid_noflush() +
> > > flush_tlb_kernel_range() for each page? We already have APIs for this.
> >
> > Having a "standard" helper with a fixed and documented purposed seemed
> > like a preferable approach for this particular case. We also may want to
> > make the driver that uses this buildable as a module, in which case we'd
> > need to export these rather low-level APIs. And then there's also the
> > fact that we typically call this on a rather large region of memory
> > (usually something like 512 MiB), so doing it page-by-page is rather
> > suboptimal.
> >
> > > The big challenge I see with any linear map manipulation, however, is
> > > that it will rely on can_set_direct_map() which likely means you need to
> > > give up some performance and/or security to make this work. Does memory
> > > become inaccesible dynamically at runtime? If not, the best bet would
> > > be to describe it as a carveout in the DT and mark it as "no-map" so
> > > we avoid mapping it in the first place.
> >
> > VPR exists in two modes: static and resizable. For static VPR we do
> > exactly that: describe it as carveout in DT with no-map and deal with it
> > accordingly in the driver. Resizable VPR is for device that have small
> > amounts of RAM. Content-protected video playback will in the worst case
> > consume around 1.8 GiB of RAM, so we want to be able to reuse for other
> > purposes when VPR is unused on those devices. In that case, the memory
> > is also described as a reserved-memory region in DT, but it is marked as
> > reusable so that it can be managed by CMA.
> >
> > The resize operation is fairly slow to begin with because we need to
> > stall the GPU and put it into reset before the operation, then take it
> > out of reset and resume it afterwards.
> >
> > What kind of performance impact do you expect?
>
> You'll need to measure it, but we've seen reports of double-digit
> percentage regressions in performance and power. As I said, the problem
> is that you need to split the linear map to 4k page at runtime to unmap
> the dynamic carveout, but that isn't something that can be done on most
> CPUs. Therefore you end up having to use page-granular mappings for the
> entire thing, similarly to how 'rodata_full' drives can_set_direct_map()
> and the perf/power hit affects everything.
>
> It's hard to know what to suggest... I wonder if any of the memory
> hotplug logic could help here?
I've read up on memory hotplug a bit and it sounds like it could fit
this really nicely. Given that we only use CMA (along with the extra
patches to it) to make sure that any buffers are reclaimed for VPR use,
we should be able to get rid of the CMA usage altogether and replace it
with online_pages() and offline_pages() instead. Rather than using a
fixed set of CMA areas like we currently do, each "chunk" in the VPR
driver could represent a memory block instead (which looks like it will
be 128 MiB for 4 KiB pages and 512 MiB for 64 KiB pages). We currently
use 512 MiB as the chunk size, so it should be relatively similar and
easy to adjust.
One issue that we would absolutely need this memory to be ZONE_MOVABLE
from the start. Using no-map in DT and then online_pages() probably will
not work because there's no struct page for the memory. So we're left
with keeping the memory onlined by default, in which case we'd need some
way for DT to instruct the memory to be put into ZONE_MOVABLE always.
There's a "hotpluggable" property for "memory" nodes, maybe that can be
extended to apply to reserved-memory nodes as well?
Thierry
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply
* Re: [BUG] tracing: Too many tries to read user space
From: Steven Rostedt @ 2026-07-08 13:18 UTC (permalink / raw)
To: Jeongho Choi
Cc: linux-trace-kernel, linux-kernel, mhiramat, ji2yoon.jo,
minki.jang, hajun.sung
In-Reply-To: <20260708123753.GB1386@KORCO121415.samsungds.net>
On Wed, 8 Jul 2026 21:37:53 +0900
Jeongho Choi <jh1012.choi@samsung.com> wrote:
> The code at the WARN location mentioned in the log above is as follows.
>
> 7374 if (WARN_ONCE(trys++ > 100, "Error: Too many
> tries to read user space"))
> 7375 return NULL;
>
This happens when something forces a schedule.
>
> Our current analysis is as follows:
>
> In the Gmail process, during a low memory situation, LMKD writes strings
> to /sys/kernel/tracing/trace_marker for systrace recording. At the same
> time, it broadcasts a sigkill due to low memory, which is causing the
> LMKD trace marker operation to stall.
>
Can you see what is being scheduled in? Perhaps use the persistent ring
buffer (if you can) and enable sched_switch tracepoint in it.
The loop is this:
do {
/*
* It is possible that something is trying to migrate this
* task. What happens then, is when preemption is enabled,
* the migration thread will preempt this task, try to
* migrate it, fail, then let it run again. That will
* cause this to loop again and never succeed.
* On failures, enabled and disable preemption with
* migration enabled, to allow the migration thread to
* migrate this task.
*/
if (trys) {
preempt_enable_notrace();
preempt_disable_notrace();
cpu = smp_processor_id();
buffer = per_cpu_ptr(tinfo->tbuf, cpu)->buf;
}
/*
* If for some reason, copy_from_user() always causes a context
* switch, this would then cause an infinite loop.
* If this task is preempted by another user space task, it
* will cause this task to try again. But just in case something
* changes where the copying from user space causes another task
* to run, prevent this from going into an infinite loop.
* 100 tries should be plenty.
*/
if (WARN_ONCE(trys++ > 100, "Error: Too many tries to read user space"))
return NULL;
/* Read the current CPU context switch counter */
cnt = nr_context_switches_cpu(cpu);
/*
* Preemption is going to be enabled, but this task must
* remain on this CPU.
*/
migrate_disable();
/*
* Now preemption is being enabled and another task can come in
* and use the same buffer and corrupt our data.
*/
preempt_enable_notrace();
/* Make sure preemption is enabled here */
lockdep_assert_preemption_enabled();
if (copy_func) {
ret = copy_func(buffer, ptr, size, data);
} else {
ret = __copy_from_user(buffer, ptr, size);
}
preempt_disable_notrace();
migrate_enable();
/* if it faulted, no need to test if the buffer was corrupted */
if (ret)
return NULL;
/*
* Preemption is disabled again, now check the per CPU context
* switch counter. If it doesn't match, then another user space
* process may have schedule in and corrupted our buffer. In that
* case the copying must be retried.
*/
} while (nr_context_switches_cpu(cpu) != cnt);
What it does is to write into a per CPU buffer from user space. If it
schedules out while trying this, it will try again as another task could
have come in and corrupted the buffer.
For some reason, when preemption is enabled to copy from user, something
is forcing a schedule of the process. If we can figure out what is doing
that, perhaps we can fix this. A similar thing happened with the migration
thread that commit edca33a56297d ("tracing: Fix failure to read user space
from system call trace events") fixed.
-- Steve
^ permalink raw reply
* [PATCH v1] tracing/remotes: Fix leak in trace_remote_alloc_buffer() error path
From: Vincent Donnefort @ 2026-07-08 13:32 UTC (permalink / raw)
To: rostedt, mhiramat, mathieu.desnoyers, linux-trace-kernel
Cc: kernel-team, linux-kernel, Vincent Donnefort, Sashiko
If page allocation fails in trace_remote_alloc_buffer(), desc->nr_cpus
is not yet incremented for the current CPU. As a consequence, on error,
half-allocated rb_desc will not be freed in trace_remote_free_buffer().
Include the failing CPU in desc->nr_cpus before going to the error path.
Fixes: 96e43537af54 ("tracing: Introduce trace remotes")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Signed-off-by: Vincent Donnefort <vdonnefort@google.com>
diff --git a/kernel/trace/trace_remote.c b/kernel/trace/trace_remote.c
index 2a6cc000ec98..62d3d431c309 100644
--- a/kernel/trace/trace_remote.c
+++ b/kernel/trace/trace_remote.c
@@ -1008,8 +1008,10 @@ int trace_remote_alloc_buffer(struct trace_buffer_desc *desc, size_t desc_size,
for (id = 0; id < nr_pages; id++) {
rb_desc->page_va[id] = (unsigned long)__get_free_page(GFP_KERNEL);
- if (!rb_desc->page_va[id])
+ if (!rb_desc->page_va[id]) {
+ desc->nr_cpus++; /* Free this partially-allocated rb_desc */
goto err;
+ }
rb_desc->nr_page_va++;
}
base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda
--
2.55.0.795.g602f6c329a-goog
^ permalink raw reply related
* Re: [RFC 2/3] arm64: kprobes: Allow reentering kprobes while single-stepping
From: Masami Hiramatsu @ 2026-07-08 14:12 UTC (permalink / raw)
To: Hongyan Xia
Cc: Pu Hu, ada.coupriediaz@arm.com, catalin.marinas@arm.com,
davem@davemloft.net, Jiazi Li,
linux-arm-kernel@lists.infradead.org,
linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org,
naveen@kernel.org, will@kernel.org, yang@os.amperecomputing.com
In-Reply-To: <68189403-9728-4938-8ee2-924e4702eab7@transsion.com>
On Wed, 8 Jul 2026 07:12:39 +0000
Hongyan Xia <hongyan.xia@transsion.com> wrote:
> On 7/8/2026 8:54 AM, Masami Hiramatsu wrote:
> > On Mon, 6 Jul 2026 08:36:49 +0000
> > Pu Hu <hupu@transsion.com> wrote:
> >
> >> From: Pu Hu <hupu@transsion.com>
> >>
> >> A kprobe can be hit while another kprobe is in KPROBE_HIT_SS state. This
> >> can happen when tracing or perf code runs from the debug exception path
> >> while the first kprobe is preparing or executing its out-of-line
> >> single-step instruction.
> >>
> >> Currently arm64 treats a kprobe hit in KPROBE_HIT_SS as unrecoverable,
> >> the same as a hit in KPROBE_REENTER. This is too strict. A hit in
> >> KPROBE_HIT_SS is still a one-level reentry and can be handled by saving
> >> the current kprobe state and setting up single-step for the new probe,
> >> just like reentry from KPROBE_HIT_ACTIVE or KPROBE_HIT_SSDONE.
> >>
> >> The truly unrecoverable case is hitting another kprobe while already in
> >> KPROBE_REENTER, because the reentry save area has already been consumed.
> >>
> >> Move KPROBE_HIT_SS to the recoverable reentry cases and leave
> >> KPROBE_REENTER as the unrecoverable nested reentry case.
> >>
> >> This mirrors the x86 fix in commit 6a5022a56ac3
> >> ("kprobes/x86: Allow to handle reentered kprobe on single-stepping").
> >
> > Can you also check the Sashiko comment?
> >
> > https://sashiko.dev/#/patchset/20260706083636.159883-1-hupu%40transsion.com?part=2
> >
> > This seems indicating potentially brakage of reenter kprobes on arm64.
> > But is it possible to hit another kprobe while SS on arm64? It is
> > the same question about the previous one, can NMI happens during
> > the single stepping? (maybe yes, because it is non-maskable)
>
> It is possible as this is what we hit. There are
> preempt_enable/disable() calls during SS which can trigger perf events
> sampling the user stack, which may then trigger page faults and send the
> CPU into one more level of exception context. I believe this is what you
> saw in 6a5022a56ac3?
Ah, it was more generic perf NMI case, not only preempt_enable/disable().
But anyway, the result is same.
> > Anyway, for making it safer, we need to add saved_irqflags to prev_kprobe.
>
> Yes, this seems to be a legit bug that needs to be fixed. Thank you.
OK, thanks!
--
Masami Hiramatsu (Google) <mhiramat@kernel.org>
^ permalink raw reply
* [PATCH v4 1/8] rv/da: introduce DA_MON_ALLOCATION_STRATEGY
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
Per-object DA storage allocation is currently limited to kmalloc on
demand. Add a compile-time selector so monitors can choose among three
strategies:
DA_ALLOC_AUTO (default) - kmalloc per object on the monitor path
DA_ALLOC_POOL - pre-allocated fixed-size llist pool;
selected by defining DA_MON_POOL_SIZE
DA_ALLOC_MANUAL - caller pre-inserts storage; framework
only links the target field
The pool strategy uses a lock-free llist (cmpxchg, no spinlock) so
pool release is safe from RCU callback context without acquiring a
lock. Moving allocation before the measurement window also prevents
kmalloc latency.
nomiss is updated to DA_ALLOC_MANUAL.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/da_monitor.h | 247 +++++++++++++++++++----
include/rv/ha_monitor.h | 6 +
kernel/trace/rv/monitors/nomiss/nomiss.c | 6 +-
3 files changed, 221 insertions(+), 38 deletions(-)
diff --git a/include/rv/da_monitor.h b/include/rv/da_monitor.h
index 34b8fba9ecd4..9c9acc123e3b 100644
--- a/include/rv/da_monitor.h
+++ b/include/rv/da_monitor.h
@@ -14,7 +14,56 @@
#ifndef _RV_DA_MONITOR_H
#define _RV_DA_MONITOR_H
+/*
+ * Allocation strategies for RV_MON_PER_OBJ monitors.
+ *
+ * Select the strategy with a single define before including this header:
+ *
+ * #define DA_MON_POOL_SIZE N - pool mode; N pre-allocated slots.
+ * Implies DA_ALLOC_POOL automatically.
+ * #define DA_MON_ALLOCATION_STRATEGY \
+ * DA_ALLOC_MANUAL - manual mode (see below).
+ * (neither) - auto mode (default).
+ *
+ * Do not define both DA_MON_POOL_SIZE and DA_MON_ALLOCATION_STRATEGY.
+ *
+ * DA_ALLOC_AUTO - lock-free kmalloc on the hot path; unbounded capacity.
+ * DA_ALLOC_POOL - pre-allocated fixed-size pool; set by defining DA_MON_POOL_SIZE.
+ * DA_ALLOC_MANUAL - caller inserts storage before da_handle_start_event();
+ * the framework only links the target field.
+ */
+#define DA_ALLOC_AUTO 0
+#define DA_ALLOC_POOL 1
+#define DA_ALLOC_MANUAL 2
+
+#ifdef DA_MON_POOL_SIZE
+#ifdef DA_MON_ALLOCATION_STRATEGY
+#error "Define only one of DA_MON_POOL_SIZE or DA_MON_ALLOCATION_STRATEGY"
+#endif
+#if DA_MON_POOL_SIZE == 0
+#error "DA_MON_POOL_SIZE must be non-zero"
+#endif
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL
+#endif
+
+#ifndef DA_MON_ALLOCATION_STRATEGY
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO
+#endif
+
+/*
+ * Provide a zero default so da_monitor_init() can reference
+ * DA_MON_POOL_SIZE in a plain C if() without an #if guard; the
+ * compiler eliminates the dead branch.
+ */
+#ifndef DA_MON_POOL_SIZE
+#if DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL
+#error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be defined and non-zero"
+#endif
+#define DA_MON_POOL_SIZE 0
+#endif
+
#include <rv/automata.h>
+#include <linux/llist.h>
#include <linux/rv.h>
#include <linux/stringify.h>
#include <linux/bug.h>
@@ -66,6 +115,16 @@ static struct rv_monitor rv_this;
#define da_monitor_sync_hook()
#endif
+/*
+ * Per-object teardown hook, called after da_monitor_reset_all() +
+ * da_monitor_sync_hook() and before hash_del_rcu() for each entry.
+ * All HA timer callbacks have completed at this point.
+ * Define before including this header. Default: no-op.
+ */
+#ifndef da_extra_cleanup
+#define da_extra_cleanup(da_mon)
+#endif
+
/*
* Type for the target id, default to int but can be overridden.
* A long type can work as hash table key (PER_OBJ) but will be downgraded to
@@ -404,6 +463,12 @@ struct da_monitor_storage {
union rv_task_monitor rv;
struct hlist_node node;
struct rcu_head rcu;
+ /*
+ * Mutually exclusive with rcu: rcu is live during the RCU callback
+ * flight; free_node when the slot is in da_pool_free_list.
+ * Present in all monitors to avoid #if-gating the pool helpers.
+ */
+ struct llist_node free_node;
};
#ifndef DA_MONITOR_HT_BITS
@@ -495,18 +560,6 @@ static inline da_id_type da_get_id(struct da_monitor *da_mon)
return container_of(da_mon, struct da_monitor_storage, rv.da_mon)->id;
}
-/*
- * da_create_or_get - create the per-object storage if not already there
- *
- * This needs a lookup so should be guarded by RCU, the condition is checked
- * directly in da_create_storage()
- */
-static inline void da_create_or_get(da_id_type id, monitor_target target)
-{
- guard(rcu)();
- da_create_storage(id, target, da_get_monitor(id, target));
-}
-
/*
* da_fill_empty_storage - store the target in a pre-allocated storage
*
@@ -537,15 +590,79 @@ static inline monitor_target da_get_target_by_id(da_id_type id)
return mon_storage->target;
}
+/*
+ * Lock-free llist (cmpxchg) rather than kmem_cache/mempool: on
+ * PREEMPT_RT spinlock_t becomes a sleeping lock, which is forbidden
+ * in the rcuc kthread context where RCU callbacks run.
+ *
+ * Multiple producers (any context, any CPU) call llist_add; a single
+ * consumer (llist_del_first, serialised by the monitor's start lock)
+ * needs no additional synchronisation.
+ *
+ * Per-TU statics: each PER_OBJ monitor gets its own pool instance;
+ * da_pool_storage and da_pool_free_list are NULL/empty and the pool
+ * paths are dead code for non-pool monitors.
+ */
+static struct da_monitor_storage *da_pool_storage;
+static LLIST_HEAD(da_pool_free_list);
+
+static void da_pool_return_cb(struct rcu_head *head)
+{
+ struct da_monitor_storage *ms =
+ container_of(head, struct da_monitor_storage, rcu);
+
+ llist_add(&ms->free_node, &da_pool_free_list);
+}
+
+/*
+ * da_create_pool_storage - pop a free pool slot and insert it into the hash.
+ *
+ * Returns the new da_monitor, or NULL if the pool is exhausted. Finding
+ * an existing entry for the same id fires WARN_ON_ONCE (double-start bug).
+ *
+ * Caller must hold an RCU read-side CS and the monitor's serialisation lock.
+ */
+static inline struct da_monitor *
+da_create_pool_storage(da_id_type id, monitor_target target,
+ struct da_monitor *da_mon)
+{
+ struct da_monitor_storage *mon_storage, *existing;
+ struct llist_node *node;
+
+ if (da_mon)
+ return da_mon;
+
+ node = llist_del_first(&da_pool_free_list);
+ if (!node)
+ return NULL;
+ mon_storage = llist_entry(node, struct da_monitor_storage, free_node);
+
+ mon_storage->id = id;
+ mon_storage->target = target;
+
+ /*
+ * The caller's serialization lock ensures llist_del_first() is
+ * single-consumer, so no concurrent start for the same id is possible.
+ * Reaching here indicates a programming error (double-start for the
+ * same pid).
+ */
+ existing = __da_get_mon_storage(id);
+ if (WARN_ON_ONCE(existing)) {
+ llist_add(&mon_storage->free_node, &da_pool_free_list);
+ return NULL;
+ }
+ hash_add_rcu(da_monitor_ht, &mon_storage->node, id);
+ return &mon_storage->rv.da_mon;
+}
+
/*
* da_destroy_storage - destroy the per-object storage
*
- * The caller is responsible to synchronise writers, either with locks or
- * implicitly. For instance, if da_destroy_storage is called at sched_exit and
- * da_create_storage can never occur after that, it's safe to call this without
- * locks.
- * This function includes an RCU read-side critical section to synchronise
- * against da_monitor_destroy().
+ * Pool mode: removes from hash and returns the slot via call_rcu().
+ * Kmalloc mode: removes from hash and frees via kfree_rcu().
+ *
+ * Includes an RCU read-side critical section to synchronise against
+ * da_monitor_destroy().
*/
static inline void da_destroy_storage(da_id_type id)
{
@@ -558,7 +675,10 @@ static inline void da_destroy_storage(da_id_type id)
return;
da_monitor_reset_hook(&mon_storage->rv.da_mon);
hash_del_rcu(&mon_storage->node);
- kfree_rcu(mon_storage, rcu);
+ if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+ call_rcu(&mon_storage->rcu, da_pool_return_cb);
+ else
+ kfree_rcu(mon_storage, rcu);
}
static void __da_monitor_reset_all(void (*reset)(struct da_monitor *))
@@ -581,41 +701,98 @@ static inline void da_monitor_reset_state_all(void)
__da_monitor_reset_all(da_monitor_reset_state);
}
+/* Not part of the public API; called only by da_monitor_init(). */
+static inline int __da_monitor_init_pool(unsigned int prealloc_count)
+{
+ unsigned int i;
+
+ da_pool_storage = kcalloc(prealloc_count, sizeof(*da_pool_storage),
+ GFP_KERNEL);
+ if (!da_pool_storage)
+ return -ENOMEM;
+
+ for (i = 0; i < prealloc_count; i++)
+ llist_add(&da_pool_storage[i].free_node, &da_pool_free_list);
+ return 0;
+}
+
+/*
+ * da_monitor_init - initialise the per-object monitor
+ */
static inline int da_monitor_init(void)
{
hash_init(da_monitor_ht);
+ if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+ return __da_monitor_init_pool(DA_MON_POOL_SIZE);
return 0;
}
+/*
+ * da_monitor_destroy - tear down the per-object monitor
+ *
+ * tracepoint_synchronize_unregister() flushes all in-flight tracepoint
+ * handlers and performs synchronize_rcu(), so no RCU reader holds a
+ * reference to any da_monitor_storage after it returns.
+ * da_monitor_reset_all() disables monitoring; combined with
+ * da_monitor_sync_hook() (synchronize_rcu() for HA), no timer callback
+ * can fire or be re-armed after this sequence.
+ *
+ * Pool mode: remaining hash entries are returned to the free list
+ * directly (no call_rcu needed -- see above). rcu_barrier() drains any
+ * da_pool_return_cb() callbacks queued by earlier da_destroy_storage()
+ * calls before the backing array is freed.
+ */
static inline void da_monitor_destroy(void)
{
- struct da_monitor_storage *mon_storage;
+ struct da_monitor_storage *ms;
struct hlist_node *tmp;
int bkt;
tracepoint_synchronize_unregister();
da_monitor_reset_all();
da_monitor_sync_hook();
- /*
- * This function is called after all probes are disabled and no longer
- * pending, we can safely assume no concurrent user.
- */
- hash_for_each_safe(da_monitor_ht, bkt, tmp, mon_storage, node) {
- hash_del_rcu(&mon_storage->node);
- kfree(mon_storage);
+
+ hash_for_each_safe(da_monitor_ht, bkt, tmp, ms, node) {
+ da_extra_cleanup(&ms->rv.da_mon);
+ hash_del_rcu(&ms->node);
+ /* No RCU readers remain; skip the grace period. */
+ if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL) {
+ llist_add(&ms->free_node, &da_pool_free_list);
+ } else {
+ kfree(ms);
+ }
+ }
+
+ if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL) {
+ /*
+ * Flush any da_pool_return_cb callbacks queued by
+ * da_destroy_storage() during normal monitor operation.
+ * After rcu_barrier(), no callback can reference pool slots;
+ * the backing array is safe to free.
+ */
+ rcu_barrier();
+ init_llist_head(&da_pool_free_list);
+ kfree(da_pool_storage);
+ da_pool_storage = NULL;
}
}
/*
- * Allow the per-object monitors to run allocation manually, necessary if the
- * start condition is in a context problematic for allocation (e.g. scheduling).
- * In such case, if the storage was pre-allocated without a target, set it now.
+ * da_prepare_storage - allocate or retrieve storage for a monitoring session
+ *
+ * Dispatches to the strategy selected by DA_MON_ALLOCATION_STRATEGY.
+ * Caller must hold an RCU read-side CS.
*/
-#ifdef DA_SKIP_AUTO_ALLOC
-#define da_prepare_storage da_fill_empty_storage
-#else
-#define da_prepare_storage da_create_storage
-#endif /* DA_SKIP_AUTO_ALLOC */
+static inline struct da_monitor *
+da_prepare_storage(da_id_type id, monitor_target target,
+ struct da_monitor *da_mon)
+{
+ if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_POOL)
+ return da_create_pool_storage(id, target, da_mon);
+ if (DA_MON_ALLOCATION_STRATEGY == DA_ALLOC_MANUAL)
+ return da_fill_empty_storage(id, target, da_mon);
+ return da_create_storage(id, target, da_mon);
+}
#endif /* RV_MON_TYPE */
diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h
index 28d3c74cabfc..83199f90afe8 100644
--- a/include/rv/ha_monitor.h
+++ b/include/rv/ha_monitor.h
@@ -365,6 +365,12 @@ static inline bool ha_check_invariant_ns(struct ha_monitor *ha_mon,
}
/*
* ha_invariant_passed_ns - prepare the invariant and return the time since reset
+ *
+ * If the env has not been initialised yet (first entry into a state with an
+ * invariant), anchor the guard clock at the current time so that the full
+ * budget is available from this point. This preserves the documented
+ * guard->invariant ordering: ha_set_invariant_ns() is always preceded by a
+ * valid guard representation in env_store.
*/
static inline u64 ha_invariant_passed_ns(struct ha_monitor *ha_mon, enum envs env,
u64 expire, u64 time_ns)
diff --git a/kernel/trace/rv/monitors/nomiss/nomiss.c b/kernel/trace/rv/monitors/nomiss/nomiss.c
index 8ead8783c29f..ac4d334e757f 100644
--- a/kernel/trace/rv/monitors/nomiss/nomiss.c
+++ b/kernel/trace/rv/monitors/nomiss/nomiss.c
@@ -17,8 +17,8 @@
#define RV_MON_TYPE RV_MON_PER_OBJ
#define HA_TIMER_TYPE HA_TIMER_WHEEL
-/* The start condition is on sched_switch, it's dangerous to allocate there */
-#define DA_SKIP_AUTO_ALLOC
+/* Allocate storage in sched_setscheduler; sched_switch is too hot to alloc. */
+#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL
typedef struct sched_dl_entity *monitor_target;
#include "nomiss.h"
#include <rv/ha_monitor.h>
@@ -214,7 +214,7 @@ static void handle_sys_enter(void *data, struct pt_regs *regs, long id)
if (p->policy == SCHED_DEADLINE)
da_reset(EXPAND_ID_TASK(p));
else if (new_policy == SCHED_DEADLINE)
- da_create_or_get(EXPAND_ID_TASK(p));
+ da_create_empty_storage(get_entity_id(&p->dl, task_cpu(p), DL_TASK));
}
static void handle_sched_wakeup(void *data, struct task_struct *tsk)
--
2.25.1
^ permalink raw reply related
* [PATCH v4 2/8] rv: add generic uprobe infrastructure for RV monitors
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
Monitors that instrument user-space function boundaries need to resolve
paths, register uprobes, and deregister them safely. Provide a thin
wrapper so monitors share a single implementation of this boilerplate.
struct rv_uprobe embeds struct uprobe_consumer directly, avoiding a
separate heap allocation per probe. Embedding is safe after
rv_uprobe_unregister(): rv_uprobe_sync() calls uprobe_unregister_sync()
which performs synchronize_rcu_tasks_trace(), waiting for all
rcu_read_lock_trace() readers (handler_chain()) to complete on all CPUs
before returning; the caller may then free the containing struct.
The API provides register, synchronous and nosync unregister, a global
handler barrier (rv_uprobe_sync), and an active-state predicate.
Handlers receive the uprobe_consumer pointer and recover per-probe state
via container_of(uc, struct rv_uprobe, uc) or the containing struct.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/rv_uprobe.h | 93 ++++++++++++++++++++++++++++++++
kernel/trace/rv/Kconfig | 7 +++
kernel/trace/rv/Makefile | 1 +
kernel/trace/rv/rv_uprobe.c | 104 ++++++++++++++++++++++++++++++++++++
4 files changed, 205 insertions(+)
create mode 100644 include/rv/rv_uprobe.h
create mode 100644 kernel/trace/rv/rv_uprobe.c
diff --git a/include/rv/rv_uprobe.h b/include/rv/rv_uprobe.h
new file mode 100644
index 000000000000..2eab5d193e13
--- /dev/null
+++ b/include/rv/rv_uprobe.h
@@ -0,0 +1,93 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ *
+ */
+
+#ifndef _RV_UPROBE_H
+#define _RV_UPROBE_H
+
+#include <linux/types.h>
+#include <linux/uprobes.h>
+
+struct pt_regs;
+struct inode;
+
+/**
+ * struct rv_uprobe - embeddable uprobe handle for RV monitors
+ *
+ * Embed via DECLARE_RV_UPROBE() in the caller's struct and pass &name to
+ * rv_uprobe_register().
+ *
+ * Lifetime: after rv_uprobe_unregister() (or rv_uprobe_unregister_nosync()
+ * followed by rv_uprobe_sync()) returns, synchronize_rcu_tasks_trace() has
+ * completed and no handler_chain() iteration can reference this struct.
+ * The caller may free the containing struct immediately after.
+ *
+ * @uc: embedded uprobe_consumer; set uc.handler / uc.ret_handler before
+ * calling rv_uprobe_register(); use container_of(uc, rv_uprobe, uc)
+ * inside handlers to reach this struct or its container
+ * @uprobe: registered uprobe pointer (NULL when not registered)
+ * @inode: inode of the probed binary (valid while registered)
+ */
+struct rv_uprobe {
+ struct uprobe_consumer uc;
+ struct uprobe *uprobe;
+ struct inode *inode;
+};
+
+/* Embed a named rv_uprobe inside a caller struct */
+#define DECLARE_RV_UPROBE(name) struct rv_uprobe name
+
+/**
+ * rv_uprobe_is_registered - test whether an uprobe is currently active
+ * @p: probe to test; may be NULL
+ */
+bool rv_uprobe_is_registered(const struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_register - initialise and register an uprobe
+ * @binpath: absolute path to the target binary
+ * @offset: byte offset within the binary
+ * @p: caller-provided rv_uprobe (embedded via DECLARE_RV_UPROBE);
+ * p->uc.handler and/or p->uc.ret_handler must be set before this call
+ *
+ * Resolves the path and registers p->uc with the uprobe subsystem.
+ * No heap allocation is performed.
+ *
+ * Returns 0 on success, negative errno on failure.
+ */
+int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_unregister - synchronously unregister a uprobe
+ * @p: probe to unregister; may be NULL (no-op)
+ *
+ * Removes the consumer from the uprobe subsystem and waits for all in-flight
+ * handlers to complete (via synchronize_rcu_tasks_trace()). After this
+ * returns, the containing struct may be safely freed by the caller.
+ * Use rv_uprobe_unregister_nosync() + rv_uprobe_sync() to batch multiple
+ * deregistrations before a single synchronisation.
+ */
+void rv_uprobe_unregister(struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
+ * @p: probe to dequeue; may be NULL (no-op)
+ *
+ * Removes the consumer without waiting for in-flight handlers. The caller
+ * must call rv_uprobe_sync() before freeing the containing struct.
+ */
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p);
+
+/**
+ * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete
+ *
+ * Global barrier: calls uprobe_unregister_sync() which performs
+ * synchronize_rcu_tasks_trace() + synchronize_srcu(&uretprobes_srcu).
+ * After this returns, no handler_chain() iteration referencing any
+ * previously deregistered consumer is still in progress.
+ */
+void rv_uprobe_sync(void);
+
+#endif /* _RV_UPROBE_H */
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index 3884b14df375..5bad1d63f411 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -59,6 +59,13 @@ config RV_PER_TASK_MONITORS
This option configures the maximum number of per-task RV monitors that can run
simultaneously.
+config RV_UPROBE
+ bool
+ depends on RV && UPROBES
+ help
+ Generic uprobe infrastructure for RV monitors. Provides path
+ resolution, registration, and safe synchronous teardown.
+
source "kernel/trace/rv/monitors/wip/Kconfig"
source "kernel/trace/rv/monitors/wwnr/Kconfig"
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index 94498da35b37..f139b904bea3 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -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
diff --git a/kernel/trace/rv/rv_uprobe.c b/kernel/trace/rv/rv_uprobe.c
new file mode 100644
index 000000000000..a80bfa64578b
--- /dev/null
+++ b/kernel/trace/rv/rv_uprobe.c
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Generic uprobe infrastructure for RV monitors.
+ *
+ * struct rv_uprobe embeds struct uprobe_consumer directly. This is safe
+ * because rv_uprobe_sync() calls uprobe_unregister_sync(), which calls
+ * synchronize_rcu_tasks_trace(). handler_chain() runs under
+ * rcu_read_lock_trace(), so after synchronize_rcu_tasks_trace() returns,
+ * all in-flight handler_chain() iterations, including any pending
+ * uc->cons_node.next reads, have completed on all CPUs. The caller may
+ * then free the struct containing rv_uprobe immediately.
+ */
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/namei.h>
+#include <linux/uprobes.h>
+#include <rv/rv_uprobe.h>
+
+/**
+ * rv_uprobe_register - initialise and register an uprobe
+ */
+int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprobe *p)
+{
+ struct inode *inode;
+ struct path path;
+ int ret;
+
+ if (!p->uc.handler && !p->uc.ret_handler)
+ return -EINVAL;
+
+ ret = kern_path(binpath, LOOKUP_FOLLOW, &path);
+ if (ret)
+ return ret;
+
+ if (!d_is_reg(path.dentry)) {
+ path_put(&path);
+ return -EINVAL;
+ }
+
+ inode = d_real_inode(path.dentry);
+ p->inode = inode;
+
+ /*
+ * uprobe_register() requires the inode (and mount) to remain
+ * referenced across the call. Keep the path alive until after
+ * uprobe_register() has stored its own reference, then release it.
+ */
+ p->uprobe = uprobe_register(inode, offset, 0, &p->uc);
+ path_put(&path);
+ if (IS_ERR(p->uprobe)) {
+ ret = PTR_ERR(p->uprobe);
+ p->uprobe = NULL;
+ p->inode = NULL;
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_register);
+
+/**
+ * rv_uprobe_is_registered - test whether an uprobe is currently active
+ */
+bool rv_uprobe_is_registered(const struct rv_uprobe *p)
+{
+ return p && p->uprobe;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_is_registered);
+
+/**
+ * rv_uprobe_unregister - synchronously unregister a uprobe
+ */
+void rv_uprobe_unregister(struct rv_uprobe *p)
+{
+ if (!p || !p->uprobe)
+ return;
+
+ rv_uprobe_unregister_nosync(p);
+ rv_uprobe_sync();
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister);
+
+/**
+ * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting
+ */
+void rv_uprobe_unregister_nosync(struct rv_uprobe *p)
+{
+ if (!p || !p->uprobe)
+ return;
+
+ uprobe_unregister_nosync(p->uprobe, &p->uc);
+ p->uprobe = NULL;
+ p->inode = NULL;
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_unregister_nosync);
+
+/**
+ * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete
+ */
+void rv_uprobe_sync(void)
+{
+ uprobe_unregister_sync();
+}
+EXPORT_SYMBOL_GPL(rv_uprobe_sync);
--
2.25.1
^ permalink raw reply related
* [PATCH v4 3/8] rv/tlob: add tlob model DOT file
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
Add the Graphviz DOT specification of the tlob hybrid automaton to
tools/verification/models/. The model has three states (running,
waiting, sleeping), four transitions (switch_in, preempt, wakeup,
sleep), and a single clock invariant clk_elapsed < BUDGET_NS() active
in all states.
Suggested-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
tools/verification/models/tlob.dot | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 tools/verification/models/tlob.dot
diff --git a/tools/verification/models/tlob.dot b/tools/verification/models/tlob.dot
new file mode 100644
index 000000000000..a1834daff2ed
--- /dev/null
+++ b/tools/verification/models/tlob.dot
@@ -0,0 +1,22 @@
+digraph state_automaton {
+ center = true;
+ size = "7,11";
+ {node [shape = plaintext, style=invis, label=""] "__init_running"};
+ {node [shape = ellipse] "running"};
+ {node [shape = plaintext] "running"};
+ {node [shape = plaintext] "waiting"};
+ {node [shape = plaintext] "sleeping"};
+ "__init_running" -> "running";
+ "running" -> "running" [ label = "start;reset(clk_elapsed)" ];
+ "running" [label = "running\nclk_elapsed < BUDGET_NS()", color = green3];
+ "waiting" [label = "waiting\nclk_elapsed < BUDGET_NS()"];
+ "sleeping" [label = "sleeping\nclk_elapsed < BUDGET_NS()"];
+ "running" -> "sleeping" [ label = "sleep" ];
+ "running" -> "waiting" [ label = "preempt" ];
+ "waiting" -> "running" [ label = "switch_in" ];
+ "sleeping" -> "waiting" [ label = "wakeup" ];
+ { rank = min ;
+ "__init_running";
+ "running";
+ }
+}
--
2.25.1
^ permalink raw reply related
* [PATCH v4 0/8] rv/tlob: Add task latency over budget RV monitor
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
From: Wen Yang <wen.yang@linux.dev>
This series introduces tlob (task latency over budget), a per-task
hybrid automaton RV monitor that measures elapsed wall-clock time across
a user-delimited code section and emits an error when the elapsed time
exceeds a configurable budget.
The monitor tracks three states (running, waiting, sleeping) driven by
sched_switch and sched_wakeup tracepoints. A single clock invariant,
clk_elapsed < BUDGET_NS(), is enforced by a per-task hrtimer
(HRTIMER_MODE_REL_HARD). On expiry, error_env_tlob is emitted,
followed by detail_env_tlob carrying a per-state time breakdown
(running_ns, waiting_ns, sleeping_ns).
Tasks are registered for monitoring by writing to the tracefs monitor
file:
echo "p /path/to/binary:OFFSET_START OFFSET_STOP threshold=NS" \
> /sys/kernel/tracing/rv/monitors/tlob/monitor
Two uprobes delimit the measured section without modifying the target
binary. Multiple uprobe pairs can be active simultaneously.
Series structure
----------------
Patch 1: rv/da: introduce DA_MON_ALLOCATION_STRATEGY
Compile-time allocation strategy selector for per-object DA storage.
Three strategies: DA_ALLOC_AUTO (default kmalloc), DA_ALLOC_POOL
(pre-allocated llist pool), DA_ALLOC_MANUAL (caller-managed).
Patch 2: rv: add generic uprobe infrastructure for RV monitors
Thin wrapper over uprobe_consumer for path resolution, registration,
and safe synchronous teardown.
Patch 3: rv/tlob: add tlob model DOT file
Graphviz DOT specification of the tlob hybrid automaton.
Patch 4: rv/ha: fix ha_invariant_passed_ns silent bypass
Without this fix, ha_invariant_passed_ns() returns 0 on first call and
leaves env_store at U64_MAX. Every subsequent state transition resets
the hrtimer to the full budget instead of the remaining time, so the
budget never expires for tasks that transition between states. This
causes tlob's detail_sleeping and detail_waiting selftests to hang.
This patch is a minimal fix in the current dual-representation framework.
Nam's series [1] refactors ha_monitor.h to a single-representation
model; once it lands, tlob will need to be updated to the new API and
this patch will be superseded by Gabriele's planned framework-level
initialisation improvement. We carry it here to keep the series
self-contained and testable.
[1] https://lore.kernel.org/lkml/08188c28f274da63a3f8549add3086a92aef45e5.1780908661.git.namcao@linutronix.de
Patches 5: rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL
#ifndef guards for da_monitor_reset_hook and EVENT_NONE_LBL.
Patch 6: rv/tlob: add tlob hybrid automaton monitor
Main tlob implementation.
Patches 7-8: Tests
KUnit tests for the uprobe-line parser; seven ftrace-style selftests.
Changes since v3
----------------
Patch 1 (rv/da):
- Pool redesigned from spinlock+pointer-stack to lock-free llist
(cmpxchg-based): pool release from RCU callback context is now safe
without acquiring a lock
- DA_MON_POOL_SIZE alone selects pool mode; no separate
DA_MON_ALLOCATION_STRATEGY define required for monitors
- All strategy dispatch uses plain C if() (no ifdeffery in functions)
- da_monitor_destroy_pool() calls da_monitor_reset_all() and
da_monitor_sync_hook() before the hash iteration, preventing hrtimer
UAF when timer callbacks are still in flight
Patch 2 (rv_uprobe):
- struct rv_uprobe now embeds struct uprobe_consumer directly; no
separate heap allocation per probe, no rv_uprobe_free()
- path_put() moved after uprobe_register() to keep the inode
referenced across the call as required by the uprobe API
Patch 6 (tlob monitor):
- da_get_target_by_id() wrapped in scoped_guard(rcu) in
tlob_start_task(): hash_for_each_possible_rcu() requires an explicit
RCU read-side CS on PREEMPT_RT where spinlock_t does not provide one
- tlob_monitor_read() releases mutex before simple_read_from_buffer()
to avoid holding the mutex across copy_to_user()
- kmem_cache replaced with a pre-allocated llist pool for
tlob_task_state, eliminating GFP_ATOMIC in the measurement window
- EXPORT_SYMBOL_IF_KUNIT added for tlob_parse_uprobe_line() and
tlob_parse_remove_line() (required when CONFIG_TLOB_KUNIT_TEST=m)
- tlob_parse_remove_line() rejects negative offsets
- Unnecessary includes removed; Kconfig entry placed after the
deadline monitors marker
Patch 7 (KUnit):
- Tests now call tlob_parse_uprobe_line() and tlob_parse_remove_line()
directly; no tracepoint enrollment, no uprobe or filesystem state
Patch 8 (selftests):
- ftracetest walk-up algorithm (Gabriele's suggestion) incorporated;
the separate verificationtest-ktap fix patch is dropped and the
series shrinks from 9 to 8 patches
- All test scripts use "! cmd || false" throughout
Testing
-------
Tested on x86_64 with CONFIG_PREEMPT_RT=y (kernel 7.1 + virtme-ng):
- All KUnit tests pass
- All 7 selftests pass (uprobe_bind, uprobe_violation, uprobe_no_event,
uprobe_multi, uprobe_detail_{running,sleeping,waiting})
Wen Yang (8):
rv/da: introduce DA_MON_ALLOCATION_STRATEGY
rv: add generic uprobe infrastructure for RV monitors
rv/tlob: add tlob model DOT file
rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check
rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable
rv/tlob: add tlob hybrid automaton monitor
rv/tlob: add KUnit tests for the tlob monitor
selftests/verification: add tlob selftests
Documentation/trace/rv/index.rst | 1 +
Documentation/trace/rv/monitor_tlob.rst | 177 ++++
include/rv/da_monitor.h | 247 ++++-
include/rv/ha_monitor.h | 24 +-
include/rv/rv_uprobe.h | 93 ++
kernel/trace/rv/Kconfig | 8 +
kernel/trace/rv/Makefile | 3 +
kernel/trace/rv/monitors/nomiss/nomiss.c | 6 +-
kernel/trace/rv/monitors/tlob/.kunitconfig | 8 +
kernel/trace/rv/monitors/tlob/Kconfig | 19 +
kernel/trace/rv/monitors/tlob/tlob.c | 970 ++++++++++++++++++
kernel/trace/rv/monitors/tlob/tlob.h | 153 +++
kernel/trace/rv/monitors/tlob/tlob_kunit.c | 139 +++
kernel/trace/rv/monitors/tlob/tlob_trace.h | 52 +
kernel/trace/rv/rv_trace.h | 1 +
kernel/trace/rv/rv_uprobe.c | 104 ++
tools/testing/selftests/ftrace/ftracetest | 18 +-
.../testing/selftests/verification/.gitignore | 2 +
tools/testing/selftests/verification/Makefile | 19 +-
.../verification/test.d/tlob/Makefile | 28 +
.../test.d/tlob/run_tlob_tests.sh | 90 ++
.../verification/test.d/tlob/tlob_sym.c | 209 ++++
.../verification/test.d/tlob/tlob_target.c | 138 +++
.../verification/test.d/tlob/uprobe_bind.tc | 37 +
.../test.d/tlob/uprobe_detail_running.tc | 51 +
.../test.d/tlob/uprobe_detail_sleeping.tc | 50 +
.../test.d/tlob/uprobe_detail_waiting.tc | 66 ++
.../verification/test.d/tlob/uprobe_multi.tc | 64 ++
.../test.d/tlob/uprobe_no_event.tc | 19 +
.../test.d/tlob/uprobe_violation.tc | 67 ++
tools/verification/models/tlob.dot | 22 +
31 files changed, 2839 insertions(+), 46 deletions(-)
create mode 100644 Documentation/trace/rv/monitor_tlob.rst
create mode 100644 include/rv/rv_uprobe.h
create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig
create mode 100644 kernel/trace/rv/monitors/tlob/Kconfig
create mode 100644 kernel/trace/rv/monitors/tlob/tlob.c
create mode 100644 kernel/trace/rv/monitors/tlob/tlob.h
create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c
create mode 100644 kernel/trace/rv/monitors/tlob/tlob_trace.h
create mode 100644 kernel/trace/rv/rv_uprobe.c
create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
create mode 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_target.c
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
create mode 100644 tools/verification/models/tlob.dot
--
2.25.1
^ permalink raw reply
* [PATCH v4 4/8] rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
When env_store is U64_MAX (its initial sentinel value),
ha_invariant_passed_ns() returns 0 immediately without initializing
env_store to the current clock. Subsequent calls to
ha_check_invariant_ns() then find env_store still at U64_MAX, causing
the elapsed comparison to wrap and always report the invariant as
satisfied, silently masking any violations.
Fix by calling ha_reset_clk_ns() to establish the guard on the first
invocation instead of returning early. Apply the same fix to
ha_invariant_passed_jiffy().
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/ha_monitor.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h
index 83199f90afe8..dddf5694bcc8 100644
--- a/include/rv/ha_monitor.h
+++ b/include/rv/ha_monitor.h
@@ -375,12 +375,12 @@ static inline bool ha_check_invariant_ns(struct ha_monitor *ha_mon,
static inline u64 ha_invariant_passed_ns(struct ha_monitor *ha_mon, enum envs env,
u64 expire, u64 time_ns)
{
- u64 passed = 0;
+ u64 passed;
if (env < 0 || env >= ENV_MAX_STORED)
return 0;
if (ha_monitor_env_invalid(ha_mon, env))
- return 0;
+ ha_reset_clk_ns(ha_mon, env, time_ns);
passed = ha_get_env(ha_mon, env, time_ns);
ha_set_invariant_ns(ha_mon, env, expire - passed, time_ns);
return passed;
@@ -414,12 +414,12 @@ static inline bool ha_check_invariant_jiffy(struct ha_monitor *ha_mon,
static inline u64 ha_invariant_passed_jiffy(struct ha_monitor *ha_mon, enum envs env,
u64 expire, u64 time_ns)
{
- u64 passed = 0;
+ u64 passed;
if (env < 0 || env >= ENV_MAX_STORED)
return 0;
if (ha_monitor_env_invalid(ha_mon, env))
- return 0;
+ ha_reset_clk_jiffy(ha_mon, env);
passed = ha_get_env(ha_mon, env, time_ns);
ha_set_invariant_jiffy(ha_mon, env, expire - passed);
return passed;
--
2.25.1
^ permalink raw reply related
* [PATCH v4 5/8] rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
Wrap both definitions with #ifndef guards so HA-based monitors can
substitute their own implementations before including this header.
tlob uses this to define a reset hook that cancels per-task hrtimers
on monitor teardown.
Overrides must still call ha_monitor_reset_env() or cancel outstanding
timers to avoid timer UAF.
No behaviour change for monitors that do not override either macro.
Reviewed-by: Gabriele Monaco <gmonaco@redhat.com>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
include/rv/ha_monitor.h | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h
index dddf5694bcc8..6d526eef8348 100644
--- a/include/rv/ha_monitor.h
+++ b/include/rv/ha_monitor.h
@@ -36,7 +36,15 @@ static bool ha_monitor_handle_constraint(struct da_monitor *da_mon,
da_id_type id);
#define da_monitor_event_hook ha_monitor_handle_constraint
#define da_monitor_init_hook ha_monitor_init_env
+
+/*
+ * Allow monitors to override da_monitor_reset_hook before including this
+ * header. The override must still call ha_monitor_reset_env() or cancel
+ * timers explicitly.
+ */
+#ifndef da_monitor_reset_hook
#define da_monitor_reset_hook ha_monitor_reset_env
+#endif
#define da_monitor_sync_hook() synchronize_rcu()
#if !defined(HA_SKIP_AUTO_CLEANUP) && RV_MON_TYPE == RV_MON_PER_TASK
@@ -75,7 +83,9 @@ _Static_assert(offsetof(struct ha_monitor, da_mon) == 0,
#define ENV_INVALID_VALUE U64_MAX
/* Error with no event occurs only on timeouts */
#define EVENT_NONE EVENT_MAX
+#ifndef EVENT_NONE_LBL
#define EVENT_NONE_LBL "none"
+#endif
#define ENV_BUFFER_SIZE 64
#ifdef CONFIG_RV_REACTORS
--
2.25.1
^ permalink raw reply related
* [PATCH v4 6/8] rv/tlob: add tlob hybrid automaton monitor
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
tlob (task latency over budget) is a per-task hybrid automaton RV
monitor that tracks wall-clock time across a user-delimited code
section and emits an error when elapsed time exceeds a configurable
budget.
The automaton has three states (running, waiting, sleeping) driven by
sched_switch and sched_wakeup tracepoints, with a single clock
invariant enforced by a per-task HRTIMER_MODE_REL_HARD timer. On
budget expiry the monitor records a per-state time breakdown
(running_ns, waiting_ns, sleeping_ns) before emitting error_env_tlob.
Uprobe pairs are registered through a tracefs monitor file as
"p PATH:OFFSET_START OFFSET_STOP threshold=NS" so arbitrary code
sections can be delimited without modifying the target binary.
Per-task state is allocated from a pre-allocated llist pool so the
uprobe entry handler incurs no dynamic allocation inside the [T0, T1]
measurement window. DA_ALLOC_POOL is used for the same reason on the
da_monitor_storage side.
tlob_start_lock is a spinlock_t because the uprobe handler runs under
rcu_read_lock_trace() (Tasks Trace SRCU), which permits sleeping on
PREEMPT_RT where spinlock_t is an rt_mutex. da_get_target_by_id() is
wrapped in scoped_guard(rcu) because hash_for_each_possible_rcu()
requires an RCU read-side critical section independent of the lock.
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
Documentation/trace/rv/index.rst | 1 +
Documentation/trace/rv/monitor_tlob.rst | 177 ++++
kernel/trace/rv/Kconfig | 1 +
kernel/trace/rv/Makefile | 1 +
kernel/trace/rv/monitors/tlob/Kconfig | 12 +
kernel/trace/rv/monitors/tlob/tlob.c | 969 +++++++++++++++++++++
kernel/trace/rv/monitors/tlob/tlob.h | 151 ++++
kernel/trace/rv/monitors/tlob/tlob_trace.h | 52 ++
kernel/trace/rv/rv_trace.h | 1 +
9 files changed, 1365 insertions(+)
create mode 100644 Documentation/trace/rv/monitor_tlob.rst
create mode 100644 kernel/trace/rv/monitors/tlob/Kconfig
create mode 100644 kernel/trace/rv/monitors/tlob/tlob.c
create mode 100644 kernel/trace/rv/monitors/tlob/tlob.h
create mode 100644 kernel/trace/rv/monitors/tlob/tlob_trace.h
diff --git a/Documentation/trace/rv/index.rst b/Documentation/trace/rv/index.rst
index 29769f06bb0f..1501545b5f08 100644
--- a/Documentation/trace/rv/index.rst
+++ b/Documentation/trace/rv/index.rst
@@ -16,5 +16,6 @@ Runtime Verification
monitor_wwnr.rst
monitor_sched.rst
monitor_rtapp.rst
+ monitor_tlob.rst
monitor_stall.rst
monitor_deadline.rst
diff --git a/Documentation/trace/rv/monitor_tlob.rst b/Documentation/trace/rv/monitor_tlob.rst
new file mode 100644
index 000000000000..327c9832bf31
--- /dev/null
+++ b/Documentation/trace/rv/monitor_tlob.rst
@@ -0,0 +1,177 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+Monitor tlob
+============
+
+- Name: tlob - task latency over budget
+- Type: per-object hybrid automaton (RV_MON_PER_OBJ)
+- Author: Wen Yang <wen.yang@linux.dev>
+
+Description
+-----------
+
+The tlob monitor tracks per-task elapsed wall-clock time (CLOCK_MONOTONIC,
+spanning running, waiting, and sleeping states) and reports a violation when
+the monitored task exceeds a configurable per-invocation budget threshold.
+
+The monitor implements a three-state hybrid automaton with a single clock
+environment variable ``clk_elapsed``. The clock invariant
+``clk_elapsed < BUDGET_NS()`` is active in all three states; when it is
+violated the HA timer fires and the framework emits ``error_env_tlob``
+then calls ``da_monitor_reset()`` automatically::
+
+ | (initial, via task_start)
+ v
+ +--------------+
+ | running | <-----------+
+ +--------------+ |
+ | | |
+ sleep preempt switch_in
+ | | |
+ v v |
+ +---------+ +---------+ |
+ | sleeping| | waiting | -------+
+ +---------+ +---------+
+ | ^
+ +---wakeup---+
+
+ Key transitions:
+ running --(sleep)------> sleeping (task blocks waiting for a resource)
+ running --(preempt)----> waiting (task preempted, back in runqueue)
+ sleeping --(wakeup)-----> waiting (resource available, enters runqueue)
+ waiting --(switch_in)--> running (scheduler picks task, back on CPU)
+
+ ``tlob_start_task()`` calls ``da_handle_start_run_event(task->pid, ws, start_tlob)``.
+ The ``start_tlob`` self-loop on the ``running`` state triggers
+ ``ha_setup_invariants()``, which resets ``clk_elapsed`` and arms the budget
+ timer automatically. ``tlob_stop_task()`` cancels the HA timer synchronously
+ via ``ha_cancel_timer_sync()``, then calls ``da_monitor_reset()``.
+
+The non-running condition (monitor not yet started or reset after a
+stop/violation) is handled implicitly by the RV framework
+(``da_mon->monitoring == 0``) - it is not an explicit DA state.
+
+Per-task state lives in ``struct tlob_task_state`` which is stored as
+``monitor_target`` in the framework's ``da_monitor_storage``, indexed by
+pid. The per-invocation ``threshold_ns`` is read via
+``ha_get_target(ha_mon)->threshold_ns`` inside the HA constraint functions,
+following the same pattern as the ``nomiss`` monitor.
+
+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=NS
+
+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=5000000" \
+ > /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+ # Remove a binding
+ echo "-/usr/bin/myapp:0x12a0" > /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+ # List registered bindings
+ cat /sys/kernel/tracing/rv/monitors/tlob/monitor
+
+ # Read violations from the trace buffer
+ cat /sys/kernel/tracing/trace
+
+Violation tracepoints
+~~~~~~~~~~~~~~~~~~~~~
+
+Two tracepoints are emitted together on a budget violation:
+
+``error_env_tlob``
+ Standard HA clock-invariant tracepoint (emitted by the RV framework).
+ Fields: ``id`` (task pid), ``state``, ``event`` (``"budget_exceeded"``),
+ ``env`` (``"clk_elapsed"``).
+
+``detail_env_tlob``
+ Tlob-specific breakdown of elapsed time per DA state.
+ Fields: ``id`` (task pid), ``threshold_ns``, ``running_ns``,
+ ``waiting_ns``, ``sleeping_ns``.
+
+ Use ``detail_env_tlob`` to diagnose *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.
+
+Example: correlate the two tracepoints to see the breakdown::
+
+ trace-cmd record -e error_env_tlob -e detail_env_tlob &
+ # ... run workload ...
+ trace-cmd report
+
+tracefs files
+~~~~~~~~~~~~~
+
+The following files are specific to tlob under
+``/sys/kernel/tracing/rv/monitors/tlob/``:
+
+``monitor`` (rw)
+ Write ``p PATH:OFFSET_START OFFSET_STOP threshold=NS``
+ to bind two entry uprobes. Write ``-PATH:OFFSET_START`` to remove a
+ binding. Read to list registered bindings in the same format.
+ See the `tracefs interface (uprobe-based external monitoring)`_ section above.
+
+Kernel API
+----------
+
+``tlob_start_task`` and ``tlob_stop_task`` are the implementation-level
+functions called by the uprobe entry/exit handlers; the interface is
+driven from userspace.
+
+.. kernel-doc:: kernel/trace/rv/monitors/tlob/tlob.c
+ :functions: tlob_start_task tlob_stop_task
+
+``tlob_start_task(task, threshold_ns)``
+ Begin monitoring *task* with a total latency budget of *threshold_ns*
+ nanoseconds. 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 (threshold out of range),
+ -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 outcome.
+ Returns 0 (clean stop, within budget), -EOVERFLOW (budget was exceeded),
+ -ESRCH (not monitored), or -EAGAIN (concurrent stop racing).
+
+Design notes
+------------
+
+Limitations:
+
+- The initial DA state is always ``running``, set by feeding the synthetic
+ event ``switch_in_tlob`` to ``da_handle_start_event()``. Monitoring a non-current
+ task that is already in waiting or sleeping state at call time misclassifies
+ the first interval as ``running_ns``.
+- ``TASK_STOPPED`` and ``TASK_TRACED`` carry ``prev_state != 0`` and are
+ therefore counted as ``sleeping_ns``, indistinguishable from
+ I/O-blocked time.
+- ``sched_wakeup_new`` is not hooked. In practice this is not an issue
+ because ``tlob_start_task`` is always called from a running context.
+
+Specification
+-------------
+
+Graphviz DOT file in tools/verification/models/tlob.dot.
+
+KUnit tests under ``kernel/trace/rv/monitors/tlob/tlob_kunit.c``
+(CONFIG_TLOB_KUNIT_TEST).
+
+User-space integration tests under ``tools/testing/selftests/verification/``
+(requires CONFIG_RV_MON_TLOB=y and root).
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index 5bad1d63f411..ed0dc8241691 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -90,6 +90,7 @@ source "kernel/trace/rv/monitors/deadline/Kconfig"
source "kernel/trace/rv/monitors/nomiss/Kconfig"
# Add new deadline monitors here
+source "kernel/trace/rv/monitors/tlob/Kconfig"
# Add new monitors here
config RV_REACTORS
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index f139b904bea3..ae59e97f8682 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -20,6 +20,7 @@ obj-$(CONFIG_RV_MON_OPID) += monitors/opid/opid.o
obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o
obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o
obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o
+obj-$(CONFIG_RV_MON_TLOB) += monitors/tlob/tlob.o
# Add new monitors here
obj-$(CONFIG_RV_UPROBE) += rv_uprobe.o
obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
diff --git a/kernel/trace/rv/monitors/tlob/Kconfig b/kernel/trace/rv/monitors/tlob/Kconfig
new file mode 100644
index 000000000000..aa43382073d2
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+config RV_MON_TLOB
+ bool "tlob monitor"
+ depends on RV && UPROBES && HIGH_RES_TIMERS
+ select HA_MON_EVENTS_ID
+ select RV_UPROBE
+ help
+ Enable the tlob (task latency over budget) hybrid-automaton RV
+ monitor. tlob tracks per-task elapsed wall-clock time across a
+ user-delimited code section and emits error_env_tlob when the
+ elapsed time exceeds a configurable per-invocation budget.
diff --git a/kernel/trace/rv/monitors/tlob/tlob.c b/kernel/trace/rv/monitors/tlob/tlob.c
new file mode 100644
index 000000000000..b45e84195131
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob.c
@@ -0,0 +1,969 @@
+// 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 the 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)
+ *
+ * Copyright (C) 2026 Wen Yang <wen.yang@linux.dev>
+ */
+#include <linux/kernel.h>
+#include <linux/llist.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/namei.h>
+#include <linux/rv.h>
+#include <linux/slab.h>
+#include <kunit/visibility.h>
+#include <rv/instrumentation.h>
+#include <rv/rv_uprobe.h>
+#include <rv.h>
+
+#define MODULE_NAME "tlob"
+
+#include <trace/events/sched.h>
+#include <rv_trace.h>
+
+/*
+ * Per-task latency monitoring state. One instance per monitoring window.
+ * Stored as monitor_target in da_monitor_storage; freed via call_rcu.
+ */
+enum tlob_acc_idx {
+ TLOB_ACC_RUNNING,
+ TLOB_ACC_WAITING,
+ TLOB_ACC_SLEEPING,
+ TLOB_ACC_MAX,
+};
+
+struct tlob_task_state {
+ struct task_struct *task; /* via get_task_struct */
+ u64 threshold_ns; /* budget in nanoseconds */
+
+ /* 1 = cleanup claimed; ha_setup_invariants won't restart the timer. */
+ atomic_t stopping;
+
+ /* Serialises accs_ns[]; held briefly (hardirq-safe). */
+ raw_spinlock_t entry_lock;
+ u64 accs_ns[TLOB_ACC_MAX]; /* per-state elapsed ns */
+ ktime_t last_ts;
+
+ struct rcu_head rcu;
+ /* Free-list node; active only between call_rcu() return and next alloc. */
+ struct llist_node free_node;
+};
+
+#define RV_MON_TYPE RV_MON_PER_OBJ
+#define HA_TIMER_TYPE HA_TIMER_HRTIMER
+
+typedef struct tlob_task_state *monitor_target;
+
+static inline void tlob_reset_notify(struct da_monitor *da_mon);
+#define da_monitor_reset_hook tlob_reset_notify
+
+static inline void tlob_extra_cleanup(struct da_monitor *da_mon);
+#define da_extra_cleanup tlob_extra_cleanup
+
+#define EVENT_NONE_LBL "budget_exceeded"
+
+#include "tlob.h"
+
+#define DA_MON_POOL_SIZE TLOB_MAX_MONITORED
+
+#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);
+
+ if (!trace_detail_env_tlob_enabled())
+ return;
+
+ 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 accs[TLOB_ACC_MAX], partial_ns;
+ 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);
+ accs[TLOB_ACC_RUNNING] = ws->accs_ns[TLOB_ACC_RUNNING] +
+ (curr_state == running_tlob ? partial_ns : 0);
+ accs[TLOB_ACC_WAITING] = ws->accs_ns[TLOB_ACC_WAITING] +
+ (curr_state == waiting_tlob ? partial_ns : 0);
+ accs[TLOB_ACC_SLEEPING] = ws->accs_ns[TLOB_ACC_SLEEPING] +
+ (curr_state == sleeping_tlob ? partial_ns : 0);
+ raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
+
+ trace_detail_env_tlob(da_get_id(da_mon), ws->threshold_ns,
+ accs[TLOB_ACC_RUNNING],
+ accs[TLOB_ACC_WAITING],
+ accs[TLOB_ACC_SLEEPING]);
+ }
+}
+
+#define BUDGET_NS(ha_mon) (ha_get_target(ha_mon)->threshold_ns)
+
+/* HA constraint functions (called by ha_monitor_handle_constraint) */
+
+static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_tlob env,
+ u64 time_ns)
+{
+ if (env == clk_elapsed_tlob)
+ return ha_get_clk_ns(ha_mon, env, time_ns);
+ return ENV_INVALID_VALUE;
+}
+
+/*
+ * ha_verify_invariants - clk_elapsed < BUDGET_NS must hold in all states.
+ *
+ * The invariant is uniform across running/waiting/sleeping; check it
+ * unconditionally rather than enumerating each state.
+ */
+static inline bool ha_verify_invariants(struct ha_monitor *ha_mon,
+ enum states curr_state, enum events event,
+ enum states next_state, u64 time_ns)
+{
+ return ha_check_invariant_ns(ha_mon, clk_elapsed_tlob, time_ns);
+}
+
+/*
+ * Convert invariant (deadline) to guard (reset anchor) on state transitions.
+ *
+ * The conversion is identical for every departing state; skip only self-loops.
+ */
+static inline void ha_convert_inv_guard(struct ha_monitor *ha_mon,
+ enum states curr_state, enum events event,
+ enum states next_state, u64 time_ns)
+{
+ if (curr_state != next_state)
+ ha_inv_to_guard(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon), time_ns);
+}
+
+/* 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,
+ enum states next_state, u64 time_ns)
+{
+ return true;
+}
+
+/*
+ * Guard on stopping: a sched_switch arriving after ha_cancel_timer_sync()
+ * would re-arm the timer and trigger an ODEBUG "activate active" splat.
+ * _acquire pairs with cmpxchg_release in tlob_stop_task.
+ */
+static inline void ha_setup_invariants(struct ha_monitor *ha_mon,
+ enum states curr_state, enum events event,
+ enum states next_state, u64 time_ns)
+{
+ if (atomic_read_acquire(&ha_get_target(ha_mon)->stopping))
+ return;
+ if (next_state < state_max_tlob)
+ ha_start_timer_ns(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon), time_ns);
+ else
+ ha_cancel_timer(ha_mon);
+}
+
+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))
+ 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))
+ return false;
+
+ ha_setup_invariants(ha_mon, curr_state, event, next_state, time_ns);
+
+ return true;
+}
+
+/*
+ * Pre-allocated pool for tlob_task_state slots. Lock-free llist so that
+ * tlob_ws_return_cb() (RCU callback) can return slots without acquiring a
+ * spinlock. Same concurrency model as da_pool_storage (see da_monitor.h).
+ */
+static struct tlob_task_state *tlob_ws_storage;
+static LLIST_HEAD(tlob_ws_free_list);
+
+static void tlob_ws_return_cb(struct rcu_head *head)
+{
+ struct tlob_task_state *ws =
+ container_of(head, struct tlob_task_state, rcu);
+
+ llist_add(&ws->free_node, &tlob_ws_free_list);
+}
+
+/* Direct return to free list without RCU delay (ws was never published). */
+static void tlob_ws_direct_return(struct tlob_task_state *ws)
+{
+ llist_add(&ws->free_node, &tlob_ws_free_list);
+}
+
+static struct tlob_task_state *tlob_ws_alloc(void)
+{
+ struct llist_node *node = llist_del_first(&tlob_ws_free_list);
+
+ if (!node)
+ return NULL;
+
+ struct tlob_task_state *ws =
+ llist_entry(node, struct tlob_task_state, free_node);
+
+ memset(ws, 0, sizeof(*ws));
+ return ws;
+}
+
+/* Uprobe binding list; protected by tlob_uprobe_mutex. */
+static LIST_HEAD(tlob_uprobe_list);
+static DEFINE_MUTEX(tlob_uprobe_mutex);
+
+/*
+ * Serialises duplicate-check + da_handle_start_run_event() per pid.
+ * spinlock_t not raw_spinlock_t: uprobe handlers run under Tasks Trace
+ * SRCU (rcu_read_lock_trace()), which permits sleeping on PREEMPT_RT.
+ */
+static DEFINE_SPINLOCK(tlob_start_lock);
+
+/* Per-uprobe-binding state: a start + stop probe pair for one binary region. */
+struct tlob_uprobe_binding {
+ struct list_head list;
+ u64 threshold_ns;
+ char binpath[TLOB_MAX_PATH];
+ loff_t offset_start;
+ loff_t offset_stop;
+ DECLARE_RV_UPROBE(start_probe);
+ DECLARE_RV_UPROBE(stop_probe);
+};
+
+/*
+ * Per-task teardown invoked by da_monitor_destroy() for each hash entry.
+ * CAS on stopping (0->1) claims exclusive cleanup ownership.
+ *
+ * No per-entry ha_cancel_timer_sync(): da_monitor_destroy() calls
+ * da_monitor_reset_all() + synchronize_rcu() before this hook, and
+ * ha_mon_destroying prevents new timer callbacks from running.
+ */
+static inline void tlob_extra_cleanup(struct da_monitor *da_mon)
+{
+ struct ha_monitor *ha_mon = to_ha_monitor(da_mon);
+ struct tlob_task_state *ws = ha_get_target(ha_mon);
+
+ if (!ws)
+ return;
+
+ if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
+ return;
+
+ put_task_struct(ws->task);
+ /*
+ * da_monitor_destroy() has already called synchronize_rcu(); no
+ * reader holds ws. Return the slot directly without call_rcu.
+ */
+ llist_add(&ws->free_node, &tlob_ws_free_list);
+}
+
+static inline bool __tlob_acc(struct task_struct *task, ktime_t now,
+ enum tlob_acc_idx idx)
+{
+ struct tlob_task_state *ws;
+ unsigned long flags;
+
+ guard(rcu)();
+ ws = da_get_target_by_id(task->pid);
+ if (!ws)
+ return false;
+ raw_spin_lock_irqsave(&ws->entry_lock, flags);
+ ws->accs_ns[idx] += ktime_to_ns(ktime_sub(now, ws->last_ts));
+ ws->last_ts = now;
+ raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
+ return true;
+}
+
+/* Accumulate running_ns for prev; returns true if prev is monitored. */
+static inline bool tlob_acc_running(struct task_struct *task, ktime_t now)
+{
+ return __tlob_acc(task, now, TLOB_ACC_RUNNING);
+}
+
+/* Accumulate waiting_ns for next; returns true if next is monitored. */
+static inline bool tlob_acc_waiting(struct task_struct *task, ktime_t now)
+{
+ return __tlob_acc(task, now, TLOB_ACC_WAITING);
+}
+
+/*
+ * handle_sched_switch - advance the DA on every context switch.
+ *
+ * Generates three DA events:
+ * prev, prev_state != 0 -> sleep_tlob (running -> sleeping)
+ * prev, prev_state == 0 -> preempt_tlob (running -> waiting)
+ * next -> switch_in_tlob (waiting -> running)
+ *
+ * A single ktime_get() at handler entry is shared by both acc calls so that
+ * prev's running_ns and next's waiting_ns share the same context-switch
+ * timestamp; neither absorbs handler overhead into its accumulator.
+ *
+ * No waiting->sleeping edge exists: a task can only block voluntarily
+ * (call schedule()) while it is executing on CPU, which corresponds to
+ * the running DA state. A task in the waiting state is TASK_RUNNING in
+ * kernel terms (on the runqueue) and cannot block itself.
+ *
+ * da_handle_event() is called unconditionally: it skips tasks that have no
+ * monitor entry in the hash table.
+ */
+static void handle_sched_switch(void *data, bool preempt_unused,
+ struct task_struct *prev,
+ struct task_struct *next,
+ unsigned int prev_state)
+{
+ ktime_t now = ktime_get();
+ bool prev_preempted = (prev_state == 0);
+
+ if (tlob_acc_running(prev, now))
+ da_handle_event(prev->pid, NULL,
+ prev_preempted ? preempt_tlob : sleep_tlob);
+ if (tlob_acc_waiting(next, now))
+ da_handle_event(next->pid, NULL, switch_in_tlob);
+}
+
+/* Accumulate sleeping_ns on wakeup; returns true if task is monitored. */
+static inline bool tlob_acc_sleeping(struct task_struct *task, ktime_t now)
+{
+ return __tlob_acc(task, now, TLOB_ACC_SLEEPING);
+}
+
+/*
+ * handle_sched_wakeup - sleeping -> waiting transition.
+ *
+ * try_to_wake_up() skips TASK_RUNNING tasks, so this never fires for a
+ * task already in running or waiting state.
+ */
+static void handle_sched_wakeup(void *data, struct task_struct *p)
+{
+ ktime_t now = ktime_get();
+
+ if (tlob_acc_sleeping(p, now))
+ da_handle_event(p->pid, NULL, wakeup_tlob);
+}
+
+/*
+ * handle_sched_process_exit - clean up if a task exits without TRACE_STOP.
+ *
+ * Called in do_exit() context; the task still has a valid pid here.
+ * tlob_stop_task() returns -ESRCH if the task is not monitored, which is fine.
+ */
+static void handle_sched_process_exit(void *data, struct task_struct *p,
+ bool group_dead)
+{
+ tlob_stop_task(p);
+}
+
+/**
+ * tlob_start_task - begin monitoring @task with budget @threshold_ns ns.
+ * @task: Task to monitor; may be current or another task.
+ * @threshold_ns: Latency budget in nanoseconds (wall-clock; running +
+ * waiting + sleeping).
+ * Must be in [1000, TLOB_MAX_THRESHOLD_NS].
+ *
+ * Returns 0, -ENODEV, -ERANGE, -EALREADY, or -ENOSPC (pool at capacity).
+ */
+int tlob_start_task(struct task_struct *task, u64 threshold_ns)
+{
+ struct tlob_task_state *ws;
+
+ if (!da_monitor_enabled())
+ return -ENODEV;
+
+ if (threshold_ns < TLOB_MIN_THRESHOLD_NS ||
+ threshold_ns > TLOB_MAX_THRESHOLD_NS)
+ return -ERANGE;
+
+ /* Serialise duplicate-check + pool-slot claim; see tlob_start_lock. */
+ guard(spinlock)(&tlob_start_lock);
+
+ /*
+ * __da_get_mon_storage() uses hash_for_each_possible_rcu(), which
+ * requires an RCU read-side critical section. On PREEMPT_RT,
+ * spinlock_t is an rt_mutex and does not satisfy this requirement.
+ */
+ scoped_guard(rcu) {
+ if (da_get_target_by_id(task->pid))
+ return -EALREADY;
+ }
+
+ /*
+ * Both tlob_ws_alloc() and da_handle_start_run_event() pop from
+ * pre-allocated pools of size TLOB_MAX_MONITORED; NULL return means
+ * the pool is at capacity.
+ */
+ ws = tlob_ws_alloc();
+ if (!ws)
+ return -ENOSPC;
+
+ ws->task = task;
+ get_task_struct(task);
+ ws->threshold_ns = threshold_ns;
+ ws->last_ts = ktime_get();
+ raw_spin_lock_init(&ws->entry_lock);
+
+ /*
+ * da_handle_start_run_event() claims a pool slot via da_prepare_storage(),
+ * initialises the monitor, and delivers start_tlob in one step: the
+ * generated ha_setup_invariants() resets clk_elapsed and arms the timer.
+ * Returns 0 if the da_monitor_storage pool is exhausted.
+ */
+ if (!da_handle_start_run_event(task->pid, ws, start_tlob)) {
+ put_task_struct(task);
+ tlob_ws_direct_return(ws);
+ return -ENOSPC;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(tlob_start_task);
+
+/**
+ * tlob_stop_task - stop monitoring @task.
+ * @task: Task to stop.
+ *
+ * CAS on ws->stopping (0->1) under RCU claims cleanup ownership;
+ * the winner cancels the timer synchronously and frees all resources.
+ *
+ * Returns 0, -EOVERFLOW (budget exceeded), -ESRCH (not monitored),
+ * or -EAGAIN (concurrent caller claimed cleanup).
+ */
+int tlob_stop_task(struct task_struct *task)
+{
+ struct da_monitor *da_mon;
+ struct ha_monitor *ha_mon;
+ struct tlob_task_state *ws;
+ bool budget_exceeded;
+
+ scoped_guard(rcu) {
+ ws = da_get_target_by_id(task->pid);
+ if (!ws)
+ return -ESRCH;
+
+ da_mon = da_get_monitor(task->pid, NULL);
+ if (unlikely(WARN_ON_ONCE(!da_mon)))
+ return -ESRCH;
+
+ ha_mon = to_ha_monitor(da_mon);
+
+ /*
+ * CAS (0->1) claims cleanup ownership under RCU (ws guaranteed valid).
+ * _release pairs with atomic_read_acquire in ha_setup_invariants.
+ */
+ if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
+ return -EAGAIN;
+ }
+ /*
+ * ws and ha_mon are used below outside the RCU guard. This is safe:
+ * the winning CAS (stopping: 0->1) is the only path that frees ws,
+ * and da_destroy_storage() below is the only call that returns the
+ * pool slot. No concurrent path can free either object.
+ */
+
+ /* Wait for in-flight timer callback before reading da_monitoring. */
+ ha_cancel_timer_sync(ha_mon);
+
+ /* Timer fired first -> budget exceeded; otherwise reset normally. */
+ scoped_guard(rcu) {
+ budget_exceeded = !da_monitoring(da_mon);
+ if (!budget_exceeded)
+ da_monitor_reset(da_mon);
+ }
+ da_destroy_storage(task->pid);
+
+ put_task_struct(ws->task);
+ call_rcu(&ws->rcu, tlob_ws_return_cb);
+ return budget_exceeded ? -EOVERFLOW : 0;
+}
+EXPORT_SYMBOL_GPL(tlob_stop_task);
+
+static int tlob_uprobe_entry_handler(struct uprobe_consumer *self,
+ struct pt_regs *regs, __u64 *data)
+{
+ struct tlob_uprobe_binding *b =
+ container_of(self, struct tlob_uprobe_binding, start_probe.uc);
+
+ tlob_start_task(current, b->threshold_ns);
+ return 0;
+}
+
+static int tlob_uprobe_stop_handler(struct uprobe_consumer *self,
+ struct pt_regs *regs, __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_ns, const char *binpath,
+ loff_t offset_start, loff_t offset_stop)
+{
+ struct tlob_uprobe_binding *tmp_b;
+ char pathbuf[TLOB_MAX_PATH];
+ struct inode *inode;
+ struct path path __free(path_put) = {};
+ char *canon;
+ int ret;
+
+ if (binpath[0] != '/')
+ return -EINVAL;
+
+ struct tlob_uprobe_binding *b __free(kfree) = kzalloc_obj(*b, GFP_KERNEL);
+ if (!b)
+ return -ENOMEM;
+
+ b->threshold_ns = threshold_ns;
+ b->offset_start = offset_start;
+ b->offset_stop = offset_stop;
+
+ ret = kern_path(binpath, LOOKUP_FOLLOW, &path);
+ if (ret)
+ return ret;
+
+ if (!d_is_reg(path.dentry))
+ return -EINVAL;
+
+ inode = d_real_inode(path.dentry);
+
+ /* Reject duplicate start offset for the same binary inode. */
+ list_for_each_entry(tmp_b, &tlob_uprobe_list, list) {
+ if (tmp_b->offset_start == offset_start &&
+ rv_uprobe_is_registered(&tmp_b->start_probe) &&
+ tmp_b->start_probe.inode == inode)
+ return -EEXIST;
+ }
+
+ canon = d_path(&path, pathbuf, sizeof(pathbuf));
+ if (IS_ERR(canon))
+ return PTR_ERR(canon);
+ strscpy(b->binpath, canon, sizeof(b->binpath));
+
+ b->start_probe.uc.handler = tlob_uprobe_entry_handler;
+ ret = rv_uprobe_register(b->binpath, offset_start, &b->start_probe);
+ if (ret)
+ return ret;
+
+ b->stop_probe.uc.handler = tlob_uprobe_stop_handler;
+ ret = rv_uprobe_register(b->binpath, offset_stop, &b->stop_probe);
+ if (ret) {
+ rv_uprobe_unregister(&b->start_probe);
+ return ret;
+ }
+
+ /*
+ * Do NOT write "b = no_free_ptr(b)": the re-assignment restores b,
+ * causing __free(kfree) to free a live list node on exit.
+ */
+ list_add_tail(&no_free_ptr(b)->list, &tlob_uprobe_list);
+ return 0;
+}
+
+static int tlob_remove_uprobe_by_key(loff_t offset_start, const char *binpath)
+{
+ struct tlob_uprobe_binding *b, *tmp;
+ struct path remove_path;
+ struct inode *inode;
+ int ret;
+
+ ret = kern_path(binpath, LOOKUP_FOLLOW, &remove_path);
+ if (ret)
+ return ret;
+
+ inode = d_real_inode(remove_path.dentry);
+
+ ret = -ENOENT;
+ list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) {
+ if (b->offset_start != offset_start)
+ continue;
+ if (b->start_probe.inode != inode)
+ continue;
+ list_del(&b->list);
+ /*
+ * rv_uprobe_sync() may sleep, blocking tlob_monitor_read() on
+ * tlob_uprobe_mutex. Safe: list_del() above made the binding
+ * invisible to new readers before we drop the mutex.
+ */
+ rv_uprobe_unregister_nosync(&b->start_probe);
+ rv_uprobe_unregister_nosync(&b->stop_probe);
+ rv_uprobe_sync();
+ 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 rv_uprobe_sync() covers all probes dequeued above.
+ * After this, no handler_chain() iteration can access any consumer.
+ * The embedded uprobe_consumers in each binding are safe to free.
+ */
+ rv_uprobe_sync();
+
+ list_for_each_entry_safe(b, tmp, &pending, list) {
+ list_del(&b->list);
+ 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;
+ 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) {
+ pos += scnprintf(buf + pos, buf_sz - pos,
+ "p %s:0x%llx 0x%llx threshold=%llu\n",
+ b->binpath,
+ (unsigned long long)b->offset_start,
+ (unsigned long long)b->offset_stop,
+ b->threshold_ns);
+ }
+ 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=NS".
+ * PATH may contain ':'; the last ':' separates path from offset.
+ * Returns 0, -EINVAL, or -ERANGE.
+ */
+static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+ 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=NS */
+ while (p && (token = strsep(&p, " \t")) != NULL) {
+ if (!*token)
+ continue;
+ if (strncmp(token, "threshold=", 10) == 0) {
+ if (kstrtoull(token + 10, 0, &thr))
+ return -EINVAL;
+ if (thr < TLOB_MIN_THRESHOLD_NS || thr > TLOB_MAX_THRESHOLD_NS)
+ return -ERANGE;
+ got_thr = true;
+ } else if (!got_stop) {
+ long long sv;
+
+ n = 0;
+ if (sscanf(token, "%lli%n", &sv, &n) != 1 || n == 0)
+ return -EINVAL;
+ if (sv < 0)
+ return -EINVAL;
+ stop_val = (unsigned long long)sv;
+ got_stop = true;
+ } else {
+ return -EINVAL;
+ }
+ }
+
+ if (!got_stop || !got_thr)
+ return -EINVAL;
+ 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).
+ */
+VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out,
+ loff_t *start_out)
+{
+ 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;
+ if (off < 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_ns;
+ 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_ns, &binpath,
+ &offset_start, &offset_stop);
+ if (ret)
+ return ret;
+ mutex_lock(&tlob_uprobe_mutex);
+ ret = tlob_add_uprobe(threshold_ns, binpath, offset_start, offset_stop);
+ 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)
+{
+ unsigned int i;
+ int retval;
+
+ tlob_ws_storage = kcalloc(TLOB_MAX_MONITORED, sizeof(*tlob_ws_storage),
+ GFP_KERNEL);
+ if (!tlob_ws_storage)
+ return -ENOMEM;
+
+ for (i = 0; i < TLOB_MAX_MONITORED; i++)
+ llist_add(&tlob_ws_storage[i].free_node, &tlob_ws_free_list);
+
+ retval = ha_monitor_init();
+ if (retval) {
+ kfree(tlob_ws_storage);
+ tlob_ws_storage = NULL;
+ init_llist_head(&tlob_ws_free_list);
+ return retval;
+ }
+
+ rv_this.enabled = 1;
+ return 0;
+}
+
+static void __tlob_destroy_monitor(void)
+{
+ rv_this.enabled = 0;
+ tlob_remove_all_uprobes();
+ ha_monitor_destroy();
+ init_llist_head(&tlob_ws_free_list);
+ kfree(tlob_ws_storage);
+ tlob_ws_storage = NULL;
+}
+
+static int tlob_enable_hooks(void)
+{
+ rv_attach_trace_probe("tlob", sched_switch, handle_sched_switch);
+ rv_attach_trace_probe("tlob", sched_wakeup, handle_sched_wakeup);
+ rv_attach_trace_probe("tlob", sched_process_exit, handle_sched_process_exit);
+ return 0;
+}
+
+static void tlob_disable_hooks(void)
+{
+ rv_detach_trace_probe("tlob", sched_switch, handle_sched_switch);
+ rv_detach_trace_probe("tlob", sched_wakeup, handle_sched_wakeup);
+ rv_detach_trace_probe("tlob", sched_process_exit, handle_sched_process_exit);
+}
+
+static int enable_tlob(void)
+{
+ int retval;
+
+ retval = __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 int __init register_tlob(void)
+{
+ int ret;
+
+ ret = rv_register_monitor(&rv_this, NULL);
+ if (ret)
+ return ret;
+
+ if (rv_this.root_d) {
+ if (!rv_create_file("monitor", RV_MODE_WRITE, rv_this.root_d, NULL,
+ &tlob_monitor_fops)) {
+ rv_unregister_monitor(&rv_this);
+ return -ENOMEM;
+ }
+ }
+
+ return 0;
+}
+
+static void __exit unregister_tlob(void)
+{
+ rv_unregister_monitor(&rv_this);
+}
+
+module_init(register_tlob);
+module_exit(unregister_tlob);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Wen Yang <wen.yang@linux.dev>");
+MODULE_DESCRIPTION("tlob: task latency over budget per-task monitor.");
diff --git a/kernel/trace/rv/monitors/tlob/tlob.h b/kernel/trace/rv/monitors/tlob/tlob.h
new file mode 100644
index 000000000000..fceeba748c85
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob.h
@@ -0,0 +1,151 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _RV_TLOB_H
+#define _RV_TLOB_H
+
+/*
+ * C representation of the tlob hybrid automaton.
+ *
+ * Three-state HA following sched_stat / wwnr monitor naming conventions:
+ *
+ * running (initial) - task on CPU [sched_stat: runtime]
+ * waiting - task in runqueue [sched_stat: wait ]
+ * sleeping - task blocked [sched_stat: sleep ]
+ *
+ * Events (derived from sched_switch / sched_wakeup tracepoints):
+ * start - tlob_start_task() running -> running (resets clock)
+ * sleep - sched_switch, prev_state != 0 running -> sleeping
+ * preempt - sched_switch, prev_state == 0 running -> waiting
+ * wakeup - sched_wakeup sleeping -> waiting
+ * switch_in - sched_switch, next == task waiting -> running
+ *
+ * One HA clock invariant:
+ * clk_elapsed < BUDGET_NS() active in all states (total latency budget)
+ *
+ * tlob_start_task() uses da_handle_start_run_event(start_tlob) to initialise
+ * the monitor: the DA framework sets the initial state and then processes the
+ * start event, which resets clk_elapsed and arms the budget hrtimer via the
+ * generated ha_setup_invariants().
+ * tlob_stop_task() calls ha_cancel_timer_sync() + da_monitor_reset() directly.
+ *
+ * For the format description see:
+ * Documentation/trace/rv/deterministic_automata.rst
+ */
+
+#include <linux/rv.h>
+#include <linux/sched.h>
+
+#define MONITOR_NAME tlob
+
+enum states_tlob {
+ running_tlob,
+ sleeping_tlob,
+ waiting_tlob,
+ state_max_tlob,
+};
+
+#define INVALID_STATE state_max_tlob
+
+enum events_tlob {
+ preempt_tlob,
+ sleep_tlob,
+ start_tlob,
+ switch_in_tlob,
+ wakeup_tlob,
+ event_max_tlob,
+};
+
+/*
+ * HA environment variable: clk_elapsed is the only clock.
+ * It measures wall-clock time since task_start and is active in all states.
+ */
+enum envs_tlob {
+ clk_elapsed_tlob,
+ env_max_tlob,
+ env_max_stored_tlob = env_max_tlob,
+};
+
+_Static_assert(env_max_stored_tlob <= MAX_HA_ENV_LEN, "Not enough slots");
+#define HA_CLK_NS
+
+struct automaton_tlob {
+ char *state_names[state_max_tlob];
+ char *event_names[event_max_tlob];
+ char *env_names[env_max_tlob];
+ unsigned char function[state_max_tlob][event_max_tlob];
+ unsigned char initial_state;
+ bool final_states[state_max_tlob];
+};
+
+static const struct automaton_tlob automaton_tlob = {
+ .state_names = {
+ "running",
+ "sleeping",
+ "waiting",
+ },
+ .event_names = {
+ "preempt",
+ "sleep",
+ "start",
+ "switch_in",
+ "wakeup",
+ },
+ .env_names = {
+ "clk_elapsed",
+ },
+ .function = {
+ /* running */
+ {
+ waiting_tlob, /* preempt (sched_switch, prev_state == 0) */
+ sleeping_tlob, /* sleep (sched_switch, prev_state != 0) */
+ running_tlob, /* start (tlob_start_task, resets clock) */
+ INVALID_STATE, /* switch_in (already on CPU) */
+ INVALID_STATE, /* wakeup (TASK_RUNNING can't be woken) */
+ },
+ /* sleeping */
+ {
+ INVALID_STATE, /* preempt (not on CPU) */
+ INVALID_STATE, /* sleep (already sleeping) */
+ INVALID_STATE, /* start (not in running state) */
+ INVALID_STATE, /* switch_in (must go through waiting first) */
+ waiting_tlob, /* wakeup */
+ },
+ /* waiting */
+ {
+ INVALID_STATE, /* preempt (not on CPU) */
+ INVALID_STATE, /* sleep (not on CPU) */
+ INVALID_STATE, /* start (not in running state) */
+ running_tlob, /* switch_in */
+ INVALID_STATE, /* wakeup (already TASK_RUNNING) */
+ },
+ },
+ .initial_state = running_tlob,
+ .final_states = { 1, 0, 0 },
+};
+
+/* Maximum number of concurrently monitored tasks. */
+#define TLOB_MAX_MONITORED 64U
+
+/* Maximum binary path length for uprobe binding. */
+#define TLOB_MAX_PATH 256
+
+/* Minimum monitoring budget (1 us). */
+#define TLOB_MIN_THRESHOLD_NS 1000ULL
+
+/*
+ * Upper bound on the monitoring budget (1 hour = 3 600 000 000 000 ns).
+ * The ns-resolution accumulators (running_ns, waiting_ns, sleeping_ns)
+ * are u64; keeping the window below this limit ensures they stay well
+ * clear of u64 overflow and covers every realistic latency-monitoring
+ * use case.
+ */
+#define TLOB_MAX_THRESHOLD_NS 3600000000000ULL
+
+/* Exported to uprobe layer and KUnit tests */
+int tlob_start_task(struct task_struct *task, u64 threshold_ns);
+int tlob_stop_task(struct task_struct *task);
+
+#if IS_ENABLED(CONFIG_KUNIT)
+int tlob_create_or_delete_uprobe(char *buf);
+#endif /* CONFIG_KUNIT */
+
+#endif /* _RV_TLOB_H */
diff --git a/kernel/trace/rv/monitors/tlob/tlob_trace.h b/kernel/trace/rv/monitors/tlob/tlob_trace.h
new file mode 100644
index 000000000000..7ebdfbe41b86
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob_trace.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+/*
+ * Snippet to be included in rv_trace.h
+ */
+
+#ifdef CONFIG_RV_MON_TLOB
+DEFINE_EVENT(event_da_monitor_id, event_tlob,
+ TP_PROTO(int id, char *state, char *event,
+ char *next_state, bool final_state),
+ TP_ARGS(id, state, event, next_state, final_state));
+
+DEFINE_EVENT(error_da_monitor_id, error_tlob,
+ TP_PROTO(int id, char *state, char *event),
+ TP_ARGS(id, state, event));
+
+DEFINE_EVENT(error_env_da_monitor_id, error_env_tlob,
+ TP_PROTO(int id, char *state, char *event, char *env),
+ TP_ARGS(id, state, event, env));
+
+/*
+ * detail_env_tlob - per-state latency breakdown emitted on budget violation.
+ *
+ * Fired immediately after error_env_tlob from the hrtimer callback.
+ * Fields show how much time was spent in each DA state since tlob_start_task().
+ * running_ns + waiting_ns + sleeping_ns approximately equals total
+ * elapsed time (threshold_ns exceeded).
+ */
+TRACE_EVENT(detail_env_tlob,
+ TP_PROTO(int id, u64 threshold_ns,
+ u64 running_ns, u64 waiting_ns, u64 sleeping_ns),
+ TP_ARGS(id, threshold_ns, running_ns, waiting_ns, sleeping_ns),
+ TP_STRUCT__entry(
+ __field(int, id)
+ __field(u64, threshold_ns)
+ __field(u64, running_ns)
+ __field(u64, waiting_ns)
+ __field(u64, sleeping_ns)
+ ),
+ TP_fast_assign(
+ __entry->id = id;
+ __entry->threshold_ns = threshold_ns;
+ __entry->running_ns = running_ns;
+ __entry->waiting_ns = waiting_ns;
+ __entry->sleeping_ns = sleeping_ns;
+ ),
+ TP_printk("pid=%d threshold_ns=%llu"
+ " running_ns=%llu waiting_ns=%llu sleeping_ns=%llu",
+ __entry->id, __entry->threshold_ns,
+ __entry->running_ns, __entry->waiting_ns, __entry->sleeping_ns)
+);
+#endif /* CONFIG_RV_MON_TLOB */
diff --git a/kernel/trace/rv/rv_trace.h b/kernel/trace/rv/rv_trace.h
index 9622c269789c..a4bc215c1f15 100644
--- a/kernel/trace/rv/rv_trace.h
+++ b/kernel/trace/rv/rv_trace.h
@@ -189,6 +189,7 @@ DECLARE_EVENT_CLASS(error_env_da_monitor_id,
#include <monitors/stall/stall_trace.h>
#include <monitors/nomiss/nomiss_trace.h>
+#include <monitors/tlob/tlob_trace.h>
// Add new monitors based on CONFIG_HA_MON_EVENTS_ID here
#endif
--
2.25.1
^ permalink raw reply related
* [PATCH v4 7/8] rv/tlob: add KUnit tests for the tlob monitor
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
Add CONFIG_TLOB_KUNIT_TEST (tristate, depends on RV_MON_TLOB && KUNIT,
default KUNIT_ALL_TESTS) with a test suite covering the uprobe-line
parser.
Tests call tlob_parse_uprobe_line() and tlob_parse_remove_line()
directly rather than going through the top-level write handler, so
they exercise parser logic only without touching the uprobe or
filesystem subsystems.
Cases cover valid inputs, malformed paths and offsets (including
negative values), out-of-range thresholds, and valid and invalid
remove lines.
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
kernel/trace/rv/Makefile | 1 +
kernel/trace/rv/monitors/tlob/.kunitconfig | 8 ++
kernel/trace/rv/monitors/tlob/Kconfig | 7 ++
kernel/trace/rv/monitors/tlob/tlob.c | 7 +-
kernel/trace/rv/monitors/tlob/tlob.h | 4 +-
kernel/trace/rv/monitors/tlob/tlob_kunit.c | 139 +++++++++++++++++++++
6 files changed, 162 insertions(+), 4 deletions(-)
create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig
create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c
diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index ae59e97f8682..316d53398345 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -21,6 +21,7 @@ obj-$(CONFIG_RV_MON_STALL) += monitors/stall/stall.o
obj-$(CONFIG_RV_MON_DEADLINE) += monitors/deadline/deadline.o
obj-$(CONFIG_RV_MON_NOMISS) += monitors/nomiss/nomiss.o
obj-$(CONFIG_RV_MON_TLOB) += monitors/tlob/tlob.o
+obj-$(CONFIG_TLOB_KUNIT_TEST) += monitors/tlob/tlob_kunit.o
# Add new monitors here
obj-$(CONFIG_RV_UPROBE) += rv_uprobe.o
obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
diff --git a/kernel/trace/rv/monitors/tlob/.kunitconfig b/kernel/trace/rv/monitors/tlob/.kunitconfig
new file mode 100644
index 000000000000..34ebf3b172ac
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/.kunitconfig
@@ -0,0 +1,8 @@
+CONFIG_FTRACE=y
+CONFIG_HIGH_RES_TIMERS=y
+CONFIG_KUNIT=y
+CONFIG_MODULES=y
+CONFIG_RV=y
+CONFIG_RV_MON_TLOB=y
+CONFIG_TLOB_KUNIT_TEST=y
+CONFIG_UPROBES=y
diff --git a/kernel/trace/rv/monitors/tlob/Kconfig b/kernel/trace/rv/monitors/tlob/Kconfig
index aa43382073d2..402ef2e5c076 100644
--- a/kernel/trace/rv/monitors/tlob/Kconfig
+++ b/kernel/trace/rv/monitors/tlob/Kconfig
@@ -10,3 +10,10 @@ config RV_MON_TLOB
monitor. tlob tracks per-task elapsed wall-clock time across a
user-delimited code section and emits error_env_tlob when the
elapsed time exceeds a configurable per-invocation budget.
+
+config TLOB_KUNIT_TEST
+ tristate "KUnit tests for tlob monitor" if !KUNIT_ALL_TESTS
+ depends on RV_MON_TLOB && KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ Enable KUnit unit tests for the tlob RV monitor.
diff --git a/kernel/trace/rv/monitors/tlob/tlob.c b/kernel/trace/rv/monitors/tlob/tlob.c
index b45e84195131..a6f9c371646c 100644
--- a/kernel/trace/rv/monitors/tlob/tlob.c
+++ b/kernel/trace/rv/monitors/tlob/tlob.c
@@ -708,7 +708,7 @@ static ssize_t tlob_monitor_read(struct file *file,
* PATH may contain ':'; the last ':' separates path from offset.
* Returns 0, -EINVAL, or -ERANGE.
*/
-static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
+VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
char **path_out,
loff_t *start_out, loff_t *stop_out)
{
@@ -782,6 +782,7 @@ static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
*stop_out = (loff_t)stop_val;
return 0;
}
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line);
/*
* Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention).
@@ -810,8 +811,9 @@ VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out,
*start_out = (loff_t)off;
return 0;
}
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_remove_line);
-VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf)
+static int tlob_create_or_delete_uprobe(char *buf)
{
loff_t offset_start, offset_stop;
u64 threshold_ns;
@@ -836,7 +838,6 @@ VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf)
mutex_unlock(&tlob_uprobe_mutex);
return ret;
}
-EXPORT_SYMBOL_IF_KUNIT(tlob_create_or_delete_uprobe);
static ssize_t tlob_monitor_write(struct file *file,
const char __user *ubuf,
diff --git a/kernel/trace/rv/monitors/tlob/tlob.h b/kernel/trace/rv/monitors/tlob/tlob.h
index fceeba748c85..15bdf3b7fade 100644
--- a/kernel/trace/rv/monitors/tlob/tlob.h
+++ b/kernel/trace/rv/monitors/tlob/tlob.h
@@ -145,7 +145,9 @@ int tlob_start_task(struct task_struct *task, u64 threshold_ns);
int tlob_stop_task(struct task_struct *task);
#if IS_ENABLED(CONFIG_KUNIT)
-int tlob_create_or_delete_uprobe(char *buf);
+int tlob_parse_uprobe_line(char *buf, u64 *thr_out, char **path_out,
+ loff_t *start_out, loff_t *stop_out);
+int tlob_parse_remove_line(char *buf, char **path_out, loff_t *start_out);
#endif /* CONFIG_KUNIT */
#endif /* _RV_TLOB_H */
diff --git a/kernel/trace/rv/monitors/tlob/tlob_kunit.c b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
new file mode 100644
index 000000000000..7448f3fab959
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the tlob RV monitor.
+ *
+ */
+#include <kunit/test.h>
+
+#include "tlob.h"
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+
+/* Valid "p PATH:START STOP threshold=NS" lines. */
+static const char * const tlob_parse_valid[] = {
+ "p /usr/bin/myapp:4768 4848 threshold=5000000",
+ "p /usr/bin/myapp:0x12a0 0x12f0 threshold=10000000",
+ "p /opt/my:app/bin:0x100 0x200 threshold=1000000",
+};
+
+/* Malformed "p ..." lines that must be rejected with -EINVAL. */
+static const char * const tlob_parse_invalid[] = {
+ "p :0x100 0x200 threshold=5000",
+ "p /usr/bin/myapp:0x100 threshold=5000",
+ "p /usr/bin/myapp:-1 0x200 threshold=5000",
+ "p /usr/bin/myapp:0x100 -1 threshold=5000000", /* negative stop offset */
+ "p /usr/bin/myapp:0x100 0x200",
+ "p /usr/bin/myapp:0x100 0x100 threshold=5000",
+};
+
+/* threshold_ns out of valid range => -ERANGE. */
+static const char * const tlob_parse_out_of_range[] = {
+ "p /usr/bin/myapp:0x100 0x200 threshold=0",
+ "p /usr/bin/myapp:0x100 0x200 threshold=999",
+ "p /usr/bin/myapp:0x100 0x200 threshold=3600000000001",
+};
+
+/* Valid "-PATH:OFFSET_START" remove lines. */
+static const char * const tlob_remove_valid[] = {
+ "-/usr/bin/myapp:0x100",
+ "-/opt/my:app/bin:0x200",
+};
+
+/* Malformed remove lines that must be rejected with -EINVAL. */
+static const char * const tlob_remove_invalid[] = {
+ "-usr/bin/myapp:0x100",
+ "-/usr/bin/myapp",
+ "-/:0x100",
+ "-/usr/bin/myapp:-1", /* negative offset */
+ "-/usr/bin/myapp:abc",
+};
+
+static void tlob_parse_valid_accepted(struct kunit *test)
+{
+ u64 thr;
+ char *path;
+ loff_t start, stop;
+ char buf[128];
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(tlob_parse_valid); i++) {
+ strscpy(buf, tlob_parse_valid[i], sizeof(buf));
+ KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr, &path,
+ &start, &stop), 0);
+ }
+}
+
+static void tlob_parse_invalid_rejected(struct kunit *test)
+{
+ u64 thr;
+ char *path;
+ loff_t start, stop;
+ char buf[128];
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(tlob_parse_invalid); i++) {
+ strscpy(buf, tlob_parse_invalid[i], sizeof(buf));
+ KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr, &path,
+ &start, &stop), -EINVAL);
+ }
+}
+
+static void tlob_parse_out_of_range_rejected(struct kunit *test)
+{
+ u64 thr;
+ char *path;
+ loff_t start, stop;
+ char buf[128];
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(tlob_parse_out_of_range); i++) {
+ strscpy(buf, tlob_parse_out_of_range[i], sizeof(buf));
+ KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr, &path,
+ &start, &stop), -ERANGE);
+ }
+}
+
+static void tlob_remove_valid_accepted(struct kunit *test)
+{
+ char *path;
+ loff_t start;
+ char buf[128];
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(tlob_remove_valid); i++) {
+ strscpy(buf, tlob_remove_valid[i], sizeof(buf));
+ KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path, &start), 0);
+ }
+}
+
+static void tlob_remove_invalid_rejected(struct kunit *test)
+{
+ char *path;
+ loff_t start;
+ char buf[128];
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(tlob_remove_invalid); i++) {
+ strscpy(buf, tlob_remove_invalid[i], sizeof(buf));
+ KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path, &start), -EINVAL);
+ }
+}
+
+static struct kunit_case tlob_parse_cases[] = {
+ KUNIT_CASE(tlob_parse_valid_accepted),
+ KUNIT_CASE(tlob_parse_invalid_rejected),
+ KUNIT_CASE(tlob_parse_out_of_range_rejected),
+ KUNIT_CASE(tlob_remove_valid_accepted),
+ KUNIT_CASE(tlob_remove_invalid_rejected),
+ {}
+};
+
+static struct kunit_suite tlob_parse_suite = {
+ .name = "tlob_parse",
+ .test_cases = tlob_parse_cases,
+};
+
+kunit_test_suite(tlob_parse_suite);
+
+MODULE_DESCRIPTION("KUnit tests for the tlob RV monitor");
+MODULE_LICENSE("GPL");
--
2.25.1
^ permalink raw reply related
* [PATCH v4 8/8] selftests/verification: add tlob selftests
From: wen.yang @ 2026-07-08 15:38 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: Nam Cao, linux-trace-kernel, linux-kernel, Wen Yang
In-Reply-To: <cover.1783524627.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
Add seven ftrace-style test scripts for the tlob RV monitor under
tools/testing/selftests/verification/test.d/tlob/. The tests cover
uprobe binding management, budget violation detection, and per-state
time accounting.
Helper binaries tlob_target and tlob_sym are included in the same
directory so the suite is self-contained. tlob_sym resolves ELF
symbol offsets for uprobe registration; tlob_target provides busy-spin,
sleep, and preempt workloads.
ftracetest is updated to walk up the directory tree when searching for
test.d/functions, so monitor subdirectories can be passed as the test
directory without placing a dummy functions shim in each new directory.
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
tools/testing/selftests/ftrace/ftracetest | 18 +-
.../testing/selftests/verification/.gitignore | 2 +
tools/testing/selftests/verification/Makefile | 19 +-
.../verification/test.d/tlob/Makefile | 28 +++
.../test.d/tlob/run_tlob_tests.sh | 90 ++++++++
.../verification/test.d/tlob/tlob_sym.c | 209 ++++++++++++++++++
.../verification/test.d/tlob/tlob_target.c | 138 ++++++++++++
.../verification/test.d/tlob/uprobe_bind.tc | 37 ++++
.../test.d/tlob/uprobe_detail_running.tc | 51 +++++
.../test.d/tlob/uprobe_detail_sleeping.tc | 50 +++++
.../test.d/tlob/uprobe_detail_waiting.tc | 66 ++++++
.../verification/test.d/tlob/uprobe_multi.tc | 64 ++++++
.../test.d/tlob/uprobe_no_event.tc | 19 ++
.../test.d/tlob/uprobe_violation.tc | 67 ++++++
14 files changed, 854 insertions(+), 4 deletions(-)
create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
create mode 100755 tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_target.c
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
diff --git a/tools/testing/selftests/ftrace/ftracetest b/tools/testing/selftests/ftrace/ftracetest
index 0a56bf209f6c..91c007b0a74a 100755
--- a/tools/testing/selftests/ftrace/ftracetest
+++ b/tools/testing/selftests/ftrace/ftracetest
@@ -159,9 +159,21 @@ parse_opts() { # opts
if [ -n "$OPT_TEST_CASES" ]; then
TEST_CASES=$OPT_TEST_CASES
fi
- if [ -n "$OPT_TEST_DIR" -a -f "$OPT_TEST_DIR"/test.d/functions ]; then
- TOP_DIR=$OPT_TEST_DIR
- TEST_DIR=$TOP_DIR/test.d
+ if [ -n "$OPT_TEST_DIR" ]; then
+ # Walk up from OPT_TEST_DIR to find the nearest ancestor containing
+ # test.d/functions, allowing monitor subdirectories to be passed directly.
+ dir=$OPT_TEST_DIR
+ while [ "$dir" != "/" ]; do
+ if [ -f "$dir/test.d/functions" ]; then
+ TOP_DIR=$dir
+ TEST_DIR=$TOP_DIR/test.d
+ break
+ fi
+ dir=$(dirname "$dir")
+ done
+ if [ -z "$TOP_DIR" ]; then
+ errexit "no test.d/functions found above $OPT_TEST_DIR"
+ fi
fi
}
diff --git a/tools/testing/selftests/verification/.gitignore b/tools/testing/selftests/verification/.gitignore
index 2659417cb2c7..cbbd03ee16c7 100644
--- a/tools/testing/selftests/verification/.gitignore
+++ b/tools/testing/selftests/verification/.gitignore
@@ -1,2 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
logs
+test.d/tlob/tlob_sym
+test.d/tlob/tlob_target
diff --git a/tools/testing/selftests/verification/Makefile b/tools/testing/selftests/verification/Makefile
index aa8790c22a71..0b32bdfdb8db 100644
--- a/tools/testing/selftests/verification/Makefile
+++ b/tools/testing/selftests/verification/Makefile
@@ -1,8 +1,25 @@
# SPDX-License-Identifier: GPL-2.0
-all:
TEST_PROGS := verificationtest-ktap
TEST_FILES := test.d settings
EXTRA_CLEAN := $(OUTPUT)/logs/*
+# Subdirectories that provide binaries used by the test runner.
+# Each entry must contain a Makefile that accepts OUTDIR= and
+# deposits its binaries there.
+BUILD_SUBDIRS := test.d/tlob
+
include ../lib.mk
+
+all: $(patsubst %,_build_%,$(BUILD_SUBDIRS))
+
+clean: $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
+
+.PHONY: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) \
+ $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
+
+$(patsubst %,_build_%,$(BUILD_SUBDIRS)): _build_%:
+ $(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)"
+
+$(patsubst %,_clean_%,$(BUILD_SUBDIRS)): _clean_%:
+ $(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
diff --git a/tools/testing/selftests/verification/test.d/tlob/Makefile b/tools/testing/selftests/verification/test.d/tlob/Makefile
new file mode 100644
index 000000000000..05a2d2599c4e
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/Makefile
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0
+# Builds tlob selftest helper binaries in the directory of this Makefile.
+#
+# Invoked by ../../Makefile via BUILD_SUBDIRS; outputs tlob_sym and
+# tlob_target alongside the .tc scripts so they are self-contained.
+
+CFLAGS += $(TOOLS_INCLUDES)
+
+# For standalone execution via vng
+FTRACETEST := ../../../ftrace/ftracetest
+LOGDIR ?= ../../logs
+
+.PHONY: all
+all: tlob_sym tlob_target
+
+tlob_sym: tlob_sym.c
+ $(CC) $(CFLAGS) -o $@ $<
+
+tlob_target: tlob_target.c
+ $(CC) $(CFLAGS) -o $@ $<
+
+.PHONY: run_tests
+run_tests: all
+ @./run_tlob_tests.sh
+
+.PHONY: clean
+clean:
+ $(RM) tlob_sym tlob_target
diff --git a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
new file mode 100755
index 000000000000..cd949756e713
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Standalone runner for tlob selftests
+# Usage: ./run_tlob_tests.sh [options]
+#
+# Options:
+# -v, --verbose Verbose output
+# -k, --keep Keep test logs
+# -l, --logdir DIR Log directory (default: ../../logs)
+# -h, --help Show this help
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+FTRACETEST="$SCRIPT_DIR/../../../ftrace/ftracetest"
+LOGDIR="$SCRIPT_DIR/../../logs"
+VERBOSE=""
+KEEP=""
+EXTRA_ARGS=""
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -v|--verbose)
+ VERBOSE="-v"
+ shift
+ ;;
+ -k|--keep)
+ KEEP="-k"
+ shift
+ ;;
+ -l|--logdir)
+ LOGDIR="$2"
+ shift 2
+ ;;
+ -h|--help)
+ echo "Usage: $0 [options]"
+ echo ""
+ echo "Options:"
+ echo " -v, --verbose Verbose output"
+ echo " -k, --keep Keep test logs"
+ echo " -l, --logdir DIR Log directory (default: ../../logs)"
+ echo " -h, --help Show this help"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Run all tlob tests"
+ echo " $0 -v # Run with verbose output"
+ echo " $0 -v -l /tmp/tlob-logs # Custom log directory"
+ echo ""
+ echo "With vng:"
+ echo " vng -v --rwdir $LOGDIR -- $0"
+ exit 0
+ ;;
+ *)
+ EXTRA_ARGS="$EXTRA_ARGS $1"
+ shift
+ ;;
+ esac
+done
+
+# Build test helpers
+echo "Building tlob test helpers..."
+make -C "$SCRIPT_DIR" all
+
+# Check ftracetest exists
+if [ ! -x "$FTRACETEST" ]; then
+ echo "Error: $FTRACETEST not found or not executable"
+ echo "Make sure you're running from the correct directory"
+ exit 1
+fi
+
+# Create log directory
+mkdir -p "$LOGDIR"
+
+# Run tests
+echo "Running tlob selftests..."
+echo "Log directory: $LOGDIR"
+echo ""
+
+# Export RV_BINDIR so test scripts can find tlob_target and tlob_sym
+export RV_BINDIR="$SCRIPT_DIR"
+
+# Pass the test directory, not individual .tc files
+# ftracetest will discover all .tc files in the directory
+"$FTRACETEST" -K $VERBOSE $KEEP --rv --logdir "$LOGDIR" \
+ "$SCRIPT_DIR" $EXTRA_ARGS
+
+echo ""
+echo "Tests completed. Logs saved to: $LOGDIR"
diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c b/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
new file mode 100644
index 000000000000..a92fc49d1304
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
@@ -0,0 +1,209 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tlob_sym.c - ELF symbol-to-file-offset utility for tlob selftests
+ *
+ * Usage: tlob_sym sym_offset <binary> <symbol>
+ *
+ * Prints the ELF file offset of <symbol> in <binary> to stdout.
+ *
+ * Exit: 0 = found, 1 = error / not found.
+ */
+#include <elf.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+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 *shstrtab_hdr;
+
+ if (ehdr->e_shnum == 0 || ehdr->e_shstrndx >= ehdr->e_shnum ||
+ (uint64_t)ehdr->e_shoff +
+ (uint64_t)ehdr->e_shnum * sizeof(Elf64_Shdr) > (uint64_t)st.st_size) {
+ fprintf(stderr, "%s: malformed ELF section table\n", binary);
+ munmap(map, (size_t)st.st_size);
+ return 1;
+ }
+ shdrs = (Elf64_Shdr *)((char *)map + ehdr->e_shoff);
+ shstrtab_hdr = &shdrs[ehdr->e_shstrndx];
+ const char *shstrtab = (char *)map + shstrtab_hdr->sh_offset;
+ int si;
+
+ 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->sh_offset;
+ Elf64_Sym *syms = (Elf64_Sym *)((char *)map + sh->sh_offset);
+ uint64_t nsyms = sh->sh_size / sizeof(Elf64_Sym);
+ uint64_t j;
+
+ for (j = 0; j < nsyms; j++) {
+ if (strcmp(strtab + syms[j].st_name, symname) == 0) {
+ sym_vaddr = syms[j].st_value;
+ found = 1;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!found) {
+ fprintf(stderr, "symbol '%s' not found in %s\n", symname, binary);
+ munmap(map, (size_t)st.st_size);
+ return 1;
+ }
+
+ Elf64_Phdr *phdrs = (Elf64_Phdr *)((char *)map + ehdr->e_phoff);
+ 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->p_offset;
+ break;
+ }
+ }
+ } else {
+ Elf32_Shdr *shdrs;
+ Elf32_Shdr *shstrtab_hdr;
+
+ if (ehdr32->e_shnum == 0 || ehdr32->e_shstrndx >= ehdr32->e_shnum ||
+ (uint64_t)ehdr32->e_shoff +
+ (uint64_t)ehdr32->e_shnum * sizeof(Elf32_Shdr) > (uint64_t)st.st_size) {
+ fprintf(stderr, "%s: malformed ELF section table\n", binary);
+ munmap(map, (size_t)st.st_size);
+ return 1;
+ }
+ shdrs = (Elf32_Shdr *)((char *)map + ehdr32->e_shoff);
+ 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->sh_offset;
+ Elf32_Sym *syms = (Elf32_Sym *)((char *)map + sh->sh_offset);
+ uint32_t nsyms = sh->sh_size / sizeof(Elf32_Sym);
+ uint32_t j;
+
+ for (j = 0; j < nsyms; j++) {
+ if (strcmp(strtab + syms[j].st_name, symname) == 0) {
+ sym_vaddr32 = syms[j].st_value;
+ found = 1;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!found) {
+ fprintf(stderr, "symbol '%s' not found in %s\n", symname, binary);
+ munmap(map, (size_t)st.st_size);
+ return 1;
+ }
+
+ Elf32_Phdr *phdrs = (Elf32_Phdr *)((char *)map + ehdr32->e_phoff);
+ 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->p_offset;
+ 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[])
+{
+ if (argc != 4 || strcmp(argv[1], "sym_offset") != 0) {
+ fprintf(stderr, "Usage: %s sym_offset <binary> <symbol>\n", argv[0]);
+ return 1;
+ }
+ return sym_offset(argv[2], argv[3]);
+}
diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_target.c b/tools/testing/selftests/verification/test.d/tlob/tlob_target.c
new file mode 100644
index 000000000000..adf4c2397fb3
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/tlob_target.c
@@ -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 + RT competitor:
+ * waiting_ns dominates
+ *
+ * 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).
+ */
+#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_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_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);
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ } while (timespec_before(&now, &deadline));
+
+ return 0;
+}
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
new file mode 100644
index 000000000000..4a1c18c7485a
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
@@ -0,0 +1,37 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor uprobe binding (visible in monitor file, removable, duplicate rejected)
+# requires: tlob:monitor
+
+RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
+UPROBE_TARGET="${RV_BINDIR}/tlob_target"
+TLOB_SYM="${RV_BINDIR}/tlob_sym"
+[ -x "$UPROBE_TARGET" ] || exit_unsupported
+[ -x "$TLOB_SYM" ] || exit_unsupported
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
+stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+[ -n "$busy_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > monitors/tlob/enable
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=5000000000" > "$TLOB_MONITOR"
+
+# Binding must appear in monitor file with canonical hex-offset format.
+grep -qE "^p ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=[0-9]+$" "$TLOB_MONITOR"
+grep -q "threshold=5000000000" "$TLOB_MONITOR"
+
+# Duplicate offset_start must be rejected.
+! echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=9999000" > "$TLOB_MONITOR" 2>/dev/null || false
+
+# Remove the binding; it must no longer appear.
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR"
+! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR" || false
+
+kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+echo 0 > monitors/tlob/enable
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
new file mode 100644
index 000000000000..afca157b5ea4
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
@@ -0,0 +1,51 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor detail running (running_ns dominates when task busy-spins between probes)
+# requires: tlob:monitor
+
+RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
+UPROBE_TARGET="${RV_BINDIR}/tlob_target"
+TLOB_SYM="${RV_BINDIR}/tlob_sym"
+[ -x "$UPROBE_TARGET" ] || exit_unsupported
+[ -x "$TLOB_SYM" ] || exit_unsupported
+TLOB_MONITOR=monitors/tlob/monitor
+
+start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
+stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+[ -n "$start_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
+
+"$UPROBE_TARGET" 5000 &
+busy_pid=$!
+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
+
+# 10 us budget; task busy-spins 200 ms per iteration -> running_ns dominates.
+echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=10000" > "$TLOB_MONITOR"
+
+found=0; i=0
+while [ "$i" -lt 30 ]; do
+ sleep 0.1
+ grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=1; break; }
+ i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$busy_pid" 2>/dev/null || true; 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/')
+# Busy-spin keeps the task on-CPU: running_ns must exceed sleeping_ns.
+[ "$running" -gt "$sleeping" ]
+
+echo > /sys/kernel/tracing/trace
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..0a6470b4cadb
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
@@ -0,0 +1,50 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor detail sleeping (sleeping_ns dominates when task blocks between probes)
+# requires: tlob:monitor
+
+RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
+UPROBE_TARGET="${RV_BINDIR}/tlob_target"
+TLOB_SYM="${RV_BINDIR}/tlob_sym"
+[ -x "$UPROBE_TARGET" ] || exit_unsupported
+[ -x "$TLOB_SYM" ] || exit_unsupported
+TLOB_MONITOR=monitors/tlob/monitor
+
+start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2>/dev/null)
+stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work_done 2>/dev/null)
+[ -n "$start_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
+
+"$UPROBE_TARGET" 5000 sleep &
+busy_pid=$!
+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=50000000" > "$TLOB_MONITOR"
+
+found=0; i=0
+while [ "$i" -lt 30 ]; do
+ sleep 0.1
+ grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=1; break; }
+ i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$busy_pid" 2>/dev/null || true; 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..ef22fce700fc
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
@@ -0,0 +1,66 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor detail waiting (waiting_ns dominates when task is preempted between probes)
+# requires: tlob:monitor
+
+RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
+UPROBE_TARGET="${RV_BINDIR}/tlob_target"
+TLOB_SYM="${RV_BINDIR}/tlob_sym"
+[ -x "$UPROBE_TARGET" ] || exit_unsupported
+[ -x "$TLOB_SYM" ] || exit_unsupported
+TLOB_MONITOR=monitors/tlob/monitor
+
+command -v chrt > /dev/null || exit_unsupported
+command -v taskset > /dev/null || exit_unsupported
+
+start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work 2>/dev/null)
+stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work_done 2>/dev/null)
+[ -n "$start_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
+
+cpu=0
+
+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=500000000" > "$TLOB_MONITOR"
+
+# 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; }
+ i=$((i+1))
+done
+
+# Kill the RT hog first so tlob_target can release any in-flight SRCU read
+# section from uprobe_notify_resume; otherwise probe removal blocks in
+# synchronize_srcu with the hog monopolising the CPU at FIFO-99.
+kill "$hog_pid" 2>/dev/null || true; wait "$hog_pid" 2>/dev/null || true
+kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null
+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
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
new file mode 100644
index 000000000000..a798f3e9b3fa
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
@@ -0,0 +1,64 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor multiple uprobe bindings (different offsets fire independently)
+# requires: tlob:monitor
+
+RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
+UPROBE_TARGET="${RV_BINDIR}/tlob_target"
+TLOB_SYM="${RV_BINDIR}/tlob_sym"
+[ -x "$UPROBE_TARGET" ] || exit_unsupported
+[ -x "$TLOB_SYM" ] || exit_unsupported
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
+busy_stop=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+sleep_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2>/dev/null)
+sleep_stop=$("$TLOB_SYM" 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
+
+"$UPROBE_TARGET" 30000 & # busy mode: tlob_busy_work fires every 200 ms
+busy_pid=$!
+"$UPROBE_TARGET" 30000 sleep & # sleep mode: tlob_sleep_work fires every 200 ms
+sleep_pid=$!
+sleep 0.05
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+# Binding A: 5 s budget on the busy probe - must not fire in 200 ms loops.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=5000000000" > "$TLOB_MONITOR"
+# Binding B: 10 us budget on the sleep probe - fires on first invocation.
+echo "p ${UPROBE_TARGET}:${sleep_offset} ${sleep_stop} threshold=10000" > "$TLOB_MONITOR"
+
+# 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; }
+ 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 || true; wait "$sleep_pid" 2>/dev/null || true
+kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+
+[ "$found" = "1" ]
+# error_env_tlob payload: clock variable must be present.
+# The event field can be "budget_exceeded" (hrtimer path) or the DA event
+# name ("sleep", "preempt") depending on which fires first; don't constrain it.
+grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q "clk_elapsed="
+# detail_env_tlob must appear alongside the error.
+grep -q "detail_env_tlob" /sys/kernel/tracing/trace
+
+echo > /sys/kernel/tracing/trace
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
new file mode 100644
index 000000000000..bb2eeef17019
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
@@ -0,0 +1,19 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor no spurious events without active uprobe binding
+# requires: tlob:monitor
+
+TLOB_MONITOR=monitors/tlob/monitor
+
+echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 1 > /sys/kernel/tracing/tracing_on
+echo 1 > monitors/tlob/enable
+echo > /sys/kernel/tracing/trace
+
+sleep 0.5
+
+! grep -q "error_env_tlob" /sys/kernel/tracing/trace || false
+
+echo 0 > monitors/tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo > /sys/kernel/tracing/trace
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..8a94bd679b88
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
@@ -0,0 +1,67 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test tlob monitor budget violation (error_env_tlob and detail_env_tlob fire with correct fields)
+# requires: tlob:monitor
+
+RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
+UPROBE_TARGET="${RV_BINDIR}/tlob_target"
+TLOB_SYM="${RV_BINDIR}/tlob_sym"
+[ -x "$UPROBE_TARGET" ] || exit_unsupported
+[ -x "$TLOB_SYM" ] || exit_unsupported
+TLOB_MONITOR=monitors/tlob/monitor
+
+busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
+stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
+[ -n "$busy_offset" ] || exit_unsupported
+[ -n "$stop_offset" ] || exit_unsupported
+
+"$UPROBE_TARGET" 30000 &
+busy_pid=$!
+sleep 0.05
+
+echo 1 > /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 us budget - fires almost immediately; task is busy-spinning on-CPU.
+echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=10000" > "$TLOB_MONITOR"
+
+# 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; }
+ i=$((i+1))
+done
+
+echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null
+kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true
+echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable
+echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable
+echo 0 > monitors/tlob/enable
+
+[ "$found" = "1" ]
+
+# error_env_tlob must carry the clk_elapsed environment field.
+# The event label is "budget_exceeded" when detected by the hrtimer callback,
+# or the triggering sched event name when detected by the constraint path on a
+# preemption that races with the timer (common on PREEMPT_RT / VM). Both are
+# valid detections; check the env field instead of the label.
+grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q "clk_elapsed="
+
+# 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_ns=10000"
+echo "$line" | grep -q "running_ns="
+echo "$line" | grep -q "waiting_ns="
+echo "$line" | grep -q "sleeping_ns="
+
+# Busy-spin keeps the task on-CPU: running_ns must exceed sleeping_ns.
+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
--
2.25.1
^ permalink raw reply related
* Re: [PATCH 1/3] rv/reactors: fix lockdep "Invalid wait context" in rv_react()
From: Thomas Weißschuh @ 2026-07-08 15:47 UTC (permalink / raw)
To: Gabriele Monaco; +Cc: wen.yang, Nam Cao, linux-trace-kernel, linux-kernel
In-Reply-To: <3c53f741559f834986e976b524c5ded90bbd5627.camel@redhat.com>
On Mon, Jul 06, 2026 at 04:02:08PM +0200, Gabriele Monaco wrote:
> On Tue, 2026-06-23 at 11:38 +0200, Thomas Weißschuh wrote:
> > This now allows reactors to take (raw) spinlocks. The original idea was
> > to not allow that as a reactor can be called from LD_WAIT_FREE context.
> > So I am not sure this is the right fix. Not that I have a better one
> > available right now.
>
> As far as I understand it, LD_WAIT_FREE is fairly impossible to apply on
> preemptible code (here we see it hit by an interrupt).
>
> Since we kind of have to allow raw spinlock to avoid this (even if we don't take
> them explicitly), why wouldn't a reactor ever be allowed to take raw spinlocks?
>
> Technically it wouldn't be wrong to take locks from RV monitors, although most
> monitors don't do it explicitly.
> If we ever happen to take the wrong lock explicitly from a reactor triggered by
> a nasty event (e.g. sched_switch), I believe lockdep would still be complaining
> down that path, so we probably don't need to be too strict in rv_react().
>
> Obviously we don't want to disable interrutps for LD_WAIT_FREE to hold either.
>
> Am I missing something?
No, I was just very confused.
Let's go ahead with this.
Thomas
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox