Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH RFC 0/2] kcov: collect early boot coverage of init_task
@ 2026-10-27 16:34 Yunseong Kim
  2026-10-27 16:34 ` [PATCH RFC 1/2] " Yunseong Kim
  2026-10-27 16:34 ` [PATCH RFC 2/2] x86/kcov: allow instrumenting noisy objects for early boot coverage Yunseong Kim
  0 siblings, 2 replies; 5+ messages in thread
From: Yunseong Kim @ 2026-10-27 16:34 UTC (permalink / raw)
  To: Jonathan Corbet, Shuah Khan, James Morris, Randy Dunlap,
	Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov,
	Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long,
	Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, Paul E. McKenney, Frederic Weisbecker,
	Neeraj Upadhyay, Joel Fernandes, Josh Triplett, Uladzislau Rezki,
	Steven Rostedt, Mathieu Desnoyers, Lai Jiangshan, Zqiang,
	Juri Lelli, Vincent Guittot, Dietmar Eggemann, Ben Segall,
	Mel Gorman, Valentin Schneider, K Prateek Nayak, Andrew Morton,
	Nathan Chancellor, Nicolas Schier, Thomas Gleixner,
	Borislav Petkov, Dave Hansen, x86, H. Peter Anvin
  Cc: linux-doc, linux-kernel, kasan-dev, workflows, linux-modules, rcu,
	linux-mm, linux-kbuild, Yunseong Kim

This RFC adds a way to collect KCOV coverage of the boot path, from
right after early parameter parsing in start_kernel() up to
rest_init(), and to read it back from userspace after boot.

Motivation
==========

1. The boot path depends heavily on the configuration

Reviewing the boot path by reading the code is hard, even if you stop
at rest_init(). start_kernel() looks like a flat list of calls, but
what runs below it depends on the kernel configuration, the
architecture, the firmware, the command line, static keys and
alternatives. Many of the calls are empty stubs in one configuration
and large subsystems in another, and a lot of the work happens in
inline helpers in headers.

Even the top-level files are full of configuration branches.
init/main.c has 13 lines with #ifdef CONFIG_*/IS_ENABLED(CONFIG_*),
mm/mm_init.c has 34 and arch/x86/kernel/setup.c has 36, and each
branch leads into code with its own branches. Some examples from the
x86_64 test setup described below:

  - Without KCOV_INSTRUMENT_NOISY, a boot records 17.3M basic block
    executions before rest_init(). 10.4M of them (60%) come from the
    step where arch_cpu_finalize_init() applies the alternatives. With
    the retpoline and rethunk mitigations enabled, that is mostly
    apply_retpolines(), apply_returns() and the x86 instruction
    decoder they use (93% of the step in the NOISY=y boot below).
    apply_retpolines() and apply_returns() are empty stubs unless
    MITIGATION_RETPOLINE/MITIGATION_RETHUNK are set.
  - The tracing options (FUNCTION_TRACER, EVENT_TRACING,
    FTRACE_SYSCALLS) add another 6.0M executions (35%) in ftrace_init()
    and trace_init(), which set up the ftrace call sites, the trace
    events and the syscall metadata.
  - Of the 11,131 distinct PCs executed (with KCOV_INSTRUMENT_NOISY),
    544 are in tracing code, which only runs with the tracing options,
    and 500 are in ACPI code, which depends on CONFIG_ACPI and on the
    firmware providing ACPI tables.

In total, the part of start_kernel() between parse_early_param() and
rest_init() executes 11,131 distinct PCs in 1,529 functions spread
over 568 source files (10,877 source lines). 131 of those files are
under arch/x86 and 109 are headers in include/linux. A different
.config, CPU or firmware gives a different set, and reading the source
cannot tell you which set your kernel runs. You have to work it out
again for every configuration.

2. Early ftrace does not show the code that actually runs

The function tracer is the usual tool for looking at early boot, but
it cannot answer "which code ran":

  - It starts late. Boot-time function tracing (ftrace=function)
    begins in tracer_alloc_buffers(), called from early_trace_init().
    Nothing before that point can be traced: setup_arch(),
    setup_per_cpu_areas(), and mm_core_init() with
    build_all_zonelists(), memblock_free_all() and the slab and
    vmalloc setup. (This series does not cover setup_arch() yet
    either, see the limitations below.) Even inside the window this
    series covers, 2,758 of the 11,131 distinct PCs (25%) are first
    reached before early_trace_init().

  - It works at function granularity. It records that a function was
    entered, not which branches inside it were taken. Those branches
    are exactly where the configuration and the hardware differences
    show up.

  - It does not see inlined code. 1,739 of the functions executed
    during this window were only ever executed inlined, and 4,912 of
    the 11,131 PCs (44%) are in inlined code. None of this has an
    fentry call site.

  - Large parts of the kernel are built without ftrace hooks. lib/
    and kernel/trace/ remove CC_FLAGS_FTRACE for the whole directory,
    and kernel/, mm/, lib/, init/, arch/x86/ and drivers/acpi/ have
    55 more per-object CFLAGS_REMOVE lines doing the same. Early KCOV
    covers 30 files in lib/ alone.

  - Its buffer is sized for events, not for loops. The ring buffer
    defaults to 1,441,792 bytes per CPU (TRACE_BUF_SIZE_DEFAULT) in
    overwrite mode. With boot loops calling the same functions
    millions of times, the earliest events are overwritten unless
    trace_buf_size= is raised a lot.

ftrace remains the right tool for timing and call order. Early KCOV
answers a different question: which basic blocks ran, in which order
they were first reached, including inlined and non-traceable code,
starting before the tracing infrastructure exists.

3. A learning and review tool

With this series the executed path becomes a concrete, ordered list
that can be resolved with addr2line against vmlinux and read next to
the source, for exactly the configuration and machine being booted.
This is useful for people learning the kernel, and for reviewers who
want to check which code a boot-time change really affects. That use
stands on its own: even if this is not accepted upstream, I intend to
keep it as a learning tool.

4. Coverage-guided fuzzing of boot-time input

A lot of input is parsed while the kernel boots: the command line and
early parameters, bootconfig, ACPI tables, the e820 map, the device
tree. For confidential guests (TDX, SEV-SNP) much of it is provided by
an untrusted host. KCOV cannot give coverage feedback for any of this
today, since collection only starts once userspace opens
/sys/kernel/debug/kcov. A fuzzer that boots a VM with mutated inputs
could use the early coverage as its feedback signal.

5. Comparing boots

Diffing the coverage of two boots shows how two configurations,
command lines, CPUs or kernel versions take different paths, and the
first-hit order shows where they diverge.

What the series does
====================

Patch 1/2: kcov: collect early boot coverage of init_task

  a) Collection, behind the new CONFIG_EARLY_KCOV (default n)

  - early_kcov_init() is called right after parse_early_param() in
    start_kernel(). It allocates a buffer with memblock_alloc(),
    attaches it to init_task and enables KCOV_MODE_TRACE_PC. The size
    defaults to CONFIG_EARLY_KCOV_SIZE_MB (256) and can be changed with
    early_kcov_size=; 0 disables the collection. The memparse() result
    is range checked as a 64-bit value before it is narrowed.
  - early_kcov_stop() (__init) is the first statement of rest_init().
    It disables collection and detaches the buffer from init_task.
  - A late_initcall frees the unused tail of the buffer and creates
    /sys/kernel/debug/early_kcov (0600). It uses the regular KCOV
    layout: word 0 is the number of PCs, then the PCs. They are
    canonicalized (KASLR offset removed) like other KCOV data, so they
    match vmlinux directly. The tail is freed there rather than in
    rest_init() because memblock memory can only be returned once
    deferred struct page init has completed. If the file cannot be
    created, the whole buffer is freed.
  - Writing to the file removes it and frees the rest of the buffer.
    I would have liked "rm" for this, but debugfs directories have no
    ->unlink. The removal runs from a work item, because
    debugfs_remove() waits for the write to return. Readers holding an
    open file descriptor get an error afterwards.
  - __sanitizer_cov_trace_pc() is not changed, so KCOV users that do
    not enable CONFIG_EARLY_KCOV are not affected.

  b) CONFIG_KCOV_INSTRUMENT_NOISY (depends on CONFIG_EARLY_KCOV)

  Many objects are built with KCOV_INSTRUMENT := n because their
  coverage is not a function of syscall inputs: the scheduler, RCU,
  locking, the page and slab allocators, parts of lib/, stack
  unwinding. That is the right choice for fuzzing, but for the boot
  path it hides sched_init(), build_all_zonelists() and much of mm
  setup.

  The new option makes those sites instrumented. The sites use a new
  kbuild helper,

      KCOV_INSTRUMENT_foo.o := $(kcov-noisy)

  which expands to "n" unless the option is set. Only sites whose
  comment says they were excluded for noise are converted. Sites
  excluded because instrumentation is unsafe are left alone: kcov.o,
  kernel/entry, head64, x86 cpu/common.o, tlb.c and mem_encrypt*.c,
  stackdepot, and the KASAN/KMSAN/KCSAN runtimes. This patch converts
  the generic sites. With the option off, the build is unchanged: no
  object was recompiled, and page_alloc.o, rcu/tree.o and string.o
  have no coverage callbacks.

  c) Documentation

  early_kcov_size= is documented in kernel-parameters.txt, and
  Documentation/dev-tools/kcov.rst gets an "Early boot coverage"
  section: configuration, the covered window, the file format, how to
  decode it with addr2line against vmlinux, how to release it, and the
  kcov-noisy convention.

