loop: Fix teardown
Subsystems (3)
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(-)
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
@@ -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
@@ -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
@@ -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,
PATCH 2/2 loop: Perform __loop_clr_fd() after disk->open_mutex is dropped
In reply to Bart Van AsscheIn 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
@@ -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,
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, callingblk_mq_wait_quiesce_done(),drain_workqueue(), orblk_mq_freeze_queue()withdisk->open_mutexheld causes lockdep warnings and potential deadlocks.
Quoted 7 lines
Use the .post_release() block device operation to execute
__loop_clr_fd()synchronously afterdisk->open_mutexhas been released by the block layer. Inside__loop_clr_fd(), outstanding I/O is flushed and the request queue is frozen before acquiringdisk->open_mutexto perform the remaining device teardown and partition rescans.Signed-off-by Bart Van Assche <[email protected]>
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@@ -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,
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-finalclose()threads can concurrently invokelo_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.
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-finalclose()threads can concurrently invokelo_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.cillustrates 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 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.