1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2025 ConnectWise 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 * SUCH DAMAGE. 26 */ 27 28 extern "C" { 29 #include <sys/param.h> 30 #include <sys/mount.h> 31 #include <sys/signal.h> 32 #include <sys/wait.h> 33 34 #include <fcntl.h> 35 #include <pthread.h> 36 #include <semaphore.h> 37 #include <signal.h> 38 } 39 40 #include "mockfs.hh" 41 #include "utils.hh" 42 43 using namespace testing; 44 45 /* Tests for behavior that happens before the server responds to FUSE_INIT */ 46 class PreInit: public FuseTest { 47 public: 48 void SetUp() { 49 m_no_auto_init = true; 50 FuseTest::SetUp(); 51 } 52 }; 53 54 /* 55 * Tests for behavior that happens before the server responds to FUSE_INIT, 56 * parameterized on default_permissions 57 */ 58 class PreInitP: public PreInit, 59 public WithParamInterface<bool> 60 { 61 void SetUp() { 62 m_default_permissions = GetParam(); 63 PreInit::SetUp(); 64 } 65 }; 66 67 static void* unmount1(void* arg __unused) { 68 ssize_t r; 69 70 r = unmount("mountpoint", 0); 71 if (r >= 0) 72 return 0; 73 else 74 return (void*)(intptr_t)errno; 75 } 76 77 /* 78 * Attempting to unmount the file system before it fully initializes should 79 * work fine. The unmount will complete after initialization does. 80 */ 81 TEST_F(PreInit, unmount_before_init) 82 { 83 sem_t sem0; 84 pthread_t th1; 85 86 ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); 87 88 EXPECT_CALL(*m_mock, process( 89 ResultOf([](auto in) { 90 return (in.header.opcode == FUSE_INIT); 91 }, Eq(true)), 92 _) 93 ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { 94 SET_OUT_HEADER_LEN(out, init); 95 out.body.init.major = FUSE_KERNEL_VERSION; 96 out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; 97 out.body.init.flags = in.body.init.flags & m_init_flags; 98 out.body.init.max_write = m_maxwrite; 99 out.body.init.max_readahead = m_maxreadahead; 100 out.body.init.time_gran = m_time_gran; 101 sem_wait(&sem0); 102 }))); 103 expect_destroy(0); 104 105 ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL)); 106 nap(); /* Wait for th1 to block in unmount() */ 107 sem_post(&sem0); 108 /* The daemon will quit after receiving FUSE_DESTROY */ 109 m_mock->join_daemon(); 110 } 111 112 /* 113 * Don't panic in this very specific scenario: 114 * 115 * The server does not respond to FUSE_INIT in timely fashion. 116 * Some other process tries to do unmount. 117 * That other process gets killed by a signal. 118 * The server finally responds to FUSE_INIT. 119 * 120 * Regression test for bug 287438 121 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438 122 */ 123 TEST_F(PreInit, signal_during_unmount_before_init) 124 { 125 sem_t sem0; 126 pid_t child; 127 128 ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); 129 130 EXPECT_CALL(*m_mock, process( 131 ResultOf([](auto in) { 132 return (in.header.opcode == FUSE_INIT); 133 }, Eq(true)), 134 _) 135 ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { 136 SET_OUT_HEADER_LEN(out, init); 137 out.body.init.major = FUSE_KERNEL_VERSION; 138 /* 139 * Use protocol 7.19, like libfuse2 does. The server must use 140 * protocol 7.27 or older to trigger the bug. 141 */ 142 out.body.init.minor = 19; 143 out.body.init.flags = in.body.init.flags & m_init_flags; 144 out.body.init.max_write = m_maxwrite; 145 out.body.init.max_readahead = m_maxreadahead; 146 out.body.init.time_gran = m_time_gran; 147 sem_wait(&sem0); 148 }))); 149 150 if ((child = ::fork()) == 0) { 151 /* 152 * In child. This will block waiting for FUSE_INIT to complete 153 * or the receipt of an asynchronous signal. 154 */ 155 (void) unmount("mountpoint", 0); 156 _exit(0); /* Unreachable, unless parent dies after fork */ 157 } else if (child > 0) { 158 /* In parent. Wait for child process to start, then kill it */ 159 nap(); 160 kill(child, SIGINT); 161 waitpid(child, NULL, WEXITED); 162 } else { 163 FAIL() << strerror(errno); 164 } 165 m_mock->m_quit = true; /* Since we are by now unmounted. */ 166 sem_post(&sem0); 167 m_mock->join_daemon(); 168 } 169 170 /* 171 * If some process attempts VOP_GETATTR for the mountpoint before init is 172 * complete, fusefs should wait, just like it does for other VOPs. 173 * 174 * To verify that fuse_vnop_getattr does indeed wait for FUSE_INIT to complete, 175 * invoke the test like this: 176 * 177 > 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" 178 ... 179 dtrace: pid 4224 has exited 180 CPU ID FUNCTION:NAME 181 0 68670 fuse_vnop_getattr:entry 182 0 68893 fuse_internal_init_callback:entry 183 0 68894 fuse_internal_init_callback:return 184 0 68671 fuse_vnop_getattr:return 185 * 186 * Note that fuse_vnop_getattr was entered first, but exitted last. 187 */ 188 TEST_P(PreInitP, getattr_before_init) 189 { 190 struct stat sb; 191 nlink_t nlink = 12345; 192 193 EXPECT_CALL(*m_mock, process( 194 ResultOf([=](auto in) { 195 return (in.header.opcode == FUSE_INIT); 196 }, Eq(true)), 197 _) 198 ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { 199 SET_OUT_HEADER_LEN(out, init); 200 out.body.init.major = FUSE_KERNEL_VERSION; 201 out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; 202 out.body.init.flags = in.body.init.flags & m_init_flags; 203 out.body.init.max_write = m_maxwrite; 204 out.body.init.max_readahead = m_maxreadahead; 205 out.body.init.time_gran = m_time_gran; 206 nap(); /* Allow stat() to run first */ 207 }))); 208 EXPECT_CALL(*m_mock, process( 209 ResultOf([=](auto in) { 210 return (in.header.opcode == FUSE_GETATTR && 211 in.header.nodeid == FUSE_ROOT_ID); 212 }, Eq(true)), 213 _) 214 ).WillOnce(Invoke(ReturnImmediate([=](auto& in, auto& out) { 215 SET_OUT_HEADER_LEN(out, attr); 216 out.body.attr.attr.ino = in.header.nodeid; 217 out.body.attr.attr.mode = S_IFDIR | 0644; 218 out.body.attr.attr.nlink = nlink; 219 out.body.attr.attr_valid = UINT64_MAX; 220 }))); 221 222 EXPECT_EQ(0, stat("mountpoint", &sb)); 223 EXPECT_EQ(nlink, sb.st_nlink); 224 } 225 226 INSTANTIATE_TEST_SUITE_P(PI, PreInitP, Bool()); 227