[PATCH 9/9] NFSD: Add allow_tags to the netlink export interface

Chuck Lever cel at kernel.org
Fri Jun 5 10:34:43 PDT 2026


From: Chuck Lever <chuck.lever at oracle.com>

The legacy exportfs cache path accepts an allow_tags clause that
restricts an export to mTLS sessions carrying at least one matching
session tag. The netlink-based svc_export interface had no such
attribute, so administrators configuring exports via netlink could
not request tag enforcement: nfsd_nl_parse_one_export() always
left ex_allow_tags empty, and check_xprtsec_policy() then granted
any authenticated peer.

Extend the svc-export attribute set with allow-tags and parse it
in nfsd_nl_parse_one_export(). Apply the same xprtsec=mtls
consistency check as svc_export_parse() so the netlink path
refuses contradictory security policy rather than silently exposing
a tagged export to plaintext or anonymous-TLS peers.

Signed-off-by: Chuck Lever <chuck.lever at oracle.com>
---
 Documentation/netlink/specs/nfsd.yaml | 10 ++++++
 fs/nfsd/export.c                      | 68 +++++++++++++++++++++++++++++++++--
 fs/nfsd/netlink.c                     |  4 ++-
 fs/nfsd/netlink.h                     |  3 +-
 include/uapi/linux/nfsd_netlink.h     |  1 +
 5 files changed, 82 insertions(+), 4 deletions(-)

diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index 8f36fadd68f7..5cbdc1dab7e3 100644
--- a/Documentation/netlink/specs/nfsd.yaml
+++ b/Documentation/netlink/specs/nfsd.yaml
@@ -7,6 +7,10 @@ uapi-header: linux/nfsd_netlink.h
 doc: NFSD configuration over generic netlink.
 
 definitions:
+  -
+    name: handshake-session-tag-max-len
+    type: const
+    header: uapi/linux/handshake.h
   -
     type: flags
     name: cache-type
@@ -253,6 +257,12 @@ attribute-sets:
       -
         name: fsid
         type: s32
+      -
+        name: allow-tags
+        type: string
+        checks:
+          max-len: handshake-session-tag-max-len
+        multi-attr: true
   -
     name: svc-export-reqs
     attributes:
