]> Gentwo Git Trees - linux/.git/commitdiff
tracing: Add a tracepoint verification check at build time
authorSteven Rostedt <rostedt@goodmis.org>
Wed, 22 Oct 2025 00:43:40 +0000 (20:43 -0400)
committerSteven Rostedt (Google) <rostedt@goodmis.org>
Fri, 24 Oct 2025 20:43:14 +0000 (16:43 -0400)
If a tracepoint is defined via DECLARE_TRACE() or TRACE_EVENT() but never
called (via the trace_<tracepoint>() function), its metadata is still
around in memory and not discarded.

When created via TRACE_EVENT() the situation is worse because the
TRACE_EVENT() creates metadata that can be around 5k per trace event.
Having unused trace events causes several thousand of wasted bytes.

Add a verifier that injects a string of the name of the tracepoint it
calls that is added to the discarded section "__tracepoint_check".
For every builtin tracepoint, its name (which is saved in the in-memory
section "__tracepoint_strings") will have its name also in the
"__tracepoint_check" section if it is used.

Add a new program that is run on build called tracepoint-update. This is
executed on the vmlinux.o before the __tracepoint_check section is
discarded (the section is discarded before vmlinux is created). This
program will create an array of each string in the __tracepoint_check
section and then sort it. Then it will walk the strings in the
__tracepoint_strings section and do a binary search to check if its name
is in the __tracepoint_check section. If it is not, then it is unused and
a warning is printed.

Note, this currently only handles tracepoints that are builtin and not in
modules.

Enabling this currently with a given config produces:

warning: tracepoint 'sched_move_numa' is unused.
warning: tracepoint 'sched_stick_numa' is unused.
warning: tracepoint 'sched_swap_numa' is unused.
warning: tracepoint 'pelt_hw_tp' is unused.
warning: tracepoint 'pelt_irq_tp' is unused.
warning: tracepoint 'rcu_preempt_task' is unused.
warning: tracepoint 'rcu_unlock_preempted_task' is unused.
warning: tracepoint 'xdp_bulk_tx' is unused.
warning: tracepoint 'xdp_redirect_map' is unused.
warning: tracepoint 'xdp_redirect_map_err' is unused.
warning: tracepoint 'vma_mas_szero' is unused.
warning: tracepoint 'vma_store' is unused.
warning: tracepoint 'hugepage_set_pmd' is unused.
warning: tracepoint 'hugepage_set_pud' is unused.
warning: tracepoint 'hugepage_update_pmd' is unused.
warning: tracepoint 'hugepage_update_pud' is unused.
warning: tracepoint 'block_rq_remap' is unused.
warning: tracepoint 'xhci_dbc_handle_event' is unused.
warning: tracepoint 'xhci_dbc_handle_transfer' is unused.
warning: tracepoint 'xhci_dbc_gadget_ep_queue' is unused.
warning: tracepoint 'xhci_dbc_alloc_request' is unused.
warning: tracepoint 'xhci_dbc_free_request' is unused.
warning: tracepoint 'xhci_dbc_queue_request' is unused.
warning: tracepoint 'xhci_dbc_giveback_request' is unused.
warning: tracepoint 'tcp_ao_wrong_maclen' is unused.
warning: tracepoint 'tcp_ao_mismatch' is unused.
warning: tracepoint 'tcp_ao_key_not_found' is unused.
warning: tracepoint 'tcp_ao_rnext_request' is unused.
warning: tracepoint 'tcp_ao_synack_no_key' is unused.
warning: tracepoint 'tcp_ao_snd_sne_update' is unused.
warning: tracepoint 'tcp_ao_rcv_sne_update' is unused.

Some of the above is totally unused but others are not used due to their
"trace_" functions being inside configs, in which case, the defined
tracepoints should also be inside those same configs. Others are
architecture specific but defined in generic code, where they should
either be moved to the architecture or be surrounded by #ifdef for the
architectures they are for.

This tool could be updated to process modules in the future.

I'd like to thank Mathieu Desnoyers for suggesting using strings instead
of pointers, as using pointers in vmlinux.o required handling relocations
and it required implementing almost a full feature linker to do so.

To enable this check, run the build with: make UT=1

Note, when all the existing unused tracepoints are removed from the build,
the "UT=1" will be removed and this will always be enabled when
tracepoints are configured to warn on any new tracepoints. The reason this
isn't always enabled now is because it will introduce a lot of warnings
for the current unused tracepoints, and all bisects would end at this
commit for those warnings.

