diff --git a/sys/fs/fuse/fuse_io.c b/sys/fs/fuse/fuse_io.c index e93791303c9e..f2d89e2aab51 100644 --- a/sys/fs/fuse/fuse_io.c +++ b/sys/fs/fuse/fuse_io.c @@ -334,10 +334,13 @@ static int fuse_read_directbackend(struct vnode *vp, struct uio *uio, struct ucred *cred, struct fuse_filehandle *fufh) { + struct fuse_data *data; struct fuse_dispatcher fdi; struct fuse_read_in *fri; int err = 0; + data = fuse_get_mpdata(vp->v_mount); + if (uio->uio_resid == 0) return (0); @@ -359,6 +362,11 @@ fuse_read_directbackend(struct vnode *vp, struct uio *uio, fri->offset = uio->uio_offset; fri->size = MIN(uio->uio_resid, fuse_get_mpdata(vp->v_mount)->max_read); + if (fuse_libabi_geq(data, 7, 9)) { + /* See comment regarding FUSE_WRITE_LOCKOWNER */ + fri->read_flags = 0; + fri->flags = 0; /* TODO */ + } SDT_PROBE1(fusefs, , io, read_directbackend_start, fri); @@ -385,6 +393,7 @@ fuse_write_directbackend(struct vnode *vp, struct uio *uio, int ioflag) { struct fuse_vnode_data *fvdat = VTOFUD(vp); + struct fuse_data *data; struct fuse_write_in *fwi; struct fuse_write_out *fwo; struct fuse_dispatcher fdi; @@ -395,6 +404,8 @@ fuse_write_directbackend(struct vnode *vp, struct uio *uio, int err = 0; bool direct_io = fufh->fuse_open_flags & FOPEN_DIRECT_IO; + data = fuse_get_mpdata(vp->v_mount); + if (uio->uio_resid == 0) return (0); @@ -404,8 +415,7 @@ fuse_write_directbackend(struct vnode *vp, struct uio *uio, fdisp_init(&fdi, 0); while (uio->uio_resid > 0) { - chunksize = MIN(uio->uio_resid, - fuse_get_mpdata(vp->v_mount)->max_write); + chunksize = MIN(uio->uio_resid, data->max_write); fdi.iosize = sizeof(*fwi) + chunksize; fdisp_make_vp(&fdi, FUSE_WRITE, vp, uio->uio_td, cred); @@ -414,7 +424,23 @@ fuse_write_directbackend(struct vnode *vp, struct uio *uio, fwi->fh = fufh->fh_id; fwi->offset = uio->uio_offset; fwi->size = chunksize; - fwi_data = (char *)fdi.indata + sizeof(*fwi); + if (fuse_libabi_geq(data, 7, 9)) { + /* + * Don't set FUSE_WRITE_LOCKOWNER. It can't be set + * accurately when using POSIX AIO, libfuse doesn't use + * it, and I'm not aware of any file systems that do. + * It was an attempt to add Linux-style mandatory + * locking to the FUSE protocol, but mandatory locking + * is deprecated even on Linux. See Linux commit + * f33321141b273d60cbb3a8f56a5489baad82ba5e . + */ + fwi->write_flags = 0; + fwi->flags = 0; /* TODO */ + fwi_data = (char *)fdi.indata + sizeof(*fwi); + } else { + fwi_data = (char *)fdi.indata + + FUSE_COMPAT_WRITE_IN_SIZE; + } if ((err = uiomove(fwi_data, chunksize, uio))) break; diff --git a/sys/fs/fuse/fuse_ipc.c b/sys/fs/fuse/fuse_ipc.c index 3c3aa82e6da5..be3338c86a3d 100644 --- a/sys/fs/fuse/fuse_ipc.c +++ b/sys/fs/fuse/fuse_ipc.c @@ -719,8 +719,17 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen) opcode = fticket_opcode(ftick); switch (opcode) { + case FUSE_LINK: case FUSE_LOOKUP: - err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL; + case FUSE_MKDIR: + case FUSE_MKNOD: + case FUSE_SYMLINK: + if (fuse_libabi_geq(ftick->tk_data, 7, 9)) { + err = (blen == sizeof(struct fuse_entry_out)) ? + 0 : EINVAL; + } else { + err = (blen == FUSE_COMPAT_ENTRY_OUT_SIZE) ? 0 : EINVAL; + } break; case FUSE_FORGET: @@ -728,29 +737,19 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen) break; case FUSE_GETATTR: - err = (blen == sizeof(struct fuse_attr_out)) ? 0 : EINVAL; - break; - case FUSE_SETATTR: - err = (blen == sizeof(struct fuse_attr_out)) ? 0 : EINVAL; + if (fuse_libabi_geq(ftick->tk_data, 7, 9)) { + err = (blen == sizeof(struct fuse_attr_out)) ? + 0 : EINVAL; + } else { + err = (blen == FUSE_COMPAT_ATTR_OUT_SIZE) ? 0 : EINVAL; + } break; case FUSE_READLINK: err = (PAGE_SIZE >= blen) ? 0 : EINVAL; break; - case FUSE_SYMLINK: - err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL; - break; - - case FUSE_MKNOD: - err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL; - break; - - case FUSE_MKDIR: - err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL; - break; - case FUSE_UNLINK: err = (blen == 0) ? 0 : EINVAL; break; @@ -763,10 +762,6 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen) err = (blen == 0) ? 0 : EINVAL; break; - case FUSE_LINK: - err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL; - break; - case FUSE_OPEN: err = (blen == sizeof(struct fuse_open_out)) ? 0 : EINVAL; break; @@ -864,8 +859,13 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen) break; case FUSE_CREATE: - err = (blen == sizeof(struct fuse_entry_out) + - sizeof(struct fuse_open_out)) ? 0 : EINVAL; + if (fuse_libabi_geq(ftick->tk_data, 7, 9)) { + err = (blen == sizeof(struct fuse_entry_out) + + sizeof(struct fuse_open_out)) ? 0 : EINVAL; + } else { + err = (blen == FUSE_COMPAT_ENTRY_OUT_SIZE + + sizeof(struct fuse_open_out)) ? 0 : EINVAL; + } break; case FUSE_DESTROY: diff --git a/sys/fs/fuse/fuse_kernel.h b/sys/fs/fuse/fuse_kernel.h index 4f2c4fac009b..e51c9239f7df 100644 --- a/sys/fs/fuse/fuse_kernel.h +++ b/sys/fs/fuse/fuse_kernel.h @@ -34,6 +34,22 @@ * $FreeBSD$ */ +/* + * This file defines the kernel interface of FUSE + * + * Protocol changelog: + * + * 7.9: + * - new fuse_getattr_in input argument of GETATTR + * - add lk_flags in fuse_lk_in + * - add lock_owner field to fuse_setattr_in, fuse_read_in and fuse_write_in + * - add blksize field to fuse_attr + * - add file flags field to fuse_read_in and fuse_write_in + */ + +#ifndef _FUSE_FUSE_KERNEL_H +#define _FUSE_FUSE_KERNEL_H + #ifndef linux #include #define __u64 uint64_t @@ -48,7 +64,7 @@ #define FUSE_KERNEL_VERSION 7 /** Minor version number of this interface */ -#define FUSE_KERNEL_MINOR_VERSION 8 +#define FUSE_KERNEL_MINOR_VERSION 9 /** The node ID of the root inode */ #define FUSE_ROOT_ID 1 @@ -77,6 +93,8 @@ struct fuse_attr { __u32 uid; __u32 gid; __u32 rdev; + __u32 blksize; + __u32 padding; }; struct fuse_kstatfs { @@ -109,6 +127,9 @@ struct fuse_file_lock { #define FATTR_ATIME (1 << 4) #define FATTR_MTIME (1 << 5) #define FATTR_FH (1 << 6) +#define FATTR_ATIME_NOW (1 << 7) +#define FATTR_MTIME_NOW (1 << 8) +#define FATTR_LOCKOWNER (1 << 9) /** * Flags returned by the OPEN request @@ -121,15 +142,45 @@ struct fuse_file_lock { /** * INIT request/reply flags + * + * FUSE_EXPORT_SUPPORT: filesystem handles lookups of "." and ".." */ #define FUSE_ASYNC_READ (1 << 0) #define FUSE_POSIX_LOCKS (1 << 1) +#define FUSE_FILE_OPS (1 << 2) +#define FUSE_ATOMIC_O_TRUNC (1 << 3) +#define FUSE_EXPORT_SUPPORT (1 << 4) +#define FUSE_BIG_WRITES (1 << 5) /** * Release flags */ #define FUSE_RELEASE_FLUSH (1 << 0) +/** + * Getattr flags + */ +#define FUSE_GETATTR_FH (1 << 0) + +/** + * Lock flags + */ +#define FUSE_LK_FLOCK (1 << 0) + +/** + * WRITE flags + * + * FUSE_WRITE_CACHE: delayed write from page cache, file handle is guessed + * FUSE_WRITE_LOCKOWNER: lock_owner field is valid + */ +#define FUSE_WRITE_CACHE (1 << 0) +#define FUSE_WRITE_LOCKOWNER (1 << 1) + +/** + * Read flags + */ +#define FUSE_READ_LOCKOWNER (1 << 1) + enum fuse_opcode { FUSE_LOOKUP = 1, FUSE_FORGET = 2, /* no reply */ @@ -172,6 +223,8 @@ enum fuse_opcode { /* The read buffer is required to be at least 8k, but may be much larger */ #define FUSE_MIN_READ_BUFFER 8192 +#define FUSE_COMPAT_ENTRY_OUT_SIZE 120 + struct fuse_entry_out { __u64 nodeid; /* Inode ID */ __u64 generation; /* Inode generation: nodeid:gen must @@ -187,6 +240,14 @@ struct fuse_forget_in { __u64 nlookup; }; +struct fuse_getattr_in { + __u32 getattr_flags; + __u32 dummy; + __u64 fh; +}; + +#define FUSE_COMPAT_ATTR_OUT_SIZE 96 + struct fuse_attr_out { __u64 attr_valid; /* Cache timeout for the attributes */ __u32 attr_valid_nsec; @@ -217,7 +278,7 @@ struct fuse_setattr_in { __u32 padding; __u64 fh; __u64 size; - __u64 unused1; + __u64 lock_owner; __u64 atime; __u64 mtime; __u64 unused2; @@ -260,14 +321,22 @@ struct fuse_read_in { __u64 fh; __u64 offset; __u32 size; + __u32 read_flags; + __u64 lock_owner; + __u32 flags; __u32 padding; }; +#define FUSE_COMPAT_WRITE_IN_SIZE 24 + struct fuse_write_in { __u64 fh; __u64 offset; __u32 size; __u32 write_flags; + __u64 lock_owner; + __u32 flags; + __u32 padding; }; struct fuse_write_out { @@ -316,6 +385,8 @@ struct fuse_lk_in { __u64 fh; __u64 owner; struct fuse_file_lock lk; + __u32 lk_flags; + __u32 padding; }; struct fuse_lk_out { @@ -386,3 +457,5 @@ struct fuse_dirent { #define FUSE_DIRENT_ALIGN(x) (((x) + sizeof(__u64) - 1) & ~(sizeof(__u64) - 1)) #define FUSE_DIRENT_SIZE(d) \ FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET + (d)->namelen) + +#endif /* _FUSE_FUSE_KERNEL_H */ diff --git a/tests/sys/fs/fusefs/create.cc b/tests/sys/fs/fusefs/create.cc index a8d2e8634551..e922cc037982 100644 --- a/tests/sys/fs/fusefs/create.cc +++ b/tests/sys/fs/fusefs/create.cc @@ -56,6 +56,16 @@ void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) }; +/* FUSE_CREATE operations for a protocol 7.8 server */ +class Create_7_8: public Create { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Create::SetUp(); +} +}; + + /* * If FUSE_CREATE sets attr_valid, then subsequent GETATTRs should use the * attribute cache @@ -338,3 +348,28 @@ TEST_F(Create, wronly_0444) EXPECT_LE(0, fd) << strerror(errno); /* Deliberately leak fd. close(2) will be tested in release.cc */ } + +TEST_F(Create_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + + EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, create_7_8); + out->body.create.entry.attr.mode = mode; + out->body.create.entry.nodeid = ino; + out->body.create.entry.entry_valid = UINT64_MAX; + out->body.create.entry.attr_valid = UINT64_MAX; + })); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + EXPECT_LE(0, fd) << strerror(errno); + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + + diff --git a/tests/sys/fs/fusefs/getattr.cc b/tests/sys/fs/fusefs/getattr.cc index d391ac40d780..b8094243385b 100644 --- a/tests/sys/fs/fusefs/getattr.cc +++ b/tests/sys/fs/fusefs/getattr.cc @@ -53,6 +53,13 @@ void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, } }; +class Getattr_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} +}; /* * If getattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs @@ -197,3 +204,62 @@ TEST_F(Getattr, ok) //FUSE can't set st_blksize until protocol 7.9 } + +TEST_F(Getattr_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + EXPECT_LOOKUP(1, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = ino; + out->body.entry.attr.nlink = 1; + out->body.entry.attr.size = 1; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto out) { + SET_OUT_HEADER_LEN(out, attr_7_8); + out->body.attr.attr.ino = ino; // Must match nodeid + out->body.attr.attr.mode = S_IFREG | 0644; + out->body.attr.attr.size = 1; + out->body.attr.attr.blocks = 2; + out->body.attr.attr.atime = 3; + out->body.attr.attr.mtime = 4; + out->body.attr.attr.ctime = 5; + out->body.attr.attr.atimensec = 6; + out->body.attr.attr.mtimensec = 7; + out->body.attr.attr.ctimensec = 8; + out->body.attr.attr.nlink = 9; + out->body.attr.attr.uid = 10; + out->body.attr.attr.gid = 11; + out->body.attr.attr.rdev = 12; + }))); + + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(1, sb.st_size); + EXPECT_EQ(2, sb.st_blocks); + EXPECT_EQ(3, sb.st_atim.tv_sec); + EXPECT_EQ(6, sb.st_atim.tv_nsec); + EXPECT_EQ(4, sb.st_mtim.tv_sec); + EXPECT_EQ(7, sb.st_mtim.tv_nsec); + EXPECT_EQ(5, sb.st_ctim.tv_sec); + EXPECT_EQ(8, sb.st_ctim.tv_nsec); + EXPECT_EQ(9ull, sb.st_nlink); + EXPECT_EQ(10ul, sb.st_uid); + EXPECT_EQ(11ul, sb.st_gid); + EXPECT_EQ(12ul, sb.st_rdev); + EXPECT_EQ(ino, sb.st_ino); + EXPECT_EQ(S_IFREG | 0644, sb.st_mode); + + //st_birthtim and st_flags are not supported by protocol 7.8. They're + //only supported as OS-specific extensions to OSX. +} diff --git a/tests/sys/fs/fusefs/link.cc b/tests/sys/fs/fusefs/link.cc index 471952dfa35c..3236b471e1f1 100644 --- a/tests/sys/fs/fusefs/link.cc +++ b/tests/sys/fs/fusefs/link.cc @@ -66,6 +66,40 @@ void expect_lookup(const char *relpath, uint64_t ino) } }; +class Link_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_link(uint64_t ino, const char *relpath, mode_t mode, uint32_t nlink) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in->body.bytes + + sizeof(struct fuse_link_in); + return (in->header.opcode == FUSE_LINK && + in->body.link.oldnodeid == ino && + (0 == strcmp(name, relpath))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.entry.nodeid = ino; + out->body.entry.attr.mode = mode; + out->body.entry.attr.nlink = nlink; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = UINT64_MAX; + }))); +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, 0, 1); +} +}; + /* * A successful link should clear the parent directory's attribute cache, * because the fuse daemon should update its mtime and ctime @@ -164,3 +198,31 @@ TEST_F(Link, ok) ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno); EXPECT_EQ(2ul, sb.st_nlink); } + +TEST_F(Link_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const uint64_t ino = 42; + mode_t mode = S_IFREG | 0644; + struct stat sb; + + EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_LOOKUP(1, RELDST) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.attr.nlink = 1; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = UINT64_MAX; + }))); + expect_link(ino, RELPATH, mode, 2); + + ASSERT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno); + // Check that the original file's nlink count has increased. + ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno); + EXPECT_EQ(2ul, sb.st_nlink); +} diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc index 660aea9fc0cb..03c51e766ad4 100644 --- a/tests/sys/fs/fusefs/lookup.cc +++ b/tests/sys/fs/fusefs/lookup.cc @@ -38,6 +38,13 @@ extern "C" { using namespace testing; class Lookup: public FuseTest {}; +class Lookup_7_8: public Lookup { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Lookup::SetUp(); +} +}; /* * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs @@ -355,3 +362,23 @@ TEST_F(Lookup, vtype_conflict) ASSERT_EQ(-1, access(SECONDFULLPATH, F_OK)); ASSERT_EQ(EAGAIN, errno); } + +TEST_F(Lookup_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_LOOKUP(1, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = 14; + }))); + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + + diff --git a/tests/sys/fs/fusefs/mkdir.cc b/tests/sys/fs/fusefs/mkdir.cc index 6b8a04386ae2..0ac070a3e501 100644 --- a/tests/sys/fs/fusefs/mkdir.cc +++ b/tests/sys/fs/fusefs/mkdir.cc @@ -38,6 +38,13 @@ extern "C" { using namespace testing; class Mkdir: public FuseTest {}; +class Mkdir_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} +}; /* * EMLINK is possible on filesystems that limit the number of hard links to a @@ -174,3 +181,32 @@ TEST_F(Mkdir, ok) ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno); } + +TEST_F(Mkdir_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + mode_t mode = 0755; + uint64_t ino = 42; + + EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in->body.bytes + + sizeof(fuse_mkdir_in); + return (in->header.opcode == FUSE_MKDIR && + in->body.mkdir.mode == (S_IFDIR | mode) && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.create.entry.attr.mode = S_IFDIR | mode; + out->body.create.entry.nodeid = ino; + out->body.create.entry.entry_valid = UINT64_MAX; + out->body.create.entry.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index 8a69a84711c2..5cfecfe6f791 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -284,7 +284,8 @@ void debug_fuseop(const mockfs_buf_in *in) } MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, - bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags) + bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, + uint32_t kernel_minor_version) { struct sigaction sa; struct iovec *iov = NULL; @@ -293,6 +294,7 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, const bool trueval = true; m_daemon_id = NULL; + m_kernel_minor_version = kernel_minor_version; m_maxreadahead = max_readahead; m_nready = -1; m_pm = pm; @@ -399,7 +401,7 @@ void MockFS::init(uint32_t flags) { out->header.unique = in->header.unique; out->header.error = 0; out->body.init.major = FUSE_KERNEL_VERSION; - out->body.init.minor = FUSE_KERNEL_MINOR_VERSION; + out->body.init.minor = m_kernel_minor_version;; out->body.init.flags = in->body.init.flags & flags; /* diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index b1ccc770f91a..eede01a4f227 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -76,6 +76,52 @@ struct fuse_create_out { struct fuse_open_out open; }; +/* Protocol 7.8 version of struct fuse_attr */ +struct fuse_attr_7_8 +{ + __u64 ino; + __u64 size; + __u64 blocks; + __u64 atime; + __u64 mtime; + __u64 ctime; + __u32 atimensec; + __u32 mtimensec; + __u32 ctimensec; + __u32 mode; + __u32 nlink; + __u32 uid; + __u32 gid; + __u32 rdev; +}; + +/* Protocol 7.8 version of struct fuse_attr_out */ +struct fuse_attr_out_7_8 +{ + __u64 attr_valid; + __u32 attr_valid_nsec; + __u32 dummy; + struct fuse_attr_7_8 attr; +}; + +/* Protocol 7.8 version of struct fuse_entry_out */ +struct fuse_entry_out_7_8 { + __u64 nodeid; /* Inode ID */ + __u64 generation; /* Inode generation: nodeid:gen must + be unique for the fs's lifetime */ + __u64 entry_valid; /* Cache timeout for the name */ + __u64 attr_valid; /* Cache timeout for the attributes */ + __u32 entry_valid_nsec; + __u32 attr_valid_nsec; + struct fuse_attr_7_8 attr; +}; + +/* Output struct for FUSE_CREATE for protocol 7.8 servers */ +struct fuse_create_out_7_8 { + struct fuse_entry_out_7_8 entry; + struct fuse_open_out open; +}; + union fuse_payloads_in { fuse_access_in access; /* value is from fuse_kern_chan.c in fusefs-libs */ @@ -116,10 +162,13 @@ struct mockfs_buf_in { union fuse_payloads_out { fuse_attr_out attr; + fuse_attr_out_7_8 attr_7_8; fuse_create_out create; + fuse_create_out_7_8 create_7_8; /* The protocol places no limits on the size of bytes */ uint8_t bytes[0x20000]; fuse_entry_out entry; + fuse_entry_out_7_8 entry_7_8; fuse_lk_out getlk; fuse_getxattr_out getxattr; fuse_init_out init; @@ -191,6 +240,9 @@ class MockFS { /* file descriptor of /dev/fuse control device */ int m_fuse_fd; + /* The minor version of the kernel API that this mock daemon targets */ + uint32_t m_kernel_minor_version; + int m_kq; /* The max_readahead filesystem option */ @@ -240,7 +292,9 @@ class MockFS { /* Create a new mockfs and mount it to a tempdir */ MockFS(int max_readahead, bool allow_other, bool default_permissions, bool push_symlinks_in, bool ro, - enum poll_method pm, uint32_t flags); + enum poll_method pm, uint32_t flags, + uint32_t kernel_minor_version); + virtual ~MockFS(); /* Kill the filesystem daemon without unmounting the filesystem */ diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc index f3ba5a30f4c5..7200f50ab2f7 100644 --- a/tests/sys/fs/fusefs/read.cc +++ b/tests/sys/fs/fusefs/read.cc @@ -54,6 +54,19 @@ void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) } }; +class Read_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); +} +}; + class AioRead: public Read { public: virtual void SetUp() { @@ -585,6 +598,29 @@ TEST_F(Read, read) /* Deliberately leak fd. close(2) will be tested in release.cc */ } +TEST_F(Read_7_8, read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + char buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + /* If the filesystem allows it, the kernel should try to readahead */ TEST_F(ReadCacheable, default_readahead) { diff --git a/tests/sys/fs/fusefs/readdir.cc b/tests/sys/fs/fusefs/readdir.cc index 706292ae2229..35aff0eefd6c 100644 --- a/tests/sys/fs/fusefs/readdir.cc +++ b/tests/sys/fs/fusefs/readdir.cc @@ -97,6 +97,19 @@ void expect_readdir(uint64_t ino, uint64_t off, vector &ents) } }; +class Readdir_7_8: public Readdir { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Readdir::SetUp(); +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFDIR | 0755, 0, 1); +} +}; + /* FUSE_READDIR returns nothing but "." and ".." */ TEST_F(Readdir, dots) { @@ -378,3 +391,34 @@ TEST_F(Readdir, seekdir) /* Deliberately leak dir. RELEASEDIR will be tested separately */ } + +TEST_F(Readdir_7_8, nodots) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_READDIR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + out->header.error = 0; + out->header.len = sizeof(out->header); + }))); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(NULL, dir) << strerror(errno); + errno = 0; + ASSERT_EQ(NULL, readdir(dir)); + ASSERT_EQ(0, errno); + + /* Deliberately leak dir. RELEASEDIR will be tested separately */ +} diff --git a/tests/sys/fs/fusefs/setattr.cc b/tests/sys/fs/fusefs/setattr.cc index 51e4f93fee30..4cb5606f5edf 100644 --- a/tests/sys/fs/fusefs/setattr.cc +++ b/tests/sys/fs/fusefs/setattr.cc @@ -49,6 +49,14 @@ virtual void SetUp() { } }; +class Setattr_7_8: public Setattr { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Setattr::SetUp(); +} +}; + /* * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs @@ -736,3 +744,37 @@ TEST_F(RofsSetattr, erofs) ASSERT_EQ(-1, chmod(FULLPATH, newmode)); ASSERT_EQ(EROFS, errno); } + +/* Change the mode of a file */ +TEST_F(Setattr_7_8, chmod) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0755; + const mode_t newmode = 0644; + + EXPECT_LOOKUP(1, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.entry.attr.mode = S_IFREG | oldmode; + out->body.entry.nodeid = ino; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + /* In protocol 7.23, ctime will be changed too */ + uint32_t valid = FATTR_MODE; + return (in->header.opcode == FUSE_SETATTR && + in->header.nodeid == ino && + in->body.setattr.valid == valid && + in->body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, attr_7_8); + out->body.attr.attr.ino = ino; // Must match nodeid + out->body.attr.attr.mode = S_IFREG | newmode; + }))); + EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/symlink.cc b/tests/sys/fs/fusefs/symlink.cc index 99dc3086a2cc..db032d74dcf1 100644 --- a/tests/sys/fs/fusefs/symlink.cc +++ b/tests/sys/fs/fusefs/symlink.cc @@ -62,6 +62,35 @@ void expect_symlink(uint64_t ino, const char *target, const char *relpath) }; +class Symlink_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_symlink(uint64_t ino, const char *target, const char *relpath) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in->body.bytes; + const char *linkname = name + strlen(name) + 1; + return (in->header.opcode == FUSE_SYMLINK && + (0 == strcmp(linkname, target)) && + (0 == strcmp(name, relpath))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.entry.attr.mode = S_IFLNK | 0777; + out->body.entry.nodeid = ino; + out->body.entry.entry_valid = UINT64_MAX; + out->body.entry.attr_valid = UINT64_MAX; + }))); +} + +}; + /* * A successful symlink should clear the parent directory's attribute cache, * because the fuse daemon should update its mtime and ctime @@ -130,3 +159,16 @@ TEST_F(Symlink, ok) EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno); } + +TEST_F(Symlink_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char dst[] = "dst"; + const uint64_t ino = 42; + + EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_symlink(ino, dst, RELPATH); + + EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index 50e06ccd5b97..890578990bca 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -96,7 +96,7 @@ void FuseTest::SetUp() { try { m_mock = new MockFS(m_maxreadahead, m_allow_other, m_default_permissions, m_push_symlinks_in, m_ro, - m_pm, m_init_flags); + m_pm, m_init_flags, m_kernel_minor_version); /* * FUSE_ACCESS is called almost universally. Expecting it in * each test case would be super-annoying. Instead, set a @@ -208,6 +208,23 @@ void FuseTest::expect_lookup(const char *relpath, uint64_t ino, mode_t mode, }))); } +void FuseTest::expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode, + uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid) +{ + EXPECT_LOOKUP(1, relpath) + .Times(times) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.attr.nlink = 1; + out->body.entry.attr_valid = attr_valid; + out->body.entry.attr.size = size; + out->body.entry.attr.uid = uid; + out->body.entry.attr.gid = gid; + }))); +} + void FuseTest::expect_open(uint64_t ino, uint32_t flags, int times) { EXPECT_CALL(*m_mock, process( @@ -334,6 +351,30 @@ void FuseTest::expect_write(uint64_t ino, uint64_t offset, uint64_t isize, }))); } +void FuseTest::expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, uint32_t flags, const void *contents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in->body.bytes + + FUSE_COMPAT_WRITE_IN_SIZE; + bool pid_ok = (pid_t)in->header.pid == getpid(); + return (in->header.opcode == FUSE_WRITE && + in->header.nodeid == ino && + in->body.write.fh == FH && + in->body.write.offset == offset && + in->body.write.size == isize && + pid_ok && + in->body.write.write_flags == flags && + 0 == bcmp(buf, contents, isize)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, write); + out->body.write.size = osize; + }))); +} + void get_unprivileged_id(uid_t *uid, gid_t *gid) { diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh index 0c918d4e9070..7c4a61dbb218 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -52,6 +52,7 @@ class FuseTest : public ::testing::Test { uint32_t m_init_flags; bool m_allow_other; bool m_default_permissions; + uint32_t m_kernel_minor_version; enum poll_method m_pm; bool m_push_symlinks_in; bool m_ro; @@ -70,6 +71,7 @@ class FuseTest : public ::testing::Test { m_init_flags(0), m_allow_other(false), m_default_permissions(false), + m_kernel_minor_version(FUSE_KERNEL_MINOR_VERSION), m_pm(BLOCKING), m_push_symlinks_in(false), m_ro(false) @@ -119,6 +121,11 @@ class FuseTest : public ::testing::Test { uint64_t size, int times, uint64_t attr_valid = UINT64_MAX, uid_t uid = 0, gid_t gid = 0); + /* The protocol 7.8 version of expect_lookup */ + void expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode, + uint64_t size, int times, uint64_t attr_valid = UINT64_MAX, + uid_t uid = 0, gid_t gid = 0); + /* * Create an expectation that FUSE_OPEN will be called for the given * inode exactly times times. It will return with open_flags flags and @@ -136,6 +143,9 @@ class FuseTest : public ::testing::Test { * Create an expectation that FUSE_READ will be called exactly once for * the given inode, at offset offset and with size isize. It will * return the first osize bytes from contents + * + * Protocol 7.8 tests can use this same expectation method because + * nothing currently validates the size of the fuse_read_in struct. */ void expect_read(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents); @@ -166,6 +176,10 @@ class FuseTest : public ::testing::Test { void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, uint32_t flags, const void *contents); + /* Protocol 7.8 version of expect_write */ + void expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, uint32_t flags, const void *contents); + /* * Helper that runs code in a child process. * diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc index 0f0313d976bc..9380467eedbf 100644 --- a/tests/sys/fs/fusefs/write.cc +++ b/tests/sys/fs/fusefs/write.cc @@ -67,6 +67,21 @@ void expect_release(uint64_t ino, ProcessMockerT r) }; +class Write_7_8: public FuseTest { + +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); +} + +}; + class AioWrite: public Write { virtual void SetUp() { const char *node = "vfs.aio.enable_unsafe"; @@ -535,6 +550,26 @@ TEST_F(Write, write_nothing) /* Deliberately leak fd. close(2) will be tested in release.cc */ } +TEST_F(Write_7_8, write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write_7_8(ino, 0, bufsize, bufsize, 0, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + EXPECT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + /* In writeback mode, dirty data should be written on close */ TEST_F(WriteBack, close) {