Re: [PATCH v6 20/20] nfsd: add support to CB_NOTIFY for dir attribute changes

"Chuck Lever" <[email protected]>
Newsgroups gmane.linux.nfs,gmane.linux.kernel,gmane.linux.documentation
Message-ID <[email protected]>

On Thu, Jun 11, 2026, at 1:50 PM, Jeff Layton wrote:
> If the client requested dir attribute change notifications, send those
> alongside any set of add/remove/rename events. Note that the server will
> still recall the delegation on a SETATTR, so these are only sent for
> changes to child dirents.
>
> The child filehandle returned in these notifications is composed by
> setup_notify_fhandle() without going through fh_compose(), so it does
> not get a MAC appended. On exports configured with NFSEXP_SIGN_FH the
> client would then get back an unsigned filehandle that fh_verify()
> rejects as stale. Pass the delegation's export down to
> setup_notify_fhandle() and append the MAC with fh_append_mac() when the
> export requires signed filehandles; if signing fails, drop the
> filehandle attribute rather than handing out an unusable one.
>
> Signed-off-by: Jeff Layton <[email protected]>
> ---
>  fs/nfsd/nfs4state.c | 25 ++++++++++++++++--
>  fs/nfsd/nfs4xdr.c   | 73 +++++++++++++++++++++++++++++++++++++++++++++--------
>  fs/nfsd/xdr4.h      |  2 ++
>  3 files changed, 88 insertions(+), 12 deletions(-)
>
> diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
> index 12627afb604f..e394278fb92e 100644
> --- a/fs/nfsd/nfs4state.c
> +++ b/fs/nfsd/nfs4state.c
> @@ -3503,10 +3503,15 @@ nfsd4_cb_notify_prepare(struct nfsd4_callback *cb)
>  	struct nfsd_notify_event *events[NOTIFY4_EVENT_QUEUE_SIZE];
>  	struct xdr_buf xdr = { .buflen = PAGE_SIZE * NOTIFY4_PAGE_ARRAY_SIZE,
>  			       .pages  = ncn->ncn_pages };
> +	int limit = NOTIFY4_EVENT_QUEUE_SIZE;

When a client requests NOTIFY4_CHANGE_DIR_ATTRS, the CB_NOTIFY event
queue can fill to NOTIFY4_EVENT_QUEUE_SIZE (3) events while the consumer
only accepts 2 (it reserves a slot for the dir-attr-change entry). The
resulting overflow path in nfsd4_cb_notify_prepare() recalls the
delegation without draining the queue, and nfsd4_cb_notify_release()
then requeues the same callback indefinitely.


