1 /*-
2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
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 * $FreeBSD$
31 */
32
33 /*
34 * Tests for the "default_permissions" mount option. They must be in their own
35 * file so they can be run as an unprivileged user
36 */
37
38 extern "C" {
39 #include <sys/types.h>
40 #include <sys/extattr.h>
41
42 #include <fcntl.h>
43 #include <semaphore.h>
44 #include <unistd.h>
45 }
46
47 #include "mockfs.hh"
48 #include "utils.hh"
49
50 using namespace testing;
51
52 class DefaultPermissions: public FuseTest {
53
SetUp()54 virtual void SetUp() {
55 m_default_permissions = true;
56 FuseTest::SetUp();
57 if (HasFatalFailure() || IsSkipped())
58 return;
59
60 if (geteuid() == 0) {
61 GTEST_SKIP() << "This test requires an unprivileged user";
62 }
63
64 /* With -o default_permissions, FUSE_ACCESS should never be called */
65 EXPECT_CALL(*m_mock, process(
66 ResultOf([=](auto in) {
67 return (in.header.opcode == FUSE_ACCESS);
68 }, Eq(true)),
69 _)
70 ).Times(0);
71 }
72
73 public:
expect_chmod(uint64_t ino,mode_t mode,uint64_t size=0)74 void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0)
75 {
76 EXPECT_CALL(*m_mock, process(
77 ResultOf([=](auto in) {
78 return (in.header.opcode == FUSE_SETATTR &&
79 in.header.nodeid == ino &&
80 in.body.setattr.valid == FATTR_MODE &&
81 in.body.setattr.mode == mode);
82 }, Eq(true)),
83 _)
84 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
85 SET_OUT_HEADER_LEN(out, attr);
86 out.body.attr.attr.ino = ino; // Must match nodeid
87 out.body.attr.attr.mode = S_IFREG | mode;
88 out.body.attr.attr.size = size;
89 out.body.attr.attr_valid = UINT64_MAX;
90 })));
91 }
92
expect_create(const char * relpath,uint64_t ino)93 void expect_create(const char *relpath, uint64_t ino)
94 {
95 EXPECT_CALL(*m_mock, process(
96 ResultOf([=](auto in) {
97 const char *name = (const char*)in.body.bytes +
98 sizeof(fuse_create_in);
99 return (in.header.opcode == FUSE_CREATE &&
100 (0 == strcmp(relpath, name)));
101 }, Eq(true)),
102 _)
103 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
104 SET_OUT_HEADER_LEN(out, create);
105 out.body.create.entry.attr.mode = S_IFREG | 0644;
106 out.body.create.entry.nodeid = ino;
107 out.body.create.entry.entry_valid = UINT64_MAX;
108 out.body.create.entry.attr_valid = UINT64_MAX;
109 })));
110 }
111
expect_getattr(uint64_t ino,mode_t mode,uint64_t attr_valid,int times,uid_t uid=0,gid_t gid=0)112 void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times,
113 uid_t uid = 0, gid_t gid = 0)
114 {
115 EXPECT_CALL(*m_mock, process(
116 ResultOf([=](auto in) {
117 return (in.header.opcode == FUSE_GETATTR &&
118 in.header.nodeid == ino);
119 }, Eq(true)),
120 _)
121 ).Times(times)
122 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
123 SET_OUT_HEADER_LEN(out, attr);
124 out.body.attr.attr.ino = ino; // Must match nodeid
125 out.body.attr.attr.mode = mode;
126 out.body.attr.attr.size = 0;
127 out.body.attr.attr.uid = uid;
128 out.body.attr.attr.uid = gid;
129 out.body.attr.attr_valid = attr_valid;
130 })));
131 }
132
expect_lookup(const char * relpath,uint64_t ino,mode_t mode,uint64_t attr_valid,uid_t uid=0,gid_t gid=0)133 void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
134 uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0)
135 {
136 FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid);
137 }
138
139 };
140
141 class Access: public DefaultPermissions {};
142 class Chown: public DefaultPermissions {};
143 class Chgrp: public DefaultPermissions {};
144 class Lookup: public DefaultPermissions {};
145 class Open: public DefaultPermissions {};
146 class Setattr: public DefaultPermissions {};
147 class Unlink: public DefaultPermissions {};
148 class Utimensat: public DefaultPermissions {};
149 class Write: public DefaultPermissions {};
150
151 /*
152 * Test permission handling during create, mkdir, mknod, link, symlink, and
153 * rename vops (they all share a common path for permission checks in
154 * VOP_LOOKUP)
155 */
156 class Create: public DefaultPermissions {};
157
158 class Deleteextattr: public DefaultPermissions {
159 public:
expect_removexattr()160 void expect_removexattr()
161 {
162 EXPECT_CALL(*m_mock, process(
163 ResultOf([=](auto in) {
164 return (in.header.opcode == FUSE_REMOVEXATTR);
165 }, Eq(true)),
166 _)
167 ).WillOnce(Invoke(ReturnErrno(0)));
168 }
169 };
170
171 class Getextattr: public DefaultPermissions {
172 public:
expect_getxattr(ProcessMockerT r)173 void expect_getxattr(ProcessMockerT r)
174 {
175 EXPECT_CALL(*m_mock, process(
176 ResultOf([=](auto in) {
177 return (in.header.opcode == FUSE_GETXATTR);
178 }, Eq(true)),
179 _)
180 ).WillOnce(Invoke(r));
181 }
182 };
183
184 class Listextattr: public DefaultPermissions {
185 public:
expect_listxattr()186 void expect_listxattr()
187 {
188 EXPECT_CALL(*m_mock, process(
189 ResultOf([=](auto in) {
190 return (in.header.opcode == FUSE_LISTXATTR);
191 }, Eq(true)),
192 _)
193 ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
194 out.body.listxattr.size = 0;
195 SET_OUT_HEADER_LEN(out, listxattr);
196 })));
197 }
198 };
199
200 class Rename: public DefaultPermissions {
201 public:
202 /*
203 * Expect a rename and respond with the given error. Don't both to
204 * validate arguments; the tests in rename.cc do that.
205 */
expect_rename(int error)206 void expect_rename(int error)
207 {
208 EXPECT_CALL(*m_mock, process(
209 ResultOf([=](auto in) {
210 return (in.header.opcode == FUSE_RENAME);
211 }, Eq(true)),
212 _)
213 ).WillOnce(Invoke(ReturnErrno(error)));
214 }
215 };
216
217 class Setextattr: public DefaultPermissions {
218 public:
expect_setxattr(int error)219 void expect_setxattr(int error)
220 {
221 EXPECT_CALL(*m_mock, process(
222 ResultOf([=](auto in) {
223 return (in.header.opcode == FUSE_SETXATTR);
224 }, Eq(true)),
225 _)
226 ).WillOnce(Invoke(ReturnErrno(error)));
227 }
228 };
229
230 /* Return a group to which this user does not belong */
excluded_group()231 static gid_t excluded_group()
232 {
233 int i, ngroups = 64;
234 gid_t newgid, groups[ngroups];
235
236 getgrouplist(getlogin(), getegid(), groups, &ngroups);
237 for (newgid = 0; ; newgid++) {
238 bool belongs = false;
239
240 for (i = 0; i < ngroups; i++) {
241 if (groups[i] == newgid)
242 belongs = true;
243 }
244 if (!belongs)
245 break;
246 }
247 /* newgid is now a group to which the current user does not belong */
248 return newgid;
249 }
250
TEST_F(Access,eacces)251 TEST_F(Access, eacces)
252 {
253 const char FULLPATH[] = "mountpoint/some_file.txt";
254 const char RELPATH[] = "some_file.txt";
255 uint64_t ino = 42;
256 mode_t access_mode = X_OK;
257
258 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
259 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
260
261 ASSERT_NE(0, access(FULLPATH, access_mode));
262 ASSERT_EQ(EACCES, errno);
263 }
264
TEST_F(Access,eacces_no_cached_attrs)265 TEST_F(Access, eacces_no_cached_attrs)
266 {
267 const char FULLPATH[] = "mountpoint/some_file.txt";
268 const char RELPATH[] = "some_file.txt";
269 uint64_t ino = 42;
270 mode_t access_mode = X_OK;
271
272 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, 0, 1);
273 expect_lookup(RELPATH, ino, S_IFREG | 0644, 0);
274 expect_getattr(ino, S_IFREG | 0644, 0, 1);
275 /*
276 * Once default_permissions is properly implemented, there might be
277 * another FUSE_GETATTR or something in here. But there should not be
278 * a FUSE_ACCESS
279 */
280
281 ASSERT_NE(0, access(FULLPATH, access_mode));
282 ASSERT_EQ(EACCES, errno);
283 }
284
TEST_F(Access,ok)285 TEST_F(Access, ok)
286 {
287 const char FULLPATH[] = "mountpoint/some_file.txt";
288 const char RELPATH[] = "some_file.txt";
289 uint64_t ino = 42;
290 mode_t access_mode = R_OK;
291
292 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
293 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
294 /*
295 * Once default_permissions is properly implemented, there might be
296 * another FUSE_GETATTR or something in here.
297 */
298
299 ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
300 }
301
302 /* Unprivileged users may chown a file to their own uid */
TEST_F(Chown,chown_to_self)303 TEST_F(Chown, chown_to_self)
304 {
305 const char FULLPATH[] = "mountpoint/some_file.txt";
306 const char RELPATH[] = "some_file.txt";
307 const uint64_t ino = 42;
308 const mode_t mode = 0755;
309 uid_t uid;
310
311 uid = geteuid();
312
313 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
314 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid);
315 /* The OS may optimize chown by omitting the redundant setattr */
316 EXPECT_CALL(*m_mock, process(
317 ResultOf([](auto in) {
318 return (in.header.opcode == FUSE_SETATTR);
319 }, Eq(true)),
320 _)
321 ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){
322 SET_OUT_HEADER_LEN(out, attr);
323 out.body.attr.attr.mode = S_IFREG | mode;
324 out.body.attr.attr.uid = uid;
325 })));
326
327 EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
328 }
329
330 /*
331 * A successful chown by a non-privileged non-owner should clear a file's SUID
332 * bit
333 */
TEST_F(Chown,clear_suid)334 TEST_F(Chown, clear_suid)
335 {
336 const char FULLPATH[] = "mountpoint/some_file.txt";
337 const char RELPATH[] = "some_file.txt";
338 uint64_t ino = 42;
339 const mode_t oldmode = 06755;
340 const mode_t newmode = 0755;
341 uid_t uid = geteuid();
342 uint32_t valid = FATTR_UID | FATTR_MODE;
343
344 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
345 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid);
346 EXPECT_CALL(*m_mock, process(
347 ResultOf([=](auto in) {
348 return (in.header.opcode == FUSE_SETATTR &&
349 in.header.nodeid == ino &&
350 in.body.setattr.valid == valid &&
351 in.body.setattr.mode == newmode);
352 }, Eq(true)),
353 _)
354 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
355 SET_OUT_HEADER_LEN(out, attr);
356 out.body.attr.attr.ino = ino; // Must match nodeid
357 out.body.attr.attr.mode = S_IFREG | newmode;
358 out.body.attr.attr_valid = UINT64_MAX;
359 })));
360
361 EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
362 }
363
364
365 /* Only root may change a file's owner */
TEST_F(Chown,eperm)366 TEST_F(Chown, eperm)
367 {
368 const char FULLPATH[] = "mountpoint/some_file.txt";
369 const char RELPATH[] = "some_file.txt";
370 const uint64_t ino = 42;
371 const mode_t mode = 0755;
372
373 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, geteuid());
374 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid());
375 EXPECT_CALL(*m_mock, process(
376 ResultOf([](auto in) {
377 return (in.header.opcode == FUSE_SETATTR);
378 }, Eq(true)),
379 _)
380 ).Times(0);
381
382 EXPECT_NE(0, chown(FULLPATH, 0, -1));
383 EXPECT_EQ(EPERM, errno);
384 }
385
386 /*
387 * A successful chgrp by a non-privileged non-owner should clear a file's SUID
388 * bit
389 */
TEST_F(Chgrp,clear_suid)390 TEST_F(Chgrp, clear_suid)
391 {
392 const char FULLPATH[] = "mountpoint/some_file.txt";
393 const char RELPATH[] = "some_file.txt";
394 uint64_t ino = 42;
395 const mode_t oldmode = 06755;
396 const mode_t newmode = 0755;
397 uid_t uid = geteuid();
398 gid_t gid = getegid();
399 uint32_t valid = FATTR_GID | FATTR_MODE;
400
401 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
402 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
403 EXPECT_CALL(*m_mock, process(
404 ResultOf([=](auto in) {
405 return (in.header.opcode == FUSE_SETATTR &&
406 in.header.nodeid == ino &&
407 in.body.setattr.valid == valid &&
408 in.body.setattr.mode == newmode);
409 }, Eq(true)),
410 _)
411 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
412 SET_OUT_HEADER_LEN(out, attr);
413 out.body.attr.attr.ino = ino; // Must match nodeid
414 out.body.attr.attr.mode = S_IFREG | newmode;
415 out.body.attr.attr_valid = UINT64_MAX;
416 })));
417
418 EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno);
419 }
420
421 /* non-root users may only chgrp a file to a group they belong to */
TEST_F(Chgrp,eperm)422 TEST_F(Chgrp, eperm)
423 {
424 const char FULLPATH[] = "mountpoint/some_file.txt";
425 const char RELPATH[] = "some_file.txt";
426 const uint64_t ino = 42;
427 const mode_t mode = 0755;
428 uid_t uid;
429 gid_t gid, newgid;
430
431 uid = geteuid();
432 gid = getegid();
433 newgid = excluded_group();
434
435 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
436 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
437 EXPECT_CALL(*m_mock, process(
438 ResultOf([](auto in) {
439 return (in.header.opcode == FUSE_SETATTR);
440 }, Eq(true)),
441 _)
442 ).Times(0);
443
444 EXPECT_NE(0, chown(FULLPATH, -1, newgid));
445 EXPECT_EQ(EPERM, errno);
446 }
447
TEST_F(Chgrp,ok)448 TEST_F(Chgrp, ok)
449 {
450 const char FULLPATH[] = "mountpoint/some_file.txt";
451 const char RELPATH[] = "some_file.txt";
452 const uint64_t ino = 42;
453 const mode_t mode = 0755;
454 uid_t uid;
455 gid_t gid, newgid;
456
457 uid = geteuid();
458 gid = 0;
459 newgid = getegid();
460
461 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
462 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
463 /* The OS may optimize chgrp by omitting the redundant setattr */
464 EXPECT_CALL(*m_mock, process(
465 ResultOf([](auto in) {
466 return (in.header.opcode == FUSE_SETATTR &&
467 in.header.nodeid == ino);
468 }, Eq(true)),
469 _)
470 ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){
471 SET_OUT_HEADER_LEN(out, attr);
472 out.body.attr.attr.mode = S_IFREG | mode;
473 out.body.attr.attr.uid = uid;
474 out.body.attr.attr.gid = newgid;
475 })));
476
477 EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno);
478 }
479
TEST_F(Create,ok)480 TEST_F(Create, ok)
481 {
482 const char FULLPATH[] = "mountpoint/some_file.txt";
483 const char RELPATH[] = "some_file.txt";
484 uint64_t ino = 42;
485 int fd;
486
487 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
488 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
489 .WillOnce(Invoke(ReturnErrno(ENOENT)));
490 expect_create(RELPATH, ino);
491
492 fd = open(FULLPATH, O_CREAT | O_EXCL, 0644);
493 EXPECT_LE(0, fd) << strerror(errno);
494 leak(fd);
495 }
496
TEST_F(Create,eacces)497 TEST_F(Create, eacces)
498 {
499 const char FULLPATH[] = "mountpoint/some_file.txt";
500 const char RELPATH[] = "some_file.txt";
501
502 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
503 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
504 .WillOnce(Invoke(ReturnErrno(ENOENT)));
505
506 ASSERT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, 0644));
507 EXPECT_EQ(EACCES, errno);
508 }
509
TEST_F(Deleteextattr,eacces)510 TEST_F(Deleteextattr, eacces)
511 {
512 const char FULLPATH[] = "mountpoint/some_file.txt";
513 const char RELPATH[] = "some_file.txt";
514 uint64_t ino = 42;
515 int ns = EXTATTR_NAMESPACE_USER;
516
517 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
518 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
519
520 ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
521 ASSERT_EQ(EACCES, errno);
522 }
523
TEST_F(Deleteextattr,ok)524 TEST_F(Deleteextattr, ok)
525 {
526 const char FULLPATH[] = "mountpoint/some_file.txt";
527 const char RELPATH[] = "some_file.txt";
528 uint64_t ino = 42;
529 int ns = EXTATTR_NAMESPACE_USER;
530
531 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
532 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
533 expect_removexattr();
534
535 ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
536 << strerror(errno);
537 }
538
539 /* Delete system attributes requires superuser privilege */
TEST_F(Deleteextattr,system)540 TEST_F(Deleteextattr, system)
541 {
542 const char FULLPATH[] = "mountpoint/some_file.txt";
543 const char RELPATH[] = "some_file.txt";
544 uint64_t ino = 42;
545 int ns = EXTATTR_NAMESPACE_SYSTEM;
546
547 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
548 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
549
550 ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
551 ASSERT_EQ(EPERM, errno);
552 }
553
554 /* Anybody with write permission can set both timestamps to UTIME_NOW */
TEST_F(Utimensat,utime_now)555 TEST_F(Utimensat, utime_now)
556 {
557 const char FULLPATH[] = "mountpoint/some_file.txt";
558 const char RELPATH[] = "some_file.txt";
559 const uint64_t ino = 42;
560 /* Write permissions for everybody */
561 const mode_t mode = 0666;
562 uid_t owner = 0;
563 const timespec times[2] = {
564 {.tv_sec = 0, .tv_nsec = UTIME_NOW},
565 {.tv_sec = 0, .tv_nsec = UTIME_NOW},
566 };
567
568 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
569 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);
570 EXPECT_CALL(*m_mock, process(
571 ResultOf([](auto in) {
572 return (in.header.opcode == FUSE_SETATTR &&
573 in.header.nodeid == ino &&
574 in.body.setattr.valid & FATTR_ATIME &&
575 in.body.setattr.valid & FATTR_MTIME);
576 }, Eq(true)),
577 _)
578 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
579 SET_OUT_HEADER_LEN(out, attr);
580 out.body.attr.attr.mode = S_IFREG | mode;
581 })));
582
583 ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0))
584 << strerror(errno);
585 }
586
587 /* Anybody can set both timestamps to UTIME_OMIT */
TEST_F(Utimensat,utime_omit)588 TEST_F(Utimensat, utime_omit)
589 {
590 const char FULLPATH[] = "mountpoint/some_file.txt";
591 const char RELPATH[] = "some_file.txt";
592 const uint64_t ino = 42;
593 /* Write permissions for no one */
594 const mode_t mode = 0444;
595 uid_t owner = 0;
596 const timespec times[2] = {
597 {.tv_sec = 0, .tv_nsec = UTIME_OMIT},
598 {.tv_sec = 0, .tv_nsec = UTIME_OMIT},
599 };
600
601 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
602 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);
603
604 ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0))
605 << strerror(errno);
606 }
607
608 /* Deleting user attributes merely requires WRITE privilege */
TEST_F(Deleteextattr,user)609 TEST_F(Deleteextattr, user)
610 {
611 const char FULLPATH[] = "mountpoint/some_file.txt";
612 const char RELPATH[] = "some_file.txt";
613 uint64_t ino = 42;
614 int ns = EXTATTR_NAMESPACE_USER;
615
616 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
617 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
618 expect_removexattr();
619
620 ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
621 << strerror(errno);
622 }
623
TEST_F(Getextattr,eacces)624 TEST_F(Getextattr, eacces)
625 {
626 const char FULLPATH[] = "mountpoint/some_file.txt";
627 const char RELPATH[] = "some_file.txt";
628 uint64_t ino = 42;
629 char data[80];
630 int ns = EXTATTR_NAMESPACE_USER;
631
632 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
633 expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
634
635 ASSERT_EQ(-1,
636 extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
637 ASSERT_EQ(EACCES, errno);
638 }
639
TEST_F(Getextattr,ok)640 TEST_F(Getextattr, ok)
641 {
642 const char FULLPATH[] = "mountpoint/some_file.txt";
643 const char RELPATH[] = "some_file.txt";
644 uint64_t ino = 42;
645 char data[80];
646 const char value[] = "whatever";
647 ssize_t value_len = strlen(value) + 1;
648 int ns = EXTATTR_NAMESPACE_USER;
649 ssize_t r;
650
651 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
652 /* Getting user attributes only requires read access */
653 expect_lookup(RELPATH, ino, S_IFREG | 0444, UINT64_MAX, 0);
654 expect_getxattr(
655 ReturnImmediate([&](auto in __unused, auto& out) {
656 memcpy((void*)out.body.bytes, value, value_len);
657 out.header.len = sizeof(out.header) + value_len;
658 })
659 );
660
661 r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
662 ASSERT_EQ(value_len, r) << strerror(errno);
663 EXPECT_STREQ(value, data);
664 }
665
666 /* Getting system attributes requires superuser privileges */
TEST_F(Getextattr,system)667 TEST_F(Getextattr, system)
668 {
669 const char FULLPATH[] = "mountpoint/some_file.txt";
670 const char RELPATH[] = "some_file.txt";
671 uint64_t ino = 42;
672 char data[80];
673 int ns = EXTATTR_NAMESPACE_SYSTEM;
674
675 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
676 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
677
678 ASSERT_EQ(-1,
679 extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
680 ASSERT_EQ(EPERM, errno);
681 }
682
TEST_F(Listextattr,eacces)683 TEST_F(Listextattr, eacces)
684 {
685 const char FULLPATH[] = "mountpoint/some_file.txt";
686 const char RELPATH[] = "some_file.txt";
687 uint64_t ino = 42;
688 int ns = EXTATTR_NAMESPACE_USER;
689
690 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
691 expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
692
693 ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
694 ASSERT_EQ(EACCES, errno);
695 }
696
TEST_F(Listextattr,ok)697 TEST_F(Listextattr, ok)
698 {
699 const char FULLPATH[] = "mountpoint/some_file.txt";
700 const char RELPATH[] = "some_file.txt";
701 uint64_t ino = 42;
702 int ns = EXTATTR_NAMESPACE_USER;
703
704 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
705 /* Listing user extended attributes merely requires read access */
706 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
707 expect_listxattr();
708
709 ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))
710 << strerror(errno);
711 }
712
713 /* Listing system xattrs requires superuser privileges */
TEST_F(Listextattr,system)714 TEST_F(Listextattr, system)
715 {
716 const char FULLPATH[] = "mountpoint/some_file.txt";
717 const char RELPATH[] = "some_file.txt";
718 uint64_t ino = 42;
719 int ns = EXTATTR_NAMESPACE_SYSTEM;
720
721 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
722 /* Listing user extended attributes merely requires read access */
723 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
724
725 ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
726 ASSERT_EQ(EPERM, errno);
727 }
728
729 /* A component of the search path lacks execute permissions */
TEST_F(Lookup,eacces)730 TEST_F(Lookup, eacces)
731 {
732 const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
733 const char RELDIRPATH[] = "some_dir";
734 uint64_t dir_ino = 42;
735
736 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
737 expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0);
738
739 EXPECT_EQ(-1, access(FULLPATH, F_OK));
740 EXPECT_EQ(EACCES, errno);
741 }
742
TEST_F(Open,eacces)743 TEST_F(Open, eacces)
744 {
745 const char FULLPATH[] = "mountpoint/some_file.txt";
746 const char RELPATH[] = "some_file.txt";
747 uint64_t ino = 42;
748
749 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
750 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
751
752 EXPECT_NE(0, open(FULLPATH, O_RDWR));
753 EXPECT_EQ(EACCES, errno);
754 }
755
TEST_F(Open,ok)756 TEST_F(Open, ok)
757 {
758 const char FULLPATH[] = "mountpoint/some_file.txt";
759 const char RELPATH[] = "some_file.txt";
760 uint64_t ino = 42;
761 int fd;
762
763 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
764 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
765 expect_open(ino, 0, 1);
766
767 fd = open(FULLPATH, O_RDONLY);
768 EXPECT_LE(0, fd) << strerror(errno);
769 leak(fd);
770 }
771
TEST_F(Rename,eacces_on_srcdir)772 TEST_F(Rename, eacces_on_srcdir)
773 {
774 const char FULLDST[] = "mountpoint/d/dst";
775 const char RELDST[] = "d/dst";
776 const char FULLSRC[] = "mountpoint/src";
777 const char RELSRC[] = "src";
778 uint64_t ino = 42;
779
780 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0);
781 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
782 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
783 .Times(AnyNumber())
784 .WillRepeatedly(Invoke(ReturnErrno(ENOENT)));
785
786 ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
787 ASSERT_EQ(EACCES, errno);
788 }
789
TEST_F(Rename,eacces_on_dstdir_for_creating)790 TEST_F(Rename, eacces_on_dstdir_for_creating)
791 {
792 const char FULLDST[] = "mountpoint/d/dst";
793 const char RELDSTDIR[] = "d";
794 const char RELDST[] = "dst";
795 const char FULLSRC[] = "mountpoint/src";
796 const char RELSRC[] = "src";
797 uint64_t src_ino = 42;
798 uint64_t dstdir_ino = 43;
799
800 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
801 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
802 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
803 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
804
805 ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
806 ASSERT_EQ(EACCES, errno);
807 }
808
TEST_F(Rename,eacces_on_dstdir_for_removing)809 TEST_F(Rename, eacces_on_dstdir_for_removing)
810 {
811 const char FULLDST[] = "mountpoint/d/dst";
812 const char RELDSTDIR[] = "d";
813 const char RELDST[] = "dst";
814 const char FULLSRC[] = "mountpoint/src";
815 const char RELSRC[] = "src";
816 uint64_t src_ino = 42;
817 uint64_t dstdir_ino = 43;
818
819 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
820 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
821 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
822 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
823
824 ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
825 ASSERT_EQ(EACCES, errno);
826 }
827
TEST_F(Rename,eperm_on_sticky_srcdir)828 TEST_F(Rename, eperm_on_sticky_srcdir)
829 {
830 const char FULLDST[] = "mountpoint/d/dst";
831 const char FULLSRC[] = "mountpoint/src";
832 const char RELSRC[] = "src";
833 uint64_t ino = 42;
834
835 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);
836 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
837
838 ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
839 ASSERT_EQ(EPERM, errno);
840 }
841
842 /*
843 * A user cannot move out a subdirectory that he does not own, because that
844 * would require changing the subdirectory's ".." dirent
845 */
TEST_F(Rename,eperm_for_subdirectory)846 TEST_F(Rename, eperm_for_subdirectory)
847 {
848 const char FULLDST[] = "mountpoint/d/dst";
849 const char FULLSRC[] = "mountpoint/src";
850 const char RELDSTDIR[] = "d";
851 const char RELDST[] = "dst";
852 const char RELSRC[] = "src";
853 uint64_t ino = 42;
854 uint64_t dstdir_ino = 43;
855
856 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
857 expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);
858 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0);
859 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
860
861 ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
862 ASSERT_EQ(EACCES, errno);
863 }
864
865 /*
866 * A user _can_ rename a subdirectory to which he lacks write permissions, if
867 * it will keep the same parent
868 */
TEST_F(Rename,subdirectory_to_same_dir)869 TEST_F(Rename, subdirectory_to_same_dir)
870 {
871 const char FULLDST[] = "mountpoint/dst";
872 const char FULLSRC[] = "mountpoint/src";
873 const char RELDST[] = "dst";
874 const char RELSRC[] = "src";
875 uint64_t ino = 42;
876
877 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
878 expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);
879 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
880 .WillOnce(Invoke(ReturnErrno(ENOENT)));
881 expect_rename(0);
882
883 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
884 }
885
TEST_F(Rename,eperm_on_sticky_dstdir)886 TEST_F(Rename, eperm_on_sticky_dstdir)
887 {
888 const char FULLDST[] = "mountpoint/d/dst";
889 const char RELDSTDIR[] = "d";
890 const char RELDST[] = "dst";
891 const char FULLSRC[] = "mountpoint/src";
892 const char RELSRC[] = "src";
893 uint64_t src_ino = 42;
894 uint64_t dstdir_ino = 43;
895 uint64_t dst_ino = 44;
896
897 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
898 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
899 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX);
900 EXPECT_LOOKUP(dstdir_ino, RELDST)
901 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
902 SET_OUT_HEADER_LEN(out, entry);
903 out.body.entry.attr.mode = S_IFREG | 0644;
904 out.body.entry.nodeid = dst_ino;
905 out.body.entry.attr_valid = UINT64_MAX;
906 out.body.entry.entry_valid = UINT64_MAX;
907 out.body.entry.attr.uid = 0;
908 })));
909
910 ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
911 ASSERT_EQ(EPERM, errno);
912 }
913
914 /* Successfully rename a file, overwriting the destination */
TEST_F(Rename,ok)915 TEST_F(Rename, ok)
916 {
917 const char FULLDST[] = "mountpoint/dst";
918 const char RELDST[] = "dst";
919 const char FULLSRC[] = "mountpoint/src";
920 const char RELSRC[] = "src";
921 // The inode of the already-existing destination file
922 uint64_t dst_ino = 2;
923 uint64_t ino = 42;
924
925 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid());
926 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
927 expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX);
928 expect_rename(0);
929
930 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
931 }
932
TEST_F(Rename,ok_to_remove_src_because_of_stickiness)933 TEST_F(Rename, ok_to_remove_src_because_of_stickiness)
934 {
935 const char FULLDST[] = "mountpoint/dst";
936 const char RELDST[] = "dst";
937 const char FULLSRC[] = "mountpoint/src";
938 const char RELSRC[] = "src";
939 uint64_t ino = 42;
940
941 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);
942 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
943 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
944 .WillOnce(Invoke(ReturnErrno(ENOENT)));
945 expect_rename(0);
946
947 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
948 }
949
TEST_F(Setattr,ok)950 TEST_F(Setattr, ok)
951 {
952 const char FULLPATH[] = "mountpoint/some_file.txt";
953 const char RELPATH[] = "some_file.txt";
954 const uint64_t ino = 42;
955 const mode_t oldmode = 0755;
956 const mode_t newmode = 0644;
957
958 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
959 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
960 EXPECT_CALL(*m_mock, process(
961 ResultOf([](auto in) {
962 return (in.header.opcode == FUSE_SETATTR &&
963 in.header.nodeid == ino &&
964 in.body.setattr.mode == newmode);
965 }, Eq(true)),
966 _)
967 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
968 SET_OUT_HEADER_LEN(out, attr);
969 out.body.attr.attr.mode = S_IFREG | newmode;
970 })));
971
972 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
973 }
974
TEST_F(Setattr,eacces)975 TEST_F(Setattr, eacces)
976 {
977 const char FULLPATH[] = "mountpoint/some_file.txt";
978 const char RELPATH[] = "some_file.txt";
979 const uint64_t ino = 42;
980 const mode_t oldmode = 0755;
981 const mode_t newmode = 0644;
982
983 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
984 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0);
985 EXPECT_CALL(*m_mock, process(
986 ResultOf([](auto in) {
987 return (in.header.opcode == FUSE_SETATTR);
988 }, Eq(true)),
989 _)
990 ).Times(0);
991
992 EXPECT_NE(0, chmod(FULLPATH, newmode));
993 EXPECT_EQ(EPERM, errno);
994 }
995
996 /*
997 * ftruncate() of a file without writable permissions should succeed as long as
998 * the file descriptor is writable. This is important when combined with
999 * O_CREAT
1000 */
TEST_F(Setattr,ftruncate_of_newly_created_file)1001 TEST_F(Setattr, ftruncate_of_newly_created_file)
1002 {
1003 const char FULLPATH[] = "mountpoint/some_file.txt";
1004 const char RELPATH[] = "some_file.txt";
1005 const uint64_t ino = 42;
1006 const mode_t mode = 0000;
1007 int fd;
1008
1009 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
1010 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
1011 .WillOnce(Invoke(ReturnErrno(ENOENT)));
1012 expect_create(RELPATH, ino);
1013 EXPECT_CALL(*m_mock, process(
1014 ResultOf([](auto in) {
1015 return (in.header.opcode == FUSE_SETATTR &&
1016 in.header.nodeid == ino &&
1017 (in.body.setattr.valid & FATTR_SIZE));
1018 }, Eq(true)),
1019 _)
1020 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
1021 SET_OUT_HEADER_LEN(out, attr);
1022 out.body.attr.attr.ino = ino;
1023 out.body.attr.attr.mode = S_IFREG | mode;
1024 out.body.attr.attr_valid = UINT64_MAX;
1025 })));
1026
1027 fd = open(FULLPATH, O_CREAT | O_RDWR, 0);
1028 ASSERT_LE(0, fd) << strerror(errno);
1029 ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno);
1030 leak(fd);
1031 }
1032
1033 /*
1034 * Setting the sgid bit should fail for an unprivileged user who doesn't belong
1035 * to the file's group
1036 */
TEST_F(Setattr,sgid_by_non_group_member)1037 TEST_F(Setattr, sgid_by_non_group_member)
1038 {
1039 const char FULLPATH[] = "mountpoint/some_file.txt";
1040 const char RELPATH[] = "some_file.txt";
1041 const uint64_t ino = 42;
1042 const mode_t oldmode = 0755;
1043 const mode_t newmode = 02755;
1044 uid_t uid = geteuid();
1045 gid_t gid = excluded_group();
1046
1047 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1048 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
1049 EXPECT_CALL(*m_mock, process(
1050 ResultOf([](auto in) {
1051 return (in.header.opcode == FUSE_SETATTR);
1052 }, Eq(true)),
1053 _)
1054 ).Times(0);
1055
1056 EXPECT_NE(0, chmod(FULLPATH, newmode));
1057 EXPECT_EQ(EPERM, errno);
1058 }
1059
1060 /* Only the superuser may set the sticky bit on a non-directory */
TEST_F(Setattr,sticky_regular_file)1061 TEST_F(Setattr, sticky_regular_file)
1062 {
1063 const char FULLPATH[] = "mountpoint/some_file.txt";
1064 const char RELPATH[] = "some_file.txt";
1065 const uint64_t ino = 42;
1066 const mode_t oldmode = 0644;
1067 const mode_t newmode = 01644;
1068
1069 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1070 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
1071 EXPECT_CALL(*m_mock, process(
1072 ResultOf([](auto in) {
1073 return (in.header.opcode == FUSE_SETATTR);
1074 }, Eq(true)),
1075 _)
1076 ).Times(0);
1077
1078 EXPECT_NE(0, chmod(FULLPATH, newmode));
1079 EXPECT_EQ(EFTYPE, errno);
1080 }
1081
TEST_F(Setextattr,ok)1082 TEST_F(Setextattr, ok)
1083 {
1084 const char FULLPATH[] = "mountpoint/some_file.txt";
1085 const char RELPATH[] = "some_file.txt";
1086 uint64_t ino = 42;
1087 const char value[] = "whatever";
1088 ssize_t value_len = strlen(value) + 1;
1089 int ns = EXTATTR_NAMESPACE_USER;
1090 ssize_t r;
1091
1092 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1093 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
1094 expect_setxattr(0);
1095
1096 r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
1097 value_len);
1098 ASSERT_EQ(value_len, r) << strerror(errno);
1099 }
1100
TEST_F(Setextattr,eacces)1101 TEST_F(Setextattr, eacces)
1102 {
1103 const char FULLPATH[] = "mountpoint/some_file.txt";
1104 const char RELPATH[] = "some_file.txt";
1105 uint64_t ino = 42;
1106 const char value[] = "whatever";
1107 ssize_t value_len = strlen(value) + 1;
1108 int ns = EXTATTR_NAMESPACE_USER;
1109
1110 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1111 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
1112
1113 ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
1114 value_len));
1115 ASSERT_EQ(EACCES, errno);
1116 }
1117
1118 // Setting system attributes requires superuser privileges
TEST_F(Setextattr,system)1119 TEST_F(Setextattr, system)
1120 {
1121 const char FULLPATH[] = "mountpoint/some_file.txt";
1122 const char RELPATH[] = "some_file.txt";
1123 uint64_t ino = 42;
1124 const char value[] = "whatever";
1125 ssize_t value_len = strlen(value) + 1;
1126 int ns = EXTATTR_NAMESPACE_SYSTEM;
1127
1128 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1129 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
1130
1131 ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
1132 value_len));
1133 ASSERT_EQ(EPERM, errno);
1134 }
1135
1136 // Setting user attributes merely requires write privileges
TEST_F(Setextattr,user)1137 TEST_F(Setextattr, user)
1138 {
1139 const char FULLPATH[] = "mountpoint/some_file.txt";
1140 const char RELPATH[] = "some_file.txt";
1141 uint64_t ino = 42;
1142 const char value[] = "whatever";
1143 ssize_t value_len = strlen(value) + 1;
1144 int ns = EXTATTR_NAMESPACE_USER;
1145 ssize_t r;
1146
1147 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1148 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
1149 expect_setxattr(0);
1150
1151 r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
1152 value_len);
1153 ASSERT_EQ(value_len, r) << strerror(errno);
1154 }
1155
TEST_F(Unlink,ok)1156 TEST_F(Unlink, ok)
1157 {
1158 const char FULLPATH[] = "mountpoint/some_file.txt";
1159 const char RELPATH[] = "some_file.txt";
1160 uint64_t ino = 42;
1161 sem_t sem;
1162
1163 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
1164
1165 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
1166 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
1167 expect_unlink(FUSE_ROOT_ID, RELPATH, 0);
1168 expect_forget(ino, 1, &sem);
1169
1170 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
1171
1172 sem_wait(&sem);
1173 sem_destroy(&sem);
1174 }
1175
1176 /*
1177 * Ensure that a cached name doesn't cause unlink to bypass permission checks
1178 * in VOP_LOOKUP.
1179 *
1180 * This test should pass because lookup(9) purges the namecache entry by doing
1181 * a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE.
1182 */
TEST_F(Unlink,cached_unwritable_directory)1183 TEST_F(Unlink, cached_unwritable_directory)
1184 {
1185 const char FULLPATH[] = "mountpoint/some_file.txt";
1186 const char RELPATH[] = "some_file.txt";
1187 uint64_t ino = 42;
1188
1189 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1190 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
1191 .Times(AnyNumber())
1192 .WillRepeatedly(Invoke(
1193 ReturnImmediate([=](auto i __unused, auto& out) {
1194 SET_OUT_HEADER_LEN(out, entry);
1195 out.body.entry.attr.mode = S_IFREG | 0644;
1196 out.body.entry.nodeid = ino;
1197 out.body.entry.entry_valid = UINT64_MAX;
1198 }))
1199 );
1200
1201 /* Fill name cache */
1202 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
1203 /* Despite cached name , unlink should fail */
1204 ASSERT_EQ(-1, unlink(FULLPATH));
1205 ASSERT_EQ(EACCES, errno);
1206 }
1207
TEST_F(Unlink,unwritable_directory)1208 TEST_F(Unlink, unwritable_directory)
1209 {
1210 const char FULLPATH[] = "mountpoint/some_file.txt";
1211 const char RELPATH[] = "some_file.txt";
1212 uint64_t ino = 42;
1213
1214 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1215 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
1216
1217 ASSERT_EQ(-1, unlink(FULLPATH));
1218 ASSERT_EQ(EACCES, errno);
1219 }
1220
TEST_F(Unlink,sticky_directory)1221 TEST_F(Unlink, sticky_directory)
1222 {
1223 const char FULLPATH[] = "mountpoint/some_file.txt";
1224 const char RELPATH[] = "some_file.txt";
1225 uint64_t ino = 42;
1226
1227 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1);
1228 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
1229
1230 ASSERT_EQ(-1, unlink(FULLPATH));
1231 ASSERT_EQ(EPERM, errno);
1232 }
1233
1234 /* A write by a non-owner should clear a file's SUID bit */
TEST_F(Write,clear_suid)1235 TEST_F(Write, clear_suid)
1236 {
1237 const char FULLPATH[] = "mountpoint/some_file.txt";
1238 const char RELPATH[] = "some_file.txt";
1239 struct stat sb;
1240 uint64_t ino = 42;
1241 mode_t oldmode = 04777;
1242 mode_t newmode = 0777;
1243 char wbuf[1] = {'x'};
1244 int fd;
1245
1246 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1247 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
1248 expect_open(ino, 0, 1);
1249 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
1250 expect_chmod(ino, newmode, sizeof(wbuf));
1251
1252 fd = open(FULLPATH, O_WRONLY);
1253 ASSERT_LE(0, fd) << strerror(errno);
1254 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
1255 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
1256 EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
1257 leak(fd);
1258 }
1259
1260 /* A write by a non-owner should clear a file's SGID bit */
TEST_F(Write,clear_sgid)1261 TEST_F(Write, clear_sgid)
1262 {
1263 const char FULLPATH[] = "mountpoint/some_file.txt";
1264 const char RELPATH[] = "some_file.txt";
1265 struct stat sb;
1266 uint64_t ino = 42;
1267 mode_t oldmode = 02777;
1268 mode_t newmode = 0777;
1269 char wbuf[1] = {'x'};
1270 int fd;
1271
1272 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1273 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
1274 expect_open(ino, 0, 1);
1275 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
1276 expect_chmod(ino, newmode, sizeof(wbuf));
1277
1278 fd = open(FULLPATH, O_WRONLY);
1279 ASSERT_LE(0, fd) << strerror(errno);
1280 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
1281 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
1282 EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
1283 leak(fd);
1284 }
1285
1286 /* Regression test for a specific recurse-of-nonrecursive-lock panic
1287 *
1288 * With writeback caching, we can't call vtruncbuf from fuse_io_strategy, or it
1289 * may panic. That happens if the FUSE_SETATTR response indicates that the
1290 * file's size has changed since the write.
1291 */
TEST_F(Write,recursion_panic_while_clearing_suid)1292 TEST_F(Write, recursion_panic_while_clearing_suid)
1293 {
1294 const char FULLPATH[] = "mountpoint/some_file.txt";
1295 const char RELPATH[] = "some_file.txt";
1296 uint64_t ino = 42;
1297 mode_t oldmode = 04777;
1298 mode_t newmode = 0777;
1299 char wbuf[1] = {'x'};
1300 int fd;
1301
1302 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
1303 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
1304 expect_open(ino, 0, 1);
1305 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
1306 /* XXX Return a smaller file size than what we just wrote! */
1307 expect_chmod(ino, newmode, 0);
1308
1309 fd = open(FULLPATH, O_WRONLY);
1310 ASSERT_LE(0, fd) << strerror(errno);
1311 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
1312 leak(fd);
1313 }
1314
1315
1316