Patch 2/2: x86/kcov: allow instrumenting noisy objects for early boot
coverage

  Converts the x86 sites excluded only for noise: apic/, delay.o, and
  the stacktrace, dumpstack and unwinder objects. The objects excluded
  because the kernel hangs or does not boot when they are instrumented
  (head64.o, cpu/common.o, perf_event.o, tlb.c, mem_encrypt*.c,
  pgprot.c) stay excluded, as do machine_kexec_64.o and physaddr.o
  (kexec invalidates the GS base). Other architectures are not
  converted, since x86 is what I could test.

Measurements
============

Test setup: x86_64 QEMU guest under virtme-ng, 4 vCPUs in 2 NUMA
nodes, GCC 16.2, the virtme-ng default config plus the options below,
on the base commit given at the end. The config has FUNCTION_TRACER,
the retpoline/rethunk mitigations and X86_FRED enabled. Each boot used
early_kcov_size=512M so that nothing was truncated.

  NOISY  RAM   PCs recorded  buffer used  distinct PCs
  -----  ----  ------------  -----------  ------------
  n      2G       17.3M       132 MiB          8,837
  n      4G       17.4M       133 MiB          8,864
  n      8G       17.5M       133 MiB          8,861
  y      2G       31.6M       241 MiB         11,131
  y      4G       34.8M       265 MiB         11,194
  y      8G       41.2M       314 MiB         11,169

Without the noisy objects the volume barely depends on the amount of
memory. With them it grows by about 1.6M PCs per GiB, from freeing
pages to the buddy allocator. The 256 MiB default holds a full
boot without the noisy objects, and with them up to about 2G of RAM.
When the buffer is too small, the remaining PCs are dropped and the
log says "buffer full, coverage truncated".

Where the executions go, attributed to the start_kernel() call that
was running (NOISY=y, 2G, 31.6M executions in total):

  executions       share  new distinct PCs  start_kernel() step
  ----------       -----  ----------------  -------------------
  14.3M            45.3%         603        trace_init()
  11.1M            35.2%       1,091        calibrate_delay() and
                                            arch_cpu_finalize_init()
   3.0M             9.4%       1,466        mm_core_init()
   2.7M             8.6%         254        ftrace_init()
   0.2M             0.5%         493        pr_info() in early_kcov_init()
   0.07M            0.2%       1,690        acpi_early_init()
   0.05M            0.2%         862        vfs_caches_init()

arch_cpu_finalize_init() is in arch/x86/kernel/cpu/common.c, which is
not instrumented, so its alternatives patching is counted together
with calibrate_delay(), the call before it. With the noisy objects,
the lib/string.c helpers (strstr(), strncmp(), memcmp(), strcmp())
are 73% of trace_init(). Going from 2G to 8G adds 9.6M executions,
96% of them in __free_pages_ok() and __free_pages_core(). The steps
with few executions are often the ones that reach the most new code:
acpi_early_init() is 0.2% of the executions but 15% of the distinct
PCs.

How to reproduce
================

The essential commands, from a tree with the series applied and no
.config. virtme-ng adds what it needs to boot, including
CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y for addr2line.

  # Build
  vng --build --configitem CONFIG_KCOV=y \
      --configitem CONFIG_KCOV_INSTRUMENT_ALL=y \
      --configitem CONFIG_EARLY_KCOV=y \
      --configitem CONFIG_KCOV_INSTRUMENT_NOISY=y \
      --configitem CONFIG_DEBUG_FS=y --configitem CONFIG_NUMA=y

  # Boot with 2 NUMA nodes, save the coverage in the current directory
  # and release the kernel buffer. --rw gives the guest write access to
  # the host file system, which is how the file reaches the host.
  vng -r arch/x86/boot/bzImage --rw --user root --cpus 4 --memory 2G \
      --numa 1G,cpus=0-1 --numa 1G,cpus=2-3 \
      --exec 'dmesg | grep kcov:;
              cat /sys/kernel/debug/early_kcov > early_kcov.raw;
              echo 1 > /sys/kernel/debug/early_kcov'

  # Resolve the distinct PCs
  od -An -v -t x8 -w8 -j8 early_kcov.raw | awk '{ print "0x" $1 }' |
      sort -u | addr2line -f -e vmlinux

The helper script I used to build, boot, measure and decode (it
produced the tables above with "measure" and "decode --stats") is not
part of the series. It is available at:

  https://gist.github.com/yskzalloc/44d2e20b48f4971fd4ab91756369b526

Testing
=======

  - Booted with and without NUMA, 1G to 8G of RAM, with buffers from
    8M to 512M, with early_kcov_size=0 (disabled) and with
    early_kcov_size=64G (rejected as out of range, "Malformed early
    option", default kept).
  - The last recorded PC is in rest_init() (init/main.c:745), and
    nothing from kernel_init() or the initcalls shows up.
  - After writing to /sys/kernel/debug/early_kcov the file is gone, the
    log shows "Freeing early boot coverage memory: 235932K", and a read
    on a file descriptor opened before the release fails.
  - Normal KCOV from userspace still works after boot with the noisy
    objects instrumented, and no warnings show up in the log.
  - Builds with CONFIG_EARLY_KCOV=n and with CONFIG_KCOV=n. The changed
    C files build without warnings at W=1, and checkpatch is clean
    apart from the missing Signed-off-by in my local tree. I could not
    build the HTML documentation (no Sphinx here).

Limitations and planned extensions
==================================

Collection currently covers parse_early_param() to rest_init(). Two
extensions would make it much more useful:

1. Start at the top of start_kernel()

   setup_arch() runs before collection starts and is not covered,
   though it holds the most firmware-facing code. On x86 it calls
   e820__memory_setup(), parse_early_param() (so the generic call in
   start_kernel() does nothing there), e820__memblock_setup(),
   acpi_boot_table_init(), early_acpi_boot_init(), initmem_init(),
   which does NUMA discovery, and acpi_boot_init(). For the fuzzing use
   case, this is the code that matters most.

   memblock is not usable until e820__memblock_setup(), so starting
   earlier needs statically allocated __initdata storage, and a full
   trace does not fit in any reasonable static buffer. Collection also
   needs current and the per-CPU preempt count to be valid. That holds
   at the top of start_kernel() on x86, but has to be checked on other
   architectures. Code that runs before start_kernel() (head64,
   startup) is already built with KCOV_INSTRUMENT := n.

2. Continue past rest_init()

   After rest_init(), pid 1 runs kernel_init() -> kernel_init_freeable():
   workqueue_init(), do_pre_smp_initcalls(), smp_init(),
   sched_init_smp(), page_alloc_init_late(), then do_basic_setup() ->
   driver_init() and do_initcalls(), until free_initmem() and
   run_init_process(). Driver probing and most device/firmware input
   handling happen here.

   pid 1 is created by kernel_clone() in rest_init(), and
   kcov_task_init() resets the child's KCOV state, so the buffer would
   have to be handed over to pid 1 explicitly. Work done in other
   threads (kthreadd, kworkers for async probing, the deferred page
   init threads, per-CPU threads) is not covered by per-task KCOV.
   That would need remote coverage (kcov_remote_start()) at the
   relevant points, or a boot-only global mode. I would like advice on
   which is acceptable.

Other limits:

  - The data can only be read after a successful boot. If boot fails,
    it is only reachable from a crash dump or a debugger.
  - Interrupt entry code that runs before the hardirq count is raised
    (for example the x86 FRED system vector wrappers) is recorded as
    if init_task executed it, and such a PC can occasionally be
    overwritten by the interrupted task. This is the existing KCOV
    behaviour and is described in kcov.rst.
  - Functions marked noinstr are never covered.
  - Only x86_64 has been tested.

Changes since the previous internal version
===========================================

  - Added CONFIG_EARLY_KCOV; the feature was on for every KCOV kernel.
  - Dropped the "unique" mode that recorded each PC only once. It
    needed a bitmap check in __sanitizer_cov_trace_pc() and loses the
    execution order and repetition that a trace gives, which GCOV
    already covers for "what ran". __sanitizer_cov_trace_pc() is now
    unchanged.
  - CONFIG_KCOV_INSTRUMENT_NOISY now depends on CONFIG_EARLY_KCOV.
  - Writing to /sys/kernel/debug/early_kcov releases the buffer.
  - early_kcov_size= is range checked before narrowing (32-bit),
    early_kcov_stop() is __init, the documentation describes the
    covered window accurately, and kcov.rst documents the interface.

Questions
=========

1. Is early boot coverage something kernel/kcov.c should carry, or is
   it better kept out of tree?

2. Is a write to the debugfs file an acceptable way to release the
   buffer, or would you prefer a different interface?

3. Is the kcov-noisy helper the right way to opt in, or would you
   prefer a different mechanism? The subsystem maintainers of the
   converted Makefiles would be Cc'ed on a non-RFC posting.

4. Is there interest in the two extensions above, in particular for
   fuzzing boot-time input?

Signed-off-by: Yunseong Kim <yunseong.kim@est.tech>
---
Yunseong Kim (2):
      kcov: collect early boot coverage of init_task
      x86/kcov: allow instrumenting noisy objects for early boot coverage

 Documentation/admin-guide/kernel-parameters.txt |  17 +++
 Documentation/dev-tools/kcov.rst                |  60 ++++++++
 arch/x86/kernel/Makefile                        |  12 +-
 arch/x86/kernel/apic/Makefile                   |   2 +-
 arch/x86/lib/Makefile                           |   2 +-
 include/linux/kcov.h                            |  13 ++
 init/main.c                                     |   4 +
 kernel/Makefile                                 |   6 +-
 kernel/kcov.c                                   | 184 ++++++++++++++++++++++++
 kernel/locking/Makefile                         |   2 +-
 kernel/module/Makefile                          |   2 +-
 kernel/rcu/Makefile                             |   2 +-
 kernel/sched/Makefile                           |   2 +-
 lib/Kconfig.debug                               |  45 ++++++
 lib/Makefile                                    |  14 +-
 mm/Makefile                                     |  20 +--
 scripts/Kbuild.include                          |   6 +
 17 files changed, 361 insertions(+), 32 deletions(-)
---
base-commit: d135ab453a7400a34930bdda1ef4bdc8acfac80e
change-id: 20261027-early-kcov-0f5f370b4bed

Best regards,
--  
Yunseong Kim <yunseong.kim@est.tech>



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 1/2] kcov: collect early boot coverage of init_task
  2026-10-27 16:34 [PATCH RFC 0/2] kcov: collect early boot coverage of init_task Yunseong Kim
