[PATCH 4/4] fuse2fs: try to grab block device O_EXCL repeatedly

Darrick J. Wong
2025-11-06T22:31:05Z

generic/646 keeps failing to mount in this fashion:

run fstests generic/646 at 2025-10-20 17:10:26
[U] FUSE2FS (sda4): mounted filesystem f8f21d10-2ec9-4aef-a509-b32659b4e6b0.
fuse: EXPERIMENTAL iomap feature enabled. Use at your own risk!
[U] FUSE2FS (sda4): shut down requested.
[U] FUSE2FS (sda4): unmounted filesystem f8f21d10-2ec9-4aef-a509-b32659b4e6b0.
[U] FUSE2FS (sda4): mounted filesystem 9efc6297-74c0-448c-b253-cecffd947239.
fuse: EXPERIMENTAL iomap feature enabled. Use at your own risk!
[U] FUSE2FS (sda4): shut down requested.
[U] FUSE2FS (sda4): unmounted filesystem 9efc6297-74c0-448c-b253-cecffd947239.
[U] FUSE2FS (sda4): mounted filesystem 9efc6297-74c0-448c-b253-cecffd947239.
[U] FUSE2FS (sda4): Warning: Mounting unchecked fs, running e2fsck is recommended.
[U] FUSE2FS (sda4): Device or resource busy.
[U] FUSE2FS (sda4): Please run e2fsck -fy.
[U] Mount failed while opening filesystem. Check dmesg(1) for details.
[U] FUSE2FS (sda4): unmounted filesystem 9efc6297-74c0-448c-b253-cecffd947239.

It turns out that one can mount a fuse filesystem and unmount it before
the kernel even has a chance to send FUSE_INIT to the fuse server. If
this occurs, the unmount code will abort the FUSE_INIT request and tear
down the fs mount immediately.

Unfortunately for fstests, the fuse server may have already opened the
block device with O_EXCL and will keep running with the bdev open until
libfuse notices that the connection to the kernel died and tells the
fuse server to destroy itself. That might not happen for a long time
after the unmount program exits, in which case a subsequent invocation
of the fuse server can race with the dying fuse server to open the block
device. When this happens, the new invocation fails with "Device or
resource busy".

This is exactly what's happening in this test, which is only noticeable
because it cycles the scratch mount so quickly.

Cc: <linux-ext4@vger.kernel.org> # v1.43
Fixes: 81cbf1ef4f5dab ("misc: add fuse2fs, a FUSE server for e2fsprogs")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
---
misc/fuse2fs.c | 67 +++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 54 insertions(+), 13 deletions(-)

diff --git a/misc/fuse2fs.c b/misc/fuse2fs.c
index bbd79d6c09f4bc..76872d793ea394 100644
--- a/misc/fuse2fs.c
+++ b/misc/fuse2fs.c
@@ -4821,21 +4821,62 @@ int main(int argc, char *argv[])
sprintf(options, "offset=%lu", fctx.offset);
if (fctx.directio)
flags |= EXT2_FLAG_DIRECT_IO;
- err = ext2fs_open2(fctx.device, options, flags, 0, 0, unix_io_manager,
- &global_fs);
- if ((err == EPERM || err == EACCES) &&
- (!fctx.ro || (flags & EXT2_FLAG_RW))) {
- /*
- * Source device cannot be opened for write. Under these
- * circumstances, mount(8) will try again with a ro mount,
- * and the kernel will open the block device readonly.
- */
- log_printf(&fctx, "%s\n",
- _("WARNING: source write-protected, mounted read-only."));
- flags &= ~EXT2_FLAG_RW;
- fctx.ro = 1;
+
+ /*
+ * If the filesystem is stored on a block device, the _EXCLUSIVE flag
+ * causes libext2fs to try to open the block device with O_EXCL. If
+ * the block device is already opened O_EXCL by something else, the
+ * open call returns EBUSY.
+ *
+ * Unfortunately, there's a nasty race between fuse2fs going through
+ * its startup sequence (open fs, parse superblock, daemonize, create
+ * mount, respond to FUSE_INIT) in response to a mount(8) invocation
+ * and another process that calls umount(2) on the same mount.
+ *
+ * If fuse2fs is being run as a mount(8) helper and has daemonized, the
+ * original fuse2fs subprocess exits and so will mount(8). This can
+ * occur before the kernel issues a FUSE_INIT request to fuse2fs. If
+ * a process then umount(2)'s the mount, the kernel will abort the
+ * fuse connection. If the FUSE_INIT request hasn't been issued, now
+ * it won't ever be issued. The kernel tears down the mount and
+ * returns from umount(2), but fuse2fs has no idea that any of this has
+ * happened because it receives no requests.
+ *
+ * At this point, the original fuse2fs server holds the block device
+ * open O_EXCL. If mount(8) is invoked again on the same device, the
+ * new fuse2fs server will try to open the block device O_EXCL and
+ * fail. A crappy solution here is to retry for 5 seconds, hoping that
+ * the first fuse2fs server will wake up and exit.
+ *
+ * If the filesystem is in a regular file, O_EXCL (without O_CREAT) has
+ * no defined behavior, but it never returns EBUSY.
+ */
+ deadline = init_deadline(FUSE2FS_OPEN_TIMEOUT);
+ do {
err = ext2fs_open2(fctx.device, options, flags, 0, 0,
unix_io_manager, &global_fs);
+ if ((err == EPERM || err == EACCES) &&
+ (!fctx.ro || (flags & EXT2_FLAG_RW))) {
+ /*
+ * Source device cannot be opened for write. Under
+ * these circumstances, mount(8) will try again with a
+ * ro mount, and the kernel will open the block device
+ * readonly.
+ */
+ log_printf(&fctx, "%s\n",
+ _("WARNING: source write-protected, mounted read-only."));
+ flags &= ~EXT2_FLAG_RW;
+ fctx.ro = 1;
+
+ /* Force the loop to run once more */
+ err = -1;
+ }
+ } while (err == -1 ||
+ (err == EBUSY && retry_before_deadline(deadline)));
+ if (err == EBUSY) {
+ err_printf(&fctx, "%s: %s.\n",
+ _("Could not lock filesystem block device"), error_message(err));
+ goto out;
}
if (err) {
err_printf(&fctx, "%s.\n", error_message(err));