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 /*
178 * Open the file in direct mode.
179 * The race condition affects both direct and non-direct writes, and
180 * they have separate code paths. However, in the non-direct case, the
181 * kernel updates last_local_modify _before_ sending FUSE_WRITE to the
182 * server. So the technique that this test program uses to invoke the
183 * race cannot work. Therefore, test with O_DIRECT only.
184 */
185 fd = open("mountpoint/some_file.txt", O_RDWR | O_DIRECT);
186 if (fd < 0)
187 return (void*)(intptr_t)errno;
188
189 r = write(fd, BUF, sizeof(BUF));
190 if (r >= 0) {
191 LastLocalModify::leak(fd);
192 return 0;
193 } else
194 return (void*)(intptr_t)errno;
195 }
196
197 /*
198 * VOP_LOOKUP should discard attributes returned by the server if they were
199 * modified by another VOP while the VOP_LOOKUP was in progress.
200 *
201 * Sequence of operations:
202 * * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock
203 * exclusively.
204 * * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the
205 * server. The server replies with the old file length. Thread 2 blocks
206 * waiting for the vnode lock.
207 * * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the
208 * file's size and updates the attribute cache. Then it releases the vnode
209 * lock.
210 * * Thread 2 acquires the vnode lock. At this point it must not add the
211 * now-stale file size to the attribute cache.
212 *
213 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
214 */
TEST_P(LastLocalModify,lookup)215 TEST_P(LastLocalModify, lookup)
216 {
217 const char FULLPATH[] = "mountpoint/some_file.txt";
218 const char RELPATH[] = "some_file.txt";
219 Sequence seq;
220 uint64_t ino = 3;
221 uint64_t mutator_unique;
222 const uint64_t oldsize = 10;
223 const uint64_t newsize = 15;
224 pthread_t th0;
225 void *thr0_value;
226 struct stat sb;
227 static sem_t sem;
228 Mutator mutator;
229 uint32_t mutator_op;
230 size_t mutator_size;
231
232 mutator = writer_from_str(GetParam());
233 mutator_op = fuse_op_from_mutator(mutator);
234
235 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
236
237 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
238 .InSequence(seq)
239 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
240 /* Called by the mutator, caches attributes but not entries */
241 SET_OUT_HEADER_LEN(out, entry);
242 out.body.entry.nodeid = ino;
243 out.body.entry.attr.size = oldsize;
244 out.body.entry.attr_valid_nsec = NAP_NS / 2;
245 out.body.entry.attr.ino = ino;
246 out.body.entry.attr.mode = S_IFREG | 0644;
247 })));
248 expect_open(ino, 0, 1);
249 EXPECT_CALL(*m_mock, process(
250 ResultOf([=](auto in) {
251 return (in.header.opcode == mutator_op &&
252 in.header.nodeid == ino);
253 }, Eq(true)),
254 _)
255 ).InSequence(seq)
256 .WillOnce(Invoke([&](auto in, auto &out __unused) {
257 /*
258 * The mutator changes the file size, but in order to simulate
259 * a race, don't reply. Instead, just save the unique for
260 * later.
261 */
262 mutator_unique = in.header.unique;
263 switch(mutator) {
264 case VOP_WRITE:
265 mutator_size = in.body.write.size;
266 break;
267 case VOP_COPY_FILE_RANGE:
268 mutator_size = in.body.copy_file_range.len;
269 break;
270 default:
271 break;
272 }
273 /* Allow the lookup thread to proceed */
274 sem_post(&sem);
275 }));
276 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
277 .InSequence(seq)
278 .WillOnce(Invoke([&](auto in __unused, auto& out) {
279 std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
280 std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
281
282 /* First complete the lookup request, returning the old size */
283 out0->header.unique = in.header.unique;
284 SET_OUT_HEADER_LEN(*out0, entry);
285 out0->body.entry.attr.mode = S_IFREG | 0644;
286 out0->body.entry.nodeid = ino;
287 out0->body.entry.attr.ino = ino;
288 out0->body.entry.entry_valid = UINT64_MAX;
289 out0->body.entry.attr_valid = UINT64_MAX;
290 out0->body.entry.attr.size = oldsize;
291 out.push_back(std::move(out0));
292
293 /* Then, respond to the mutator request */
294 out1->header.unique = mutator_unique;
295 switch(mutator) {
296 case VOP_ALLOCATE:
297 out1->header.error = 0;
298 out1->header.len = sizeof(out1->header);
299 break;
300 case VOP_COPY_FILE_RANGE:
301 SET_OUT_HEADER_LEN(*out1, write);
302 out1->body.write.size = mutator_size;
303 break;
304 case VOP_SETATTR:
305 SET_OUT_HEADER_LEN(*out1, attr);
306 out1->body.attr.attr.ino = ino;
307 out1->body.attr.attr.mode = S_IFREG | 0644;
308 out1->body.attr.attr.size = newsize; // Changed size
309 out1->body.attr.attr_valid = UINT64_MAX;
310 break;
311 case VOP_WRITE:
312 SET_OUT_HEADER_LEN(*out1, write);
313 out1->body.write.size = mutator_size;
314 break;
315 }
316 out.push_back(std::move(out1));
317 }));
318
319 /* Start the mutator thread */
320 switch(mutator) {
321 case VOP_ALLOCATE:
322 ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
323 NULL)) << strerror(errno);
324 break;
325 case VOP_COPY_FILE_RANGE:
326 ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
327 NULL)) << strerror(errno);
328 break;
329 case VOP_SETATTR:
330 ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL))
331 << strerror(errno);
332 break;
333 case VOP_WRITE:
334 ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL))
335 << strerror(errno);
336 break;
337 }
338
339
340 /* Wait for FUSE_SETATTR to be sent */
341 sem_wait(&sem);
342
343 /* Lookup again, which will race with the mutator */
344 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
345 ASSERT_EQ((off_t)newsize, sb.st_size);
346
347 /* ftruncate should've completed without error */
348 pthread_join(th0, &thr0_value);
349 EXPECT_EQ(0, (intptr_t)thr0_value);
350 }
351
352 /*
353 * VFS_VGET should discard attributes returned by the server if they were
354 * modified by another VOP while the VFS_VGET was in progress.
355 *
356 * Sequence of operations:
357 * * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP
358 * * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock
359 * exclusively and issues a FUSE op like FUSE_SETATTR.
360 * * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks
361 * waiting for the vnode lock.
362 * * Thread 2's FUSE op returns, and that thread sets the file's new size
363 * in the attribute cache. Finally it releases the vnode lock.
364 * * The vnode lock acquired, thread 1 must not overwrite the attr cache's size
365 * with the old value.
366 *
367 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
368 */
TEST_P(LastLocalModify,vfs_vget)369 TEST_P(LastLocalModify, vfs_vget)
370 {
371 const char FULLPATH[] = "mountpoint/some_file.txt";
372 const char RELPATH[] = "some_file.txt";
373 Sequence seq;
374 uint64_t ino = 3;
375 uint64_t lookup_unique;
376 const uint64_t oldsize = 10;
377 const uint64_t newsize = 15;
378 pthread_t th0;
379 void *thr0_value;
380 struct stat sb;
381 static sem_t sem;
382 fhandle_t fhp;
383 Mutator mutator;
384 uint32_t mutator_op;
385
386 if (geteuid() != 0)
387 GTEST_SKIP() << "This test requires a privileged user";
388
389 mutator = writer_from_str(GetParam());
390 mutator_op = fuse_op_from_mutator(mutator);
391
392 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
393
394 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
395 .Times(1)
396 .InSequence(seq)
397 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
398 {
399 /* Called by getfh, caches attributes but not entries */
400 SET_OUT_HEADER_LEN(out, entry);
401 out.body.entry.nodeid = ino;
402 out.body.entry.attr.size = oldsize;
403 out.body.entry.attr_valid_nsec = NAP_NS / 2;
404 out.body.entry.attr.ino = ino;
405 out.body.entry.attr.mode = S_IFREG | 0644;
406 })));
407 EXPECT_LOOKUP(ino, ".")
408 .InSequence(seq)
409 .WillOnce(Invoke([&](auto in, auto &out __unused) {
410 /* Called by fhstat. Block to simulate a race */
411 lookup_unique = in.header.unique;
412 sem_post(&sem);
413 }));
414
415 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
416 .Times(1)
417 .InSequence(seq)
418 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
419 {
420 /* Called by VOP_SETATTR, caches attributes but not entries */
421 SET_OUT_HEADER_LEN(out, entry);
422 out.body.entry.nodeid = ino;
423 out.body.entry.attr.size = oldsize;
424 out.body.entry.attr_valid_nsec = NAP_NS / 2;
425 out.body.entry.attr.ino = ino;
426 out.body.entry.attr.mode = S_IFREG | 0644;
427 })));
428
429 /* Called by the mutator thread */
430 expect_open(ino, 0, 1);
431
432 EXPECT_CALL(*m_mock, process(
433 ResultOf([=](auto in) {
434 return (in.header.opcode == mutator_op &&
435 in.header.nodeid == ino);
436 }, Eq(true)),
437 _)
438 ).InSequence(seq)
439 .WillOnce(Invoke([&](auto in __unused, auto& out) {
440 std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
441 std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
442
443 /* First complete the lookup request, returning the old size */
444 out0->header.unique = lookup_unique;
445 SET_OUT_HEADER_LEN(*out0, entry);
446 out0->body.entry.attr.mode = S_IFREG | 0644;
447 out0->body.entry.nodeid = ino;
448 out0->body.entry.attr.ino = ino;
449 out0->body.entry.entry_valid = UINT64_MAX;
450 out0->body.entry.attr_valid = UINT64_MAX;
451 out0->body.entry.attr.size = oldsize;
452 out.push_back(std::move(out0));
453
454 /* Then, respond to the mutator request */
455 out1->header.unique = in.header.unique;
456 switch(mutator) {
457 case VOP_ALLOCATE:
458 out1->header.error = 0;
459 out1->header.len = sizeof(out1->header);
460 break;
461 case VOP_COPY_FILE_RANGE:
462 SET_OUT_HEADER_LEN(*out1, write);
463 out1->body.write.size = in.body.copy_file_range.len;
464 break;
465 case VOP_SETATTR:
466 SET_OUT_HEADER_LEN(*out1, attr);
467 out1->body.attr.attr.ino = ino;
468 out1->body.attr.attr.mode = S_IFREG | 0644;
469 out1->body.attr.attr.size = newsize; // Changed size
470 out1->body.attr.attr_valid = UINT64_MAX;
471 break;
472 case VOP_WRITE:
473 SET_OUT_HEADER_LEN(*out1, write);
474 out1->body.write.size = in.body.write.size;
475 break;
476 }
477 out.push_back(std::move(out1));
478 }));
479
480 /* First get a file handle */
481 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
482
483 /* Start the mutator thread */
484 switch(mutator) {
485 case VOP_ALLOCATE:
486 ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
487 (void*)&sem)) << strerror(errno);
488 break;
489 case VOP_COPY_FILE_RANGE:
490 ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
491 (void*)&sem)) << strerror(errno);
492 break;
493 case VOP_SETATTR:
494 ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th,
495 (void*)&sem)) << strerror(errno);
496 break;
497 case VOP_WRITE:
498 ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem))
499 << strerror(errno);
500 break;
501 }
502
503 /* Lookup again, which will race with setattr */
504 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
505
506 ASSERT_EQ((off_t)newsize, sb.st_size);
507
508 /* mutator should've completed without error */
509 pthread_join(th0, &thr0_value);
510 EXPECT_EQ(0, (intptr_t)thr0_value);
511 }
512
513
514 INSTANTIATE_TEST_SUITE_P(LLM, LastLocalModify,
515 Values(
516 "VOP_ALLOCATE",
517 "VOP_COPY_FILE_RANGE",
518 "VOP_SETATTR",
519 "VOP_WRITE"
520 )
521 );
522