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:
SetUp()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 {
SetUp()61 void SetUp() {
62 m_default_permissions = GetParam();
63 PreInit::SetUp();
64 }
65 };
66
unmount1(void * arg __unused)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 */
TEST_F(PreInit,unmount_before_init)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 */
TEST_F(PreInit,signal_during_unmount_before_init)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 */
TEST_P(PreInitP,getattr_before_init)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