@ 2026-10-27 16:34 ` Yunseong Kim
  2026-10-28 16:35   ` sashiko-bot
  2026-10-27 16:34 ` [PATCH RFC 2/2] x86/kcov: allow instrumenting noisy objects for early boot coverage Yunseong Kim
  1 sibling, 1 reply; 5+ messages in thread
From: Yunseong Kim @ 2026-10-27 16:34 UTC (permalink / raw)
  To: Jonathan Corbet, Shuah Khan, James Morris, Randy Dunlap,
	Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov,
	Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long,
	Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, Paul E. McKenney, Frederic Weisbecker,
	Neeraj Upadhyay, Joel Fernandes, Josh Triplett, Uladzislau Rezki,
	Steven Rostedt, Mathieu Desnoyers, Lai Jiangshan, Zqiang,
	Juri Lelli, Vincent Guittot, Dietmar Eggemann, Ben Segall,
	Mel Gorman, Valentin Schneider, K Prateek Nayak, Andrew Morton,
	Nathan Chancellor, Nicolas Schier, Thomas Gleixner,
	Borislav Petkov, Dave Hansen, x86, H. Peter Anvin
  Cc: linux-doc, linux-kernel, kasan-dev, workflows, linux-modules, rcu,
	linux-mm, linux-kbuild, Yunseong Kim

KCOV only collects coverage once a task has opened
/sys/kernel/debug/kcov, so everything that runs while the kernel boots
is invisible to it. The boot path is hard to follow from the source:
what start_kernel() really executes depends on the configuration, the
architecture, the firmware and the command line, and much of it is in
inlined helpers. Early ftrace does not fill the gap: function tracing
only starts in early_trace_init(), works at function granularity, and
cannot see inlined code or the many objects built without ftrace
hooks.

Add CONFIG_EARLY_KCOV. With it, every PC executed by init_task from
right after early parameter parsing in start_kernel() (that is, after
setup_arch()) until the start of rest_init() is recorded with
KCOV_TRACE_PC semantics:

 - early_kcov_init() allocates a memblock buffer, attaches it to
   init_task and enables KCOV_MODE_TRACE_PC. The size defaults to
   CONFIG_EARLY_KCOV_SIZE_MB (256) and can be changed with
   early_kcov_size=; 0 disables the collection.
 - early_kcov_stop() disables collection at the start of rest_init().
 - A late_initcall frees the unused tail of the buffer and exposes the
   rest as /sys/kernel/debug/early_kcov, in the usual KCOV layout
   (word 0 is the number of PCs, the PCs have the KASLR offset
   removed). This is done there because memblock memory can only be
   returned once deferred struct page init has completed. If the file
   cannot be created, the buffer is freed right away.
 - Writing to the file removes it and frees the buffer. debugfs does
   not support unlink, so "rm" is not possible; the removal is done
   from a work item because debugfs_remove() waits for the write to
   return.

__sanitizer_cov_trace_pc() is not changed, so KCOV users that do not
enable CONFIG_EARLY_KCOV are not affected.

Many objects are excluded from KCOV because their coverage is noise
for syscall fuzzing: the scheduler, RCU, locking, the page and slab
allocators, parts of lib/. For boot coverage they matter; without them
sched_init() and build_all_zonelists() do not show up at all. Add
CONFIG_KCOV_INSTRUMENT_NOISY, which depends on CONFIG_EARLY_KCOV, and a
kcov-noisy kbuild helper, and use the helper for those generic
objects. With the option off the build is unchanged. Objects excluded
because instrumenting them is unsafe are left alone.

Boot loops run the same blocks millions of times, so a full boot
records a lot of PCs: on a 2G two-node x86_64 guest, 16.9M PCs
(129 MiB) with KCOV_INSTRUMENT_ALL and 31.7M PCs (242 MiB) with
KCOV_INSTRUMENT_NOISY as well, for about 11k distinct PCs. The volume
grows with the amount of memory.

Document early_kcov_size= and the debugfs interface in
kernel-parameters.txt and Documentation/dev-tools/kcov.rst.

Signed-off-by: Yunseong Kim <yunseong.kim@est.tech>
---
 Documentation/admin-guide/kernel-parameters.txt |  17 +++
 Documentation/dev-tools/kcov.rst                |  60 ++++++++
 include/linux/kcov.h                            |  13 ++
 init/main.c                                     |   4 +
 kernel/Makefile                                 |   6 +-
 kernel/kcov.c                                   | 184 ++++++++++++++++++++++++
 kernel/locking/Makefile                         |   2 +-
 kernel/module/Makefile                          |   2 +-
 kernel/rcu/Makefile                             |   2 +-
 kernel/sched/Makefile                           |   2 +-
 lib/Kconfig.debug                               |  45 ++++++
 lib/Makefile                                    |  14 +-
 mm/Makefile                                     |  20 +--
 scripts/Kbuild.include                          |   6 +
 14 files changed, 353 insertions(+), 24 deletions(-)

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 2e8ee12e06de1..fb1f2def7eb3b 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -1404,6 +1404,23 @@ Kernel parameters
 			is useful for tracking down temporary early mappings
 			which are not unmapped.
 
+	early_kcov_size=nn[KMG]
+			[KNL,EARLY] When CONFIG_EARLY_KCOV is enabled, set the
+			size of the buffer that records the code coverage
+			(KCOV_TRACE_PC) of the boot task from right after
+			early parameter parsing in start_kernel() (that is,
+			after setup_arch()) until rest_init(). The size is
+			rounded up to a page and must be less than 2^32
+			words and fit in the address space; larger values
+			are rejected and the default is kept. 0 disables
+			early boot coverage.
+			PCs that do not fit in the buffer are not recorded.
+			The coverage is read from /sys/kernel/debug/early_kcov,
+			see Documentation/dev-tools/kcov.rst. The unused part
+			of the buffer is freed at the end of boot, the rest
+			when the file is written to.
+			Default: CONFIG_EARLY_KCOV_SIZE_MB
+
 	earlycon=	[KNL,EARLY] Output early console device and options.
 
 			When used with no options, the early console is
diff --git a/Documentation/dev-tools/kcov.rst b/Documentation/dev-tools/kcov.rst
index 1a739290c8ecc..8d449084ecd8d 100644
--- a/Documentation/dev-tools/kcov.rst
+++ b/Documentation/dev-tools/kcov.rst
@@ -383,3 +383,63 @@ local tasks spawned by the process and the global task that handles USB bus #1:
 		perror("close"), exit(1);
 	return 0;
     }
+
+Early boot coverage
+-------------------
+
+KCOV can also record the code executed while the kernel boots, before any
+task can open the ``kcov`` file. To enable it, configure the kernel with::
+
+        CONFIG_KCOV=y
+        CONFIG_KCOV_INSTRUMENT_ALL=y
+        CONFIG_EARLY_KCOV=y
+
+Every PC executed by the boot task (``init_task``) from right after early
+parameter parsing in ``start_kernel()`` until the start of ``rest_init()`` is
+recorded with the same semantics as ``KCOV_TRACE_PC``. ``setup_arch()`` runs
+before that point and is not covered. Coverage ends before the initcalls run.
+
+The buffer is allocated from memblock. Its size defaults to
+``CONFIG_EARLY_KCOV_SIZE_MB`` and can be changed with the ``early_kcov_size=``
+boot parameter, where ``early_kcov_size=0`` disables the collection. Boot loops
+execute the same blocks millions of times, so a full x86_64 boot records tens of
+millions of PCs. If the buffer fills up, the remaining PCs are not recorded and
+the kernel log says so::
+
+        kcov: Early boot coverage stopped. Recorded 2097151 / 2097151 PCs (buffer full, coverage truncated; increase early_kcov_size=)
+
+After boot, the coverage can be read from ``/sys/kernel/debug/early_kcov``. It
+has the same layout as the buffer of the ``kcov`` file: the first word (an
+``unsigned long``) is the number of recorded PCs, followed by the PCs in
+execution order. No ``ioctl()`` or ``mmap()`` is needed. As with the ``kcov``
+file, the KASLR offset is subtracted from the PCs, so they must be resolved
+against ``vmlinux``, not ``/proc/kallsyms``. On a 64-bit kernel::
+
+        cat /sys/kernel/debug/early_kcov > early_kcov.raw
+        od -An -v -t x8 -w8 -j8 early_kcov.raw | awk '{ print "0x" $1 }' |
+                sort -u | addr2line -f -e vmlinux
+
+The unused part of the buffer is freed at the end of boot. The rest stays
+allocated until the file is written to: writing anything to it removes the file
+and frees the buffer::
+
+        echo 1 > /sys/kernel/debug/early_kcov
+
+Like the per-task coverage, early boot coverage skips interrupt context. Some
+interrupt entry code runs before the hardirq count is raised, for example the
+x86 FRED system vector wrappers, and is recorded as if ``init_task`` executed it.
+If such an interrupt arrives in the middle of recording a PC, one of the two
+PCs can be lost.
+
+Many objects are built without KCOV instrumentation because their coverage is
+not a function of syscall inputs: the scheduler, RCU, locking, the page and slab
+allocators and parts of ``lib/``. They run during boot too. To include them in
+the early boot coverage, set::
+
+        CONFIG_KCOV_INSTRUMENT_NOISY=y
+
+which instruments every object whose Makefile excludes it from KCOV with
+``KCOV_INSTRUMENT_foo.o := $(kcov-noisy)`` (or ``KCOV_INSTRUMENT :=
+$(kcov-noisy)`` for a whole directory). Objects that cannot be instrumented
+safely keep ``KCOV_INSTRUMENT := n``. As this option degrades the coverage
+signal for syscall fuzzing, it depends on ``CONFIG_EARLY_KCOV``.
diff --git a/include/linux/kcov.h b/include/linux/kcov.h
index 895b761b2db15..b9b1a2c71df8f 100644
--- a/include/linux/kcov.h
+++ b/include/linux/kcov.h
@@ -107,4 +107,17 @@ static inline void kcov_remote_start_usb_softirq(u64 id) {}
 static inline void kcov_remote_stop_softirq(void) {}
 
 #endif /* CONFIG_KCOV */
+
+#ifdef CONFIG_EARLY_KCOV
+/*
+ * Coverage of init_task from right after early parameter parsing in
+ * start_kernel() until rest_init().
+ */
+void early_kcov_init(void);
+void early_kcov_stop(void);
+#else
+static inline void early_kcov_init(void) {}
+static inline void early_kcov_stop(void) {}
+#endif /* CONFIG_EARLY_KCOV */
+
 #endif /* _LINUX_KCOV_H */
diff --git a/init/main.c b/init/main.c
index 27f4cb392349d..958b4d476de7f 100644
--- a/init/main.c
+++ b/init/main.c
@@ -35,6 +35,7 @@
 #include <linux/percpu.h>
 #include <linux/kmod.h>
 #include <linux/kprobes.h>
+#include <linux/kcov.h>
 #include <linux/kmsan.h>
 #include <linux/ksysfs.h>
 #include <linux/vmalloc.h>
@@ -749,6 +750,7 @@ static noinline void __ref __noreturn rest_init(void)
 	struct task_struct *tsk;
 	int pid;
 
+	early_kcov_stop();
 	rcu_scheduler_starting();
 	/*
 	 * We need to spawn init first so that it obtains pid 1, however
@@ -1084,6 +1086,8 @@ void start_kernel(void)
 	print_kernel_cmdline(saved_command_line);
 	/* parameters may set static keys */
 	parse_early_param();
+	/* Needs early_kcov_size= and must run before memblock is retired */
+	early_kcov_init();
 	after_dashes = parse_args("Booting kernel",
 				  static_command_line, __start___param,
 				  __stop___param - __start___param,
diff --git a/kernel/Makefile b/kernel/Makefile
index 4ff92e24680c5..b9e4f06e02ad0 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -27,14 +27,14 @@ endif
 
 # Prevents flicker of uninteresting __do_softirq()/__local_bh_disable_ip()
 # in coverage traces.
-KCOV_INSTRUMENT_softirq.o := n
+KCOV_INSTRUMENT_softirq.o := $(kcov-noisy)
 # Avoid KCSAN instrumentation in softirq ("No shared variables, all the data
 # are CPU local" => assume no data races), to reduce overhead in interrupts.
 KCSAN_SANITIZE_softirq.o = n
 # These are called from save_stack_trace() on slub debug path,
 # and produce insane amounts of uninteresting coverage.
-KCOV_INSTRUMENT_extable.o := n
-KCOV_INSTRUMENT_stacktrace.o := n
+KCOV_INSTRUMENT_extable.o := $(kcov-noisy)
+KCOV_INSTRUMENT_stacktrace.o := $(kcov-noisy)
 # Don't self-instrument.
 KCOV_INSTRUMENT_kcov.o := n
 # If sanitizers detect any issues in kcov, it may lead to recursion
diff --git a/kernel/kcov.c b/kernel/kcov.c
index 35420f0ac524d..e9f2b5cbb0060 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -25,6 +25,9 @@
 #include <linux/kcov.h>
 #include <linux/refcount.h>
 #include <linux/log2.h>
+#include <linux/kmemleak.h>
+#include <linux/memblock.h>
+#include <linux/workqueue.h>
 #include <asm/setup.h>
 
 #define kcov_debug(fmt, ...) pr_debug("%s: " fmt, __func__, ##__VA_ARGS__)
@@ -1098,6 +1101,187 @@ struct kcov_common_handle_id kcov_common_handle(void)
 }
 EXPORT_SYMBOL(kcov_common_handle);
 
+#ifdef CONFIG_EARLY_KCOV
+/*
+ * Early boot coverage.
+ *
+ * Every PC executed by init_task from right after early parameter parsing in
+ * start_kernel() (that is, after setup_arch()) until the start of rest_init()
+ * is recorded, with the same semantics as KCOV_TRACE_PC, into a buffer
+ * allocated from memblock. The buffer uses the regular kcov layout: area[0] is
+ * the number of recorded PCs, followed by the PCs. It is exported via
+ * /sys/kernel/debug/early_kcov; writing to that file removes it and frees the
+ * buffer.
+ */
+#define EARLY_KCOV_DEFAULT_SIZE	((unsigned long)CONFIG_EARLY_KCOV_SIZE_MB << 20)
+
+/* Requested buffer size in bytes, 0 disables early coverage. */
+static unsigned long early_kcov_size __initdata = EARLY_KCOV_DEFAULT_SIZE;
+/* Buffer that was attached to init_task, kept for the debugfs reader. */
+static unsigned long *early_kcov_area;
+/* Size of early_kcov_area in unsigned longs. */
+static unsigned long early_kcov_nr_entries;
+static struct dentry *early_kcov_dentry;
+
+static int __init early_kcov_size_setup(char *arg)
+{
+	/* task_struct::kcov_size is an unsigned int number of longs. */
+	const u64 max = round_down(min_t(u64, ULONG_MAX,
+					 (u64)UINT_MAX * sizeof(unsigned long)),
+				   PAGE_SIZE);
+	unsigned long long size;
+	char *end;
+
+	if (!arg)
+		return -EINVAL;
+
+	/* Range check before narrowing, memparse() returns a 64-bit value. */
+	size = memparse(arg, &end);
+	if (end == arg || size > max)
+		return -EINVAL;
+
+	early_kcov_size = PAGE_ALIGN(size);
+	return 0;
+}
+early_param("early_kcov_size", early_kcov_size_setup);
+
+void __init early_kcov_init(void)
+{
+	unsigned long *area;
+	unsigned long nr_entries;
+
+	if (!early_kcov_size) {
+		pr_info("Early boot coverage disabled\n");
+		return;
+	}
+
+	area = memblock_alloc(early_kcov_size, PAGE_SIZE);
+	if (!area) {
+		pr_warn("Failed to allocate %lu bytes for early boot coverage\n",
+			early_kcov_size);
+		return;
+	}
+
+	nr_entries = early_kcov_size / sizeof(unsigned long);
+	/* The first word is the number of PCs, see __sanitizer_cov_trace_pc(). */
+	area[0] = 0;
+	early_kcov_area = area;
+	early_kcov_nr_entries = nr_entries;
+
+	init_task.kcov_size = nr_entries;
+	init_task.kcov_area = area;
+	/* See comment in check_kcov_mode(). */
+	barrier();
+	WRITE_ONCE(init_task.kcov_mode, KCOV_MODE_TRACE_PC);
+
+	pr_info("Early boot coverage started (%lu bytes, %lu PCs max)\n",
+		early_kcov_size, nr_entries - 1);
+}
+
+void __init early_kcov_stop(void)
+{
+	unsigned long nr_pcs;
+
+	if (!early_kcov_area)
+		return;
+
+	WRITE_ONCE(init_task.kcov_mode, KCOV_MODE_DISABLED);
+	barrier();
+	init_task.kcov_area = NULL;
+	init_task.kcov_size = 0;
+
+	nr_pcs = READ_ONCE(early_kcov_area[0]);
+	pr_info("Early boot coverage stopped. Recorded %lu / %lu PCs%s\n",
+		nr_pcs, early_kcov_nr_entries - 1,
+		nr_pcs >= early_kcov_nr_entries - 1 ?
+		" (buffer full, coverage truncated; increase early_kcov_size=)" :
+		"");
+}
+
+static ssize_t early_kcov_read(struct file *file, char __user *buf,
+			       size_t count, loff_t *ppos)
+{
+	unsigned long nr_words;
+
+	/* Coverage collection has stopped, the buffer does not change. */
+	nr_words = min(READ_ONCE(early_kcov_area[0]) + 1, early_kcov_nr_entries);
+
+	return simple_read_from_buffer(buf, count, ppos, early_kcov_area,
+				       nr_words * sizeof(unsigned long));
+}
+
+static void early_kcov_release(struct work_struct *work)
+{
+	unsigned long size = early_kcov_nr_entries * sizeof(unsigned long);
+
+	if (!early_kcov_dentry)
+		return;
+
+	/* Waits for readers that are still in early_kcov_read(). */
+	debugfs_remove(early_kcov_dentry);
+	early_kcov_dentry = NULL;
+
+	kmemleak_free_part_phys(__pa(early_kcov_area), size);
+	free_reserved_area(early_kcov_area, (void *)early_kcov_area + size, -1,
+			   "early boot coverage");
+	early_kcov_area = NULL;
+	early_kcov_nr_entries = 0;
+}
+static DECLARE_WORK(early_kcov_release_work, early_kcov_release);
+
+static ssize_t early_kcov_write(struct file *file, const char __user *buf,
+				size_t count, loff_t *ppos)
+{
+	/*
+	 * Removing the file from here would deadlock: debugfs_remove() waits
+	 * for this write to return.
+	 */
+	schedule_work(&early_kcov_release_work);
+	return count;
+}
+
+static const struct file_operations early_kcov_fops = {
+	.read		= early_kcov_read,
+	.write		= early_kcov_write,
+	.llseek		= default_llseek,
+};
+
+static int __init early_kcov_late_init(void)
+{
+	unsigned long size, used;
+
+	if (!early_kcov_area)
+		return 0;
+
+	/*
+	 * Deferred struct page init has completed by now, so memblock memory
+	 * can be handed to the page allocator: drop the unused tail of the
+	 * buffer.
+	 */
+	size = early_kcov_nr_entries * sizeof(unsigned long);
+	used = PAGE_ALIGN((READ_ONCE(early_kcov_area[0]) + 1) *
+			  sizeof(unsigned long));
+	if (used < size) {
+		memblock_free((void *)early_kcov_area + used, size - used);
+		early_kcov_nr_entries = used / sizeof(unsigned long);
+		pr_info("Freed %lu KiB of unused early boot coverage buffer\n",
+			(size - used) >> 10);
+	}
+
+	early_kcov_dentry = debugfs_create_file("early_kcov", 0600, NULL, NULL,
+						&early_kcov_fops);
+	if (IS_ERR(early_kcov_dentry)) {
+		/* Nobody can read the coverage, do not keep it around. */
+		early_kcov_dentry = NULL;
+		memblock_free(early_kcov_area, used);
+		early_kcov_area = NULL;
+		early_kcov_nr_entries = 0;
+	}
+	return 0;
+}
+late_initcall(early_kcov_late_init);
+#endif /* CONFIG_EARLY_KCOV */
+
 #ifdef CONFIG_KCOV_SELFTEST
 static void __init selftest(void)
 {
diff --git a/kernel/locking/Makefile b/kernel/locking/Makefile
index cee1901d4cff8..9041cbbec9aae 100644
--- a/kernel/locking/Makefile
+++ b/kernel/locking/Makefile
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 # Any varying coverage in these files is non-deterministic
 # and is generally not a function of system call inputs.
-KCOV_INSTRUMENT		:= n
+KCOV_INSTRUMENT		:= $(kcov-noisy)
 
 CONTEXT_ANALYSIS_mutex.o := y
 CONTEXT_ANALYSIS_rtmutex_api.o := y
diff --git a/kernel/module/Makefile b/kernel/module/Makefile
index 50ffcc413b545..6d163116d2840 100644
--- a/kernel/module/Makefile
+++ b/kernel/module/Makefile
@@ -5,7 +5,7 @@
 
 # These are called from save_stack_trace() on slub debug path,
 # and produce insane amounts of uninteresting coverage.
-KCOV_INSTRUMENT_main.o := n
+KCOV_INSTRUMENT_main.o := $(kcov-noisy)
 
 obj-y += main.o
 obj-y += strict_rwx.o
diff --git a/kernel/rcu/Makefile b/kernel/rcu/Makefile
index 0cfb009a99b9f..fd23c1dad641b 100644
--- a/kernel/rcu/Makefile
+++ b/kernel/rcu/Makefile
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 # Any varying coverage in these files is non-deterministic
 # and is generally not a function of system call inputs.
-KCOV_INSTRUMENT := n
+KCOV_INSTRUMENT := $(kcov-noisy)
 
 ifeq ($(CONFIG_KCSAN),y)
 KBUILD_CFLAGS += -g -fno-omit-frame-pointer
diff --git a/kernel/sched/Makefile b/kernel/sched/Makefile
index b1f1a367034ff..4b4db5cca9b08 100644
--- a/kernel/sched/Makefile
+++ b/kernel/sched/Makefile
@@ -9,7 +9,7 @@ ccflags-y += $(call cc-disable-warning, unused-but-set-variable)
 
 # These files are disabled because they produce non-interesting flaky coverage
 # that is not a function of syscall inputs. E.g. involuntary context switches.
-KCOV_INSTRUMENT := n
+KCOV_INSTRUMENT := $(kcov-noisy)
 
 # Disable KCSAN to avoid excessive noise and performance degradation. To avoid
 # false positives ensure barriers implied by sched functions are instrumented.
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 6759087154edd..40bf654ec7515 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -2233,6 +2233,51 @@ config KCOV_INSTRUMENT_ALL
 	  filesystem fuzzing with AFL) then you will want to enable coverage
 	  for more specific subsets of files, and should say n here.
 
+config EARLY_KCOV
+	bool "Collect code coverage of the early boot path"
+	depends on KCOV && DEBUG_FS
+	help
+	  Record every PC executed by the boot task (init_task) from right
+	  after early parameter parsing in start_kernel() until rest_init(),
+	  with KCOV_TRACE_PC semantics, and export it via
+	  /sys/kernel/debug/early_kcov. Writing to that file removes it and
+	  frees the buffer. This shows which code a given configuration and
+	  machine actually run during boot. Only instrumented code is
+	  recorded, so this is mostly useful with KCOV_INSTRUMENT_ALL.
+	  See Documentation/dev-tools/kcov.rst.
+
+	  This reserves EARLY_KCOV_SIZE_MB of memory during boot and slows
+	  down the boot. If unsure, say n.
+
+config EARLY_KCOV_SIZE_MB
+	int "Default early boot coverage buffer size in MiB"
+	depends on EARLY_KCOV
+	range 0 2048
+	default 256
+	help
+	  Size of the early boot coverage buffer, rounded up to a page. It
+	  can be overridden with early_kcov_size=. 0 disables the collection
+	  unless early_kcov_size= is given.
+
+	  Each recorded PC takes one word. With KCOV_INSTRUMENT_ALL an x86_64
+	  boot records about 17M PCs (130 MiB), and 30M to 40M PCs (240 to
+	  310 MiB) with KCOV_INSTRUMENT_NOISY, growing with the amount of
+	  memory. PCs that do not fit are not recorded.
+
+config KCOV_INSTRUMENT_NOISY
+	bool "Also instrument code excluded only for noisy coverage"
+	depends on EARLY_KCOV && KCOV_INSTRUMENT_ALL
+	help
+	  Some code (scheduler, RCU, locking, page and slab allocators,
+	  rbtree, stack unwinding, ...) is excluded from KCOV instrumentation
+	  because its coverage is not a function of system call inputs and
+	  only adds noise to coverage-guided fuzzing. Code that is excluded
+	  because it cannot be instrumented safely is not affected.
+
+	  Say y to instrument the noisy code as well, so that the early boot
+	  coverage shows the complete execution path. This degrades the
+	  coverage signal for syscall fuzzing, so say n for fuzzing kernels.
+
 config KCOV_IRQ_AREA_SIZE
 	hex "Size of interrupt coverage collection area in words"
 	depends on KCOV
diff --git a/lib/Makefile b/lib/Makefile
index a2afe954805cb..f81096084d18d 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -13,13 +13,13 @@ endif
 # These files are disabled because they produce lots of non-interesting and/or
 # flaky coverage that is not a function of syscall inputs. For example,
 # rbtree can be global and individual rotations don't correlate with inputs.
-KCOV_INSTRUMENT_string.o := n
-KCOV_INSTRUMENT_rbtree.o := n
-KCOV_INSTRUMENT_list_debug.o := n
-KCOV_INSTRUMENT_debugobjects.o := n
-KCOV_INSTRUMENT_dynamic_debug.o := n
-KCOV_INSTRUMENT_fault-inject.o := n
-KCOV_INSTRUMENT_find_bit.o := n
+KCOV_INSTRUMENT_string.o := $(kcov-noisy)
+KCOV_INSTRUMENT_rbtree.o := $(kcov-noisy)
+KCOV_INSTRUMENT_list_debug.o := $(kcov-noisy)
+KCOV_INSTRUMENT_debugobjects.o := $(kcov-noisy)
+KCOV_INSTRUMENT_dynamic_debug.o := $(kcov-noisy)
+KCOV_INSTRUMENT_fault-inject.o := $(kcov-noisy)
+KCOV_INSTRUMENT_find_bit.o := $(kcov-noisy)
 
 # string.o implements standard library functions like memset/memcpy etc.
 # Use -ffreestanding to ensure that the compiler does not try to "optimize"
diff --git a/mm/Makefile b/mm/Makefile
index 2a3ec53d62eef..d272f2bb4c3db 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -20,16 +20,16 @@ KCSAN_INSTRUMENT_BARRIERS := y
 # These files are disabled because they produce non-interesting and/or
 # flaky coverage that is not a function of syscall inputs. E.g. slab is out of
 # free pages, or a task is migrated between nodes.
-KCOV_INSTRUMENT_slab_common.o := n
-KCOV_INSTRUMENT_slub.o := n
-KCOV_INSTRUMENT_page_alloc.o := n
-KCOV_INSTRUMENT_debug-pagealloc.o := n
-KCOV_INSTRUMENT_kmemleak.o := n
-KCOV_INSTRUMENT_memcontrol.o := n
-KCOV_INSTRUMENT_memcontrol-v1.o := n
-KCOV_INSTRUMENT_mmzone.o := n
-KCOV_INSTRUMENT_vmstat.o := n
-KCOV_INSTRUMENT_failslab.o := n
+KCOV_INSTRUMENT_slab_common.o := $(kcov-noisy)
+KCOV_INSTRUMENT_slub.o := $(kcov-noisy)
+KCOV_INSTRUMENT_page_alloc.o := $(kcov-noisy)
+KCOV_INSTRUMENT_debug-pagealloc.o := $(kcov-noisy)
+KCOV_INSTRUMENT_kmemleak.o := $(kcov-noisy)
+KCOV_INSTRUMENT_memcontrol.o := $(kcov-noisy)
+KCOV_INSTRUMENT_memcontrol-v1.o := $(kcov-noisy)
+KCOV_INSTRUMENT_mmzone.o := $(kcov-noisy)
+KCOV_INSTRUMENT_vmstat.o := $(kcov-noisy)
+KCOV_INSTRUMENT_failslab.o := $(kcov-noisy)
 
 CFLAGS_init-mm.o += -Wno-override-init
 
diff --git a/scripts/Kbuild.include b/scripts/Kbuild.include
index 8c311b997e246..8b1f540e8c3e7 100644
--- a/scripts/Kbuild.include
+++ b/scripts/Kbuild.include
@@ -52,6 +52,12 @@ basetarget = $(basename $(notdir $@))
 # real prerequisites without phony targets
 real-prereqs = $(filter-out $(PHONY), $^)
 
+###
+# KCOV_INSTRUMENT value for code that is excluded from KCOV only because its
+# coverage is noise for syscall fuzzing, see CONFIG_KCOV_INSTRUMENT_NOISY.
+# Usage: KCOV_INSTRUMENT_foo.o := $(kcov-noisy)
+kcov-noisy = $(if $(CONFIG_KCOV_INSTRUMENT_NOISY),,n)
+
 ###
 # Escape single quote for use in echo statements
 escsq = $(subst $(squote),'\$(squote)',$1)

-- 
2.47.3



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH RFC 2/2] x86/kcov: allow instrumenting noisy objects for early boot coverage
  2026-10-27 16:34 [PATCH RFC 0/2] kcov: collect early boot coverage of init_task Yunseong Kim
  2026-10-27 16:34 ` [PATCH RFC 1/2] " Yunseong Kim
