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