git: dfb3365aa112 - stable/13 - fusefs: implement FUSE_NO_OPEN_SUPPORT and FUSE_NO_OPENDIR_SUPPORT

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Tue, 07 Dec 2021 05:04:27 UTC
The branch stable/13 has been updated by asomers:

URL: https://cgit.FreeBSD.org/src/commit/?id=dfb3365aa112d49d794b47e5b985416762152ba9

commit dfb3365aa112d49d794b47e5b985416762152ba9
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2021-09-25 03:53:28 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2021-12-07 04:51:55 +0000

    fusefs: implement FUSE_NO_OPEN_SUPPORT and FUSE_NO_OPENDIR_SUPPORT
    
    For file systems that allow it, fusefs will skip FUSE_OPEN,
    FUSE_RELEASE, FUSE_OPENDIR, and FUSE_RELEASEDIR operations, a minor
    optimization.
    
    Reviewed by:    pfg
    Differential Revision: https://reviews.freebsd.org/D32141
    
    (cherry picked from commit 7124d2bc3a3bd58f6d3803a0579f2e0fa853b56d)
---
 sys/fs/fuse/fuse_file.c        | 65 +++++++++++++++++++++++++++++------------
 sys/fs/fuse/fuse_file.h        |  3 +-
 sys/fs/fuse/fuse_internal.c    |  8 +++--
 sys/fs/fuse/fuse_ipc.h         |  2 ++
 sys/fs/fuse/fuse_kernel.h      |  4 ++-
 tests/sys/fs/fusefs/open.cc    | 66 ++++++++++++++++++++++++++++++++++++++++++
 tests/sys/fs/fusefs/opendir.cc | 48 ++++++++++++++++++++++++++++++
 7 files changed, 174 insertions(+), 22 deletions(-)

