[PATCH] nfsd: bound GSS session fore-channel slots to the GSS replay window
Vjaceslavs Klimovs <[email protected]>
| Newsgroups | gmane.linux.nfs |
|---|---|
| Message-ID | <CAC_j7i1WXvMS6Q66w7-EGxZYFrdJ9R_DzA26xweP5=7z05KWzg@mail.gmail.com> |
An RPCSEC_GSS context authenticates each RPC with a per-context
sequence number, and the server enforces a fixed 128-entry
replay/reorder window per context (GSS_SEQ_WIN,
net/sunrpc/auth_gss/svcauth_gss.c). A client mounting with nconnect
> 1 round-robins a single context's sequence numbers across several
TCP connections that drain at different rates, and every retransmit
consumes a fresh sequence number. The spread between the oldest
in-flight and the newest issued sequence number can therefore exceed
the number of in-flight RPCs; once it passes the window the server
drops the lagging request, the hard mount retransmits, and a krb5p
streaming write collapses into a retransmit storm (a stall to ~0 in
the field; an EIO under the krb5-nfs-perf reproducer).
The NFSv4.1 fore-channel slot count caps how many RPCs the client
keeps in flight, so it is the effective ceiling on the
sequence-number spread. The default Linux client negotiates 64
fore-channel slots (NFS4_DEF_SLOT_TABLE_SIZE), and commit
60aa6564317d ("nfsd: allocate new session-based DRC slots on demand.")
lets the server grow the table far beyond that -- both exceed the
128-entry window once nconnect reordering and retransmits are
accounted for.
Bound a GSS-authenticated session's fore-channel slots to a quarter
of the window, at both the initial CREATE_SESSION grant and on-demand
growth, so the sequence-number spread stays inside the replay window.
A quarter (32) keeps a 4x margin against overrun yet still allows
enough in-flight RPCs to keep the per-byte krb5p crypto (AES plus
HMAC) saturated across many server CPUs, so it does not cap
streaming-write throughput on large machines. sec=sys has no
per-context sequence window and keeps the full
NFSD_MAX_SLOTS_PER_SESSION table.
Signed-off-by: Vjaceslavs Klimovs <[email protected]>
---
Symptom / impact
A production krb5p NFSv4.2 mount (vers=4.2, rsize=wsize=1M, proto=tcp,
nconnect=8, sec=krb5p, enctype aes256-cts-hmac-sha384-192) that used to
sustain ~1 GB/s on a 10 GbE link began stalling to ~0 after a 6.12 -> 6.18
upgrade. sec=sys over the same path is unaffected, and nconnect=1 does not
trigger it.
Bisect
Onset is 60aa6564317d ("nfsd: allocate new session-based DRC slots on
demand.", v6.14). Its parent caps the session slot table; the commit lets
it grow on demand, so in-flight RPCs (already 64 from the client default)
climb past the 128-entry GSS window and storm.
Reproduction / validation
Two-VM QEMU harness (one VM a KDC + nfsd server, the other a krb5p client;
nconnect=8; sequential write to a tmpfs-backed export):
unpatched 6.18.x : retransmit storm (hundreds of retransmits) -> EIO
patched : clean, retrans 0
Confirmed at both 2 and 16 vCPU per VM.
Why the cap is 32 (and why a server-side cap at all)
The krb5p throughput ceiling here is GSS crypto, not the slot count: under
load ~7-9 cores sit ~99% in-kernel on both ends (AES-NI plus HMAC-SHA384
over every byte), and throughput scales with cores (~0.6 GB/s at 2 vCPU ->
~2.0 GB/s at 16). The slot count is a *secondary* lever, because it caps
how many RPCs are in flight to feed that parallel crypto. Measured at
16 vCPU, varying only the cap:
16 slots -> ~1.7 GB/s 32 -> ~2.0 GB/s 40 -> ~2.15 GB/s sys -> ~2.3
40 starts to graze the window again; 32 was the largest value that stayed
clean (retrans 0) across the sweep, so it keeps a 4x window margin without
throttling streaming writes on many-core servers. (16 only looked "free"
at a 2-vCPU test point, where cores -- not slots -- were the bottleneck.)
Possible client-side follow-ups (not addressed by this patch)
This is a server-side defensive bound. Two client behaviors look worth a
separate look:
- the default of 64 GSS fore-channel slots already over-commits a single
128-entry context window once spread across nconnect links; and
- the v6.16 RFC2203 seqno cache (08d6ee6d8a10 "sunrpc: implement rfc2203
rpcsec_gss seqnum cache", plus follow-ups) -- the intended mitigation
for this storm -- in our testing converts a recoverable window graze
into a hard EIO at the 64-slot floor rather than recovering. Cc Nikhil
Jha, who wrote it for the same workload.
fs/nfsd/nfs4state.c | 4 ++++
fs/nfsd/state.h | 8 ++++++++
2 files changed, 12 insertions(+)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index c5dba49c9035..f077f1b8a780 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -3998,6 +3998,9 @@ nfsd4_create_session(struct svc_rqst *rqstp,
status = check_forechannel_attrs(&cr_ses->fore_channel, nn);
if (status)
return status;
+ if (rqstp->rq_authop->flavour == RPC_AUTH_GSS &&
+ cr_ses->fore_channel.maxreqs > NFSD_GSS_MAX_SLOTS_PER_SESSION)
+ cr_ses->fore_channel.maxreqs = NFSD_GSS_MAX_SLOTS_PER_SESSION;
status = check_backchannel_attrs(&cr_ses->back_channel);
if (status)
goto out_err;
@@ -4511,6 +4514,7 @@ nfsd4_sequence(struct svc_rqst *rqstp, struct
nfsd4_compound_state *cstate,
*/
if (seq->slotid == session->se_fchannel.maxreqs - 1 &&
session->se_target_maxslots >= session->se_fchannel.maxreqs &&
+ rqstp->rq_authop->flavour != RPC_AUTH_GSS &&
session->se_fchannel.maxreqs < NFSD_MAX_SLOTS_PER_SESSION) {
int s = session->se_fchannel.maxreqs;
int cnt = DIV_ROUND_UP(s, 5);
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index aaf513ed9104..4bec489ed26b 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -278,6 +278,14 @@ static inline struct nfs4_delegation
*delegstateid(struct nfs4_stid *s)
* A large number can be needed to get good throughput on high-latency servers.
*/
#define NFSD_MAX_SLOTS_PER_SESSION 2048
+/*
+ * Bound GSS sessions to a quarter of the 128-entry RPCSEC_GSS replay window
+ * (GSS_SEQ_WIN): nconnect spreads one context seqno space across links and
+ * each retransmit burns another, so cap both the initial grant and growth.
+ * 32 keeps a 4x margin against window overrun while still allowing enough
+ * in-flight RPCs to saturate the per-byte GSS crypto across many CPUs.
+ */
+#define NFSD_GSS_MAX_SLOTS_PER_SESSION 32
/* Maximum session per slot cache size */
#define NFSD_SLOT_CACHE_SIZE 2048
/* Maximum number of NFSD_SLOT_CACHE_SIZE slots per session */