1 /*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2021 Alan Somers
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/stat.h>
32
33 #include <fcntl.h>
34 #include <pthread.h>
35 #include <semaphore.h>
36 }
37
38 #include "mockfs.hh"
39 #include "utils.hh"
40
41 using namespace testing;
42
43 /*
44 * "Last Local Modify" bugs
45 *
46 * This file tests a class of race conditions caused by one thread fetching a
47 * file's size with FUSE_LOOKUP while another thread simultaneously modifies it
48 * with FUSE_SETATTR, FUSE_WRITE, FUSE_COPY_FILE_RANGE or similar. It's
49 * possible for the second thread to start later yet finish first. If that
50 * happens, the first thread must not override the size set by the second
51 * thread.
52 *
53 * FUSE_GETATTR is not vulnerable to the same race, because it is always called
54 * with the vnode lock held.
55 *
56 * A few other operations like FUSE_LINK can also trigger the same race but
57 * with the file's ctime instead of size. However, the consequences of an
58 * incorrect ctime are much less disastrous than an incorrect size, so fusefs
59 * does not attempt to prevent such races.
60 */
61
62 enum Mutator {
63 VOP_ALLOCATE,
64 VOP_COPY_FILE_RANGE,
65 VOP_SETATTR,
66 VOP_WRITE,
67 };
68
69 /*
70 * Translate a poll method's string representation to the enum value.
71 * Using strings with ::testing::Values gives better output with
72 * --gtest_list_tests
73 */
writer_from_str(const char * s)74 enum Mutator writer_from_str(const char* s) {
75 if (0 == strcmp("VOP_ALLOCATE", s))
76 return VOP_ALLOCATE;
77 else if (0 == strcmp("VOP_COPY_FILE_RANGE", s))
78 return VOP_COPY_FILE_RANGE;
79 else if (0 == strcmp("VOP_SETATTR", s))
80 return VOP_SETATTR;
81 else
82 return VOP_WRITE;
83 }
84
fuse_op_from_mutator(enum Mutator mutator)85 uint32_t fuse_op_from_mutator(enum Mutator mutator) {
86 switch(mutator) {
87 case VOP_ALLOCATE:
88 return(FUSE_FALLOCATE);
89 case VOP_COPY_FILE_RANGE:
90 return(FUSE_COPY_FILE_RANGE);
91 case VOP_SETATTR:
92 return(FUSE_SETATTR);
93 case VOP_WRITE:
94 return(FUSE_WRITE);
95 }
96 }
97
98 class LastLocalModify: public FuseTest, public WithParamInterface<const char*> {
99 public:
SetUp()100 virtual void SetUp() {
101 m_init_flags = FUSE_EXPORT_SUPPORT;
102
103 FuseTest::SetUp();
104 }
105 };
106
allocate_th(void * arg)107 static void* allocate_th(void* arg) {
108 int fd;
109 ssize_t r;
110 sem_t *sem = (sem_t*) arg;
111
112 if (sem)
113 sem_wait(sem);
114
115 fd = open("mountpoint/some_file.txt", O_RDWR);
116 if (fd < 0)
117 return (void*)(intptr_t)errno;
118
119 r = posix_fallocate(fd, 0, 15);
120 LastLocalModify::leak(fd);
121 if (r >= 0)
122 return 0;
123 else
124 return (void*)(intptr_t)errno;
125 }
126
copy_file_range_th(void * arg)127 static void* copy_file_range_th(void* arg) {
128 ssize_t r;
129 int fd;
130 sem_t *sem = (sem_t*) arg;
131 off_t off_in = 0;
132 off_t off_out = 10;
133 ssize_t len = 5;
134
135 if (sem)
136 sem_wait(sem);
137 fd = open("mountpoint/some_file.txt", O_RDWR);
138 if (fd < 0)
139 return (void*)(intptr_t)errno;
140
141 r = copy_file_range(fd, &off_in, fd, &off_out, len, 0);
142 if (r >= 0) {
143 LastLocalModify::leak(fd);
144 return 0;
145 } else
146 return (void*)(intptr_t)errno;
147 }
148
setattr_th(void * arg)149 static void* setattr_th(void* arg) {
150 int fd;
151 ssize_t r;
152 sem_t *sem = (sem_t*) arg;
153
154 if (sem)
155 sem_wait(sem);
156
157 fd = open("mountpoint/some_file.txt", O_RDWR);
158 if (fd < 0)
159 return (void*)(intptr_t)errno;
160
161 r = ftruncate(fd, 15);
162 LastLocalModify::leak(fd);
163 if (r >= 0)
164 return 0;
165 else
166 return (void*)(intptr_t)errno;
167 }
168
write_th(void * arg)169 static void* write_th(void* arg) {
170 ssize_t r;
171 int fd;
172 sem_t *sem = (sem_t*) arg;
173 const char BUF[] = "abcdefghijklmn";
174
175 if (sem)
176 sem_wait(sem);
177 fd = open("mountpoint/some_file.txt", O_RDWR);
178 if (fd < 0)
179 return (void*)(intptr_t)errno;
180
181 r = write(fd, BUF, sizeof(BUF));
182 if (r >= 0) {
183 LastLocalModify::leak(fd);
184 return 0;
185 } else
186 return (void*)(intptr_t)errno;
187 }
188
189 /*
190 * VOP_LOOKUP should discard attributes returned by the server if they were
191 * modified by another VOP while the VOP_LOOKUP was in progress.
192 *
193 * Sequence of operations:
194 * * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock
195 * exclusively.
196 * * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the
197 * server. The server replies with the old file length. Thread 2 blocks
198 * waiting for the vnode lock.
199 * * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the
200 * file's size and updates the attribute cache. Then it releases the vnode
201 * lock.
202 * * Thread 2 acquires the vnode lock. At this point it must not add the
203 * now-stale file size to the attribute cache.
204 *
205 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
206 */
TEST_P(LastLocalModify,lookup)207 TEST_P(LastLocalModify, lookup)
208 {
209 const char FULLPATH[] = "mountpoint/some_file.txt";
210 const char RELPATH[] = "some_file.txt";
211 Sequence seq;
212 uint64_t ino = 3;
213 uint64_t mutator_unique;
214 const uint64_t oldsize = 10;
215 const uint64_t newsize = 15;
216 pthread_t th0;
217 void *thr0_value;
218 struct stat sb;
219 static sem_t sem;
220 Mutator mutator;
221 uint32_t mutator_op;
222 size_t mutator_size;
223
224 mutator = writer_from_str(GetParam());
225 mutator_op = fuse_op_from_mutator(mutator);
226
227 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
228
229 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
230 .InSequence(seq)
231 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
232 /* Called by the mutator, caches attributes but not entries */
233 SET_OUT_HEADER_LEN(out, entry);
234 out.body.entry.nodeid = ino;
235 out.body.entry.attr.size = oldsize;
236 out.body.entry.nodeid = ino;
237 out.body.entry.attr_valid_nsec = NAP_NS / 2;
238 out.body.entry.attr.ino = ino;
239 out.body.entry.attr.mode = S_IFREG | 0644;
240 })));
241 expect_open(ino, 0, 1);
242 EXPECT_CALL(*m_mock, process(
243 ResultOf([=](auto in) {
244 return (in.header.opcode == mutator_op &&
245 in.header.nodeid == ino);
246 }, Eq(true)),
247 _)
248 ).InSequence(seq)
249 .WillOnce(Invoke([&](auto in, auto &out __unused) {
250 /*
251 * The mutator changes the file size, but in order to simulate
252 * a race, don't reply. Instead, just save the unique for
253 * later.
254 */
255 mutator_unique = in.header.unique;
256 switch(mutator) {
257 case VOP_WRITE:
258 mutator_size = in.body.write.size;
259 break;
260 case VOP_COPY_FILE_RANGE:
261 mutator_size = in.body.copy_file_range.len;
262 break;
263 default:
264 break;
265 }
266 /* Allow the lookup thread to proceed */
267 sem_post(&sem);
268 }));
269 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
270 .InSequence(seq)
271 .WillOnce(Invoke([&](auto in __unused, auto& out) {
272 std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
273 std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
274
275 /* First complete the lookup request, returning the old size */
276 out0->header.unique = in.header.unique;
277 SET_OUT_HEADER_LEN(*out0, entry);
278 out0->body.entry.attr.mode = S_IFREG | 0644;
279 out0->body.entry.nodeid = ino;
280 out0->body.entry.entry_valid = UINT64_MAX;
281 out0->body.entry.attr_valid = UINT64_MAX;
282 out0->body.entry.attr.size = oldsize;
283 out.push_back(std::move(out0));
284
285 /* Then, respond to the mutator request */
286 out1->header.unique = mutator_unique;
287 switch(mutator) {
288 case VOP_ALLOCATE:
289 out1->header.error = 0;
290 out1->header.len = sizeof(out1->header);
291 break;
292 case VOP_COPY_FILE_RANGE:
293 SET_OUT_HEADER_LEN(*out1, write);
294 out1->body.write.size = mutator_size;
295 break;
296 case VOP_SETATTR:
297 SET_OUT_HEADER_LEN(*out1, attr);
298 out1->body.attr.attr.ino = ino;
299 out1->body.attr.attr.mode = S_IFREG | 0644;
300 out1->body.attr.attr.size = newsize; // Changed size
301 out1->body.attr.attr_valid = UINT64_MAX;
302 break;
303 case VOP_WRITE:
304 SET_OUT_HEADER_LEN(*out1, write);
305 out1->body.write.size = mutator_size;
306 break;
307 }
308 out.push_back(std::move(out1));
309 }));
310
311 /* Start the mutator thread */
312 switch(mutator) {
313 case VOP_ALLOCATE:
314 ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
315 NULL)) << strerror(errno);
316 break;
317 case VOP_COPY_FILE_RANGE:
318 ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
319 NULL)) << strerror(errno);
320 break;
321 case VOP_SETATTR:
322 ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL))
323 << strerror(errno);
324 break;
325 case VOP_WRITE:
326 ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL))
327 << strerror(errno);
328 break;
329 }
330
331
332 /* Wait for FUSE_SETATTR to be sent */
333 sem_wait(&sem);
334
335 /* Lookup again, which will race with setattr */
336 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
337 ASSERT_EQ((off_t)newsize, sb.st_size);
338
339 /* ftruncate should've completed without error */
340 pthread_join(th0, &thr0_value);
341 EXPECT_EQ(0, (intptr_t)thr0_value);
342 }
343
344 /*
345 * VFS_VGET should discard attributes returned by the server if they were
346 * modified by another VOP while the VFS_VGET was in progress.
347 *
348 * Sequence of operations:
349 * * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP
350 * * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock
351 * exclusively and issues a FUSE op like FUSE_SETATTR.
352 * * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks
353 * waiting for the vnode lock.
354 * * Thread 2's FUSE op returns, and that thread sets the file's new size
355 * in the attribute cache. Finally it releases the vnode lock.
356 * * The vnode lock acquired, thread 1 must not overwrite the attr cache's size
357 * with the old value.
358 *
359 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
360 */
TEST_P(LastLocalModify,vfs_vget)361 TEST_P(LastLocalModify, vfs_vget)
362 {
363 const char FULLPATH[] = "mountpoint/some_file.txt";
364 const char RELPATH[] = "some_file.txt";
365 Sequence seq;
366 uint64_t ino = 3;
367 uint64_t lookup_unique;
368 const uint64_t oldsize = 10;
369 const uint64_t newsize = 15;
370 pthread_t th0;
371 void *thr0_value;
372 struct stat sb;
373 static sem_t sem;
374 fhandle_t fhp;
375 Mutator mutator;
376 uint32_t mutator_op;
377
378 if (geteuid() != 0)
379 GTEST_SKIP() << "This test requires a privileged user";
380
381 mutator = writer_from_str(GetParam());
382 mutator_op = fuse_op_from_mutator(mutator);
383
384 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
385
386 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
387 .Times(1)
388 .InSequence(seq)
389 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
390 {
391 /* Called by getfh, caches attributes but not entries */
392 SET_OUT_HEADER_LEN(out, entry);
393 out.body.entry.nodeid = ino;
394 out.body.entry.attr.size = oldsize;
395 out.body.entry.nodeid = ino;
396 out.body.entry.attr_valid_nsec = NAP_NS / 2;
397 out.body.entry.attr.ino = ino;
398 out.body.entry.attr.mode = S_IFREG | 0644;
399 })));
400 EXPECT_LOOKUP(ino, ".")
401 .InSequence(seq)
402 .WillOnce(Invoke([&](auto in, auto &out __unused) {
403 /* Called by fhstat. Block to simulate a race */
404 lookup_unique = in.header.unique;
405 sem_post(&sem);
406 }));
407
408 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
409 .Times(1)
410 .InSequence(seq)
411 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
412 {
413 /* Called by VOP_SETATTR, caches attributes but not entries */
414 SET_OUT_HEADER_LEN(out, entry);
415 out.body.entry.nodeid = ino;
416 out.body.entry.attr.size = oldsize;
417 out.body.entry.nodeid = ino;
418 out.body.entry.attr_valid_nsec = NAP_NS / 2;
419 out.body.entry.attr.ino = ino;
420 out.body.entry.attr.mode = S_IFREG | 0644;
421 })));
422
423 /* Called by the mutator thread */
424 expect_open(ino, 0, 1);
425
426 EXPECT_CALL(*m_mock, process(
427 ResultOf([=](auto in) {
428 return (in.header.opcode == mutator_op &&
429 in.header.nodeid == ino);
430 }, Eq(true)),
431 _)
432 ).InSequence(seq)
433 .WillOnce(Invoke([&](auto in __unused, auto& out) {
434 std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
435 std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
436
437 /* First complete the lookup request, returning the old size */
438 out0->header.unique = lookup_unique;
439 SET_OUT_HEADER_LEN(*out0, entry);
440 out0->body.entry.attr.mode = S_IFREG | 0644;
441 out0->body.entry.nodeid = ino;
442 out0->body.entry.entry_valid = UINT64_MAX;
443 out0->body.entry.attr_valid = UINT64_MAX;
444 out0->body.entry.attr.size = oldsize;
445 out.push_back(std::move(out0));
446
447 /* Then, respond to the mutator request */
448 out1->header.unique = in.header.unique;
449 switch(mutator) {
450 case VOP_ALLOCATE:
451 out1->header.error = 0;
452 out1->header.len = sizeof(out1->header);
453 break;
454 case VOP_COPY_FILE_RANGE:
455 SET_OUT_HEADER_LEN(*out1, write);
456 out1->body.write.size = in.body.copy_file_range.len;
457 break;
458 case VOP_SETATTR:
459 SET_OUT_HEADER_LEN(*out1, attr);
460 out1->body.attr.attr.ino = ino;
461 out1->body.attr.attr.mode = S_IFREG | 0644;
462 out1->body.attr.attr.size = newsize; // Changed size
463 out1->body.attr.attr_valid = UINT64_MAX;
464 break;
465 case VOP_WRITE:
466 SET_OUT_HEADER_LEN(*out1, write);
467 out1->body.write.size = in.body.write.size;
468 break;
469 }
470 out.push_back(std::move(out1));
471 }));
472
473 /* First get a file handle */
474 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
475
476 /* Start the mutator thread */
477 switch(mutator) {
478 case VOP_ALLOCATE:
479 ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
480 (void*)&sem)) << strerror(errno);
481 break;
482 case VOP_COPY_FILE_RANGE:
483 ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
484 (void*)&sem)) << strerror(errno);
485 break;
486 case VOP_SETATTR:
487 ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th,
488 (void*)&sem)) << strerror(errno);
489 break;
490 case VOP_WRITE:
491 ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem))
492 << strerror(errno);
493 break;
494 }
495
496 /* Lookup again, which will race with setattr */
497 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
498
499 ASSERT_EQ((off_t)newsize, sb.st_size);
500
501 /* mutator should've completed without error */
502 pthread_join(th0, &thr0_value);
503 EXPECT_EQ(0, (intptr_t)thr0_value);
504 }
505
506
507 INSTANTIATE_TEST_SUITE_P(LLM, LastLocalModify,
508 Values(
509 "VOP_ALLOCATE",
510 "VOP_COPY_FILE_RANGE",
511 "VOP_SETATTR",
512 "VOP_WRITE"
513 )
514 );
515