BUG: unable to handle kernel paging request in remove_spares

Status: upstream: reported syz repro on 2026/11/05 11:09
Subsystems: raid
Labels: prio:low
[Documentation on labels]
Reported-by: syzbot+a53c2918175b36cfd601@syzkaller.appspotmail.com
First crash: 1d11h, last: 1d10h
✨ AI-generated root cause summary:

In the MD (Multiple Devices / RAID) subsystem, md_start_sync is scheduled as a work item on the md_misc workqueue to initiate resynchronization or manage spare devices. For read-only arrays (!md_is_rdwr(mddev)), md_start_sync executes remove_and_add_spares(), which invokes remove_spares(). However, md_start_sync fails to verify whether the array's personality (mddev->pers) is still valid before calling into personality operations, and array stop/detach sequences do not ensure that mddev->sync_work is completely halted or prevented from running after mddev->pers is cleared. When remove_spares() executes with mddev->pers == NULL, it attempts to invoke mddev->pers->hot_remove_disk (located at offset 0x50), triggering a NULL pointer dereference crash.

▶ ▼ ✨ Full AI root cause analysis
Based on kernel commit 6628e7d4c6b2:

Root Cause Analysis

Involved Components

  • Source File: drivers/md/md.c
  • Functions: md_start_sync(), remove_and_add_spares(), remove_spares(), stop_sync_thread(), do_md_stop(), md_check_recovery()
  • Structures and Fields:
    • struct mddev: sync_work, pers, ro, recovery
    • struct md_personality: hot_remove_disk (at byte offset 0x50 / bit offset 640), hot_add_disk, sync_request

