syzbot
WARNING: lock held when returning to user space in elevator_change
Status:
upstream: reported syz repro on 2026/11/01 00:14
Subsystems:
block
Labels:
prio:low
[Documentation on labels]
Reported-by: syzbot+48ca80a8587c1d97c433@syzkaller.appspotmail.com
First crash: 4d03h, last: 4d03h
✨ AI-generated root cause summary:
A race condition exists between concurrent DM_TABLE_LOAD and DM_DEV_REMOVE operations on a device-mapper mapped device. In __dm_destroy(), blk_mark_disk_dead() is invoked to mark the queue dying and start queue draining before waiting for mapped_device holders to drain, allowing blk_freeze_set_owner() to overwrite q->mq_freeze_queue_dying on the request queue after another thread in table_load() freezes the queue via elevator_change(). Consequently, blk_unfreeze_release_lock() observes q->mq_freeze_queue_dying as true and skips releasing the lockdep map for &q->q_usage_counter, triggering a lockdep warning upon returning to userspace.
▶
▼
✨ Full AI root cause analysis
Root Cause Analysis
1. Overview and Key Actors
- Subsystem & Drivers: Device Mapper (
drivers/md/dm-ioctl.c, drivers/md/dm.c) and Block Layer MQ/Elevator subsystem (block/elevator.c, block/blk-mq.c, block/blk.h).
- Structs & Fields:
struct mapped_device (holders, open_count, flags, disk, queue)
struct request_queue (q_usage_counter, q_lockdep_map, mq_freeze_lock, mq_freeze_depth, mq_freeze_owner, mq_freeze_owner_depth, mq_freeze_disk_dead, mq_freeze_queue_dying)
- Functions Involved:
2. Detailed Technical Mechanism & Trigger Conditions
-
Initial Setup:
A mapped device is created via DM_DEV_CREATE. Userspace then initiates two concurrent/interleaved operations: DM_TABLE_LOAD (Thread A) and DM_DEV_REMOVE (Thread B).
-
Thread A (table_load):
-
Thread B (dev_remove):
-
The Unfreeze Race and Lockdep Desynchronization:
- Back in Thread A (
elevator_change()), line 683 runs:
blk_mq_unfreeze_queue(q, memflags);
- This invokes
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);
}
- In
__blk_mq_unfreeze_queue(q, false):
- It acquires
q->mq_freeze_lock.
q->mq_freeze_depth is decremented from 1 to 0.
blk_unfreeze_check_owner(q) is called: q->mq_freeze_owner is cleared to NULL and q->mq_freeze_owner_depth is decremented to 0.
__blk_mq_unfreeze_queue() returns true.
- Defect 2:
mutex_unlock(&q->mq_freeze_lock) is released before blk_unfreeze_release_lock(q) is called.
- Immediately upon
q->mq_freeze_lock being dropped (and q->mq_freeze_depth == 0), Thread B's __blk_freeze_queue_start() enters blk_freeze_set_owner():
- Seeing
!q->mq_freeze_depth (0), Thread B sets q->mq_freeze_queue_dying = blk_queue_dying(q);, which is now true.
- Now Thread A continues and executes
blk_unfreeze_release_lock(q):
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_);
}
- Thread A evaluates
!q->mq_freeze_queue_dying and finds it false (because Thread B overwrote q->mq_freeze_queue_dying to true).
- Consequently,
rwsem_release(&q->q_lockdep_map, _RET_IP_) is skipped!
- In lockdep's internal bookkeeping,
&q->q_usage_counter(queue) acquired at block/elevator.c:668 remains held by Thread A.
-
Return to Userspace:
- Thread A finishes
dm_setup_md_queue(), drops md_type_lock, and acquires _hash_lock.
- Looking up the hash cell via
hc = dm_get_mdptr(md), it finds hc == NULL because Thread B removed it.
- Thread A logs
"device-mapper: ioctl: device has been removed from the dev hash table.", cleans up the table, drops its reference via dm_put(md), and returns -ENXIO.
- As Thread A exits back to userspace, lockdep detects that
&q->q_usage_counter(queue) was never released, emitting:
WARNING: lock held when returning to user space!.
3. Production Impact Classification
- MemoryCorruption: false
The bug is a lockdep tracking desynchronization race caused by global
request_queue lockdep flags being modified across thread boundaries. In production kernels without debugging sanitizers (CONFIG_LOCKDEP disabled), the rwsem_acquire/rwsem_release calls inside blk_freeze_acquire_lock and blk_unfreeze_release_lock compile to no-ops. Memory allocation, reference counting, and disk deletion proceed without memory corruption or use-after-free.
- InfoLeak: none
No uninitialized data or kernel pointers are exposed to userspace.
- RawDenialOfService: false
In a production kernel without
CONFIG_LOCKDEP, this lockdep warning does not exist. The underlying request queue depth correctly reaches 0, Thread B waits cleanly for md->holders to drain before tearing down the gendisk and request queue via del_gendisk(), and no kernel panic, hang, deadlock, or resource leak occurs.
- IsNetCandidate: false
The bug is entirely within the Device Mapper and block layer elevator/request-queue subsystem.
- IsKVMCandidate: false
No virtualization or KVM subsystems are involved.
Sample crash report:
device-mapper: ioctl: device has been removed from the dev hash table.
================================================
WARNING: lock held when returning to user space!
syzkaller #0 Not tainted
------------------------------------------------
syz.1.593/7980 is leaving the kernel with locks still held!
locks held by syz.1.593/7980: 1, last CPU#1:
#0: ffff888030823878 (&q->q_usage_counter(queue)#53){+.+.}-{0:0}, at: blk_mq_freeze_queue include/linux/blk-mq.h:953 [inline]
#0: ffff888030823878 (&q->q_usage_counter(queue)#53){+.+.}-{0:0}, at: elevator_change+0x194/0x480 block/elevator.c:668
* Struck through repros no longer work on HEAD.