@ 2026-10-27 16:34 ` Yunseong Kim
  2026-10-28 16:35   ` sashiko-bot
  1 sibling, 1 reply; 5+ messages in thread
From: Yunseong Kim @ 2026-10-27 16:34 UTC (permalink / raw)
  To: Jonathan Corbet, Shuah Khan, James Morris, Randy Dunlap,
	Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov,
	Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long,
	Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, Paul E. McKenney, Frederic Weisbecker,
	Neeraj Upadhyay, Joel Fernandes, Josh Triplett, Uladzislau Rezki,
	Steven Rostedt, Mathieu Desnoyers, Lai Jiangshan, Zqiang,
	Juri Lelli, Vincent Guittot, Dietmar Eggemann, Ben Segall,
	Mel Gorman, Valentin Schneider, K Prateek Nayak, Andrew Morton,
	Nathan Chancellor, Nicolas Schier, Thomas Gleixner,
	Borislav Petkov, Dave Hansen, x86, H. Peter Anvin
  Cc: linux-doc, linux-kernel, kasan-dev, workflows, linux-modules, rcu,
	linux-mm, linux-kbuild, Yunseong Kim

A few x86 objects are excluded from KCOV only because their coverage
is noise for syscall fuzzing: the APIC code (interrupts land in random
places), delay.o, and the stack trace, dumpstack and unwinder objects.

