1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2020 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 31 #include <fcntl.h> 32 } 33 34 #include "mockfs.hh" 35 #include "utils.hh" 36 37 using namespace testing; 38 39 class Lseek: public FuseTest {}; 40 class LseekPathconf: public Lseek {}; 41 class LseekPathconf_7_23: public LseekPathconf { 42 public: 43 virtual void SetUp() { 44 m_kernel_minor_version = 23; 45 FuseTest::SetUp(); 46 } 47 }; 48 class LseekSeekHole: public Lseek {}; 49 class LseekSeekData: public Lseek {}; 50 51 /* 52 * If a previous lseek operation has already returned enosys, then pathconf can 53 * return EINVAL immediately. 54 */ 55 TEST_F(LseekPathconf, already_enosys) 56 { 57 const char FULLPATH[] = "mountpoint/some_file.txt"; 58 const char RELPATH[] = "some_file.txt"; 59 const uint64_t ino = 42; 60 off_t fsize = 1 << 30; /* 1 GiB */ 61 off_t offset_in = 1 << 28; 62 int fd; 63 64 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 65 expect_open(ino, 0, 1); 66 EXPECT_CALL(*m_mock, process( 67 ResultOf([=](auto in) { 68 return (in.header.opcode == FUSE_LSEEK); 69 }, Eq(true)), 70 _) 71 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 72 73 fd = open(FULLPATH, O_RDONLY); 74 ASSERT_LE(0, fd); 75 76 EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); 77 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 78 EXPECT_EQ(EINVAL, errno); 79 80 leak(fd); 81 } 82 83 /* 84 * If a previous lseek operation has already returned successfully, then 85 * pathconf can return 1 immediately. 1 means "holes are reported, but size is 86 * not specified". 87 */ 88 TEST_F(LseekPathconf, already_seeked) 89 { 90 const char FULLPATH[] = "mountpoint/some_file.txt"; 91 const char RELPATH[] = "some_file.txt"; 92 const uint64_t ino = 42; 93 off_t fsize = 1 << 30; /* 1 GiB */ 94 off_t offset = 1 << 28; 95 int fd; 96 97 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 98 expect_open(ino, 0, 1); 99 EXPECT_CALL(*m_mock, process( 100 ResultOf([=](auto in) { 101 return (in.header.opcode == FUSE_LSEEK); 102 }, Eq(true)), 103 _) 104 ).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) { 105 SET_OUT_HEADER_LEN(out, lseek); 106 out.body.lseek.offset = i.body.lseek.offset; 107 }))); 108 fd = open(FULLPATH, O_RDONLY); 109 ASSERT_LE(0, fd); 110 EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA)); 111 112 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 113 114 leak(fd); 115 } 116 117 /* 118 * Use pathconf on a file not already opened. The server returns EACCES when 119 * the kernel tries to open it. The kernel should return EACCES, and make no 120 * judgement about whether the server does or does not support FUSE_LSEEK. 121 */ 122 TEST_F(LseekPathconf, eacces) 123 { 124 const char FULLPATH[] = "mountpoint/some_file.txt"; 125 const char RELPATH[] = "some_file.txt"; 126 const uint64_t ino = 42; 127 off_t fsize = 1 << 30; /* 1 GiB */ 128 129 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 130 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 131 SET_OUT_HEADER_LEN(out, entry); 132 out.body.entry.entry_valid = UINT64_MAX; 133 out.body.entry.attr.mode = S_IFREG | 0644; 134 out.body.entry.nodeid = ino; 135 out.body.entry.attr.size = fsize; 136 }))); 137 EXPECT_CALL(*m_mock, process( 138 ResultOf([=](auto in) { 139 return (in.header.opcode == FUSE_OPEN && 140 in.header.nodeid == ino); 141 }, Eq(true)), 142 _) 143 ).Times(2) 144 .WillRepeatedly(Invoke(ReturnErrno(EACCES))); 145 146 EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)); 147 EXPECT_EQ(EACCES, errno); 148 /* Check again, to ensure that the kernel didn't record the response */ 149 EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)); 150 EXPECT_EQ(EACCES, errno); 151 } 152 153 /* 154 * If the server returns some weird error when we try FUSE_LSEEK, send that to 155 * the caller but don't record the answer. 156 */ 157 TEST_F(LseekPathconf, eio) 158 { 159 const char FULLPATH[] = "mountpoint/some_file.txt"; 160 const char RELPATH[] = "some_file.txt"; 161 const uint64_t ino = 42; 162 off_t fsize = 1 << 30; /* 1 GiB */ 163 int fd; 164 165 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 166 expect_open(ino, 0, 1); 167 EXPECT_CALL(*m_mock, process( 168 ResultOf([=](auto in) { 169 return (in.header.opcode == FUSE_LSEEK); 170 }, Eq(true)), 171 _) 172 ).Times(2) 173 .WillRepeatedly(Invoke(ReturnErrno(EIO))); 174 175 fd = open(FULLPATH, O_RDONLY); 176 ASSERT_LE(0, fd); 177 178 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 179 EXPECT_EQ(EIO, errno); 180 /* Check again, to ensure that the kernel didn't record the response */ 181 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 182 EXPECT_EQ(EIO, errno); 183 184 leak(fd); 185 } 186 187 /* 188 * If no FUSE_LSEEK operation has been attempted since mount, try once as soon 189 * as a pathconf request comes in. 190 */ 191 TEST_F(LseekPathconf, enosys_now) 192 { 193 const char FULLPATH[] = "mountpoint/some_file.txt"; 194 const char RELPATH[] = "some_file.txt"; 195 const uint64_t ino = 42; 196 off_t fsize = 1 << 30; /* 1 GiB */ 197 int fd; 198 199 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 200 expect_open(ino, 0, 1); 201 EXPECT_CALL(*m_mock, process( 202 ResultOf([=](auto in) { 203 return (in.header.opcode == FUSE_LSEEK); 204 }, Eq(true)), 205 _) 206 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 207 208 fd = open(FULLPATH, O_RDONLY); 209 ASSERT_LE(0, fd); 210 211 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 212 EXPECT_EQ(EINVAL, errno); 213 214 leak(fd); 215 } 216 217 /* 218 * Use pathconf, rather than fpathconf, on a file not already opened. 219 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278135 220 */ 221 TEST_F(LseekPathconf, pathconf) 222 { 223 const char FULLPATH[] = "mountpoint/some_file.txt"; 224 const char RELPATH[] = "some_file.txt"; 225 const uint64_t ino = 42; 226 off_t fsize = 1 << 30; /* 1 GiB */ 227 off_t offset_out = 1 << 29; 228 229 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 230 expect_open(ino, 0, 1); 231 EXPECT_CALL(*m_mock, process( 232 ResultOf([=](auto in) { 233 return (in.header.opcode == FUSE_LSEEK); 234 }, Eq(true)), 235 _) 236 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 237 SET_OUT_HEADER_LEN(out, lseek); 238 out.body.lseek.offset = offset_out; 239 }))); 240 expect_release(ino, FuseTest::FH); 241 242 EXPECT_EQ(1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)) << strerror(errno); 243 } 244 245 /* 246 * If no FUSE_LSEEK operation has been attempted since mount, try one as soon 247 * as a pathconf request comes in. This is the typical pattern of bsdtar. It 248 * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported. 249 */ 250 TEST_F(LseekPathconf, seek_now) 251 { 252 const char FULLPATH[] = "mountpoint/some_file.txt"; 253 const char RELPATH[] = "some_file.txt"; 254 const uint64_t ino = 42; 255 off_t fsize = 1 << 30; /* 1 GiB */ 256 off_t offset_initial = 1 << 27; 257 off_t offset_out = 1 << 29; 258 int fd; 259 260 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 261 expect_open(ino, 0, 1); 262 EXPECT_CALL(*m_mock, process( 263 ResultOf([=](auto in) { 264 return (in.header.opcode == FUSE_LSEEK); 265 }, Eq(true)), 266 _) 267 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 268 SET_OUT_HEADER_LEN(out, lseek); 269 out.body.lseek.offset = offset_out; 270 }))); 271 272 fd = open(FULLPATH, O_RDONLY); 273 ASSERT_LE(0, fd); 274 EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET)); 275 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 276 /* And check that the file pointer hasn't changed */ 277 EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR)); 278 279 leak(fd); 280 } 281 282 /* 283 * If the user calls pathconf(_, _PC_MIN_HOLE_SIZE) on a fully sparse or 284 * zero-length file, then SEEK_DATA will return ENXIO. That should be 285 * interpreted as success. 286 */ 287 TEST_F(LseekPathconf, zerolength) 288 { 289 const char FULLPATH[] = "mountpoint/some_file.txt"; 290 const char RELPATH[] = "some_file.txt"; 291 const uint64_t ino = 42; 292 off_t fsize = 0; 293 int fd; 294 295 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 296 expect_open(ino, 0, 1); 297 EXPECT_CALL(*m_mock, process( 298 ResultOf([=](auto in) { 299 return (in.header.opcode == FUSE_LSEEK && 300 in.header.nodeid == ino && 301 in.body.lseek.whence == SEEK_DATA); 302 }, Eq(true)), 303 _) 304 ).WillOnce(Invoke(ReturnErrno(ENXIO))); 305 306 fd = open(FULLPATH, O_RDONLY); 307 ASSERT_LE(0, fd); 308 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 309 /* Check again, to ensure that the kernel recorded the response */ 310 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 311 312 leak(fd); 313 } 314 315 /* 316 * For servers using older protocol versions, no FUSE_LSEEK should be attempted 317 */ 318 TEST_F(LseekPathconf_7_23, already_enosys) 319 { 320 const char FULLPATH[] = "mountpoint/some_file.txt"; 321 const char RELPATH[] = "some_file.txt"; 322 const uint64_t ino = 42; 323 off_t fsize = 1 << 30; /* 1 GiB */ 324 int fd; 325 326 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 327 expect_open(ino, 0, 1); 328 EXPECT_CALL(*m_mock, process( 329 ResultOf([=](auto in) { 330 return (in.header.opcode == FUSE_LSEEK); 331 }, Eq(true)), 332 _) 333 ).Times(0); 334 335 fd = open(FULLPATH, O_RDONLY); 336 ASSERT_LE(0, fd); 337 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 338 EXPECT_EQ(EINVAL, errno); 339 340 leak(fd); 341 } 342 343 TEST_F(LseekSeekData, ok) 344 { 345 const char FULLPATH[] = "mountpoint/some_file.txt"; 346 const char RELPATH[] = "some_file.txt"; 347 const uint64_t ino = 42; 348 off_t fsize = 1 << 30; /* 1 GiB */ 349 off_t offset_in = 1 << 28; 350 off_t offset_out = 1 << 29; 351 int fd; 352 353 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 354 expect_open(ino, 0, 1); 355 EXPECT_CALL(*m_mock, process( 356 ResultOf([=](auto in) { 357 return (in.header.opcode == FUSE_LSEEK && 358 in.header.nodeid == ino && 359 in.body.lseek.fh == FH && 360 (off_t)in.body.lseek.offset == offset_in && 361 in.body.lseek.whence == SEEK_DATA); 362 }, Eq(true)), 363 _) 364 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 365 SET_OUT_HEADER_LEN(out, lseek); 366 out.body.lseek.offset = offset_out; 367 }))); 368 fd = open(FULLPATH, O_RDONLY); 369 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA)); 370 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 371 372 leak(fd); 373 } 374 375 /* 376 * If the server returns ENOSYS, fusefs should fall back to the default 377 * behavior, and never query the server again. 378 */ 379 TEST_F(LseekSeekData, enosys) 380 { 381 const char FULLPATH[] = "mountpoint/some_file.txt"; 382 const char RELPATH[] = "some_file.txt"; 383 const uint64_t ino = 42; 384 off_t fsize = 1 << 30; /* 1 GiB */ 385 off_t offset_in = 1 << 28; 386 int fd; 387 388 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 389 expect_open(ino, 0, 1); 390 EXPECT_CALL(*m_mock, process( 391 ResultOf([=](auto in) { 392 return (in.header.opcode == FUSE_LSEEK && 393 in.header.nodeid == ino && 394 in.body.lseek.fh == FH && 395 (off_t)in.body.lseek.offset == offset_in && 396 in.body.lseek.whence == SEEK_DATA); 397 }, Eq(true)), 398 _) 399 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 400 fd = open(FULLPATH, O_RDONLY); 401 ASSERT_LE(0, fd); 402 403 /* 404 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset 405 * otherwise. 406 */ 407 EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); 408 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 409 EXPECT_EQ(ENXIO, errno); 410 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 411 EXPECT_EQ(ENXIO, errno); 412 413 leak(fd); 414 } 415 416 TEST_F(LseekSeekHole, ok) 417 { 418 const char FULLPATH[] = "mountpoint/some_file.txt"; 419 const char RELPATH[] = "some_file.txt"; 420 const uint64_t ino = 42; 421 off_t fsize = 1 << 30; /* 1 GiB */ 422 off_t offset_in = 1 << 28; 423 off_t offset_out = 1 << 29; 424 int fd; 425 426 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 427 expect_open(ino, 0, 1); 428 EXPECT_CALL(*m_mock, process( 429 ResultOf([=](auto in) { 430 return (in.header.opcode == FUSE_LSEEK && 431 in.header.nodeid == ino && 432 in.body.lseek.fh == FH && 433 (off_t)in.body.lseek.offset == offset_in && 434 in.body.lseek.whence == SEEK_HOLE); 435 }, Eq(true)), 436 _) 437 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 438 SET_OUT_HEADER_LEN(out, lseek); 439 out.body.lseek.offset = offset_out; 440 }))); 441 fd = open(FULLPATH, O_RDONLY); 442 ASSERT_LE(0, fd); 443 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE)); 444 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 445 446 leak(fd); 447 } 448 449 /* 450 * If the server returns ENOSYS, fusefs should fall back to the default 451 * behavior, and never query the server again. 452 */ 453 TEST_F(LseekSeekHole, enosys) 454 { 455 const char FULLPATH[] = "mountpoint/some_file.txt"; 456 const char RELPATH[] = "some_file.txt"; 457 const uint64_t ino = 42; 458 off_t fsize = 1 << 30; /* 1 GiB */ 459 off_t offset_in = 1 << 28; 460 int fd; 461 462 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 463 expect_open(ino, 0, 1); 464 EXPECT_CALL(*m_mock, process( 465 ResultOf([=](auto in) { 466 return (in.header.opcode == FUSE_LSEEK && 467 in.header.nodeid == ino && 468 in.body.lseek.fh == FH && 469 (off_t)in.body.lseek.offset == offset_in && 470 in.body.lseek.whence == SEEK_HOLE); 471 }, Eq(true)), 472 _) 473 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 474 fd = open(FULLPATH, O_RDONLY); 475 ASSERT_LE(0, fd); 476 477 /* 478 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize 479 * otherwise. 480 */ 481 EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE)); 482 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 483 EXPECT_EQ(ENXIO, errno); 484 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 485 EXPECT_EQ(ENXIO, errno); 486 487 leak(fd); 488 } 489 490 /* lseek should return ENXIO when offset points to EOF */ 491 TEST_F(LseekSeekHole, enxio) 492 { 493 const char FULLPATH[] = "mountpoint/some_file.txt"; 494 const char RELPATH[] = "some_file.txt"; 495 const uint64_t ino = 42; 496 off_t fsize = 1 << 30; /* 1 GiB */ 497 off_t offset_in = fsize; 498 int fd; 499 500 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 501 expect_open(ino, 0, 1); 502 EXPECT_CALL(*m_mock, process( 503 ResultOf([=](auto in) { 504 return (in.header.opcode == FUSE_LSEEK && 505 in.header.nodeid == ino && 506 in.body.lseek.fh == FH && 507 (off_t)in.body.lseek.offset == offset_in && 508 in.body.lseek.whence == SEEK_HOLE); 509 }, Eq(true)), 510 _) 511 ).WillOnce(Invoke(ReturnErrno(ENXIO))); 512 fd = open(FULLPATH, O_RDONLY); 513 ASSERT_LE(0, fd); 514 EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE)); 515 EXPECT_EQ(ENXIO, errno); 516 517 leak(fd); 518 } 519