>  	struct xdr_stream stream;
>  	struct nfsd_file *nf;
> -	int count, i;
>  	bool error = false;
> +	int count, i;
> +
> +	/* Save a slot for dir attr update if requested */
> +	if (dp->dl_notify_mask & BIT(NOTIFY4_CHANGE_DIR_ATTRS))
> +		--limit;
> 
>  	xdr_init_encode_pages(&stream, &xdr);
> 
> @@ -3520,7 +3525,7 @@ nfsd4_cb_notify_prepare(struct nfsd4_callback *cb)
>  	}
> 
>  	/* we can't keep up! */
> -	if (count > NOTIFY4_EVENT_QUEUE_SIZE) {
> +	if (count > limit) {
>  		spin_unlock(&ncn->ncn_lock);
>  		goto out_recall;
>  	}
> @@ -3567,6 +3572,22 @@ nfsd4_cb_notify_prepare(struct nfsd4_callback 
> *cb)
>  		nfsd_notify_event_put(nne);
>  	}
>  	if (!error) {
> +		if (dp->dl_notify_mask & BIT(NOTIFY4_CHANGE_DIR_ATTRS)) {
> +			u32 *maskp = (u32 *)xdr_reserve_space(&stream, sizeof(*maskp));
> +
> +			if (maskp) {
> +				u8 *p = nfsd4_encode_dir_attr_change(&stream, dp, nf);
> +
> +				if (p) {
> +					*maskp = BIT(NOTIFY4_CHANGE_DIR_ATTRS);
> +					ncn->ncn_nf[count].notify_mask.count = 1;
> +					ncn->ncn_nf[count].notify_mask.element = maskp;
> +					ncn->ncn_nf[count].notify_vals.data = p;
> +					ncn->ncn_nf[count].notify_vals.len = (u8 *)stream.p - p;
> +					++count;
> +				}
> +			}
> +		}

Nit:

When xdr_reserve_space() for maskp succeeds but nfsd4_encode_dir_attr_change()
returns NULL, the 4-byte reservation is never rolled back and *maskp is never
written, yet the function still takes the success path (return true). Unlike
the child-event loop, this branch does not escalate to error = true.

This is probably benign only because nfs4_xdr_enc_cb_notify re-encodes from
the ncn_nf[] array (and count was not incremented), so the garbage hole is
never transmitted.


>  		ncn->ncn_nf_cnt = count;
>  		nfsd_file_put(nf);
>  		return true;
> diff --git a/fs/nfsd/nfs4xdr.c b/fs/nfsd/nfs4xdr.c
> index 1e3c360c06cd..7dd8476028d6 100644
> --- a/fs/nfsd/nfs4xdr.c
> +++ b/fs/nfsd/nfs4xdr.c
> @@ -4199,7 +4199,8 @@ nfsd4_encode_fattr4(struct svc_rqst *rqstp, 
> struct xdr_stream *xdr,
> 
>  static bool
>  setup_notify_fhandle(struct dentry *dentry, struct nfs4_file *fi,
> -		     struct nfsd_file *nf, struct nfsd4_fattr_args *args)
> +		     struct nfsd_file *nf, struct svc_export *exp,
> +		     struct nfsd4_fattr_args *args)
>  {
>  	int fileid_type, fsid_len, maxsize, flags = 0;
>  	struct knfsd_fh *fhp = &args->fhandle;

The function dereferences the new exp parameter unconditionally.

The argument is dp->dl_stid.sc_export, read unlocked at
nfs4xdr.c:4297 and handed down. An in-flight CB_NOTIFY callback holds a
sc_count reference but NOT an export reference. drop_stid_export() can
run concurrently (admin revoke / unexport), NULL sc_export, and drop what
may be the last export reference, freeing the svc_export while the
callback dereferences it.


> @@ -4227,6 +4228,17 @@ setup_notify_fhandle(struct dentry *dentry, 
> struct nfs4_file *fi,
> 
>  	fhp->fh_fileid_type = fileid_type;
>  	fhp->fh_size += maxsize * 4;
> +
> +	/*
> +	 * fh_compose() appends a MAC to filehandles on signed exports; this
> +	 * hand-rolled filehandle must do the same or the client will get back
> +	 * an unsigned filehandle that fh_verify() later rejects as stale.
> +	 * If we can't sign it, don't hand it out at all.
> +	 */
> +	if (exp && (exp->ex_flags & NFSEXP_SIGN_FH))
> +		if (!fh_append_mac(fhp, NFS4_FHSIZE, exp->cd->net))
> +			return false;
> +
>  	return true;
>  }
> 
> @@ -4240,11 +4252,11 @@ nfsd4_setup_notify_entry4(struct notify_entry4 
> *ne, struct xdr_stream *xdr,
>  			  struct nfsd_file *nf, char *name, u32 namelen)
>  {
>  	struct nfs4_file *fi = dp->dl_stid.sc_file;
> -	struct path path =  { .mnt = nf->nf_file->f_path.mnt,
> -			      .dentry = dentry };
> +	struct path path = nf->nf_file->f_path;
>  	struct nfsd4_fattr_args args = { };
>  	uint32_t *attrmask;
>  	__be32 status;
> +	bool parent;
>  	int ret;
> 
>  	/* Reserve space for attrmask */
> @@ -4256,6 +4268,9 @@ nfsd4_setup_notify_entry4(struct notify_entry4 
> *ne, struct xdr_stream *xdr,
>  	ne->ne_file.len = namelen;
>  	ne->ne_attrs.attrmask.element = attrmask;
> 
> +	parent = (dentry == path.dentry);
> +	path.dentry = dentry;
> +
>  	/* FIXME: d_find_alias for inode ? */
>  	if (!path.dentry || !d_inode(path.dentry))
>  		goto noattrs;
> @@ -4271,15 +4286,21 @@ nfsd4_setup_notify_entry4(struct notify_entry4 
> *ne, struct xdr_stream *xdr,
> 
>  	args.change_attr = nfsd4_change_attribute(&args.stat);
> 
> -	attrmask[0] = dp->dl_child_attrs[0];
> -	attrmask[1] = dp->dl_child_attrs[1];
> -	attrmask[2] = 0;
> +	if (parent) {
> +		attrmask[0] = dp->dl_dir_attrs[0];
> +		attrmask[1] = dp->dl_dir_attrs[1];
> +	} else {
> +		attrmask[0] = dp->dl_child_attrs[0];
> +		attrmask[1] = dp->dl_child_attrs[1];
> 
> -	if (!setup_notify_fhandle(dentry, fi, nf, &args))
> -		attrmask[0] &= ~FATTR4_WORD0_FILEHANDLE;
> +		if (!setup_notify_fhandle(dentry, fi, nf,
> +					  dp->dl_stid.sc_export, &args))
> +			attrmask[0] &= ~FATTR4_WORD0_FILEHANDLE;
> 
> -	if (!(args.stat.result_mask & STATX_BTIME))
> -		attrmask[1] &= ~FATTR4_WORD1_TIME_CREATE;
> +		if (!(args.stat.result_mask & STATX_BTIME))
> +			attrmask[1] &= ~FATTR4_WORD1_TIME_CREATE;
> +	}
> +	attrmask[2] = 0;
> 
>  	ne->ne_attrs.attrmask.count = 2;
>  	ne->ne_attrs.attr_vals.data = (u8 *)xdr->p;
> @@ -4392,6 +4413,38 @@ u8 *nfsd4_encode_notify_event(struct xdr_stream 
> *xdr, struct nfsd_notify_event *
>  	return NULL;
>  }
> 
> +/**
> + * nfsd4_encode_dir_attr_change
> + * @xdr: stream to which to encode the fattr4
> + * @dp: delegation where the event occurred
> + * @nf: nfsd_file opened on the directory
> + *
> + * Encode a dir attr change event.
> + */
> +u8 *nfsd4_encode_dir_attr_change(struct xdr_stream *xdr, struct 
> nfs4_delegation *dp,
> +				 struct nfsd_file *nf)
> +{
> +	struct dentry *dentry = nf->nf_file->f_path.dentry;
> +	struct notify_attr4 na = { };
> +	bool ret;
> +	u8 *p = NULL;
> +
> +	if (!(dp->dl_notify_mask & BIT(NOTIFY4_CHANGE_DIR_ATTRS)))
> +		return NULL;

It looks like this if() re-checks dl_notify_mask even though its
sole caller already gated on the identical check.

nfsd4_encode_notify_event() does not repeat its caller's check.
The guard is unreachable from current callers.


> +
> +	/* RFC 8881 s10.4.3: ne_file must be a zero-length string for dir 
> attrs */
> +	ret = nfsd4_setup_notify_entry4(&na.na_changed_entry, xdr,
> +					dentry, dp, nf, "", 0);
> +
> +	/* Don't bother with the event if we're not encoding attrs */
> +	if (ret && na.na_changed_entry.ne_attrs.attr_vals.len) {
> +		p = (u8 *)xdr->p;
> +		if (!xdrgen_encode_notify_attr4(xdr, &na))
> +			p = NULL;
> +	}
> +	return p;
> +}
> +
>  static void svcxdr_init_encode_from_buffer(struct xdr_stream *xdr,
>  				struct xdr_buf *buf, __be32 *p, int bytes)
>  {
> diff --git a/fs/nfsd/xdr4.h b/fs/nfsd/xdr4.h
> index 62ac790428be..805c7122eb93 100644
> --- a/fs/nfsd/xdr4.h
> +++ b/fs/nfsd/xdr4.h
> @@ -973,6 +973,8 @@ __be32 nfsd4_encode_fattr_to_buf(__be32 **p, int 
> words,
>  u8 *nfsd4_encode_notify_event(struct xdr_stream *xdr, struct 
> nfsd_notify_event *nne,
>  			      struct nfs4_delegation *dd, struct nfsd_file *nf,
>  			      u32 *notify_mask);
> +u8 *nfsd4_encode_dir_attr_change(struct xdr_stream *xdr, struct 
> nfs4_delegation *dp,
> +				 struct nfsd_file *nf);
>  extern __be32 nfsd4_setclientid(struct svc_rqst *rqstp,
>  		struct nfsd4_compound_state *, union nfsd4_op_u *u);
>  extern __be32 nfsd4_setclientid_confirm(struct svc_rqst *rqstp,
>
> -- 
> 2.54.0

-- 
Chuck Lever
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.