Index: sys/fs/fuse/fuse_vnops.c =================================================================== --- sys/fs/fuse/fuse_vnops.c +++ sys/fs/fuse/fuse_vnops.c @@ -2225,6 +2225,20 @@ } /* + * List extended attributes + * + * The FUSE_LISTXATTR operation is based on Linux's listxattr(2) syscall, which + * has a number of differences compared to its FreeBSD equivalent, + * extattr_list_file: + * + * - FUSE_LISTXATTR returns all extended attributes across all namespaces, + * whereas listxattr(2) only returns attributes for a single namespace + * - FUSE_LISTXATTR prepends each attribute name with "namespace." + * - If the provided buffer is not large enough to hold the result, + * FUSE_LISTXATTR should return ERANGE, whereas listxattr is expected to + * return as many results as will fit. + */ +/* struct vop_listextattr_args { struct vop_generic_args a_gen; struct vnode *a_vp; @@ -2275,6 +2289,7 @@ fdisp_init(&fdi, sizeof(*list_xattr_in)); fdisp_make_vp(&fdi, FUSE_LISTXATTR, vp, td, cred); +retry: /* * Retrieve Linux / FUSE compatible list size. */ @@ -2303,14 +2318,29 @@ */ fdisp_refresh_vp(&fdi, FUSE_LISTXATTR, vp, td, cred); list_xattr_in = fdi.indata; - list_xattr_in->size = linux_list_len + sizeof(*list_xattr_out); + list_xattr_in->size = linux_list_len; err = fdisp_wait_answ(&fdi); - if (err != 0) + if (err == ERANGE) { + /* + * Race detected. The attribute list must've grown since the + * first FUSE_LISTXATTR call. Start over. + */ + fdisp_refresh_vp(&fdi, FUSE_LISTXATTR, vp, td, cred); + goto retry; + } else if (err != 0) goto out; linux_list = fdi.answ; - linux_list_len = fdi.iosize; + /* FUSE doesn't allow the server to return more data than requested */ + if (fdi.iosize > linux_list_len) { + printf("WARNING: FUSE protocol violation. Server returned " + "more extended attribute data than requested; " + "should've returned ERANGE instead"); + } else { + /* But returning less data is fine */ + linux_list_len = fdi.iosize; + } /* * Retrieve the BSD compatible list values. Index: tests/sys/fs/fusefs/mockfs.cc =================================================================== --- tests/sys/fs/fusefs/mockfs.cc +++ tests/sys/fs/fusefs/mockfs.cc @@ -204,6 +204,9 @@ case FUSE_LINK: printf(" oldnodeid=%" PRIu64, in.body.link.oldnodeid); break; + case FUSE_LISTXATTR: + printf(" size=%" PRIu32, in.body.listxattr.size); + break; case FUSE_LOOKUP: printf(" %s", in.body.lookup); break; Index: tests/sys/fs/fusefs/xattr.cc =================================================================== --- tests/sys/fs/fusefs/xattr.cc +++ tests/sys/fs/fusefs/xattr.cc @@ -62,17 +62,31 @@ ).WillOnce(Invoke(r)); } -void expect_listxattr(uint64_t ino, uint32_t size, ProcessMockerT r) +void expect_listxattr(uint64_t ino, uint32_t size, ProcessMockerT r, + Sequence *seq = NULL) { - EXPECT_CALL(*m_mock, process( - ResultOf([=](auto in) { - return (in.header.opcode == FUSE_LISTXATTR && - in.header.nodeid == ino && - in.body.listxattr.size == size); - }, Eq(true)), - _) - ).WillOnce(Invoke(r)) - .RetiresOnSaturation(); + if (seq == NULL) { + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LISTXATTR && + in.header.nodeid == ino && + in.body.listxattr.size == size); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)) + .RetiresOnSaturation(); + } else { + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LISTXATTR && + in.header.nodeid == ino && + in.body.listxattr.size == size); + }, Eq(true)), + _) + ).InSequence(*seq) + .WillOnce(Invoke(r)) + .RetiresOnSaturation(); + } } void expect_removexattr(uint64_t ino, const char *attr, int error) @@ -175,6 +189,7 @@ * behavior. * * This test case covers a filesystem that uses the Linux behavior + * TODO: require FreeBSD Behavior. */ TEST_F(Getxattr, erange) { @@ -305,22 +320,32 @@ * * On Linux, however, the filesystem is supposed to return ERANGE. * - * libfuse specifies the Linux behavior. However, that's probably an error. - * It would probably be correct for the filesystem to use platform-dependent - * behavior. - * - * This test case covers a filesystem that uses the Linux behavior + * fusefs(5) must guarantee the usual FreeBSD behavior. */ TEST_F(Listxattr, erange) { uint64_t ino = 42; int ns = EXTATTR_NAMESPACE_USER; + char attrs[9] = "user.foo"; + char expected[3] = {3, 'f', 'o'}; + char buf[3]; expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); - expect_listxattr(ino, 0, ReturnErrno(ERANGE)); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs); + SET_OUT_HEADER_LEN(out, listxattr); + })); + expect_listxattr(ino, sizeof(attrs), + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((void*)out.body.bytes, attrs, sizeof(attrs)); + out.header.len = sizeof(fuse_out_header) + sizeof(attrs); + })); - ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); - ASSERT_EQ(ERANGE, errno); + + ASSERT_EQ(static_cast(sizeof(buf)), + extattr_list_file(FULLPATH, ns, buf, sizeof(buf))); + ASSERT_EQ(0, memcmp(expected, buf, sizeof(buf))); } /* @@ -351,21 +376,20 @@ { uint64_t ino = 42; int ns = EXTATTR_NAMESPACE_USER; + char attrs[9] = "user.foo"; expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); - expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) { - out.body.listxattr.size = 45; + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs); SET_OUT_HEADER_LEN(out, listxattr); })); - // TODO: fix the expected size after fixing the size calculation bug in - // fuse_vnop_listextattr. It should be exactly 45. - expect_listxattr(ino, 53, - ReturnImmediate([](auto in __unused, auto& out) { - const char l[] = "user.foo"; - strlcpy((char*)out.body.bytes, l, - sizeof(out.body.bytes)); - out.header.len = sizeof(fuse_out_header) + sizeof(l); + expect_listxattr(ino, sizeof(attrs), + ReturnImmediate([=](auto in __unused, auto& out) { + size_t l = sizeof(attrs); + strlcpy((char*)out.body.bytes, attrs, l); + out.header.len = sizeof(fuse_out_header) + l; }) ); @@ -373,6 +397,79 @@ << strerror(errno); } +/* + * The list of extended attributes grows in between the server's two calls to + * FUSE_LISTXATTR + */ +TEST_F(Listxattr, size_only_race_bigger) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + char attrs0[9] = "user.foo"; + char attrs1[18] = "user.foo\0user.bar"; + Sequence seq; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs0); + SET_OUT_HEADER_LEN(out, listxattr); + }), &seq); + + /* + * After the first FUSE_LISTXATTR the list grew, so the second + * operation returns ERANGE. + */ + expect_listxattr(ino, sizeof(attrs0), ReturnErrno(ERANGE), &seq); + + /* And now the kernel retries */ + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs1); + SET_OUT_HEADER_LEN(out, listxattr); + }), &seq); + expect_listxattr(ino, sizeof(attrs1), + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1)); + out.header.len = sizeof(fuse_out_header) + + sizeof(attrs1); + }), &seq + ); + + /* Userspace should never know about the retry */ + ASSERT_EQ(8, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + +/* + * The list of extended attributes shrinks in between the server's two calls to + * FUSE_LISTXATTR + */ +TEST_F(Listxattr, size_only_race_smaller) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + char attrs0[18] = "user.foo\0user.bar"; + char attrs1[9] = "user.foo"; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs0); + SET_OUT_HEADER_LEN(out, listxattr); + })); + expect_listxattr(ino, sizeof(attrs0), + ReturnImmediate([&](auto in __unused, auto& out) { + strlcpy((char*)out.body.bytes, attrs1, sizeof(attrs1)); + out.header.len = sizeof(fuse_out_header) + + sizeof(attrs1); + }) + ); + + ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + TEST_F(Listxattr, size_only_really_big) { uint64_t ino = 42; @@ -384,9 +481,7 @@ SET_OUT_HEADER_LEN(out, listxattr); })); - // TODO: fix the expected size after fixing the size calculation bug in - // fuse_vnop_listextattr. It should be exactly 16000. - expect_listxattr(ino, 16008, + expect_listxattr(ino, 16000, ReturnImmediate([](auto in __unused, auto& out) { const char l[16] = "user.foobarbang"; for (int i=0; i < 1000; i++) { @@ -420,9 +515,7 @@ }) ); - // TODO: fix the expected size after fixing the size calculation bug in - // fuse_vnop_listextattr. - expect_listxattr(ino, sizeof(attrs) + 8, + expect_listxattr(ino, sizeof(attrs), ReturnImmediate([&](auto in __unused, auto& out) { memcpy((void*)out.body.bytes, attrs, sizeof(attrs)); out.header.len = sizeof(fuse_out_header) + sizeof(attrs); @@ -454,9 +547,7 @@ }) ); - // TODO: fix the expected size after fixing the size calculation bug in - // fuse_vnop_listextattr. - expect_listxattr(ino, sizeof(attrs) + 8, + expect_listxattr(ino, sizeof(attrs), ReturnImmediate([&](auto in __unused, auto& out) { memcpy((void*)out.body.bytes, attrs, sizeof(attrs)); out.header.len = sizeof(fuse_out_header) + sizeof(attrs);