All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v6] loop: Wait for I/O completion in __loop_clr_fd()
@ 2026-10-05 21:06 Bart Van Assche
  2026-10-06 12:01 ` Tetsuo Handa
  2026-10-07 13:08 ` Christoph Hellwig
  0 siblings, 2 replies; 11+ messages in thread
From: Bart Van Assche @ 2026-10-05 21:06 UTC (permalink / raw)
  To: Jens Axboe
  Cc: linux-block, Christoph Hellwig, Tetsuo Handa, Nilay Shroff,
	Edward Adam Davis, Dave Kleikamp, jfs-discussion, Bart Van Assche

From: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>

Syzbot reports a NULL pointer dereference in lo_rw_aio(). This crash
probably happens because commit b15e4310633f ("jfs: Set the lbmDONE flag at
the end of lbmIODone"), merged in the 7.1 merge window, lets the JFS log
code report a write as finished while it is still in flight. JFS then tears
down and releases the loop device with that write still sitting in the loop
workqueue.

Although the root cause is in JFS, improve the loop driver by waiting in
__loop_clr_fd() for ongoing I/O. Release &disk->open_mutex temporarily
because otherwise a deadlock would be triggered.

This patch introduces a behavior change: openers racing with teardown now
get -ENXIO instead of waiting.

Link: https://lkml.kernel.org/r/fbb3edda-f108-4e5b-acf2-266f043f8125@I-love.SAKURA.ne.jp
Link: https://syzkaller.appspot.com/bug?extid=cd8a9a308e879a4e2c28
Link: https://syzkaller.appspot.com/bug?extid=bc273027d5643e48e5b3
Cc: Christoph Hellwig <hch@lst.de>
Cc: Edward Adam Davis <eadavis@qq.com>
Cc: Dave Kleikamp <shaggy@kernel.org>
Cc: jfs-discussion@lists.sourceforge.net
Assisted-by: Gemini Pro
Assisted-by: Claude Opus
Signed-off-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>
[ bvanassche: rewrote description, eliminated .post_release() callback,
  changed a synchronize_rcu() call into quiescing + unquiescing the
  request queue, reordered code in __loop_clr_fd() and rewrote several
  comments ]
Signed-off-by: Bart Van Assche <bvanassche@acm.org>
---

Changes from v5 to v6:
- Drop the invasive core block-layer changes (adding .post_release to
  struct block_device_operations and calling it from block/bdev.c). Keep
  all changes completely self-contained within drivers/block/loop.c.
- Instead of deferring teardown to a core post_release callback, temporarily
  unlock and re-acquire &disk->open_mutex inside __loop_clr_fd().
- Add WARN_ON_ONCE(lo->lo_state != Lo_rundown) checks around the unlock/lock
  window to ensure racing openers get -ENXIO via lo_open() rather than seeing
  a partially torn-down device.
- Replace drain_workqueue() with flush_workqueue() to prevent warnings about
  queueing work on a draining workqueue (__WQ_DRAINING).
- Replace synchronize_rcu() with blk_mq_quiesce_queue() + blk_mq_unquiesce_queue()
  to properly synchronize against in-flight loop_queue_rq() instances using
  standard block-layer primitives.
- Add an extensive comment documenting the exact 3-step circular locking
  scenario caught by syzbot (disk->open_mutex -> wq_completion -> VFS/subsystem
  locks -> disk->open_mutex).
- Identify and document the probable trigger in JFS (commit b15e4310633f,
  "jfs: Set the lbmDONE flag at the end of lbmIODone") in the commit message.
- Add Cc tags for JFS maintainers (Dave Kleikamp, jfs-discussion) and Christoph
  Hellwig. Update AI assistance tags to conform to Linux kernel attribution guidelines.

Changes from v4 to v5:
- Introduce a new .post_release() callback in struct block_device_operations
  called from bdev_release() after dropping disk->open_mutex.
- Invoke __loop_clr_fd() from the new loop_post_release() callback instead of
  calling it synchronously under disk->open_mutex in loop_release().

Changes from v3 to v4:
- Add blk_mq_freeze_queue() / blk_mq_unfreeze_queue() in __loop_clr_fd() to
  wait for pending asynchronous direct I/O (lo_rw_aio returning -EIOCBQUEUED)
  which drain_workqueue() alone does not wait for.

Changes from v2 to v3:
- Address the syzbot circular locking report (bug extid=2f62807dc3239b8f584e)
  caused by calling drain_workqueue() while holding disk->open_mutex.

Changes from v1 to v2:
- Remove the custom SRCU domain (loop_io_srcu).
- Replace srcu_read_lock() / synchronize_srcu() with rcu_read_lock() and
  synchronize_rcu().

 drivers/block/loop.c | 72 ++++++++++++++++++++++++++++++++++++++------
 1 file changed, 63 insertions(+), 9 deletions(-)

diff --git a/drivers/block/loop.c b/drivers/block/loop.c
index 758c20678bf6..94a62b92b5a1 100644
--- a/drivers/block/loop.c
+++ b/drivers/block/loop.c
@@ -1138,11 +1138,65 @@ 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;
 	int err;
 
+	/*
+	 * lo->lo_state must be Lo_rundown before we release disk->open_mutex
+	 * to ensure that lo_open() fails while disk->open_mutex is unlocked.
+	 */
+	WARN_ON_ONCE(lo->lo_state != Lo_rundown);
+
+	/*
+	 * Unlock disk->open_mutex to prevent circular locking. Without the
+	 * mutex_unlock() call below, circular locking would be triggered as
+	 * follows when an autoclearing loop device is closed by its last opener:
+	 * 1. open_mutex → Workqueue Completion / Queue Freeze: Waiting for
+	 *    pending I/O requires draining the loop device's execution
+	 *    workqueue (flush_workqueue(lo->workqueue)) and/or freezing the
+	 *    request queue (blk_mq_freeze_queue()). Draining the workqueue
+	 *    acquires its completion lockdep tracking context
+	 *    ((wq_completion)loopX). If called while disk->open_mutex is held,
+	 *    lockdep records the lock acquisition ordering: disk->open_mutex ⟶
+	 *    (wq_completion)loopX
+	 * 2. Workqueue Completion → Subsystem/VFS Locks: In-flight work items
+	 *    executed by worker threads on lo->workqueue (loop_process_work()→
+	 *    lo_rw_aio()) perform file operations on the underlying backing
+	 *    file. These operations acquire VFS and filesystem locks (such as
+	 *    inode locks, file-specific locks, kernfs mutexes like &of->mutex,
+	 *    or seq_file locks).
+	 * 3. Subsystem/VFS Locks → open_mutex: Other code paths in the kernel
+	 *    (for instance, partition scanners, power management/hibernation
+	 *    resume routines such as resume_store() → swsusp_check(), or sysfs
+	 *    operations) acquire subsystem/filesystem locks first and then call
+	 *    bdev_open(), which acquires disk->open_mutex.
+	 *
+	 * This creates a circular locking dependency: disk->open_mutex ⟶
+	 * (wq_completion)loopX ⟶ VFS / Subsystem Locks ⟶ disk->open_mutex
+	 *
+	 * Syzbot caught this exact circular locking scenario. See also
+	 * https://syzkaller.appspot.com/bug?extid=2f62807dc3239b8f584e.
+	 */
+	mutex_unlock(&disk->open_mutex);
+
+	/* Wait for all outstanding I/O requests to finish. */
+	blk_mq_unfreeze_queue(lo->lo_queue, blk_mq_freeze_queue(lo->lo_queue));
+	/*
+	 * Wait until all ongoing loop_queue_rq() calls have finished. Freezing
+	 * the request queue may finish before loop_queue_rq() has finished.
+	 */
+	blk_mq_quiesce_queue(lo->lo_queue);
+	blk_mq_unquiesce_queue(lo->lo_queue);
+	/* Wait for ongoing loop_handle_cmd() calls to finish. */
+	flush_workqueue(lo->workqueue);
+
+	/* Reacquire disk->open_mutex and clear *lo. */
+	mutex_lock(&disk->open_mutex);
+	WARN_ON_ONCE(lo->lo_state != Lo_rundown);
+
 	spin_lock_irq(&lo->lo_lock);
 	filp = lo->lo_backing_file;
 	lo->lo_backing_file = NULL;
@@ -1153,16 +1207,16 @@ 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.
-	 */
+	/* Reset the block size to the default. */
 	lim = queue_limits_start_update(lo->lo_queue);
 	lim.logical_block_size = SECTOR_SIZE;
 	lim.physical_block_size = SECTOR_SIZE;
 	lim.io_min = SECTOR_SIZE;
+	/*
+	 * No queue freeze around the queue limits update below because any I/O
+	 * that races with this update fails in loop_queue_rq(), so a
+	 * transiently inconsistent view of the limits is harmless.
+	 */
 	queue_limits_commit_update(lo->lo_queue, &lim);
 
 	invalidate_disk(lo->lo_disk);
@@ -1202,9 +1256,9 @@ static void __loop_clr_fd(struct loop_device *lo)
 	mutex_unlock(&lo->lo_mutex);
 
 	/*
-	 * Need not hold lo_mutex to fput backing file. Calling fput holding
-	 * lo_mutex triggers a circular lock dependency possibility warning as
-	 * fput can take open_mutex which is usually taken before lo_mutex.
+	 * fput() calls file->f_op->release() asynchronously. Hence, calling
+	 * fput() while holding disk->open_mutex won't trigger recursive locking
+	 * for disk->open_mutex in case of stacked loop devices.
 	 */
 	fput(filp);
 }

^ permalink raw reply related	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-10-12 11:39 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-05 21:06 [PATCH v6] loop: Wait for I/O completion in __loop_clr_fd() Bart Van Assche
2026-10-06 12:01 ` Tetsuo Handa
2026-10-06 17:15   ` Bart Van Assche
2026-10-07 13:08 ` Christoph Hellwig
2026-10-07 13:35   ` Tetsuo Handa
2026-10-11 10:37     ` [PATCH] loop: Detect write requests that failed to wait for completion Tetsuo Handa
2026-10-11 11:29       ` [PATCH v2] " Tetsuo Handa
2026-10-11 13:26         ` [PATCH v3] " Tetsuo Handa
2026-10-11 19:24           ` [syzbot ci] " syzbot ci
2026-10-12  8:26           ` [PATCH] block: Wait for inflight requests in bdev_release() Tetsuo Handa
2026-10-12 11:39             ` Tetsuo Handa

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.