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 */ 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 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: 100 virtual void SetUp() { 101 m_init_flags = FUSE_EXPORT_SUPPORT; 102 103 FuseTest::SetUp(); 104 } 105 }; 106 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 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 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 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 */ 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 */ 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