diff --git a/sys/fs/fuse/fuse_file.c b/sys/fs/fuse/fuse_file.c
index f9b0c781f49a..a7357d85a8b3 100644
--- a/sys/fs/fuse/fuse_file.c
+++ b/sys/fs/fuse/fuse_file.c
@@ -124,44 +124,68 @@ int
 fuse_filehandle_open(struct vnode *vp, int a_mode,
     struct fuse_filehandle **fufhp, struct thread *td, struct ucred *cred)
 {
+	struct mount *mp = vnode_mount(vp);
+	struct fuse_data *data = fuse_get_mpdata(mp);
 	struct fuse_dispatcher fdi;
-	struct fuse_open_in *foi;
-	struct fuse_open_out *foo;
+	const struct fuse_open_out default_foo = {
+		.fh = 0,
+		.open_flags = FOPEN_KEEP_CACHE,
+		.padding = 0
+	};
+	struct fuse_open_in *foi = NULL;
+	const struct fuse_open_out *foo;
 	fufh_type_t fufh_type;
-
+	int dataflags = data->dataflags;
 	int err = 0;
 	int oflags = 0;
 	int op = FUSE_OPEN;
+	int relop = FUSE_RELEASE;
+	int fsess_no_op_support = FSESS_NO_OPEN_SUPPORT;
 
 	fufh_type = fflags_2_fufh_type(a_mode);
 	oflags = fufh_type_2_fflags(fufh_type);
 
 	if (vnode_isdir(vp)) {
 		op = FUSE_OPENDIR;
+		relop = FUSE_RELEASEDIR;
+		fsess_no_op_support = FSESS_NO_OPENDIR_SUPPORT;
 		/* vn_open_vnode already rejects FWRITE on directories */
 		MPASS(fufh_type == FUFH_RDONLY || fufh_type == FUFH_EXEC);
 	}
 	fdisp_init(&fdi, sizeof(*foi));
-	fdisp_make_vp(&fdi, op, vp, td, cred);
-
-	foi = fdi.indata;
-	foi->flags = oflags;
-
-	if ((err = fdisp_wait_answ(&fdi))) {
-		SDT_PROBE2(fusefs, , file, trace, 1,
-			"OUCH ... daemon didn't give fh");
-		if (err == ENOENT) {
-			fuse_internal_vnode_disappear(vp);
+	if (fsess_not_impl(mp, op) && dataflags & fsess_no_op_support) {
+		/* The operation implicitly succeeds */
+		foo = &default_foo;
+	} else {
+		fdisp_make_vp(&fdi, op, vp, td, cred);
+
+		foi = fdi.indata;
+		foi->flags = oflags;
+
+		err = fdisp_wait_answ(&fdi);
+		if (err == ENOSYS && dataflags & fsess_no_op_support) {
+			/* The operation implicitly succeeds */
+			foo = &default_foo;
+			fsess_set_notimpl(mp, op);
+			fsess_set_notimpl(mp, relop);
+			err = 0;
+		} else if (err) {
+			SDT_PROBE2(fusefs, , file, trace, 1,
+				"OUCH ... daemon didn't give fh");
+			if (err == ENOENT)
+				fuse_internal_vnode_disappear(vp);
+			goto out;
+		} else {
+			foo = fdi.answ;
 		}
-		goto out;
 	}
-	foo = fdi.answ;
 
 	fuse_filehandle_init(vp, fufh_type, fufhp, td, cred, foo);
 	fuse_vnode_open(vp, foo->open_flags, td);
 
 out:
-	fdisp_destroy(&fdi);
+	if (foi)
+		fdisp_destroy(&fdi);
 	return err;
 }
 
@@ -169,6 +193,7 @@ int
 fuse_filehandle_close(struct vnode *vp, struct fuse_filehandle *fufh,
     struct thread *td, struct ucred *cred)
 {
+	struct mount *mp = vnode_mount(vp);
 	struct fuse_dispatcher fdi;
 	struct fuse_release_in *fri;
 
@@ -180,6 +205,10 @@ fuse_filehandle_close(struct vnode *vp, struct fuse_filehandle *fufh,
 	}
 	if (vnode_isdir(vp))
 		op = FUSE_RELEASEDIR;
+
+	if (fsess_not_impl(mp, op))
+		goto out;
+
 	fdisp_init(&fdi, sizeof(*fri));
 	fdisp_make_vp(&fdi, op, vp, td, cred);
 	fri = fdi.indata;
@@ -327,8 +356,8 @@ fuse_filehandle_getrw(struct vnode *vp, int fflag,
 
 void
 fuse_filehandle_init(struct vnode *vp, fufh_type_t fufh_type,
-    struct fuse_filehandle **fufhp, struct thread *td, struct ucred *cred,
-    struct fuse_open_out *foo)
+    struct fuse_filehandle **fufhp, struct thread *td, const struct ucred *cred,
+    const struct fuse_open_out *foo)
 {
 	struct fuse_vnode_data *fvdat = VTOFUD(vp);
 	struct fuse_filehandle *fufh;
diff --git a/sys/fs/fuse/fuse_file.h b/sys/fs/fuse/fuse_file.h
index 10c799d5d40d..25004beead51 100644
--- a/sys/fs/fuse/fuse_file.h
+++ b/sys/fs/fuse/fuse_file.h
@@ -211,7 +211,8 @@ int fuse_filehandle_getrw(struct vnode *vp, int fflag,
 
 void fuse_filehandle_init(struct vnode *vp, fufh_type_t fufh_type,
 		          struct fuse_filehandle **fufhp, struct thread *td,
-			  struct ucred *cred, struct fuse_open_out *foo);
+			  const struct ucred *cred,
+			  const struct fuse_open_out *foo);
 int fuse_filehandle_open(struct vnode *vp, int mode,
                          struct fuse_filehandle **fufhp, struct thread *td,
                          struct ucred *cred);
diff --git a/sys/fs/fuse/fuse_internal.c b/sys/fs/fuse/fuse_internal.c
index 731ac9c2ba24..c52f12d3a71a 100644
--- a/sys/fs/fuse/fuse_internal.c
+++ b/sys/fs/fuse/fuse_internal.c
@@ -1009,6 +1009,10 @@ fuse_internal_init_callback(struct fuse_ticket *tick, struct uio *uio)
 				data->dataflags |= FSESS_POSIX_LOCKS;
 			if (fiio->flags & FUSE_EXPORT_SUPPORT)
 				data->dataflags |= FSESS_EXPORT_SUPPORT;
+			if (fiio->flags & FUSE_NO_OPEN_SUPPORT)
+				data->dataflags |= FSESS_NO_OPEN_SUPPORT;
+			if (fiio->flags & FUSE_NO_OPENDIR_SUPPORT)
+				data->dataflags |= FSESS_NO_OPENDIR_SUPPORT;
 			/* 
 			 * Don't bother to check FUSE_BIG_WRITES, because it's
 			 * redundant with max_write
@@ -1098,7 +1102,6 @@ fuse_internal_send_init(struct fuse_data *data, struct thread *td)
 	 * FUSE_DO_READDIRPLUS: not yet implemented
 	 * FUSE_READDIRPLUS_AUTO: not yet implemented
 	 * FUSE_ASYNC_DIO: not yet implemented
-	 * FUSE_NO_OPEN_SUPPORT: not yet implemented
 	 * FUSE_PARALLEL_DIROPS: not yet implemented
 	 * FUSE_HANDLE_KILLPRIV: not yet implemented
 	 * FUSE_POSIX_ACL: not yet implemented
@@ -1107,7 +1110,8 @@ fuse_internal_send_init(struct fuse_data *data, struct thread *td)
 	 * FUSE_MAX_PAGES: not yet implemented
 	 */
 	fiii->flags = FUSE_ASYNC_READ | FUSE_POSIX_LOCKS | FUSE_EXPORT_SUPPORT
-		| FUSE_BIG_WRITES | FUSE_WRITEBACK_CACHE;
+		| FUSE_BIG_WRITES | FUSE_WRITEBACK_CACHE
+		| FUSE_NO_OPEN_SUPPORT | FUSE_NO_OPENDIR_SUPPORT;
 
 	fuse_insert_callback(fdi.tick, fuse_internal_init_callback);
 	fuse_insert_message(fdi.tick, false);
diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h
index 7b27fd97e212..fe616b3639a7 100644
--- a/sys/fs/fuse/fuse_ipc.h
+++ b/sys/fs/fuse/fuse_ipc.h
@@ -229,6 +229,8 @@ struct fuse_data {
                                          /* (and being observed by the daemon) */
 #define FSESS_PUSH_SYMLINKS_IN    0x0020 /* prefix absolute symlinks with mp */
 #define FSESS_DEFAULT_PERMISSIONS 0x0040 /* kernel does permission checking */
+#define FSESS_NO_OPEN_SUPPORT     0x0080 /* can elide FUSE_OPEN ops */
+#define FSESS_NO_OPENDIR_SUPPORT  0x0100 /* can elide FUSE_OPENDIR ops */
 #define FSESS_ASYNC_READ          0x1000 /* allow multiple reads of some file */
 #define FSESS_POSIX_LOCKS         0x2000 /* daemon supports POSIX locks */
 #define FSESS_EXPORT_SUPPORT      0x10000 /* daemon supports NFS-style lookups */
diff --git a/sys/fs/fuse/fuse_kernel.h b/sys/fs/fuse/fuse_kernel.h
index bd7323e9def2..51445637b9a8 100644
--- a/sys/fs/fuse/fuse_kernel.h
+++ b/sys/fs/fuse/fuse_kernel.h
@@ -198,7 +198,7 @@
 #define FUSE_KERNEL_VERSION 7
 
 /** Minor version number of this interface */
-#define FUSE_KERNEL_MINOR_VERSION 28
+#define FUSE_KERNEL_MINOR_VERSION 29
 
 /** The node ID of the root inode */
 #define FUSE_ROOT_ID 1
@@ -300,6 +300,7 @@ struct fuse_file_lock {
  * FUSE_ABORT_ERROR: reading the device after abort returns ECONNABORTED
  * FUSE_MAX_PAGES: init_out.max_pages contains the max number of req pages
  * FUSE_CACHE_SYMLINKS: cache READLINK responses
+ * FUSE_NO_OPENDIR_SUPPORT: kernel supports zero-message opendir
  */
 #define FUSE_ASYNC_READ		(1 << 0)
 #define FUSE_POSIX_LOCKS	(1 << 1)
@@ -325,6 +326,7 @@ struct fuse_file_lock {
 #define FUSE_ABORT_ERROR	(1 << 21)
 #define FUSE_MAX_PAGES		(1 << 22)
 #define FUSE_CACHE_SYMLINKS	(1 << 23)
+#define FUSE_NO_OPENDIR_SUPPORT (1 << 24)
 
 #ifdef linux
 /**
diff --git a/tests/sys/fs/fusefs/open.cc b/tests/sys/fs/fusefs/open.cc
index ede7ea9f04f7..7ac177a65d14 100644
--- a/tests/sys/fs/fusefs/open.cc
+++ b/tests/sys/fs/fusefs/open.cc
@@ -73,6 +73,13 @@ void test_ok(int os_flags, int fuse_flags) {
 };
 
 
+class OpenNoOpenSupport: public FuseTest {
+	virtual void SetUp() {
+		m_init_flags = FUSE_NO_OPEN_SUPPORT;
+		FuseTest::SetUp();
+	}
+};
+
 /* 
  * fusefs(5) does not support I/O on device nodes (neither does UFS).  But it
  * shouldn't crash
@@ -274,3 +281,62 @@ TEST_F(Open, o_rdwr)
 	test_ok(O_RDWR, O_RDWR);
 }
 
+/*
+ * Without FUSE_NO_OPEN_SUPPORT, returning ENOSYS is an error
+ */
+TEST_F(Open, enosys)
+{
+	const char FULLPATH[] = "mountpoint/some_file.txt";
+	const char RELPATH[] = "some_file.txt";
+	uint64_t ino = 42;
+	int fd;
+
+	FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_OPEN &&
+				in.body.open.flags == (uint32_t)O_RDONLY &&
+				in.header.nodeid == ino);
+		}, Eq(true)),
+		_)
+	).Times(1)
+	.WillOnce(Invoke(ReturnErrno(ENOSYS)));
+
+	fd = open(FULLPATH, O_RDONLY);
+	ASSERT_EQ(-1, fd) << strerror(errno);
+	EXPECT_EQ(ENOSYS, errno);
+}
+
+/*
+ * If a fuse server sets FUSE_NO_OPEN_SUPPORT and returns ENOSYS to a
+ * FUSE_OPEN, then it and subsequent FUSE_OPEN and FUSE_RELEASE operations will
+ * also succeed automatically without being sent to the server.
+ */
+TEST_F(OpenNoOpenSupport, enosys)
+{
+	const char FULLPATH[] = "mountpoint/some_file.txt";
+	const char RELPATH[] = "some_file.txt";
+	uint64_t ino = 42;
+	int fd;
+
+	FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_OPEN &&
+				in.body.open.flags == (uint32_t)O_RDONLY &&
+				in.header.nodeid == ino);
+		}, Eq(true)),
+		_)
+	).Times(1)
+	.WillOnce(Invoke(ReturnErrno(ENOSYS)));
+	expect_flush(ino, 1, ReturnErrno(ENOSYS));
+
+	fd = open(FULLPATH, O_RDONLY);
+	ASSERT_LE(0, fd) << strerror(errno);
+	close(fd);
+
+	fd = open(FULLPATH, O_RDONLY);
+	ASSERT_LE(0, fd) << strerror(errno);
+
+	leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/opendir.cc b/tests/sys/fs/fusefs/opendir.cc
index 7943be3124ca..365aa8dff9e2 100644
--- a/tests/sys/fs/fusefs/opendir.cc
+++ b/tests/sys/fs/fusefs/opendir.cc
@@ -73,6 +73,13 @@ void expect_opendir(uint64_t ino, uint32_t flags, ProcessMockerT r)
 
 };
 
