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