]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - tests/sys/fs/fusefs/default_permissions.cc
fusefs: drop suid after a successful chown by a non-root user
[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 /* Unprivileged users may chown a file to their own uid */
298 TEST_F(Chown, chown_to_self)
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         uid_t uid;
305
306         uid = geteuid();
307
308         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1, uid);
309         expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid);
310         /* The OS may optimize chown by omitting the redundant setattr */
311         EXPECT_CALL(*m_mock, process(
312                 ResultOf([](auto in) {
313                         return (in->header.opcode == FUSE_SETATTR);
314                 }, Eq(true)),
315                 _)
316         ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out){
317                 SET_OUT_HEADER_LEN(out, attr);
318                 out->body.attr.attr.mode = S_IFREG | mode;
319                 out->body.attr.attr.uid = uid;
320         })));
321
322         EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
323 }
324
325 /*
326  * A successful chown by a non-privileged non-owner should clear a file's SUID
327  * bit
328  */
329 TEST_F(Chown, clear_suid)
330 {
331         const char FULLPATH[] = "mountpoint/some_file.txt";
332         const char RELPATH[] = "some_file.txt";
333         uint64_t ino = 42;
334         const mode_t oldmode = 06755;
335         const mode_t newmode = 0755;
336         uid_t uid = geteuid();
337         uint32_t valid = FATTR_UID | FATTR_MODE;
338
339         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1, uid);
340         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid);
341         EXPECT_CALL(*m_mock, process(
342                 ResultOf([=](auto in) {
343                         return (in->header.opcode == FUSE_SETATTR &&
344                                 in->header.nodeid == ino &&
345                                 in->body.setattr.valid == valid &&
346                                 in->body.setattr.mode == newmode);
347                 }, Eq(true)),
348                 _)
349         ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
350                 SET_OUT_HEADER_LEN(out, attr);
351                 out->body.attr.attr.ino = ino;  // Must match nodeid
352                 out->body.attr.attr.mode = S_IFREG | newmode;
353                 out->body.attr.attr_valid = UINT64_MAX;
354         })));
355
356         EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
357 }
358
359
360 /* Only root may change a file's owner */
361 TEST_F(Chown, eperm)
362 {
363         const char FULLPATH[] = "mountpoint/some_file.txt";
364         const char RELPATH[] = "some_file.txt";
365         const uint64_t ino = 42;
366         const mode_t mode = 0755;
367
368         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1, geteuid());
369         expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid());
370         EXPECT_CALL(*m_mock, process(
371                 ResultOf([](auto in) {
372                         return (in->header.opcode == FUSE_SETATTR);
373                 }, Eq(true)),
374                 _)
375         ).Times(0);
376
377         EXPECT_NE(0, chown(FULLPATH, 0, -1));
378         EXPECT_EQ(EPERM, errno);
379 }
380
381 /*
382  * A successful chgrp by a non-privileged non-owner should clear a file's SUID
383  * bit
384  */
385 TEST_F(Chgrp, clear_suid)
386 {
387         const char FULLPATH[] = "mountpoint/some_file.txt";
388         const char RELPATH[] = "some_file.txt";
389         uint64_t ino = 42;
390         const mode_t oldmode = 06755;
391         const mode_t newmode = 0755;
392         uid_t uid = geteuid();
393         gid_t gid = getegid();
394         uint32_t valid = FATTR_GID | FATTR_MODE;
395
396         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1, uid);
397         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
398         EXPECT_CALL(*m_mock, process(
399                 ResultOf([=](auto in) {
400                         return (in->header.opcode == FUSE_SETATTR &&
401                                 in->header.nodeid == ino &&
402                                 in->body.setattr.valid == valid &&
403                                 in->body.setattr.mode == newmode);
404                 }, Eq(true)),
405                 _)
406         ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
407                 SET_OUT_HEADER_LEN(out, attr);
408                 out->body.attr.attr.ino = ino;  // Must match nodeid
409                 out->body.attr.attr.mode = S_IFREG | newmode;
410                 out->body.attr.attr_valid = UINT64_MAX;
411         })));
412
413         EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno);
414 }
415
416 /* non-root users may only chgrp a file to a group they belong to */
417 TEST_F(Chgrp, eperm)
418 {
419         const char FULLPATH[] = "mountpoint/some_file.txt";
420         const char RELPATH[] = "some_file.txt";
421         const uint64_t ino = 42;
422         const mode_t mode = 0755;
423         uid_t uid;
424         gid_t gid, newgid;
425
426         uid = geteuid();
427         gid = getegid();
428         newgid = excluded_group();
429
430         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
431         expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
432         EXPECT_CALL(*m_mock, process(
433                 ResultOf([](auto in) {
434                         return (in->header.opcode == FUSE_SETATTR);
435                 }, Eq(true)),
436                 _)
437         ).Times(0);
438
439         EXPECT_NE(0, chown(FULLPATH, -1, newgid));
440         EXPECT_EQ(EPERM, errno);
441 }
442
443 TEST_F(Chgrp, ok)
444 {
445         const char FULLPATH[] = "mountpoint/some_file.txt";
446         const char RELPATH[] = "some_file.txt";
447         const uint64_t ino = 42;
448         const mode_t mode = 0755;
449         uid_t uid;
450         gid_t gid, newgid;
451
452         uid = geteuid();
453         gid = 0;
454         newgid = getegid();
455
456         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
457         expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
458         /* The OS may optimize chgrp by omitting the redundant setattr */
459         EXPECT_CALL(*m_mock, process(
460                 ResultOf([](auto in) {
461                         return (in->header.opcode == FUSE_SETATTR &&
462                                 in->header.nodeid == ino);
463                 }, Eq(true)),
464                 _)
465         ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out){
466                 SET_OUT_HEADER_LEN(out, attr);
467                 out->body.attr.attr.mode = S_IFREG | mode;
468                 out->body.attr.attr.uid = uid;
469                 out->body.attr.attr.gid = newgid;
470         })));
471
472         EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno);
473 }
474
475 TEST_F(Create, ok)
476 {
477         const char FULLPATH[] = "mountpoint/some_file.txt";
478         const char RELPATH[] = "some_file.txt";
479         uint64_t ino = 42;
480         int fd;
481
482         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1);
483         EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
484         expect_create(RELPATH, ino);
485
486         fd = open(FULLPATH, O_CREAT | O_EXCL, 0644);
487         EXPECT_LE(0, fd) << strerror(errno);
488         /* Deliberately leak fd.  close(2) will be tested in release.cc */
489 }
490
491 TEST_F(Create, eacces)
492 {
493         const char FULLPATH[] = "mountpoint/some_file.txt";
494         const char RELPATH[] = "some_file.txt";
495
496         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
497         EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
498
499         EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, 0644));
500         EXPECT_EQ(EACCES, errno);
501 }
502
503 TEST_F(Deleteextattr, eacces)
504 {
505         const char FULLPATH[] = "mountpoint/some_file.txt";
506         const char RELPATH[] = "some_file.txt";
507         uint64_t ino = 42;
508         int ns = EXTATTR_NAMESPACE_USER;
509
510         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
511         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
512
513         ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
514         ASSERT_EQ(EACCES, errno);
515 }
516
517 TEST_F(Deleteextattr, ok)
518 {
519         const char FULLPATH[] = "mountpoint/some_file.txt";
520         const char RELPATH[] = "some_file.txt";
521         uint64_t ino = 42;
522         int ns = EXTATTR_NAMESPACE_USER;
523
524         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
525         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
526         expect_removexattr();
527
528         ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
529                 << strerror(errno);
530 }
531
532 /* Delete system attributes requires superuser privilege */
533 TEST_F(Deleteextattr, system)
534 {
535         const char FULLPATH[] = "mountpoint/some_file.txt";
536         const char RELPATH[] = "some_file.txt";
537         uint64_t ino = 42;
538         int ns = EXTATTR_NAMESPACE_SYSTEM;
539
540         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
541         expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
542
543         ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
544         ASSERT_EQ(EPERM, errno);
545 }
546
547 /* Deleting user attributes merely requires WRITE privilege */
548 TEST_F(Deleteextattr, user)
549 {
550         const char FULLPATH[] = "mountpoint/some_file.txt";
551         const char RELPATH[] = "some_file.txt";
552         uint64_t ino = 42;
553         int ns = EXTATTR_NAMESPACE_USER;
554
555         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
556         expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
557         expect_removexattr();
558
559         ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
560                 << strerror(errno);
561 }
562
563 TEST_F(Getextattr, eacces)
564 {
565         const char FULLPATH[] = "mountpoint/some_file.txt";
566         const char RELPATH[] = "some_file.txt";
567         uint64_t ino = 42;
568         char data[80];
569         int ns = EXTATTR_NAMESPACE_USER;
570
571         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
572         expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
573
574         ASSERT_EQ(-1,
575                 extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
576         ASSERT_EQ(EACCES, errno);
577 }
578
579 TEST_F(Getextattr, ok)
580 {
581         const char FULLPATH[] = "mountpoint/some_file.txt";
582         const char RELPATH[] = "some_file.txt";
583         uint64_t ino = 42;
584         char data[80];
585         const char value[] = "whatever";
586         ssize_t value_len = strlen(value) + 1;
587         int ns = EXTATTR_NAMESPACE_USER;
588         ssize_t r;
589
590         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
591         /* Getting user attributes only requires read access */
592         expect_lookup(RELPATH, ino, S_IFREG | 0444, UINT64_MAX, 0);
593         expect_getxattr(
594                 ReturnImmediate([&](auto in __unused, auto out) {
595                         memcpy((void*)out->body.bytes, value, value_len);
596                         out->header.len = sizeof(out->header) + value_len;
597                 })
598         );
599
600         r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
601         ASSERT_EQ(value_len, r)  << strerror(errno);
602         EXPECT_STREQ(value, data);
603 }
604
605 /* Getting system attributes requires superuser privileges */
606 TEST_F(Getextattr, system)
607 {
608         const char FULLPATH[] = "mountpoint/some_file.txt";
609         const char RELPATH[] = "some_file.txt";
610         uint64_t ino = 42;
611         char data[80];
612         int ns = EXTATTR_NAMESPACE_SYSTEM;
613
614         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
615         expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
616
617         ASSERT_EQ(-1,
618                 extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
619         ASSERT_EQ(EPERM, errno);
620 }
621
622 TEST_F(Listextattr, eacces)
623 {
624         const char FULLPATH[] = "mountpoint/some_file.txt";
625         const char RELPATH[] = "some_file.txt";
626         uint64_t ino = 42;
627         int ns = EXTATTR_NAMESPACE_USER;
628
629         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1);
630         expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
631
632         ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
633         ASSERT_EQ(EACCES, errno);
634 }
635
636 TEST_F(Listextattr, ok)
637 {
638         const char FULLPATH[] = "mountpoint/some_file.txt";
639         const char RELPATH[] = "some_file.txt";
640         uint64_t ino = 42;
641         int ns = EXTATTR_NAMESPACE_USER;
642
643         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1);
644         /* Listing user extended attributes merely requires read access */
645         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
646         expect_listxattr();
647
648         ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))
649                 << strerror(errno);
650 }
651
652 /* Listing system xattrs requires superuser privileges */
653 TEST_F(Listextattr, system)
654 {
655         const char FULLPATH[] = "mountpoint/some_file.txt";
656         const char RELPATH[] = "some_file.txt";
657         uint64_t ino = 42;
658         int ns = EXTATTR_NAMESPACE_SYSTEM;
659
660         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1);
661         /* Listing user extended attributes merely requires read access */
662         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
663
664         ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
665         ASSERT_EQ(EPERM, errno);
666 }
667
668 /* A component of the search path lacks execute permissions */
669 TEST_F(Lookup, eacces)
670 {
671         const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
672         const char RELDIRPATH[] = "some_dir";
673         uint64_t dir_ino = 42;
674
675         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
676         expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0);
677
678         EXPECT_EQ(-1, access(FULLPATH, F_OK));
679         EXPECT_EQ(EACCES, errno);
680 }
681
682 TEST_F(Open, eacces)
683 {
684         const char FULLPATH[] = "mountpoint/some_file.txt";
685         const char RELPATH[] = "some_file.txt";
686         uint64_t ino = 42;
687
688         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
689         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
690
691         EXPECT_NE(0, open(FULLPATH, O_RDWR));
692         EXPECT_EQ(EACCES, errno);
693 }
694
695 TEST_F(Open, ok)
696 {
697         const char FULLPATH[] = "mountpoint/some_file.txt";
698         const char RELPATH[] = "some_file.txt";
699         uint64_t ino = 42;
700         int fd;
701
702         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
703         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
704         expect_open(ino, 0, 1);
705
706         fd = open(FULLPATH, O_RDONLY);
707         EXPECT_LE(0, fd) << strerror(errno);
708         /* Deliberately leak fd.  close(2) will be tested in release.cc */
709 }
710
711 TEST_F(Rename, eacces_on_srcdir)
712 {
713         const char FULLDST[] = "mountpoint/d/dst";
714         const char RELDST[] = "d/dst";
715         const char FULLSRC[] = "mountpoint/src";
716         const char RELSRC[] = "src";
717         uint64_t ino = 42;
718
719         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1, 0);
720         expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
721         EXPECT_LOOKUP(1, RELDST)
722                 .Times(AnyNumber())
723                 .WillRepeatedly(Invoke(ReturnErrno(ENOENT)));
724
725         ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
726         ASSERT_EQ(EACCES, errno);
727 }
728
729 TEST_F(Rename, eacces_on_dstdir_for_creating)
730 {
731         const char FULLDST[] = "mountpoint/d/dst";
732         const char RELDSTDIR[] = "d";
733         const char RELDST[] = "dst";
734         const char FULLSRC[] = "mountpoint/src";
735         const char RELSRC[] = "src";
736         uint64_t src_ino = 42;
737         uint64_t dstdir_ino = 43;
738
739         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1, 0);
740         expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
741         expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
742         EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
743
744         ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
745         ASSERT_EQ(EACCES, errno);
746 }
747
748 TEST_F(Rename, eacces_on_dstdir_for_removing)
749 {
750         const char FULLDST[] = "mountpoint/d/dst";
751         const char RELDSTDIR[] = "d";
752         const char RELDST[] = "dst";
753         const char FULLSRC[] = "mountpoint/src";
754         const char RELSRC[] = "src";
755         uint64_t src_ino = 42;
756         uint64_t dstdir_ino = 43;
757
758         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1, 0);
759         expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
760         expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
761         EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
762
763         ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
764         ASSERT_EQ(EACCES, errno);
765 }
766
767 TEST_F(Rename, eperm_on_sticky_srcdir)
768 {
769         const char FULLDST[] = "mountpoint/d/dst";
770         const char FULLSRC[] = "mountpoint/src";
771         const char RELSRC[] = "src";
772         uint64_t ino = 42;
773
774         expect_getattr(1, S_IFDIR | 01777, UINT64_MAX, 1, 0);
775         expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
776
777         ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
778         ASSERT_EQ(EPERM, errno);
779 }
780
781 TEST_F(Rename, eperm_on_sticky_dstdir)
782 {
783         const char FULLDST[] = "mountpoint/d/dst";
784         const char RELDSTDIR[] = "d";
785         const char RELDST[] = "dst";
786         const char FULLSRC[] = "mountpoint/src";
787         const char RELSRC[] = "src";
788         uint64_t src_ino = 42;
789         uint64_t dstdir_ino = 43;
790         uint64_t dst_ino = 44;
791
792         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1, 0);
793         expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
794         expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX);
795         EXPECT_LOOKUP(dstdir_ino, RELDST)
796         .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
797                 SET_OUT_HEADER_LEN(out, entry);
798                 out->body.entry.attr.mode = S_IFREG | 0644;
799                 out->body.entry.nodeid = dst_ino;
800                 out->body.entry.attr_valid = UINT64_MAX;
801                 out->body.entry.entry_valid = UINT64_MAX;
802                 out->body.entry.attr.uid = 0;
803         })));
804
805         ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
806         ASSERT_EQ(EPERM, errno);
807 }
808
809 /* Successfully rename a file, overwriting the destination */
810 TEST_F(Rename, ok)
811 {
812         const char FULLDST[] = "mountpoint/dst";
813         const char RELDST[] = "dst";
814         const char FULLSRC[] = "mountpoint/src";
815         const char RELSRC[] = "src";
816         // The inode of the already-existing destination file
817         uint64_t dst_ino = 2;
818         uint64_t ino = 42;
819
820         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1, geteuid());
821         expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
822         expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX);
823         expect_rename(0);
824
825         ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
826 }
827
828 TEST_F(Rename, ok_to_remove_src_because_of_stickiness)
829 {
830         const char FULLDST[] = "mountpoint/dst";
831         const char RELDST[] = "dst";
832         const char FULLSRC[] = "mountpoint/src";
833         const char RELSRC[] = "src";
834         uint64_t ino = 42;
835
836         expect_getattr(1, S_IFDIR | 01777, UINT64_MAX, 1, 0);
837         expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
838         EXPECT_LOOKUP(1, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
839         expect_rename(0);
840
841         ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
842 }
843
844 TEST_F(Setattr, ok)
845 {
846         const char FULLPATH[] = "mountpoint/some_file.txt";
847         const char RELPATH[] = "some_file.txt";
848         const uint64_t ino = 42;
849         const mode_t oldmode = 0755;
850         const mode_t newmode = 0644;
851
852         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
853         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
854         EXPECT_CALL(*m_mock, process(
855                 ResultOf([](auto in) {
856                         return (in->header.opcode == FUSE_SETATTR &&
857                                 in->header.nodeid == ino &&
858                                 in->body.setattr.mode == newmode);
859                 }, Eq(true)),
860                 _)
861         ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) {
862                 SET_OUT_HEADER_LEN(out, attr);
863                 out->body.attr.attr.mode = S_IFREG | newmode;
864         })));
865
866         EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
867 }
868
869 TEST_F(Setattr, eacces)
870 {
871         const char FULLPATH[] = "mountpoint/some_file.txt";
872         const char RELPATH[] = "some_file.txt";
873         const uint64_t ino = 42;
874         const mode_t oldmode = 0755;
875         const mode_t newmode = 0644;
876
877         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
878         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0);
879         EXPECT_CALL(*m_mock, process(
880                 ResultOf([](auto in) {
881                         return (in->header.opcode == FUSE_SETATTR);
882                 }, Eq(true)),
883                 _)
884         ).Times(0);
885
886         EXPECT_NE(0, chmod(FULLPATH, newmode));
887         EXPECT_EQ(EPERM, errno);
888 }
889
890 /*
891  * ftruncate() of a file without writable permissions should succeed as long as
892  * the file descriptor is writable.  This is important when combined with
893  * O_CREAT
894  */
895 TEST_F(Setattr, ftruncate_of_newly_created_file)
896 {
897         const char FULLPATH[] = "mountpoint/some_file.txt";
898         const char RELPATH[] = "some_file.txt";
899         const uint64_t ino = 42;
900         const mode_t mode = 0000;
901         int fd;
902
903         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1);
904         EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
905         expect_create(RELPATH, ino);
906         EXPECT_CALL(*m_mock, process(
907                 ResultOf([](auto in) {
908                         return (in->header.opcode == FUSE_SETATTR &&
909                                 in->header.nodeid == ino &&
910                                 (in->body.setattr.valid & FATTR_SIZE));
911                 }, Eq(true)),
912                 _)
913         ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
914                 SET_OUT_HEADER_LEN(out, attr);
915                 out->body.attr.attr.ino = ino;
916                 out->body.attr.attr.mode = S_IFREG | mode;
917                 out->body.attr.attr_valid = UINT64_MAX;
918         })));
919
920         fd = open(FULLPATH, O_CREAT | O_RDWR, 0);
921         ASSERT_LE(0, fd) << strerror(errno);
922         ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno);
923         /* Deliberately leak fd */
924 }
925
926 /* 
927  * Setting the sgid bit should fail for an unprivileged user who doesn't belong
928  * to the file's group
929  */
930 TEST_F(Setattr, sgid_by_non_group_member)
931 {
932         const char FULLPATH[] = "mountpoint/some_file.txt";
933         const char RELPATH[] = "some_file.txt";
934         const uint64_t ino = 42;
935         const mode_t oldmode = 0755;
936         const mode_t newmode = 02755;
937         uid_t uid = geteuid();
938         gid_t gid = excluded_group();
939
940         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
941         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
942         EXPECT_CALL(*m_mock, process(
943                 ResultOf([](auto in) {
944                         return (in->header.opcode == FUSE_SETATTR);
945                 }, Eq(true)),
946                 _)
947         ).Times(0);
948
949         EXPECT_NE(0, chmod(FULLPATH, newmode));
950         EXPECT_EQ(EPERM, errno);
951 }
952
953 /* Only the superuser may set the sticky bit on a non-directory */
954 TEST_F(Setattr, sticky_regular_file)
955 {
956         const char FULLPATH[] = "mountpoint/some_file.txt";
957         const char RELPATH[] = "some_file.txt";
958         const uint64_t ino = 42;
959         const mode_t oldmode = 0644;
960         const mode_t newmode = 01644;
961
962         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
963         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
964         EXPECT_CALL(*m_mock, process(
965                 ResultOf([](auto in) {
966                         return (in->header.opcode == FUSE_SETATTR);
967                 }, Eq(true)),
968                 _)
969         ).Times(0);
970
971         EXPECT_NE(0, chmod(FULLPATH, newmode));
972         EXPECT_EQ(EFTYPE, errno);
973 }
974
975 TEST_F(Setextattr, ok)
976 {
977         const char FULLPATH[] = "mountpoint/some_file.txt";
978         const char RELPATH[] = "some_file.txt";
979         uint64_t ino = 42;
980         const char value[] = "whatever";
981         ssize_t value_len = strlen(value) + 1;
982         int ns = EXTATTR_NAMESPACE_USER;
983         ssize_t r;
984
985         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
986         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
987         expect_setxattr(0);
988
989         r = extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len);
990         ASSERT_EQ(value_len, r) << strerror(errno);
991 }
992
993 TEST_F(Setextattr, eacces)
994 {
995         const char FULLPATH[] = "mountpoint/some_file.txt";
996         const char RELPATH[] = "some_file.txt";
997         uint64_t ino = 42;
998         const char value[] = "whatever";
999         ssize_t value_len = strlen(value) + 1;
1000         int ns = EXTATTR_NAMESPACE_USER;
1001
1002         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
1003         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
1004
1005         ASSERT_EQ(-1,
1006                 extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len));
1007         ASSERT_EQ(EACCES, errno);
1008 }
1009
1010 // Setting system attributes requires superuser privileges
1011 TEST_F(Setextattr, system)
1012 {
1013         const char FULLPATH[] = "mountpoint/some_file.txt";
1014         const char RELPATH[] = "some_file.txt";
1015         uint64_t ino = 42;
1016         const char value[] = "whatever";
1017         ssize_t value_len = strlen(value) + 1;
1018         int ns = EXTATTR_NAMESPACE_SYSTEM;
1019
1020         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
1021         expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
1022
1023         ASSERT_EQ(-1,
1024                 extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len));
1025         ASSERT_EQ(EPERM, errno);
1026 }
1027
1028 // Setting user attributes merely requires write privileges
1029 TEST_F(Setextattr, user)
1030 {
1031         const char FULLPATH[] = "mountpoint/some_file.txt";
1032         const char RELPATH[] = "some_file.txt";
1033         uint64_t ino = 42;
1034         const char value[] = "whatever";
1035         ssize_t value_len = strlen(value) + 1;
1036         int ns = EXTATTR_NAMESPACE_USER;
1037         ssize_t r;
1038
1039         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
1040         expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
1041         expect_setxattr(0);
1042
1043         r = extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len);
1044         ASSERT_EQ(value_len, r) << strerror(errno);
1045 }
1046
1047 TEST_F(Unlink, ok)
1048 {
1049         const char FULLPATH[] = "mountpoint/some_file.txt";
1050         const char RELPATH[] = "some_file.txt";
1051         uint64_t ino = 42;
1052
1053         expect_getattr(1, S_IFDIR | 0777, UINT64_MAX, 1);
1054         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
1055         expect_unlink(1, RELPATH, 0);
1056
1057         ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
1058 }
1059
1060 /*
1061  * Ensure that a cached name doesn't cause unlink to bypass permission checks
1062  * in VOP_LOOKUP.
1063  *
1064  * This test should pass because lookup(9) purges the namecache entry by doing
1065  * a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE.
1066  */
1067 TEST_F(Unlink, cached_unwritable_directory)
1068 {
1069         const char FULLPATH[] = "mountpoint/some_file.txt";
1070         const char RELPATH[] = "some_file.txt";
1071         uint64_t ino = 42;
1072
1073         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
1074         EXPECT_LOOKUP(1, RELPATH)
1075         .Times(AnyNumber())
1076         .WillRepeatedly(Invoke(
1077                 ReturnImmediate([=](auto i __unused, auto out) {
1078                         SET_OUT_HEADER_LEN(out, entry);
1079                         out->body.entry.attr.mode = S_IFREG | 0644;
1080                         out->body.entry.nodeid = ino;
1081                         out->body.entry.entry_valid = UINT64_MAX;
1082                 }))
1083         );
1084
1085         /* Fill name cache */
1086         ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
1087         /* Despite cached name , unlink should fail */
1088         ASSERT_EQ(-1, unlink(FULLPATH));
1089         ASSERT_EQ(EACCES, errno);
1090 }
1091
1092 TEST_F(Unlink, unwritable_directory)
1093 {
1094         const char FULLPATH[] = "mountpoint/some_file.txt";
1095         const char RELPATH[] = "some_file.txt";
1096         uint64_t ino = 42;
1097
1098         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
1099         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
1100
1101         ASSERT_EQ(-1, unlink(FULLPATH));
1102         ASSERT_EQ(EACCES, errno);
1103 }
1104
1105 TEST_F(Unlink, sticky_directory)
1106 {
1107         const char FULLPATH[] = "mountpoint/some_file.txt";
1108         const char RELPATH[] = "some_file.txt";
1109         uint64_t ino = 42;
1110
1111         expect_getattr(1, S_IFDIR | 01777, UINT64_MAX, 1);
1112         expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
1113
1114         ASSERT_EQ(-1, unlink(FULLPATH));
1115         ASSERT_EQ(EPERM, errno);
1116 }
1117
1118 /* A write by a non-owner should clear a file's SUID bit */
1119 TEST_F(Write, clear_suid)
1120 {
1121         const char FULLPATH[] = "mountpoint/some_file.txt";
1122         const char RELPATH[] = "some_file.txt";
1123         struct stat sb;
1124         uint64_t ino = 42;
1125         mode_t oldmode = 04777;
1126         mode_t newmode = 0777;
1127         char wbuf[1] = {'x'};
1128         int fd;
1129
1130         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
1131         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
1132         expect_open(ino, 0, 1);
1133         expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, wbuf);
1134         expect_chmod(ino, newmode);
1135
1136         fd = open(FULLPATH, O_WRONLY);
1137         ASSERT_LE(0, fd) << strerror(errno);
1138         ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
1139         ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
1140         EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
1141         /* Deliberately leak fd.  close(2) will be tested in release.cc */
1142 }
1143
1144 /* A write by a non-owner should clear a file's SGID bit */
1145 TEST_F(Write, clear_sgid)
1146 {
1147         const char FULLPATH[] = "mountpoint/some_file.txt";
1148         const char RELPATH[] = "some_file.txt";
1149         struct stat sb;
1150         uint64_t ino = 42;
1151         mode_t oldmode = 02777;
1152         mode_t newmode = 0777;
1153         char wbuf[1] = {'x'};
1154         int fd;
1155
1156         expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
1157         expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
1158         expect_open(ino, 0, 1);
1159         expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, wbuf);
1160         expect_chmod(ino, newmode);
1161
1162         fd = open(FULLPATH, O_WRONLY);
1163         ASSERT_LE(0, fd) << strerror(errno);
1164         ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
1165         ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
1166         EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
1167         /* Deliberately leak fd.  close(2) will be tested in release.cc */
1168 }