WARNING: still has locks held in blk_mq_freeze_queue_nomemsave

Status: moderation: reported on 2026/11/12 22:56
Subsystems: kernel
Labels: prio:low
[Documentation on labels]
Reported-by: syzbot+1bfd267e0e2c91c70e38@syzkaller.appspotmail.com
First crash: 10h31m, last: 10h31m
✨ AI-generated root cause summary:

In blk_mq_unfreeze_queue_nomemrestore(), __blk_mq_unfreeze_queue() drops q->mq_freeze_depth to 0, clears q->mq_freeze_owner, and releases q->mq_freeze_lock before blk_unfreeze_release_lock() is called. If a concurrent operation (such as blk_mark_disk_dead() during DM_DEV_REMOVE) initiates a new freeze via __blk_freeze_queue_start() in this window, blk_freeze_set_owner() runs under q->mq_freeze_lock and overwrites q->mq_freeze_queue_dying to true. When the unfreezing thread subsequently executes blk_unfreeze_release_lock(), it checks q->mq_freeze_queue_dying outside the lock, finds it set to true, and skips rwsem_release(&q->q_lockdep_map, RET_IP), leaving q->q_lockdep_map held on the task and triggering a lockdep warning at process exit.

▶ ▼ ✨ Full AI root cause analysis
Based on kernel commit 9d66f26fefac:

Technical Analysis of the Root Cause

Overview and Context

The reported bug is a lockdep warning (WARNING: syz... still has locks held!) triggered at process exit (do_exit() -> debug_check_no_locks_held()), reporting that &q->q_usage_counter(queue) (q->q_lockdep_map) acquired by blk_mq_freeze_queue_nomemsave() at block/blk-mq.c:206 was never released by task syz.8.2692/13204.

The task was exiting from syscall ioctl(fd, DM_TABLE_LOAD, ...) (command 0xc138fd09) which returned -ENXIO (RAX = 0xfffffffffffffffa).

Affected Files, Structs, and Functions

  • Files: block/blk-mq.c, block/blk.h
  • Structs: struct request_queue
  • Fields: q->mq_freeze_lock, q->mq_freeze_depth, q->mq_freeze_owner, q->mq_freeze_queue_dying, q->mq_freeze_disk_dead, q->q_lockdep_map, q->io_lockdep_map
  • Functions: blk_freeze_queue_start(), __blk_freeze_queue_start(), blk_freeze_set_owner(), blk_freeze_acquire_lock(), blk_mq_unfreeze_queue_nomemrestore(), __blk_mq_unfreeze_queue(), blk_unfreeze_release_lock()

Mechanism of the Defect

1. Asymmetric Locking in Queue Freeze / Unfreeze

In block/blk-mq.c, blk_freeze_queue_start() calls __blk_freeze_queue_start() followed by blk_freeze_acquire_lock():

void blk_freeze_queue_start(struct request_queue *q)
{
	if (__blk_freeze_queue_start(q, current))
		blk_freeze_acquire_lock(q);
}

Under q->mq_freeze_lock, blk_freeze_set_owner() checks whether !q->mq_freeze_depth and records state:

	if (!q->mq_freeze_depth) {
		q->mq_freeze_owner = owner;
		q->mq_freeze_owner_depth = 1;
		q->mq_freeze_disk_dead = !q->disk ||
			test_bit(GD_DEAD, &q->disk->state) ||
			!blk_queue_registered(q);
		q->mq_freeze_queue_dying = blk_queue_dying(q);
		return true;
	}

When blk_freeze_acquire_lock(q) runs (block/blk.h):

static inline void blk_freeze_acquire_lock(struct request_queue *q)
{
	if (!q->mq_freeze_disk_dead)
		rwsem_acquire(&q->io_lockdep_map, 0, 1, _RET_IP_);
	if (!q->mq_freeze_queue_dying)
		rwsem_acquire(&q->q_lockdep_map, 0, 1, _RET_IP_);
}

If !blk_queue_registered(q) is true (e.g. on an allocated device queue before add_disk() completes registration, such as during dm_setup_md_queue()) and blk_queue_dying(q) is false, q->mq_freeze_disk_dead is set to true and q->mq_freeze_queue_dying is false. Thus, only q->q_lockdep_map is acquired.

2. Race Condition on Queue Unfreeze

In blk_mq_unfreeze_queue_nomemrestore(q):

void blk_mq_unfreeze_queue_nomemrestore(struct request_queue *q)
{
	if (__blk_mq_unfreeze_queue(q, false))
		blk_unfreeze_release_lock(q);
}

Inside __blk_mq_unfreeze_queue(q, false):

	mutex_lock(&q->mq_freeze_lock);
	...
	q->mq_freeze_depth--;
	...
	unfreeze = blk_unfreeze_check_owner(q);
	mutex_unlock(&q->mq_freeze_lock);

	return unfreeze;

q->mq_freeze_lock is released as soon as q->mq_freeze_depth drops to 0. Outside q->mq_freeze_lock, blk_unfreeze_release_lock(q) is called:

static inline void blk_unfreeze_release_lock(struct request_queue *q)
{
	if (!q->mq_freeze_queue_dying)
		rwsem_release(&q->q_lockdep_map, _RET_IP_);
	if (!q->mq_freeze_disk_dead)
		rwsem_release(&q->io_lockdep_map, _RET_IP_);
}