Use $(kcov-noisy) for them, so that CONFIG_KCOV_INSTRUMENT_NOISY=y
includes them in early boot coverage. With the option off nothing
changes.

The objects excluded because the kernel does not boot or hangs when
they are instrumented (head64.o, cpu/common.o, perf_event.o, tlb.c,
mem_encrypt*.c, pgprot.c) stay excluded, as do machine_kexec_64.o and
physaddr.o, which must not use KCOV during kexec once load_segments()
has invalidated the GS base.

With FRED, the system vector wrappers in apic.c are not noinstr and run
before the hardirq count is raised, so with this change interrupt PCs
such as the LAPIC timer can be recorded as if init_task executed them.
The same already happens for the wrappers in irq.c, smp.c and others.

Signed-off-by: Yunseong Kim <yunseong.kim@est.tech>
---
 arch/x86/kernel/Makefile      | 12 ++++++------
 arch/x86/kernel/apic/Makefile |  2 +-
 arch/x86/lib/Makefile         |  2 +-
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/arch/x86/kernel/Makefile b/arch/x86/kernel/Makefile
index 31f46fd005278..44cfd3781c30a 100644
--- a/arch/x86/kernel/Makefile
+++ b/arch/x86/kernel/Makefile
@@ -37,12 +37,12 @@ KMSAN_SANITIZE_nmi.o					:= n
 KCOV_INSTRUMENT_head$(BITS).o				:= n
 # These are called from save_stack_trace() on debug paths,
 # and produce large amounts of uninteresting coverage.