+class OpendirNoOpendirSupport: public Opendir {
+	virtual void SetUp() {
+		m_init_flags = FUSE_NO_OPENDIR_SUPPORT;
+		FuseTest::SetUp();
+	}
+};
+
 
 /* 
  * The fuse daemon fails the request with enoent.  This usually indicates a
@@ -172,3 +179,44 @@ TEST_F(Opendir, opendir)
 	errno = 0;
 	EXPECT_NE(nullptr, opendir(FULLPATH)) << strerror(errno);
 }
+
+/*
+ * Without FUSE_NO_OPENDIR_SUPPORT, returning ENOSYS is an error
+ */
+TEST_F(Opendir, enosys)
+{
+	const char FULLPATH[] = "mountpoint/some_file.txt";
+	const char RELPATH[] = "some_file.txt";
+	uint64_t ino = 42;
+
+	expect_lookup(RELPATH, ino);
+	expect_opendir(ino, O_RDONLY, ReturnErrno(ENOSYS));
+
+	EXPECT_EQ(-1, open(FULLPATH, O_DIRECTORY));
+	EXPECT_EQ(ENOSYS, errno);
+}
+
+/*
+ * If a fuse server sets FUSE_NO_OPENDIR_SUPPORT and returns ENOSYS to a
+ * FUSE_OPENDIR, then it and subsequent FUSE_OPENDIR and FUSE_RELEASEDIR
+ * operations will also succeed automatically without being sent to the server.
+ */
+TEST_F(OpendirNoOpendirSupport, enosys)
+{
+	const char FULLPATH[] = "mountpoint/some_file.txt";
+	const char RELPATH[] = "some_file.txt";
+	uint64_t ino = 42;
+	int fd;
+
+	FuseTest::expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 2);
+	expect_opendir(ino, O_RDONLY, ReturnErrno(ENOSYS));
+
+	fd = open(FULLPATH, O_DIRECTORY);
+	ASSERT_LE(0, fd) << strerror(errno);
+	close(fd);
+
+	fd = open(FULLPATH, O_DIRECTORY);
+	ASSERT_LE(0, fd) << strerror(errno);
+
+	leak(fd);
+}