Hello,

It seems that code executed during IRQ entry/exit may be traced as task context
because HARDIRQ_OFFSET is not reflected in preempt_count until irq_enter_rcu()
is called. As a result, both in_task() and in_hardirq() may incorrectly classify
some IRQ-entry code, causing IRQ-related functions to appear even when IRQ
tracing is disabled.


I've been developing a minimal syscall tracer using ftrace's api from a kernel
module. During the process I've tried to prevent interrupt-related code from
appearing in the trace data, therefore I use the in_task() macro to filter
everything that is not in running in "task context". However, some functions
executed in hardirq context bypass the check, as they are executed before
preempt_count is updated.

I was not certain whether this was a bug on how the flag is set or the intended
behaviour, so, I traced the same code using the function-graph-tracer with
'funcgraph-irqs' option active, but I saw the same behaviour.


I'm using a fully preemptive kernel, compiled for both aarch64 (this one runs
on an evaluation board) and riscv (this one runs on qemu). I have not checked
if it happens in any other arch. I first saw this behaviour in v6.15.5 with
aarch64, but it's still present in v7.2.2. I have not checked older versions.


The root of the problem seems to be that there is instrumentable code that
executes on every IRQ before and after HARDIRQ_OFFSET is added/substracted to
preempt_count, so function tracers cannot correctly filter IRQ using 
in_hardirq().

        (All the source I show comes from kernel v7.2.2)

        In file: "/arch/arm64/kernel/entry-common.c" (lines 501-513)

                static __always_inline void __el1_irq(struct pt_regs *regs,
                                                      void (*handler)(struct 
pt_regs *))
                {
                        irqentry_state_t state;

                        state = arm64_enter_from_kernel_mode(regs);

                        irq_enter_rcu();  // <-- 
`preempt_count_add(HARDIRQ_OFFSET)`
                        do_interrupt_handler(regs, handler);
                        irq_exit_rcu();   // <-- 
`preempt_count_sub(HARDIRQ_OFFSET);`

                        arm64_exit_to_kernel_mode(regs, state);
                }

I've tried to wrap this function between preempt_count_add(HARDIRQ_OFFSET)
and preempt_count_sub(HARDIRQ_OFFSET), but kernel won't finish booting up.

Similarly, this also happens in riscv, as it can be seen in file
"/arch/riscv/kernel/traps.c" (lines 424-445) that irq_enter_rcu is called after
the equivalent function to arm64_enter_from_kernel_mode.


Test using function graph tracer:

As I said, I wanted to check if IRQs also appeared inside the trace file while
option funcgraph-irqs was disabled. Ftrace's documentation
(https://docs.kernel.org/trace/ftrace.html) decribes this parameter as follows:

        "When disabled, functions that happen inside an interrupt will not be 
traced."

so I expected it to remove every function called inside __el1_irq from the 
trace.

I isolated CPU 3 for it to run the test program alone with taskset.
I traced with the following configuration:

        TRACEFS="/sys/kernel/tracing"

        # Stop tracing and clean buffer
        echo 0 > "${TRACEFS}/tracing_on"
        : > "${TRACEFS}/trace"

        # Trace only CPU 3 with 1GB buffer
        echo 8 > "${TRACEFS}/tracing_cpumask"
        echo 1048576 > "${TRACEFS}/per_cpu/cpu3/buffer_size_kb"

        echo local > "${TRACEFS}/trace_clock"
        echo function_graph > "${TRACEFS}/current_tracer"

        for opt in 'funcgraph-proc' 'funcgraph-tail' 'irq-info' 
'nofuncgraph-irqs'; do
                echo "$opt" > "${TRACEFS}/trace_options"
        done

        # Clean filters and trace only syscalls
        : > "${TRACEFS}/set_graph_function"
        echo "__arm64_sys_*" > "${TRACEFS}/set_graph_function"

The resulting trace shows only syscalls code, being sometimes interrupted by
code inside __el1_irq:

 ...
 3) randbyt-14268  |               |            mntput() {                  
//__arm64_sys_openat
 3) randbyt-14268  |               |              mntput_no_expire() {      
//__arm64_sys_openat
 3) randbyt-14268  |   1.180 us    |                
__rcu_irq_enter_check_tick(); //IRQ CODE
 3) randbyt-14268  |   1.900 us    |                irq_enter_rcu();
 3) randbyt-14268  |   1.340 us    |                idle_cpu();
 3) randbyt-14268  |               |                tick_nohz_irq_exit() {
 ...
 3) randbyt-14268  | + 13.072 us   |                } /* tick_nohz_irq_exit */
 3) randbyt-14268  |               |                
raw_irqentry_exit_cond_resched() {
 3) randbyt-14268  |               |                  preempt_schedule_irq() {
 ...
 3) randbyt-14268  | + 73.637 us   |                  } /* preempt_schedule_irq 
*/
 3) randbyt-14268  | + 75.938 us   |                } /* 
raw_irqentry_exit_cond_resched */
 3) randbyt-14268  |   0.960 us    |                __rcu_read_lock();
 3) randbyt-14268  |   1.070 us    |                __rcu_read_unlock();    
//IRQ CODE
 3) randbyt-14268  | ! 146.225 us  |              } /* mntput_no_expire */  
//__arm64_sys_openat
 3) randbyt-14268  | ! 148.385 us  |            } /* mntput */              
//__arm64_sys_openat
 ...

>From here I suspected that funcgraph-irqs' implementation relied on the same
flags I checked in my custom tracer, so I searched inside the graph tracer code
and found this early-exit condition being called from graph_entry:

        In file: "/kernel/trace/trace_functions_graph.c" (lines 209-217)

                static inline int ftrace_graph_ignore_irqs(struct trace_array 
*tr)
                {
                        if (!ftrace_graph_skip_irqs || 
trace_recursion_test(TRACE_IRQ_BIT))
                                return 0;

                        if (tracer_flags_is_set(tr, TRACE_GRAPH_PRINT_IRQS))
                                return 0;

                        return in_hardirq();
                }

Both in_task() an in_hardirq() macros check preempt_count, which has the fault
of not being fully set before any code executes on IRQ context.


I do not know whether preempt_count is intended to provide precise context
information during IRQ entry/exit transitions. If not, would there be a
recommended mechanism for tracers that need to distinguish task and IRQ contexts
during these early entry stages?

For my tracer I patched __el1_irq in aarch64, making it noinline instead of
__always_inline. That way I can catch its beginning and end registering a
ftrace_direct routine with the minimal changes to vanilla Linux.
I have not tried any fix for riscv.

Is this behavior intentional, meaning that code executed before irq_enter_rcu()
is considered outside hardirq context, or is this an unintended limitation of
tracing based on preempt_count?

Reply via email to