Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 12 Jun 2025 23:06:12 GMT
From:      Alan Somers <asomers@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: cc9c21308679 - main - fusefs: Fix a panic when unmounting before init
Message-ID:  <202506122306.55CN6CuX031423@gitrepo.freebsd.org>

next in thread | raw e-mail | index | archive | help
The branch main has been updated by asomers:

URL: https://cgit.FreeBSD.org/src/commit/?id=cc9c213086795fd821e6c17ee99a1db81a2141b2

commit cc9c213086795fd821e6c17ee99a1db81a2141b2
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2025-06-05 20:00:48 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2025-06-12 23:04:34 +0000

    fusefs: Fix a panic when unmounting before init
    
    fusefs would page fault due to the following sequence of events:
    * The server did not respond to FUSE_INIT in timely fashion.
    * Some other process tried to do unmount.
    * That other process got killed by a signal.
    * The server finally responded to FUSE_INIT.
    
    PR:             287438
    MFC after:      2 weeks
    Sponsored by:   ConnectWise
    Reviewed by:    arrowd
    Differential Revision: https://reviews.freebsd.org/D50799
---
 sys/fs/fuse/fuse_internal.c     |   3 +
 tests/sys/fs/fusefs/Makefile    |   1 +
 tests/sys/fs/fusefs/mockfs.cc   |  18 +++--
 tests/sys/fs/fusefs/mockfs.hh   |   6 +-
 tests/sys/fs/fusefs/pre-init.cc | 154 ++++++++++++++++++++++++++++++++++++++++
 tests/sys/fs/fusefs/utils.cc    |   3 +-
 tests/sys/fs/fusefs/utils.hh    |   2 +
 7 files changed, 179 insertions(+), 8 deletions(-)

diff --git a/sys/fs/fuse/fuse_internal.c b/sys/fs/fuse/fuse_internal.c
index c6354ae7150f..61fe2ed032f6 100644
--- a/sys/fs/fuse/fuse_internal.c
+++ b/sys/fs/fuse/fuse_internal.c
@@ -979,6 +979,9 @@ fuse_internal_init_callback(struct fuse_ticket *tick, struct uio *uio)
 	struct fuse_data *data = tick->tk_data;
 	struct fuse_init_out *fiio = NULL;
 
+	if (fdata_get_dead(data))
+		goto out;
+
 	if ((err = tick->tk_aw_ohead.error)) {
 		goto out;
 	}
diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile
index 4265f5b71dfa..b1ac704ab4f6 100644
--- a/tests/sys/fs/fusefs/Makefile
+++ b/tests/sys/fs/fusefs/Makefile
@@ -39,6 +39,7 @@ GTESTS+=	nfs
 GTESTS+=	notify
 GTESTS+=	open
 GTESTS+=	opendir
+GTESTS+=	pre-init
 GTESTS+=	read
 GTESTS+=	readdir
 GTESTS+=	readlink
diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc
index b1621d05703c..65cdc3919652 100644
--- a/tests/sys/fs/fusefs/mockfs.cc
+++ b/tests/sys/fs/fusefs/mockfs.cc
@@ -420,7 +420,7 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other,
 	bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags,
 	uint32_t kernel_minor_version, uint32_t max_write, bool async,
 	bool noclusterr, unsigned time_gran, bool nointr, bool noatime,
-	const char *fsname, const char *subtype)
+	const char *fsname, const char *subtype, bool no_auto_init)
 	: m_daemon_id(NULL),
 	  m_kernel_minor_version(kernel_minor_version),
 	  m_kq(pm == KQ ? kqueue() : -1),
@@ -529,7 +529,9 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other,
 	ON_CALL(*this, process(_, _))
 		.WillByDefault(Invoke(this, &MockFS::process_default));
 
-	init(flags);
+	if (!no_auto_init)
+		init(flags);
+
 	bzero(&sa, sizeof(sa));
 	sa.sa_handler = sigint_handler;
 	sa.sa_flags = 0;	/* Don't set SA_RESTART! */