Because blk_unfreeze_release_lock(q) runs after q->mq_freeze_lock has been unlocked and q->mq_freeze_depth is already 0:

  1. Thread 1 (DM_TABLE_LOAD) unfreezes md->queue. __blk_mq_unfreeze_queue() decrements q->mq_freeze_depth to 0, clears q->mq_freeze_owner, and releases q->mq_freeze_lock.
  2. Before Thread 1 can execute blk_unfreeze_release_lock(q) (e.g. due to preemption under PREEMPT(full) or SMP concurrency), Thread 2 (DM_DEV_REMOVE or DM_REMOVE_ALL) runs concurrently.
  3. Thread 2 removes the device from the hash cell (__hash_remove() -> dm_set_mdptr(hc->md, NULL)), drops _hash_lock, and calls dm_destroy(md) -> __dm_destroy(md, true).
  4. In __dm_destroy(), blk_mark_disk_dead(md->disk) is called (block/genhd.c:689):
    • Sets QUEUE_FLAG_DYING on q.
    • Calls __blk_mark_disk_dead() -> blk_queue_start_drain(q) -> __blk_freeze_queue_start(q, current).
  5. In __blk_freeze_queue_start(), Thread 2 acquires q->mq_freeze_lock. Since q->mq_freeze_depth == 0, blk_freeze_set_owner() executes and updates:
    q->mq_freeze_queue_dying = blk_queue_dying(q); /* now true! */
    
  6. Thread 1 resumes and executes blk_unfreeze_release_lock(q). It reads q->mq_freeze_queue_dying, which is now true.
  7. Because !q->mq_freeze_queue_dying evaluates to false, Thread 1 skips rwsem_release(&q->q_lockdep_map, _RET_IP_).
  8. Thread 1 finishes its operation and proceeds to stage the new table in drivers/md/dm-ioctl.c:table_load(). It finds dm_get_mdptr(md) == NULL (cleared by Thread 2) and returns -ENXIO.
  9. Upon returning toward userspace, Thread 1 receives a signal, aborts the syscall, and calls do_group_exit() -> do_exit(). In debug_check_no_locks_held(), lockdep detects that &q->q_usage_counter(queue) acquired during the earlier freeze remains held, triggering the warning.

Summary of the Invariant Violation

The lockdep release logic in blk_unfreeze_release_lock() relies on mutable queue-global flags (q->mq_freeze_queue_dying and q->mq_freeze_disk_dead) read outside of q->mq_freeze_lock after q->mq_freeze_depth has already been reduced to zero. A concurrent caller starting a new freeze can overwrite these flags before the unfreezing caller releases the lockdep map, resulting in skipped lock releases and leaked lockdep tracking state.

▶ ▼ ✨ AI Jobs (1)
ID Workflow Result Correct Bug Created Started Finished Revision
77fb85dc assessment-security DenialOfService: ❌ Exploitable: ❌ FilesystemTrigger: ❌ FuzzingArtifact: ❌ MemoryCorruption: ❌ NetworkNeedsSetup: ❌ NetworkTrigger: ❌ PeripheralTrigger: ❌ RemoteTrigger: ❌ Unprivileged: ❌ UserNamespace: ❌ VMGuestTrigger: ❌ VMHostTrigger: ❌ ❓ WARNING: still has locks held in blk_mq_freeze_queue_nomemsave 2026/11/12 21:54 2026/11/12 21:54 2026/11/12 22:45 c0087d7f

Sample crash report:
====================================
WARNING: syz.8.2692/13204 still has locks held!
syzkaller #0 Tainted: G             L     
------------------------------------
locks held by syz.8.2692/13204: 1, on CPU#0:
 #0: ffff88802e2441c8 (&q->q_usage_counter(queue)#52){+.+.}-{0:0}, at: blk_mq_freeze_queue_nomemsave+0x15/0x20 block/blk-mq.c:206

stack backtrace:
CPU: 0 UID: 0 PID: 13204 Comm: syz.8.2692 Tainted: G             L      syzkaller #0 PREEMPT(full) 
Tainted: [L]=SOFTLOCKUP
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 11/06/2026
Call Trace:
 <TASK>
 __dump_stack lib/dump_stack.c:94 [inline]
 dump_stack_lvl+0x100/0x190 lib/dump_stack.c:120
 print_held_locks_bug kernel/locking/lockdep.c:6848 [inline]
 debug_check_no_locks_held+0x90/0xa0 kernel/locking/lockdep.c:6856
 do_exit+0x13b4/0x2a00 kernel/exit.c:1002
 do_group_exit+0xd5/0x2a0 kernel/exit.c:1123
 get_signal+0x1f58/0x22e0 kernel/signal.c:3113
 arch_do_signal_or_restart+0x91/0x7e0 arch/x86/kernel/signal.c:337
 __exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
 exit_to_user_mode_loop+0x139/0x700 kernel/entry/common.c:101
 __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
 syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
 syscall_exit_to_user_mode include/linux/entry-common.h:343 [inline]
 do_syscall_64+0x661/0x7b0 arch/x86/entry/syscall_64.c:107
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f35e7fa94d9
Code: Unable to access opcode bytes at 0x7f35e7fa94af.
RSP: 002b:00007f35e8e4f028 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: fffffffffffffffa RBX: 00007f35e8225fa0 RCX: 00007f35e7fa94d9
RDX: 00002000000005c0 RSI: 00000000c138fd09 RDI: 0000000000000003
RBP: 00007f35e803804e R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f35e8226038 R14: 00007f35e8225fa0 R15: 00007ffe45fe85e8
 </TASK>

Crashes (1):
Time Kernel Commit Syzkaller Config Log Report Syz repro C repro VM info Assets (help?) Manager Title
2026/11/12 13:19 upstream 9d66f26fefac d6166547 .config console log report info [disk image] [vmlinux] [kernel image] ci-upstream-kasan-badwrites-root WARNING: still has locks held in blk_mq_freeze_queue_nomemsave
* Struck through repros no longer work on HEAD.