mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Stanislav Kinsburskii <skinsburskii@gmail.com>
To: Bernd Schubert <bernd@bsbernd.com>
Cc: Miklos Szeredi <miklos@szeredi.hu>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>,
	Randy Dunlap <rdunlap@infradead.org>,
	Shuah Khan <shuah@kernel.org>,
	Robert Byrnes <byrnes@wildpumpkin.net>,
	fuse-devel@lists.linux.dev, linux-doc@vger.kernel.org,
	linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org
Subject: Re: [PATCH v3 1/2] fuse: add negotiated per-inode open and release suppression
Date: Fri, 9 Oct 2026 16:25:56 -0700	[thread overview]
Message-ID: <asl4BJhIO6aIZnIm@skinsburskii> (raw)
In-Reply-To: <5d69dce1-ba9b-4eca-8949-94f4a29cea57@bsbernd.com>

On Fri, Oct 09, 2026 at 08:34:30PM +0200, Bernd Schubert wrote:
> 
> 
> On 10/8/26 21:20, Stanislav Kinsburskii wrote:
> > Filesystems serving cached content may not need per-open state for most
> > inodes, while still relying on OPEN for control files. The connection-wide
> > no-open behavior selected by ENOSYS cannot express this distinction.
> > 
> > Add FUSE_PER_INODE_NO_OPEN to INIT negotiation and FUSE_ATTR_NO_OPEN to
> > inode attributes. For marked inodes, use the existing zero-handle defaults
> > and normally omit OPEN/OPENDIR and the corresponding RELEASE/RELEASEDIR.
> > Remember whether each handle was opened locally, so later attribute updates
> > do not determine the release behavior of existing handles.
> > 
> > Retain RELEASE after a successful remote flock operation, even when OPEN
> > was skipped, so FUSE_RELEASE_FLOCK_UNLOCK can clean up server-side locks.
> > Servers negotiating remote flock must accept this RELEASE with a zero file
> > handle and no preceding OPEN, including after an explicit unlock.
> 
> fuse_file_put() would get a lot easier if FUSE_PER_INODE_NO_OPEN is ignored 
> when FUSE_PER_INODE_NO_OPEN gets ignore if FUSE_FLOCK_LOCKS is set. 
> I personally don't like the idea of release without an open.
> 
> Do you really need flock and this per-inode no-open feature?
> 

I'm fine with making remote flock and per-inode open suppression mutually
exclusive for a given handle. However, FUSE_FLOCK_LOCKS is negotiated per
connection, while FUSE_ATTR_NO_OPEN applies per inode. Disabling the new
capability at INIT would also prevent suppressing opens on cached content
files when remote flock is needed only for unmarked control files.

Would keeping both capabilities, but rejecting remote flock on handles
opened locally through FUSE_ATTR_NO_OPEN, be acceptable?
That would avoid RELEASE without OPEN while preserving remote flock for
other files.