@@ -543,10 +545,7 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other,
 
 MockFS::~MockFS() {
 	kill_daemon();
-	if (m_daemon_id != NULL) {
-		pthread_join(m_daemon_id, NULL);
-		m_daemon_id = NULL;
-	}
+	join_daemon();
 	::unmount("mountpoint", MNT_FORCE);
 	rmdir("mountpoint");
 	if (m_kq >= 0)
@@ -787,6 +786,13 @@ void MockFS::kill_daemon() {
 	m_fuse_fd = -1;
 }
 
+void MockFS::join_daemon() {
+	if (m_daemon_id != NULL) {
+		pthread_join(m_daemon_id, NULL);
+		m_daemon_id = NULL;
+	}
+}
+
 void MockFS::loop() {
 	std::vector<std::unique_ptr<mockfs_buf_out>> out;
 
diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh
index 1de77767d0c9..ba6f7fded9d0 100644
--- a/tests/sys/fs/fusefs/mockfs.hh
+++ b/tests/sys/fs/fusefs/mockfs.hh
@@ -366,13 +366,17 @@ class MockFS {
 		enum poll_method pm, uint32_t flags,
 		uint32_t kernel_minor_version, uint32_t max_write, bool async,
 		bool no_clusterr, unsigned time_gran, bool nointr,
-		bool noatime, const char *fsname, const char *subtype);
+		bool noatime, const char *fsname, const char *subtype,
+		bool no_auto_init);
 
 	virtual ~MockFS();
 
 	/* Kill the filesystem daemon without unmounting the filesystem */
 	void kill_daemon();
 
+	/* Wait until the daemon thread terminates */
+	void join_daemon();
+
 	/* Process FUSE requests endlessly */
 	void loop();
 
diff --git a/tests/sys/fs/fusefs/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc
new file mode 100644
index 000000000000..e990d3cafffa
--- /dev/null
+++ b/tests/sys/fs/fusefs/pre-init.cc
@@ -0,0 +1,154 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 ConnectWise
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+extern "C" {
+#include <sys/param.h>
+#include <sys/mount.h>
+#include <sys/signal.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <semaphore.h>
+#include <signal.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* Tests for behavior that happens before the server responds to FUSE_INIT */
+class PreInit: public FuseTest {
+void SetUp() {
+	m_no_auto_init = true;
+	FuseTest::SetUp();
+}
+};
+
+static void* unmount1(void* arg __unused) {
+	ssize_t r;
+
+	r = unmount("mountpoint", 0);
+	if (r >= 0)
+		return 0;
+	else
+		return (void*)(intptr_t)errno;
+}
+
+/*
+ * Attempting to unmount the file system before it fully initializes should
+ * work fine.  The unmount will complete after initialization does.
+ */
+TEST_F(PreInit, unmount_before_init)
+{
+	sem_t sem0;
+	pthread_t th1;
+
+	ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([](auto in) {
+			return (in.header.opcode == FUSE_INIT);
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+		SET_OUT_HEADER_LEN(out, init);
+		out.body.init.major = FUSE_KERNEL_VERSION;
+		out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;
+		out.body.init.flags = in.body.init.flags & m_init_flags;
+		out.body.init.max_write = m_maxwrite;
+		out.body.init.max_readahead = m_maxreadahead;
+		out.body.init.time_gran = m_time_gran;
+		sem_wait(&sem0);
+	})));
+	expect_destroy(0);
+
+	ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL));
+	nap();	/* Wait for th1 to block in unmount() */
+	sem_post(&sem0);
+	/* The daemon will quit after receiving FUSE_DESTROY */
+	m_mock->join_daemon();
+}
+
+/*
+ * Don't panic in this very specific scenario:
+ *
+ * The server does not respond to FUSE_INIT in timely fashion.
+ * Some other process tries to do unmount.
+ * That other process gets killed by a signal.
+ * The server finally responds to FUSE_INIT.
+ *
+ * Regression test for bug 287438
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438
+ */
+TEST_F(PreInit, signal_during_unmount_before_init)
+{
+	sem_t sem0;
+	pid_t child;
+
+	ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([](auto in) {
+			return (in.header.opcode == FUSE_INIT);
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+		SET_OUT_HEADER_LEN(out, init);
+		out.body.init.major = FUSE_KERNEL_VERSION;
+		/*
+		 * Use protocol 7.19, like libfuse2 does.  The server must use
+		 * protocol 7.27 or older to trigger the bug.
+		 */
+		out.body.init.minor = 19;
+		out.body.init.flags = in.body.init.flags & m_init_flags;
+		out.body.init.max_write = m_maxwrite;
+		out.body.init.max_readahead = m_maxreadahead;
+		out.body.init.time_gran = m_time_gran;
+		sem_wait(&sem0);
+	})));
+
+	if ((child = ::fork()) == 0) {
+		/*
+		 * In child.  This will block waiting for FUSE_INIT to complete
+		 * or the receipt of an asynchronous signal.
+		 */
+		(void) unmount("mountpoint", 0);
+		_exit(0);	/* Unreachable, unless parent dies after fork */
+	} else if (child > 0) {
+		/* In parent.  Wait for child process to start, then kill it */
+		nap();
+		kill(child, SIGINT);
+		waitpid(child, NULL, WEXITED);
+	} else {
+		FAIL() << strerror(errno);
+	}
+	m_mock->m_quit = true;	/* Since we are by now unmounted. */
+	sem_post(&sem0);
+	m_mock->join_daemon();
+}
diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc
index d702dec50247..125b7e2d6fc7 100644
--- a/tests/sys/fs/fusefs/utils.cc
+++ b/tests/sys/fs/fusefs/utils.cc
@@ -151,7 +151,8 @@ void FuseTest::SetUp() {
 			m_default_permissions, m_push_symlinks_in, m_ro,
 			m_pm, m_init_flags, m_kernel_minor_version,
 			m_maxwrite, m_async, m_noclusterr, m_time_gran,
-			m_nointr, m_noatime, m_fsname, m_subtype);
+			m_nointr, m_noatime, m_fsname, m_subtype,
+			m_no_auto_init);
 		/* 
 		 * FUSE_ACCESS is called almost universally.  Expecting it in
 		 * each test case would be super-annoying.  Instead, set a
diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh
index 9dd8dad6b5cc..91bbba909672 100644
--- a/tests/sys/fs/fusefs/utils.hh
+++ b/tests/sys/fs/fusefs/utils.hh
@@ -69,6 +69,7 @@ class FuseTest : public ::testing::Test {
 	bool m_async;
 	bool m_noclusterr;
 	bool m_nointr;
+	bool m_no_auto_init;
 	unsigned m_time_gran;
 	MockFS *m_mock = NULL;
 	const static uint64_t FH = 0xdeadbeef1a7ebabe;
@@ -95,6 +96,7 @@ class FuseTest : public ::testing::Test {
 		m_async(false),
 		m_noclusterr(false),
 		m_nointr(false),
+		m_no_auto_init(false),
 		m_time_gran(1),
 		m_fsname(""),
 		m_subtype(""),



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?202506122306.55CN6CuX031423>