xref: /freebsd/tests/sys/fs/fusefs/xattr.cc (revision 62cfcf62f627e5093fb37026a6d8c98e4d2ef04c)
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 /* Tests for all things relating to extended attributes and FUSE */
34 
35 extern "C" {
36 #include <sys/types.h>
37 #include <sys/extattr.h>
38 #include <sys/wait.h>
39 #include <semaphore.h>
40 #include <signal.h>
41 #include <string.h>
42 }
43 
44 #include "mockfs.hh"
45 #include "utils.hh"
46 
47 using namespace testing;
48 
49 const char FULLPATH[] = "mountpoint/some_file.txt";
50 const char RELPATH[] = "some_file.txt";
51 static sem_t killer_semaphore;
52 
53 void* killer(void* target) {
54 	pid_t pid = *(pid_t*)target;
55 	sem_wait(&killer_semaphore);
56 	if (verbosity > 1)
57 		printf("Killing! pid %d\n", pid);
58 	kill(pid, SIGINT);
59 
60 	return(NULL);
61 }
62 
63 class Xattr: public FuseTest {
64 public:
65 void expect_listxattr(uint64_t ino, uint32_t size, ProcessMockerT r,
66     Sequence *seq = NULL)
67 {
68 	if (seq == NULL) {
69 		EXPECT_CALL(*m_mock, process(
70 			ResultOf([=](auto in) {
71 				return (in.header.opcode == FUSE_LISTXATTR &&
72 					in.header.nodeid == ino &&
73 					in.body.listxattr.size == size);
74 			}, Eq(true)),
75 			_)
76 		).WillOnce(Invoke(r))
77 		.RetiresOnSaturation();
78 	} else {
79 		EXPECT_CALL(*m_mock, process(
80 			ResultOf([=](auto in) {
81 				return (in.header.opcode == FUSE_LISTXATTR &&
82 					in.header.nodeid == ino &&
83 					in.body.listxattr.size == size);
84 			}, Eq(true)),
85 			_)
86 		).InSequence(*seq)
87 		.WillOnce(Invoke(r))
88 		.RetiresOnSaturation();
89 	}
90 }
91 
92 void expect_removexattr(uint64_t ino, const char *attr, int error)
93 {
94 	EXPECT_CALL(*m_mock, process(
95 		ResultOf([=](auto in) {
96 			const char *a = (const char*)in.body.bytes;
97 			return (in.header.opcode == FUSE_REMOVEXATTR &&
98 				in.header.nodeid == ino &&
99 				0 == strcmp(attr, a));
100 		}, Eq(true)),
101 		_)
102 	).WillOnce(Invoke(ReturnErrno(error)));
103 }
104 
105 void expect_setxattr(uint64_t ino, const char *attr, const char *value,
106 	ProcessMockerT r)
107 {
108 	EXPECT_CALL(*m_mock, process(
109 		ResultOf([=](auto in) {
110 			const char *a = (const char*)in.body.bytes +
111 				sizeof(fuse_setxattr_in);
112 			const char *v = a + strlen(a) + 1;
113 			return (in.header.opcode == FUSE_SETXATTR &&
114 				in.header.nodeid == ino &&
115 				0 == strcmp(attr, a) &&
116 				0 == strcmp(value, v));
117 		}, Eq(true)),
118 		_)
119 	).WillOnce(Invoke(r));
120 }
121 
122 };
123 
124 class Getxattr: public Xattr {};
125 
126 class Listxattr: public Xattr {};
127 
128 /* Listxattr tests that need to use a signal */
129 class ListxattrSig: public Listxattr {
130 public:
131 pthread_t m_killer_th;
132 pid_t m_child;
133 
134 void SetUp() {
135 	/*
136 	 * Mount with -o nointr so the mount can't get interrupted while
137 	 * waiting for a response from the server
138 	 */
139 	m_nointr = true;
140 	FuseTest::SetUp();
141 
142 	ASSERT_EQ(0, sem_init(&killer_semaphore, 0, 0)) << strerror(errno);
143 }
144 
145 void TearDown() {
146 	if (m_killer_th != NULL) {
147 		pthread_join(m_killer_th, NULL);
148 	}
149 
150 	sem_destroy(&killer_semaphore);
151 
152 	FuseTest::TearDown();
153 }
154 };
155 
156 class Removexattr: public Xattr {};
157 class Setxattr: public Xattr {};
158 class RofsXattr: public Xattr {
159 public:
160 virtual void SetUp() {
161 	m_ro = true;
162 	Xattr::SetUp();
163 }
164 };
165 
166 /*
167  * If the extended attribute does not exist on this file, the daemon should
168  * return ENOATTR (ENODATA on Linux, but it's up to the daemon to choose the
169  * correct errror code)
170  */
171 TEST_F(Getxattr, enoattr)
172 {
173 	char data[80];
174 	uint64_t ino = 42;
175 	int ns = EXTATTR_NAMESPACE_USER;
176 	ssize_t r;
177 
178 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
179 	expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR));
180 
181 	r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
182 	ASSERT_EQ(-1, r);
183 	ASSERT_EQ(ENOATTR, errno);
184 }
185 
186 /*
187  * If the filesystem returns ENOSYS, then it will be treated as a permanent
188  * failure and all future VOP_GETEXTATTR calls will fail with EOPNOTSUPP
189  * without querying the filesystem daemon
190  */
191 TEST_F(Getxattr, enosys)
192 {
193 	char data[80];
194 	uint64_t ino = 42;
195 	int ns = EXTATTR_NAMESPACE_USER;
196 	ssize_t r;
197 
198 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
199 	expect_getxattr(ino, "user.foo", ReturnErrno(ENOSYS));
200 
201 	r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
202 	ASSERT_EQ(-1, r);
203 	EXPECT_EQ(EOPNOTSUPP, errno);
204 
205 	/* Subsequent attempts should not query the filesystem at all */
206 	r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
207 	ASSERT_EQ(-1, r);
208 	EXPECT_EQ(EOPNOTSUPP, errno);
209 }
210 
211 /*
212  * On FreeBSD, if the user passes an insufficiently large buffer then the
213  * filesystem is supposed to copy as much of the attribute's value as will fit.
214  *
215  * On Linux, however, the filesystem is supposed to return ERANGE.
216  *
217  * libfuse specifies the Linux behavior.  However, that's probably an error.
218  * It would probably be correct for the filesystem to use platform-dependent
219  * behavior.
220  *
221  * This test case covers a filesystem that uses the Linux behavior
222  * TODO: require FreeBSD Behavior.
223  */
224 TEST_F(Getxattr, erange)
225 {
226 	char data[10];
227 	uint64_t ino = 42;
228 	int ns = EXTATTR_NAMESPACE_USER;
229 	ssize_t r;
230 
231 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
232 	expect_getxattr(ino, "user.foo", ReturnErrno(ERANGE));
233 
234 	r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
235 	ASSERT_EQ(-1, r);
236 	ASSERT_EQ(ERANGE, errno);
237 }
238 
239 /*
240  * If the user passes a 0-length buffer, then the daemon should just return the
241  * size of the attribute
242  */
243 TEST_F(Getxattr, size_only)
244 {
245 	uint64_t ino = 42;
246 	int ns = EXTATTR_NAMESPACE_USER;
247 
248 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
249 	expect_getxattr(ino, "user.foo",
250 		ReturnImmediate([](auto in __unused, auto& out) {
251 			SET_OUT_HEADER_LEN(out, getxattr);
252 			out.body.getxattr.size = 99;
253 		})
254 	);
255 
256 	ASSERT_EQ(99, extattr_get_file(FULLPATH, ns, "foo", NULL, 0))
257 		<< strerror(errno);;
258 }
259 
260 /*
261  * Successfully get an attribute from the system namespace
262  */
263 TEST_F(Getxattr, system)
264 {
265 	uint64_t ino = 42;
266 	char data[80];
267 	const char value[] = "whatever";
268 	ssize_t value_len = strlen(value) + 1;
269 	int ns = EXTATTR_NAMESPACE_SYSTEM;
270 	ssize_t r;
271 
272 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
273 	expect_getxattr(ino, "system.foo",
274 		ReturnImmediate([&](auto in __unused, auto& out) {
275 			memcpy((void*)out.body.bytes, value, value_len);
276 			out.header.len = sizeof(out.header) + value_len;
277 		})
278 	);
279 
280 	r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
281 	ASSERT_EQ(value_len, r)  << strerror(errno);
282 	EXPECT_STREQ(value, data);
283 }
284 
285 /*
286  * Successfully get an attribute from the user namespace
287  */
288 TEST_F(Getxattr, user)
289 {
290 	uint64_t ino = 42;
291 	char data[80];
292 	const char value[] = "whatever";
293 	ssize_t value_len = strlen(value) + 1;
294 	int ns = EXTATTR_NAMESPACE_USER;
295 	ssize_t r;
296 
297 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
298 	expect_getxattr(ino, "user.foo",
299 		ReturnImmediate([&](auto in __unused, auto& out) {
300 			memcpy((void*)out.body.bytes, value, value_len);
301 			out.header.len = sizeof(out.header) + value_len;
302 		})
303 	);
304 
305 	r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
306 	ASSERT_EQ(value_len, r)  << strerror(errno);
307 	EXPECT_STREQ(value, data);
308 }
309 
310 /*
311  * If the filesystem returns ENOSYS, then it will be treated as a permanent
312  * failure and all future VOP_LISTEXTATTR calls will fail with EOPNOTSUPP
313  * without querying the filesystem daemon
314  */
315 TEST_F(Listxattr, enosys)
316 {
317 	uint64_t ino = 42;
318 	int ns = EXTATTR_NAMESPACE_USER;
319 
320 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
321 	expect_listxattr(ino, 0, ReturnErrno(ENOSYS));
322 
323 	ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
324 	EXPECT_EQ(EOPNOTSUPP, errno);
325 
326 	/* Subsequent attempts should not query the filesystem at all */
327 	ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
328 	EXPECT_EQ(EOPNOTSUPP, errno);
329 }
330 
331 /*
332  * Listing extended attributes failed because they aren't configured on this
333  * filesystem
334  */
335 TEST_F(Listxattr, enotsup)
336 {
337 	uint64_t ino = 42;
338 	int ns = EXTATTR_NAMESPACE_USER;
339 
340 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
341 	expect_listxattr(ino, 0, ReturnErrno(ENOTSUP));
342 
343 	ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
344 	ASSERT_EQ(ENOTSUP, errno);
345 }
346 
347 /*
348  * On FreeBSD, if the user passes an insufficiently large buffer to
349  * extattr_list_file(2) or VOP_LISTEXTATTR(9), then the file system is supposed
350  * to copy as much of the attribute's value as will fit.
351  *
352  * On Linux, however, the file system is supposed to return ERANGE if an
353  * insufficiently large buffer is passed to listxattr(2).
354  *
355  * fusefs(5) must guarantee the usual FreeBSD behavior.
356  */
357 TEST_F(Listxattr, erange)
358 {
359 	uint64_t ino = 42;
360 	int ns = EXTATTR_NAMESPACE_USER;
361 	char attrs[9] = "user.foo";
362 	char expected[3] = {3, 'f', 'o'};
363 	char buf[3];
364 
365 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
366 	expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
367 	{
368 		out.body.listxattr.size = sizeof(attrs);
369 		SET_OUT_HEADER_LEN(out, listxattr);
370 	}));
371 	expect_listxattr(ino, sizeof(attrs),
372 	ReturnImmediate([&](auto in __unused, auto& out) {
373 		memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
374 		out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
375 	}));
376 
377 
378 	ASSERT_EQ(static_cast<ssize_t>(sizeof(buf)),
379 		  extattr_list_file(FULLPATH, ns, buf, sizeof(buf)));
380 	ASSERT_EQ(0, memcmp(expected, buf, sizeof(buf)));
381 }
382 
383 /*
384  * A buggy or malicious file system always returns ERANGE, even if we pass an
385  * appropriately sized buffer.  That will send the kernel into an infinite
386  * loop.  This test will ensure that the loop is interruptible by killing the
387  * blocked process with SIGINT.
388  */
389 TEST_F(ListxattrSig, erange_forever)
390 {
391 	uint64_t ino = 42;
392 	uint32_t lie_size = 10;
393 	int status;
394 
395 	fork(false, &status, [&] {
396 		EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
397 		.WillRepeatedly(Invoke(
398 			ReturnImmediate([=](auto in __unused, auto& out) {
399 			SET_OUT_HEADER_LEN(out, entry);
400 			out.body.entry.attr.mode = S_IFREG | 0644;
401 			out.body.entry.nodeid = ino;
402 			out.body.entry.attr.nlink = 1;
403 			out.body.entry.attr_valid = UINT64_MAX;
404 			out.body.entry.entry_valid = UINT64_MAX;
405 		})));
406 		EXPECT_CALL(*m_mock, process(
407 			ResultOf([=](auto in) {
408 				return (in.header.opcode == FUSE_LISTXATTR &&
409 					in.header.nodeid == ino &&
410 					in.body.listxattr.size == 0);
411 			}, Eq(true)),
412 			_)
413 		).WillRepeatedly(ReturnImmediate([=](auto i __unused, auto& out)
414 		{
415 			/* The file system requests 10 bytes, but it's a lie */
416 			out.body.listxattr.size = lie_size;
417 			SET_OUT_HEADER_LEN(out, listxattr);
418 			/*
419 			 * We can send the signal any time after fusefs enters
420 			 * VOP_LISTEXTATTR
421 			 */
422 			sem_post(&killer_semaphore);
423 		}));
424 		/*
425 		 * Even though the kernel faithfully respects our size request,
426 		 * we'll return ERANGE anyway.
427 		 */
428 		EXPECT_CALL(*m_mock, process(
429 			ResultOf([=](auto in) {
430 				return (in.header.opcode == FUSE_LISTXATTR &&
431 					in.header.nodeid == ino &&
432 					in.body.listxattr.size == lie_size);
433 			}, Eq(true)),
434 			_)
435 		).WillRepeatedly(ReturnErrno(ERANGE));
436 
437 		ASSERT_EQ(0, pthread_create(&m_killer_th, NULL, killer,
438 					    &m_mock->m_child_pid))
439 			<< strerror(errno);
440 
441 	}, [] {
442 		/* Child process will block until it gets signaled */
443 		int ns = EXTATTR_NAMESPACE_USER;
444 		char buf[3];
445 		extattr_list_file(FULLPATH, ns, buf, sizeof(buf));
446 		return 0;
447 	}
448 	);
449 
450 	ASSERT_TRUE(WIFSIGNALED(status));
451 }
452 
453 /*
454  * Get the size of the list that it would take to list no extended attributes
455  */
456 TEST_F(Listxattr, size_only_empty)
457 {
458 	uint64_t ino = 42;
459 	int ns = EXTATTR_NAMESPACE_USER;
460 
461 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
462 	expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
463 		out.body.listxattr.size = 0;
464 		SET_OUT_HEADER_LEN(out, listxattr);
465 	}));
466 
467 	ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))
468 		<< strerror(errno);
469 }
470 
471 /*
472  * Get the size of the list that it would take to list some extended
473  * attributes.  Due to the format differences between a FreeBSD and a
474  * Linux/FUSE extended attribute list, fuse(4) will actually allocate a buffer
475  * and get the whole list, then convert it, just to figure out its size.
476  */
477 TEST_F(Listxattr, size_only_nonempty)
478 {
479 	uint64_t ino = 42;
480 	int ns = EXTATTR_NAMESPACE_USER;
481 	char attrs[9] = "user.foo";
482 
483 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
484 	expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
485 	{
486 		out.body.listxattr.size = sizeof(attrs);
487 		SET_OUT_HEADER_LEN(out, listxattr);
488 	}));
489 
490 	expect_listxattr(ino, sizeof(attrs),
491 		ReturnImmediate([=](auto in __unused, auto& out) {
492 			size_t l = sizeof(attrs);
493 			strlcpy((char*)out.body.bytes, attrs, l);
494 			out.header.len = sizeof(fuse_out_header) + l;
495 		})
496 	);
497 
498 	ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0))
499 		<< strerror(errno);
500 }
501 
502 /*
503  * The list of extended attributes grows in between the server's two calls to
504  * FUSE_LISTXATTR.
505  */
506 TEST_F(Listxattr, size_only_race_bigger)
507 {
508 	uint64_t ino = 42;
509 	int ns = EXTATTR_NAMESPACE_USER;
510 	char attrs0[9] = "user.foo";
511 	char attrs1[18] = "user.foo\0user.bar";
512 	Sequence seq;
513 
514 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
515 	.WillRepeatedly(Invoke(
516 		ReturnImmediate([=](auto in __unused, auto& out) {
517 		SET_OUT_HEADER_LEN(out, entry);
518 		out.body.entry.attr.mode = S_IFREG | 0644;
519 		out.body.entry.nodeid = ino;
520 		out.body.entry.attr.nlink = 1;
521 		out.body.entry.attr_valid = UINT64_MAX;
522 		out.body.entry.entry_valid = UINT64_MAX;
523 	})));
524 	expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
525 	{
526 		out.body.listxattr.size = sizeof(attrs0);
527 		SET_OUT_HEADER_LEN(out, listxattr);
528 	}), &seq);
529 
530 	/*
531 	 * After the first FUSE_LISTXATTR the list grew, so the second
532 	 * operation returns ERANGE.
533 	 */
534 	expect_listxattr(ino, sizeof(attrs0), ReturnErrno(ERANGE), &seq);
535 
536 	/* And now the kernel retries */
537 	expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
538 	{
539 		out.body.listxattr.size = sizeof(attrs1);
540 		SET_OUT_HEADER_LEN(out, listxattr);
541 	}), &seq);
542 	expect_listxattr(ino, sizeof(attrs1),
543 		ReturnImmediate([&](auto in __unused, auto& out) {
544 			memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1));
545 			out.header.len = sizeof(fuse_out_header) +
546 			    sizeof(attrs1);
547 		}), &seq
548 	);
549 
550 	/* Userspace should never know about the retry */
551 	ASSERT_EQ(8, extattr_list_file(FULLPATH, ns, NULL, 0))
552 		<< strerror(errno);
553 }
554 
555 /*
556  * The list of extended attributes shrinks in between the server's two calls to
557  * FUSE_LISTXATTR
558  */
559 TEST_F(Listxattr, size_only_race_smaller)
560 {
561 	uint64_t ino = 42;
562 	int ns = EXTATTR_NAMESPACE_USER;
563 	char attrs0[18] = "user.foo\0user.bar";
564 	char attrs1[9] = "user.foo";
565 
566 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
567 	expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
568 	{
569 		out.body.listxattr.size = sizeof(attrs0);
570 		SET_OUT_HEADER_LEN(out, listxattr);
571 	}));
572 	expect_listxattr(ino, sizeof(attrs0),
573 		ReturnImmediate([&](auto in __unused, auto& out) {
574 			strlcpy((char*)out.body.bytes, attrs1, sizeof(attrs1));
575 			out.header.len = sizeof(fuse_out_header) +
576 			    sizeof(attrs1);
577 		})
578 	);
579 
580 	ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0))
581 		<< strerror(errno);
582 }
583 
584 TEST_F(Listxattr, size_only_really_big)
585 {
586 	uint64_t ino = 42;
587 	int ns = EXTATTR_NAMESPACE_USER;
588 
589 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
590 	expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
591 		out.body.listxattr.size = 16000;
592 		SET_OUT_HEADER_LEN(out, listxattr);
593 	}));
594 
595 	expect_listxattr(ino, 16000,
596 		ReturnImmediate([](auto in __unused, auto& out) {
597 			const char l[16] = "user.foobarbang";
598 			for (int i=0; i < 1000; i++) {
599 				memcpy(&out.body.bytes[16 * i], l, 16);
600 			}
601 			out.header.len = sizeof(fuse_out_header) + 16000;
602 		})
603 	);
604 
605 	ASSERT_EQ(11000, extattr_list_file(FULLPATH, ns, NULL, 0))
606 		<< strerror(errno);
607 }
608 
609 /*
610  * List all of the user attributes of a file which has both user and system
611  * attributes
612  */
613 TEST_F(Listxattr, user)
614 {
615 	uint64_t ino = 42;
616 	int ns = EXTATTR_NAMESPACE_USER;
617 	char data[80];
618 	char expected[9] = {3, 'f', 'o', 'o', 4, 'b', 'a', 'n', 'g'};
619 	char attrs[28] = "user.foo\0system.x\0user.bang";
620 
621 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
622 	expect_listxattr(ino, 0,
623 		ReturnImmediate([&](auto in __unused, auto& out) {
624 			out.body.listxattr.size = sizeof(attrs);
625 			SET_OUT_HEADER_LEN(out, listxattr);
626 		})
627 	);
628 
629 	expect_listxattr(ino, sizeof(attrs),
630 	ReturnImmediate([&](auto in __unused, auto& out) {
631 		memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
632 		out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
633 	}));
634 
635 	ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
636 		extattr_list_file(FULLPATH, ns, data, sizeof(data)))
637 		<< strerror(errno);
638 	ASSERT_EQ(0, memcmp(expected, data, sizeof(expected)));
639 }
640 
641 /*
642  * List all of the system attributes of a file which has both user and system
643  * attributes
644  */
645 TEST_F(Listxattr, system)
646 {
647 	uint64_t ino = 42;
648 	int ns = EXTATTR_NAMESPACE_SYSTEM;
649 	char data[80];
650 	char expected[2] = {1, 'x'};
651 	char attrs[28] = "user.foo\0system.x\0user.bang";
652 
653 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
654 	expect_listxattr(ino, 0,
655 		ReturnImmediate([&](auto in __unused, auto& out) {
656 			out.body.listxattr.size = sizeof(attrs);
657 			SET_OUT_HEADER_LEN(out, listxattr);
658 		})
659 	);
660 
661 	expect_listxattr(ino, sizeof(attrs),
662 	ReturnImmediate([&](auto in __unused, auto& out) {
663 		memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
664 		out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
665 	}));
666 
667 	ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
668 		extattr_list_file(FULLPATH, ns, data, sizeof(data)))
669 		<< strerror(errno);
670 	ASSERT_EQ(0, memcmp(expected, data, sizeof(expected)));
671 }
672 
673 /* Fail to remove a nonexistent attribute */
674 TEST_F(Removexattr, enoattr)
675 {
676 	uint64_t ino = 42;
677 	int ns = EXTATTR_NAMESPACE_USER;
678 
679 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
680 	expect_removexattr(ino, "user.foo", ENOATTR);
681 
682 	ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
683 	ASSERT_EQ(ENOATTR, errno);
684 }
685 
686 /*
687  * If the filesystem returns ENOSYS, then it will be treated as a permanent
688  * failure and all future VOP_DELETEEXTATTR calls will fail with EOPNOTSUPP
689  * without querying the filesystem daemon
690  */
691 TEST_F(Removexattr, enosys)
692 {
693 	uint64_t ino = 42;
694 	int ns = EXTATTR_NAMESPACE_USER;
695 
696 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
697 	expect_removexattr(ino, "user.foo", ENOSYS);
698 
699 	ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
700 	EXPECT_EQ(EOPNOTSUPP, errno);
701 
702 	/* Subsequent attempts should not query the filesystem at all */
703 	ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
704 	EXPECT_EQ(EOPNOTSUPP, errno);
705 }
706 
707 /* Successfully remove a user xattr */
708 TEST_F(Removexattr, user)
709 {
710 	uint64_t ino = 42;
711 	int ns = EXTATTR_NAMESPACE_USER;
712 
713 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
714 	expect_removexattr(ino, "user.foo", 0);
715 
716 	ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
717 		<< strerror(errno);
718 }
719 
720 /* Successfully remove a system xattr */
721 TEST_F(Removexattr, system)
722 {
723 	uint64_t ino = 42;
724 	int ns = EXTATTR_NAMESPACE_SYSTEM;
725 
726 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
727 	expect_removexattr(ino, "system.foo", 0);
728 
729 	ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
730 		<< strerror(errno);
731 }
732 
733 /*
734  * If the filesystem returns ENOSYS, then it will be treated as a permanent
735  * failure and all future VOP_SETEXTATTR calls will fail with EOPNOTSUPP
736  * without querying the filesystem daemon
737  */
738 TEST_F(Setxattr, enosys)
739 {
740 	uint64_t ino = 42;
741 	const char value[] = "whatever";
742 	ssize_t value_len = strlen(value) + 1;
743 	int ns = EXTATTR_NAMESPACE_USER;
744 	ssize_t r;
745 
746 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
747 	expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOSYS));
748 
749 	r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
750 		value_len);
751 	ASSERT_EQ(-1, r);
752 	EXPECT_EQ(EOPNOTSUPP, errno);
753 
754 	/* Subsequent attempts should not query the filesystem at all */
755 	r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
756 		value_len);
757 	ASSERT_EQ(-1, r);
758 	EXPECT_EQ(EOPNOTSUPP, errno);
759 }
760 
761 /*
762  * SETXATTR will return ENOTSUP if the namespace is invalid or the filesystem
763  * as currently configured doesn't support extended attributes.
764  */
765 TEST_F(Setxattr, enotsup)
766 {
767 	uint64_t ino = 42;
768 	const char value[] = "whatever";
769 	ssize_t value_len = strlen(value) + 1;
770 	int ns = EXTATTR_NAMESPACE_USER;
771 	ssize_t r;
772 
773 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
774 	expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOTSUP));
775 
776 	r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
777 		value_len);
778 	ASSERT_EQ(-1, r);
779 	EXPECT_EQ(ENOTSUP, errno);
780 }
781 
782 /*
783  * Successfully set a user attribute.
784  */
785 TEST_F(Setxattr, user)
786 {
787 	uint64_t ino = 42;
788 	const char value[] = "whatever";
789 	ssize_t value_len = strlen(value) + 1;
790 	int ns = EXTATTR_NAMESPACE_USER;
791 	ssize_t r;
792 
793 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
794 	expect_setxattr(ino, "user.foo", value, ReturnErrno(0));
795 
796 	r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
797 		value_len);
798 	ASSERT_EQ(value_len, r) << strerror(errno);
799 }
800 
801 /*
802  * Successfully set a system attribute.
803  */
804 TEST_F(Setxattr, system)
805 {
806 	uint64_t ino = 42;
807 	const char value[] = "whatever";
808 	ssize_t value_len = strlen(value) + 1;
809 	int ns = EXTATTR_NAMESPACE_SYSTEM;
810 	ssize_t r;
811 
812 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
813 	expect_setxattr(ino, "system.foo", value, ReturnErrno(0));
814 
815 	r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
816 		value_len);
817 	ASSERT_EQ(value_len, r) << strerror(errno);
818 }
819 
820 TEST_F(RofsXattr, deleteextattr_erofs)
821 {
822 	uint64_t ino = 42;
823 	int ns = EXTATTR_NAMESPACE_USER;
824 
825 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
826 
827 	ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
828 	ASSERT_EQ(EROFS, errno);
829 }
830 
831 TEST_F(RofsXattr, setextattr_erofs)
832 {
833 	uint64_t ino = 42;
834 	const char value[] = "whatever";
835 	ssize_t value_len = strlen(value) + 1;
836 	int ns = EXTATTR_NAMESPACE_USER;
837 	ssize_t r;
838 
839 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
840 
841 	r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
842 		value_len);
843 	ASSERT_EQ(-1, r);
844 	EXPECT_EQ(EROFS, errno);
845 }
846