From: Jeff Layton <jlayton@kernel.org>
To: 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>,
Andy Adamson <andros@netapp.com>
Cc: Chris Mason <clm@meta.com>,
linux-nfs@vger.kernel.org, linux-kernel@vger.kernel.org,
Jeff Layton <jlayton@kernel.org>
Subject: [PATCH v2 09/10] nfsd: make the copy offload stateid a first-class nfs4_stid
Date: Thu, 09 Jul 2026 14:47:46 -0400 [thread overview]
Message-ID: <20260709-nfsd-testing-v2-9-0a1ba233bf87@kernel.org> (raw)
In-Reply-To: <20260709-nfsd-testing-v2-0-0a1ba233bf87@kernel.org>
The async COPY offload stateid was a copy_stateid_t in the per-net
nn->s2s_cp_stateids IDR, sharing that table with COPY_NOTIFY stateids even
though every reader (laundromat, manage_cpntf_state()) accepts only
NFS4_COPYNOTIFY_STID. It was inserted there only to mint a unique so_id;
OFFLOAD_CANCEL and OFFLOAD_STATUS find the copy by walking
clp->async_copies.
Building on the nfsd4_async_copy split, promote it to a first-class
nfs4_stid (SC_TYPE_COPY) embedded at the head of nfsd4_async_copy and
allocated from the client's cl_stateids via nfs4_alloc_stid(). This:
- makes the stateid per-client by construction rather than relying on a
guessable cyclic id in a global table;
- reuses the common id allocation, refcounting, and teardown
(nfs4_put_stid() + sc_free), removing the bespoke
nfs4_init_copy_state()/nfs4_free_copy_state(); and
- leaves nn->s2s_cp_stateids exclusively for COPY_NOTIFY stateids.
The async-copy lifetime model is unchanged; nf4_put_copy() now drops the
stid's single reference, which removes it from cl_stateids and frees the
slab.
Per RFC 7862 Section 4.8 a copy offload stateid is valid only for
COPY/OFFLOAD_CANCEL/OFFLOAD_STATUS/CB_OFFLOAD, not FREE_STATEID or
TEST_STATEID, so find_stateid_locked() hides SC_TYPE_COPY and those paths
keep returning bad_stateid as before. Its seqid MUST NOT be zero, so set
si_generation to 1 (nfs4_alloc_stid() leaves it zero).
Follow-ups (not done here): NFS4_COPY_STID, the now-always-COPYNOTIFY
branch in nfs4_init_cp_state(), and the redundant cs_type checks in the
laundromat and manage_cpntf_state() are vestigial.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/nfs4proc.c | 37 +++++++++++++-----------------
fs/nfsd/nfs4state.c | 66 +++++++++++++++++++++++++++++++++++++++++------------
fs/nfsd/state.h | 4 ++--
fs/nfsd/xdr4.h | 2 +-
4 files changed, 70 insertions(+), 39 deletions(-)
diff --git a/fs/nfsd/nfs4proc.c b/fs/nfsd/nfs4proc.c
index 7d44c85335c7..64cc830830e9 100644
--- a/fs/nfsd/nfs4proc.c
+++ b/fs/nfsd/nfs4proc.c
@@ -1521,11 +1521,13 @@ static void nfs4_put_copy(struct nfsd4_async_copy *copy)
{
if (!refcount_dec_and_test(©->refcount))
return;
- /* Drop the task_struct reference taken in nfsd4_copy(). */
- if (copy->copy_task)
- put_task_struct(copy->copy_task);
- kfree(copy->cp_copy.cp_src);
- kfree(copy);
+ /*
+ * Release the copy offload stateid. This drops the stid's sole
+ * reference, which removes it from cl_stateids and frees the
+ * nfsd4_async_copy via nfsd4_free_async_copy_stid(). The task_struct
+ * pin and cp_src are released there.
+ */
+ nfs4_put_stid(©->cp_stid);
}
static void release_copy_files(struct nfsd4_copy *copy);
@@ -2099,7 +2101,6 @@ static void release_copy_files(struct nfsd4_copy *copy)
*/
static void cleanup_async_copy(struct nfsd4_async_copy *copy)
{
- nfs4_free_copy_state(copy);
release_copy_files(©->cp_copy);
nfs4_put_copy(copy);
}
@@ -2258,7 +2259,13 @@ nfsd4_copy(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
if (nfsd4_copy_is_async(copy)) {
struct task_struct *task;
- async_copy = kzalloc_obj(struct nfsd4_async_copy);
+ /*
+ * Allocate the durable async copy. Its copy offload stateid is
+ * a first-class nfs4_stid in cstate->clp->cl_stateids, minted
+ * here and returned to the client; it outlives this COMPOUND and
+ * is freed only when the background copy is torn down.
+ */
+ async_copy = nfs4_alloc_copy_stid(cstate->clp);
if (!async_copy)
goto out_err;
async_copy->cp_copy.cp_nn = nn;
@@ -2273,19 +2280,7 @@ nfsd4_copy(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
if (!async_copy->cp_copy.cp_src)
goto out_dec_async_copy_err;
dup_copy_fields(copy, &async_copy->cp_copy);
- /*
- * Register the copy stateid on the long-lived async_copy
- * rather than on the transient COMPOUND argument buffer
- * (&u->copy). nfs4_init_copy_state() installs a pointer to
- * the copy_stateid_t in nn->s2s_cp_stateids, and that pointer
- * outlives this call (it is removed only when the background
- * copy finishes). Pointing it at &u->copy would leave a stale
- * pointer into reused request memory that the laundromat and
- * OFFLOAD_CANCEL later dereference.
- */
- if (!nfs4_init_copy_state(nn, async_copy))
- goto out_dec_async_copy_err;
- memcpy(&result->cb_stateid, &async_copy->cp_stateid.cs_stid,
+ memcpy(&result->cb_stateid, &async_copy->cp_stid.sc_stateid,
sizeof(result->cb_stateid));
if ((READ_ONCE(copy->nf_dst->nf_file->f_mode) &
FMODE_NOCMTIME) != 0)
@@ -2357,7 +2352,7 @@ find_async_copy_locked(struct nfs4_client *clp, stateid_t *stateid)
lockdep_assert_held(&clp->async_lock);
list_for_each_entry(copy, &clp->async_copies, copies) {
- if (memcmp(©->cp_stateid.cs_stid, stateid, NFS4_STATEID_SIZE))
+ if (memcmp(©->cp_stid.sc_stateid, stateid, NFS4_STATEID_SIZE))
continue;
return copy;
}
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 594cf392f61f..8ac76db31fbb 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -121,6 +121,7 @@ static struct kmem_cache *file_slab;
static struct kmem_cache *stateid_slab;
static struct kmem_cache *deleg_slab;
static struct kmem_cache *odstate_slab;
+static struct kmem_cache *async_copy_slab;
static void free_session(struct nfsd4_session *);
@@ -982,9 +983,41 @@ static int nfs4_init_cp_state(struct nfsd_net *nn, copy_stateid_t *stid,
return 1;
}
-int nfs4_init_copy_state(struct nfsd_net *nn, struct nfsd4_async_copy *copy)
+/*
+ * sc_free callback for a copy offload stateid. Runs from nfs4_put_stid()
+ * once the stid's last reference is dropped and it has been removed from
+ * cl_stateids.
+ */
+static void nfsd4_free_async_copy_stid(struct nfs4_stid *stid)
+{
+ struct nfsd4_async_copy *copy =
+ container_of(stid, struct nfsd4_async_copy, cp_stid);
+
+ if (copy->copy_task)
+ put_task_struct(copy->copy_task);
+ kfree(copy->cp_copy.cp_src);
+ kmem_cache_free(async_copy_slab, copy);
+}
+
+/*
+ * Allocate the durable state for an asynchronous COPY. The copy offload
+ * stateid is a first-class nfs4_stid (SC_TYPE_COPY) living in the issuing
+ * client's cl_stateids IDR, so it is per-client by construction and shares
+ * the common stateid refcounting and teardown. It is never matched by the
+ * generic stateid lookups (find_stateid_locked() skips SC_TYPE_COPY);
+ * OFFLOAD_CANCEL/OFFLOAD_STATUS find it by walking clp->async_copies.
+ */
+struct nfsd4_async_copy *nfs4_alloc_copy_stid(struct nfs4_client *clp)
{
- return nfs4_init_cp_state(nn, ©->cp_stateid, NFS4_COPY_STID, NULL);
+ struct nfs4_stid *stid;
+
+ stid = nfs4_alloc_stid(clp, async_copy_slab, nfsd4_free_async_copy_stid);
+ if (!stid)
+ return NULL;
+ stid->sc_type = SC_TYPE_COPY;
+ /* RFC 7862 Section 4.8: a copy offload stateid's seqid MUST NOT be 0 */
+ stid->sc_stateid.si_generation = 1;
+ return container_of(stid, struct nfsd4_async_copy, cp_stid);
}
struct nfs4_cpntf_state *nfs4_alloc_init_cpntf_state(struct nfsd_net *nn,
@@ -1024,19 +1057,6 @@ struct nfs4_cpntf_state *nfs4_alloc_init_cpntf_state(struct nfsd_net *nn,
return NULL;
}
-void nfs4_free_copy_state(struct nfsd4_async_copy *copy)
-{
- struct nfsd_net *nn;
-
- if (copy->cp_stateid.cs_type != NFS4_COPY_STID)
- return;
- nn = net_generic(copy->cp_copy.cp_clp->net, nfsd_net_id);
- spin_lock(&nn->s2s_cp_lock);
- idr_remove(&nn->s2s_cp_stateids,
- copy->cp_stateid.cs_stid.si_opaque.so_id);
- spin_unlock(&nn->s2s_cp_lock);
-}
-
/*
* Drop the parent stid's reference on a cpntf entry that has already been
* removed from sc_cp_list and the s2s_cp_stateids IDR. If a concurrent holder
@@ -3073,6 +3093,16 @@ find_stateid_locked(struct nfs4_client *cl, stateid_t *t)
ret = idr_find(&cl->cl_stateids, t->si_opaque.so_id);
if (!ret || !ret->sc_type)
return NULL;
+ /*
+ * Copy offload stateids live in cl_stateids for id allocation and
+ * refcounting, but they are not valid targets for the generic
+ * stateid operations (FREE_STATEID, TEST_STATEID, I/O). Per RFC 7862
+ * they are managed only via OFFLOAD_CANCEL/OFFLOAD_STATUS/CB_OFFLOAD,
+ * which locate them through clp->async_copies. Hide them here so those
+ * paths continue to return NFS4ERR_BAD_STATEID.
+ */
+ if (ret->sc_type == SC_TYPE_COPY)
+ return NULL;
return ret;
}
@@ -5408,6 +5438,7 @@ nfsd4_free_slabs(void)
kmem_cache_destroy(stateid_slab);
kmem_cache_destroy(deleg_slab);
kmem_cache_destroy(odstate_slab);
+ kmem_cache_destroy(async_copy_slab);
}
int
@@ -5434,8 +5465,13 @@ nfsd4_init_slabs(void)
odstate_slab = KMEM_CACHE(nfs4_clnt_odstate, 0);
if (odstate_slab == NULL)
goto out_free_deleg_slab;
+ async_copy_slab = KMEM_CACHE(nfsd4_async_copy, 0);
+ if (async_copy_slab == NULL)
+ goto out_free_odstate_slab;
return 0;
+out_free_odstate_slab:
+ kmem_cache_destroy(odstate_slab);
out_free_deleg_slab:
kmem_cache_destroy(deleg_slab);
out_free_stateid_slab:
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index 4526b67c90d4..5dc4a473246e 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -121,6 +121,7 @@ struct nfs4_stid {
#define SC_TYPE_LOCK BIT(1)
#define SC_TYPE_DELEG BIT(2)
#define SC_TYPE_LAYOUT BIT(3)
+#define SC_TYPE_COPY BIT(4)
unsigned short sc_type;
/* nn->deleg_lock protects sc_status for delegation stateids.
@@ -884,8 +885,7 @@ __be32 nfsd4_lookup_stateid(struct nfsd4_compound_state *cstate,
struct nfs4_stid **s, struct nfsd_net *nn);
struct nfs4_stid *nfs4_alloc_stid(struct nfs4_client *cl, struct kmem_cache *slab,
void (*sc_free)(struct nfs4_stid *));
-int nfs4_init_copy_state(struct nfsd_net *nn, struct nfsd4_async_copy *copy);
-void nfs4_free_copy_state(struct nfsd4_async_copy *copy);
+struct nfsd4_async_copy *nfs4_alloc_copy_stid(struct nfs4_client *clp);
struct nfs4_cpntf_state *nfs4_alloc_init_cpntf_state(struct nfsd_net *nn,
struct nfs4_stid *p_stid);
void nfs4_put_stid(struct nfs4_stid *s);
diff --git a/fs/nfsd/xdr4.h b/fs/nfsd/xdr4.h
index f1c817e2f93c..cbd40bf3ee2a 100644
--- a/fs/nfsd/xdr4.h
+++ b/fs/nfsd/xdr4.h
@@ -784,13 +784,13 @@ struct nfsd4_copy {
* never points into the request buffer.
*/
struct nfsd4_async_copy {
+ struct nfs4_stid cp_stid; /* SC_TYPE_COPY, in cl_stateids */
struct nfsd4_copy cp_copy; /* operation params + result */
struct list_head copies; /* nfs4_client.async_copies */
struct task_struct *copy_task;
refcount_t refcount;
unsigned int cp_ttl;
- copy_stateid_t cp_stateid; /* s2s_cp_stateids IDR entry */
struct nfsd4_cb_offload cp_cb_offload;
};
--
2.55.0
next prev parent reply other threads:[~2026-07-09 18:48 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-09 18:47 [PATCH v2 00/10] nfsd: copy offload fixes Jeff Layton
2026-07-09 18:47 ` [PATCH v2 01/10] nfsd: fix cpntf publish race in nfs4_init_cp_state Jeff Layton
2026-07-09 18:47 ` [PATCH v2 02/10] nfsd: fix UAF in async copy cancel and shutdown Jeff Layton
2026-07-09 18:47 ` [PATCH v2 03/10] nfsd: fix stale s2s_cp_stateids IDR entry for async COPY Jeff Layton
2026-07-09 21:18 ` Chuck Lever
2026-07-09 18:47 ` [PATCH v2 04/10] nfsd: initialize copy-notify stateid before publishing it Jeff Layton
2026-07-09 18:47 ` [PATCH v2 05/10] nfsd: check client ownership when cancelling a copy-notify stateid Jeff Layton
2026-07-09 18:47 ` [PATCH v2 06/10] nfsd: revoke copy-notify stateids before dropping their reference Jeff Layton
2026-07-09 18:47 ` [PATCH v2 07/10] nfsd: return NFS4ERR_NOTSUPP for unsupported netloc4 types Jeff Layton
2026-07-09 18:47 ` [PATCH v2 08/10] nfsd: split nfsd4_copy into transient and durable async copy objects Jeff Layton
2026-07-09 18:47 ` Jeff Layton [this message]
2026-07-09 18:47 ` [PATCH v2 10/10] nfsd: drop dead COPY-vs-COPYNOTIFY type handling from s2s stateid IDR Jeff Layton
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=20260709-nfsd-testing-v2-9-0a1ba233bf87@kernel.org \
--to=jlayton@kernel.org \
--cc=Dai.Ngo@oracle.com \
--cc=andros@netapp.com \
--cc=cel@kernel.org \
--cc=clm@meta.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-nfs@vger.kernel.org \
--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