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 m_daemon_timeout = 1; 51 FuseTest::SetUp(); 52 } 53 }; 54 55 /* 56 * Tests for behavior that happens before the server responds to FUSE_INIT, 57 * parameterized on default_permissions 58 */ 59 class PreInitP: public PreInit, 60 public WithParamInterface<bool> 61 { 62 void SetUp() { 63 m_default_permissions = GetParam(); 64 PreInit::SetUp(); 65 } 66 }; 67 68 const char FULLPATH0[] = "mountpoint/some_file.txt"; 69 static void* access0(void* arg __unused) { 70 ssize_t r; 71 72 r = access(FULLPATH0, F_OK); 73 if (r >= 0) 74 return 0; 75 else 76 return (void*)(intptr_t)errno; 77 } 78 79 static void* stat1(void* arg) { 80 struct stat *sb = (struct stat*) arg; 81 int r; 82 83 r = stat("mountpoint", sb); 84 if (r != 0) 85 return 0; 86 else 87 return (void*)(intptr_t)errno; 88 } 89 90 static void* unmount1(void* arg __unused) { 91 ssize_t r; 92 93 r = unmount("mountpoint", 0); 94 if (r >= 0) 95 return 0; 96 else 97 return (void*)(intptr_t)errno; 98 } 99 100 /* 101 * Attempting to unmount the file system before it fully initializes should 102 * work fine. The unmount will complete after initialization does. 103 */ 104 TEST_F(PreInit, unmount_before_init) 105 { 106 sem_t sem0; 107 pthread_t th1; 108 109 ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); 110 111 EXPECT_CALL(*m_mock, process( 112 ResultOf([](auto in) { 113 return (in.header.opcode == FUSE_INIT); 114 }, Eq(true)), 115 _) 116 ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { 117 SET_OUT_HEADER_LEN(out, init); 118 out.body.init.major = FUSE_KERNEL_VERSION; 119 out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; 120 out.body.init.flags = in.body.init.flags & m_init_flags; 121 out.body.init.max_write = m_maxwrite; 122 out.body.init.max_readahead = m_maxreadahead; 123 out.body.init.time_gran = m_time_gran; 124 sem_wait(&sem0); 125 }))); 126 expect_destroy(0); 127 128 m_mock->start_service(); 129 130 ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL)); 131 nap(); /* Wait for th1 to block in unmount() */ 132 sem_post(&sem0); 133 /* The daemon will quit after receiving FUSE_DESTROY */ 134 m_mock->join_daemon(); 135 } 136 137 /* 138 * Don't panic in this very specific scenario: 139 * 140 * The server does not respond to FUSE_INIT in timely fashion. 141 * Some other process tries to do unmount. 142 * That other process gets killed by a signal. 143 * The server finally responds to FUSE_INIT. 144 * 145 * Regression test for bug 287438 146 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438 147 */ 148 TEST_F(PreInit, signal_during_unmount_before_init) 149 { 150 sem_t sem0; 151 pid_t child; 152 153 ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); 154 155 EXPECT_CALL(*m_mock, process( 156 ResultOf([](auto in) { 157 return (in.header.opcode == FUSE_INIT); 158 }, Eq(true)), 159 _) 160 ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { 161 SET_OUT_HEADER_LEN(out, init); 162 out.body.init.major = FUSE_KERNEL_VERSION; 163 /* 164 * Use protocol 7.19, like libfuse2 does. The server must use 165 * protocol 7.27 or older to trigger the bug. 166 */ 167 out.body.init.minor = 19; 168 out.body.init.flags = in.body.init.flags & m_init_flags; 169 out.body.init.max_write = m_maxwrite; 170 out.body.init.max_readahead = m_maxreadahead; 171 out.body.init.time_gran = m_time_gran; 172 sem_wait(&sem0); 173 }))); 174 175 m_mock->start_service(); 176 177 if ((child = ::fork()) == 0) { 178 /* 179 * In child. This will block waiting for FUSE_INIT to complete 180 * or the receipt of an asynchronous signal. 181 */ 182 (void) unmount("mountpoint", 0); 183 _exit(0); /* Unreachable, unless parent dies after fork */ 184 } else if (child > 0) { 185 /* In parent. Wait for child process to start, then kill it */ 186 nap(); 187 kill(child, SIGINT); 188 waitpid(child, NULL, WEXITED); 189 } else { 190 FAIL() << strerror(errno); 191 } 192 m_mock->m_quit = true; /* Since we are by now unmounted. */ 193 sem_post(&sem0); 194 m_mock->join_daemon(); 195 } 196 197 /* 198 * If the daemon tries to unmount without ever completing INIT but after an 199 * operation like FUSE_ACCESS is blocking, waiting for the daemon, don't 200 * deadlock. This will probably also happen if the unmount comes from a 201 * different process. 202 * 203 * When m_default_permissions is true, this is a regression test for bug 287431 204 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287431 205 */ 206 TEST_F(PreInit, access_but_never_init) 207 { 208 pthread_t th0; 209 210 /* 211 * Don't call m_mock->start_service(), so nothing will ever respond to 212 * FUSE_INIT. 213 */ 214 215 /* 216 * Create a thread to issue a fuse operation. It will block since 217 * FUSE_INIT is not complete. 218 */ 219 ASSERT_EQ(0, pthread_create(&th0, NULL, access0, NULL)); 220 nap(); 221 222 /* 223 * Unmounting now, without ever responding to FUSE_INIT, should 224 * hopefully not trigger a deadlock. 225 */ 226 m_mock->unmount(); 227 228 pthread_join(th0, NULL); 229 } 230 231 /* 232 * If some process attempts to access a vnode on the mountpoint before init is 233 * complete, fusefs should wait. 234 * 235 * To verify that fuse_vfsop_root does indeed wait for FUSE_INIT to complete, 236 * invoke the test like this: 237 * 238 > 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" 239 ... 240 dtrace: pid 7399 has exited 241 CPU ID FUNCTION:NAME 242 ... 243 0 72004 fuse_vfsop_root:entry tid=102466 244 0 72245 fuse_internal_init_callback:entry tid=102465 245 0 72246 fuse_internal_init_callback:return tid=102465 246 0 72005 fuse_vfsop_root:return tid=102466 247 0 72017 fuse_vnop_getattr:entry tid=102466 248 0 72018 fuse_vnop_getattr:return tid=102466 249 ... 250 * 251 * Note that fuse_vnop_getattr's thread entered fuse_vfsop_root first, but 252 * exitted only after fuse_internal_init_callback completed. 253 */ 254 TEST_P(PreInitP, getattr_before_init) 255 { 256 pthread_t th0; 257 struct stat sb; 258 nlink_t nlink = 12345; 259 260 EXPECT_CALL(*m_mock, process( 261 ResultOf([=](auto in) { 262 return (in.header.opcode == FUSE_INIT); 263 }, Eq(true)), 264 _) 265 ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { 266 SET_OUT_HEADER_LEN(out, init); 267 out.body.init.major = FUSE_KERNEL_VERSION; 268 out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; 269 out.body.init.flags = in.body.init.flags & m_init_flags; 270 out.body.init.max_write = m_maxwrite; 271 out.body.init.max_readahead = m_maxreadahead; 272 out.body.init.time_gran = m_time_gran; 273 nap(); /* Allow stat() to run first */ 274 }))); 275 EXPECT_CALL(*m_mock, process( 276 ResultOf([=](auto in) { 277 return (in.header.opcode == FUSE_GETATTR && 278 in.header.nodeid == FUSE_ROOT_ID); 279 }, Eq(true)), 280 _) 281 ).WillOnce(Invoke(ReturnImmediate([=](auto& in, auto& out) { 282 SET_OUT_HEADER_LEN(out, attr); 283 out.body.attr.attr.ino = in.header.nodeid; 284 out.body.attr.attr.mode = S_IFDIR | 0644; 285 out.body.attr.attr.nlink = nlink; 286 out.body.attr.attr_valid = UINT64_MAX; 287 }))); 288 289 m_mock->start_service(); 290 291 ASSERT_EQ(0, pthread_create(&th0, NULL, stat1, &sb)); 292 pthread_join(th0, NULL); 293 EXPECT_EQ(nlink, sb.st_nlink); 294 } 295 296 INSTANTIATE_TEST_SUITE_P(PI, PreInitP, Bool()); 297