> > 
> > Keep the release argument allocation for regular files, which pins the
> > inode while asynchronous I/O completes. Honor the hint for internal opens
> > used by file-attribute ioctls as well. The capability check excludes CUSE
> > before accessing its non-FUSE inode as a fuse_inode.
> > 
> > The per-inode hint does not suppress OPEN for atomic O_TRUNC, since the
> > server must perform the truncation. CREATE retains its existing handle
> > lifecycle. Connection-wide no-open/no-opendir behavior selected by ENOSYS
> > continues to take precedence.
> > 
> > Update the cached hint from LOOKUP, GETATTR, SETATTR and READDIRPLUS
> > attributes. Preserve it across STATX replies, which do not carry
> > fuse_attr.flags.
> > 
> > Document negotiation, cache and handle semantics, and the server's
> > responsibilities. No additional access-time or open-reference accounting
> > is introduced.
> > 
> > Signed-off-by: Stanislav Kinsburskii <skinsburskii@gmail.com>
> > ---
> >  Documentation/filesystems/fuse/fuse-no-open.rst | 49 +++++++++++++++++++++++++
> >  Documentation/filesystems/fuse/index.rst        |  1 +
> >  fs/fuse/file.c                                  | 26 ++++++++++---
> >  fs/fuse/fuse_i.h                                | 17 ++++++++-
> >  fs/fuse/inode.c                                 |  9 +++++
> >  fs/fuse/ioctl.c                                 |  3 +-
> >  include/uapi/linux/fuse.h                       | 13 ++++++-
> >  7 files changed, 110 insertions(+), 8 deletions(-)
> > 
> > diff --git a/Documentation/filesystems/fuse/fuse-no-open.rst b/Documentation/filesystems/fuse/fuse-no-open.rst
> > new file mode 100644
> > index 000000000000..2e0cf43f9ef7
> > --- /dev/null
> > +++ b/Documentation/filesystems/fuse/fuse-no-open.rst
> > @@ -0,0 +1,49 @@
> > +.. SPDX-License-Identifier: GPL-2.0
> > +
> > +Per-inode open suppression
> > +==========================
> > +
> > +A filesystem can avoid OPEN and RELEASE requests for individual inodes by
> > +negotiating FUSE_PER_INODE_NO_OPEN in INIT and setting FUSE_ATTR_NO_OPEN in
> > +``fuse_attr.flags``.  For directories, the flag suppresses OPENDIR and
> > +RELEASEDIR instead.  This allows, for example, cached content files to avoid
> > +open round trips while control files on the same connection retain their
> > +open handlers.  Without the negotiated capability the attribute is ignored.
> > +
> > +The kernel updates the hint when it accepts attributes in replies such as
> > +LOOKUP, GETATTR, SETATTR and READDIRPLUS.  STATX replies do not carry
> > +``fuse_attr.flags`` and leave the hint unchanged.  The hint is cached inode
> > +state; it is not independently revalidated on every open.  A server changing
> > +the hint must arrange for fresh attributes to reach the kernel and tolerate
> > +concurrent opens using the previous value.
> > +
> > +For an open served locally, the file handle is zero, FOPEN_KEEP_CACHE is
> > +set, and directories also have FOPEN_CACHE_DIR set.  Subsequent requests
> > +identify the object by the node ID and may carry a zero file handle.  The
> > +server must support these requests without per-open state.  Whether OPEN was
> > +sent is recorded for each handle and is not changed by later attribute
> > +updates.  An existing server-opened handle still receives its matching
> > +RELEASE if the inode hint subsequently becomes set.
> > +
> > +Remote flock locking is an exception to RELEASE suppression.  When
> > +FUSE_FLOCK_LOCKS is negotiated and a handle has successfully performed a
> > +server-side flock operation, its final close sends RELEASE with
> > +FUSE_RELEASE_FLOCK_UNLOCK and the lock owner, even if OPEN was suppressed.
> > +The server must accept this RELEASE with a zero file handle and no preceding
> > +OPEN, and use the node ID and lock owner to remove any remaining flock locks.
> > +This also applies after an explicit unlock, since the kernel records whether
> > +a flock operation succeeded rather than tracking the server's current locks.
> > +
> > +When FUSE_ATOMIC_O_TRUNC is negotiated, an open with O_TRUNC still sends
> > +OPEN and receives a matching RELEASE, so the server can perform truncation.
> > +CREATE also retains its usual open and release semantics.  Connection-wide
> > +no-open behavior selected by an ENOSYS response continues to take precedence.
> > +
> > +The hint does not make an inode immutable, grant permissions, or suppress
> > +other operations such as FLUSH, FSYNC, locking or data I/O.  Servers supporting
> > +remote flock must implement the RELEASE cleanup described above.  A server must
> > +only set it when its access policy and file semantics permit the default
> > +open behavior described above.  Servers needing per-open authorization,
> > +nonzero handles, direct I/O, passthrough or other OPEN reply flags must keep
> > +handling OPEN for those inodes.  No additional access-time or open-reference
> > +accounting is performed by this feature.
> > diff --git a/Documentation/filesystems/fuse/index.rst b/Documentation/filesystems/fuse/index.rst
> > index 3dada6c4057a..6dd9192f74fe 100644
> > --- a/Documentation/filesystems/fuse/index.rst
> > +++ b/Documentation/filesystems/fuse/index.rst
> > @@ -12,4 +12,5 @@ FUSE (Filesystem in Userspace) Technical Documentation
> >     fuse-io
> >     fuse-io-uring
> >     fuse-passthrough
> > +   fuse-no-open
> >     uapi/fuse-uapi-io-uring
> > diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> > index 3d209e2b71ba..31c22ff5c8c1 100644
> > --- a/fs/fuse/file.c
> > +++ b/fs/fuse/file.c
> > @@ -108,8 +108,9 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
> >  			fuse_file_io_release(ff, ra->inode);
> >  
> >  		if (!args) {
> > -			/* Do nothing when server does not implement 'opendir' */
> > -		} else if (args->opcode == FUSE_RELEASE && ff->fm->fc->no_open) {
> > +			/* No release needed when OPENDIR was skipped. */
> > +		} else if (args->opcode == FUSE_RELEASE &&
> > +			   (ff->fm->fc->no_open || (ff->no_open && !ff->flock))) {
> >  			fuse_release_end(args, 0);
> 
> I think this could be become
> 
> if (!args) {
>         /* No release needed when OPENDIR was skipped. */
> } else if (ff->no_open) {
>         fuse_release_end(args, 0);
> } else if (sync) {
> 
> just in fuse_file_open() you could set ff->no_open for the -ENOSYS condition and
> if FUSE_PER_INODE_NO_OPEN gets ignored when FUSE_RELEASE_FLOCK_UNLOCK is set.
> 
> 
> What do you think?
> 

I agree.

Thanks,
Stanislav

> 
> Thanks,
> Bernd
> 
> 
> >  		} else if (sync) {
> >  			fuse_simple_request(ff->fm, args);
> > @@ -130,13 +131,26 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
> >  	}
> >  }
> >  
> > +static bool fuse_open_needed(struct fuse_conn *fc, unsigned int open_flags,
> > +			     bool isdir, bool no_open)
> > +{
> > +	if (isdir ? fc->no_opendir : fc->no_open)
> > +		return false;
> > +
> > +	/* Atomic truncation must still be performed by the server's OPEN. */
> > +	if ((open_flags & O_TRUNC) && fc->atomic_o_trunc)
> > +		return true;
> > +
> > +	return !no_open;
> > +}
> > +
> >  struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> > -				 unsigned int open_flags, bool isdir)
> > +				 unsigned int open_flags, bool isdir, bool no_open)
> >  {
> >  	struct fuse_conn *fc = fm->fc;
> >  	struct fuse_file *ff;
> >  	int opcode = isdir ? FUSE_OPENDIR : FUSE_OPEN;
> > -	bool open = isdir ? !fc->no_opendir : !fc->no_open;
> > +	bool open = fuse_open_needed(fc, open_flags, isdir, no_open);
> >  	bool release = !isdir || open;
> >  
> >  	/*
> > @@ -152,6 +166,7 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> >  		return ERR_PTR(-ENOMEM);
> >  
> >  	ff->fh = 0;
> > +	ff->no_open = !open;
> >  	/* Default for no-open */
> >  	ff->open_flags = FOPEN_KEEP_CACHE | (isdir ? FOPEN_CACHE_DIR : 0);
> >  	if (open) {
> > @@ -189,7 +204,8 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> >  int fuse_do_open(struct fuse_mount *fm, u64 nodeid, struct file *file,
> >  		 bool isdir)
> >  {
> > -	struct fuse_file *ff = fuse_file_open(fm, nodeid, file->f_flags, isdir);
> > +	struct fuse_file *ff = fuse_file_open(fm, nodeid, file->f_flags, isdir,
> > +					    fuse_inode_no_open(fm->fc, file_inode(file)));
> >  
> >  	if (!IS_ERR(ff))
> >  		file->private_data = ff;
> > diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> > index 87e2bd9d4bb1..76157a84db22 100644
> > --- a/fs/fuse/fuse_i.h
> > +++ b/fs/fuse/fuse_i.h
> > @@ -257,6 +257,8 @@ enum {
> >  	 * or the fuse server has an exclusive "lease" on distributed fs
> >  	 */
> >  	FUSE_I_EXCLUSIVE,
> > +	/* Server does not need OPEN/OPENDIR for this inode */
> > +	FUSE_I_NO_OPEN,
> >  };
> >  
> >  struct fuse_conn;
> > @@ -322,6 +324,9 @@ struct fuse_file {
> >  
> >  	/** @flock: Has flock been performed on this file? */
> >  	bool flock:1;
> > +
> > +	/** @no_open: Open was served locally without an OPEN/OPENDIR request */
> > +	bool no_open:1;
> >  };
> >  
> >  struct fuse_release_args {
> > @@ -556,6 +561,9 @@ struct fuse_conn {
> >  	/** @no_opendir: Is opendir/releasedir not implemented by fs? */
> >  	unsigned no_opendir:1;
> >  
> > +	/** @per_inode_no_open: Honor FUSE_ATTR_NO_OPEN */
> > +	unsigned per_inode_no_open:1;
> > +
> >  	/** @no_fsync: Is fsync not implemented by fs? */
> >  	unsigned no_fsync:1;
> >  
> > @@ -833,6 +841,13 @@ static inline struct fuse_inode *get_fuse_inode(const struct inode *inode)
> >  	return container_of(inode, struct fuse_inode, inode);
> >  }
> >  
> > +static inline bool fuse_inode_no_open(struct fuse_conn *fc, struct inode *inode)
> > +{
> > +	/* CUSE uses a non-FUSE inode and cannot negotiate this capability. */
> > +	return fc->per_inode_no_open &&
> > +		test_bit(FUSE_I_NO_OPEN, &get_fuse_inode(inode)->state);
> > +}
> > +
> >  static inline u64 get_node_id(struct inode *inode)
> >  {
> >  	return get_fuse_inode(inode)->nodeid;
> > @@ -1261,7 +1276,7 @@ void fuse_file_io_release(struct fuse_file *ff, struct inode *inode);
> >  
> >  /* file.c */
> >  struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> > -				 unsigned int open_flags, bool isdir);
> > +				 unsigned int open_flags, bool isdir, bool no_open);
> >  void fuse_file_release(struct inode *inode, struct fuse_file *ff,
> >  		       unsigned int open_flags, fl_owner_t id, bool isdir);
> >  
> > diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
> > index cbb10e19e7e8..63924d95caa3 100644
> > --- a/fs/fuse/inode.c
> > +++ b/fs/fuse/inode.c
> > @@ -299,6 +299,11 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr,
> >  	 * anyway. Its less efficient but should be safe.
> >  	 */
> >  	inode->i_flags &= ~S_NOSEC;
> > +
> > +	/* STATX replies do not carry fuse_attr.flags. */
> > +	if (fc->per_inode_no_open && !sx)
> > +		assign_bit(FUSE_I_NO_OPEN, &fi->state,
> > +			   attr->flags & FUSE_ATTR_NO_OPEN);
> >  }
> >  
> >  u32 fuse_get_cache_mask(struct inode *inode)
> > @@ -1432,6 +1437,8 @@ static void process_init_reply(struct fuse_args *args, int error)
> >  
> >  			if (fuse_syncfs_enable(fc, flags))
> >  				fc->sync_fs = 1;
> > +			if (flags & FUSE_PER_INODE_NO_OPEN)
> > +				fc->per_inode_no_open = 1;
> >  		} else {
> >  			ra_pages = fc->max_read / PAGE_SIZE;
> >  			fc->no_lock = 1;
> > @@ -1510,6 +1517,8 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
> >  	if (fuse_uring_enabled())
> >  		flags |= FUSE_OVER_IO_URING | FUSE_HAS_IO_URING_BUFPOOL;
> >  
> > +	flags |= FUSE_PER_INODE_NO_OPEN;
> > +
> >  	ia->in.flags = flags;
> >  	ia->in.flags2 = flags >> 32;
> >  
> > diff --git a/fs/fuse/ioctl.c b/fs/fuse/ioctl.c
> > index ce1807704da6..7fc11bb3eee9 100644
> > --- a/fs/fuse/ioctl.c
> > +++ b/fs/fuse/ioctl.c
> > @@ -492,7 +492,8 @@ static struct fuse_file *fuse_priv_ioctl_prepare(struct inode *inode)
> >  	if (!S_ISREG(inode->i_mode) && !isdir)
> >  		return ERR_PTR(-ENOTTY);
> >  
> > -	return fuse_file_open(fm, get_node_id(inode), O_RDONLY, isdir);
> > +	return fuse_file_open(fm, get_node_id(inode), O_RDONLY, isdir,
> > +			      fuse_inode_no_open(fm->fc, inode));
> >  }
> >  
> >  static void fuse_priv_ioctl_cleanup(struct inode *inode, struct fuse_file *ff)
> > diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
> > index 10a7f31c4bdf..1b5699407e11 100644
> > --- a/include/uapi/linux/fuse.h
> > +++ b/include/uapi/linux/fuse.h
> > @@ -251,6 +251,9 @@
> >   *
> >   *  7.47
> >   *  - add FUSE_HAS_SYNCFS opt-in flag for privileged userspace servers
> > + *
> > + *  7.48
> > + *  - add FUSE_PER_INODE_NO_OPEN and FUSE_ATTR_NO_OPEN
> >   */
> >  
> >  #ifndef _LINUX_FUSE_H
> > @@ -286,7 +289,7 @@
> >  #define FUSE_KERNEL_VERSION 7
> >  
> >  /** Minor version number of this interface */
> > -#define FUSE_KERNEL_MINOR_VERSION 47
> > +#define FUSE_KERNEL_MINOR_VERSION 48
> >  
> >  /** The node ID of the root inode */
> >  #define FUSE_ROOT_ID 1
> > @@ -473,6 +476,7 @@ struct fuse_file_lock {
> >   *		with CAP_SYS_ADMIN in the initial user namespace (the same
> >   *		privilege that mounting virtiofs or fuseblk requires).
> >   *		Insufficiently privileged servers ignore it.
> > + * FUSE_PER_INODE_NO_OPEN: honor FUSE_ATTR_NO_OPEN in inode attributes
> >   */
> >  #define FUSE_ASYNC_READ		(1 << 0)
> >  #define FUSE_POSIX_LOCKS	(1 << 1)
> > @@ -522,6 +526,7 @@ struct fuse_file_lock {
> >  #define FUSE_REQUEST_TIMEOUT	(1ULL << 42)
> >  #define FUSE_HAS_IO_URING_BUFPOOL (1ULL << 43)
> >  #define FUSE_HAS_SYNCFS		(1ULL << 44)
> > +#define FUSE_PER_INODE_NO_OPEN	(1ULL << 45)
> >  
> >  /**
> >   * CUSE INIT request/reply flags
> > @@ -605,9 +610,15 @@ struct fuse_file_lock {
> >   *
> >   * FUSE_ATTR_SUBMOUNT: Object is a submount root
> >   * FUSE_ATTR_DAX: Enable DAX for this file in per inode DAX mode
> > + * FUSE_ATTR_NO_OPEN: Skip OPEN/OPENDIR and matching RELEASE/RELEASEDIR;
> > + *                   requires FUSE_PER_INODE_NO_OPEN. Atomic O_TRUNC opens
> > + *                   still go to the server. RELEASE is also sent after a
> > + *                   successful remote flock operation to clean up locks,
> > + *                   even when OPEN was skipped and the file handle is zero.
> >   */
> >  #define FUSE_ATTR_SUBMOUNT      (1 << 0)
> >  #define FUSE_ATTR_DAX		(1 << 1)
> > +#define FUSE_ATTR_NO_OPEN	(1 << 2)
> >  
> >  /**
> >   * Open flags
> > 
> 

  reply	other threads:[~2026-10-09 23:25 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-08 19:20 [PATCH v3 0/2] fuse: support " Stanislav Kinsburskii
2026-10-08 19:20 ` [PATCH v3 1/2] fuse: add negotiated " Stanislav Kinsburskii
2026-10-09 18:34   ` Bernd Schubert
2026-10-09 23:25     ` Stanislav Kinsburskii [this message]
2026-10-08 19:20 ` [PATCH v3 2/2] selftests: fuse: test per-inode open suppression Stanislav Kinsburskii

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=asl4BJhIO6aIZnIm@skinsburskii \
    --to=skinsburskii@gmail.com \
    --cc=bernd@bsbernd.com \
    --cc=byrnes@wildpumpkin.net \
    --cc=corbet@lwn.net \
    --cc=fuse-devel@lists.linux.dev \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=miklos@szeredi.hu \
    --cc=rdunlap@infradead.org \
    --cc=shuah@kernel.org \
    --cc=skhan@linuxfoundation.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®