Re: [PATCH 7/7] nfsd: use vfs_lookup_open() for non-creating open requests too.

NeilBrown <[email protected]>
Newsgroups org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-nfs
Message-ID <[email protected]>
On Fri, 11 Sep 2026, Chuck Lever wrote:
> On Thu, 10 Sep 2026, NeilBrown wrote:
> 
> > nfsd: use vfs_lookup_open() for non-creating open requests too.
> >
> > Now that we have vfs_lookup_open() for open requests which create, we
> > can use it for non-creating requests too as vfs_lookup_open() is already
> > able to do that.  nfsd4_create_file() is renamed to nfsd4_open_file()
> > and enhanced to not always create, and is then used for all OPEN
> > requests.
> >
> > The resulting simplification allows fh_fill_pre_attrs_unlocked() to be
> > moved into nfsd4_open_file() so it is closer to fh_full_post_attrs and
> > fh_fill_post_noop calls.
> 
> fh_full_post_attrs is fh_fill_post_attrs().
> 

Fixed.

> 
> > As ->op_create_mode isn't defined when op_create is zero, we need a
> > local create_mode which is -1 (illegal value) when op_create is zero.
> 
> The field is ->op_createmode and the local is createmode.
> 

Fixed.

> 
> > The non-create path now doesn't use nfsd_lookup().  As mount-point
> > crossing including nfsd_check_access() is already included for existing
> > names, this does not lose us anything.
> 
> The function is check_nfsd_access(), reached through nfsd_cross_mnt().
> 
> Is "already included for existing names" accurate? The only mount-point
> crossing left in nfsd4_open_file() is the try_lookup_noperm() block,
> and that consults only the dcache. More on that below.

It is now!

> 
> 
> > Signed-off-by: NeilBrown <[email protected]>
> > ---
> >  fs/nfsd/nfs4proc.c | 109 ++++++++++++++++++++++++++---------------------------
> >  1 file changed, 53 insertions(+), 56 deletions(-)
> >
> > diff --git a/fs/nfsd/nfs4proc.c b/fs/nfsd/nfs4proc.c
> > index a6ba7618a307..1f3096486565 100644
> > --- a/fs/nfsd/nfs4proc.c
> > +++ b/fs/nfsd/nfs4proc.c
> > @@ -251,29 +251,30 @@ static inline bool nfsd4_create_is_exclusive(int createmode)
> >  }
> >
> >  /*
> > - * Implement NFSv4's unchecked, guarded, and exclusive create
> > - * semantics for regular files. Open state for this new file is
> > - * subsequently fabricated in nfsd4_process_open2().
> > - *
> > + * Implement NFSv4's open semantics for regular files.
> > + * Both create (unchecked, guarded, and exclusive) and non-create.
> > + * Open state for this new file is subsequently fabricated in
> > + * nfsd4_process_open2().
> >   * Upon return, caller must release @fhp and @resfhp.
> >   */
> 
> "Both create ... and non-create." is a fragment, and "this new file" no
> longer fits when the file may already exist. Perhaps:

Thanks.

