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/types.h> 33 #include <sys/extattr.h> 34 35 #include <fcntl.h> 36 #include <unistd.h> 37 } 38 39 #include "mockfs.hh" 40 #include "utils.hh" 41 42 using namespace testing; 43 44 class Access: public FuseTest { 45 public: 46 virtual void SetUp() { 47 FuseTest::SetUp(); 48 // Clear the default FUSE_ACCESS expectation 49 Mock::VerifyAndClearExpectations(m_mock); 50 } 51 52 void expect_lookup(const char *relpath, uint64_t ino) 53 { 54 FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1); 55 } 56 57 /* 58 * Expect that FUSE_ACCESS will never be called for the given inode, with any 59 * bits in the supplied access_mask set 60 */ 61 void expect_noaccess(uint64_t ino, mode_t access_mask) 62 { 63 EXPECT_CALL(*m_mock, process( 64 ResultOf([=](auto in) { 65 return (in.header.opcode == FUSE_ACCESS && 66 in.header.nodeid == ino && 67 in.body.access.mask & access_mask); 68 }, Eq(true)), 69 _) 70 ).Times(0); 71 } 72 73 }; 74 75 class RofsAccess: public Access { 76 public: 77 virtual void SetUp() { 78 m_ro = true; 79 Access::SetUp(); 80 } 81 }; 82 83 /* 84 * Change the mode of a file. 85 * 86 * There should never be a FUSE_ACCESS sent for this operation, except for 87 * search permissions on the parent directory. 88 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 89 */ 90 TEST_F(Access, chmod) 91 { 92 const char FULLPATH[] = "mountpoint/some_file.txt"; 93 const char RELPATH[] = "some_file.txt"; 94 const uint64_t ino = 42; 95 const mode_t newmode = 0644; 96 97 expect_access(FUSE_ROOT_ID, X_OK, 0); 98 expect_lookup(RELPATH, ino); 99 expect_noaccess(ino, 0); 100 EXPECT_CALL(*m_mock, process( 101 ResultOf([](auto in) { 102 return (in.header.opcode == FUSE_SETATTR && 103 in.header.nodeid == ino); 104 }, Eq(true)), 105 _) 106 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 107 SET_OUT_HEADER_LEN(out, attr); 108 out.body.attr.attr.ino = ino; // Must match nodeid 109 out.body.attr.attr.mode = S_IFREG | newmode; 110 }))); 111 112 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); 113 } 114 115 /* 116 * Create a new file 117 * 118 * There should never be a FUSE_ACCESS sent for this operation, except for 119 * search permissions on the parent directory. 120 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 121 */ 122 TEST_F(Access, create) 123 { 124 const char FULLPATH[] = "mountpoint/some_file.txt"; 125 const char RELPATH[] = "some_file.txt"; 126 mode_t mode = S_IFREG | 0755; 127 uint64_t ino = 42; 128 129 expect_access(FUSE_ROOT_ID, X_OK, 0); 130 expect_noaccess(FUSE_ROOT_ID, R_OK | W_OK); 131 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 132 .WillOnce(Invoke(ReturnErrno(ENOENT))); 133 expect_noaccess(ino, 0); 134 EXPECT_CALL(*m_mock, process( 135 ResultOf([=](auto in) { 136 return (in.header.opcode == FUSE_CREATE); 137 }, Eq(true)), 138 _) 139 ).WillOnce(ReturnErrno(EPERM)); 140 141 EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode)); 142 EXPECT_EQ(EPERM, errno); 143 } 144 145 /* The error case of FUSE_ACCESS. */ 146 TEST_F(Access, eaccess) 147 { 148 const char FULLPATH[] = "mountpoint/some_file.txt"; 149 const char RELPATH[] = "some_file.txt"; 150 uint64_t ino = 42; 151 mode_t access_mode = X_OK; 152 153 expect_access(FUSE_ROOT_ID, X_OK, 0); 154 expect_lookup(RELPATH, ino); 155 expect_access(ino, access_mode, EACCES); 156 157 ASSERT_NE(0, access(FULLPATH, access_mode)); 158 ASSERT_EQ(EACCES, errno); 159 } 160 161 /* 162 * If the filesystem returns ENOSYS, then it is treated as a permanent success, 163 * and subsequent VOP_ACCESS calls will succeed automatically without querying 164 * the daemon. 165 */ 166 TEST_F(Access, enosys) 167 { 168 const char FULLPATH[] = "mountpoint/some_file.txt"; 169 const char RELPATH[] = "some_file.txt"; 170 uint64_t ino = 42; 171 mode_t access_mode = R_OK; 172 173 expect_access(FUSE_ROOT_ID, X_OK, ENOSYS); 174 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); 175 176 ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); 177 ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); 178 } 179 180 TEST_F(RofsAccess, erofs) 181 { 182 const char FULLPATH[] = "mountpoint/some_file.txt"; 183 const char RELPATH[] = "some_file.txt"; 184 uint64_t ino = 42; 185 mode_t access_mode = W_OK; 186 187 expect_access(FUSE_ROOT_ID, X_OK, 0); 188 expect_lookup(RELPATH, ino); 189 190 ASSERT_NE(0, access(FULLPATH, access_mode)); 191 ASSERT_EQ(EROFS, errno); 192 } 193 194 195 /* 196 * Lookup an extended attribute 197 * 198 * There should never be a FUSE_ACCESS sent for this operation, except for 199 * search permissions on the parent directory. 200 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 201 */ 202 TEST_F(Access, Getxattr) 203 { 204 const char FULLPATH[] = "mountpoint/some_file.txt"; 205 const char RELPATH[] = "some_file.txt"; 206 uint64_t ino = 42; 207 char data[80]; 208 int ns = EXTATTR_NAMESPACE_USER; 209 ssize_t r; 210 211 expect_access(FUSE_ROOT_ID, X_OK, 0); 212 expect_lookup(RELPATH, ino); 213 expect_noaccess(ino, 0); 214 expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR)); 215 216 r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); 217 ASSERT_EQ(-1, r); 218 ASSERT_EQ(ENOATTR, errno); 219 } 220 221 /* The successful case of FUSE_ACCESS. */ 222 TEST_F(Access, ok) 223 { 224 const char FULLPATH[] = "mountpoint/some_file.txt"; 225 const char RELPATH[] = "some_file.txt"; 226 uint64_t ino = 42; 227 mode_t access_mode = R_OK; 228 229 expect_access(FUSE_ROOT_ID, X_OK, 0); 230 expect_lookup(RELPATH, ino); 231 expect_access(ino, access_mode, 0); 232 233 ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); 234 } 235 236 /* 237 * Unlink a file 238 * 239 * There should never be a FUSE_ACCESS sent for this operation, except for 240 * search permissions on the parent directory. 241 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 242 */ 243 TEST_F(Access, unlink) 244 { 245 const char FULLPATH[] = "mountpoint/some_file.txt"; 246 const char RELPATH[] = "some_file.txt"; 247 uint64_t ino = 42; 248 249 expect_access(FUSE_ROOT_ID, X_OK, 0); 250 expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK); 251 expect_noaccess(ino, 0); 252 expect_lookup(RELPATH, ino); 253 expect_unlink(1, RELPATH, EPERM); 254 255 ASSERT_NE(0, unlink(FULLPATH)); 256 ASSERT_EQ(EPERM, errno); 257 } 258 259 /* 260 * Unlink a file whose parent diretory's sticky bit is set 261 * 262 * There should never be a FUSE_ACCESS sent for this operation, except for 263 * search permissions on the parent directory. 264 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 265 */ 266 TEST_F(Access, unlink_sticky_directory) 267 { 268 const char FULLPATH[] = "mountpoint/some_file.txt"; 269 const char RELPATH[] = "some_file.txt"; 270 uint64_t ino = 42; 271 272 expect_access(FUSE_ROOT_ID, X_OK, 0); 273 expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK); 274 expect_noaccess(ino, 0); 275 EXPECT_CALL(*m_mock, process( 276 ResultOf([=](auto in) { 277 return (in.header.opcode == FUSE_GETATTR && 278 in.header.nodeid == FUSE_ROOT_ID); 279 }, Eq(true)), 280 _) 281 ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) 282 { 283 SET_OUT_HEADER_LEN(out, attr); 284 out.body.attr.attr.ino = FUSE_ROOT_ID; 285 out.body.attr.attr.mode = S_IFDIR | 01777; 286 out.body.attr.attr.uid = 0; 287 out.body.attr.attr_valid = UINT64_MAX; 288 }))); 289 EXPECT_CALL(*m_mock, process( 290 ResultOf([=](auto in) { 291 return (in.header.opcode == FUSE_ACCESS && 292 in.header.nodeid == ino); 293 }, Eq(true)), 294 _) 295 ).Times(0); 296 expect_lookup(RELPATH, ino); 297 expect_unlink(FUSE_ROOT_ID, RELPATH, EPERM); 298 299 ASSERT_EQ(-1, unlink(FULLPATH)); 300 ASSERT_EQ(EPERM, errno); 301 } 302