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