From: Jeff Layton <jlayton@kernel.org>
To: Hyunsol Mun <muumthf@gmail.com>, linux-nfs@vger.kernel.org
Cc: Chuck Lever <cel@kernel.org>, NeilBrown <neil@brown.name>,
Olga Kornievskaia <okorniev@redhat.com>,
Dai Ngo <Dai.Ngo@oracle.com>, Tom Talpey <tom@talpey.com>,
bobtobabz@gmail.com
Subject: Re: [PATCH v2] nfsd: pin source client while using COPY_NOTIFY stateid
Date: Fri, 02 Oct 2026 08:20:16 +0100 [thread overview]
Message-ID: <6ad7a2fd190a94aa3b2592314b79109d21f38436.camel@kernel.org> (raw)
In-Reply-To: <20260930053915.299362-1-muumthf@gmail.com>
On Wed, 2026-09-30 at 14:39 +0900, Hyunsol Mun wrote:
> A COPY_NOTIFY token can resolve to a stateid owned by the source client.
> find_cpntf_state() drops its source-client reference before returning
> that stateid, so concurrent source-client expiry can free sc_client
> while stateid validation or the caller still uses the stateid.
>
> Add a local nfsd4_get_client() helper as the counterpart to
> nfsd4_put_client(). Return a paired client reference from
> nfs4_preprocess_stateid_op() when lookup crosses through COPY_NOTIFY
> state. Keep it until the caller releases the returned stateid; internal
> and error paths release it locally.
>
> An unprivileged NFSv4.2 client with access to the exported file can execute
> the OPEN, COPY_NOTIFY, source-client replacement, and token READ sequence.
> Four hundred natural trials reached the protocol path without reproducing
> the report, so a reliable timing-assistance-free trigger was not
> demonstrated.
>
> For deterministic validation, a test-only kprobe delayed nfsd_permission()
> after COPY_NOTIFY state lookup. The module only widened the race window; it
> did not allocate, free, or modify an NFSD object. Validation used the
> nfsd-testing base recorded below. On the unmodified KASAN kernel, five
> trials registered four delayed calls and reproduced the source-client
> use-after-free in nfs4_put_stid(). With this patch, five trials registered
> five delayed calls and produced no KASAN report or kernel failure. Three
> token READs returned NFS4_OK with 23 bytes; two returned NFS4ERR_EXPIRED as
> client replacement won the race.
>
> The full KASAN kernel built with CONFIG_WERROR without a compiler
> diagnostic. Basic NFSv4.2 and NFSv3 read/write/unmount smoke tests passed.
> A source reproducer, timing-module source, complete logs, and the kernel
> configuration are available privately on request.
>
> The vulnerability research and validation were conducted by members of
> the Tobabz team as part of the Best of the Best 15th program.
>
> Fixes: 624322f1adc5 ("NFSD add COPY_NOTIFY operation")
> Assisted-by: LLM
> Signed-off-by: Hyunsol Mun <muumthf@gmail.com>
> ---
> Changes in v2:
> - Describe current timing-assisted baseline and patched results.
> - Add full-build and NFSv4.2/NFSv3 smoke-test results.
> - Keep nfsd4_get_client() local to nfs4state.c.
>
> fs/nfsd/nfs4proc.c | 31 +++++++++++++++++++++----------
> fs/nfsd/nfs4state.c | 34 +++++++++++++++++++++++++++-------
> fs/nfsd/state.h | 2 +-
> 3 files changed, 49 insertions(+), 18 deletions(-)
>
> diff --git a/fs/nfsd/nfs4proc.c b/fs/nfsd/nfs4proc.c
> index 7df60abfb..d5b5f59f1 100644
> --- a/fs/nfsd/nfs4proc.c
> +++ b/fs/nfsd/nfs4proc.c
> @@ -1144,7 +1144,7 @@ nfsd4_read(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> /* check stateid */
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->current_fh,
> &read->rd_stateid, RD_STATE,
> - &read->rd_nf, NULL);
> + &read->rd_nf, NULL, NULL);
>
> read->rd_rqstp = rqstp;
> read->rd_fhp = &cstate->current_fh;
> @@ -1342,6 +1342,7 @@ nfsd4_setattr(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> .na_dpacl = posix_acl_dup(setattr->sa_dpacl),
> };
> bool save_no_wcc, deleg_attrs;
> + struct nfs4_client *stid_clp = NULL;
> struct nfs4_stid *st = NULL;
> struct inode *inode;
> __be32 status = nfs_ok;
> @@ -1358,7 +1359,7 @@ nfsd4_setattr(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
>
> status = nfs4_preprocess_stateid_op(rqstp, cstate,
> &cstate->current_fh, &setattr->sa_stateid,
> - flags, NULL, &st);
> + flags, NULL, &st, &stid_clp);
> if (status)
> goto out_err;
> }
> @@ -1375,8 +1376,11 @@ nfsd4_setattr(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> }
> }
> }
> - if (st)
> + if (st) {
> nfs4_put_stid(st);
> + if (stid_clp)
> + nfsd4_put_client(stid_clp);
> + }
> if (status)
> goto out_err;
>
> @@ -1439,6 +1443,7 @@ nfsd4_write(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> struct nfsd4_write *write = &u->write;
> stateid_t *stateid = &write->wr_stateid;
> struct nfs4_stid *stid = NULL;
> + struct nfs4_client *stid_clp = NULL;
> struct nfsd_file *nf = NULL;
> __be32 status = nfs_ok;
> unsigned long cnt;
> @@ -1451,13 +1456,16 @@ nfsd4_write(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> trace_nfsd_write_start(rqstp, &cstate->current_fh,
> write->wr_offset, cnt);
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->current_fh,
> - stateid, WR_STATE, &nf, &stid);
> + stateid, WR_STATE, &nf, &stid,
> + &stid_clp);
> if (status)
> return status;
>
> if (stid) {
> nfsd4_file_mark_deleg_written(stid->sc_file);
> nfs4_put_stid(stid);
> + if (stid_clp)
> + nfsd4_put_client(stid_clp);
> }
>
> write->wr_how_written = write->wr_stable_how;
> @@ -1484,12 +1492,12 @@ nfsd4_verify_copy(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> return nfserr_nofilehandle;
>
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->save_fh,
> - src_stateid, RD_STATE, src, NULL);
> + src_stateid, RD_STATE, src, NULL, NULL);
> if (status)
> goto out;
>
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->current_fh,
> - dst_stateid, WR_STATE, dst, NULL);
> + dst_stateid, WR_STATE, dst, NULL, NULL);
> if (status)
> goto out_put_src;
>
> @@ -1954,7 +1962,7 @@ nfsd4_setup_inter_ssc(struct svc_rqst *rqstp,
> /* Verify the destination stateid and set dst struct file*/
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->current_fh,
> ©->cp_dst_stateid,
> - WR_STATE, ©->nf_dst, NULL);
> + WR_STATE, ©->nf_dst, NULL, NULL);
> if (status)
> goto out;
>
> @@ -2498,12 +2506,13 @@ nfsd4_copy_notify(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> struct nfsd4_copy_notify *cn = &u->copy_notify;
> __be32 status;
> struct nfsd_net *nn = net_generic(SVC_NET(rqstp), nfsd_net_id);
> + struct nfs4_client *stid_clp = NULL;
> struct nfs4_stid *stid = NULL;
> struct nfs4_cpntf_state *cps;
>
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->current_fh,
> &cn->cpn_src_stateid, RD_STATE, NULL,
> - &stid);
> + &stid, &stid_clp);
> if (status)
> return status;
> if (!stid)
> @@ -2536,6 +2545,8 @@ nfsd4_copy_notify(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
> nfs4_put_cpntf_state(nn, cps);
> out:
> nfs4_put_stid(stid);
> + if (stid_clp)
> + nfsd4_put_client(stid_clp);
> return status;
> }
>
> @@ -2548,7 +2559,7 @@ nfsd4_fallocate(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
>
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->current_fh,
> &fallocate->falloc_stateid,
> - WR_STATE, &nf, NULL);
> + WR_STATE, &nf, NULL, NULL);
> if (status != nfs_ok)
> return status;
>
> @@ -2612,7 +2623,7 @@ nfsd4_seek(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
>
> status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->current_fh,
> &seek->seek_stateid,
> - RD_STATE, &nf, NULL);
> + RD_STATE, &nf, NULL, NULL);
> if (status)
> return status;
>
> diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
> index bbc16dd22..43906f87e 100644
> --- a/fs/nfsd/nfs4state.c
> +++ b/fs/nfsd/nfs4state.c
> @@ -2805,6 +2805,15 @@ static void __free_client(struct kref *k)
> kmem_cache_free(client_slab, clp);
> }
>
> +/**
> + * nfsd4_get_client - acquire a reference on an nfs4_client
> + * @clp: the client to be acquired
> + */
> +static void nfsd4_get_client(struct nfs4_client *clp)
> +{
> + kref_get(&clp->cl_nfsdfs.cl_ref);
> +}
> +
If you're going to add this helper, then you should convert all of the
other places that call kref_get() on this object to use it, ideally in
a separate patch.
> /**
> * nfsd4_put_client - release a reference on an nfs4_client
> * @clp: the client to be released
> @@ -8504,7 +8513,8 @@ __be32 manage_cpntf_state(struct nfsd_net *nn, stateid_t *st,
> }
>
> static __be32 find_cpntf_state(struct nfsd_net *nn, stateid_t *st,
> - struct nfs4_stid **stid)
> + struct nfs4_stid **stid,
> + struct nfs4_client **stid_clp)
> {
> __be32 status;
> struct nfs4_cpntf_state *cps = NULL;
> @@ -8524,10 +8534,13 @@ static __be32 find_cpntf_state(struct nfsd_net *nn, stateid_t *st,
> *stid = find_stateid_by_type(found, &cps->cp_p_stateid,
> SC_TYPE_DELEG|SC_TYPE_OPEN|SC_TYPE_LOCK,
> 0);
> - if (*stid)
> + if (*stid) {
> + nfsd4_get_client(found);
> + *stid_clp = found;
> status = nfs_ok;
> - else
> + } else {
> status = nfserr_bad_stateid;
> + }
>
> put_client_renew(found);
> out:
> @@ -8551,6 +8564,7 @@ void nfs4_put_cpntf_state(struct nfsd_net *nn, struct nfs4_cpntf_state *cps)
> * @flags: flags describing type of operation to be done
> * @nfp: optional nfsd_file return pointer (may be NULL)
> * @cstid: optional returned nfs4_stid pointer (may be NULL)
> + * @cstid_clp: client reference paired with @cstid (required with @cstid)
> *
> * Given info from the client, look up a nfs4_stid for the operation. On
> * success, it returns a reference to the nfs4_stid and/or the nfsd_file
> @@ -8560,10 +8574,11 @@ __be32
> nfs4_preprocess_stateid_op(struct svc_rqst *rqstp,
> struct nfsd4_compound_state *cstate, struct svc_fh *fhp,
> stateid_t *stateid, int flags, struct nfsd_file **nfp,
> - struct nfs4_stid **cstid)
> + struct nfs4_stid **cstid, struct nfs4_client **cstid_clp)
The bug seems possible (esp given the fault-injected reproducer), but
this function is really devolving into a big mess. It would be very
nice to move the COPY-specific code into a dedicated helper that is
only called from the COPY-related codepaths. Consider reorganizing this
code along those lines.
> {
> struct net *net = SVC_NET(rqstp);
> struct nfsd_net *nn = net_generic(net, nfsd_net_id);
> + struct nfs4_client *stid_clp = NULL;
> struct nfs4_stid *s = NULL;
> __be32 status;
>
> @@ -8579,7 +8594,7 @@ nfs4_preprocess_stateid_op(struct svc_rqst *rqstp,
> SC_TYPE_DELEG|SC_TYPE_OPEN|SC_TYPE_LOCK,
> 0, &s, nn);
> if (status == nfserr_bad_stateid)
> - status = find_cpntf_state(nn, stateid, &s);
> + status = find_cpntf_state(nn, stateid, &s, &stid_clp);
> if (status)
> return status;
> status = nfsd4_stid_check_stateid_generation(stateid, s,
> @@ -8605,11 +8620,16 @@ nfs4_preprocess_stateid_op(struct svc_rqst *rqstp,
> status = nfs4_check_file(rqstp, fhp, s, nfp, flags);
> out:
> if (s) {
> - if (!status && cstid)
> + if (!status && cstid) {
> *cstid = s;
> - else
> + *cstid_clp = stid_clp;
> + stid_clp = NULL;
> + } else {
> nfs4_put_stid(s);
> + }
> }
> + if (stid_clp)
> + nfsd4_put_client(stid_clp);
> return status;
> }
>
> diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
> index 1a0da82cb..7910dabb4 100644
> --- a/fs/nfsd/state.h
> +++ b/fs/nfsd/state.h
> @@ -911,7 +911,7 @@ struct nfsd4_async_copy;
> extern __be32 nfs4_preprocess_stateid_op(struct svc_rqst *rqstp,
> struct nfsd4_compound_state *cstate, struct svc_fh *fhp,
> stateid_t *stateid, int flags, struct nfsd_file **filp,
> - struct nfs4_stid **cstid);
> + struct nfs4_stid **cstid, struct nfs4_client **cstid_clp);
> __be32 nfsd4_lookup_stateid(struct nfsd4_compound_state *cstate,
> stateid_t *stateid, unsigned short typemask,
> unsigned short statusmask,
>
> base-commit: 32eb1a60b456980761cf7a9cee8f907fdc08afb8
--
Jeff Layton <jlayton@kernel.org>
next prev parent reply other threads:[~2026-10-02 7:20 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-30 5:39 [PATCH v2] nfsd: pin source client while using COPY_NOTIFY stateid Hyunsol Mun
2026-10-02 7:20 ` Jeff Layton [this message]
2026-10-02 15:55 ` Chuck Lever
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=6ad7a2fd190a94aa3b2592314b79109d21f38436.camel@kernel.org \
--to=jlayton@kernel.org \
--cc=Dai.Ngo@oracle.com \
--cc=bobtobabz@gmail.com \
--cc=cel@kernel.org \
--cc=linux-nfs@vger.kernel.org \
--cc=muumthf@gmail.com \
--cc=neil@brown.name \
--cc=okorniev@redhat.com \
--cc=tom@talpey.com \
/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