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 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 */ 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 */ 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