[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 */
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.