Link: https://lore.kernel.org/all/20250528114549.4d8a5e03@gandalf.local.home/
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mark Rutland <mark.rutland@arm.com>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Arnd Bergmann <arnd@arndb.de>
Cc: Masahiro Yamada <masahiroy@kernel.org>
Cc: Nathan Chancellor <nathan@kernel.org>
Cc: Nicolas Schier <nicolas.schier@linux.dev>
Cc: Nick Desaulniers <nick.desaulniers+lkml@gmail.com>
Cc: Catalin Marinas <catalin.marinas@arm.com>
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Cc: Randy Dunlap <rdunlap@infradead.org>
Cc: Stephen Rothwell <sfr@canb.auug.org.au>
Link: https://lore.kernel.org/20251022004452.920728129@kernel.org
Suggested-by: Mathieu Desnoyers <mathieu.desnoyers@efficios.com> # for using strings instead of pointers
Signed-off-by: Steven Rostedt (Google) <rostedt@goodmis.org>
Makefile
include/asm-generic/vmlinux.lds.h
include/linux/tracepoint.h
scripts/Makefile
scripts/link-vmlinux.sh
scripts/tracepoint-update.c [new file with mode: 0644]

index d14824792227bdf08266295898b48f057e0f8333..9823c20c4278db318c6c42e7f5786870e054adc7 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -810,6 +810,25 @@ ifdef CONFIG_FUNCTION_TRACER
   CC_FLAGS_FTRACE := -pg
 endif
 
+ifdef CONFIG_TRACEPOINTS
+# To check for unused tracepoints (tracepoints that are defined but never
+# called), run with:
+#
+# make UT=1
+#
+# Each unused tracepoints can take up to 5KB of memory in the running kernel.
+# It is best to remove any that are not used.
+#
+# This command line option will be removed when all current unused
+# tracepoints are removed.
+
+ifeq ("$(origin UT)", "command line")
+  WARN_ON_UNUSED_TRACEPOINTS := $(UT)
+endif
+endif # CONFIG_TRACEPOINTS
+
+export WARN_ON_UNUSED_TRACEPOINTS
+
 include $(srctree)/arch/$(SRCARCH)/Makefile
 
 ifdef need-config
@@ -1772,6 +1791,8 @@ help:
        @echo  '                c: extra checks in the configuration stage (Kconfig)'
        @echo  '                e: warnings are being treated as errors'
        @echo  '                Multiple levels can be combined with W=12 or W=123'
+       @echo  '  make UT=1   [targets] Warn if a tracepoint is defined but not used.'
+       @echo  '          [ This will be removed when all current unused tracepoints are eliminated. ]'
        @$(if $(dtstree), \
                echo '  make CHECK_DTBS=1 [targets] Check all generated dtb files against schema'; \
                echo '         This can be applied both to "dtbs" and to individual "foo.dtb" targets' ; \
index 8a9a2e732a65b04d18453e266900895ef41bb615..c510fb097a8c2b76e83fe9cdb7c7248a73e43a8d 100644 (file)
@@ -1048,6 +1048,7 @@ defined(CONFIG_AUTOFDO_CLANG) || defined(CONFIG_PROPELLER_CLANG)
        *(.no_trim_symbol)                                              \
        /* ld.bfd warns about .gnu.version* even when not emitted */    \
        *(.gnu.version*)                                                \
+       *(__tracepoint_check)                                           \
 
 #define DISCARDS                                                       \
        /DISCARD/ : {                                                   \
index 826ce3f8e1f851edba507d321bcdfb6fdf55a2bc..1e53d3626c7802c36419719f772e55ef1a25ebda 100644 (file)
@@ -221,6 +221,15 @@ static inline struct tracepoint *tracepoint_ptr_deref(tracepoint_ptr_t *p)
                __do_trace_##name(args);                                \
        }
 
+/*
+ * When a tracepoint is used, it's name is added to the __tracepoint_check
+ * section. This section is only used at build time to make sure all
+ * defined tracepoints are used. It is discarded after the build.
+ */
+# define TRACEPOINT_CHECK(name)                                                \
+       static const char __used __section("__tracepoint_check") __trace_check[] = \
+               #name;
+
 /*
  * Make sure the alignment of the structure in the __tracepoints section will
  * not add unwanted padding between the beginning of the section and the
@@ -270,6 +279,7 @@ static inline struct tracepoint *tracepoint_ptr_deref(tracepoint_ptr_t *p)
        __DECLARE_TRACE_COMMON(name, PARAMS(proto), PARAMS(args), PARAMS(data_proto)) \
        static inline void __do_trace_##name(proto)                     \
        {                                                               \
+               TRACEPOINT_CHECK(name)                                  \
                if (cond) {                                             \
                        guard(preempt_notrace)();                       \
                        __DO_TRACE_CALL(name, TP_ARGS(args));           \
@@ -289,6 +299,7 @@ static inline struct tracepoint *tracepoint_ptr_deref(tracepoint_ptr_t *p)
        __DECLARE_TRACE_COMMON(name, PARAMS(proto), PARAMS(args), PARAMS(data_proto)) \
        static inline void __do_trace_##name(proto)                     \
        {                                                               \
+               TRACEPOINT_CHECK(name)                                  \
                guard(rcu_tasks_trace)();                               \
                __DO_TRACE_CALL(name, TP_ARGS(args));                   \
        }                                                               \
index f19624b3ed929fc1c3fa1b495a6a0e204ed06306..0941e5ce7b575f0f43929777eae814b012e90710 100644 (file)
@@ -11,8 +11,10 @@ hostprogs-always-$(CONFIG_MODULE_SIG_FORMAT)         += sign-file
 hostprogs-always-$(CONFIG_SYSTEM_EXTRA_CERTIFICATE)    += insert-sys-cert
 hostprogs-always-$(CONFIG_RUST_KERNEL_DOCTESTS)                += rustdoc_test_builder
 hostprogs-always-$(CONFIG_RUST_KERNEL_DOCTESTS)                += rustdoc_test_gen
+hostprogs-always-$(CONFIG_TRACEPOINTS)                 += tracepoint-update
 
 sorttable-objs := sorttable.o elf-parse.o
+tracepoint-update-objs := tracepoint-update.o elf-parse.o
 
 ifneq ($(or $(CONFIG_X86_64),$(CONFIG_X86_32)),)
 always-$(CONFIG_RUST)                                  += target.json
@@ -27,6 +29,7 @@ generate_rust_target-rust := y
 rustdoc_test_builder-rust := y
 rustdoc_test_gen-rust := y
 
+HOSTCFLAGS_tracepoint-update.o = -I$(srctree)/tools/include
 HOSTCFLAGS_elf-parse.o = -I$(srctree)/tools/include
 HOSTCFLAGS_sorttable.o = -I$(srctree)/tools/include
 HOSTLDLIBS_sorttable = -lpthread
index 433849ff7529e1fb8bb24e01163455844c8a4c88..d304029fa6da8a61cef5bfe88acc4c169bdf22b6 100755 (executable)
@@ -208,6 +208,13 @@ kallsymso=
 strip_debug=
 generate_map=
 
+# Use "make UT=1" to trigger warnings on unused tracepoints
+case "${WARN_ON_UNUSED_TRACEPOINTS}" in
+*1*)
+       ${objtree}/scripts/tracepoint-update vmlinux.o
+       ;;
+esac
+
 if is_enabled CONFIG_KALLSYMS; then
        true > .tmp_vmlinux0.syms
        kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms
diff --git a/scripts/tracepoint-update.c b/scripts/tracepoint-update.c
new file mode 100644 (file)
index 0000000..6ec30f3
--- /dev/null
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <getopt.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <pthread.h>
+
+#include "elf-parse.h"
+
+static Elf_Shdr *check_data_sec;
+static Elf_Shdr *tracepoint_data_sec;
+
+static inline void *get_index(void *start, int entsize, int index)
+{
+       return start + (entsize * index);
+}
+
+static int compare_strings(const void *a, const void *b)
+{
+       const char *av = *(const char **)a;
+       const char *bv = *(const char **)b;
+
+       return strcmp(av, bv);
+}
+
+struct elf_tracepoint {
+       Elf_Ehdr *ehdr;
+       const char **array;
+       int count;
+};
+
+#define REALLOC_SIZE (1 << 10)
+#define REALLOC_MASK (REALLOC_SIZE - 1)
+
+static int add_string(const char *str, const char ***vals, int *count)
+{
+       const char **array = *vals;
+
+       if (!(*count & REALLOC_MASK)) {
+               int size = (*count) + REALLOC_SIZE;
+
+               array = realloc(array, sizeof(char *) * size);
+               if (!array) {
+                       fprintf(stderr, "Failed memory allocation\n");
+                       return -1;
+               }
+               *vals = array;
+       }
+
+       array[(*count)++] = str;
+       return 0;
+}
+
+/**
+ * for_each_shdr_str - iterator that reads strings that are in an ELF section.
+ * @len: "int" to hold the length of the current string
+ * @ehdr: A pointer to the ehdr of the ELF file
+ * @sec: The section that has the strings to iterate on
+ *
+ * This is a for loop that iterates over all the nul terminated strings
+ * that are in a given ELF section. The variable "str" will hold
+ * the current string for each iteration and the passed in @len will
+ * contain the strlen() of that string.
+ */
+#define for_each_shdr_str(len, ehdr, sec)                              \
+       for (const char *str = (void *)(ehdr) + shdr_offset(sec),       \
+                       *end = str + shdr_size(sec);                    \
+            len = strlen(str), str < end;                              \
+            str += (len) + 1)
+
+
+static void make_trace_array(struct elf_tracepoint *etrace)
+{
+       Elf_Ehdr *ehdr = etrace->ehdr;
+       const char **vals = NULL;
+       int count = 0;
+       int len;
+
+       etrace->array = NULL;
+
+       /*
+        * The __tracepoint_check section is filled with strings of the
+        * names of tracepoints (in tracepoint_strings). Create an array
+        * that points to each string and then sort the array.
+        */
+       for_each_shdr_str(len, ehdr, check_data_sec) {
+               if (!len)
+                       continue;
+               if (add_string(str, &vals, &count) < 0)
+                       return;
+       }
+
+       /* If CONFIG_TRACEPOINT_VERIFY_USED is not set, there's nothing to do */
+       if (!count)
+               return;
+
+       qsort(vals, count, sizeof(char *), compare_strings);
+
+       etrace->array = vals;
+       etrace->count = count;
+}
+
+static int find_event(const char *str, void *array, size_t size)
+{
+       return bsearch(&str, array, size, sizeof(char *), compare_strings) != NULL;
+}
+
+static void check_tracepoints(struct elf_tracepoint *etrace)
+{
+       Elf_Ehdr *ehdr = etrace->ehdr;
+       int len;
+
+       if (!etrace->array)
+               return;
+
+       /*
+        * The __tracepoints_strings section holds all the names of the
+        * defined tracepoints. If any of them are not in the
+        * __tracepoint_check_section it means they are not used.
+        */
+       for_each_shdr_str(len, ehdr, tracepoint_data_sec) {
+               if (!len)
+                       continue;
+               if (!find_event(str, etrace->array, etrace->count)) {
+                       fprintf(stderr, "warning: tracepoint '%s' is unused.\n", str);
+               }
+       }
+
+       free(etrace->array);
+}
+
+static void *tracepoint_check(struct elf_tracepoint *etrace)
+{
+       make_trace_array(etrace);
+       check_tracepoints(etrace);
+
+       return NULL;
+}
+
+static int process_tracepoints(void *addr, char const *const fname)
+{
+       struct elf_tracepoint etrace = {0};
+       Elf_Ehdr *ehdr = addr;
+       Elf_Shdr *shdr_start;
+       Elf_Shdr *string_sec;
+       const char *secstrings;
+       unsigned int shnum;
+       unsigned int shstrndx;
+       int shentsize;
+       int idx;
+       int done = 2;
+
+       shdr_start = (Elf_Shdr *)((char *)ehdr + ehdr_shoff(ehdr));
+       shentsize = ehdr_shentsize(ehdr);
+
+       shstrndx = ehdr_shstrndx(ehdr);
+       if (shstrndx == SHN_XINDEX)
+               shstrndx = shdr_link(shdr_start);
+       string_sec = get_index(shdr_start, shentsize, shstrndx);
+       secstrings = (const char *)ehdr + shdr_offset(string_sec);
+
+       shnum = ehdr_shnum(ehdr);
+       if (shnum == SHN_UNDEF)
+               shnum = shdr_size(shdr_start);
+
+       for (int i = 0; done && i < shnum; i++) {
+               Elf_Shdr *shdr = get_index(shdr_start, shentsize, i);
+
+               idx = shdr_name(shdr);
+
+               /* locate the __tracepoint_check in vmlinux */
+               if (!strcmp(secstrings + idx, "__tracepoint_check")) {
+                       check_data_sec = shdr;
+                       done--;
+               }
+
+               /* locate the __tracepoints_ptrs section in vmlinux */
+               if (!strcmp(secstrings + idx, "__tracepoints_strings")) {
+                       tracepoint_data_sec = shdr;
+                       done--;
+               }
+       }
+
+       if (!check_data_sec) {
+               fprintf(stderr, "no __tracepoint_check in file: %s\n", fname);
+               return -1;
+       }
+
+       if (!tracepoint_data_sec) {
+               fprintf(stderr, "no __tracepoint_strings in file: %s\n", fname);
+               return -1;
+       }
+
+       etrace.ehdr = ehdr;
+       tracepoint_check(&etrace);
+       return 0;
+}
+
+int main(int argc, char *argv[])
+{
+       int n_error = 0;
+       size_t size = 0;
+       void *addr = NULL;
+
+       if (argc < 2) {
+               fprintf(stderr, "usage: tracepoint-update vmlinux...\n");
+               return 0;
+       }
+
+       /* Process each file in turn, allowing deep failure. */
+       for (int i = 1; i < argc; i++) {
+               addr = elf_map(argv[i], &size, 1 << ET_REL);
+               if (!addr) {
+                       ++n_error;
+                       continue;
+               }
+
+               if (process_tracepoints(addr, argv[i]))
+                       ++n_error;
+
+               elf_unmap(addr, size);
+       }
+
+       return !!n_error;
+}