/*-1* SPDX-License-Identifier: BSD-2-Clause2*3* Copyright (c) 2025 ConnectWise4*5* Redistribution and use in source and binary forms, with or without6* modification, are permitted provided that the following conditions7* are met:8* 1. Redistributions of source code must retain the above copyright9* notice, this list of conditions and the following disclaimer.10* 2. Redistributions in binary form must reproduce the above copyright11* notice, this list of conditions and the following disclaimer in the12* documentation and/or other materials provided with the distribution.13*14* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND15* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE16* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE17* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE18* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL19* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS20* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)21* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT22* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY23* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF24* SUCH DAMAGE.25*/2627extern "C" {28#include <sys/param.h>29#include <sys/mount.h>30#include <sys/signal.h>31#include <sys/wait.h>3233#include <fcntl.h>34#include <pthread.h>35#include <semaphore.h>36#include <signal.h>37}3839#include "mockfs.hh"40#include "utils.hh"4142using namespace testing;4344/* Tests for behavior that happens before the server responds to FUSE_INIT */45class PreInit: public FuseTest {46public:47void SetUp() {48m_no_auto_init = true;49FuseTest::SetUp();50}51};5253/*54* Tests for behavior that happens before the server responds to FUSE_INIT,55* parameterized on default_permissions56*/57class PreInitP: public PreInit,58public WithParamInterface<bool>59{60void SetUp() {61m_default_permissions = GetParam();62PreInit::SetUp();63}64};6566static void* unmount1(void* arg __unused) {67ssize_t r;6869r = unmount("mountpoint", 0);70if (r >= 0)71return 0;72else73return (void*)(intptr_t)errno;74}7576/*77* Attempting to unmount the file system before it fully initializes should78* work fine. The unmount will complete after initialization does.79*/80TEST_F(PreInit, unmount_before_init)81{82sem_t sem0;83pthread_t th1;8485ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);8687EXPECT_CALL(*m_mock, process(88ResultOf([](auto in) {89return (in.header.opcode == FUSE_INIT);90}, Eq(true)),91_)92).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {93SET_OUT_HEADER_LEN(out, init);94out.body.init.major = FUSE_KERNEL_VERSION;95out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;96out.body.init.flags = in.body.init.flags & m_init_flags;97out.body.init.max_write = m_maxwrite;98out.body.init.max_readahead = m_maxreadahead;99out.body.init.time_gran = m_time_gran;100sem_wait(&sem0);101})));102expect_destroy(0);103104ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL));105nap(); /* Wait for th1 to block in unmount() */106sem_post(&sem0);107/* The daemon will quit after receiving FUSE_DESTROY */108m_mock->join_daemon();109}110111/*112* Don't panic in this very specific scenario:113*114* The server does not respond to FUSE_INIT in timely fashion.115* Some other process tries to do unmount.116* That other process gets killed by a signal.117* The server finally responds to FUSE_INIT.118*119* Regression test for bug 287438120* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438121*/122TEST_F(PreInit, signal_during_unmount_before_init)123{124sem_t sem0;125pid_t child;126127ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);128129EXPECT_CALL(*m_mock, process(130ResultOf([](auto in) {131return (in.header.opcode == FUSE_INIT);132}, Eq(true)),133_)134).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {135SET_OUT_HEADER_LEN(out, init);136out.body.init.major = FUSE_KERNEL_VERSION;137/*138* Use protocol 7.19, like libfuse2 does. The server must use139* protocol 7.27 or older to trigger the bug.140*/141out.body.init.minor = 19;142out.body.init.flags = in.body.init.flags & m_init_flags;143out.body.init.max_write = m_maxwrite;144out.body.init.max_readahead = m_maxreadahead;145out.body.init.time_gran = m_time_gran;146sem_wait(&sem0);147})));148149if ((child = ::fork()) == 0) {150/*151* In child. This will block waiting for FUSE_INIT to complete152* or the receipt of an asynchronous signal.153*/154(void) unmount("mountpoint", 0);155_exit(0); /* Unreachable, unless parent dies after fork */156} else if (child > 0) {157/* In parent. Wait for child process to start, then kill it */158nap();159kill(child, SIGINT);160waitpid(child, NULL, WEXITED);161} else {162FAIL() << strerror(errno);163}164m_mock->m_quit = true; /* Since we are by now unmounted. */165sem_post(&sem0);166m_mock->join_daemon();167}168169/*170* If some process attempts VOP_GETATTR for the mountpoint before init is171* complete, fusefs should wait, just like it does for other VOPs.172*173* To verify that fuse_vnop_getattr does indeed wait for FUSE_INIT to complete,174* invoke the test like this:175*176> 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"177...178dtrace: pid 4224 has exited179CPU ID FUNCTION:NAME1800 68670 fuse_vnop_getattr:entry1810 68893 fuse_internal_init_callback:entry1820 68894 fuse_internal_init_callback:return1830 68671 fuse_vnop_getattr:return184*185* Note that fuse_vnop_getattr was entered first, but exitted last.186*/187TEST_P(PreInitP, getattr_before_init)188{189struct stat sb;190nlink_t nlink = 12345;191192EXPECT_CALL(*m_mock, process(193ResultOf([=](auto in) {194return (in.header.opcode == FUSE_INIT);195}, Eq(true)),196_)197).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {198SET_OUT_HEADER_LEN(out, init);199out.body.init.major = FUSE_KERNEL_VERSION;200out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;201out.body.init.flags = in.body.init.flags & m_init_flags;202out.body.init.max_write = m_maxwrite;203out.body.init.max_readahead = m_maxreadahead;204out.body.init.time_gran = m_time_gran;205nap(); /* Allow stat() to run first */206})));207EXPECT_CALL(*m_mock, process(208ResultOf([=](auto in) {209return (in.header.opcode == FUSE_GETATTR &&210in.header.nodeid == FUSE_ROOT_ID);211}, Eq(true)),212_)213).WillOnce(Invoke(ReturnImmediate([=](auto& in, auto& out) {214SET_OUT_HEADER_LEN(out, attr);215out.body.attr.attr.ino = in.header.nodeid;216out.body.attr.attr.mode = S_IFDIR | 0644;217out.body.attr.attr.nlink = nlink;218out.body.attr.attr_valid = UINT64_MAX;219})));220221EXPECT_EQ(0, stat("mountpoint", &sb));222EXPECT_EQ(nlink, sb.st_nlink);223}224225INSTANTIATE_TEST_SUITE_P(PI, PreInitP, Bool());226227228