* [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check
@ 2026-11-01 15:51 Chuck Lever
2026-11-01 15:51 ` [PATCH v6 1/3] NFSD: Fix PUTFH wrongsec check when SAVEFH follows Chuck Lever
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Chuck Lever @ 2026-11-01 15:51 UTC (permalink / raw)
To: VeroFess, Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey
Cc: linux-nfs
VeroFess reported that an AUTH_SYS NFSv4.0 client can read or write
a file on a sec=krb5p export with another client's OPEN stateid by
placing SAVEFH between PUTFH and the I/O operation. The v4 posting
carried that fix together with changes to COPY, LOOKUP, READDIR, and
SECINFO in one patch, which would not apply to the stable kernels
that need the fix. This series splits that patch. The
need_wrongsec_check() change stands alone (patch 1) and applies to
every LTS kernel, and the remaining patches carry only the Fixes:
tags that match their code. VeroFess developed the original fix with
the assistance of an LLM, and an LLM assisted with the split and the
commit messages in this series.
VeroFess confirmed on a kernel with only the need_wrongsec_check()
change that PUTFH; SAVEFH; READ and PUTFH; SAVEFH; WRITE return
NFS4ERR_WRONGSEC, and on a kernel with that change plus the COPY
source check that PUTFH; SAVEFH; PUTFH; COPY returns NFS4ERR_ACCESS.
The series as posted has not been run against the reproducer yet.
VeroFess has offered to do that.
After this series, NFSD still relies on per-operation
check_nfsd_access() calls to enforce a PUTFH check that was deferred.
A filehandle could instead carry a check-pending state that the
COMPOUND loop enforces before any operation consumes the filehandle.
Carrying such a state is later work.
Based on nfsd-testing.
---
Changes in v6:
- Verify the current filehandle in SECINFO_NO_NAME(CURRENT_FH) (sashiko).
- Note that PUTFH still returns WRONGSEC for a GSS or xprtsec= mismatch.
- Link to v5: https://patch.msgid.link/20261030-verofess-v5-0-242eea2991f3@kernel.org
Changes in v5:
- Split the single patch into three, each with its own Fixes: tags.
- Drop the nfs4_check_file() and COPY_NOTIFY export checks.
- Check the entered export in nfsd_cross_mnt() and nfsd_lookup_parent().
- Replace the for_secinfo flag with NFSD_MAY_SECINFO.
- Report a SECINFO transport policy mismatch as NFS4ERR_ACCESS.
- Keep the PUTFH export check for NFSv4.0 PUTFH; SECINFO.
- Link to v4: https://lore.kernel.org/r/20261029063917.138184-1-coderzeng@gmail.com
Changes in v4:
- Skip only the flavor check for SECINFO; xprtsec= stays enforced.
- Re-check xprtsec= when a SECINFO lookup selects another export.
- Link to v3: https://lore.kernel.org/r/20261029051430.96326-1-coderzeng@gmail.com
Changes in v3:
- Use nfsd_lookup_dentry() for SECINFO_NO_NAME(PARENT).
- Let SECINFO and SECINFO_NO_NAME(PARENT) query a strict starting export.
- Skip the source export's policy check on a SECINFO mount crossing.
- Link to v2: https://lore.kernel.org/r/20261029013648.12015-1-coderzeng@gmail.com
Changes in v2:
- Check cached stateid I/O and COPY_NOTIFY against the current export.
- Check the selected export after LOOKUP and READDIR mount crossing.
- Map READDIR's NFS4ERR_WRONGSEC to NFS4ERR_ACCESS without rdattr_error.
- Add the missing Fixes: tags and regenerate the whitespace-damaged diff.
- v1 was sent off-list.
---
VeroFess (3):
NFSD: Fix PUTFH wrongsec check when SAVEFH follows
NFSD: Check export security on the COPY and CLONE source
NFSD: Enforce export security on LOOKUP, READDIR, and SECINFO
fs/nfsd/export.c | 6 ++--
fs/nfsd/nfs3proc.c | 2 +-
fs/nfsd/nfs4proc.c | 84 ++++++++++++++++++++++++++++++++++++++---------------
fs/nfsd/nfs4state.c | 2 +-
fs/nfsd/nfs4xdr.c | 8 +++--
fs/nfsd/nfsfh.c | 12 ++++++--
fs/nfsd/nfsproc.c | 2 +-
fs/nfsd/trace.h | 3 +-
fs/nfsd/vfs.c | 72 +++++++++++++++++++++++++--------------------
fs/nfsd/vfs.h | 43 +++++++++++++++++++++++----
10 files changed, 162 insertions(+), 72 deletions(-)
---
base-commit: d9130c9f6535392d423351ffb5521e7689cae6b6
change-id: 20261030-verofess-f0281be339bc
Best regards,
--
Chuck Lever <cel@kernel.org>
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v6 1/3] NFSD: Fix PUTFH wrongsec check when SAVEFH follows
2026-11-01 15:51 [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check Chuck Lever
@ 2026-11-01 15:51 ` Chuck Lever
2026-11-01 15:51 ` [PATCH v6 2/3] NFSD: Check export security on the COPY and CLONE source Chuck Lever
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Chuck Lever @ 2026-11-01 15:51 UTC (permalink / raw)
To: VeroFess, Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey
Cc: linux-nfs
From: VeroFess <coderzeng@gmail.com>
An AUTH_SYS NFSv4.0 client can read or write a file on a sec=krb5p
export using another client's valid OPEN stateid by inserting
SAVEFH between PUTFH and READ or WRITE.
PUTFH resolves the handle with NFSD_MAY_BYPASS_GSS and defers the
security flavor check to need_wrongsec_check(), which looks only at
the next operation. SAVEFH carries OP_HANDLES_WRONGSEC even though
it only saves the handle, so PUTFH skips NFS4ERR_WRONGSEC. The READ
or WRITE that follows then reuses a cached nfsd_file.
With these export options:
rw,sync,no_subtree_check,fsid=0,sec=krb5p,secure,root_squash
a Kerberos privacy NFSv4.0 client held an OPEN stateid. With the
same stateid, PUTFH; READ over AUTH_SYS was rejected, but PUTFH;
SAVEFH; READ and PUTFH; SAVEFH; WRITE succeeded, and the Kerberos
client subsequently read the changed contents.
SAVEFH leaves CURRENT_FH in place. When deciding whether PUTFH must
report NFS4ERR_WRONGSEC, look past SAVEFH to the first operation
that uses or replaces CURRENT_FH. If another PUTFH replaces it,
leave the earlier PUTFH unchecked, as RFC 8881 Section 2.6.3.1.1
requires. Drop OP_HANDLES_WRONGSEC from SAVEFH, which performs no
check of its own and is never consulted once it is skipped.
Fixes: 68d93184352f ("nfsd4: fix wrongsec handling for PUTFH + op cases")
Signed-off-by: VeroFess <coderzeng@gmail.com>
Tested-by: VeroFess <coderzeng@gmail.com>
Signed-off-by: Chuck Lever <cel@kernel.org>
---
fs/nfsd/nfs4proc.c | 35 +++++++++++++++++++++--------------
1 file changed, 21 insertions(+), 14 deletions(-)
diff --git a/fs/nfsd/nfs4proc.c b/fs/nfsd/nfs4proc.c
index d0290fce9187..50f38d6e147b 100644
--- a/fs/nfsd/nfs4proc.c
+++ b/fs/nfsd/nfs4proc.c
@@ -3237,32 +3237,39 @@ static bool need_wrongsec_check(struct svc_rqst *rqstp)
struct nfsd4_compoundres *resp = rqstp->rq_resp;
struct nfsd4_compoundargs *argp = rqstp->rq_argp;
struct nfsd4_op *this = &argp->ops[resp->opcnt - 1];
- struct nfsd4_op *next = &argp->ops[resp->opcnt];
+ struct nfsd4_op *next = NULL;
const struct nfsd4_operation *thisd = OPDESC(this);
const struct nfsd4_operation *nextd;
+ unsigned int i;
- /*
- * Most ops check wronsec on our own; only the putfh-like ops
- * have special rules.
- */
+ /* Only put-filehandle operations need a deferred WRONGSEC check. */
if (!(thisd->op_flags & OP_IS_PUTFH_LIKE))
return false;
+
/*
- * rfc 5661 2.6.3.1.1.6: don't bother erroring out a
- * put-filehandle operation if we're not going to use the
- * result:
+ * RFC 8881 Section 2.6.3.1.1.1: SAVEFH does not count as the
+ * next operation.
*/
- if (argp->opcnt == resp->opcnt)
+ for (i = resp->opcnt; i < argp->opcnt; i++) {
+ next = &argp->ops[i];
+ if (next->opnum != OP_SAVEFH)
+ break;
+ }
+
+ /* RFC 8881 Section 2.6.3.1.1.6: nothing uses the filehandle. */
+ if (i == argp->opcnt)
return false;
+
if (next->opnum == OP_ILLEGAL)
return false;
nextd = OPDESC(next);
/*
- * Rest of 2.6.3.1.1: certain operations will return WRONGSEC
- * errors themselves as necessary; others should check for them
- * now:
+ * RFC 8881 Section 2.6.3.1.1.2: a later put-filehandle operation
+ * replaces this one. Other marked operations check WRONGSEC
+ * themselves.
*/
- return !(nextd->op_flags & OP_HANDLES_WRONGSEC);
+ return !(nextd->op_flags &
+ (OP_HANDLES_WRONGSEC | OP_IS_PUTFH_LIKE));
}
#ifdef CONFIG_NFSD_V4_2_INTER_SSC
@@ -4047,7 +4054,7 @@ static const struct nfsd4_operation nfsd4_ops[] = {
},
[OP_SAVEFH] = {
.op_func = nfsd4_savefh,
- .op_flags = OP_HANDLES_WRONGSEC | OP_MODIFIES_SOMETHING,
+ .op_flags = OP_MODIFIES_SOMETHING,
.op_name = "OP_SAVEFH",
.op_rsize_bop = nfsd4_only_status_rsize,
},
--
2.56.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v6 2/3] NFSD: Check export security on the COPY and CLONE source
2026-11-01 15:51 [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check Chuck Lever
2026-11-01 15:51 ` [PATCH v6 1/3] NFSD: Fix PUTFH wrongsec check when SAVEFH follows Chuck Lever
@ 2026-11-01 15:51 ` Chuck Lever
2026-11-01 15:51 ` [PATCH v6 3/3] NFSD: Enforce export security on LOOKUP, READDIR, and SECINFO Chuck Lever
2026-11-02 2:19 ` [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check VeroFess
3 siblings, 0 replies; 5+ messages in thread
From: Chuck Lever @ 2026-11-01 15:51 UTC (permalink / raw)
To: VeroFess, Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey
Cc: linux-nfs
From: VeroFess <coderzeng@gmail.com>
RFC 8881 Section 2.6.3.1.1 forbids PUTFH from returning
NFS4ERR_WRONGSEC when another PUTFH follows, so in PUTFH; SAVEFH;
PUTFH; COPY the source handle reaches nfsd4_verify_copy() with its
export's security flavor unchecked. The source stateid then finds a
cached nfsd_file and the copy proceeds.
On a kernel with only the need_wrongsec_check() fix, an AUTH_SYS
READ of a sec=krb5p source returned NFS4ERR_WRONGSEC, but PUTFH;
SAVEFH; PUTFH; COPY of the same file completed and wrote the source
bytes to the destination.
Check the saved export with check_nfsd_access() before using the
source stateid. Map NFS4ERR_WRONGSEC to NFS4ERR_ACCESS, since
neither COPY nor CLONE permits NFS4ERR_WRONGSEC.
Fixes: 29ae7f9dc21a ("NFSD: Implement the COPY call")
Signed-off-by: VeroFess <coderzeng@gmail.com>
Tested-by: VeroFess <coderzeng@gmail.com>
Signed-off-by: Chuck Lever <cel@kernel.org>
---
fs/nfsd/nfs4proc.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/fs/nfsd/nfs4proc.c b/fs/nfsd/nfs4proc.c
index 50f38d6e147b..b6dcb889b5eb 100644
--- a/fs/nfsd/nfs4proc.c
+++ b/fs/nfsd/nfs4proc.c
@@ -1491,6 +1491,11 @@ nfsd4_verify_copy(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
if (!cstate->save_fh.fh_dentry)
return nfserr_nofilehandle;
+ /* A cached open stateid does not authorize the saved source export. */
+ status = check_nfsd_access(cstate->save_fh.fh_export, rqstp);
+ if (status)
+ return status == nfserr_wrongsec ? nfserr_acces : status;
+
status = nfs4_preprocess_stateid_op(rqstp, cstate, &cstate->save_fh,
src_stateid, RD_STATE, src, NULL);
if (status)
--
2.56.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v6 3/3] NFSD: Enforce export security on LOOKUP, READDIR, and SECINFO
2026-11-01 15:51 [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check Chuck Lever
2026-11-01 15:51 ` [PATCH v6 1/3] NFSD: Fix PUTFH wrongsec check when SAVEFH follows Chuck Lever
2026-11-01 15:51 ` [PATCH v6 2/3] NFSD: Check export security on the COPY and CLONE source Chuck Lever
@ 2026-11-01 15:51 ` Chuck Lever
2026-11-02 2:19 ` [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check VeroFess
3 siblings, 0 replies; 5+ messages in thread
From: Chuck Lever @ 2026-11-01 15:51 UTC (permalink / raw)
To: VeroFess, Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey
Cc: linux-nfs
From: VeroFess <coderzeng@gmail.com>
A bind-mounted regular file can have a stricter export than its
parent. nfsd_cross_mnt() checks the request's flavor against the
export being left, not the one being entered, so LOOKUP, READDIR, and
the NFSv2 and NFSv3 CREATE paths all hand out filehandles on the child
export without consulting its sec= list. Stepping up with ".." has the
same gap: nfsd_lookup_parent() switches to the parent export without any
check, so LOOKUPP from the root of a sec=sys export returns a filehandle
on a sec=krb5p parent. Every caller has already passed fh_verify()
on the export being left, so check only the export being entered, in
nfsd_cross_mnt() and in nfsd_lookup_parent().
READDIR crosses into child exports to fetch entry attributes, so the
new check also fires there. When the client requests rdattr_error,
report NFS4ERR_WRONGSEC in that attribute, as RFC 8881 Section 2.6.3.1
directs. Otherwise the whole READDIR fails, as Section 18.23.3 requires;
NFS4ERR_WRONGSEC is not a READDIR status, so report NFS4ERR_ACCESS. The
Linux, FreeBSD, illumos, and Windows clients all request rdattr_error on
every READDIR.
SECINFO and SECINFO_NO_NAME report export security policies. RFC
8881 Section 2.6.3.1.1.5 forbids NFS4ERR_WRONGSEC from either
operation, which allows a client using any flavor to learn whether
a name exists in a directory on a sec=krb5p export. The starting
directory's fh_verify(..., NFSD_MAY_EXEC) call rejects SECINFO and
SECINFO_NO_NAME(PARENT) when that export does not allow the request's
flavor, and a mount crossing rejects it again on the crossed-into
export. SECINFO_NO_NAME(CURRENT_FH) never calls fh_verify() at all, so
after PUTROOTFH it reports the pseudo-root's flavors without checking
that export's transport policy or secure port requirement.
Add NFSD_MAY_SECINFO, which fh_verify() and the lookup path both
read: it skips the starting export's security flavor check and
the flavor check on a mount crossing or parent step-up. Verify the
current filehandle with it in SECINFO_NO_NAME(CURRENT_FH) as well.
NFSv4.0 is different: RFC 7530 permits NFS4ERR_WRONGSEC from PUTFH and
from neither SECINFO nor the operation after it, and a v4.0 SECINFO
leaves the current filehandle in place. A v4.0 put-filehandle operation
followed by SECINFO therefore keeps its own export check, so the error
is reported where RFC 7530 allows it. The Linux and illumos clients
send SECINFO only with a parent directory they already hold under the
same flavor, and FreeBSD does not send SECINFO at all, so the check does
not fire for any of them. The export's transport policy (xprtsec=) is a
transport-layer requirement, not part of the flavor negotiation SECINFO
performs, so SECINFO still enforces it on the starting export and on any
export a crossing enters. When SECINFO or SECINFO_NO_NAME itself detects
the transport policy mismatch, report NFS4ERR_ACCESS, the error RFC 8881
Section 18.29.3 specifies for a SECINFO access failure. The directory
type, secure-port, and execute-permission checks still apply.
The put-filehandle operation that precedes SECINFO is unchanged.
Its fh_verify(..., NFSD_MAY_BYPASS_GSS) call waives the flavor check
only for AUTH_NULL and AUTH_SYS, so a krb5 or krb5i request against a
sec=krb5p export still gets NFS4ERR_WRONGSEC from PUTFH, and so does a
plaintext request against an xprtsec=tls export. Section 2.6.3.1.1.5
forbids both for NFSv4.1 and later.
Fixes: f0d6ad5a2f67 ("nfsd: move check_nfsd_access() call into nfsd_cross_mnt()")
Fixes: 29a78a3ed7fc ("nfsd4: make fh_verify responsibility of nfsd_lookup_dentry caller")
Fixes: 04f4ad16b231 ("nfsd4: implement secinfo_no_name")
Signed-off-by: VeroFess <coderzeng@gmail.com>
Tested-by: VeroFess <coderzeng@gmail.com>
Signed-off-by: Chuck Lever <cel@kernel.org>
---
fs/nfsd/export.c | 6 +++--
fs/nfsd/nfs3proc.c | 2 +-
fs/nfsd/nfs4proc.c | 44 ++++++++++++++++++++++++--------
fs/nfsd/nfs4state.c | 2 +-
fs/nfsd/nfs4xdr.c | 8 ++++--
fs/nfsd/nfsfh.c | 12 ++++++---
fs/nfsd/nfsproc.c | 2 +-
fs/nfsd/trace.h | 3 ++-
fs/nfsd/vfs.c | 72 ++++++++++++++++++++++++++++++-----------------------
fs/nfsd/vfs.h | 43 +++++++++++++++++++++++++++-----
10 files changed, 136 insertions(+), 58 deletions(-)
diff --git a/fs/nfsd/export.c b/fs/nfsd/export.c
index 397767d756c6..0b4c50d82364 100644
--- a/fs/nfsd/export.c
+++ b/fs/nfsd/export.c
@@ -1976,8 +1976,10 @@ static struct svc_export *exp_find(struct cache_detail *cd,
*
* Helper function for check_nfsd_access(). Note that callers should be
* using check_nfsd_access() instead of calling this function directly. The
- * one exception is __fh_verify() since it has logic that may result in one
- * or both of the helpers being skipped.
+ * exceptions are __fh_verify(), which has logic that may result in one
+ * or both of the helpers being skipped, and the mount-crossing paths in
+ * nfsd_cross_mnt() and nfsd_lookup_parent(), which enforce only the
+ * transport policy for SECINFO.
*
* Return values:
* %nfs_ok if access is granted, or
diff --git a/fs/nfsd/nfs3proc.c b/fs/nfsd/nfs3proc.c
index 11e05b2e5e84..1025cec447a9 100644
--- a/fs/nfsd/nfs3proc.c
+++ b/fs/nfsd/nfs3proc.c
@@ -961,7 +961,7 @@ nfsd3_create_file(struct svc_rqst *rqstp, struct svc_fh *dirfhp,
*/
if (d_is_reg(child) &&
unlikely(nfsd_mountpoint(child, exp))) {
- status = nfsd_cross_mnt(rqstp, &child, &exp);
+ status = nfsd_cross_mnt(rqstp, &child, &exp, 0);
if (status != nfs_ok) {
exp_put(exp);
goto out;
diff --git a/fs/nfsd/nfs4proc.c b/fs/nfsd/nfs4proc.c
index b6dcb889b5eb..b2bfa31264c8 100644
--- a/fs/nfsd/nfs4proc.c
+++ b/fs/nfsd/nfs4proc.c
@@ -342,7 +342,7 @@ nfsd4_create_file(struct svc_rqst *rqstp, struct svc_fh *fhp,
unlikely(nfsd_mountpoint(child, fhp->fh_export))) {
struct svc_export *exp = exp_get(fhp->fh_export);
- status = nfsd_cross_mnt(rqstp, &child, &exp);
+ status = nfsd_cross_mnt(rqstp, &child, &exp, 0);
if (status == nfs_ok)
status = fh_compose(resfhp, exp,
child, fhp);
@@ -1079,7 +1079,17 @@ nfsd4_link(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
return status;
}
-static __be32 nfsd4_do_lookupp(struct svc_rqst *rqstp, struct svc_fh *fh)
+/*
+ * RFC 8881 Section 2.6.3.1.1.5 forbids NFS4ERR_WRONGSEC from SECINFO.
+ * Section 18.29.3 has SECINFO report a denied lookup as NFS4ERR_ACCESS.
+ */
+static __be32 nfsd4_secinfo_status(__be32 status)
+{
+ return status == nfserr_wrongsec ? nfserr_acces : status;
+}
+
+static __be32 nfsd4_do_lookupp(struct svc_rqst *rqstp, struct svc_fh *fh,
+ int access)
{
struct svc_fh tmp_fh;
__be32 ret;
@@ -1093,14 +1103,14 @@ static __be32 nfsd4_do_lookupp(struct svc_rqst *rqstp, struct svc_fh *fh)
return nfserr_noent;
}
fh_put(&tmp_fh);
- return nfsd_lookup(rqstp, fh, "..", 2, fh);
+ return __nfsd_lookup(rqstp, fh, "..", 2, fh, access);
}
static __be32
nfsd4_lookupp(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
union nfsd4_op_u *u)
{
- return nfsd4_do_lookupp(rqstp, &cstate->current_fh);
+ return nfsd4_do_lookupp(rqstp, &cstate->current_fh, NFSD_MAY_EXEC);
}
static __be32
@@ -1252,14 +1262,16 @@ nfsd4_secinfo(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate,
struct dentry *dentry;
__be32 err;
- err = fh_verify(rqstp, &cstate->current_fh, S_IFDIR, NFSD_MAY_EXEC);
+ err = fh_verify(rqstp, &cstate->current_fh, S_IFDIR,
+ NFSD_MAY_EXEC | NFSD_MAY_SECINFO);
if (err)
- return err;
+ return nfsd4_secinfo_status(err);
err = nfsd_lookup_dentry(rqstp, &cstate->current_fh,
secinfo->si_name, secinfo->si_namelen,
- &exp, &dentry);
+ &exp, &dentry,
+ NFSD_MAY_EXEC | NFSD_MAY_SECINFO);
if (err)
- return err;
+ return nfsd4_secinfo_status(err);
if (d_really_is_negative(dentry)) {
exp_put(exp);
err = nfserr_noent;
@@ -1280,11 +1292,16 @@ nfsd4_secinfo_no_name(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstat
switch (u->secinfo_no_name.sin_style) {
case NFS4_SECINFO_STYLE4_CURRENT_FH:
+ err = fh_verify(rqstp, &cstate->current_fh, 0,
+ NFSD_MAY_SECINFO);
+ if (err)
+ return nfsd4_secinfo_status(err);
break;
case NFS4_SECINFO_STYLE4_PARENT:
- err = nfsd4_do_lookupp(rqstp, &cstate->current_fh);
+ err = nfsd4_do_lookupp(rqstp, &cstate->current_fh,
+ NFSD_MAY_EXEC | NFSD_MAY_SECINFO);
if (err)
- return err;
+ return nfsd4_secinfo_status(err);
break;
default:
return nfserr_inval;
@@ -3267,6 +3284,13 @@ static bool need_wrongsec_check(struct svc_rqst *rqstp)
if (next->opnum == OP_ILLEGAL)
return false;
+ /*
+ * RFC 7530 permits NFS4ERR_WRONGSEC from PUTFH but not from
+ * SECINFO, and NFSv4.0 SECINFO leaves the current filehandle in
+ * place for the next operation.
+ */
+ if (resp->cstate.minorversion == 0 && next->opnum == OP_SECINFO)
+ return true;
nextd = OPDESC(next);
/*
* RFC 8881 Section 2.6.3.1.1.2: a later put-filehandle operation
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index cafdf80f32de..97b4fa30362a 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -7126,7 +7126,7 @@ nfsd4_verify_deleg_dentry(struct nfsd4_open *open, struct nfs4_file *fp,
err = nfsd_lookup_dentry(open->op_rqstp, parent,
open->op_fname, open->op_fnamelen,
- &exp, &child);
+ &exp, &child, 0);
if (err)
return -ESTALE;
diff --git a/fs/nfsd/nfs4xdr.c b/fs/nfsd/nfs4xdr.c
index d4a88bef8444..111bf2b8ebd1 100644
--- a/fs/nfsd/nfs4xdr.c
+++ b/fs/nfsd/nfs4xdr.c
@@ -4676,7 +4676,7 @@ nfsd4_encode_entry4_fattr(struct nfsd4_readdir *cd, const char *name,
* Different "."/".." handling? Something else?
* At least, add a comment here to explain....
*/
- nfserr = nfsd_cross_mnt(cd->rd_rqstp, &dentry, &exp);
+ nfserr = nfsd_cross_mnt(cd->rd_rqstp, &dentry, &exp, 0);
if (nfserr)
goto out_put;
crossed = true;
@@ -4775,8 +4775,12 @@ nfsd4_encode_entry4(void *ccdv, const char *name, int namlen,
* then in accordance with the spec, we fail the
* entire READDIR operation(!)
*/
- if (!(cd->rd_bmval[0] & FATTR4_WORD0_RDATTR_ERROR))
+ if (!(cd->rd_bmval[0] & FATTR4_WORD0_RDATTR_ERROR)) {
+ /* READDIR cannot return NFS4ERR_WRONGSEC itself. */
+ if (nfserr == nfserr_wrongsec)
+ nfserr = nfserr_acces;
goto fail;
+ }
if (nfsd4_encode_entry4_rdattr_error(xdr, nfserr)) {
nfserr = nfserr_toosmall;
goto fail;
diff --git a/fs/nfsd/nfsfh.c b/fs/nfsd/nfsfh.c
index f1ba5a14bb85..f9fe985c7433 100644
--- a/fs/nfsd/nfsfh.c
+++ b/fs/nfsd/nfsfh.c
@@ -471,9 +471,15 @@ __fh_verify(struct svc_rqst *rqstp,
&& exp->ex_path.dentry == dentry)
may_bypass_gss = true;
- error = check_security_flavor(exp, rqstp, may_bypass_gss);
- if (error)
- goto out;
+ /*
+ * SECINFO reports the export's security flavors instead of
+ * failing on a flavor mismatch. The xprtsec= policy still applies.
+ */
+ if (!(access & NFSD_MAY_SECINFO)) {
+ error = check_security_flavor(exp, rqstp, may_bypass_gss);
+ if (error)
+ goto out;
+ }
svc_xprt_set_valid(rqstp->rq_xprt);
diff --git a/fs/nfsd/nfsproc.c b/fs/nfsd/nfsproc.c
index 2f8da62ad6b0..43e6a4b6a259 100644
--- a/fs/nfsd/nfsproc.c
+++ b/fs/nfsd/nfsproc.c
@@ -712,7 +712,7 @@ nfsd_create_file(struct svc_rqst *rqstp, struct svc_fh *fhp,
exp = exp_get(fhp->fh_export);
if (d_is_reg(dchild) &&
unlikely(nfsd_mountpoint(dchild, exp))) {
- status = nfsd_cross_mnt(rqstp, &dchild, &exp);
+ status = nfsd_cross_mnt(rqstp, &dchild, &exp, 0);
if (status != nfs_ok) {
exp_put(exp);
goto out;
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index e7b186307c21..b937217b81d2 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -142,7 +142,8 @@ DEFINE_NFSD_DYNTHREAD_EVENT(trylock_fail);
{ NFSD_MAY_BYPASS_GSS, "BYPASS_GSS" }, \
{ NFSD_MAY_READ_IF_EXEC, "READ_IF_EXEC" }, \
{ NFSD_MAY_64BIT_COOKIE, "64BIT_COOKIE" }, \
- { NFSD_MAY_LOCALIO, "LOCALIO" })
+ { NFSD_MAY_LOCALIO, "LOCALIO" }, \
+ { NFSD_MAY_SECINFO, "SECINFO" })
TRACE_EVENT(nfsd_compound,
TP_PROTO(
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index a8cf41e4dfea..a06a92bea95c 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -124,14 +124,13 @@ nfserrno(int errno)
}
/*
- * Called from nfsd_lookup and encode_dirent. Check if we have crossed
- * a mount point.
+ * Check if we have crossed a mount point.
* Returns an nfs error leaving *dpp and *expp unchanged,
* or nfs_ok having possibly changed *dpp and *expp
*/
__be32
nfsd_cross_mnt(struct svc_rqst *rqstp, struct dentry **dpp,
- struct svc_export **expp)
+ struct svc_export **expp, int access)
{
struct svc_export *exp = *expp, *exp2 = NULL;
struct dentry *dentry = *dpp;
@@ -169,7 +168,9 @@ nfsd_cross_mnt(struct svc_rqst *rqstp, struct dentry **dpp,
err = 0;
} else if (nfsd_v4client(rqstp) ||
(exp->ex_flags & NFSEXP_CROSSMOUNT) || EX_NOHIDE(exp2)) {
- nfserr = check_nfsd_access(exp, rqstp);
+ nfserr = (access & NFSD_MAY_SECINFO) ?
+ check_xprtsec_policy(exp2, rqstp) :
+ check_nfsd_access(exp2, rqstp);
if (nfserr == nfs_ok) {
/* successfully crossed mount point */
/*
@@ -204,11 +205,14 @@ static void follow_to_parent(struct path *path)
path->dentry = dp;
}
-static int nfsd_lookup_parent(struct svc_rqst *rqstp, struct dentry *dparent, struct svc_export **exp, struct dentry **dentryp)
+static __be32 nfsd_lookup_parent(struct svc_rqst *rqstp, struct dentry *dparent,
+ struct svc_export **exp, struct dentry **dentryp,
+ int access)
{
struct svc_export *exp2;
struct path path = {.mnt = mntget((*exp)->ex_path.mnt),
.dentry = dget(dparent)};
+ __be32 nfserr = nfs_ok;
follow_to_parent(&path);
@@ -216,15 +220,21 @@ static int nfsd_lookup_parent(struct svc_rqst *rqstp, struct dentry *dparent, st
if (PTR_ERR(exp2) == -ENOENT) {
*dentryp = dget(dparent);
} else if (IS_ERR(exp2)) {
- path_put(&path);
- return PTR_ERR(exp2);
+ nfserr = nfserrno(PTR_ERR(exp2));
} else {
- *dentryp = dget(path.dentry);
- exp_put(*exp);
- *exp = exp2;
+ nfserr = (access & NFSD_MAY_SECINFO) ?
+ check_xprtsec_policy(exp2, rqstp) :
+ check_nfsd_access(exp2, rqstp);
+ if (nfserr != nfs_ok) {
+ exp_put(exp2);
+ } else {
+ *dentryp = dget(path.dentry);
+ exp_put(*exp);
+ *exp = exp2;
+ }
}
path_put(&path);
- return 0;
+ return nfserr;
}
/*
@@ -255,7 +265,8 @@ int nfsd_mountpoint(struct dentry *dentry, struct svc_export *exp)
__be32
nfsd_lookup_dentry(struct svc_rqst *rqstp, struct svc_fh *fhp,
const char *name, unsigned int len,
- struct svc_export **exp_ret, struct dentry **dentry_ret)
+ struct svc_export **exp_ret, struct dentry **dentry_ret,
+ int access)
{
struct svc_export *exp;
struct dentry *dparent;
@@ -277,9 +288,13 @@ nfsd_lookup_dentry(struct svc_rqst *rqstp, struct svc_fh *fhp,
dentry = dget(dparent); /* .. == . just like at / */
else {
/* checking mountpoint crossing is very different when stepping up */
- host_err = nfsd_lookup_parent(rqstp, dparent, &exp, &dentry);
- if (host_err)
- goto out_nfserr;
+ __be32 nfserr = nfsd_lookup_parent(rqstp, dparent,
+ &exp, &dentry, access);
+
+ if (nfserr) {
+ exp_put(exp);
+ return nfserr;
+ }
}
} else {
dentry = lookup_one_unlocked(&nop_mnt_idmap,
@@ -288,7 +303,8 @@ nfsd_lookup_dentry(struct svc_rqst *rqstp, struct svc_fh *fhp,
if (IS_ERR(dentry))
goto out_nfserr;
if (nfsd_mountpoint(dentry, exp)) {
- __be32 nfserr = nfsd_cross_mnt(rqstp, &dentry, &exp);
+ __be32 nfserr = nfsd_cross_mnt(rqstp, &dentry, &exp,
+ access);
if (nfserr) {
dput(dentry);
@@ -307,37 +323,31 @@ nfsd_lookup_dentry(struct svc_rqst *rqstp, struct svc_fh *fhp,
}
/**
- * nfsd_lookup - look up a single path component for nfsd
+ * __nfsd_lookup - look up a single path component for nfsd
*
* @rqstp: the request context
* @fhp: the file handle of the directory
* @name: the component name, or %NULL to look up parent
* @len: length of name to examine
* @resfh: pointer to pre-initialised filehandle to hold result.
+ * @access: NFSD_MAY_* flags for the fh_verify() of @fhp; adding
+ * NFSD_MAY_SECINFO skips the export security flavor checks
*
- * Look up one component of a pathname.
- * N.B. After this call _both_ fhp and resfh need an fh_put
- *
- * If the lookup would cross a mountpoint, and the mounted filesystem
- * is exported to the client with NFSEXP_NOHIDE, then the lookup is
- * accepted as it stands and the mounted directory is
- * returned. Otherwise the covered directory is returned.
- * NOTE: this mountpoint crossing is not supported properly by all
- * clients and is explicitly disallowed for NFSv3
- *
+ * Variant of nfsd_lookup() that takes the fh_verify() access flags.
+ * The filehandle and mountpoint crossing rules are documented there.
*/
__be32
-nfsd_lookup(struct svc_rqst *rqstp, struct svc_fh *fhp, const char *name,
- unsigned int len, struct svc_fh *resfh)
+__nfsd_lookup(struct svc_rqst *rqstp, struct svc_fh *fhp, const char *name,
+ unsigned int len, struct svc_fh *resfh, int access)
{
struct svc_export *exp;
struct dentry *dentry;
__be32 err;
- err = fh_verify(rqstp, fhp, S_IFDIR, NFSD_MAY_EXEC);
+ err = fh_verify(rqstp, fhp, S_IFDIR, access);
if (err)
return err;
- err = nfsd_lookup_dentry(rqstp, fhp, name, len, &exp, &dentry);
+ err = nfsd_lookup_dentry(rqstp, fhp, name, len, &exp, &dentry, access);
if (err)
return err;
/*
diff --git a/fs/nfsd/vfs.h b/fs/nfsd/vfs.h
index 3cac25adf16b..cdfdb6c4d836 100644
--- a/fs/nfsd/vfs.h
+++ b/fs/nfsd/vfs.h
@@ -33,6 +33,7 @@
#define NFSD_MAY_64BIT_COOKIE 0x1000 /* 64 bit readdir cookies for >= NFSv3 */
#define NFSD_MAY_LOCALIO 0x2000 /* for tracing, reflects when localio used */
+#define NFSD_MAY_SECINFO 0x4000 /* query export security policy */
#define NFSD_MAY_CREATE (NFSD_MAY_EXEC|NFSD_MAY_WRITE)
#define NFSD_MAY_REMOVE (NFSD_MAY_EXEC|NFSD_MAY_WRITE|NFSD_MAY_TRUNC)
@@ -128,12 +129,42 @@ void nfsd_maybe_reset_write_verifier(struct nfsd_net *nn,
struct svc_rqst *rqstp,
int err);
__be32 nfsd_cross_mnt(struct svc_rqst *rqstp, struct dentry **dpp,
- struct svc_export **expp);
-__be32 nfsd_lookup(struct svc_rqst *, struct svc_fh *,
- const char *, unsigned int, struct svc_fh *);
-__be32 nfsd_lookup_dentry(struct svc_rqst *, struct svc_fh *,
- const char *, unsigned int,
- struct svc_export **, struct dentry **);
+ struct svc_export **expp, int access);
+__be32 __nfsd_lookup(struct svc_rqst *rqstp, struct svc_fh *fhp,
+ const char *name, unsigned int len,
+ struct svc_fh *resfh, int access);
+
+/**
+ * nfsd_lookup - look up a single path component for nfsd
+ *
+ * @rqstp: the request context
+ * @fhp: the file handle of the directory
+ * @name: the component name, or %NULL to look up parent
+ * @len: length of name to examine
+ * @resfh: pointer to pre-initialised filehandle to hold result.
+ *
+ * Look up one component of a pathname.
+ * N.B. After this call _both_ fhp and resfh need an fh_put
+ *
+ * If the lookup would cross a mountpoint, and the mounted filesystem
+ * is exported to the client with NFSEXP_NOHIDE, then the lookup is
+ * accepted as it stands and the mounted directory is
+ * returned. Otherwise the covered directory is returned.
+ * NOTE: this mountpoint crossing is not supported properly by all
+ * clients and is explicitly disallowed for NFSv3
+ *
+ */
+static inline __be32 nfsd_lookup(struct svc_rqst *rqstp, struct svc_fh *fhp,
+ const char *name, unsigned int len,
+ struct svc_fh *resfh)
+{
+ return __nfsd_lookup(rqstp, fhp, name, len, resfh, NFSD_MAY_EXEC);
+}
+
+__be32 nfsd_lookup_dentry(struct svc_rqst *rqstp, struct svc_fh *fhp,
+ const char *name, unsigned int len,
+ struct svc_export **exp_ret,
+ struct dentry **dentry_ret, int access);
__be32 nfsd_setattr(struct svc_rqst *, struct svc_fh *,
struct nfsd_attrs *, const struct timespec64 *);
int nfsd_mountpoint(struct dentry *, struct svc_export *);
--
2.56.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check
2026-11-01 15:51 [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check Chuck Lever
` (2 preceding siblings ...)
2026-11-01 15:51 ` [PATCH v6 3/3] NFSD: Enforce export security on LOOKUP, READDIR, and SECINFO Chuck Lever
@ 2026-11-02 2:19 ` VeroFess
3 siblings, 0 replies; 5+ messages in thread
From: VeroFess @ 2026-11-02 2:19 UTC (permalink / raw)
To: Chuck Lever
Cc: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
linux-nfs
On Sun, Nov 01, 2026 at 10:51:09AM -0500, Chuck Lever wrote:
> Changes in v6:
> - Verify the current filehandle in SECINFO_NO_NAME(CURRENT_FH) (sashiko).
Hi Chuck,
I tested all three v6 patches on the stated nfsd-testing base,
d9130c9f6535392d423351ffb5521e7689cae6b6. I built and booted
patch 1 alone, patches 1 and 2 together, and the full series in an
x86_64 KASAN guest. The full-series tests also used a second guest for
inter-server COPY and RPC-over-TLS.
I checked the v6 change directly with AUTH_SYS
PUTROOTFH; SECINFO_NO_NAME(CURRENT_FH) on NFSv4.1 and v4.2. With an
xprtsec=tls export over plain TCP, v5 returned OK and v6 returned
NFS4ERR_ACCESS at SECINFO_NO_NAME. With a secure export and a high
source port, v5 returned OK and v6 returned NFS4ERR_PERM at that
operation. PUTROOTFH succeeded in both v6 cases. A mismatched sec=
flavor still returned OK and listed krb5p, as SECINFO_NO_NAME needs
to do. The operation succeeded from a reserved port and over real
TLS 1.3 or mTLS when the export allowed the transport; TLS without
a client certificate was denied by an xprtsec=mtls export.
The original NFSv4.0 Kerberos OPEN stateid reproducer returned
NFS4ERR_WRONGSEC at PUTFH for AUTH_SYS direct and SAVEFH READ/WRITE.
The file was unchanged, and the Kerberos client could still read it.
A second guest tried the same 108 stateid candidates as in the earlier
test and obtained no data. With only patch 1, a protected saved-source
COPY succeeded and changed the destination; CLONE also returned OK. With
patch 2 added, both returned NFS4ERR_ACCESS at their own operations
and left the destination unchanged; authorized COPY and CLONE passed.
With patches 1 and 2, LOOKUPP still crossed from a sec=sys child to a
sec=krb5p parent. The full series returned NFS4ERR_WRONGSEC there
on v4.0, v4.1, and v4.2. SECINFO_NO_NAME(PARENT) remained usable on
v4.1 and v4.2. I also reran the SECINFO name and permission cases,
READDIR with and without rdattr_error or FILEHANDLE, the NFSv2/v3
CREATE cases, and NFSv4 OPEN. Their status and error placement matched
the v5 run.
The other regression tests passed: permitted Btrfs COPY and reflink
CLONE returned matching destination bytes; asynchronous inter-server
COPY completed 4096 bytes between the two guests; protected local
COPY, LINK, and RENAME were rejected; and retained stateids could not
be used after tightening the export's sec= policy. Plaintext, TLS,
and mTLS mount crossings behaved as before on v4.0, v4.1, and v4.2.
A raw v4.0 SECINFO request also succeeded over both TLS and mTLS.
The 259-case source-derived decision test passed under ASan/UBSan.
W=1 fs/nfsd/nfs4proc.o built cleanly with inter-server COPY enabled
and disabled. I saw no KASAN report or kernel Oops. I found no v6
regression in these tests.
Tested-by: VeroFess <coderzeng@gmail.com>
Thanks,
VeroFess
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-02 2:20 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-01 15:51 [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check Chuck Lever
2026-11-01 15:51 ` [PATCH v6 1/3] NFSD: Fix PUTFH wrongsec check when SAVEFH follows Chuck Lever
2026-11-01 15:51 ` [PATCH v6 2/3] NFSD: Check export security on the COPY and CLONE source Chuck Lever
2026-11-01 15:51 ` [PATCH v6 3/3] NFSD: Enforce export security on LOOKUP, READDIR, and SECINFO Chuck Lever
2026-11-02 2:19 ` [PATCH v6 0/3] NFSD: Enforce export security after a deferred PUTFH check VeroFess
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.