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 * If no FUSE_LSEEK operation has been attempted since mount, try once as soon 117 * as a pathconf request comes in. 118 */ 119 TEST_F(LseekPathconf, enosys_now) 120 { 121 const char FULLPATH[] = "mountpoint/some_file.txt"; 122 const char RELPATH[] = "some_file.txt"; 123 const uint64_t ino = 42; 124 off_t fsize = 1 << 30; /* 1 GiB */ 125 int fd; 126 127 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 128 expect_open(ino, 0, 1); 129 EXPECT_CALL(*m_mock, process( 130 ResultOf([=](auto in) { 131 return (in.header.opcode == FUSE_LSEEK); 132 }, Eq(true)), 133 _) 134 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 135 136 fd = open(FULLPATH, O_RDONLY); 137 138 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 139 EXPECT_EQ(EINVAL, errno); 140 141 leak(fd); 142 } 143 144 /* 145 * If no FUSE_LSEEK operation has been attempted since mount, try one as soon 146 * as a pathconf request comes in. This is the typical pattern of bsdtar. It 147 * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported. 148 */ 149 TEST_F(LseekPathconf, seek_now) 150 { 151 const char FULLPATH[] = "mountpoint/some_file.txt"; 152 const char RELPATH[] = "some_file.txt"; 153 const uint64_t ino = 42; 154 off_t fsize = 1 << 30; /* 1 GiB */ 155 off_t offset_initial = 1 << 27; 156 off_t offset_out = 1 << 29; 157 int fd; 158 159 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 160 expect_open(ino, 0, 1); 161 EXPECT_CALL(*m_mock, process( 162 ResultOf([=](auto in) { 163 return (in.header.opcode == FUSE_LSEEK); 164 }, Eq(true)), 165 _) 166 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 167 SET_OUT_HEADER_LEN(out, lseek); 168 out.body.lseek.offset = offset_out; 169 }))); 170 171 fd = open(FULLPATH, O_RDONLY); 172 EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET)); 173 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 174 /* And check that the file pointer hasn't changed */ 175 EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR)); 176 177 leak(fd); 178 } 179 180 /* 181 * For servers using older protocol versions, no FUSE_LSEEK should be attempted 182 */ 183 TEST_F(LseekPathconf_7_23, already_enosys) 184 { 185 const char FULLPATH[] = "mountpoint/some_file.txt"; 186 const char RELPATH[] = "some_file.txt"; 187 const uint64_t ino = 42; 188 off_t fsize = 1 << 30; /* 1 GiB */ 189 int fd; 190 191 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 192 expect_open(ino, 0, 1); 193 EXPECT_CALL(*m_mock, process( 194 ResultOf([=](auto in) { 195 return (in.header.opcode == FUSE_LSEEK); 196 }, Eq(true)), 197 _) 198 ).Times(0); 199 200 fd = open(FULLPATH, O_RDONLY); 201 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 202 EXPECT_EQ(EINVAL, errno); 203 204 leak(fd); 205 } 206 207 TEST_F(LseekSeekData, ok) 208 { 209 const char FULLPATH[] = "mountpoint/some_file.txt"; 210 const char RELPATH[] = "some_file.txt"; 211 const uint64_t ino = 42; 212 off_t fsize = 1 << 30; /* 1 GiB */ 213 off_t offset_in = 1 << 28; 214 off_t offset_out = 1 << 29; 215 int fd; 216 217 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 218 expect_open(ino, 0, 1); 219 EXPECT_CALL(*m_mock, process( 220 ResultOf([=](auto in) { 221 return (in.header.opcode == FUSE_LSEEK && 222 in.header.nodeid == ino && 223 in.body.lseek.fh == FH && 224 (off_t)in.body.lseek.offset == offset_in && 225 in.body.lseek.whence == SEEK_DATA); 226 }, Eq(true)), 227 _) 228 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 229 SET_OUT_HEADER_LEN(out, lseek); 230 out.body.lseek.offset = offset_out; 231 }))); 232 fd = open(FULLPATH, O_RDONLY); 233 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA)); 234 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 235 236 leak(fd); 237 } 238 239 /* 240 * If the server returns ENOSYS, fusefs should fall back to the default 241 * behavior, and never query the server again. 242 */ 243 TEST_F(LseekSeekData, enosys) 244 { 245 const char FULLPATH[] = "mountpoint/some_file.txt"; 246 const char RELPATH[] = "some_file.txt"; 247 const uint64_t ino = 42; 248 off_t fsize = 1 << 30; /* 1 GiB */ 249 off_t offset_in = 1 << 28; 250 int fd; 251 252 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 253 expect_open(ino, 0, 1); 254 EXPECT_CALL(*m_mock, process( 255 ResultOf([=](auto in) { 256 return (in.header.opcode == FUSE_LSEEK && 257 in.header.nodeid == ino && 258 in.body.lseek.fh == FH && 259 (off_t)in.body.lseek.offset == offset_in && 260 in.body.lseek.whence == SEEK_DATA); 261 }, Eq(true)), 262 _) 263 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 264 fd = open(FULLPATH, O_RDONLY); 265 266 /* 267 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset 268 * otherwise. 269 */ 270 EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); 271 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 272 EXPECT_EQ(ENXIO, errno); 273 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 274 EXPECT_EQ(ENXIO, errno); 275 276 leak(fd); 277 } 278 279 TEST_F(LseekSeekHole, ok) 280 { 281 const char FULLPATH[] = "mountpoint/some_file.txt"; 282 const char RELPATH[] = "some_file.txt"; 283 const uint64_t ino = 42; 284 off_t fsize = 1 << 30; /* 1 GiB */ 285 off_t offset_in = 1 << 28; 286 off_t offset_out = 1 << 29; 287 int fd; 288 289 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 290 expect_open(ino, 0, 1); 291 EXPECT_CALL(*m_mock, process( 292 ResultOf([=](auto in) { 293 return (in.header.opcode == FUSE_LSEEK && 294 in.header.nodeid == ino && 295 in.body.lseek.fh == FH && 296 (off_t)in.body.lseek.offset == offset_in && 297 in.body.lseek.whence == SEEK_HOLE); 298 }, Eq(true)), 299 _) 300 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 301 SET_OUT_HEADER_LEN(out, lseek); 302 out.body.lseek.offset = offset_out; 303 }))); 304 fd = open(FULLPATH, O_RDONLY); 305 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE)); 306 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 307 308 leak(fd); 309 } 310 311 /* 312 * If the server returns ENOSYS, fusefs should fall back to the default 313 * behavior, and never query the server again. 314 */ 315 TEST_F(LseekSeekHole, enosys) 316 { 317 const char FULLPATH[] = "mountpoint/some_file.txt"; 318 const char RELPATH[] = "some_file.txt"; 319 const uint64_t ino = 42; 320 off_t fsize = 1 << 30; /* 1 GiB */ 321 off_t offset_in = 1 << 28; 322 int fd; 323 324 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 325 expect_open(ino, 0, 1); 326 EXPECT_CALL(*m_mock, process( 327 ResultOf([=](auto in) { 328 return (in.header.opcode == FUSE_LSEEK && 329 in.header.nodeid == ino && 330 in.body.lseek.fh == FH && 331 (off_t)in.body.lseek.offset == offset_in && 332 in.body.lseek.whence == SEEK_HOLE); 333 }, Eq(true)), 334 _) 335 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 336 fd = open(FULLPATH, O_RDONLY); 337 338 /* 339 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize 340 * otherwise. 341 */ 342 EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE)); 343 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 344 EXPECT_EQ(ENXIO, errno); 345 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 346 EXPECT_EQ(ENXIO, errno); 347 348 leak(fd); 349 } 350 351 /* lseek should return ENXIO when offset points to EOF */ 352 TEST_F(LseekSeekHole, enxio) 353 { 354 const char FULLPATH[] = "mountpoint/some_file.txt"; 355 const char RELPATH[] = "some_file.txt"; 356 const uint64_t ino = 42; 357 off_t fsize = 1 << 30; /* 1 GiB */ 358 off_t offset_in = fsize; 359 int fd; 360 361 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 362 expect_open(ino, 0, 1); 363 EXPECT_CALL(*m_mock, process( 364 ResultOf([=](auto in) { 365 return (in.header.opcode == FUSE_LSEEK && 366 in.header.nodeid == ino && 367 in.body.lseek.fh == FH && 368 (off_t)in.body.lseek.offset == offset_in && 369 in.body.lseek.whence == SEEK_HOLE); 370 }, Eq(true)), 371 _) 372 ).WillOnce(Invoke(ReturnErrno(ENXIO))); 373 fd = open(FULLPATH, O_RDONLY); 374 EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE)); 375 EXPECT_EQ(ENXIO, errno); 376 377 leak(fd); 378 } 379