> 
>  * Implement NFSv4 OPEN semantics for regular files, both non-creating
>  * and creating (unchecked, guarded, and exclusive). Open state for the
>  * file is subsequently fabricated in nfsd4_process_open2().
>  *
>  * Upon return, caller must release @fhp and @resfhp.
> 
> 
> > @@ -285,6 +286,10 @@ nfsd4_create_file(struct svc_rqst *rqstp, struct svc_fh *fhp,
> >  	if (status != nfs_ok)
> >  		return status;
> >
> > +	status = fh_fill_pre_attrs_unlocked(fhp);
> > +	if (status)
> > +		return status;
> > +
> >  	if (open->op_createmode == NFS4_CREATE_UNCHECKED) {
> >  		/*
> >  		 * If name is already in dcache we need to check for mountpoints
> 
> First, the test reads open->op_createmode, which the commit message says
> is undefined when op_create is zero. It works because
> nfsd4_decode_open() zeroes the whole struct, so a non-creating OPEN
> sees NFS4_CREATE_UNCHECKED here. Could the op_create test that sets
> createmode be hoisted above this block, and the condition become

oops. Fixed.

> 
> 	if (!open->op_create || createmode == NFS4_CREATE_UNCHECKED) {
> 
> so the non-create path does not depend on that zero?
> 
> Second, nfsd_lookup_dentry() did a real lookup and then called
> nfsd_cross_mnt() whenever nfsd_mountpoint() was true. try_lookup_noperm()
> returns NULL when the name is not cached. In that case vfs_lookup_open()
> opens whatever it finds on the parent's mount and fh_compose() uses the
> parent's export.
> 
> For a referral junction (a regular file carrying the junction xattr),
> nfsd_cross_mnt() would have swapped in the referral export. On a cold
> dcache the client instead gets an ordinary filehandle for the junction
> file, and a second OPEN of the same name, now cached, takes the
> crossing path. The same applies to a bind-mounted file whose mount
> target has its own export.
> 
> The Linux client sends OPEN by name without a preceding LOOKUP, so the
> first access after a server reboot reaches this. Could the block do a
> real lookup of the name, as nfsd_lookup_dentry() does, before deciding
> whether to cross?

I don't think we want a real lookup.  I think we want to stick with the
the vfs_open_lookup() call, but if it says it didn't create the file,
then we need to check for a mountpoint.  I think this might be a
long-standing buglet.  I've addressed for my next posting.

> 
> 
> > @@ -373,11 +382,11 @@ nfsd4_create_file(struct svc_rqst *rqstp, struct svc_fh *fhp,
> >  		iap->ia_atime.tv_nsec = 0;
> >  	}
> >
> > -	create_status = fh_verify(rqstp, fhp, S_IFDIR, NFSD_MAY_CREATE);
> > -	if (create_status)
> > -		/* Might still succeed if no create is needed */
> > -		oflags &= ~O_CREAT;
> > -
> > +	if (oflags & O_CREAT) {
> 
> Nit: O_CREAT is set only from open->op_create a few lines up. Would
> testing open->op_create here read more directly?

As the guarded code clears O_CREAT if there is an error, I think it
works well to test for O_CREAT.  So I'd prefer to leave this one as-is.

> 
> 
> > +		create_status = fh_verify(rqstp, fhp, S_IFDIR, NFSD_MAY_CREATE);
> > +		if (create_status)
> > +			oflags &= ~O_CREAT;
> > +	}
> >  	dget(parent.dentry);
> >  	open->op_filp = vfs_lookup_open(&parent,
> >  					&QSTR_LEN(open->op_fname,
> 
> This changes when a delegation is recalled relative to the access
> check. oflags carries O_WRONLY or O_RDWR from op_share_access, and
> vfs_lookup_open() opens the child with those flags. do_dentry_open()
> does no permission check of its own and calls break_lease() on the
> inode, so a conflicting delegation held by another client is recalled
> here. Only afterwards does do_open_lookup() call do_open_permission()
> and return NFS4ERR_ACCESS.
> 
> Before this patch a non-creating OPEN went through nfsd_lookup() and
> do_open_permission() first, and the open that breaks the lease
> happened later in nfsd_file_do_acquire(), where fh_verify() precedes
> nfsd_open_break_lease(). So a client that can search the directory
> but has no write permission on the file can now recall other clients'
> write delegations on it by sending OPEN with SHARE_ACCESS_WRITE.
> 
> The create path has had this ordering since nfsd4_create_file() started
> opening the file itself, but it was limited to opens that pass
> NFSD_MAY_CREATE on the directory. Extending it to every OPEN makes it
> reachable from any client with lookup access.
> 
> Could the access check move ahead of the open for the non-creating
> case, or could the open be done O_RDONLY for the type check and the
> share-access open deferred to the filecache as before?

Yes.  vfs_lookup_open() really should be doing an access check before
calling vfs_open() - calling do_open() there has the desired effect.
I've added a patch.

> 
> 
> > @@ -387,7 +396,7 @@ nfsd4_create_file(struct svc_rqst *rqstp, struct svc_fh *fhp,
> >  	if (IS_ERR(open->op_filp)) {
> >  		status = nfserrno(PTR_ERR(open->op_filp));
> >  		if (status == nfserr_wrong_type) {
> > -			if (nfsd4_create_is_exclusive(open->op_createmode))
> > +			if (nfsd4_create_is_exclusive(createmode))
> >  				status = nfserr_exist;
> >  			else
> >  				status = nfsd_check_obj_isreg(parent.dentry);
> 
> On a re-export this arm changes the status a non-creating OPEN of a
> directory returns. nfsd_check_obj_isreg(NULL) returns
> nfserr_wrong_type, where nfsd_lookup() used to return nfserr_isdir.

I've changed this code a lot - there is no longer a chance of this
problem occurring.

> 
> The Linux client has no mapping for NFS4ERR_WRONG_TYPE, so
> nfs4_map_errors() turns it into EIO. A "cat" of a re-exported directory
> that is not yet in the server's dcache now fails with EIO instead of
> EISDIR. NFSv4.0 clients are covered by the nfserr_symlink conversion in
> do_open_lookup() and recover through a LOOKUP; v4.1 and later see the
> change. Ceph splices before returning -EFTYPE, so it is not affected,
> but nfs, gfs2 and smb all return -EFTYPE before instantiating anything.

I've decide to fix the various atomic_open implementations so they
consistently do what I want them too :-) I've even added
NFS4ERR_WRONG_TYPE handling for nfs.

Thanks for the awesome review!

NeilBrown


> 
> 
> -- 
> Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.