-KCOV_INSTRUMENT_stacktrace.o				:= n
-KCOV_INSTRUMENT_dumpstack.o				:= n
-KCOV_INSTRUMENT_dumpstack_$(BITS).o			:= n
-KCOV_INSTRUMENT_unwind_orc.o				:= n
-KCOV_INSTRUMENT_unwind_frame.o				:= n
-KCOV_INSTRUMENT_unwind_guess.o				:= n
+KCOV_INSTRUMENT_stacktrace.o				:= $(kcov-noisy)
+KCOV_INSTRUMENT_dumpstack.o				:= $(kcov-noisy)
+KCOV_INSTRUMENT_dumpstack_$(BITS).o			:= $(kcov-noisy)
+KCOV_INSTRUMENT_unwind_orc.o				:= $(kcov-noisy)
+KCOV_INSTRUMENT_unwind_frame.o				:= $(kcov-noisy)
+KCOV_INSTRUMENT_unwind_guess.o				:= $(kcov-noisy)
 
 # Disable KCOV to prevent crashes during kexec: load_segments() invalidates
 # the GS base, which KCOV relies on for per-CPU data.
diff --git a/arch/x86/kernel/apic/Makefile b/arch/x86/kernel/apic/Makefile
index 581db89477f92..146cbd766c924 100644
--- a/arch/x86/kernel/apic/Makefile
+++ b/arch/x86/kernel/apic/Makefile
@@ -5,7 +5,7 @@
 
 # Leads to non-deterministic coverage that is not a function of syscall inputs.
 # In particular, smp_apic_timer_interrupt() is called in random places.