diff --git a/fs/nfsd/export.c b/fs/nfsd/export.c
index a2aaa3cd6c52..25802de2de40 100644
--- a/fs/nfsd/export.c
+++ b/fs/nfsd/export.c
@@ -831,6 +831,7 @@ static struct svc_export *svc_export_update(struct svc_export *new,
 static struct svc_export *svc_export_lookup(struct svc_export *);
 static int check_export(const struct path *path, int *flags,
 			unsigned char *uuid);
+static int check_allow_tags(const struct svc_export *exp);
 
 /**
  * nfsd_nl_parse_one_export - parse one svc_export entry from a netlink message
@@ -845,14 +846,14 @@ static int check_export(const struct path *path, int *flags,
 static int nfsd_nl_parse_one_export(struct cache_detail *cd,
 				    struct nlattr *attr)
 {
-	struct nlattr *tb[NFSD_A_SVC_EXPORT_FSID + 1];
+	struct nlattr *tb[NFSD_A_SVC_EXPORT_ALLOW_TAGS + 1];
 	struct auth_domain *dom = NULL;
 	struct svc_export exp = {}, *expp;
 	struct nlattr *secinfo_attr;
 	struct timespec64 boot;
 	int err, rem;
 
-	err = nla_parse_nested(tb, NFSD_A_SVC_EXPORT_FSID, attr,
+	err = nla_parse_nested(tb, NFSD_A_SVC_EXPORT_ALLOW_TAGS, attr,
 			       nfsd_svc_export_nl_policy, NULL);
 	if (err)
 		return err;
@@ -993,6 +994,68 @@ static int nfsd_nl_parse_one_export(struct cache_detail *cd,
 			}
 		}
 
+		/* allow-tags (multi-attr string) */
+		if (tb[NFSD_A_SVC_EXPORT_ALLOW_TAGS]) {
+			struct nlattr *tag_attr;
+			unsigned int count = 0;
+
+			/*
+			 * The NLA_STRING policy does not guarantee a
+			 * terminating NUL, so each tag is copied with
+			 * the length-aware nla_strdup(). Embedded NUL
+			 * bytes are rejected here because the policy
+			 * cannot express that check; a tag containing
+			 * one could never match a handshake-supplied
+			 * tag, which net/handshake rejects the same
+			 * way.
+			 */
+			nla_for_each_nested_type(tag_attr,
+						 NFSD_A_SVC_EXPORT_ALLOW_TAGS,
+						 attr, rem) {
+				const char *src = nla_data(tag_attr);
+				size_t srclen = nla_len(tag_attr);
+
+				if (srclen > 0 && src[srclen - 1] == '\0')
+					srclen--;
+				if (srclen == 0 ||
+				    memchr(src, '\0', srclen)) {
+					err = -EINVAL;
+					goto out_uuid;
+				}
+				count++;
+			}
+			if (count > NFSD_MAX_ALLOW_TAGS) {
+				err = -EINVAL;
+				goto out_uuid;
+			}
+			if (!tagset_alloc(&exp.ex_allow_tags, count,
+					  GFP_KERNEL)) {
+				err = -ENOMEM;
+				goto out_uuid;
+			}
+			nla_for_each_nested_type(tag_attr,
+						 NFSD_A_SVC_EXPORT_ALLOW_TAGS,
+						 attr, rem) {
+				char *tag;
+
+				tag = nla_strdup(tag_attr, GFP_KERNEL);
+				if (!tag) {
+					err = -ENOMEM;
+					goto out_uuid;
+				}
+				if (!tagset_add(&exp.ex_allow_tags, tag)) {
+					kfree(tag);
+					err = -ENOMEM;
+					goto out_uuid;
+				}
+			}
+			tagset_finalize(&exp.ex_allow_tags);
+		}
+
+		err = check_allow_tags(&exp);
+		if (err)
+			goto out_uuid;
+
 		err = check_export(&exp.ex_path, &exp.ex_flags,
 				   exp.ex_uuid);
 		if (err)
@@ -1026,6 +1089,7 @@ static int nfsd_nl_parse_one_export(struct cache_detail *cd,
 	}
 
 out_uuid:
+	tagset_destroy(&exp.ex_allow_tags);
 	kfree(exp.ex_uuid);
 out_fslocs:
 	nfsd4_fslocs_free(&exp.ex_fslocs);
diff --git a/fs/nfsd/netlink.c b/fs/nfsd/netlink.c
index fbee3676d253..4db094b1021f 100644
--- a/fs/nfsd/netlink.c
+++ b/fs/nfsd/netlink.c
@@ -10,6 +10,7 @@
 #include "netlink.h"
 
 #include <uapi/linux/nfsd_netlink.h>
+#include <uapi/linux/handshake.h>
 
 /* Common nested types */
 const struct nla_policy nfsd_auth_flavor_nl_policy[NFSD_A_AUTH_FLAVOR_FLAGS + 1] = {
@@ -41,7 +42,7 @@ const struct nla_policy nfsd_sock_nl_policy[NFSD_A_SOCK_TRANSPORT_NAME + 1] = {
 	[NFSD_A_SOCK_TRANSPORT_NAME] = { .type = NLA_NUL_STRING, },
 };
 
-const struct nla_policy nfsd_svc_export_nl_policy[NFSD_A_SVC_EXPORT_FSID + 1] = {
+const struct nla_policy nfsd_svc_export_nl_policy[NFSD_A_SVC_EXPORT_ALLOW_TAGS + 1] = {
 	[NFSD_A_SVC_EXPORT_SEQNO] = { .type = NLA_U64, },
 	[NFSD_A_SVC_EXPORT_CLIENT] = { .type = NLA_NUL_STRING, },
 	[NFSD_A_SVC_EXPORT_PATH] = { .type = NLA_NUL_STRING, },
@@ -55,6 +56,7 @@ const struct nla_policy nfsd_svc_export_nl_policy[NFSD_A_SVC_EXPORT_FSID + 1] =
 	[NFSD_A_SVC_EXPORT_XPRTSEC] = NLA_POLICY_MASK(NLA_U32, 0x7),
 	[NFSD_A_SVC_EXPORT_FLAGS] = NLA_POLICY_MASK(NLA_U32, 0x3ffff),
 	[NFSD_A_SVC_EXPORT_FSID] = { .type = NLA_S32, },
+	[NFSD_A_SVC_EXPORT_ALLOW_TAGS] = { .type = NLA_STRING, .len = HANDSHAKE_SESSION_TAG_MAX_LEN, },
 };
 
 const struct nla_policy nfsd_version_nl_policy[NFSD_A_VERSION_ENABLED + 1] = {
diff --git a/fs/nfsd/netlink.h b/fs/nfsd/netlink.h
index af41aa0d4a65..133e99a0a3fc 100644
--- a/fs/nfsd/netlink.h
+++ b/fs/nfsd/netlink.h
@@ -11,6 +11,7 @@
 #include <net/genetlink.h>
 
 #include <uapi/linux/nfsd_netlink.h>
+#include <uapi/linux/handshake.h>
 
 /* Common nested types */
 extern const struct nla_policy nfsd_auth_flavor_nl_policy[NFSD_A_AUTH_FLAVOR_FLAGS + 1];
@@ -18,7 +19,7 @@ extern const struct nla_policy nfsd_expkey_nl_policy[NFSD_A_EXPKEY_PATH + 1];
 extern const struct nla_policy nfsd_fslocation_nl_policy[NFSD_A_FSLOCATION_PATH + 1];
 extern const struct nla_policy nfsd_fslocations_nl_policy[NFSD_A_FSLOCATIONS_LOCATION + 1];
 extern const struct nla_policy nfsd_sock_nl_policy[NFSD_A_SOCK_TRANSPORT_NAME + 1];
-extern const struct nla_policy nfsd_svc_export_nl_policy[NFSD_A_SVC_EXPORT_FSID + 1];
+extern const struct nla_policy nfsd_svc_export_nl_policy[NFSD_A_SVC_EXPORT_ALLOW_TAGS + 1];
 extern const struct nla_policy nfsd_version_nl_policy[NFSD_A_VERSION_ENABLED + 1];
 
 int nfsd_nl_rpc_status_get_dumpit(struct sk_buff *skb,
diff --git a/include/uapi/linux/nfsd_netlink.h b/include/uapi/linux/nfsd_netlink.h
index f5b75d5caba9..23a42c26ede0 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -165,6 +165,7 @@ enum {
 	NFSD_A_SVC_EXPORT_XPRTSEC,
 	NFSD_A_SVC_EXPORT_FLAGS,
 	NFSD_A_SVC_EXPORT_FSID,
+	NFSD_A_SVC_EXPORT_ALLOW_TAGS,
 
 	__NFSD_A_SVC_EXPORT_MAX,
 	NFSD_A_SVC_EXPORT_MAX = (__NFSD_A_SVC_EXPORT_MAX - 1)

-- 
2.54.0




More information about the Linux-nvme mailing list