Detailed Technical Explanation
1. Context and Bug Manifestation
The bug report indicates a soft lockup on CPU 1 lasting for 112 seconds (watchdog: BUG: soft lockup - CPU#1 stuck for 112s! [syz-executor:5638]).
When the watchdog fired on CPU 1, it sent an NMI to CPU 0 to capture all CPU traces:
- CPU 1 (Task Context): Task
syz-executor:5638 was executing kernel_clone() $\to$ copy_process() $\to$ copy_mm() $\to$ dup_mm() $\to$ dup_mmap() $\to$ copy_page_range() $\to$ copy_pmd_range() $\to$ copy_pte_range(). Within copy_pte_range(), it traversed present page table entries (PTEs) via copy_present_ptes() $\to$ __copy_present_ptes() $\to$ set_ptes() $\to$ page_table_check_ptes_set() $\to$ page_table_check_set().
- CPU 1 (Interrupt/Softirq Context): The task on CPU 1 was interrupted by the APIC timer (
sysvec_apic_timer_interrupt()), which invoked softirqs on interrupt exit (__irq_exit_rcu() $\to$ invoke_softirq() $\to$ __do_softirq() $\to$ run_timer_softirq() $\to$ __run_timer_base() $\to$ __run_timers() $\to$ expire_timers()).
- CPU 0: Task
syz-executor:5627 was in exit_mmap() $\to$ arch_exit_mmap() $\to$ free_ldt_pgtables() $\to$ tlb_finish_mmu() $\to$ flush_tlb_mm_range() $\to$ flush_tlb_multi() $\to$ smp_call_function_many_cond(), waiting in csd_lock_wait() for CPU 1 to complete a synchronous TLB flush cross-call.
2. Root Cause and Mechanism
-
Sanitizer and Tracing Multiplication under Spinlocks:
In mm/memory.c, copy_pte_range() acquires source and destination PTE spinlocks (src_ptl and dst_ptl), which disable preemption. The function iterates through up to 32 PTEs (if (++progress >= 32)) before checking need_resched() || spin_needbreak(...) to drop the spinlocks and invoke cond_resched().
With CONFIG_PAGE_TABLE_CHECK, lockdep, perf lock tracing (CONFIG_LOCKDEP, perf_trace_lock), and KASAN (CONFIG_KASAN) simultaneously enabled:
- For every single mapped PTE,
page_table_check_set() in mm/page_table_check.c performs an rcu_read_lock() and rcu_read_unlock().
- Each
rcu_read_unlock() calls lock_release() $\to$ trace_lock_release() $\to$ perf_trace_lock() $\to$ perf_trace_buf_alloc().
- Allocating and zeroing the perf trace buffer invokes
__asan_memset() and kasan_check_range().
This introduces massive per-PTE overhead under non-preemptible spinlock sections when duplicating large address spaces.
-
Softirq Timer Starvation:
When CPU 1 was interrupted by the timer interrupt, softirqs were serviced. Because CPU 1 had accumulated expired timer ticks, run_timer_softirq() called __run_timers(), which loops in kernel/time/timer.c:
while (time_after_eq(jiffies, base->clk) &&
time_after_eq(jiffies, base->next_expiry))
Softirq execution runs with preemption disabled. Inside expire_timers(), _raw_spin_unlock_irq() temporarily re-enables hard interrupts around timer callback execution, but softirq context is maintained. Because the softlockup watchdog stopper thread (migration/1 / softlockup_fn) requires task preemption to run and update watchdog_touch_ts, it remained starved across the combined runtime of the heavy PTE duplication loop and the subsequent timer expiration processing.
-
CPU 0 Cross-Call Serialization:
While CPU 1 was cycling through timers with local hard interrupts disabled during raw_spin_lock_irq(&base->lock) acquisitions, CPU 0 attempted to execute an SMP function call (flush_tlb_multi()) to flush lazy TLB entries during free_ldt_pgtables(). CPU 0 waited in csd_lock_wait() for CPU 1 to process the interrupt, leading to mutual delay until the watchdog threshold was exceeded.
3. Runtime Consequences in Production Kernels
In a production kernel without debugging sanitizers (CONFIG_PAGE_TABLE_CHECK disabled or dormant, CONFIG_KASAN=n, CONFIG_LOCKDEP=n, lock tracing disabled):
-
rcu_read_lock() / rcu_read_unlock() inside page table modifications are zero-cost compiler barriers.
-
copy_pte_range() processes PTE copies in nanoseconds per entry and reliably yields every 32 PTEs via cond_resched() if a higher-priority task (such as the migration/watchdog thread) is queued.
-
The soft lockup timeout (112 seconds) is purely an artifact of debug sanitizer and tracing amplification; it does not occur in production builds.
-
MemoryCorruption: false — There are no out-of-bounds writes, use-after-free conditions, wild pointers, or corrupted reference counters.
-
InfoLeak: "none" — No uninitialized or sensitive kernel memory is leaked to userspace or external observers.
-
RawDenialOfService: false — In a production kernel build without sanitizers, copy_pte_range() efficiently relinquishes the CPU via cond_resched(), preventing system hangs or watchdog timeouts.
-
IsNetCandidate: false — Networking is not involved in this process duplication and timer execution path.
-
IsKVMCandidate: false — Virtualization / KVM subsystems are not involved.