-KCOV_INSTRUMENT		:= n
+KCOV_INSTRUMENT		:= $(kcov-noisy)
 
 obj-$(CONFIG_X86_LOCAL_APIC)	+= apic.o apic_common.o apic_noop.o ipi.o vector.o init.o
 obj-y				+= hw_nmi.o
diff --git a/arch/x86/lib/Makefile b/arch/x86/lib/Makefile
index 2dba7f83ef97c..151ecee2229f3 100644
--- a/arch/x86/lib/Makefile
+++ b/arch/x86/lib/Makefile
@@ -4,7 +4,7 @@
 #
 
 # Produces uninteresting flaky coverage.
-KCOV_INSTRUMENT_delay.o	:= n
+KCOV_INSTRUMENT_delay.o	:= $(kcov-noisy)
 
 # KCSAN uses udelay for introducing watchpoint delay; avoid recursion.
 KCSAN_SANITIZE_delay.o := n

-- 
2.47.3



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH RFC 1/2] kcov: collect early boot coverage of init_task
  2026-10-27 16:34 ` [PATCH RFC 1/2] " Yunseong Kim
@ 2026-10-28 16:35   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-10-28 16:35 UTC (permalink / raw)
  To: Yunseong Kim, Jonathan Corbet, Shuah Khan, James Morris,
	Randy Dunlap, Andrey Konovalov, Alexander Potapenko,
	Dmitry Vyukov, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Boqun Feng, Waiman Long, Luis Chamberlain, Petr Pavlu,
	Daniel Gomez, Sami Tolvanen, Aaron Tomlin, Paul E. McKenney,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes,
	Josh Triplett, Uladzislau Rezki, Steven Rostedt,
	Mathieu Desnoyers, Lai Jiangshan, Zqiang, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Andrew Morton,
	Nathan Chancellor, Nicolas Schier, Thomas Gleixner,
	Borislav Petkov, Dave Hansen, x86, H. Peter Anvin
  Cc: kasan-dev, linux-modules, linux-mm, linux-kbuild

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] kcov: NULL deref in __sanitizer_cov_trace_pc() during selftest()
--

commit 2f2cd36073f62980c1305efe82a4239b11c076f5
Author: Yunseong Kim <yunseong.kim@est.tech>

kcov: collect early boot coverage of init_task

This commit adds CONFIG_EARLY_KCOV to record code coverage of init_task
from right after early parameter parsing in start_kernel() until the start
of rest_init(), and exposes the collected PCs via
/sys/kernel/debug/early_kcov. It also introduces CONFIG_KCOV_INSTRUMENT_NOISY
and a kcov-noisy kbuild helper to instrument core subsystems normally
excluded due to coverage noise.

> diff --git a/kernel/Makefile b/kernel/Makefile
> index 4ff92e24680c5..b9e4f06e02ad0 100644
> --- a/kernel/Makefile
> +++ b/kernel/Makefile
> @@ -27,14 +27,14 @@ endif
>  
>  # Prevents flicker of uninteresting __do_softirq()/__local_bh_disable_ip()
>  # in coverage traces.
> -KCOV_INSTRUMENT_softirq.o := n
> +KCOV_INSTRUMENT_softirq.o := $(kcov-noisy)

[ ... ]

> diff --git a/kernel/locking/Makefile b/kernel/locking/Makefile
> index cee1901d4cff8..9041cbbec9aae 100644
> --- a/kernel/locking/Makefile
> +++ b/kernel/locking/Makefile
> @@ -1,7 +1,7 @@
>  # SPDX-License-Identifier: GPL-2.0
>  # Any varying coverage in these files is non-deterministic
>  # and is generally not a function of system call inputs.
> -KCOV_INSTRUMENT		:= n
> +KCOV_INSTRUMENT		:= $(kcov-noisy)

[ ... ]

> diff --git a/kernel/rcu/Makefile b/kernel/rcu/Makefile
> index 0cfb009a99b9f..fd23c1dad641b 100644
> --- a/kernel/rcu/Makefile
> +++ b/kernel/rcu/Makefile
> @@ -1,7 +1,7 @@
>  # SPDX-License-Identifier: GPL-2.0
>  # Any varying coverage in these files is non-deterministic
>  # and is generally not a function of system call inputs.
> -KCOV_INSTRUMENT := n
> +KCOV_INSTRUMENT := $(kcov-noisy)

[ ... ]

> diff --git a/kernel/sched/Makefile b/kernel/sched/Makefile
> index b1f1a367034ff..4b4db5cca9b08 100644
> --- a/kernel/sched/Makefile
> +++ b/kernel/sched/Makefile
> @@ -9,7 +9,7 @@ ccflags-y += $(call cc-disable-warning, unused-but-set-variable)
>  
>  # These files are disabled because they produce non-interesting flaky coverage
>  # that is not a function of syscall inputs. E.g. involuntary context switches.
> -KCOV_INSTRUMENT := n
> +KCOV_INSTRUMENT := $(kcov-noisy)

[ ... ]

> diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
> index 6759087154edd..40bf654ec7515 100644
> --- a/lib/Kconfig.debug
> +++ b/lib/Kconfig.debug
> @@ -2233,6 +2233,51 @@ config KCOV_INSTRUMENT_ALL
>  	  filesystem fuzzing with AFL) then you will want to enable coverage
>  	  for more specific subsets of files, and should say n here.
>  

[ ... ]

> +config KCOV_INSTRUMENT_NOISY
> +	bool "Also instrument code excluded only for noisy coverage"
> +	depends on EARLY_KCOV && KCOV_INSTRUMENT_ALL
> +	help
> +	  Some code (scheduler, RCU, locking, page and slab allocators,
> +	  rbtree, stack unwinding, ...) is excluded from KCOV instrumentation
> +	  because its coverage is not a function of system call inputs and
> +	  only adds noise to coverage-guided fuzzing. Code that is excluded
> +	  because it cannot be instrumented safely is not affected.
> +
> +	  Say y to instrument the noisy code as well, so that the early boot
> +	  coverage shows the complete execution path. This degrades the
> +	  coverage signal for syscall fuzzing, so say n for fuzzing kernels.

[Severity: High]
Can enabling CONFIG_KCOV_INSTRUMENT_NOISY trigger a NULL pointer dereference
in __sanitizer_cov_trace_pc() during kcov_init() -> selftest(), as well as
record interrupt entry and exit PCs into task coverage buffers?

In kernel/kcov.c, check_kcov_mode() filters out interrupt context using
in_task(), which relies on HARDIRQ_OFFSET being set in preempt_count().
However, several functions in kernel/softirq.c, kernel/sched/core.c,
kernel/locking/lockdep.c, and kernel/rcu/ run on every hardware interrupt
either before HARDIRQ_OFFSET is added or after it is subtracted, while
in_task() is still true:

- In kernel/softirq.c, irq_enter_rcu() and irq_enter() are not marked
  noinstr or __no_sanitize_coverage, so the compiler inserts a call to
  __sanitizer_cov_trace_pc() at function entry before __irq_enter_raw()
  adds HARDIRQ_OFFSET to preempt_count(). Likewise, __irq_exit_rcu()
  (inlined into irq_exit_rcu() and irq_exit()) calls
  preempt_count_sub(HARDIRQ_OFFSET) before finishing execution.

- When CONFIG_DEBUG_PREEMPT=y or CONFIG_TRACE_PREEMPT_TOGGLE=y,
  __irq_enter_raw() calls preempt_count_add(HARDIRQ_OFFSET) in
  kernel/sched/core.c, which is instrumented at entry before
  __preempt_count_add() updates preempt_count().

- On interrupt exit after HARDIRQ_OFFSET is cleared,
  irqentry_exit_to_kernel_mode() / irqentry_exit_cond_resched() can call
  trace_hardirqs_on() -> lockdep_hardirqs_on_prepare() in
  kernel/locking/lockdep.c, rcu_irq_exit_check_preempt() in kernel/rcu/,
  and preempt_schedule_irq() -> __schedule() in kernel/sched/core.c
  (before prepare_task_switch() sets KCOV_IN_CTXSW and after
  kcov_finish_switch() clears it).

When CONFIG_KCOV_SELFTEST=y is also enabled (for example in allyesconfig
builds), kcov_init() calls selftest() at device_initcall time with
current->kcov_area == NULL and current->kcov_size == 0:

kernel/kcov.c:selftest() {
	...
	start = jiffies;
	WRITE_ONCE(current->kcov_mode, KCOV_MODE_TRACE_PC);
	while ((jiffies - start) * MSEC_PER_SEC / HZ < 300)
		;
	WRITE_ONCE(current->kcov_mode, 0);
}

When a timer interrupt fires during that loop, the instrumented interrupt
entry/exit path invokes __sanitizer_cov_trace_pc() while in_task() is true:

kernel/kcov.c:__sanitizer_cov_trace_pc() {
	...
	t = current;
	if (!check_kcov_mode(KCOV_MODE_TRACE_PC, t))
		return;

	area = t->kcov_area;
	/* The first 64-bit word is the number of subsequent PCs. */
	pos = READ_ONCE(area[0]) + 1;
	if (likely(pos < t->kcov_size)) {
	...
}

Because area[0] is read before checking t->kcov_size, dereferencing area
when t->kcov_area is NULL crashes the kernel on boot.

Should these interrupt entry/exit helpers be marked __no_sanitize_coverage
(or noinstr), or should CONFIG_KCOV_SELFTEST depend on
!KCOV_INSTRUMENT_NOISY?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261027-early-kcov-v1-0-6f2f053255ad@est.tech?part=1


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH RFC 2/2] x86/kcov: allow instrumenting noisy objects for early boot coverage
  2026-10-27 16:34 ` [PATCH RFC 2/2] x86/kcov: allow instrumenting noisy objects for early boot coverage Yunseong Kim
