From nobody Wed Sep 23 22:32:41 2026 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hqsD95JV2z6t4px for ; Wed, 23 Sep 2026 22:32:41 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hqsD94ljgz4vs8 for ; Wed, 23 Sep 2026 22:32:41 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1790202761; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=FQlisLhzPxOTetjPq8THRm86UwOH/t90nceZuxybJ8I=; b=BV89XA8vXvgEdGA47BTGTeMBfxzRg56Tyeo53xh68wqvFhC6j6UCwRx+PqH4wtrvTMlajr WTPqWI4FoOtYiDQiyQ/BMFvILOSotyJOOIfbYu+OvS1sbYIvn1dAijxLl0aKmiL3Sf4UEl 6nH/vZ6aEVM3EWMZvvNIWaZrVFAHnrP+nPhedAWlQrVLeRt/3foIHmB9mj364rLK8PHPFT jaTfj+nQXsxkr/JvS+LzLOCKenmWKh7RKX/4gUG7fFBQOtuXASOwmNIQvyOwpyoaibBc+k Iob2eeCH+raqE5XSSq+LXCJ0IxLXg7L8T39tDFjNIU8GSSPwjOkxjfotKM3Q8w== ARC-Seal: i=1; a=rsa-sha256; d=freebsd.org; s=dkim; cv=none; t=1790202761; b=ChDD70SZmeD941pCyhILJN8XruCIjxBGO33FmNz/Fqhl0wvEjcgjMZUgA81Z4NIKDUg+Ts 4gx2LAUpSxzMhXN2sF8SlMwii+o2bFmqQ/UwVrllsa/4McrWNwdfWDdes6FVkSvlXpXTrq v3ii5gRX20I437HbxwLcMFK2vnIzlXyYLJVD87mZZX21pJLpmK4MDIXA67RHZ9sHFLA9L+ dkulE/LhkZJJSX4Hkn8sWapFd2qeUDezRYqz7q72EP27pA5A5ITIrDbPEImC/oNYR1HVTJ BKQnPeISTDrgqhWI7EgexFXQGT3y67SSRkG9CwSHG8quliKE+KniydroYiWM8w== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1790202761; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=FQlisLhzPxOTetjPq8THRm86UwOH/t90nceZuxybJ8I=; b=tfn6FjNmoWEO/U4sGhM6RiaPgz6Wnyd4y1rv2ykzOAq8byQqCAa1tw/Xl6fibD/1fpt5ex EOL4dcnphzJuqWVlXGyLJ2wJ2kMnqYvL9kNxBxgRdU0M4Ggsz3oG9LuH/N5dPuq+sc/+AF o97K99zBCZewnj9BgPP9ol9gpW4l9+vkf37px3bZuI9hep8WQtpeI/bIOmgyyM7MXQ81NV BiF4WkXv6UR3yr7otDd4LVKe0TijNoGE2DyCCRc7qaSUc2DMXYLhbuLOj+vcYovhzns+bj jN9grN5ltd46Viu3el9+xPA1jmSS7jxP/YHP8weokH5IL4d29L59KE893cH3DQ== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hqsD93pzJz1C17 for ; Wed, 23 Sep 2026 22:32:41 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 43da4 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Wed, 23 Sep 2026 22:32:41 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Alan Somers Subject: git: 0bd6348b52e1 - main - fusefs: fix a deadlock caused by daemons that never initialize List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: asomers X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 0bd6348b52e1991fafc55128173c989dab36dfd9 Auto-Submitted: auto-generated Date: Wed, 23 Sep 2026 22:32:41 +0000 Message-Id: <6ab45389.43da4.6500142e@gitrepo.freebsd.org> The branch main has been updated by asomers: URL: https://cgit.FreeBSD.org/src/commit/?id=0bd6348b52e1991fafc55128173c989dab36dfd9 commit 0bd6348b52e1991fafc55128173c989dab36dfd9 Author: Alan Somers AuthorDate: 2026-09-15 21:23:50 +0000 Commit: Alan Somers CommitDate: 2026-09-23 22:32:37 +0000 fusefs: fix a deadlock caused by daemons that never initialize If a daemon never responds to FUSE_INIT, but some process attempts to access the mountpoint, and a different process (possibly the daemon itself) attempts to unmount, a deadlock would result. Fix this bug by blocking any thread that enters fuse_vfsop_root until the daemon responds to FUSE_INIT or it times out. That will block any thread attempting to lookup a path in the fuse mountpoint, before it even gets to fuse VOPs. Remove less thorough initialization checks in fuse_ticket_fetch and fuse_vnop_access that are no longer necessary. And add a test case. PR: 287431 MFC after: 2 weeks Sponsored by: ConnectWise Reviewed by: js Differential Revision: https://reviews.freebsd.org/D59737 --- sys/fs/fuse/fuse_ipc.c | 24 +----------- sys/fs/fuse/fuse_ipc.h | 1 - sys/fs/fuse/fuse_vfsops.c | 31 +++++++++++++++ sys/fs/fuse/fuse_vnops.c | 14 ------- tests/sys/fs/fusefs/mockfs.cc | 8 +++- tests/sys/fs/fusefs/mockfs.hh | 2 +- tests/sys/fs/fusefs/pre-init.cc | 86 +++++++++++++++++++++++++++++++++++------ tests/sys/fs/fusefs/utils.cc | 2 +- tests/sys/fs/fusefs/utils.hh | 2 + 9 files changed, 118 insertions(+), 52 deletions(-) diff --git a/sys/fs/fuse/fuse_ipc.c b/sys/fs/fuse/fuse_ipc.c index c24e11a7ff2f..6b0bbc22d3c4 100644 --- a/sys/fs/fuse/fuse_ipc.c +++ b/sys/fs/fuse/fuse_ipc.c @@ -590,28 +590,6 @@ fdata_set_dead(struct fuse_data *data) FUSE_UNLOCK(); } -struct fuse_ticket * -fuse_ticket_fetch(struct fuse_data *data) -{ - int err = 0; - struct fuse_ticket *ftick; - - ftick = fticket_alloc(data); - - if (!(data->dataflags & FSESS_INITED)) { - /* Sleep until get answer for INIT message */ - FUSE_LOCK(); - if (!(data->dataflags & FSESS_INITED) && data->ticketer > 2) { - err = msleep(&data->ticketer, &fuse_mtx, PCATCH | PDROP, - "fu_ini", 0); - if (err) - fdata_set_dead(data); - } else - FUSE_UNLOCK(); - } - return ftick; -} - int fuse_ticket_drop(struct fuse_ticket *ftick) { @@ -924,7 +902,7 @@ fdisp_make_pid(struct fuse_dispatcher *fdip, enum fuse_opcode op, if (fdip->tick) { fticket_refresh(fdip->tick); } else { - fdip->tick = fuse_ticket_fetch(data); + fdip->tick = fticket_alloc(data); } /* FUSE_DIMALLOC will bzero the fiovs when it enlarges them */ diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h index 7091296bb453..788f48c3ffe0 100644 --- a/sys/fs/fuse/fuse_ipc.h +++ b/sys/fs/fuse/fuse_ipc.h @@ -389,7 +389,6 @@ fuse_aw_pop(struct fuse_data *data) return (ftick); } -struct fuse_ticket *fuse_ticket_fetch(struct fuse_data *data); int fuse_ticket_drop(struct fuse_ticket *ftick); void fuse_insert_callback(struct fuse_ticket *ftick, fuse_handler_t *handler); void fuse_insert_message(struct fuse_ticket *ftick, bool irq); diff --git a/sys/fs/fuse/fuse_vfsops.c b/sys/fs/fuse/fuse_vfsops.c index 7b6ad86e4b2b..aba059fdffc0 100644 --- a/sys/fs/fuse/fuse_vfsops.c +++ b/sys/fs/fuse/fuse_vfsops.c @@ -627,10 +627,37 @@ fuse_vfsop_root(struct mount *mp, int lkflags, struct vnode **vpp) int err = 0; if (data->vroot != NULL) { + if (!(data->dataflags & FSESS_INITED)) { + /* Block all operations until init completes */ + FUSE_LOCK(); + if (data->dataflags & FSESS_INITED) { + /* We must've just gotten initialized */ + FUSE_UNLOCK(); + } else { + err = msleep(&data->ticketer, &fuse_mtx, + PCATCH | PDROP, "fu_ini", + data->daemon_timeout * hz); + if (err) { + /* + * The daemon didn't initialize on + * time. Mark it as dead. Most vnops + * will fail as a result, but VFS_ROOT + * must still succeed for the sake of + * unmount. + */ + fdata_set_dead(data); + } + } + } err = vget(data->vroot, lkflags); if (err == 0) *vpp = data->vroot; } else { + /* + * Get a vnode for the root. Note that this step can happen + * straight from sys_nmount, before INIT is complete. + */ + err = fuse_vnode_get(mp, NULL, FUSE_ROOT_ID, NULL, vpp, NULL, VDIR); if (err == 0) { @@ -667,6 +694,10 @@ fuse_vfsop_statfs(struct mount *mp, struct statfs *sbp) data = fuse_get_mpdata(mp); + /* + * We must fake the statfs data before initialization completes, + * because nmount itself calls VFS_STATFS. + */ if (!(data->dataflags & FSESS_INITED)) goto fake; diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c index 1a4fc60760d8..a5ba292d03c4 100644 --- a/sys/fs/fuse/fuse_vnops.c +++ b/sys/fs/fuse/fuse_vnops.c @@ -507,10 +507,6 @@ fuse_vnop_access(struct vop_access_args *ap) { struct vnode *vp = ap->a_vp; int accmode = ap->a_accmode; - struct ucred *cred = ap->a_cred; - - struct fuse_data *data = fuse_get_mpdata(vnode_mount(vp)); - int err; if (fuse_isdeadfs(vp)) { @@ -520,16 +516,6 @@ fuse_vnop_access(struct vop_access_args *ap) return (EXTERROR(ENXIO, "This FUSE session is about " "to be closed")); } - if (!(data->dataflags & FSESS_INITED)) { - if (vnode_isvroot(vp)) { - if (priv_check_cred(cred, PRIV_VFS_ADMIN) || - (fuse_match_cred(data->daemoncred, cred) == 0)) { - return 0; - } - } - return (EXTERROR(EBADF, "Access denied until FUSE session " - "is initialized")); - } if (vnode_islnk(vp)) { return 0; } diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index fe4979762d92..d2aec2e7828c 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -427,7 +427,7 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other, 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, bool no_auto_init, - bool auto_unmount) + bool auto_unmount, unsigned daemon_timeout) : m_daemon_id(NULL), m_kernel_minor_version(kernel_minor_version), m_kq(pm == KQ ? kqueue() : -1), @@ -524,6 +524,12 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other, build_iovec(&iov, &iovlen, "auto_unmount", __DECONST(void*, &trueval), sizeof(bool)); } + if (daemon_timeout > 0) { + char val[12]; + + snprintf(val, sizeof(val), "%u", daemon_timeout); + build_iovec(&iov, &iovlen, "timeout=", &val, -1); + } if (*fsname) { build_iovec(&iov, &iovlen, "fsname=", __DECONST(void*, fsname), -1); diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index 19693e50d212..5955a4bca582 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -372,7 +372,7 @@ class MockFS { 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 no_auto_init, bool auto_unmount); + bool no_auto_init, bool auto_unmount, unsigned daemon_timeout); virtual ~MockFS(); diff --git a/tests/sys/fs/fusefs/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc index 70d7ddcdb0f4..988040eb6c48 100644 --- a/tests/sys/fs/fusefs/pre-init.cc +++ b/tests/sys/fs/fusefs/pre-init.cc @@ -47,6 +47,7 @@ class PreInit: public FuseTest { public: void SetUp() { m_no_auto_init = true; + m_daemon_timeout = 1; FuseTest::SetUp(); } }; @@ -64,6 +65,28 @@ void SetUp() { } }; +const char FULLPATH0[] = "mountpoint/some_file.txt"; +static void* access0(void* arg __unused) { + ssize_t r; + + r = access(FULLPATH0, F_OK); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +static void* stat1(void* arg) { + struct stat *sb = (struct stat*) arg; + int r; + + r = stat("mountpoint", sb); + if (r != 0) + return 0; + else + return (void*)(intptr_t)errno; +} + static void* unmount1(void* arg __unused) { ssize_t r; @@ -172,25 +195,65 @@ TEST_F(PreInit, signal_during_unmount_before_init) } /* - * If some process attempts VOP_GETATTR for the mountpoint before init is - * complete, fusefs should wait, just like it does for other VOPs. + * If the daemon tries to unmount without ever completing INIT but after an + * operation like FUSE_ACCESS is blocking, waiting for the daemon, don't + * deadlock. This will probably also happen if the unmount comes from a + * different process. * - * To verify that fuse_vnop_getattr does indeed wait for FUSE_INIT to complete, + * When m_default_permissions is true, this is a regression test for bug 287431 + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287431 + */ +TEST_F(PreInit, access_but_never_init) +{ + pthread_t th0; + + /* + * Don't call m_mock->start_service(), so nothing will ever respond to + * FUSE_INIT. + */ + + /* + * Create a thread to issue a fuse operation. It will block since + * FUSE_INIT is not complete. + */ + ASSERT_EQ(0, pthread_create(&th0, NULL, access0, NULL)); + nap(); + + /* + * Unmounting now, without ever responding to FUSE_INIT, should + * hopefully not trigger a deadlock. + */ + m_mock->unmount(); + + pthread_join(th0, NULL); +} + +/* + * If some process attempts to access a vnode on the mountpoint before init is + * complete, fusefs should wait. + * + * To verify that fuse_vfsop_root does indeed wait for FUSE_INIT to complete, * invoke the test like this: * -> sudo cpuset -c -l 0 dtrace -i 'fbt:fusefs:fuse_internal_init_callback:' -i 'fbt:fusefs:fuse_vnop_getattr:' -c "./pre-init --gtest_filter=PI/PreInitP.getattr_before_init/0" +> sudo cpuset -c -l 0 dtrace -i 'fbt:fusefs:fuse_vfsop_root: {printf("tid=%d", tid);}' -i 'fbt:fusefs:fuse_internal_init_callback: {printf("tid=%d", tid);}' -i 'fbt:fusefs:fuse_vnop_getattr: {printf("tid=%d", tid);}' -c "./pre-init --gtest_filter=PI/PreInitP.getattr_before_init/0" ... -dtrace: pid 4224 has exited +dtrace: pid 7399 has exited CPU ID FUNCTION:NAME - 0 68670 fuse_vnop_getattr:entry - 0 68893 fuse_internal_init_callback:entry - 0 68894 fuse_internal_init_callback:return - 0 68671 fuse_vnop_getattr:return +... + 0 72004 fuse_vfsop_root:entry tid=102466 + 0 72245 fuse_internal_init_callback:entry tid=102465 + 0 72246 fuse_internal_init_callback:return tid=102465 + 0 72005 fuse_vfsop_root:return tid=102466 + 0 72017 fuse_vnop_getattr:entry tid=102466 + 0 72018 fuse_vnop_getattr:return tid=102466 +... * - * Note that fuse_vnop_getattr was entered first, but exitted last. + * Note that fuse_vnop_getattr's thread entered fuse_vfsop_root first, but + * exitted only after fuse_internal_init_callback completed. */ TEST_P(PreInitP, getattr_before_init) { + pthread_t th0; struct stat sb; nlink_t nlink = 12345; @@ -225,7 +288,8 @@ TEST_P(PreInitP, getattr_before_init) m_mock->start_service(); - EXPECT_EQ(0, stat("mountpoint", &sb)); + ASSERT_EQ(0, pthread_create(&th0, NULL, stat1, &sb)); + pthread_join(th0, NULL); EXPECT_EQ(nlink, sb.st_nlink); } diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index eece4ae100cf..25324f0289bc 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -152,7 +152,7 @@ void FuseTest::SetUp() { 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_no_auto_init, m_auto_unmount); + m_no_auto_init, m_auto_unmount, m_daemon_timeout); /* * 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 ebd8d41d6961..8bf3765ac5a6 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -72,6 +72,7 @@ class FuseTest : public ::testing::Test { bool m_no_auto_init; bool m_auto_unmount; unsigned m_time_gran; + unsigned m_daemon_timeout; MockFS *m_mock = NULL; const static uint64_t FH = 0xdeadbeef1a7ebabe; const char *reclaim_mib = "debug.try_reclaim_vnode"; @@ -100,6 +101,7 @@ class FuseTest : public ::testing::Test { m_no_auto_init(false), m_auto_unmount(false), m_time_gran(1), + m_daemon_timeout(0), m_fsname(""), m_subtype(""), m_maxbcachebuf(0),