From e666e652835937b8ff2cc739ee7471611ad38522 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Wed, 30 Sep 2026 00:09:41 +0900 Subject: [PATCH 1/9] selftests/ftrace: Add generic boot tracing test framework Add a generic test framework under tools/testing/selftests/ftrace/boottime/ to verify kernel boot tracing configurations (bootconfig, kernel command-line tracing options, and persistent ring buffers) during early boot. The test runner (run_boottime_test.sh) sources ktap_helpers.sh to generate output in TAP version 13 format. It builds a minimal initramfs with busybox, dynamically resolves test specifications (.bconf for bootconfig, .cmdline for kernel boot parameters, .qemuopts for QEMU options, and # REBOOT: 1 for multi-boot crash/reboot tests with doubled timeout), boots QEMU, and runs corresponding tracefs checker scripts. Link: https://lore.kernel.org/all/178649541846.438282.672153252242971310.stgit@devnote2/ Assisted-by: LLM Signed-off-by: Masami Hiramatsu (Google) --- tools/testing/selftests/ftrace/Makefile | 4 +- tools/testing/selftests/ftrace/boottime-ktap | 6 + .../selftests/ftrace/boottime/Makefile | 10 + .../testing/selftests/ftrace/boottime/README | 74 ++++ .../ftrace/boottime/run_boottime_test.sh | 403 ++++++++++++++++++ tools/testing/selftests/ftrace/config | 6 + 6 files changed, 501 insertions(+), 2 deletions(-) create mode 100755 tools/testing/selftests/ftrace/boottime-ktap create mode 100644 tools/testing/selftests/ftrace/boottime/Makefile create mode 100644 tools/testing/selftests/ftrace/boottime/README create mode 100755 tools/testing/selftests/ftrace/boottime/run_boottime_test.sh diff --git a/tools/testing/selftests/ftrace/Makefile b/tools/testing/selftests/ftrace/Makefile index 7c12263f8260..3d41f9545683 100644 --- a/tools/testing/selftests/ftrace/Makefile +++ b/tools/testing/selftests/ftrace/Makefile @@ -2,8 +2,8 @@ all: TEST_PROGS_EXTENDED := ftracetest -TEST_PROGS := ftracetest-ktap -TEST_FILES := test.d settings +TEST_PROGS := ftracetest-ktap boottime-ktap +TEST_FILES := test.d settings boottime EXTRA_CLEAN := $(OUTPUT)/logs/* TEST_GEN_FILES := poll diff --git a/tools/testing/selftests/ftrace/boottime-ktap b/tools/testing/selftests/ftrace/boottime-ktap new file mode 100755 index 000000000000..1fed68c8a944 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime-ktap @@ -0,0 +1,6 @@ +#!/bin/sh -e +# SPDX-License-Identifier: GPL-2.0-only +# +# boottime-ktap: Wrapper to integrate boottime tracing test framework with kselftest runner + +exec ./boottime/run_boottime_test.sh "$@" diff --git a/tools/testing/selftests/ftrace/boottime/Makefile b/tools/testing/selftests/ftrace/boottime/Makefile new file mode 100644 index 000000000000..fd0975350f10 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0 + +all: + +run_tests: + @./run_boottime_test.sh + +clean: + +.PHONY: all run_tests clean diff --git a/tools/testing/selftests/ftrace/boottime/README b/tools/testing/selftests/ftrace/boottime/README new file mode 100644 index 000000000000..1eb0191d75d4 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/README @@ -0,0 +1,74 @@ +Boot Tracing Test Suite +======================= + +This directory contains an automated test framework to verify kernel boot +tracing configurations (bootconfig, kernel command-line tracing options, and +persistent ring buffers) during early boot. + +Overview +-------- +The test runner (`run_boottime_test.sh`) constructs a lightweight initramfs with +busybox and a tracefs checker script from `tests/`, attaches optional bootconfigs +from `bootconfigs/` using the `bootconfig` tool, applies optional command-line +parameters from `cmdlines/`, boots the target kernel image under QEMU, and reports +results according to TAP version 13 format. + +Test Specification Format +------------------------- +For each test script `tests/.sh`, the harness dynamically detects: +- `bootconfigs/.bconf` or `.bconf`: Bootconfig configuration file. +- `cmdlines/.cmdline` or `# CMDLINE: ` header in script: Kernel boot parameters. +- `qemuopts/.qemuopts` or `# QEMUOPTS: ` header in script: Additional QEMU CLI arguments. +- `# APPLETS: ` header in script: Additional BusyBox applet symlinks to create in initramfs. +- `# REBOOT: 1` header in script: Multi-boot reboot/crash test mode (omits `-no-reboot` and doubles timeout). + +Required Tools +-------------- +- Host Architecture: x86 (x86_64/i686/i386/x86; tests skip on non-x86 hosts) +- `qemu-system-x86_64` (or custom QEMU executable) +- `busybox` (host binary at `/usr/bin/busybox` or custom path) +- `bootconfig` (compiled from `tools/bootconfig/bootconfig` or in PATH) +- `file` (standard file architecture identifier) +- `cpio` (standard archiver) +- `timeout` (standard coreutils execution timer) + +Required Kernel Configurations +------------------------------ +The kernel binary under test must be compiled with: +- `CONFIG_BOOT_CONFIG=y` +- `CONFIG_BOOTTIME_TRACING=y` +- `CONFIG_MAGIC_SYSRQ=y` (for guest reboot and poweroff) + +Additional feature-specific config options enable respective testcases: +- `CONFIG_KPROBE_EVENTS=y` (for kprobe tests) +- `CONFIG_SYNTH_EVENTS=y` (for synthetic event tests) +- `CONFIG_EPROBE_EVENTS=y` (for eprobe tests) +- `CONFIG_FPROBE_EVENTS=y` (for fprobe and tracepoint probe tests) +- `CONFIG_FUNCTION_TRACER=y` (for tracer options) +- `CONFIG_RESERVE_MEM=y` (for persistent ring buffer tests) + +Usage +----- +To run all test cases with a compiled kernel image: + + $ ./run_boottime_test.sh -k /path/to/bzImage + +To run a specific test case (e.g. `01-kprobe`): + + $ ./run_boottime_test.sh -k /path/to/bzImage -t 01-kprobe + +Customizing Tool Paths: + + $ ./run_boottime_test.sh -b /path/to/bootconfig \ + -k /path/to/bzImage \ + -q /path/to/qemu-system-x86_64 \ + -B /path/to/busybox + +Environment Variables +--------------------- +Tool locations and kernel paths can also be set via environment variables: +- `BOOTCONFIG`: Path to the `bootconfig` tool +- `KERNEL`: Path to the `bzImage` kernel binary +- `QEMU`: Path to the QEMU binary +- `BUSYBOX`: Path to the `busybox` binary +- `LOGDIR`: Directory path to preserve QEMU execution log files diff --git a/tools/testing/selftests/ftrace/boottime/run_boottime_test.sh b/tools/testing/selftests/ftrace/boottime/run_boottime_test.sh new file mode 100755 index 000000000000..6e8593d5a2c5 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/run_boottime_test.sh @@ -0,0 +1,403 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# +# Generic Boot Tracing Test Harness +# Builds a lightweight initramfs, applies bootconfigs and kernel cmdline parameters, +# boots QEMU, and verifies tracefs configuration using test scripts. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -d "$SCRIPT_DIR/boottime" ]; then + SCRIPT_DIR="$SCRIPT_DIR/boottime" +fi +KERNEL_SRC="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" + +# Source KTAP helpers +if [ -f "$SCRIPT_DIR/../../kselftest/ktap_helpers.sh" ]; then + KSELFTEST_DIR="$(cd "$SCRIPT_DIR/../../kselftest" && pwd)" +elif [ -f "$SCRIPT_DIR/../kselftest/ktap_helpers.sh" ]; then + KSELFTEST_DIR="$(cd "$SCRIPT_DIR/../kselftest" && pwd)" +else + echo "Error: ktap_helpers.sh not found relative to $SCRIPT_DIR" >&2 + exit 1 +fi +. "$KSELFTEST_DIR/ktap_helpers.sh" + +BOOTCONFIG="${BOOTCONFIG:-"$KERNEL_SRC/tools/bootconfig/bootconfig"}" +KERNEL="${KERNEL:-"$KERNEL_SRC/arch/x86/boot/bzImage"}" +QEMU="${QEMU:-"qemu-system-x86_64"}" +BUSYBOX="${BUSYBOX:-"$(which busybox 2>/dev/null || echo "/usr/bin/busybox")"}" +TIMEOUT="${TIMEOUT:-60s}" +LOGDIR="${LOGDIR:-""}" +TARGET_TEST="all" + +BUSYBOX_APPLETS=( + sh + cat + mount + umount + echo + sync + reboot + poweroff + cpio + grep + sleep + sed +) + +usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " -b, --bootconfig PATH Path to bootconfig tool (default: $BOOTCONFIG)" + echo " -k, --kernel PATH Path to kernel image (default: $KERNEL)" + echo " -q, --qemu PATH Path to QEMU executable (default: $QEMU)" + echo " -B, --busybox PATH Path to busybox binary (default: $BUSYBOX)" + echo " -T, --timeout DURATION QEMU execution timeout (default: $TIMEOUT)" + echo " -l, --logdir DIR Directory to save QEMU log files" + echo " -t, --test NAME Run specific test (e.g. 01-kprobe) or 'all'" + echo " -h, --help Show this help message" + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + -b|--bootconfig) + BOOTCONFIG="$2" + shift 2 + ;; + -k|--kernel) + KERNEL="$2" + shift 2 + ;; + -q|--qemu) + QEMU="$2" + shift 2 + ;; + -B|--busybox) + BUSYBOX="$2" + shift 2 + ;; + -T|--timeout) + TIMEOUT="$2" + shift 2 + ;; + -l|--logdir) + LOGDIR="$2" + shift 2 + ;; + -t|--test) + TARGET_TEST="$2" + shift 2 + ;; + -h|--help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +# Check host architecture +HOST_ARCH="$(uname -m)" +case "$HOST_ARCH" in + x86_64|i686|i386|x86) + ;; + *) + if [ "$KERNEL" = "$KERNEL_SRC/arch/x86/boot/bzImage" ]; then + ktap_print_header + ktap_skip_all \ + "Default boot tracing test is x86 only ($HOST_ARCH)" + exit 0 + fi + ;; +esac + +# Ensure bootconfig tool is built if Makefile exists +if [ ! -x "$BOOTCONFIG" ]; then + if [ -f "$KERNEL_SRC/tools/bootconfig/Makefile" ]; then + make -C "$KERNEL_SRC/tools/bootconfig" > /dev/null 2>&1 || true + fi + if [ ! -x "$BOOTCONFIG" ]; then + BOOTCONFIG="$(which bootconfig 2>/dev/null || true)" + fi +fi + +if [ ! -x "$BOOTCONFIG" ]; then + ktap_print_header + ktap_skip_all "bootconfig tool not found at $BOOTCONFIG" + exit 0 +fi + +if [ ! -f "$KERNEL" ]; then + ktap_print_header + ktap_skip_all "Kernel image not found at $KERNEL" + exit 0 +fi + +if ! command -v "$QEMU" >/dev/null 2>&1; then + ktap_print_header + ktap_skip_all "QEMU binary not found: $QEMU" + exit 0 +fi + +if [ ! -x "$BUSYBOX" ]; then + ktap_print_header + ktap_skip_all "busybox binary not found at $BUSYBOX" + exit 0 +fi + +# Verify file command existence and architecture compatibility +if ! command -v file >/dev/null 2>&1; then + ktap_print_header + ktap_skip_all "file tool not found" + exit 0 +fi + +KERNEL_INFO="$(file -bL "$KERNEL" 2>/dev/null || true)" +case "$KERNEL_INFO" in + *x86*) + ;; + *) + ktap_print_header + ktap_skip_all "Kernel architecture is not x86 ($KERNEL_INFO)" + exit 0 + ;; +esac + +BUSYBOX_INFO="$(file -bL "$BUSYBOX" 2>/dev/null || true)" +case "$BUSYBOX_INFO" in + *x86-64*|*x86_64*|*80386*|*i386*) + ;; + *) + ktap_print_header + ktap_skip_all \ + "busybox binary architecture is incompatible with x86 kernel ($BUSYBOX_INFO)" + exit 0 + ;; +esac + +if ! command -v cpio >/dev/null 2>&1; then + ktap_print_header + ktap_skip_all "cpio tool not found" + exit 0 +fi + +if ! command -v timeout >/dev/null 2>&1; then + ktap_print_header + ktap_skip_all "timeout tool not found" + exit 0 +fi + +double_timeout() { + local val="$1" + if [[ "$val" =~ ^([0-9]+)([a-z]*)$ ]]; then + local num="${BASH_REMATCH[1]}" + local unit="${BASH_REMATCH[2]}" + echo "$((num * 2))$unit" + else + echo "120s" + fi +} + +run_single_test() { + local test_script="$1" + local name + local dir + name="$(basename "$test_script" .sh)" + dir="$(dirname "$test_script")" + + local bconf_file="" + if [ -f "$SCRIPT_DIR/bootconfigs/$name.bconf" ]; then + bconf_file="$SCRIPT_DIR/bootconfigs/$name.bconf" + elif [ -f "$dir/$name.bconf" ]; then + bconf_file="$dir/$name.bconf" + fi + + local test_cmdline="" + if [ -f "$SCRIPT_DIR/cmdlines/$name.cmdline" ]; then + test_cmdline="$(cat "$SCRIPT_DIR/cmdlines/$name.cmdline")" + elif [ -f "$dir/$name.cmdline" ]; then + test_cmdline="$(cat "$dir/$name.cmdline")" + fi + + local inline_cmdline + inline_cmdline="$(grep -E '^# *CMDLINE:' "$test_script" | sed 's/^# *CMDLINE://' | xargs || true)" + if [ -n "$inline_cmdline" ]; then + test_cmdline="$test_cmdline $inline_cmdline" + fi + + local test_qemuopts="" + if [ -f "$SCRIPT_DIR/qemuopts/$name.qemuopts" ]; then + test_qemuopts="$(cat "$SCRIPT_DIR/qemuopts/$name.qemuopts")" + elif [ -f "$dir/$name.qemuopts" ]; then + test_qemuopts="$(cat "$dir/$name.qemuopts")" + fi + + local inline_qemuopts + inline_qemuopts="$(grep -E '^# *QEMUOPTS:' "$test_script" | sed 's/^# *QEMUOPTS://' | xargs || true)" + if [ -n "$inline_qemuopts" ]; then + test_qemuopts="$test_qemuopts $inline_qemuopts" + fi + + local test_timeout="$TIMEOUT" + local inline_timeout + inline_timeout="$(grep -E '^# *TIMEOUT:' "$test_script" | sed 's/^# *TIMEOUT://' | xargs || true)" + if [ -n "$inline_timeout" ]; then + test_timeout="$inline_timeout" + fi + + local is_reboot=0 + if grep -q -E '^# *REBOOT: *1' "$test_script" || [ -f "$dir/$name.reboot" ]; then + is_reboot=1 + fi + + local effective_timeout="$test_timeout" + if [ "$is_reboot" -eq 1 ]; then + effective_timeout="$(double_timeout "$test_timeout")" + fi + + local workdir + workdir="$(mktemp -d)" + if [ -z "$workdir" ] || [ ! -d "$workdir" ]; then + ktap_test_fail "$name (failed to create temporary directory)" + return 0 + fi + trap 'rm -rf "$workdir"' EXIT + + local test_applets=("${BUSYBOX_APPLETS[@]}") + local inline_applets + inline_applets="$(grep -E '^# *APPLETS:' "$test_script" | sed 's/^# *APPLETS://' | xargs || true)" + if [ -n "$inline_applets" ]; then + read -r -a extra_applets <<< "$inline_applets" || true + test_applets+=("${extra_applets[@]}") + fi + + local rootfs="$workdir/rootfs" + mkdir -p "$rootfs"/{bin,sbin,etc,proc,sys,dev,tmp} + + cp -L "$BUSYBOX" "$rootfs/bin/busybox" + chmod +x "$rootfs/bin/busybox" + (cd "$rootfs/bin" && for applet in "${test_applets[@]}"; do ln -sf busybox "$applet"; done) + + if command -v ldd >/dev/null 2>&1; then + for lib in $(ldd "$BUSYBOX" 2>/dev/null | grep -o '/[^ ]*' || true); do + if [ -f "$lib" ]; then + mkdir -p "$rootfs$(dirname "$lib")" + cp -L "$lib" "$rootfs$lib" 2>/dev/null || true + fi + done + fi + + cat << 'EOF' > "$rootfs/init" +#!/bin/sh +mount -t proc proc /proc 2>/dev/null +mount -t sysfs sys /sys 2>/dev/null +mount -t tracefs nodev /sys/kernel/tracing 2>/dev/null + +/bin/check_test.sh +RET=$? + +if [ $RET -eq 0 ]; then + echo "TEST RESULT: PASS" +else + echo "TEST RESULT: FAIL" +fi +sync +echo o > /proc/sysrq-trigger 2>/dev/null || true +poweroff -f 2>/dev/null || reboot -f 2>/dev/null || true +while true; do sleep 100 2>/dev/null || break; done +EOF + chmod +x "$rootfs/init" + + cp "$test_script" "$rootfs/bin/check_test.sh" + chmod +x "$rootfs/bin/check_test.sh" + + local initramfs="$workdir/initramfs.cpio" + (cd "$rootfs" && find . | cpio -o -H newc --quiet) > "$initramfs" + + if [ -n "$bconf_file" ] && [ -f "$bconf_file" ]; then + "$BOOTCONFIG" -a "$bconf_file" "$initramfs" > /dev/null + fi + + + local logfile + local base_cmdline="bootconfig console=tty0 console=ttyS0 panic=-1" + if [ -n "$test_cmdline" ]; then + base_cmdline="$base_cmdline $test_cmdline" + fi + + if [ -n "$LOGDIR" ]; then + mkdir -p "$LOGDIR" + logfile="$LOGDIR/$name.log" + base_cmdline="$base_cmdline dump_bconf" + else + logfile="$workdir/qemu.log" + base_cmdline="quiet $base_cmdline" + fi + + local qemu_args=() + if [ -n "$test_qemuopts" ]; then + read -r -d '' -a qemu_args <<< "$test_qemuopts" || true + fi + + if [ "$is_reboot" -ne 1 ]; then + qemu_args+=("-no-reboot") + fi + + timeout "$effective_timeout" "$QEMU" -kernel "$KERNEL" \ + -initrd "$initramfs" \ + -append "$base_cmdline" \ + -display none \ + -serial stdio \ + "${qemu_args[@]}" < /dev/null > "$logfile" 2>&1 || true + + if grep -q "TEST RESULT: PASS" "$logfile"; then + ktap_test_pass "$name" + else + ktap_test_fail "$name" + ktap_print_msg "QEMU Console Output for $name:" + while IFS= read -r line; do + ktap_print_msg "$line" + done < "$logfile" + fi + + rm -rf "$workdir" + trap - EXIT + return 0 +} + +TEST_SCRIPTS=() +if [ -d "$SCRIPT_DIR/tests" ]; then + while IFS= read -r f; do + [ -n "$f" ] && TEST_SCRIPTS+=("$f") + done < <(find "$SCRIPT_DIR/tests" -type f -name "*.sh" | sort) +fi + +TOTAL_TESTS=0 +for test_script in "${TEST_SCRIPTS[@]}"; do + name="$(basename "$test_script" .sh)" + if [ "$TARGET_TEST" != "all" ] && [ "$TARGET_TEST" != "$name" ]; then + continue + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) +done + +ktap_print_header +ktap_set_plan "$TOTAL_TESTS" + +for test_script in "${TEST_SCRIPTS[@]}"; do + name="$(basename "$test_script" .sh)" + + if [ "$TARGET_TEST" != "all" ] && [ "$TARGET_TEST" != "$name" ]; then + continue + fi + + run_single_test "$test_script" +done + +ktap_finished diff --git a/tools/testing/selftests/ftrace/config b/tools/testing/selftests/ftrace/config index 544de0db5f58..29f05f7c6e2c 100644 --- a/tools/testing/selftests/ftrace/config +++ b/tools/testing/selftests/ftrace/config @@ -1,3 +1,5 @@ +CONFIG_BOOT_CONFIG=y +CONFIG_BOOTTIME_TRACING=y CONFIG_BPF_SYSCALL=y CONFIG_DEBUG_INFO_BTF=y CONFIG_DEBUG_INFO_DWARF4=y @@ -8,22 +10,26 @@ CONFIG_FTRACE=y CONFIG_FTRACE_SYSCALLS=y CONFIG_FUNCTION_GRAPH_RETVAL=y CONFIG_FUNCTION_PROFILER=y +CONFIG_FUNCTION_TRACER=y CONFIG_HIST_TRIGGERS=y CONFIG_IRQSOFF_TRACER=y CONFIG_KALLSYMS_ALL=y CONFIG_KPROBES=y CONFIG_KPROBE_EVENTS=y +CONFIG_MAGIC_SYSRQ=y CONFIG_MODULES=y CONFIG_MODULE_UNLOAD=y CONFIG_PREEMPTIRQ_DELAY_TEST=m CONFIG_PREEMPT_TRACER=y CONFIG_PROBE_EVENTS_BTF_ARGS=y +CONFIG_RESERVE_MEM=y CONFIG_SAMPLES=y CONFIG_SAMPLE_FTRACE_DIRECT=m CONFIG_SAMPLE_TRACE_EVENTS=m CONFIG_SAMPLE_TRACE_PRINTK=m CONFIG_SCHED_TRACER=y CONFIG_STACK_TRACER=y +CONFIG_SYNTH_EVENTS=y CONFIG_TRACER_SNAPSHOT=y CONFIG_UPROBES=y CONFIG_UPROBE_EVENTS=y From 50cc9e8fe8d570bfda05f6f71302eb91e1e4c49b Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Wed, 30 Sep 2026 00:09:41 +0900 Subject: [PATCH 2/9] selftests/ftrace: Add boot-time tracing testcases Add test cases for boot-time tracing configuration via bootconfig (see Documentation/trace/boottime-trace.rst). These test cases verify: - 01-kprobe: Dynamic kprobe events via bootconfig - 02-synth: Synthetic events via bootconfig - 03-eprobe: Event probes (eprobes) via bootconfig - 04-fprobe: Function probes (fprobes) via bootconfig - 05-tprobe: Tracepoint probes via bootconfig - 06-instance: Custom trace instances via bootconfig Link: https://lore.kernel.org/all/178649542809.438282.16167291066989252262.stgit@devnote2/ Assisted-by: LLM Signed-off-by: Masami Hiramatsu (Google) --- .../boottime/bootconfigs/01-kprobe.bconf | 4 +++ .../boottime/bootconfigs/02-synth.bconf | 4 +++ .../boottime/bootconfigs/03-eprobe.bconf | 4 +++ .../boottime/bootconfigs/04-fprobe.bconf | 4 +++ .../boottime/bootconfigs/05-tprobe.bconf | 4 +++ .../boottime/bootconfigs/06-instance.bconf | 5 ++++ .../ftrace/boottime/tests/01-kprobe.sh | 25 ++++++++++++++++ .../ftrace/boottime/tests/02-synth.sh | 25 ++++++++++++++++ .../ftrace/boottime/tests/03-eprobe.sh | 25 ++++++++++++++++ .../ftrace/boottime/tests/04-fprobe.sh | 25 ++++++++++++++++ .../ftrace/boottime/tests/05-tprobe.sh | 25 ++++++++++++++++ .../ftrace/boottime/tests/06-instance.sh | 30 +++++++++++++++++++ 12 files changed, 180 insertions(+) create mode 100644 tools/testing/selftests/ftrace/boottime/bootconfigs/01-kprobe.bconf create mode 100644 tools/testing/selftests/ftrace/boottime/bootconfigs/02-synth.bconf create mode 100644 tools/testing/selftests/ftrace/boottime/bootconfigs/03-eprobe.bconf create mode 100644 tools/testing/selftests/ftrace/boottime/bootconfigs/04-fprobe.bconf create mode 100644 tools/testing/selftests/ftrace/boottime/bootconfigs/05-tprobe.bconf create mode 100644 tools/testing/selftests/ftrace/boottime/bootconfigs/06-instance.bconf create mode 100644 tools/testing/selftests/ftrace/boottime/tests/01-kprobe.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/02-synth.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/03-eprobe.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/04-fprobe.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/05-tprobe.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/06-instance.sh diff --git a/tools/testing/selftests/ftrace/boottime/bootconfigs/01-kprobe.bconf b/tools/testing/selftests/ftrace/boottime/bootconfigs/01-kprobe.bconf new file mode 100644 index 000000000000..13d30d1e51a6 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/bootconfigs/01-kprobe.bconf @@ -0,0 +1,4 @@ +ftrace.event.kprobes.vfs_read { + probes = "vfs_read $arg1" + enable +} diff --git a/tools/testing/selftests/ftrace/boottime/bootconfigs/02-synth.bconf b/tools/testing/selftests/ftrace/boottime/bootconfigs/02-synth.bconf new file mode 100644 index 000000000000..b1f37713f33c --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/bootconfigs/02-synth.bconf @@ -0,0 +1,4 @@ +ftrace.event.synthetic.boot_lat { + fields = "unsigned long id", "u64 delta" + enable +} diff --git a/tools/testing/selftests/ftrace/boottime/bootconfigs/03-eprobe.bconf b/tools/testing/selftests/ftrace/boottime/bootconfigs/03-eprobe.bconf new file mode 100644 index 000000000000..c1a8138e605d --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/bootconfigs/03-eprobe.bconf @@ -0,0 +1,4 @@ +ftrace.event.eprobes.ep_read { + probes = "sched/sched_switch" + enable +} diff --git a/tools/testing/selftests/ftrace/boottime/bootconfigs/04-fprobe.bconf b/tools/testing/selftests/ftrace/boottime/bootconfigs/04-fprobe.bconf new file mode 100644 index 000000000000..82ae2ac0629f --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/bootconfigs/04-fprobe.bconf @@ -0,0 +1,4 @@ +ftrace.event.fprobes.fp_read { + probes = "vfs_read" + enable +} diff --git a/tools/testing/selftests/ftrace/boottime/bootconfigs/05-tprobe.bconf b/tools/testing/selftests/ftrace/boottime/bootconfigs/05-tprobe.bconf new file mode 100644 index 000000000000..02a2803c770a --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/bootconfigs/05-tprobe.bconf @@ -0,0 +1,4 @@ +ftrace.event.tracepoints.tp_sched { + probes = "sched_switch" + enable +} diff --git a/tools/testing/selftests/ftrace/boottime/bootconfigs/06-instance.bconf b/tools/testing/selftests/ftrace/boottime/bootconfigs/06-instance.bconf new file mode 100644 index 000000000000..3fdb6717afe2 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/bootconfigs/06-instance.bconf @@ -0,0 +1,5 @@ +ftrace.instance.foo { + event.sched.sched_switch { + enable + } +} diff --git a/tools/testing/selftests/ftrace/boottime/tests/01-kprobe.sh b/tools/testing/selftests/ftrace/boottime/tests/01-kprobe.sh new file mode 100644 index 000000000000..6ed7caca4c5f --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/01-kprobe.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check kprobe bootconfig settings on tracefs +TRACEDIR="/sys/kernel/tracing" + +if [ -f /proc/bootconfig ] && grep -q "dump_bconf" /proc/cmdline 2>/dev/null; then + echo "=== /proc/bootconfig ===" + cat /proc/bootconfig + echo "========================" +fi + +if [ ! -d "$TRACEDIR/events/kprobes/vfs_read" ]; then + echo "FAIL: kprobe event kprobes/vfs_read does not exist" + exit 1 +fi + +ENABLE=$(cat "$TRACEDIR/events/kprobes/vfs_read/enable") +if [ "$ENABLE" != "1" ]; then + echo "FAIL: kprobe event kprobes/vfs_read is not enabled ($ENABLE)" + exit 1 +fi + +echo "PASS: 01-kprobe" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/02-synth.sh b/tools/testing/selftests/ftrace/boottime/tests/02-synth.sh new file mode 100644 index 000000000000..745c328dde15 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/02-synth.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check synthetic event bootconfig settings on tracefs +TRACEDIR="/sys/kernel/tracing" + +if [ -f /proc/bootconfig ] && grep -q "dump_bconf" /proc/cmdline 2>/dev/null; then + echo "=== /proc/bootconfig ===" + cat /proc/bootconfig + echo "========================" +fi + +if [ ! -d "$TRACEDIR/events/synthetic/boot_lat" ]; then + echo "FAIL: synthetic event synthetic/boot_lat does not exist" + exit 1 +fi + +ENABLE=$(cat "$TRACEDIR/events/synthetic/boot_lat/enable") +if [ "$ENABLE" != "1" ]; then + echo "FAIL: synthetic event synthetic/boot_lat is not enabled ($ENABLE)" + exit 1 +fi + +echo "PASS: 02-synth" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/03-eprobe.sh b/tools/testing/selftests/ftrace/boottime/tests/03-eprobe.sh new file mode 100644 index 000000000000..9b3b7ab23c69 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/03-eprobe.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check eprobe event bootconfig settings on tracefs +TRACEDIR="/sys/kernel/tracing" + +if [ -f /proc/bootconfig ] && grep -q "dump_bconf" /proc/cmdline 2>/dev/null; then + echo "=== /proc/bootconfig ===" + cat /proc/bootconfig + echo "========================" +fi + +if [ ! -d "$TRACEDIR/events/eprobes/ep_read" ]; then + echo "FAIL: eprobe event eprobes/ep_read does not exist" + exit 1 +fi + +ENABLE=$(cat "$TRACEDIR/events/eprobes/ep_read/enable") +if [ "$ENABLE" != "1" ]; then + echo "FAIL: eprobe event eprobes/ep_read is not enabled ($ENABLE)" + exit 1 +fi + +echo "PASS: 03-eprobe" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/04-fprobe.sh b/tools/testing/selftests/ftrace/boottime/tests/04-fprobe.sh new file mode 100644 index 000000000000..27e090c28a4b --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/04-fprobe.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check fprobe event bootconfig settings on tracefs +TRACEDIR="/sys/kernel/tracing" + +if [ -f /proc/bootconfig ] && grep -q "dump_bconf" /proc/cmdline 2>/dev/null; then + echo "=== /proc/bootconfig ===" + cat /proc/bootconfig + echo "========================" +fi + +if [ ! -d "$TRACEDIR/events/fprobes/fp_read" ]; then + echo "FAIL: fprobe event fprobes/fp_read does not exist" + exit 1 +fi + +ENABLE=$(cat "$TRACEDIR/events/fprobes/fp_read/enable") +if [ "$ENABLE" != "1" ]; then + echo "FAIL: fprobe event fprobes/fp_read is not enabled ($ENABLE)" + exit 1 +fi + +echo "PASS: 04-fprobe" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/05-tprobe.sh b/tools/testing/selftests/ftrace/boottime/tests/05-tprobe.sh new file mode 100644 index 000000000000..810d34055f94 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/05-tprobe.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check tracepoint probe event bootconfig settings on tracefs +TRACEDIR="/sys/kernel/tracing" + +if [ -f /proc/bootconfig ] && grep -q "dump_bconf" /proc/cmdline 2>/dev/null; then + echo "=== /proc/bootconfig ===" + cat /proc/bootconfig + echo "========================" +fi + +if [ ! -d "$TRACEDIR/events/tracepoints/tp_sched" ]; then + echo "FAIL: tracepoint probe event tracepoints/tp_sched does not exist" + exit 1 +fi + +ENABLE=$(cat "$TRACEDIR/events/tracepoints/tp_sched/enable") +if [ "$ENABLE" != "1" ]; then + echo "FAIL: tracepoint probe event tracepoints/tp_sched is not enabled ($ENABLE)" + exit 1 +fi + +echo "PASS: 05-tprobe" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/06-instance.sh b/tools/testing/selftests/ftrace/boottime/tests/06-instance.sh new file mode 100644 index 000000000000..f6e8519912e4 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/06-instance.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check instance bootconfig settings on tracefs +TRACEDIR="/sys/kernel/tracing" + +if [ -f /proc/bootconfig ] && grep -q "dump_bconf" /proc/cmdline 2>/dev/null; then + echo "=== /proc/bootconfig ===" + cat /proc/bootconfig + echo "========================" +fi + +if [ ! -d "$TRACEDIR/instances/foo" ]; then + echo "FAIL: trace instance foo does not exist" + exit 1 +fi + +if [ ! -d "$TRACEDIR/instances/foo/events/sched/sched_switch" ]; then + echo "FAIL: event sched_switch does not exist in instance foo" + exit 1 +fi + +ENABLE=$(cat "$TRACEDIR/instances/foo/events/sched/sched_switch/enable") +if [ "$ENABLE" != "1" ]; then + echo "FAIL: event sched_switch is not enabled in instance foo ($ENABLE)" + exit 1 +fi + +echo "PASS: 06-instance" +exit 0 From 8dd296e4ec19bda8cc954d7a6e2d5a63a395a9a8 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Wed, 30 Sep 2026 00:09:41 +0900 Subject: [PATCH 3/9] selftests/ftrace: Add kernel cmdline tracing testcases Add test cases for kernel command-line tracing options. It verifies various parameters like trace_buf_size and trace_options by reading the output of tracing files in sysfs. Link: https://lore.kernel.org/all/178649543741.438282.2638887907664217934.stgit@devnote2/ Assisted-by: LLM Signed-off-by: Masami Hiramatsu (Google) --- .../cmdlines/cmdline-01-ftrace.cmdline | 1 + .../cmdlines/cmdline-02-trace-event.cmdline | 1 + .../cmdline-03-trace-buf-size.cmdline | 1 + .../cmdlines/cmdline-04-trace-options.cmdline | 1 + .../cmdlines/cmdline-05-trace-clock.cmdline | 1 + .../cmdline-06-trace-instance.cmdline | 1 + .../boottime/tests/cmdline-01-ftrace.sh | 19 ++++++++++++ .../boottime/tests/cmdline-02-trace-event.sh | 26 +++++++++++++++++ .../tests/cmdline-03-trace-buf-size.sh | 29 +++++++++++++++++++ .../tests/cmdline-04-trace-options.sh | 23 +++++++++++++++ .../boottime/tests/cmdline-05-trace-clock.sh | 19 ++++++++++++ .../tests/cmdline-06-trace-instance.sh | 24 +++++++++++++++ 12 files changed, 146 insertions(+) create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-01-ftrace.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-02-trace-event.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-03-trace-buf-size.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-04-trace-options.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-05-trace-clock.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-06-trace-instance.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/tests/cmdline-01-ftrace.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/cmdline-02-trace-event.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/cmdline-03-trace-buf-size.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/cmdline-04-trace-options.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/cmdline-05-trace-clock.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/cmdline-06-trace-instance.sh diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-01-ftrace.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-01-ftrace.cmdline new file mode 100644 index 000000000000..4f6fc54240c8 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-01-ftrace.cmdline @@ -0,0 +1 @@ +ftrace=function diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-02-trace-event.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-02-trace-event.cmdline new file mode 100644 index 000000000000..66bcd29b14cb --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-02-trace-event.cmdline @@ -0,0 +1 @@ +trace_event=sched:sched_switch,kmem:kmalloc diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-03-trace-buf-size.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-03-trace-buf-size.cmdline new file mode 100644 index 000000000000..e5d1c4a8ca89 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-03-trace-buf-size.cmdline @@ -0,0 +1 @@ +trace_buf_size=2048K diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-04-trace-options.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-04-trace-options.cmdline new file mode 100644 index 000000000000..51d82f8e948d --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-04-trace-options.cmdline @@ -0,0 +1 @@ +trace_options=sym-addr,verbose diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-05-trace-clock.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-05-trace-clock.cmdline new file mode 100644 index 000000000000..f72a4ac808a3 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-05-trace-clock.cmdline @@ -0,0 +1 @@ +trace_clock=global diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-06-trace-instance.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-06-trace-instance.cmdline new file mode 100644 index 000000000000..9618e3e2b88b --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/cmdline-06-trace-instance.cmdline @@ -0,0 +1 @@ +trace_instance=bar,sched:sched_switch diff --git a/tools/testing/selftests/ftrace/boottime/tests/cmdline-01-ftrace.sh b/tools/testing/selftests/ftrace/boottime/tests/cmdline-01-ftrace.sh new file mode 100644 index 000000000000..23a0b15f2dd3 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/cmdline-01-ftrace.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check ftrace= kernel command-line tracer setting +TRACEDIR="/sys/kernel/tracing" + +if [ ! -f "$TRACEDIR/current_tracer" ]; then + echo "FAIL: current_tracer does not exist" + exit 1 +fi + +read -r TRACER _ < "$TRACEDIR/current_tracer" +if [ "$TRACER" != "function" ]; then + echo "FAIL: current_tracer is '$TRACER', expected 'function'" + exit 1 +fi + +echo "PASS: cmdline-01-ftrace" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/cmdline-02-trace-event.sh b/tools/testing/selftests/ftrace/boottime/tests/cmdline-02-trace-event.sh new file mode 100644 index 000000000000..b793907c48cd --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/cmdline-02-trace-event.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check trace_event= kernel command-line setting +TRACEDIR="/sys/kernel/tracing" + +if [ ! -d "$TRACEDIR/events/sched/sched_switch" ]; then + echo "FAIL: event sched:sched_switch does not exist" + exit 1 +fi + +if [ ! -d "$TRACEDIR/events/kmem/kmalloc" ]; then + echo "FAIL: event kmem:kmalloc does not exist" + exit 1 +fi + +ENABLE1=$(cat "$TRACEDIR/events/sched/sched_switch/enable") +ENABLE2=$(cat "$TRACEDIR/events/kmem/kmalloc/enable") + +if [ "$ENABLE1" != "1" ] || [ "$ENABLE2" != "1" ]; then + echo "FAIL: events not enabled (sched_switch=$ENABLE1, kmalloc=$ENABLE2)" + exit 1 +fi + +echo "PASS: cmdline-02-trace-event" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/cmdline-03-trace-buf-size.sh b/tools/testing/selftests/ftrace/boottime/tests/cmdline-03-trace-buf-size.sh new file mode 100644 index 000000000000..4b936f84d4d7 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/cmdline-03-trace-buf-size.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# APPLETS: awk +# Check trace_buf_size= kernel command-line setting +TRACEDIR="/sys/kernel/tracing" + +if [ ! -f "$TRACEDIR/buffer_size_kb" ]; then + echo "FAIL: buffer_size_kb does not exist" + exit 1 +fi + +BUF_RAW=$(cat "$TRACEDIR/buffer_size_kb") +case "$BUF_RAW" in + *"expanded:"*) + BUFSIZE=$(echo "$BUF_RAW" | sed -n 's/.*expanded: *\([0-9]*\).*/\1/p') + ;; + *) + BUFSIZE=$(echo "$BUF_RAW" | awk '{print $1}') + ;; +esac + +if [ -z "$BUFSIZE" ] || [ "$BUFSIZE" -lt 2048 ]; then + echo "FAIL: buffer_size_kb is '$BUF_RAW', expected >= 2048" + exit 1 +fi + +echo "PASS: cmdline-03-trace-buf-size" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/cmdline-04-trace-options.sh b/tools/testing/selftests/ftrace/boottime/tests/cmdline-04-trace-options.sh new file mode 100644 index 000000000000..53a65b55f7a7 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/cmdline-04-trace-options.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check trace_options= kernel command-line setting +TRACEDIR="/sys/kernel/tracing" + +if [ ! -f "$TRACEDIR/trace_options" ]; then + echo "FAIL: trace_options file does not exist" + exit 1 +fi + +if ! grep -qw "sym-addr" "$TRACEDIR/trace_options"; then + echo "FAIL: sym-addr option is not set in trace_options" + exit 1 +fi + +if ! grep -qw "verbose" "$TRACEDIR/trace_options"; then + echo "FAIL: verbose option is not set in trace_options" + exit 1 +fi + +echo "PASS: cmdline-04-trace-options" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/cmdline-05-trace-clock.sh b/tools/testing/selftests/ftrace/boottime/tests/cmdline-05-trace-clock.sh new file mode 100644 index 000000000000..d6de9f6e2090 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/cmdline-05-trace-clock.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check trace_clock= kernel command-line setting +TRACEDIR="/sys/kernel/tracing" + +if [ ! -f "$TRACEDIR/trace_clock" ]; then + echo "FAIL: trace_clock file does not exist" + exit 1 +fi + +if ! grep -q '\[global\]' "$TRACEDIR/trace_clock"; then + CLOCK=$(cat "$TRACEDIR/trace_clock") + echo "FAIL: trace_clock is not set to global ($CLOCK)" + exit 1 +fi + +echo "PASS: cmdline-05-trace-clock" +exit 0 diff --git a/tools/testing/selftests/ftrace/boottime/tests/cmdline-06-trace-instance.sh b/tools/testing/selftests/ftrace/boottime/tests/cmdline-06-trace-instance.sh new file mode 100644 index 000000000000..aae7f0a86d81 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/cmdline-06-trace-instance.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check trace_instance= kernel command-line setting +TRACEDIR="/sys/kernel/tracing" + +if [ ! -d "$TRACEDIR/instances/bar" ]; then + echo "FAIL: trace instance bar does not exist" + exit 1 +fi + +if [ ! -d "$TRACEDIR/instances/bar/events/sched/sched_switch" ]; then + echo "FAIL: event sched_switch does not exist in instance bar" + exit 1 +fi + +ENABLE=$(cat "$TRACEDIR/instances/bar/events/sched/sched_switch/enable") +if [ "$ENABLE" != "1" ]; then + echo "FAIL: event sched_switch is not enabled in instance bar ($ENABLE)" + exit 1 +fi + +echo "PASS: cmdline-06-trace-instance" +exit 0 From 313cb5169f1a68d056af6ec37651198c21156e56 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Wed, 30 Sep 2026 00:09:41 +0900 Subject: [PATCH 4/9] selftests/ftrace: Add persistent ring buffer testcases Add test cases for persistent ring buffer (reserve_mem= combined with trace_instance=) and backup instance (trace_instance=backup=boot_map). These test cases verify: - persistent-01-reserve-mem: Memory reservation via reserve_mem= and trace_instance=boot_map@trace. Verifies trace data retention in the boot_map instance across guest crash/reboot via sysrq-trigger. - persistent-02-backup-instance: Backup instance creation via trace_instance=backup=boot_map. Verifies that previous boot trace log is preserved in the backup instance on the subsequent boot. These test cases specify '# REBOOT: 1' to enable multi-boot guest crash/reboot testing with doubled QEMU timeout. Link: https://lore.kernel.org/all/178649544683.438282.14573895196260214231.stgit@devnote2/ Assisted-by: LLM Signed-off-by: Masami Hiramatsu (Google) --- .../persistent-01-reserve-mem.cmdline | 1 + .../persistent-02-backup-instance.cmdline | 1 + .../tests/persistent-01-reserve-mem.sh | 28 +++++++++++++ .../tests/persistent-02-backup-instance.sh | 40 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/persistent-01-reserve-mem.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/cmdlines/persistent-02-backup-instance.cmdline create mode 100644 tools/testing/selftests/ftrace/boottime/tests/persistent-01-reserve-mem.sh create mode 100644 tools/testing/selftests/ftrace/boottime/tests/persistent-02-backup-instance.sh diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/persistent-01-reserve-mem.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/persistent-01-reserve-mem.cmdline new file mode 100644 index 000000000000..c69eeb27deb6 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/persistent-01-reserve-mem.cmdline @@ -0,0 +1 @@ +reserve_mem=12M:32M:trace trace_instance=boot_map@trace diff --git a/tools/testing/selftests/ftrace/boottime/cmdlines/persistent-02-backup-instance.cmdline b/tools/testing/selftests/ftrace/boottime/cmdlines/persistent-02-backup-instance.cmdline new file mode 100644 index 000000000000..43217c986ca1 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/cmdlines/persistent-02-backup-instance.cmdline @@ -0,0 +1 @@ +reserve_mem=12M:32M:trace trace_instance=boot_map@trace trace_instance=backup=boot_map diff --git a/tools/testing/selftests/ftrace/boottime/tests/persistent-01-reserve-mem.sh b/tools/testing/selftests/ftrace/boottime/tests/persistent-01-reserve-mem.sh new file mode 100644 index 000000000000..49abaf518481 --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/persistent-01-reserve-mem.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check persistent ring buffer across guest crash/reboot +# REBOOT: 1 +TRACEDIR="/sys/kernel/tracing" + +if [ ! -f "$TRACEDIR/instances/boot_map/trace" ]; then + echo "FAIL: persistent trace instance boot_map/trace file missing" + exit 1 +fi + +# Check if Boot 1 marker was already written +if grep -q "BOOT1_MARKER" "$TRACEDIR/instances/boot_map/trace" 2>/dev/null; then + # Second boot: verify persistent ring buffer content from first boot + echo "PASS: persistent-01-reserve-mem" + exit 0 +fi + +# First boot: write marker to persistent buffer and trigger kernel crash/reboot +echo "BOOT1_MARKER" > "$TRACEDIR/instances/boot_map/trace_marker" +sync + +# Trigger reboot to restart into second boot +echo b > /proc/sysrq-trigger 2>/dev/null || echo c > /proc/sysrq-trigger 2>/dev/null || true +sleep 5 +echo "FAIL: reboot trigger failed on first boot" +exit 1 diff --git a/tools/testing/selftests/ftrace/boottime/tests/persistent-02-backup-instance.sh b/tools/testing/selftests/ftrace/boottime/tests/persistent-02-backup-instance.sh new file mode 100644 index 000000000000..0b2a2f03ce4f --- /dev/null +++ b/tools/testing/selftests/ftrace/boottime/tests/persistent-02-backup-instance.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026, Google LLC. +# Check persistent backup instance across guest crash/reboot +# REBOOT: 1 +TRACEDIR="/sys/kernel/tracing" + +# Ensure both boot_map/trace and backup/trace files exist +if [ ! -f "$TRACEDIR/instances/boot_map/trace" ]; then + echo "FAIL: boot_map/trace file missing" + exit 1 +fi + +if [ ! -f "$TRACEDIR/instances/backup/trace" ]; then + echo "FAIL: backup/trace file missing" + exit 1 +fi + +# Check if BOOT1_MARKER is in boot_map/trace (indicates second boot) +if grep -q "BOOT1_MARKER" "$TRACEDIR/instances/boot_map/trace" 2>/dev/null; then + # Second boot: verify BOOT1_MARKER was copied into backup/trace from Boot 1 + if grep -q "BOOT1_MARKER" "$TRACEDIR/instances/backup/trace" 2>/dev/null; then + echo "PASS: persistent-02-backup-instance" + exit 0 + else + echo "FAIL: BOOT1_MARKER found in boot_map/trace" \ + "but missing from backup/trace on second boot" + exit 1 + fi +fi + +# First boot: write BOOT1_MARKER to boot_map and reboot via sysrq-trigger +echo "BOOT1_MARKER" > "$TRACEDIR/instances/boot_map/trace_marker" +sync + +# Trigger reboot to restart into second boot +echo b > /proc/sysrq-trigger 2>/dev/null || echo c > /proc/sysrq-trigger 2>/dev/null || true +sleep 5 +echo "FAIL: reboot trigger failed on first boot" +exit 1 From 0d4cbc5e83baccd73ebcb0ac975980d66347d5a8 Mon Sep 17 00:00:00 2001 From: Bradley Morgan Date: Wed, 30 Sep 2026 00:09:42 +0900 Subject: [PATCH 5/9] kprobes: use DEFINE_DEBUGFS_ATTRIBUTE for the enabled knob This enabled knob is a disgusting terrible hack. It has rolled its own read/write pair since 2007, writing '1' or '0' into a three byte buffer by hand just to print a single character, with an XXX comment begging debugfs for write callbacks on bool files. DEFINE_DEBUGFS_ATTRIBUTE showed up in 2016 and does exactly that, so the disgusting terrible hack has outlived its excuse for nine years. Kill it, and the stale comment with it. The behavior does not change, except the write only accepts 0/1 now instead of y/n/on/off, and nothing uses anything else. Link: https://lore.kernel.org/all/20260818015318.24103-1-include@grrlz.net/ Signed-off-by: Bradley Morgan Signed-off-by: Masami Hiramatsu (Google) --- kernel/kprobes.c | 43 +++++++++---------------------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/kernel/kprobes.c b/kernel/kprobes.c index 4edd8ca5c657..7b6baa4c6fda 100644 --- a/kernel/kprobes.c +++ b/kernel/kprobes.c @@ -3025,47 +3025,22 @@ static int disarm_all_kprobes(void) return ret; } -/* - * XXX: The debugfs bool file interface doesn't allow for callbacks - * when the bool state is switched. We can reuse that facility when - * available - */ -static ssize_t read_enabled_file_bool(struct file *file, - char __user *user_buf, size_t count, loff_t *ppos) +static int kprobes_enabled_set(void *data, u64 val) { - char buf[3]; + if (val) + return arm_all_kprobes(); - if (!kprobes_all_disarmed) - buf[0] = '1'; - else - buf[0] = '0'; - buf[1] = '\n'; - buf[2] = 0x00; - return simple_read_from_buffer(user_buf, count, ppos, buf, 2); + return disarm_all_kprobes(); } -static ssize_t write_enabled_file_bool(struct file *file, - const char __user *user_buf, size_t count, loff_t *ppos) +static int kprobes_enabled_get(void *data, u64 *val) { - bool enable; - int ret; - - ret = kstrtobool_from_user(user_buf, count, &enable); - if (ret) - return ret; - - ret = enable ? arm_all_kprobes() : disarm_all_kprobes(); - if (ret) - return ret; - - return count; + *val = !kprobes_all_disarmed; + return 0; } -static const struct file_operations fops_kp = { - .read = read_enabled_file_bool, - .write = write_enabled_file_bool, - .llseek = default_llseek, -}; +DEFINE_DEBUGFS_ATTRIBUTE(fops_kp, kprobes_enabled_get, + kprobes_enabled_set, "%llu\n"); static int __init debugfs_kprobe_init(void) { From f3c6c0549e87b92d309700cf2c0ebe94056428e0 Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Wed, 30 Sep 2026 00:09:42 +0900 Subject: [PATCH 6/9] tracing/probes: Add const to new_argv allocation type In preparation for converting the kmalloc family of allocators to the type-aware kmalloc_obj family, we need to make sure that the returned type from the allocation matches the type of the variable being assigned. (The kmalloc family returns "void *", which can be implicitly cast to any pointer type.) The assigned type is "const char **", but the converted allocation type would be "char **", which is the same type without the const qualifier. As there is no general way to safely add const qualifiers, take the size from the assignment target instead. No change in allocation size results. Build tested ARCH=x86_64 allmodconfig with GCC 16.2.0: kernel/trace/trace_probe.o Link: https://lore.kernel.org/all/20260917211022.i.677-kees@kernel.org/ Assisted-by: LLM coccinelle Signed-off-by: Kees Cook Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 804442b2f7d2..cfb710095f65 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -2316,7 +2316,7 @@ const char **traceprobe_expand_meta_args(int argc, const char *argv[], else *new_argc = argc; - new_argv = kcalloc(*new_argc, sizeof(char *), GFP_KERNEL); + new_argv = kcalloc(*new_argc, sizeof(*new_argv), GFP_KERNEL); if (!new_argv) return ERR_PTR(-ENOMEM); From 06e0d3b89da12f24ed5ed434acc8e2cb504f5e4f Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Fri, 2 Oct 2026 13:48:49 +0900 Subject: [PATCH 7/9] kprobes: Make optprobe optimizer multi-generational and asynchronous Currently, kprobe_optimizer() holds kprobe_mutex, text_mutex, and cpus_read_lock() simultaneously while executing synchronize_rcu_tasks(). Under PREEMPT_LAZY and server workloads with long-running CPU-bound kernel tasks or heavy cgroup writeback loops, synchronize_rcu_tasks() can block for seconds to minutes. Because text_mutex and cpus_read_lock() are held during this entire wait, any concurrent static key updates, module loading/unloading, CPU hotplug, or tracing updates stall, frequently triggering hung-task detector panics. Instead of penalizing the kernel preemption path with invasive hooks and global hash lookups, decouple kprobe jump optimization from synchronous waiting entirely: 1. Replace synchronize_rcu_tasks() with call_rcu_tasks(). Locks (text_mutex and cpus_read_lock()) are held only for the brief moment needed to patch instructions via arch_unoptimize_kprobes() and arch_optimize_kprobes() (microseconds), and are completely released while waiting for the Tasks RCU grace period. 2. Introduce a fixed ring of generations (optprobe_gens[OPTPROBE_GEN_MAX]) to avoid any dynamic memory allocation (kmalloc) or -ENOMEM failure modes. 3. The last generation slot in the array is reserved as a "waiting room" and is not dispatched to RCU until another in-flight generation has finished. While a generation is waiting for its Tasks RCU grace period, any newly registered or unregistered probes accumulate in the waiting room generation without blocking or requiring additional slots. 4. When the Tasks RCU callback fires, it marks the generation as ready and wakes up the optimizer thread to finalize optimization (poking the jump instructions) and free cleaned probe slots. Once finalized, the generation is marked idle, allowing the waiting room generation to be dispatched next. 5. Flushing via wait_for_kprobe_optimizer() waits asynchronously for all in-flight and queued generations to drain without stalling other kernel subsystems. On non-preemptive kernels or configs where CONFIG_TASKS_RCU=n, call_rcu_tasks() transparently aliases to call_rcu(), preserving full portability. Link: https://lore.kernel.org/all/179091652953.526694.3402334249215311330.stgit@devnote2/ Assisted-by: LLM Signed-off-by: Masami Hiramatsu (Google) --- kernel/kprobes.c | 477 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 356 insertions(+), 121 deletions(-) diff --git a/kernel/kprobes.c b/kernel/kprobes.c index 7b6baa4c6fda..11add929ea16 100644 --- a/kernel/kprobes.c +++ b/kernel/kprobes.c @@ -43,6 +43,7 @@ #include #include #include +#include #include #include @@ -67,7 +68,7 @@ static struct hlist_head kprobe_table[KPROBE_TABLE_SIZE]; /* NOTE: change this value only with 'kprobe_mutex' held */ static bool kprobes_all_disarmed; -/* This protects 'kprobe_table' and 'optimizing_list' */ +/* This protects 'kprobe_table' and 'optprobe_gens' */ static DEFINE_MUTEX(kprobe_mutex); static DEFINE_PER_CPU(struct kprobe *, kprobe_instance); @@ -476,18 +477,6 @@ bool kprobe_disarmed(struct kprobe *p) return kprobe_disabled(p) && list_empty(&op->list); } -/* Return true if the probe is queued on (un)optimizing lists */ -static bool kprobe_queued(struct kprobe *p) -{ - struct optimized_kprobe *op; - - if (kprobe_aggrprobe(p)) { - op = container_of(p, struct optimized_kprobe, kp); - if (!list_empty(&op->list)) - return true; - } - return false; -} /* * Return an optimized kprobe whose optimizing code replaces @@ -512,10 +501,39 @@ static struct kprobe *get_optimized_kprobe(kprobe_opcode_t *addr) return NULL; } -/* Optimization staging list, protected by 'kprobe_mutex' */ -static LIST_HEAD(optimizing_list); -static LIST_HEAD(unoptimizing_list); -static LIST_HEAD(freeing_list); +#define OPTPROBE_GEN_MAX 2 + +struct optprobe_generation { + /* Probes waiting for grace period to drain tasks from original text */ + struct list_head optimizing_list; + /* Probes queued for batch unoptimization upon dispatch */ + struct list_head unoptimizing_list; + /* In-use probes waiting for grace period to drain optinsn slot */ + struct list_head cooling_list; + /* Unused probes unhashed and waiting for batch release */ + struct list_head freeing_list; + /* Tasks RCU callback head for asynchronous waiting */ + struct rcu_head rcu; + /* True if dispatched and awaiting Tasks RCU callback */ + bool in_flight; + /* True when Tasks RCU callback has fired and ready to finalize */ + bool ready; +}; + +/* + * Generational ring of optprobes. + * + * Incoming probe requests are queued into the waiting room generation + * (optprobe_gens[optprobe_cur_gen]). When dispatched, the generation + * unoptimizes its probes, invokes call_rcu_tasks(), and optprobe_cur_gen + * advances to the next slot. + * + * To ensure an idle generation is always available to collect incoming + * requests without dynamic allocation, the last available generation slot + * is never dispatched until another generation has finished. + */ +static struct optprobe_generation optprobe_gens[OPTPROBE_GEN_MAX]; +static int optprobe_cur_gen; static void optimize_kprobe(struct kprobe *p); static struct task_struct *kprobe_optimizer_task; @@ -533,11 +551,66 @@ static unsigned long optimizer_passes; #define OPTIMIZE_DELAY 5 /* - * Optimize (replace a breakpoint with a jump) kprobes listed on - * 'optimizing_list'. + * Note: gen->cooling_list is not checked here because it is strictly + * an in-flight holding list for dispatched generations, so it is always + * empty in optprobe_cur_gen. */ -static void do_optimize_kprobes(void) +static bool optprobe_has_queued_probes(void) { + struct optprobe_generation *gen = &optprobe_gens[optprobe_cur_gen]; + + return !list_empty(&gen->optimizing_list) || + !list_empty(&gen->unoptimizing_list) || + !list_empty(&gen->freeing_list); +} + +static int optprobe_active_gens_count(void) +{ + int count = 0; + int i; + + for (i = 0; i < OPTPROBE_GEN_MAX; i++) { + if (optprobe_gens[i].in_flight || optprobe_gens[i].ready) + count++; + } + return count; +} + +/* + * The last generation must not be fired until another generation is done. + * (Thus the last generation acts as the waiting room.) + */ +static bool optprobe_can_fire(void) +{ + return optprobe_active_gens_count() < OPTPROBE_GEN_MAX - 1; +} + +static bool optprobe_has_ready_gens(void) +{ + int i; + + for (i = 0; i < OPTPROBE_GEN_MAX; i++) { + if (READ_ONCE(optprobe_gens[i].ready)) + return true; + } + return false; +} + +static bool optprobe_optimizer_busy(void) +{ + return optprobe_has_queued_probes() || (optprobe_active_gens_count() > 0); +} + +/* + * Unoptimize (replace a jump with a breakpoint and remove the breakpoint + * if need) kprobes listed on 'unopt_list'. + */ +static void do_unoptimize_kprobes(struct list_head *unopt_list, + struct list_head *free_list, + struct list_head *cooling_list) +{ + struct optimized_kprobe *op, *tmp; + lockdep_assert_held(&text_mutex); /* * The optimization/unoptimization refers 'online_cpus' via @@ -551,31 +624,11 @@ static void do_optimize_kprobes(void) */ lockdep_assert_cpus_held(); - /* Optimization never be done when disarmed */ - if (kprobes_all_disarmed || !kprobes_allow_optimization || - list_empty(&optimizing_list)) - return; + if (!list_empty(unopt_list)) + arch_unoptimize_kprobes(unopt_list, free_list); - arch_optimize_kprobes(&optimizing_list); -} - -/* - * Unoptimize (replace a jump with a breakpoint and remove the breakpoint - * if need) kprobes listed on 'unoptimizing_list'. - */ -static void do_unoptimize_kprobes(void) -{ - struct optimized_kprobe *op, *tmp; - - lockdep_assert_held(&text_mutex); - /* See comment in do_optimize_kprobes() */ - lockdep_assert_cpus_held(); - - if (!list_empty(&unoptimizing_list)) - arch_unoptimize_kprobes(&unoptimizing_list, &freeing_list); - - /* Loop on 'freeing_list' for disarming and removing from kprobe hash list */ - list_for_each_entry_safe(op, tmp, &freeing_list, list) { + /* Loop on 'free_list' for disarming and removing from kprobe hash list */ + list_for_each_entry_safe(op, tmp, free_list, list) { /* Switching from detour code to origin */ op->kp.flags &= ~KPROBE_FLAG_OPTIMIZED; /* Disarm probes if marked disabled and not gone */ @@ -587,18 +640,26 @@ static void do_unoptimize_kprobes(void) * for synchronization, these probes are reclaimed. * (reclaiming is done by do_free_cleaned_kprobes().) */ - hlist_del_rcu(&op->kp.hlist); - } else - list_del_init(&op->list); + hlist_del_init_rcu(&op->kp.hlist); + } else { + /* + * Keep on cooling_list until the quiescence period + * completes so that kprobe_disarmed() remains false and + * unregister_kprobes() does not prematurely free it. + */ + list_move(&op->list, cooling_list); + } } } -/* Reclaim all kprobes on the 'freeing_list' */ -static void do_free_cleaned_kprobes(void) +/* Reclaim all kprobes on the 'free_list' */ +static void do_free_cleaned_kprobes(struct list_head *free_list) { struct optimized_kprobe *op, *tmp; - list_for_each_entry_safe(op, tmp, &freeing_list, list) { + list_for_each_entry_safe(op, tmp, free_list, list) { + struct kprobe *_p; + list_del_init(&op->list); if (WARN_ON_ONCE(!kprobe_unused(&op->kp))) { /* @@ -610,11 +671,10 @@ static void do_free_cleaned_kprobes(void) /* * The aggregator was holding back another probe while it sat on the - * unoptimizing/freeing lists. Now that the aggregator has been fully + * unoptimizing/freeing lists. Now that the aggregator has been fully * reverted we can safely retry the optimization of that sibling. */ - - struct kprobe *_p = get_optimized_kprobe(op->kp.addr); + _p = get_optimized_kprobe(op->kp.addr); if (unlikely(_p)) optimize_kprobe(_p); @@ -624,67 +684,146 @@ static void do_free_cleaned_kprobes(void) static void kick_kprobe_optimizer(void); -/* Kprobe jump optimizer */ -static void kprobe_optimizer(void) +static void optprobe_generation_rcu_cb(struct rcu_head *rcu) { - guard(mutex)(&kprobe_mutex); + struct optprobe_generation *gen; + + gen = container_of(rcu, struct optprobe_generation, rcu); + WRITE_ONCE(gen->ready, true); + wake_up(&kprobe_optimizer_wait); +} + +static void optprobe_finalize_generation(struct optprobe_generation *gen) +{ + struct optimized_kprobe *op, *tmp; + + lockdep_assert_held(&kprobe_mutex); + + scoped_guard(cpus_read_lock) { + guard(mutex)(&text_mutex); + + /* Optimization never be done when disarmed */ + if (!kprobes_all_disarmed && kprobes_allow_optimization && + !list_empty(&gen->optimizing_list)) + arch_optimize_kprobes(&gen->optimizing_list); + } + + /* Free cleaned kprobes after quiescence period */ + do_free_cleaned_kprobes(&gen->freeing_list); + + /* Finalize unoptimized kprobes whose quiescence period completed */ + list_for_each_entry_safe(op, tmp, &gen->cooling_list, list) { + if (kprobe_unused(&op->kp)) { + /* + * Unregistered while quiescence period was in flight. + * Unhash it now and move to cur_gen's freeing list so + * it will be reclaimed after the next quiescence period. + */ + hlist_del_init_rcu(&op->kp.hlist); + list_move(&op->list, &optprobe_gens[optprobe_cur_gen].freeing_list); + kick_kprobe_optimizer(); + } else { + /* Still in use; now safely disarmed */ + list_del_init(&op->list); + if (!kprobe_disabled(&op->kp)) + optimize_kprobe(&op->kp); + } + } + + gen->in_flight = false; + WRITE_ONCE(gen->ready, false); +} + +static bool optprobe_dispatch_generation(void) +{ + struct optprobe_generation *gen; + + lockdep_assert_held(&kprobe_mutex); + + if (!optprobe_can_fire() || !optprobe_has_queued_probes()) + return false; + + gen = &optprobe_gens[optprobe_cur_gen]; scoped_guard(cpus_read_lock) { guard(mutex)(&text_mutex); /* - * Step 1: Unoptimize kprobes and collect cleaned (unused and disarmed) - * kprobes before waiting for quiesence period. + * Unoptimize kprobes and collect cleaned (unused and disarmed) + * kprobes before waiting for quiescence period. */ - do_unoptimize_kprobes(); - - /* - * Step 2: Wait for quiesence period to ensure all potentially - * preempted tasks to have normally scheduled. Because optprobe - * may modify multiple instructions, there is a chance that Nth - * instruction is preempted. In that case, such tasks can return - * to 2nd-Nth byte of jump instruction. This wait is for avoiding it. - * Note that on non-preemptive kernel, this is transparently converted - * to synchronoze_sched() to wait for all interrupts to have completed. - */ - synchronize_rcu_tasks(); - - /* Step 3: Optimize kprobes after quiesence period */ - do_optimize_kprobes(); - - /* Step 4: Free cleaned kprobes after quiesence period */ - do_free_cleaned_kprobes(); + do_unoptimize_kprobes(&gen->unoptimizing_list, &gen->freeing_list, + &gen->cooling_list); } - /* Step 5: Wake up flushers, and kick optimizer again if needed. */ - optimizer_passes++; - wake_up_var_locked(&optimizer_passes, &kprobe_mutex); + /* Advance cur_gen to the next generation slot */ + optprobe_cur_gen = (optprobe_cur_gen + 1) % OPTPROBE_GEN_MAX; - if (!list_empty(&optimizing_list) || !list_empty(&unoptimizing_list)) - kick_kprobe_optimizer(); /*normal kick*/ + gen->in_flight = true; + WRITE_ONCE(gen->ready, false); + + call_rcu_tasks(&gen->rcu, optprobe_generation_rcu_cb); + return true; +} + +/* Kprobe jump optimizer */ +static void kprobe_optimizer(void) +{ + bool progress = false; + int i; + + guard(mutex)(&kprobe_mutex); + + /* Step 1: Finalize any generation whose Tasks RCU grace period completed */ + for (i = 0; i < OPTPROBE_GEN_MAX; i++) { + if (READ_ONCE(optprobe_gens[i].ready)) { + optprobe_finalize_generation(&optprobe_gens[i]); + progress = true; + } + } + + /* Step 2: Dispatch waiting room generation if allowed */ + if (optprobe_dispatch_generation()) + progress = true; + + /* Step 3: Wake up flushers if progress was made */ + if (progress) { + optimizer_passes++; + wake_up_var_locked(&optimizer_passes, &kprobe_mutex); + } + + if (optprobe_has_queued_probes() && optprobe_can_fire()) { + /* Probes remain and can be fired immediately (e.g. retried siblings) */ + kick_kprobe_optimizer(); + } } static int kprobe_optimizer_thread(void *data) { while (!kthread_should_stop()) { - /* To avoid hung_task, wait in interruptible state. */ + /* Wait until there is work to do or a generation is ready */ wait_event_interruptible(kprobe_optimizer_wait, - atomic_read(&optimizer_state) != OPTIMIZER_ST_IDLE || - kthread_should_stop()); + atomic_read(&optimizer_state) != OPTIMIZER_ST_IDLE || + optprobe_has_ready_gens() || + kthread_should_stop()); if (kthread_should_stop()) break; /* - * If it was a normal kick, wait for OPTIMIZE_DELAY. - * This wait can be interrupted by a flush request. + * If it was a normal kick and no generation is ready to finalize, + * wait for OPTIMIZE_DELAY to batch incoming requests. + * This wait can be interrupted by a flush request or a ready generation. */ - if (atomic_read(&optimizer_state) == 1) + if (atomic_read(&optimizer_state) == OPTIMIZER_ST_KICKED && + !optprobe_has_ready_gens()) { wait_event_interruptible_timeout( kprobe_optimizer_wait, atomic_read(&optimizer_state) == OPTIMIZER_ST_FLUSHING || + optprobe_has_ready_gens() || kthread_should_stop(), OPTIMIZE_DELAY); + } if (kthread_should_stop()) break; @@ -709,20 +848,20 @@ static void wait_for_kprobe_optimizer_locked(void) { lockdep_assert_held(&kprobe_mutex); - while (!list_empty(&optimizing_list) || !list_empty(&unoptimizing_list)) { + while (optprobe_optimizer_busy()) { unsigned long passes = optimizer_passes; - /* - * Set state to OPTIMIZER_ST_FLUSHING and wake up the thread if it's - * idle. If it's already kicked, it will see the state change. - */ - if (atomic_xchg_acquire(&optimizer_state, - OPTIMIZER_ST_FLUSHING) != OPTIMIZER_ST_FLUSHING) - wake_up(&kprobe_optimizer_wait); + /* Wake up optimizer thread if it can make progress */ + if ((optprobe_can_fire() && optprobe_has_queued_probes()) || + optprobe_has_ready_gens()) { + if (atomic_xchg_acquire(&optimizer_state, + OPTIMIZER_ST_FLUSHING) != OPTIMIZER_ST_FLUSHING) + wake_up(&kprobe_optimizer_wait); + } /* * kprobe_optimizer() holds 'kprobe_mutex' for a whole pass, which - * this drops while sleeping, so a new count means a full pass ran. + * this drops while sleeping, so a new count means progress was made. */ wait_var_event_mutex(&optimizer_passes, optimizer_passes != passes, &kprobe_mutex); @@ -740,10 +879,43 @@ void wait_for_kprobe_optimizer(void) bool optprobe_queued_unopt(struct optimized_kprobe *op) { struct optimized_kprobe *_op; + int i; - list_for_each_entry(_op, &unoptimizing_list, list) { - if (op == _op) - return true; + for (i = 0; i < OPTPROBE_GEN_MAX; i++) { + list_for_each_entry(_op, &optprobe_gens[i].unoptimizing_list, list) { + if (op == _op) + return true; + } + } + + return false; +} + +static bool optprobe_queued_freeing(struct optimized_kprobe *op) +{ + struct optimized_kprobe *_op; + int i; + + for (i = 0; i < OPTPROBE_GEN_MAX; i++) { + list_for_each_entry(_op, &optprobe_gens[i].freeing_list, list) { + if (op == _op) + return true; + } + } + + return false; +} + +static bool optprobe_queued_cooling(struct optimized_kprobe *op) +{ + struct optimized_kprobe *_op; + int i; + + for (i = 0; i < OPTPROBE_GEN_MAX; i++) { + list_for_each_entry(_op, &optprobe_gens[i].cooling_list, list) { + if (op == _op) + return true; + } } return false; @@ -777,6 +949,15 @@ static void optimize_kprobe(struct kprobe *p) } return; } + + if (optprobe_queued_cooling(op)) { + /* Under in-flight unoptimization. It will be re-optimized upon finalize */ + return; + } + + if (optprobe_queued_freeing(op)) + list_del_init(&op->list); + op->kp.flags |= KPROBE_FLAG_OPTIMIZED; /* @@ -786,7 +967,7 @@ static void optimize_kprobe(struct kprobe *p) if (WARN_ON_ONCE(!list_empty(&op->list))) return; - list_add(&op->list, &optimizing_list); + list_add(&op->list, &optprobe_gens[optprobe_cur_gen].optimizing_list); kick_kprobe_optimizer(); } @@ -801,14 +982,14 @@ static void force_unoptimize_kprobe(struct optimized_kprobe *op) /* Unoptimize a kprobe if p is optimized */ static void unoptimize_kprobe(struct kprobe *p, bool force) { + struct optprobe_generation *cur; struct optimized_kprobe *op; if (!kprobe_aggrprobe(p) || kprobe_disarmed(p)) return; /* This is not an optprobe nor optimized */ op = container_of(p, struct optimized_kprobe, kp); - if (!kprobe_optimized(p)) - return; + cur = &optprobe_gens[optprobe_cur_gen]; if (!list_empty(&op->list)) { if (optprobe_queued_unopt(op)) { @@ -816,10 +997,30 @@ static void unoptimize_kprobe(struct kprobe *p, bool force) if (force) { /* * Forcibly unoptimize the kprobe here, and queue it - * in the freeing list for release afterwards. + * for cooling down (or freeing if unused). */ force_unoptimize_kprobe(op); - list_move(&op->list, &freeing_list); + if (kprobe_unused(&op->kp)) { + if (!kprobe_gone(&op->kp)) + arch_disarm_kprobe(&op->kp); + hlist_del_init_rcu(&op->kp.hlist); + list_move(&op->list, &cur->freeing_list); + } else { + list_move(&op->list, &cur->cooling_list); + } + kick_kprobe_optimizer(); + } + } else if (optprobe_queued_cooling(op)) { + if (force && kprobe_unused(&op->kp)) { + /* + * Already unoptimized, move to freeing list for + * release afterwards. + */ + if (!kprobe_gone(&op->kp)) + arch_disarm_kprobe(&op->kp); + hlist_del_init_rcu(&op->kp.hlist); + list_move(&op->list, &cur->freeing_list); + kick_kprobe_optimizer(); } } else { /* Dequeue from the optimizing queue */ @@ -829,12 +1030,15 @@ static void unoptimize_kprobe(struct kprobe *p, bool force) return; } + if (!kprobe_optimized(p)) + return; + /* Optimized kprobe case */ if (force) { /* Forcibly update the code: this is a special case */ force_unoptimize_kprobe(op); } else { - list_add(&op->list, &unoptimizing_list); + list_add(&op->list, &cur->unoptimizing_list); kick_kprobe_optimizer(); } } @@ -866,23 +1070,29 @@ static void kill_optimized_kprobe(struct kprobe *p) struct optimized_kprobe *op; op = container_of(p, struct optimized_kprobe, kp); - if (!list_empty(&op->list)) - /* Dequeue from the (un)optimization queue */ - list_del_init(&op->list); + if (!list_empty(&op->list)) { + if (kprobe_unused(p)) { + if (optprobe_queued_unopt(op) || optprobe_queued_cooling(op)) { + hlist_del_init_rcu(&op->kp.hlist); + list_move(&op->list, + &optprobe_gens[optprobe_cur_gen].freeing_list); + kick_kprobe_optimizer(); + } else if (!optprobe_queued_freeing(op)) { + list_del_init(&op->list); + } + } else { + list_del_init(&op->list); + } + } op->kp.flags &= ~KPROBE_FLAG_OPTIMIZED; - if (kprobe_unused(p)) { - /* - * Unused kprobe is on unoptimizing or freeing list. We move it - * to freeing_list and let the kprobe_optimizer() remove it from - * the kprobe hash list and free it. - */ - if (optprobe_queued_unopt(op)) - list_move(&op->list, &freeing_list); - } - - /* Don't touch the code, because it is already freed. */ - arch_remove_optimized_kprobe(op); + /* + * Don't remove the slot if it is queued for freeing or unoptimization; + * the optimizer will reclaim it after the quiescence period. + */ + if (!optprobe_queued_freeing(op) && !optprobe_queued_unopt(op) && + !optprobe_queued_cooling(op)) + arch_remove_optimized_kprobe(op); } static inline @@ -1058,6 +1268,19 @@ static void __arm_kprobe(struct kprobe *p) optimize_kprobe(p); /* Try to optimize (add kprobe to a list) */ } +/* Return true if the probe is queued on unoptimizing lists */ +static bool kprobe_queued_unopt(struct kprobe *p) +{ + struct optimized_kprobe *op; + + if (kprobe_aggrprobe(p)) { + op = container_of(p, struct optimized_kprobe, kp); + if (optprobe_queued_unopt(op)) + return true; + } + return false; +} + /* Remove the breakpoint of a probe. */ static void __disarm_kprobe(struct kprobe *p, bool reopt) { @@ -1068,7 +1291,7 @@ static void __disarm_kprobe(struct kprobe *p, bool reopt) /* Try to unoptimize */ unoptimize_kprobe(p, kprobes_all_disarmed); - if (!kprobe_queued(p)) { + if (!kprobe_queued_unopt(p)) { arch_disarm_kprobe(p); /* If another kprobe was blocked, re-optimize it. */ _p = get_optimized_kprobe(p->addr); @@ -1079,11 +1302,23 @@ static void __disarm_kprobe(struct kprobe *p, bool reopt) static void __init init_optprobe(void) { + int i; + #ifdef __ARCH_WANT_KPROBES_INSN_SLOT /* Init 'kprobe_optinsn_slots' for allocation */ kprobe_optinsn_slots.insn_size = MAX_OPTINSN_SIZE; #endif + for (i = 0; i < OPTPROBE_GEN_MAX; i++) { + INIT_LIST_HEAD(&optprobe_gens[i].optimizing_list); + INIT_LIST_HEAD(&optprobe_gens[i].unoptimizing_list); + INIT_LIST_HEAD(&optprobe_gens[i].cooling_list); + INIT_LIST_HEAD(&optprobe_gens[i].freeing_list); + optprobe_gens[i].in_flight = false; + optprobe_gens[i].ready = false; + } + optprobe_cur_gen = 0; + init_waitqueue_head(&kprobe_optimizer_wait); atomic_set(&optimizer_state, OPTIMIZER_ST_IDLE); kprobe_optimizer_task = kthread_run(kprobe_optimizer_thread, NULL, From 3ccfe2a1a85f3639d770c9efe153ffdc58c60ac1 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Fri, 25 Sep 2026 13:12:23 +0900 Subject: [PATCH 8/9] tracing/probes: Clarify btf_find_struct_member() container parameter and add kernel-doc btf_find_struct_member() returns the containing structure or union type (which can be an inner anonymous struct/union rather than the outer passed type) via its last parameter. However, the parameter was named @member_type, which easily confuses readers and static analyzers into thinking it returns the member field's own datatype. Rename @member_type to @container_type in the prototype, definition, and callers to make the relationship explicit. Also convert the comment above btf_find_struct_member() to standard kernel-doc format, documenting all parameters and return values. Link: https://lore.kernel.org/all/179030954365.77418.17193777972248047975.stgit@devnote2/ Assisted-by: LLM Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_btf.c | 34 ++++++++++++++++++++++++---------- kernel/trace/trace_btf.h | 2 +- kernel/trace/trace_probe.c | 6 +++--- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/kernel/trace/trace_btf.c b/kernel/trace/trace_btf.c index ee7a04886bf6..a534907336ff 100644 --- a/kernel/trace/trace_btf.c +++ b/kernel/trace/trace_btf.c @@ -59,19 +59,33 @@ struct btf_anon_stack { u32 offset; }; -/* - * Find a member of data structure/union by name and return it. - * Return NULL if not found, or ERR_PTR(-EINVAL) if parameter is invalid. - * If the member is a member of an anonymous union/structure, the bit offset - * of that anonymous union/structure is stored into @anon_offset. - * If @member_type is non-NULL, the actual containing structure/union type - * of the found member is stored into @member_type. +/** + * btf_find_struct_member() - Find a member of data structure/union by name + * @btf: Pointer to BTF object. + * @type: Pointer to the struct/union BTF type to search within. + * @member_name: Name of the member to find. + * @anon_offset: Pointer to store bit offset of enclosing anonymous union/struct. + * @container_type: Pointer to store the actual containing struct/union type. + * + * Find a member of data structure/union by name, traversing into anonymous + * unions and structures if necessary. + * + * If the member is inside an anonymous union/structure, the bit offset + * of that anonymous union/structure from @type is stored into @anon_offset. + * If @container_type is non-NULL, the actual struct/union BTF type that + * directly contains the found member is stored into @container_type. + * + * Return: + * * Pointer to &struct btf_member on success. + * * %NULL if the member was not found. + * * %ERR_PTR(-EINVAL) if @type is not a struct/union. + * * %ERR_PTR(-ENOMEM) if memory allocation failed. */ const struct btf_member *btf_find_struct_member(struct btf *btf, const struct btf_type *type, const char *member_name, u32 *anon_offset, - const struct btf_type **member_type) + const struct btf_type **container_type) { struct btf_anon_stack *anon_stack; const struct btf_member *member; @@ -103,8 +117,8 @@ const struct btf_member *btf_find_struct_member(struct btf *btf, if (name && !strcmp(member_name, name)) { if (anon_offset) *anon_offset = cur_offset; - if (member_type) - *member_type = type; + if (container_type) + *container_type = type; goto out; } } diff --git a/kernel/trace/trace_btf.h b/kernel/trace/trace_btf.h index 4bd26bceae23..68f1d61fefb0 100644 --- a/kernel/trace/trace_btf.h +++ b/kernel/trace/trace_btf.h @@ -9,4 +9,4 @@ const struct btf_member *btf_find_struct_member(struct btf *btf, const struct btf_type *type, const char *member_name, u32 *anon_offset, - const struct btf_type **member_type); + const struct btf_type **container_type); diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index cfb710095f65..c08f048f9716 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -625,7 +625,7 @@ static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **pty { const struct btf_type *type = *ptype; const struct btf_member *field; - const struct btf_type *mtype; + const struct btf_type *container_type; struct btf *btf = ctx_btf(ctx); char *fieldname = *pfieldname; int bitoffs = 0; @@ -641,7 +641,7 @@ static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **pty anon_offs = 0; field = btf_find_struct_member(btf, type, fieldname, - &anon_offs, &mtype); + &anon_offs, &container_type); if (IS_ERR(field)) { trace_probe_log_err(ctx->offset, BAD_BTF_TID); return PTR_ERR(field); @@ -654,7 +654,7 @@ static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **pty bitoffs += anon_offs; /* Accumulate the bit-offsets of the dot-connected fields */ - if (btf_type_kflag(mtype)) { + if (btf_type_kflag(container_type)) { bitoffs += BTF_MEMBER_BIT_OFFSET(field->offset); ctx->last_bitsize = BTF_MEMBER_BITFIELD_SIZE(field->offset); } else { From d4d028a7ae6401ce70825a3eb663f0a2dfc49db6 Mon Sep 17 00:00:00 2001 From: Alexander Gordeev Date: Tue, 25 Aug 2026 18:58:40 +0200 Subject: [PATCH 9/9] ftrace/selftests: Change non-unique test symbol to init_once The name_show symbol is not the best choise, because it often appears only three times on s390 in kallsyms, with two of them having module annotations [mdev] and [i2c_core]. Switch to init_once which has many more duplicate instances all in vmlinux, making it a more reliable non-unique symbol for testing kprobe registration failure. Link: https://lore.kernel.org/all/67ac21cd214ec9371276df9ba790e9832cf328d4.1787670777.git.agordeev@linux.ibm.com/ Signed-off-by: Alexander Gordeev Signed-off-by: Masami Hiramatsu (Google) --- .../selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc index 07b1177c1634..03eb9e70b438 100644 --- a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc +++ b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_non_uniq_symbol.tc @@ -3,7 +3,7 @@ # description: Test failure of registering kprobe on non unique symbol # requires: kprobe_events -SYMBOL='name_show' +SYMBOL='init_once' # We skip this test on kernel where SYMBOL is unique or does not exist. if [ "$(grep -c -E "[[:alnum:]]+ t ${SYMBOL}$" /proc/kallsyms)" -le '1' ]; then