Skip to contentSkip to conversation

loop: Fix teardown

PATCH Superseded Bart Van Assche 3 participants 7 messages Sep 16 – Sep 19

Subsystems (3)

v1 of 2 Superseded Latest is v2 →

lore
Share & tools
lore
#0 lore raw

cover loop: Fix teardown

Hi Jens,

My attempts to help Tetsuo with fixing the issues in his loop driver patch series have not been successful so far. Hence this patch series. (See also
https://lore.kernel.org/linux-block/[email protected]/).

When tearing down an autoclear loop device upon release, the loop driver must drain in-flight I/O and flush workqueues to prevent NULL pointer dereferences in lo_rw_aio() and related I/O paths.

However, the .release() block device callback is invoked while holding disk->open_mutex. Freezing the request queue or draining workqueues under disk->open_mutex causes lock inversion and circular locking dependencies (e.g., when worker threads or I/O completion paths also acquire open_mutex or interact with request queue synchronization).

This patch series resolves the lock inversion by:
1. Adding a .post_release() block device operation that is called synchronously from bdev_release() immediately after disk->open_mutex is released.
2. Migrating __loop_clr_fd() to .post_release(), allowing loop device teardown and queue freezing to happen outside of disk->open_mutex.

Please consider applying this patch series.

Thanks,

Bart.

Bart Van Assche (2):
block: Add post_release() operation
loop: Perform __loop_clr_fd() after disk->open_mutex is dropped

 block/bdev.c                     |  3 ++
 drivers/block/loop.c             | 51 +++++++++++++++++++++++---------
 include/linux/blkdev.h           |  5 ++++
 rust/kernel/block/mq/gen_disk.rs |  1 +
 4 files changed, 46 insertions(+), 14 deletions(-)
#1 lore raw

PATCH 1/2 block: Add post_release() operation

Add post_release() block device operation which provides a hook for performing synchronous cleanup without disk->open_mutex held. This is needed by block drivers (such as the loop device) that must drain in-flight I/O and perform synchronization after disk->open_mutex has been released.

---
 block/bdev.c                     | 3 +++
 include/linux/blkdev.h           | 5 +++++
 rust/kernel/block/mq/gen_disk.rs | 1 +
 3 files changed, 9 insertions(+)
block/bdev.c +3 -0
index fac74319e9fb..350f3c29d682 100644
@@ -1189,6 +1189,9 @@ void bdev_release(struct file *bdev_file)
 		blkdev_put_whole(bdev);
 	mutex_unlock(&disk->open_mutex);
 
+	if (disk->fops->post_release)
+		disk->fops->post_release(disk);
+
 	module_put(disk->fops->owner);
 put_no_open:
 	blkdev_put_no_open(bdev);
include/linux/blkdev.h +5 -0
index d003a9d2d1f6..b35f8af1243e 100644
@@ -1580,6 +1580,11 @@ struct block_device_operations {
 			unsigned int flags);
 	int (*open)(struct gendisk *disk, blk_mode_t mode);
 	void (*release)(struct gendisk *disk);
+	/*
+	 * Called after disk->open_mutex is released in the bdev_release() path,
+	 * after .release(). Implementations may sleep.
+	 */
+	void (*post_release)(struct gendisk *disk);
 	int (*ioctl)(struct block_device *bdev, blk_mode_t mode,
 			unsigned cmd, unsigned long arg);
 	int (*compat_ioctl)(struct block_device *bdev, blk_mode_t mode,
rust/kernel/block/mq/gen_disk.rs +1 -0
index fc97dd873974..2ff77ef49781 100644
@@ -129,6 +129,7 @@ pub fn build<T: Operations>(
             submit_bio: None,
             open: None,
             release: None,
+            post_release: None,
             ioctl: None,
             compat_ioctl: None,
             check_events: None,
#2 lore raw

PATCH 2/2 loop: Perform __loop_clr_fd() after disk->open_mutex is dropped

In reply to Bart Van Assche

In order to prevent NULL pointer dereferences in lo_rw_aio() when tearing down a loop device, outstanding I/O must be flushed before clearing the backing file and device state. However, calling blk_mq_wait_quiesce_done(), drain_workqueue(), or blk_mq_freeze_queue() with disk->open_mutex held causes lockdep warnings and potential deadlocks.

Use the .post_release() block device operation to execute __loop_clr_fd() synchronously after disk->open_mutex has been released by the block layer. Inside __loop_clr_fd(), outstanding I/O is flushed and the request queue is frozen before acquiring disk->open_mutex to perform the remaining device teardown and partition rescans.

---
 drivers/block/loop.c | 51 ++++++++++++++++++++++++++++++++------------
 1 file changed, 37 insertions(+), 14 deletions(-)
drivers/block/loop.c +37 -14
index 758c20678bf6..d3e686bebcd3 100644
@@ -1138,11 +1138,33 @@ static int loop_configure(struct loop_device *lo, blk_mode_t mode,
 
 static void __loop_clr_fd(struct loop_device *lo)
 {
+	struct gendisk *disk = lo->lo_disk;
 	struct queue_limits lim;
 	struct file *filp;
 	gfp_t gfp = lo->old_gfp_mask;
+	unsigned int memflags;
 	int err;
 
+	WARN_ON_ONCE(READ_ONCE(lo->lo_state) != Lo_rundown);
+
+	/*
+	 * Wait for ongoing loop_queue_rq() calls. Subsequent loop_queue_rq()
+	 * calls which are made after this call returned will see lo->lo_state
+	 * != Lo_bound and return with BLK_STS_IOERR.
+	 */
+	blk_mq_wait_quiesce_done(&lo->tag_set);
+
+	/* loop_queue_rq() queues work on lo->workqueue, hence drain it. */
+	drain_workqueue(lo->workqueue);
+
+	lim = queue_limits_start_update(lo->lo_queue);
+
+	/*
+	 * Freeze the request queue while updating parameters used while
+	 * processing requests.
+	 */
+	memflags = blk_mq_freeze_queue(lo->lo_queue);
+
 	spin_lock_irq(&lo->lo_lock);
 	filp = lo->lo_backing_file;
 	lo->lo_backing_file = NULL;
@@ -1153,18 +1175,17 @@ static void __loop_clr_fd(struct loop_device *lo)
 	lo->lo_sizelimit = 0;
 	memset(lo->lo_file_name, 0, LO_NAME_SIZE);
 
-	/*
-	 * Reset the block size to the default.
-	 *
-	 * No queue freezing needed because this is called from the final
-	 * ->release call only, so there can't be any outstanding I/O.
-	 */
-	lim = queue_limits_start_update(lo->lo_queue);
+	/* Reset the block size to the default. */
 	lim.logical_block_size = SECTOR_SIZE;
 	lim.physical_block_size = SECTOR_SIZE;
 	lim.io_min = SECTOR_SIZE;
 	queue_limits_commit_update(lo->lo_queue, &lim);
 
+	blk_mq_unfreeze_queue(lo->lo_queue, memflags);
+
+	/* Serialize against concurrent bdev_open() calls. */
+	mutex_lock(&disk->open_mutex);
+
 	invalidate_disk(lo->lo_disk);
 	loop_sysfs_exit(lo);
 	/* let user-space know about this change */
@@ -1178,9 +1199,6 @@ static void __loop_clr_fd(struct loop_device *lo)
 	/*
 	 * Remove all partitions, including partitions added manually with
 	 * BLKPG, which may exist even if LO_FLAGS_PARTSCAN is not set.
-	 *
-	 * open_mutex has been held already in release path, so don't acquire
-	 * it here.
 	 */
 	err = bdev_disk_changed(lo->lo_disk, false);
 	if (err)
@@ -1197,6 +1215,8 @@ static void __loop_clr_fd(struct loop_device *lo)
 	lo->lo_flags = 0;
 	if (!part_shift)
 		set_bit(GD_SUPPRESS_PART_SCAN, &lo->lo_disk->state);
+	mutex_unlock(&disk->open_mutex);
+
 	mutex_lock(&lo->lo_mutex);
 	WRITE_ONCE(lo->lo_state, Lo_unbound);
 	mutex_unlock(&lo->lo_mutex);
@@ -1754,7 +1774,6 @@ static int lo_open(struct gendisk *disk, blk_mode_t mode)
 static void lo_release(struct gendisk *disk)
 {
 	struct loop_device *lo = disk->private_data;
-	bool need_clear = false;
 
 	if (disk_openers(disk) > 0)
 		return;
@@ -1767,11 +1786,14 @@ static void lo_release(struct gendisk *disk)
 	mutex_lock(&lo->lo_mutex);
 	if (lo->lo_state == Lo_bound && (lo->lo_flags & LO_FLAGS_AUTOCLEAR))
 		WRITE_ONCE(lo->lo_state, Lo_rundown);
-
-	need_clear = (lo->lo_state == Lo_rundown);
 	mutex_unlock(&lo->lo_mutex);
+}
+
+static void lo_post_release(struct gendisk *disk)
+{
+	struct loop_device *lo = disk->private_data;
 
-	if (need_clear)
+	if (READ_ONCE(lo->lo_state) == Lo_rundown)
 		__loop_clr_fd(lo);
 }
 
@@ -1791,6 +1813,7 @@ static const struct block_device_operations lo_fops = {
 	.owner =	THIS_MODULE,
 	.open =         lo_open,
 	.release =	lo_release,
+	.post_release = lo_post_release,
 	.ioctl =	lo_ioctl,
 #ifdef CONFIG_COMPAT
 	.compat_ioctl =	lo_compat_ioctl,
Nacked-by on v1 2/2 NAK
#3 lore raw

Re: [PATCH 2/2] loop: Perform __loop_clr_fd() after disk->open_mutex is dropped

Quoted 6 lines · Bart Van Assche

In order to prevent NULL pointer dereferences in lo_rw_aio() when tearing down a loop device, outstanding I/O must be flushed before clearing the backing file and device state. However, calling blk_mq_wait_quiesce_done(), drain_workqueue(), or blk_mq_freeze_queue() with disk->open_mutex held causes lockdep warnings and potential deadlocks.

Quoted 7 lines

Use the .post_release() block device operation to execute __loop_clr_fd() synchronously after disk->open_mutex has been released by the block layer. Inside __loop_clr_fd(), outstanding I/O is flushed and the request queue is frozen before acquiring disk->open_mutex to perform the remaining device teardown and partition rescans.

Please stop stealing my series and stop proposing broken series. Instead of spreading lies claiming that my patch series hasn't been working, please answer my questions.

Your proposed patch introduces a critical concurrency regression that results in kernel crash (NULL-filp dereference).

Since .post_release is called entirely outside of disk->open_mutex, multiple non-final close() threads can concurrently invoke lo_post_release() and race inside __loop_clr_fd() without any serialization. Specifically, Thread 1 can nullify lo->lo_backing_file under lo->lo_lock. A concurrent Thread 2 will then fetch lo_backing_file as NULL, yet immediately attempt to dereference it inside mapping_set_gfp_mask(filp->f_mapping, ...). This triggers an instantaneous NULL pointer dereference (Oops) and panics the kernel.

The "post_release() operation is intended for performing only idempotent actions such as flush_work()" in my series is a requirement. Note that concurrently calling flush_work() from lo_post_release() is safe.

Quoted 120 lines
---
 drivers/block/loop.c | 51 ++++++++++++++++++++++++++++++++------------
 1 file changed, 37 insertions(+), 14 deletions(-)
drivers/block/loop.c +37 -14
index 758c20678bf6..d3e686bebcd3 100644
@@ -1138,11 +1138,33 @@ static int loop_configure(struct loop_device *lo, blk_mode_t mode,
 
 static void __loop_clr_fd(struct loop_device *lo)
 {
+	struct gendisk *disk = lo->lo_disk;
 	struct queue_limits lim;
 	struct file *filp;
 	gfp_t gfp = lo->old_gfp_mask;
+	unsigned int memflags;
 	int err;
 
+	WARN_ON_ONCE(READ_ONCE(lo->lo_state) != Lo_rundown);
+
+	/*
+	 * Wait for ongoing loop_queue_rq() calls. Subsequent loop_queue_rq()
+	 * calls which are made after this call returned will see lo->lo_state
+	 * != Lo_bound and return with BLK_STS_IOERR.
+	 */
+	blk_mq_wait_quiesce_done(&lo->tag_set);
+
+	/* loop_queue_rq() queues work on lo->workqueue, hence drain it. */
+	drain_workqueue(lo->workqueue);
+
+	lim = queue_limits_start_update(lo->lo_queue);
+
+	/*
+	 * Freeze the request queue while updating parameters used while
+	 * processing requests.
+	 */
+	memflags = blk_mq_freeze_queue(lo->lo_queue);
+
 	spin_lock_irq(&lo->lo_lock);
 	filp = lo->lo_backing_file;
 	lo->lo_backing_file = NULL;
@@ -1153,18 +1175,17 @@ static void __loop_clr_fd(struct loop_device *lo)
 	lo->lo_sizelimit = 0;
 	memset(lo->lo_file_name, 0, LO_NAME_SIZE);
 
-	/*
-	 * Reset the block size to the default.
-	 *
-	 * No queue freezing needed because this is called from the final
-	 * ->release call only, so there can't be any outstanding I/O.
-	 */
-	lim = queue_limits_start_update(lo->lo_queue);
+	/* Reset the block size to the default. */
 	lim.logical_block_size = SECTOR_SIZE;
 	lim.physical_block_size = SECTOR_SIZE;
 	lim.io_min = SECTOR_SIZE;
 	queue_limits_commit_update(lo->lo_queue, &lim);
 
+	blk_mq_unfreeze_queue(lo->lo_queue, memflags);
+
+	/* Serialize against concurrent bdev_open() calls. */
+	mutex_lock(&disk->open_mutex);
+
 	invalidate_disk(lo->lo_disk);
 	loop_sysfs_exit(lo);
 	/* let user-space know about this change */
@@ -1178,9 +1199,6 @@ static void __loop_clr_fd(struct loop_device *lo)
 	/*
 	 * Remove all partitions, including partitions added manually with
 	 * BLKPG, which may exist even if LO_FLAGS_PARTSCAN is not set.
-	 *
-	 * open_mutex has been held already in release path, so don't acquire
-	 * it here.
 	 */
 	err = bdev_disk_changed(lo->lo_disk, false);
 	if (err)
@@ -1197,6 +1215,8 @@ static void __loop_clr_fd(struct loop_device *lo)
 	lo->lo_flags = 0;
 	if (!part_shift)
 		set_bit(GD_SUPPRESS_PART_SCAN, &lo->lo_disk->state);
+	mutex_unlock(&disk->open_mutex);
+
 	mutex_lock(&lo->lo_mutex);
 	WRITE_ONCE(lo->lo_state, Lo_unbound);
 	mutex_unlock(&lo->lo_mutex);
@@ -1754,7 +1774,6 @@ static int lo_open(struct gendisk *disk, blk_mode_t mode)
 static void lo_release(struct gendisk *disk)
 {
 	struct loop_device *lo = disk->private_data;
-	bool need_clear = false;
 
 	if (disk_openers(disk) > 0)
 		return;
@@ -1767,11 +1786,14 @@ static void lo_release(struct gendisk *disk)
 	mutex_lock(&lo->lo_mutex);
 	if (lo->lo_state == Lo_bound && (lo->lo_flags & LO_FLAGS_AUTOCLEAR))
 		WRITE_ONCE(lo->lo_state, Lo_rundown);
-
-	need_clear = (lo->lo_state == Lo_rundown);
 	mutex_unlock(&lo->lo_mutex);
+}
+
+static void lo_post_release(struct gendisk *disk)
+{
+	struct loop_device *lo = disk->private_data;
 
-	if (need_clear)
+	if (READ_ONCE(lo->lo_state) == Lo_rundown)
 		__loop_clr_fd(lo);
 }
 
@@ -1791,6 +1813,7 @@ static const struct block_device_operations lo_fops = {
 	.owner =	THIS_MODULE,
 	.open =         lo_open,
 	.release =	lo_release,
+	.post_release = lo_post_release,
 	.ioctl =	lo_ioctl,
 #ifdef CONFIG_COMPAT
 	.compat_ioctl =	lo_compat_ioctl,
#4 lore raw

Re: [PATCH 2/2] loop: Perform __loop_clr_fd() after disk->open_mutex is dropped

Quoted 1 line · Tetsuo Handa

Please stop stealing my series

No, I'm not stealing your work. I credited you in the first patch with a Suggested-by.

Quoted 1 line

and stop proposing broken series.

I think we disagree. My view is that your series is broken. Additionally, I have explained multiple times in detail why I think your series is broken but you chose to ignore my feedback.

Quoted 3 lines

Since .post_release is called entirely outside of disk->open_mutex, multiple non-final close() threads can concurrently invoke lo_post_release() and race inside __loop_clr_fd() without any serialization.

This is wrong. The .release() and .post_release() callbacks are only called once. The following code from fs/file_table.c illustrates this:

void fput(struct file *file)

{
	if (unlikely(file_ref_put(&file->f_ref)))
		__fput_deferred(file);
}

/* the real guts of fput() - releasing the last reference to file */ static void __fput(struct file *file)

{
	[ ... ]
	if (file->f_op->release)
		file->f_op->release(inode, file);
	[ ... ]
}

Thanks,

Bart.

#5 lore raw

Re: [PATCH 2/2] loop: Perform __loop_clr_fd() after disk->open_mutex is dropped

Quoted 13 lines · Bart Van Assche
Quoted 3 lines

Since .post_release is called entirely outside of disk->open_mutex, multiple non-final close() threads can concurrently invoke lo_post_release() and race inside __loop_clr_fd() without any serialization.

This is wrong. The .release() and .post_release() callbacks are only called once. The following code from fs/file_table.c illustrates this:

void fput(struct file *file)
{
    if (unlikely(file_ref_put(&file->f_ref)))
        __fput_deferred(file);
}

Quoted 8 lines

/* the real guts of fput() - releasing the last reference to file */ static void __fput(struct file *file)
{
    [ ... ]
    if (file->f_op->release)
        file->f_op->release(inode, file);
    [ ... ]
}

You are misunderstanding bdev_release() concurrency for the same "struct gendisk" even if __fput() for the same "struct file" does not run concurrently.

Two processes can obtain file descriptor of the same loop device using open() syscall. Two processes can release that file descriptor using close() syscall.

While __fput() is serialized within each thread because it runs under the task work context, lo_post_release() (for the same "struct loop_device" which can be derived from "struct gendisk") is not serialized between two processes.

syzbot Tested-by [email protected] on the v1 series
#6 lore raw
In reply to Bart Van Assche
syzbot ci has tested the following series

syzbot ci has tested the following series

[v1] loop: Fix teardown
https://lore.kernel.org/all/[email protected]
* [PATCH 1/2] block: Add post_release() operation
* [PATCH 2/2] loop: Perform __loop_clr_fd() after disk->open_mutex is dropped

and found the following issue:

general protection fault in lo_post_release

Full report is available here:
https://ci.syzbot.org/series/81bfb244-11b0-4b8a-aa02-a212681ff0af

***

general protection fault in lo_post_release
tree:      axboe
URL:       https://kernel.googlesource.com/pub/scm/linux/kernel/git/axboe/linux.git
base:      2dd9768a37dbb99398c5e332485dbb107ac397c8
arch:      amd64
compiler:  Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8
config:    https://ci.syzbot.org/builds/e21a78bb-e498-4518-8734-d31ae58a2538/config
Oops: general protection fault, probably for non-canonical address 0xdffffc000000000a: 0000 [#1] SMP KASAN PTI
KASAN: null-ptr-deref in range [0x0000000000000050-0x0000000000000057]
CPU: 0 UID: 0 PID: 26297 Comm: udevd Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
RIP: 0010:lo_post_release+0x498/0x7f0
Code: 4c 89 ff e8 7a 73 f9 fb bf e0 01 00 00 49 03 3f be 02 00 00 00 e8 18 05 9e 05 48 8b 44 24 10 4c 8d 60 50 4c 89 e0 48 c1 e8 03 <42> 80 3c 30 00 74 08 4c 89 e7 e8 49 73 f9 fb 41 bf e8 00 00 00 4d
RSP: 0018:ffffc90003d6fb60 EFLAGS: 00010206
RAX: 000000000000000a RBX: ffff8881699b3000 RCX: 0000000000000046
RDX: 0000000000000006 RSI: ffffffff8e414976 RDI: ffffffff8c6da780
RBP: ffffc90003d6fd08 R08: ffffffff907a477f R09: 1ffffffff20f48ef
R10: dffffc0000000000 R11: fffffbfff20f48f0 R12: 0000000000000050
R13: 1ffff920007adf74 R14: dffffc0000000000 R15: ffff888169a02080
FS:  00007fa1afb46c80(0000) GS:ffff88818d6ce000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f0d423a8440 CR3: 00000001bee1a000 CR4: 00000000000006f0
Call Trace:
 <TASK>
 bdev_release+0x59a/0x6b0
 blkdev_release+0x15/0x20
 __fput+0x418/0xa50
 fput_close_sync+0x11f/0x240
 __x64_sys_close+0x7e/0x110
 do_syscall_64+0x166/0x520
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7fa1af7170a8
Code: 48 8b 05 83 9d 0d 00 64 c7 00 16 00 00 00 83 c8 ff 48 83 c4 20 5b c3 64 8b 04 25 18 00 00 00 85 c0 75 20 b8 03 00 00 00 0f 05 <48> 3d 00 f0 ff ff 76 5b 48 8b 15 51 9d 0d 00 f7 d8 64 89 02 48 83
RSP: 002b:00007ffeef68bbb8 EFLAGS: 00000246 ORIG_RAX: 0000000000000003
RAX: ffffffffffffffda RBX: 00007fa1afb46ae0 RCX: 00007fa1af7170a8
RDX: 0000559097caaeb5 RSI: 00007ffeef68b3b8 RDI: 0000000000000008
RBP: 00005595ce9a3e90 R08: 0000000000000006 R09: a8cc15e7d6acaf3b
R10: 000000000000010f R11: 0000000000000246 R12: 0000000000000002
R13: 00005595ce999920 R14: 0000000000000008 R15: 00005595ce964910
 </TASK>
Modules linked in:
---[ end trace 0000000000000000 ]---
RIP: 0010:lo_post_release+0x498/0x7f0
Code: 4c 89 ff e8 7a 73 f9 fb bf e0 01 00 00 49 03 3f be 02 00 00 00 e8 18 05 9e 05 48 8b 44 24 10 4c 8d 60 50 4c 89 e0 48 c1 e8 03 <42> 80 3c 30 00 74 08 4c 89 e7 e8 49 73 f9 fb 41 bf e8 00 00 00 4d
RSP: 0018:ffffc90003d6fb60 EFLAGS: 00010206
RAX: 000000000000000a RBX: ffff8881699b3000 RCX: 0000000000000046
RDX: 0000000000000006 RSI: ffffffff8e414976 RDI: ffffffff8c6da780
RBP: ffffc90003d6fd08 R08: ffffffff907a477f R09: 1ffffffff20f48ef
R10: dffffc0000000000 R11: fffffbfff20f48f0 R12: 0000000000000050
R13: 1ffff920007adf74 R14: dffffc0000000000 R15: ffff888169a02080
FS:  00007fa1afb46c80(0000) GS:ffff88818d6ce000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 000055556cac6a68 CR3: 00000001bee1a000 CR4: 00000000000006f0

***

If these findings have caused you to resend the series or submit a separate fix, please add the following tag to your commit message:

---
This report is generated by a bot. It may contain errors. syzbot ci engineers can be reached at [email protected].

To test a fix for this bug, please reply with #syz test (on a separate line) and attach the patch to the email.

Notes:
- The patch will be applied on top of the tested series (as an incremental fix).
- To test a new version of the whole series, please send it directly to [email protected].
- Arguments like custom git repos and branches are not supported.