@ 2026-10-28 16:35   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-10-28 16:35 UTC (permalink / raw)
  To: Yunseong Kim, Jonathan Corbet, Shuah Khan, James Morris,
	Randy Dunlap, Andrey Konovalov, Alexander Potapenko,
	Dmitry Vyukov, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Boqun Feng, Waiman Long, Luis Chamberlain, Petr Pavlu,
	Daniel Gomez, Sami Tolvanen, Aaron Tomlin, Paul E. McKenney,
	Frederic Weisbecker, Neeraj Upadhyay, Joel Fernandes,
	Josh Triplett, Uladzislau Rezki, Steven Rostedt,
	Mathieu Desnoyers, Lai Jiangshan, Zqiang, Juri Lelli,
	Vincent Guittot, Dietmar Eggemann, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Andrew Morton,
	Nathan Chancellor, Nicolas Schier, Thomas Gleixner,
	Borislav Petkov, Dave Hansen, x86, H. Peter Anvin
  Cc: kasan-dev, linux-modules, linux-mm, linux-kbuild

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] x86/kcov: instrumenting apic/ crashes selftest() on FRED timer entry
--

commit c1145245b36a2aa7f3b165ab0f5b126b2c810470
Author: Yunseong Kim <yunseong.kim@est.tech>

x86/kcov: allow instrumenting noisy objects for early boot coverage

This commit switches several x86 Makefile exclusions from hardcoded n to
$(kcov-noisy) so that CONFIG_KCOV_INSTRUMENT_NOISY=y can collect early boot
coverage from arch/x86/kernel/, arch/x86/kernel/apic/, and arch/x86/lib/.

> diff --git a/arch/x86/kernel/apic/Makefile b/arch/x86/kernel/apic/Makefile
> index 581db89477f92..146cbd766c924 100644
> --- a/arch/x86/kernel/apic/Makefile
> +++ b/arch/x86/kernel/apic/Makefile
> @@ -5,7 +5,7 @@
>  
>  # Leads to non-deterministic coverage that is not a function of syscall inputs.
>  # In particular, smp_apic_timer_interrupt() is called in random places.
> -KCOV_INSTRUMENT		:= n
> +KCOV_INSTRUMENT		:= $(kcov-noisy)

[Severity: High]
Does instrumenting arch/x86/kernel/apic/apic.o cause a NULL pointer
dereference during boot when CONFIG_KCOV_INSTRUMENT_NOISY=y and
CONFIG_KCOV_SELFTEST=y on FRED-enabled x86_64 systems?

In arch/x86/kernel/apic/apic.c, DEFINE_IDTENTRY_SYSVEC(sysvec_apic_timer_interrupt)
expands via arch/x86/include/asm/idtentry.h:

    void fred_##func(struct pt_regs *regs)
    {
        instr_##func (regs);
    }

Unlike the IDT entry wrapper sysvec_apic_timer_interrupt(),
fred_sysvec_apic_timer_interrupt() is not marked noinstr or
__no_sanitize_coverage. When apic.o is instrumented with KCOV,
fred_sysvec_apic_timer_interrupt() and the inlined
instr_sysvec_apic_timer_interrupt() -> run_sysvec_on_irqstack_cond() ->
call_on_irqstack_cond() call into __sanitizer_cov_trace_pc() before
irq_enter_rcu() adds HARDIRQ_OFFSET to preempt_count (and again after
irq_exit_rcu() removes HARDIRQ_OFFSET).

During boot, after rest_init() -> early_kcov_stop() clears
init_task.kcov_area to NULL, device_initcall(kcov_init) invokes selftest()
in kernel/kcov.c:

    start = jiffies;
    WRITE_ONCE(current->kcov_mode, KCOV_MODE_TRACE_PC);
    while ((jiffies - start) * MSEC_PER_SEC / HZ < 300)
        ;
    WRITE_ONCE(current->kcov_mode, 0);

While selftest() spins waiting for timer interrupts to advance jiffies with
current->kcov_area == NULL, the first LAPIC timer interrupt under FRED
enters fred_sysvec_apic_timer_interrupt() while in_task() in
check_kcov_mode() is still true, reaching __sanitizer_cov_trace_pc() in
kernel/kcov.c:

    area = t->kcov_area;
    /* The first 64-bit word is the number of subsequent PCs. */
    pos = READ_ONCE(area[0]) + 1;

and dereferencing NULL.

In addition, during normal task KCOV tracing on FRED systems, if a LAPIC
timer interrupt preempts a traced task inside __sanitizer_cov_trace_pc() or
write_comp_data() between reading area[0] and writing back the updated
count, can the nested KCOV calls in fred_sysvec_apic_timer_interrupt() have
their area[0] updates overwritten when the interrupted context resumes and
stores its stale index?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261027-early-kcov-v1-0-6f2f053255ad@est.tech?part=2


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-10-28 16:35 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-27 16:34 [PATCH RFC 0/2] kcov: collect early boot coverage of init_task Yunseong Kim
2026-10-27 16:34 ` [PATCH RFC 1/2] " Yunseong Kim
2026-10-28 16:35   ` sashiko-bot
2026-10-27 16:34 ` [PATCH RFC 2/2] x86/kcov: allow instrumenting noisy objects for early boot coverage Yunseong Kim
2026-10-28 16:35   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox