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 * $FreeBSD$ 28 */ 29 30 extern "C" { 31 #include <sys/param.h> 32 33 #include <fcntl.h> 34 } 35 36 #include "mockfs.hh" 37 #include "utils.hh" 38 39 using namespace testing; 40 41 class Lseek: public FuseTest {}; 42 class LseekPathconf: public Lseek {}; 43 class LseekPathconf_7_23: public LseekPathconf { 44 public: 45 virtual void SetUp() { 46 m_kernel_minor_version = 23; 47 FuseTest::SetUp(); 48 } 49 }; 50 class LseekSeekHole: public Lseek {}; 51 class LseekSeekData: public Lseek {}; 52 53 /* 54 * If a previous lseek operation has already returned enosys, then pathconf can 55 * return EINVAL immediately. 56 */ 57 TEST_F(LseekPathconf, already_enosys) 58 { 59 const char FULLPATH[] = "mountpoint/some_file.txt"; 60 const char RELPATH[] = "some_file.txt"; 61 const uint64_t ino = 42; 62 off_t fsize = 1 << 30; /* 1 GiB */ 63 off_t offset_in = 1 << 28; 64 int fd; 65 66 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 67 expect_open(ino, 0, 1); 68 EXPECT_CALL(*m_mock, process( 69 ResultOf([=](auto in) { 70 return (in.header.opcode == FUSE_LSEEK); 71 }, Eq(true)), 72 _) 73 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 74 75 fd = open(FULLPATH, O_RDONLY); 76 77 EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); 78 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 79 EXPECT_EQ(EINVAL, errno); 80 81 leak(fd); 82 } 83 84 /* 85 * If a previous lseek operation has already returned successfully, then 86 * pathconf can return 1 immediately. 1 means "holes are reported, but size is 87 * not specified". 88 */ 89 TEST_F(LseekPathconf, already_seeked) 90 { 91 const char FULLPATH[] = "mountpoint/some_file.txt"; 92 const char RELPATH[] = "some_file.txt"; 93 const uint64_t ino = 42; 94 off_t fsize = 1 << 30; /* 1 GiB */ 95 off_t offset = 1 << 28; 96 int fd; 97 98 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 99 expect_open(ino, 0, 1); 100 EXPECT_CALL(*m_mock, process( 101 ResultOf([=](auto in) { 102 return (in.header.opcode == FUSE_LSEEK); 103 }, Eq(true)), 104 _) 105 ).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) { 106 SET_OUT_HEADER_LEN(out, lseek); 107 out.body.lseek.offset = i.body.lseek.offset; 108 }))); 109 fd = open(FULLPATH, O_RDONLY); 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 * If no FUSE_LSEEK operation has been attempted since mount, try once as soon 119 * as a pathconf request comes in. 120 */ 121 TEST_F(LseekPathconf, enosys_now) 122 { 123 const char FULLPATH[] = "mountpoint/some_file.txt"; 124 const char RELPATH[] = "some_file.txt"; 125 const uint64_t ino = 42; 126 off_t fsize = 1 << 30; /* 1 GiB */ 127 int fd; 128 129 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 130 expect_open(ino, 0, 1); 131 EXPECT_CALL(*m_mock, process( 132 ResultOf([=](auto in) { 133 return (in.header.opcode == FUSE_LSEEK); 134 }, Eq(true)), 135 _) 136 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 137 138 fd = open(FULLPATH, O_RDONLY); 139 140 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 141 EXPECT_EQ(EINVAL, errno); 142 143 leak(fd); 144 } 145 146 /* 147 * If no FUSE_LSEEK operation has been attempted since mount, try one as soon 148 * as a pathconf request comes in. This is the typical pattern of bsdtar. It 149 * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported. 150 */ 151 TEST_F(LseekPathconf, seek_now) 152 { 153 const char FULLPATH[] = "mountpoint/some_file.txt"; 154 const char RELPATH[] = "some_file.txt"; 155 const uint64_t ino = 42; 156 off_t fsize = 1 << 30; /* 1 GiB */ 157 off_t offset_initial = 1 << 27; 158 off_t offset_out = 1 << 29; 159 int fd; 160 161 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 162 expect_open(ino, 0, 1); 163 EXPECT_CALL(*m_mock, process( 164 ResultOf([=](auto in) { 165 return (in.header.opcode == FUSE_LSEEK); 166 }, Eq(true)), 167 _) 168 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 169 SET_OUT_HEADER_LEN(out, lseek); 170 out.body.lseek.offset = offset_out; 171 }))); 172 173 fd = open(FULLPATH, O_RDONLY); 174 EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET)); 175 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 176 /* And check that the file pointer hasn't changed */ 177 EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR)); 178 179 leak(fd); 180 } 181 182 /* 183 * For servers using older protocol versions, no FUSE_LSEEK should be attempted 184 */ 185 TEST_F(LseekPathconf_7_23, already_enosys) 186 { 187 const char FULLPATH[] = "mountpoint/some_file.txt"; 188 const char RELPATH[] = "some_file.txt"; 189 const uint64_t ino = 42; 190 off_t fsize = 1 << 30; /* 1 GiB */ 191 int fd; 192 193 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 194 expect_open(ino, 0, 1); 195 EXPECT_CALL(*m_mock, process( 196 ResultOf([=](auto in) { 197 return (in.header.opcode == FUSE_LSEEK); 198 }, Eq(true)), 199 _) 200 ).Times(0); 201 202 fd = open(FULLPATH, O_RDONLY); 203 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 204 EXPECT_EQ(EINVAL, errno); 205 206 leak(fd); 207 } 208 209 TEST_F(LseekSeekData, ok) 210 { 211 const char FULLPATH[] = "mountpoint/some_file.txt"; 212 const char RELPATH[] = "some_file.txt"; 213 const uint64_t ino = 42; 214 off_t fsize = 1 << 30; /* 1 GiB */ 215 off_t offset_in = 1 << 28; 216 off_t offset_out = 1 << 29; 217 int fd; 218 219 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 220 expect_open(ino, 0, 1); 221 EXPECT_CALL(*m_mock, process( 222 ResultOf([=](auto in) { 223 return (in.header.opcode == FUSE_LSEEK && 224 in.header.nodeid == ino && 225 in.body.lseek.fh == FH && 226 (off_t)in.body.lseek.offset == offset_in && 227 in.body.lseek.whence == SEEK_DATA); 228 }, Eq(true)), 229 _) 230 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 231 SET_OUT_HEADER_LEN(out, lseek); 232 out.body.lseek.offset = offset_out; 233 }))); 234 fd = open(FULLPATH, O_RDONLY); 235 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA)); 236 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 237 238 leak(fd); 239 } 240 241 /* 242 * If the server returns ENOSYS, fusefs should fall back to the default 243 * behavior, and never query the server again. 244 */ 245 TEST_F(LseekSeekData, enosys) 246 { 247 const char FULLPATH[] = "mountpoint/some_file.txt"; 248 const char RELPATH[] = "some_file.txt"; 249 const uint64_t ino = 42; 250 off_t fsize = 1 << 30; /* 1 GiB */ 251 off_t offset_in = 1 << 28; 252 int fd; 253 254 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 255 expect_open(ino, 0, 1); 256 EXPECT_CALL(*m_mock, process( 257 ResultOf([=](auto in) { 258 return (in.header.opcode == FUSE_LSEEK && 259 in.header.nodeid == ino && 260 in.body.lseek.fh == FH && 261 (off_t)in.body.lseek.offset == offset_in && 262 in.body.lseek.whence == SEEK_DATA); 263 }, Eq(true)), 264 _) 265 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 266 fd = open(FULLPATH, O_RDONLY); 267 268 /* 269 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset 270 * otherwise. 271 */ 272 EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); 273 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 274 EXPECT_EQ(ENXIO, errno); 275 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 276 EXPECT_EQ(ENXIO, errno); 277 278 leak(fd); 279 } 280 281 TEST_F(LseekSeekHole, ok) 282 { 283 const char FULLPATH[] = "mountpoint/some_file.txt"; 284 const char RELPATH[] = "some_file.txt"; 285 const uint64_t ino = 42; 286 off_t fsize = 1 << 30; /* 1 GiB */ 287 off_t offset_in = 1 << 28; 288 off_t offset_out = 1 << 29; 289 int fd; 290 291 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 292 expect_open(ino, 0, 1); 293 EXPECT_CALL(*m_mock, process( 294 ResultOf([=](auto in) { 295 return (in.header.opcode == FUSE_LSEEK && 296 in.header.nodeid == ino && 297 in.body.lseek.fh == FH && 298 (off_t)in.body.lseek.offset == offset_in && 299 in.body.lseek.whence == SEEK_HOLE); 300 }, Eq(true)), 301 _) 302 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 303 SET_OUT_HEADER_LEN(out, lseek); 304 out.body.lseek.offset = offset_out; 305 }))); 306 fd = open(FULLPATH, O_RDONLY); 307 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE)); 308 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 309 310 leak(fd); 311 } 312 313 /* 314 * If the server returns ENOSYS, fusefs should fall back to the default 315 * behavior, and never query the server again. 316 */ 317 TEST_F(LseekSeekHole, enosys) 318 { 319 const char FULLPATH[] = "mountpoint/some_file.txt"; 320 const char RELPATH[] = "some_file.txt"; 321 const uint64_t ino = 42; 322 off_t fsize = 1 << 30; /* 1 GiB */ 323 off_t offset_in = 1 << 28; 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 in.header.nodeid == ino && 332 in.body.lseek.fh == FH && 333 (off_t)in.body.lseek.offset == offset_in && 334 in.body.lseek.whence == SEEK_HOLE); 335 }, Eq(true)), 336 _) 337 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 338 fd = open(FULLPATH, O_RDONLY); 339 340 /* 341 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize 342 * otherwise. 343 */ 344 EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE)); 345 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 346 EXPECT_EQ(ENXIO, errno); 347 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 348 EXPECT_EQ(ENXIO, errno); 349 350 leak(fd); 351 } 352 353 /* lseek should return ENXIO when offset points to EOF */ 354 TEST_F(LseekSeekHole, enxio) 355 { 356 const char FULLPATH[] = "mountpoint/some_file.txt"; 357 const char RELPATH[] = "some_file.txt"; 358 const uint64_t ino = 42; 359 off_t fsize = 1 << 30; /* 1 GiB */ 360 off_t offset_in = fsize; 361 int fd; 362 363 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 364 expect_open(ino, 0, 1); 365 EXPECT_CALL(*m_mock, process( 366 ResultOf([=](auto in) { 367 return (in.header.opcode == FUSE_LSEEK && 368 in.header.nodeid == ino && 369 in.body.lseek.fh == FH && 370 (off_t)in.body.lseek.offset == offset_in && 371 in.body.lseek.whence == SEEK_HOLE); 372 }, Eq(true)), 373 _) 374 ).WillOnce(Invoke(ReturnErrno(ENXIO))); 375 fd = open(FULLPATH, O_RDONLY); 376 EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE)); 377 EXPECT_EQ(ENXIO, errno); 378 379 leak(fd); 380 } 381