1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2019 The FreeBSD Foundation 5 * 6 * This software was developed by BFF Storage Systems, LLC under sponsorship 7 * from the FreeBSD Foundation. 8 * 9 * Redistribution and use in source and binary forms, with or without 10 * modification, are permitted provided that the following conditions 11 * are met: 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in the 16 * documentation and/or other materials provided with the distribution. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 28 * SUCH DAMAGE. 29 */ 30 31 extern "C" { 32 #include <sys/param.h> 33 #include <sys/ioctl.h> 34 #include <sys/filio.h> 35 36 #include <fcntl.h> 37 } 38 39 #include "mockfs.hh" 40 #include "utils.hh" 41 42 using namespace testing; 43 44 const static char FULLPATH[] = "mountpoint/foo"; 45 const static char RELPATH[] = "foo"; 46 47 class Bmap: public FuseTest, 48 public WithParamInterface<tuple<int, int>> 49 { 50 public: 51 virtual void SetUp() { 52 m_maxreadahead = UINT32_MAX; 53 m_init_flags |= get<0>(GetParam()); 54 FuseTest::SetUp(); 55 } 56 void expect_bmap(uint64_t ino, uint64_t lbn, uint32_t blocksize, uint64_t pbn) 57 { 58 EXPECT_CALL(*m_mock, process( 59 ResultOf([=](auto in) { 60 return (in.header.opcode == FUSE_BMAP && 61 in.header.nodeid == ino && 62 in.body.bmap.block == lbn && 63 in.body.bmap.blocksize == blocksize); 64 }, Eq(true)), 65 _) 66 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 67 SET_OUT_HEADER_LEN(out, bmap); 68 out.body.bmap.block = pbn; 69 }))); 70 } 71 72 void expect_lookup(const char *relpath, uint64_t ino, off_t size) 73 { 74 FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1, 75 UINT64_MAX); 76 } 77 }; 78 79 class BmapEof: public Bmap {}; 80 81 /* 82 * Test FUSE_BMAP 83 */ 84 TEST_P(Bmap, bmap) 85 { 86 struct fiobmap2_arg arg; 87 /* 88 * Pick fsize and lbn large enough that max length runs won't reach 89 * either beginning or end of file 90 */ 91 const off_t filesize = 1 << 30; 92 int64_t lbn = 100; 93 int64_t pbn = 12345; 94 const ino_t ino = 42; 95 int fd; 96 97 expect_lookup(RELPATH, 42, filesize); 98 expect_open(ino, 0, 1); 99 expect_bmap(ino, lbn, m_maxbcachebuf, pbn); 100 101 fd = open(FULLPATH, O_RDWR); 102 ASSERT_LE(0, fd) << strerror(errno); 103 104 arg.bn = lbn; 105 arg.runp = -1; 106 arg.runb = -1; 107 ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); 108 EXPECT_EQ(arg.bn, pbn); 109 /* 110 * XXX The FUSE protocol does not include the runp and runb variables, 111 * so those must be guessed in-kernel. There's no "right" answer, so 112 * just check that they're within reasonable limits. 113 */ 114 EXPECT_LE(arg.runb, lbn); 115 EXPECT_LE((unsigned long)arg.runb, m_maxreadahead / m_maxbcachebuf); 116 EXPECT_LE((unsigned long)arg.runb, m_maxphys / m_maxbcachebuf); 117 EXPECT_GT(arg.runb, 0); 118 EXPECT_LE(arg.runp, filesize / m_maxbcachebuf - lbn); 119 EXPECT_LE((unsigned long)arg.runp, m_maxreadahead / m_maxbcachebuf); 120 EXPECT_LE((unsigned long)arg.runp, m_maxphys / m_maxbcachebuf); 121 EXPECT_GT(arg.runp, 0); 122 123 leak(fd); 124 } 125 126 /* 127 * If the daemon does not implement VOP_BMAP, fusefs should return sensible 128 * defaults. 129 */ 130 TEST_P(Bmap, default_) 131 { 132 struct fiobmap2_arg arg; 133 const off_t filesize = 1 << 30; 134 const ino_t ino = 42; 135 int64_t lbn; 136 int fd; 137 138 expect_lookup(RELPATH, 42, filesize); 139 expect_open(ino, 0, 1); 140 EXPECT_CALL(*m_mock, process( 141 ResultOf([=](auto in) { 142 return (in.header.opcode == FUSE_BMAP); 143 }, Eq(true)), 144 _) 145 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 146 147 fd = open(FULLPATH, O_RDWR); 148 ASSERT_LE(0, fd) << strerror(errno); 149 150 /* First block */ 151 lbn = 0; 152 arg.bn = lbn; 153 arg.runp = -1; 154 arg.runb = -1; 155 ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); 156 EXPECT_EQ(arg.bn, 0); 157 EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1); 158 EXPECT_EQ(arg.runb, 0); 159 160 /* In the middle */ 161 lbn = filesize / m_maxbcachebuf / 2; 162 arg.bn = lbn; 163 arg.runp = -1; 164 arg.runb = -1; 165 ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); 166 EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); 167 EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1); 168 EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1); 169 170 /* Last block */ 171 lbn = filesize / m_maxbcachebuf - 1; 172 arg.bn = lbn; 173 arg.runp = -1; 174 arg.runb = -1; 175 ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); 176 EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); 177 EXPECT_EQ(arg.runp, 0); 178 EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1); 179 180 leak(fd); 181 } 182 183 /* 184 * The server returns an error for some reason for FUSE_BMAP. fusefs should 185 * faithfully report that error up to the caller. 186 */ 187 TEST_P(Bmap, einval) 188 { 189 struct fiobmap2_arg arg; 190 const off_t filesize = 1 << 30; 191 int64_t lbn = 100; 192 const ino_t ino = 42; 193 int fd; 194 195 expect_lookup(RELPATH, 42, filesize); 196 expect_open(ino, 0, 1); 197 EXPECT_CALL(*m_mock, process( 198 ResultOf([=](auto in) { 199 return (in.header.opcode == FUSE_BMAP && 200 in.header.nodeid == ino); 201 }, Eq(true)), 202 _) 203 ).WillOnce(Invoke(ReturnErrno(EINVAL))); 204 205 fd = open(FULLPATH, O_RDWR); 206 ASSERT_LE(0, fd) << strerror(errno); 207 208 arg.bn = lbn; 209 arg.runp = -1; 210 arg.runb = -1; 211 ASSERT_EQ(-1, ioctl(fd, FIOBMAP2, &arg)); 212 EXPECT_EQ(EINVAL, errno); 213 214 leak(fd); 215 } 216 217 /* 218 * Even if the server returns EINVAL during VOP_BMAP, we should still be able 219 * to successfully read a block. This is a regression test for 220 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=264196 . The bug did not 221 * lie in fusefs, but this is a convenient place for a regression test. 222 */ 223 TEST_P(Bmap, spurious_einval) 224 { 225 const off_t filesize = 4ull << 30; 226 const ino_t ino = 42; 227 int fd, r; 228 char buf[1]; 229 230 expect_lookup(RELPATH, 42, filesize); 231 expect_open(ino, 0, 1); 232 EXPECT_CALL(*m_mock, process( 233 ResultOf([=](auto in) { 234 return (in.header.opcode == FUSE_BMAP && 235 in.header.nodeid == ino); 236 }, Eq(true)), 237 _) 238 ).WillRepeatedly(Invoke(ReturnErrno(EINVAL))); 239 EXPECT_CALL(*m_mock, process( 240 ResultOf([=](auto in) { 241 return (in.header.opcode == FUSE_READ && 242 in.header.nodeid == ino && 243 in.body.read.offset == 0 && 244 in.body.read.size == (uint64_t)m_maxbcachebuf); 245 }, Eq(true)), 246 _) 247 ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { 248 size_t osize = in.body.read.size; 249 250 assert(osize < sizeof(out.body.bytes)); 251 out.header.len = sizeof(struct fuse_out_header) + osize; 252 bzero(out.body.bytes, osize); 253 }))); 254 255 fd = open(FULLPATH, O_RDWR); 256 ASSERT_LE(0, fd) << strerror(errno); 257 258 /* 259 * Read the same block multiple times. On a system affected by PR 260 * 264196 , the second read will fail. 261 */ 262 r = read(fd, buf, sizeof(buf)); 263 EXPECT_EQ(r, 1) << strerror(errno); 264 r = read(fd, buf, sizeof(buf)); 265 EXPECT_EQ(r, 1) << strerror(errno); 266 r = read(fd, buf, sizeof(buf)); 267 EXPECT_EQ(r, 1) << strerror(errno); 268 } 269 270 /* 271 * VOP_BMAP should not query the server for the file's size, even if its cached 272 * attributes have expired. 273 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256937 274 */ 275 TEST_P(BmapEof, eof) 276 { 277 /* 278 * Outline: 279 * 1) lookup the file, setting attr_valid=0 280 * 2) Read more than one block, causing the kernel to issue VOP_BMAP to 281 * plan readahead. 282 * 3) Nothing should panic 283 * 4) Repeat the tests, truncating the file after different numbers of 284 * GETATTR operations. 285 */ 286 Sequence seq; 287 const off_t filesize = 2 * m_maxbcachebuf; 288 const ino_t ino = 42; 289 mode_t mode = S_IFREG | 0644; 290 char *buf; 291 int fd; 292 int ngetattrs; 293 294 ngetattrs = get<1>(GetParam()); 295 FuseTest::expect_lookup(RELPATH, ino, mode, filesize, 1, 0); 296 expect_open(ino, 0, 1); 297 // Depending on ngetattrs, FUSE_READ could be called with either 298 // filesize or filesize / 2 . 299 EXPECT_CALL(*m_mock, process( 300 ResultOf([=](auto in) { 301 return (in.header.opcode == FUSE_READ && 302 in.header.nodeid == ino && 303 in.body.read.offset == 0 && 304 ( in.body.read.size == filesize || 305 in.body.read.size == filesize / 2)); 306 }, Eq(true)), 307 _) 308 ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { 309 size_t osize = in.body.read.size; 310 311 assert(osize < sizeof(out.body.bytes)); 312 out.header.len = sizeof(struct fuse_out_header) + osize; 313 bzero(out.body.bytes, osize); 314 }))); 315 EXPECT_CALL(*m_mock, process( 316 ResultOf([](auto in) { 317 return (in.header.opcode == FUSE_GETATTR && 318 in.header.nodeid == ino); 319 }, Eq(true)), 320 _) 321 ).Times(Between(ngetattrs - 1, ngetattrs)) 322 .InSequence(seq) 323 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 324 SET_OUT_HEADER_LEN(out, attr); 325 out.body.attr.attr_valid = 0; 326 out.body.attr.attr.ino = ino; 327 out.body.attr.attr.mode = S_IFREG | 0644; 328 out.body.attr.attr.size = filesize; 329 }))); 330 EXPECT_CALL(*m_mock, process( 331 ResultOf([](auto in) { 332 return (in.header.opcode == FUSE_GETATTR && 333 in.header.nodeid == ino); 334 }, Eq(true)), 335 _) 336 ).InSequence(seq) 337 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 338 SET_OUT_HEADER_LEN(out, attr); 339 out.body.attr.attr_valid = 0; 340 out.body.attr.attr.ino = ino; 341 out.body.attr.attr.mode = S_IFREG | 0644; 342 out.body.attr.attr.size = filesize / 2; 343 }))); 344 345 buf = new char[filesize](); 346 fd = open(FULLPATH, O_RDWR); 347 ASSERT_LE(0, fd) << strerror(errno); 348 read(fd, buf, filesize); 349 350 delete[] buf; 351 leak(fd); 352 } 353 354 /* 355 * Try with and without async reads, because it affects the type of vnode lock 356 * on entry to fuse_vnop_bmap. 357 */ 358 INSTANTIATE_TEST_SUITE_P(B, Bmap, Values( 359 tuple(0, 0), 360 tuple(FUSE_ASYNC_READ, 0) 361 )); 362 363 INSTANTIATE_TEST_SUITE_P(BE, BmapEof, Values( 364 tuple(0, 1), 365 tuple(0, 2), 366 tuple(0, 3) 367 )); 368