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