Technical Mechanism and Lifecycle Violation

  1. Scheduling sync_work in Read-Only State: In the MD (Multiple Devices / RAID) subsystem, resynchronization or spare management is handled asynchronously by queuing mddev->sync_work onto md_misc_wq (running md_start_sync()). In md_check_recovery(), when an array is read-only (!md_is_rdwr(mddev), such as mddev->ro == MD_RDONLY), if spare disk reconfiguration is needed (md_spares_need_change(mddev)), it sets MD_RECOVERY_RUNNING and queues mddev->sync_work:

    if (md_spares_need_change(mddev)) {
        set_bit(MD_RECOVERY_RUNNING, &mddev->recovery);
        queue_work(md_misc_wq, &mddev->sync_work);
    }
    

    Unlike the read-write path in md_check_recovery() (which checks !test_bit(MD_RECOVERY_FROZEN, &mddev->recovery)), the read-only branch does not check MD_RECOVERY_FROZEN before queuing sync_work.

  2. Array Teardown / Personality Detach: When an array is stopped or set inactive (e.g., via do_md_stop() through sysfs or ioctl), the array stop sequence invokes stop_sync_thread() and then __md_stop().

    • In stop_sync_thread(), if sync_work has already begun execution (process_one_work() in the worker thread clears WORK_STRUCT_PENDING_BIT), work_pending(&mddev->sync_work) evaluates to false, bypassing flush_work(&mddev->sync_work). Furthermore, if MD_RECOVERY_RUNNING was not set, stop_sync_thread() returns early without flushing.
    • do_md_stop() then calls __md_stop(), which detaches the personality and sets:
      spin_lock(&mddev->lock);
      mddev->pers = NULL;
      spin_unlock(&mddev->lock);
      
    • While do_md_stop() holds reconfig_mutex (mddev_lock), md_start_sync() may be queued or waiting for reconfig_mutex (after calling mddev_suspend()).
  3. Missing pers Validation in md_start_sync(): Once do_md_stop() finishes and releases reconfig_mutex, md_start_sync() acquires mddev_lock_nointr(mddev). It assumes mddev->pers is always valid and does not check if (!mddev->pers) before dispatching operations. Because !md_is_rdwr(mddev) is true, it enters:

    if (!md_is_rdwr(mddev)) {
        remove_and_add_spares(mddev, NULL);
        goto not_running;
    }
    
  4. The Crash Site: remove_and_add_spares() invokes remove_spares(mddev, NULL). When evaluating removable disks:

    rdev_for_each(rdev, mddev) {
        if ((this == NULL || rdev == this) && rdev_removeable(rdev) &&
            !mddev->pers->hot_remove_disk(mddev, rdev)) {
    

    remove_spares() dereferences mddev->pers->hot_remove_disk. Because mddev->pers is NULL, computing the function pointer address adds offset 0x50 (offsetof(struct md_personality, hot_remove_disk)) to NULL. KASAN intercepts the access at shadow address dfff800000000000 + (0x50 >> 3) = dfff80000000000a, triggering a kernel translation fault / NULL pointer dereference oops.

▶ ▼ ✨ AI Jobs (1)
ID Workflow Result Correct Bug Created Started Finished Revision
65515df9 assessment-security DenialOfService: ✅ Exploitable: ❌ FilesystemTrigger: ❌ FuzzingArtifact: ❌ MemoryCorruption: ❌ NetworkNeedsSetup: ❌ NetworkTrigger: ❌ PeripheralTrigger: ❌ RemoteTrigger: ❌ Unprivileged: ❌ UserNamespace: ❌ VMGuestTrigger: ❌ VMHostTrigger: ❌ ❓ BUG: unable to handle kernel paging request in remove_spares 2026/11/04 02:36 2026/11/04 02:37 2026/11/04 03:10 d02c3949
▶ ▼ Discussions (1)
Title Replies (including bot) Last reply
[syzbot] [raid?] BUG: unable to handle kernel paging request in remove_spares 0 (1) 2026/11/05 11:09

Sample crash report:
Unable to handle kernel paging request at virtual address dfff80000000000a
KASAN: null-ptr-deref in range [0x0000000000000050-0x0000000000000057]
Mem abort info:
  ESR = 0x0000000096000005
  EC = 0x25: DABT (current EL), IL = 32 bits
  SET = 0, FnV = 0
  EA = 0, S1PTW = 0
  FSC = 0x05: level 1 translation fault
Data abort info:
  ISV = 0, ISS = 0x00000005, ISS2 = 0x00000000
  CM = 0, WnR = 0, TnD = 0, TagAccess = 0
  GCS = 0, Overlay = 0, DirtyBit = 0
[dfff80000000000a] address between user and kernel address ranges
Internal error: Oops: 0000000096000005 [#1]  SMP
Modules linked in:
CPU: 1 UID: 0 PID: 4993 Comm: kworker/1:5 Not tainted syzkaller #0 PREEMPT 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 09/23/2026
Workqueue: md_misc md_start_sync
pstate: 83400005 (Nzcv daif +PAN -UAO +TCO +DIT -SSBS BTYPE=--)
pc : remove_spares+0x100/0x3c8 drivers/md/md.c:10182
lr : remove_spares+0xe0/0x3c8 drivers/md/md.c:10181
sp : ffff800097277900
x29: ffff8000972779a0 x28: ffff0000f4c1c070 x27: 1fffe0001e98380c
x26: dfff800000000000 x25: ffff700012e4ef24 x24: 0000000000000050
x23: 0000000000000000 x22: ffff0000f4c1d108 x21: 0000000000000000
x20: ffff0000f4c1c058 x19: ffff0000f4c1c060 x18: 00000000ffffffff
x17: ffff80012364b000 x16: ffff80008f920000 x15: ffff80008b586000
x14: 00000000001b18f8 x13: 0000000000000001 x12: 0000000000000000
x11: 0000000000000000 x10: ffff60001e983a48 x9 : 0000000000000000
x8 : 000000000000000a x7 : 0000000000000000 x6 : 0000000000000000
x5 : 0000000000000001 x4 : 0000000000000008 x3 : ffff8000841063ac
x2 : 0000000000000000 x1 : ffff0000c4d55880 x0 : 0000000000000001
Call trace:
 remove_spares+0x100/0x3c8 drivers/md/md.c:10182 (P)
 remove_and_add_spares+0xf4/0x580 drivers/md/md.c:10207
 md_start_sync+0x1f8/0xc80 drivers/md/md.c:10323
 process_one_work kernel/workqueue.c:3495 [inline]
 process_scheduled_works+0x91c/0x1250 kernel/workqueue.c:3578
 worker_thread+0x798/0xbd0 kernel/workqueue.c:3659
 kthread+0x304/0x3d4 kernel/kthread.c:436
 ret_from_fork+0x10/0x20 arch/arm64/kernel/entry.S:854
Code: 972be018 f9400268 91014118 d343ff08 (387a6908) 
---[ end trace 0000000000000000 ]---
----------------
Code disassembly (best guess):
   0:	972be018 	bl	0xfffffffffcaf8060
   4:	f9400268 	ldr	x8, [x19]
   8:	91014118 	add	x24, x8, #0x50
   c:	d343ff08 	lsr	x8, x24, #3
* 10:	387a6908 	ldrb	w8, [x8, x26] <-- trapping instruction

Crashes (2):
Time Kernel Commit Syzkaller Config Log Report Syz repro C repro VM info Assets (help?) Manager Title
2026/11/04 03:01 git://git.kernel.org/pub/scm/linux/kernel/git/arm64/linux.git for-kernelci 6628e7d4c6b2 d02c3949 .config console log report syz / log [disk image] [vmlinux] [kernel image] ci-upstream-gce-arm64 BUG: unable to handle kernel paging request in remove_spares
2026/11/04 02:36 git://git.kernel.org/pub/scm/linux/kernel/git/arm64/linux.git for-kernelci 6628e7d4c6b2 d02c3949 .config console log report info [disk image] [vmlinux] [kernel image] ci-upstream-gce-arm64 BUG: unable to handle kernel paging request in remove_spares
* Struck through repros no longer work on HEAD.