* [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD)
@ 2026-11-12 14:52 Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 01/10] handshake: Add tags to "done" downcall Chuck Lever
` (9 more replies)
0 siblings, 10 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs,
Hannes Reinecke
NFSD and similar upper-layer services want access-control decisions
based on TLS peer-certificate characteristics, but in-kernel x.509
parsing would duplicate work mature userspace libraries already do.
tlshd evaluates certificates against admin-defined policy and reports
the policies that matched back to the kernel as opaque string tags.
Intersection against per-resource tag sets stays the consumer's
problem.
The series builds on commit 81246a65303d ("handshake: Require admin
permission for DONE command"), now in net-next. Without it a forged
DONE downcall could set session tags for a real handshake.
Four architectural choices shape the series, only one of which is
visible in any single patch.
The tagging vocabulary is opaque to the kernel. tlshd decides what
each tag means, and the handshake layer and its consumers only test
membership. Any future attribute the kernel wants to gate on has to
be expressed as a tag rather than as a new netlink field.
HANDSHAKE_MAX_SESSIONTAGS rides on every ACCEPT reply as
HANDSHAKE_A_ACCEPT_MAX_SESSION_TAGS (patch 7), so tlshd can size its
DONE-side tag list to the running kernel's limit rather than to a
header constant.
Session tags have one owner at each step. The DONE handler collects
them into a private stringset before taking the completion gate, so
no allocation happens inside the gate. The gate's winner hands the
set to handshake_finish_complete() (patch 6 adds a kunit test for the
gate). svcsock copies the set into the transport and orders the copy
before setting XPT_TLS_SESSION. check_xprtsec_policy() reads that bit
with test_bit_acquire(), so a transport that reports a TLS session
always has its tags visible.
The stringset helper (patch 2) is a generic library so that NFSD
export tagging (patches 9 and 10) can use it without further churn in
net/handshake/.
This series applies to the nfsd-testing branch of
https://git.kernel.org/pub/scm/linux/kernel/git/cel/linux.git
The base-commit below is the tip of that branch as of August 18.
---
Changes in v2:
- Drop v1's 1/9, sent ahead of the set per Jeff and now merged.
- Adopt the pinned hr_file/fput lifetime model.
- Adapt to the errno-valued handshake status (hp_done takes int).
- Rename "tagset" to "stringset" throughout (Hannes)
- Warn and return false when a query precedes stringset_finalize().
- Add a patch with KUnit tests for stringset.
- Split the hp_done session-tags argument into its own patch.
- Collect the session tags before taking the completion gate.
- Pass ownership of the tag set to handshake_finish_complete().
- Add handshake_session_tag_len(), shared with the NFSD parser.
- Add KUnit tests for session-tag collection.
- Check the file reference count in the completion-gate KUnit case.
- Rename the ACCEPT cap attribute MAX_TAGS to MAX_SESSION_TAGS.
- Publish the saved tags with smp_wmb() and test_bit_acquire().
- Deny a tagged export on the non-mTLS arms of check_xprtsec_policy().
- Extract nfsd_nl_parse_allow_tags() from the netlink export parser.
- Build stringset only when a consumer selects CONFIG_STRINGSET.
- Free the session tags as soon as hp_done() returns.
- Report a rejected session tag or allow-tag through the extack.
- Show allow_tags in /proc/fs/nfsd/exports.
- Record the client address and XID in nfsd_export_tags_denied.
- Link to v1: https://patch.msgid.link/20260605-tls-session-tags-v1-0-47bd1d94d552@oracle.com
---
Chuck Lever (10):
handshake: Add tags to "done" downcall
lib: Add a "stringset" data structure
lib: Add KUnit tests for the stringset data structure
handshake: Add a session-tags argument to the TLS done callback
handshake: Pick up session tags passed during the DONE downcall
handshake: Add a kunit test for the completion gate
handshake: Advertise the session-tag cap to user space
SUNRPC: Copy the TLS session tags when they are available
NFSD: Implement export tagging
NFSD: Add allow_tags to the netlink export interface
Documentation/core-api/index.rst | 1 +
Documentation/core-api/stringset.rst | 243 +++++++++++++++++++++++
Documentation/netlink/specs/handshake.yaml | 19 ++
Documentation/netlink/specs/nfsd.yaml | 10 +
Documentation/networking/tls-handshake.rst | 62 +++++-
MAINTAINERS | 8 +
drivers/nvme/host/tcp.c | 3 +-
drivers/nvme/target/tcp.c | 3 +-
fs/nfsd/Kconfig | 1 +
fs/nfsd/export.c | 194 +++++++++++++++++-
fs/nfsd/export.h | 9 +
fs/nfsd/netlink.c | 4 +-
fs/nfsd/netlink.h | 3 +-
fs/nfsd/trace.h | 27 +++
include/linux/stringset.h | 191 ++++++++++++++++++
include/linux/sunrpc/svc_xprt.h | 7 +-
include/net/handshake.h | 50 ++++-
include/uapi/linux/handshake.h | 4 +
include/uapi/linux/nfsd_netlink.h | 1 +
lib/Kconfig | 12 ++
lib/Kconfig.debug | 6 +
lib/Makefile | 1 +
lib/stringset.c | 190 ++++++++++++++++++
lib/tests/Makefile | 1 +
lib/tests/stringset_kunit.c | 308 +++++++++++++++++++++++++++++
net/Kconfig | 1 +
net/handshake/genl.c | 5 +-
net/handshake/handshake-test.c | 307 ++++++++++++++++++++++++++++
net/handshake/handshake.h | 9 +
net/handshake/netlink.c | 110 ++++++++++-
net/handshake/request.c | 61 +++++-
net/handshake/tlshd.c | 10 +-
net/sunrpc/Kconfig | 1 +
net/sunrpc/svc_xprt.c | 11 +-
net/sunrpc/svcauth_unix.c | 14 +-
net/sunrpc/svcsock.c | 63 +++++-
net/sunrpc/xprtsock.c | 4 +-
tools/net/ynl/Makefile.deps | 3 +-
38 files changed, 1911 insertions(+), 46 deletions(-)
---
base-commit: c88fd6989aea9237a60a4859139a265bfb7ddc0b
change-id: 20260512-tls-session-tags-9d0042583f44
Best regards,
--
Chuck Lever <cel@kernel.org>
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH net-next v2 01/10] handshake: Add tags to "done" downcall
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 02/10] lib: Add a "stringset" data structure Chuck Lever
` (8 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs,
Hannes Reinecke
tlshd has no way to report certificate characteristics to the
kernel. An Upper Layer Protocol (such as NFS) therefore cannot
authorize access based on what a client's certificate contains.
For example, an export might be restricted to clients that present
certificates whose issuer DN contains "O=Oracle". tlshd can parse
incoming certificates, and add an "oraclegroup" tag to handshakes
where a client presents a certificate with "O=Oracle" somewhere in
its Issuer field. NFSD can then be configured to look for that tag
and permit access only when it is present. NFSD needs no knowledge
of x.509 certificates.
Plumb in the netlink protocol elements for tlshd to return a list
of tags to the kernel when a handshake succeeds. Subsequent patches
add tag extraction and storage in the handshake layer.
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Hannes Reinecke <hare@kernel.org>
---
Documentation/netlink/specs/handshake.yaml | 15 +++++++++++++++
include/uapi/linux/handshake.h | 3 +++
net/handshake/genl.c | 5 +++--
3 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/Documentation/netlink/specs/handshake.yaml b/Documentation/netlink/specs/handshake.yaml
index 0498ada2dd97..17521b159e89 100644
--- a/Documentation/netlink/specs/handshake.yaml
+++ b/Documentation/netlink/specs/handshake.yaml
@@ -18,6 +18,10 @@ definitions:
value: 4095
header: linux/err.h
scope: kernel
+ -
+ name: session-tag-max-len
+ type: const
+ value: 255
-
type: enum
name: handler-class
@@ -96,6 +100,16 @@ attribute-sets:
name: remote-auth
type: s32
multi-attr: true
+ -
+ name: tag
+ type: string
+ doc: >-
+ A tag the handshake agent assigns to this handshake. The
+ payload may omit the trailing NUL. An embedded NUL is
+ rejected.
+ checks:
+ max-len: session-tag-max-len
+ multi-attr: true
operations:
list:
@@ -133,6 +147,7 @@ operations:
- status
- sockfd
- remote-auth
+ - tag
mcast-groups:
list:
diff --git a/include/uapi/linux/handshake.h b/include/uapi/linux/handshake.h
index f907aa082520..bbb9ea2fa4fb 100644
--- a/include/uapi/linux/handshake.h
+++ b/include/uapi/linux/handshake.h
@@ -10,6 +10,8 @@
#define HANDSHAKE_FAMILY_NAME "handshake"
#define HANDSHAKE_FAMILY_VERSION 1
+#define HANDSHAKE_SESSION_TAG_MAX_LEN 255
+
enum handshake_handler_class {
HANDSHAKE_HANDLER_CLASS_NONE,
HANDSHAKE_HANDLER_CLASS_TLSHD,
@@ -59,6 +61,7 @@ enum {
HANDSHAKE_A_DONE_STATUS = 1,
HANDSHAKE_A_DONE_SOCKFD,
HANDSHAKE_A_DONE_REMOTE_AUTH,
+ HANDSHAKE_A_DONE_TAG,
__HANDSHAKE_A_DONE_MAX,
HANDSHAKE_A_DONE_MAX = (__HANDSHAKE_A_DONE_MAX - 1)
diff --git a/net/handshake/genl.c b/net/handshake/genl.c
index 26e3efec64ca..7150b32a54c4 100644
--- a/net/handshake/genl.c
+++ b/net/handshake/genl.c
@@ -18,10 +18,11 @@ static const struct nla_policy handshake_accept_nl_policy[HANDSHAKE_A_ACCEPT_HAN
};
/* HANDSHAKE_CMD_DONE - do */
-static const struct nla_policy handshake_done_nl_policy[HANDSHAKE_A_DONE_REMOTE_AUTH + 1] = {
+static const struct nla_policy handshake_done_nl_policy[HANDSHAKE_A_DONE_TAG + 1] = {
[HANDSHAKE_A_DONE_STATUS] = NLA_POLICY_MAX(NLA_U32, MAX_ERRNO),
[HANDSHAKE_A_DONE_SOCKFD] = { .type = NLA_S32, },
[HANDSHAKE_A_DONE_REMOTE_AUTH] = { .type = NLA_S32, },
+ [HANDSHAKE_A_DONE_TAG] = { .type = NLA_STRING, .len = HANDSHAKE_SESSION_TAG_MAX_LEN, },
};
/* Ops table for handshake */
@@ -37,7 +38,7 @@ static const struct genl_split_ops handshake_nl_ops[] = {
.cmd = HANDSHAKE_CMD_DONE,
.doit = handshake_nl_done_doit,
.policy = handshake_done_nl_policy,
- .maxattr = HANDSHAKE_A_DONE_REMOTE_AUTH,
+ .maxattr = HANDSHAKE_A_DONE_TAG,
.flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
},
};
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 02/10] lib: Add a "stringset" data structure
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 01/10] handshake: Add tags to "done" downcall Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-13 14:52 ` sashiko-bot
2026-11-12 14:52 ` [PATCH net-next v2 03/10] lib: Add KUnit tests for the stringset " Chuck Lever
` (7 subsequent siblings)
9 siblings, 1 reply; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
Access control mechanisms sometimes need to match metadata tags
between a session and a resource. A stringset provides efficient
membership testing and set intersection operations for this purpose.
Set intersection, not membership, is what motivates a purpose-built
structure. xarray and rbtree answer a membership query in O(log M),
so intersecting by probing costs O(N log M). rhashtable probes in
O(1) expected, but pays a hash per element and gives up the ordering
a merge relies on. Two sorted arrays instead merge in a single
O(N + M) pass.
The implementation therefore uses a sorted array of string pointers.
Beyond the intersection cost, a sorted array needs no per-element
node allocation. An rbtree does, so it carries more memory overhead
for the small sets typical of resource tagging.
The API follows a build-then-query pattern: callers initialize,
allocate capacity, add strings, then finalize before querying. Once
finalized, the stringset is suitable for concurrent read access.
Build the library only when a consumer selects CONFIG_STRINGSET.
Consumers of the new APIs arrive in subsequent patches.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
Documentation/core-api/index.rst | 1 +
Documentation/core-api/stringset.rst | 243 +++++++++++++++++++++++++++++++++++
MAINTAINERS | 7 +
include/linux/stringset.h | 191 +++++++++++++++++++++++++++
lib/Kconfig | 12 ++
lib/Makefile | 1 +
lib/stringset.c | 190 +++++++++++++++++++++++++++
7 files changed, 645 insertions(+)
diff --git a/Documentation/core-api/index.rst b/Documentation/core-api/index.rst
index 92f91c6a0d79..929bf11828c3 100644
--- a/Documentation/core-api/index.rst
+++ b/Documentation/core-api/index.rst
@@ -45,6 +45,7 @@ Library functionality that is used throughout the kernel.
idr
circular-buffers
rbtree
+ stringset
generic-radix-tree
packing
this_cpu_ops
diff --git a/Documentation/core-api/stringset.rst b/Documentation/core-api/stringset.rst
new file mode 100644
index 000000000000..549199ae1fa2
--- /dev/null
+++ b/Documentation/core-api/stringset.rst
@@ -0,0 +1,243 @@
+.. SPDX-License-Identifier: GPL-2.0+
+
+=========
+Stringset
+=========
+
+Overview
+========
+
+A stringset is a set of strings, intended for resource tagging where
+metadata about a resource is represented simply by a name. The public
+API can be found in ``<linux/stringset.h>``.
+
+
+Initialization and Destruction
+==============================
+
+A stringset must be initialized before use::
+
+ struct stringset set;
+ stringset_init(&set);
+
+``DEFINE_STRINGSET()`` declares a stringset and initializes it in
+one step::
+
+ DEFINE_STRINGSET(set);
+
+Before adding strings, allocate capacity::
+
+ if (!stringset_alloc(&set, num_strings, GFP_KERNEL))
+ return -ENOMEM;
+
+When finished, release all resources::
+
+ stringset_destroy(&set);
+
+This frees all strings and the array. The stringset is reinitialized
+and may be reused by calling ``stringset_alloc()`` again.
+
+
+Adding Strings
+==============
+
+Two functions are provided for adding strings. Both require that
+sufficient capacity has been allocated via ``stringset_alloc()``.
+
+``stringset_add()``
+ Adds a string that is already in kmalloc'd memory. On success,
+ the stringset takes ownership of the string and frees it when
+ the stringset is destroyed, or at ``stringset_finalize()`` if it
+ is a duplicate::
+
+ char *str = kstrdup("example", GFP_KERNEL);
+ if (!str || !stringset_add(&set, str))
+ kfree(str); /* failed, caller must free */
+
+``stringset_add_dup()``
+ Duplicates the string internally. The caller retains ownership
+ of the original string::
+
+ if (!stringset_add_dup(&set, "example", GFP_KERNEL))
+ return -ENOMEM;
+
+Both functions return true on success, false on failure. Strings must
+not be added after ``stringset_finalize()`` has been called. Pass
+``GFP_ATOMIC`` to ``stringset_add_dup()`` in atomic context.
+
+
+Finalizing
+==========
+
+After adding all strings, the stringset must be finalized before
+querying::
+
+ stringset_finalize(&set);
+
+This sorts the array to enable efficient binary search and removes
+any duplicate strings. Calling ``stringset_is_member()`` or
+``stringset_intersection()`` on a non-finalized stringset warns and
+returns ``false``.
+
+
+Querying Strings
+================
+
+``stringset_is_empty()``
+ Returns true if the stringset contains no strings.
+
+``stringset_count()``
+ Returns the number of strings in the stringset. Before finalization,
+ this is the number of strings added; after finalization, this is
+ the number of unique strings.
+
+``stringset_is_member()``
+ Returns true if a string is present in the stringset. Uses binary
+ search for O(log N) complexity::
+
+ if (stringset_is_member(&set, "example"))
+ pr_info("string found\n");
+
+``stringset_intersection()``
+ Returns true if two stringsets share at least one common string.
+ Uses merge-style comparison for O(N+M) complexity::
+
+ if (stringset_intersection(&set1, &set2))
+ pr_info("sets overlap\n");
+
+
+Iteration
+=========
+
+Use ``stringset_for_each()`` to iterate over all strings::
+
+ unsigned int index;
+ char *str;
+
+ stringset_for_each(&set, index, str)
+ pr_info("string: %s\n", str);
+
+Callers should not depend on the order in which strings are returned.
+Modifying the stringset during iteration produces undefined behavior.
+
+
+Copying
+=======
+
+``stringset_copy()`` duplicates all strings from one stringset to
+another::
+
+ struct stringset copy;
+
+ stringset_init(©);
+ if (!stringset_copy(©, &original, GFP_KERNEL))
+ pr_err("copy failed\n");
+
+The source stringset must be finalized before copying; copying an
+unfinalized stringset warns and returns failure. The destination
+stringset is initialized and ready for queries after this function
+returns (no separate ``stringset_finalize()`` call is needed). Each
+string is duplicated, so the two stringsets are fully independent after
+copying.
+
+
+Typical Usage Pattern
+=====================
+
+A typical usage pattern for building a stringset::
+
+ struct stringset set;
+
+ stringset_init(&set);
+ if (!stringset_alloc(&set, count, GFP_KERNEL))
+ return -ENOMEM;
+
+ for (i = 0; i < count; i++) {
+ if (!stringset_add_dup(&set, strings[i], GFP_KERNEL)) {
+ stringset_destroy(&set);
+ return -ENOMEM;
+ }
+ }
+ stringset_finalize(&set);
+
+ /* Now safe to query */
+ if (stringset_is_member(&set, "target"))
+ do_something();
+
+ stringset_destroy(&set);
+
+
+Thread Safety
+=============
+
+Stringsets have no internal locking. Callers provide synchronization
+between writers and readers.
+
+The build-and-finalize phase mutates the stringset; the post-finalize
+query phase reads from it. The two phases must be separated by a
+publication boundary, because ``stringset_finalize()`` itself carries
+no memory barrier. A reader that observes the finalized stringset
+before the writer's stores have propagated may see a stale
+``ss_count`` or a partially populated ``ss_strings[]`` array.
+
+Three publication patterns are sufficient:
+
+* Lock release after ``stringset_finalize()``, lock acquire before
+ each query. The matching unlock/lock pair supplies release and
+ acquire ordering.
+
+* ``rcu_assign_pointer()`` of the stringset pointer after
+ ``stringset_finalize()``, paired with ``rcu_dereference()`` inside
+ an RCU read-side critical section on the reader.
+
+* ``smp_store_release()`` of the stringset pointer after
+ ``stringset_finalize()``, paired with ``smp_load_acquire()`` on the
+ reader.
+
+Once published, the stringset must remain immutable until no further
+readers can observe it. ``stringset_destroy()`` is not safe against
+concurrent readers, and ``stringset_finalize()`` must not be called
+more than once. With RCU publication, callers typically defer
+destruction to a grace period (``synchronize_rcu()`` before
+``stringset_destroy()``, or ``stringset_destroy()`` from a
+``call_rcu()`` callback) so that in-flight readers drain before
+the storage is freed.
+
+
+Implementation
+==============
+
+Each stringset is rooted on the following structure::
+
+ struct stringset {
+ char **ss_strings;
+ unsigned int ss_count;
+ unsigned int ss_capacity;
+ bool ss_finalized;
+ };
+
+The implementation uses a sorted array of string pointers, providing
+O(log N) membership testing and O(N+M) intersection operations.
+
++----------------------------+-------------+
+| Operation | Complexity |
++============================+=============+
+| stringset_add | O(1) |
++----------------------------+-------------+
+| stringset_finalize | O(N log N) |
++----------------------------+-------------+
+| stringset_is_member | O(log N) |
++----------------------------+-------------+
+| stringset_intersection | O(N + M) |
++----------------------------+-------------+
+| stringset_copy | O(N) |
++----------------------------+-------------+
+
+API Reference
+=============
+
+.. kernel-doc:: include/linux/stringset.h
+ :internal:
+
+.. kernel-doc:: lib/stringset.c
+ :export:
diff --git a/MAINTAINERS b/MAINTAINERS
index 893b0670a7b6..b977559f24f6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27013,6 +27013,13 @@ S: Maintained
F: Documentation/networking/device_drivers/ethernet/stmicro/
F: drivers/net/ethernet/stmicro/stmmac/
+STRINGSET DATA STRUCTURE
+M: Chuck Lever <cel@kernel.org>
+S: Maintained
+F: Documentation/core-api/stringset.rst
+F: include/linux/stringset.h
+F: lib/stringset.c
+
SUN HAPPY MEAL ETHERNET DRIVER
M: Sean Anderson <seanga2@gmail.com>
S: Maintained
diff --git a/include/linux/stringset.h b/include/linux/stringset.h
new file mode 100644
index 000000000000..fd41ea23f2b1
--- /dev/null
+++ b/include/linux/stringset.h
@@ -0,0 +1,191 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Stringsets
+ *
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Author: Chuck Lever <chuck.lever@oracle.com>
+ *
+ * A stringset is a set of strings. See
+ * Documentation/core-api/stringset.rst for how to use stringsets.
+ *
+ * Stringsets have no internal locking. Callers synchronize writers
+ * against readers, publish the finalized set with release/acquire
+ * ordering, and drain readers before stringset_destroy(). See the
+ * Thread Safety section of Documentation/core-api/stringset.rst.
+ */
+
+#ifndef _LINUX_STRINGSET_H
+#define _LINUX_STRINGSET_H
+
+#include <linux/bug.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+struct stringset {
+ char **ss_strings;
+ unsigned int ss_count;
+ unsigned int ss_capacity;
+ bool ss_finalized;
+};
+
+#define STRINGSET_INIT \
+ { .ss_strings = NULL, .ss_count = 0, .ss_capacity = 0, .ss_finalized = false }
+#define DEFINE_STRINGSET(name) \
+ struct stringset name = STRINGSET_INIT
+
+/**
+ * stringset_for_each - Iterate over items in a stringset
+ * @set: An initialized stringset containing zero or more items
+ * @index: Index counter for the iteration
+ * @str: String retrieved from the stringset
+ */
+#define stringset_for_each(set, index, str) \
+ for ((index) = 0; (index) < (set)->ss_count && \
+ ((str) = (set)->ss_strings[index], true); (index)++)
+
+/**
+ * stringset_init - Initialize an empty stringset
+ * @set: stringset to be initialized
+ */
+static inline void stringset_init(struct stringset *set)
+{
+ set->ss_strings = NULL;
+ set->ss_count = 0;
+ set->ss_capacity = 0;
+ set->ss_finalized = false;
+}
+
+/**
+ * stringset_alloc - Pre-allocate space for strings
+ * @set: An initialized stringset
+ * @capacity: Number of strings to allocate space for
+ * @gfp: Memory allocation flags
+ *
+ * This function allocates only once per stringset. Calling it on a
+ * stringset that is already allocated returns failure. stringset_destroy()
+ * releases the allocation, after which the stringset may be allocated
+ * again.
+ *
+ * @capacity may be zero. The stringset then represents an empty set, and
+ * stringset_add() rejects further additions. The same state is produced
+ * by DEFINE_STRINGSET() and stringset_init() alone, so callers that know
+ * the set is empty may skip stringset_alloc() entirely. An empty set
+ * still goes through stringset_finalize() before stringset_is_member()
+ * or stringset_intersection() can query it. A zero-capacity call allocates
+ * nothing, so a later stringset_alloc() on the same set still succeeds.
+ *
+ * Return:
+ * %true: allocation succeeded (or @capacity was zero)
+ * %false: allocation failed or stringset already allocated
+ */
+static inline __must_check bool
+stringset_alloc(struct stringset *set, unsigned int capacity, gfp_t gfp)
+{
+ if (set->ss_strings)
+ return false;
+ if (capacity == 0)
+ return true;
+
+ set->ss_strings = kcalloc(capacity, sizeof(char *), gfp);
+ if (!set->ss_strings)
+ return false;
+ set->ss_capacity = capacity;
+ return true;
+}
+
+/**
+ * stringset_is_empty - Determine if a stringset contains any strings
+ * @set: An initialized stringset to be checked
+ *
+ * Return:
+ * %true: if @set is empty
+ * %false: if @set contains one or more strings
+ */
+static inline bool stringset_is_empty(const struct stringset *set)
+{
+ return set->ss_count == 0;
+}
+
+/**
+ * stringset_count - Return the number of strings in a stringset
+ * @set: An initialized stringset to be checked
+ *
+ * If called before stringset_finalize(), returns the number of strings
+ * added. If called after, returns the number of unique strings.
+ *
+ * Return:
+ * The number of strings in @set
+ */
+static inline unsigned int stringset_count(const struct stringset *set)
+{
+ return set->ss_count;
+}
+
+/**
+ * stringset_add - Add a string to a stringset
+ * @set: An initialized stringset with available capacity
+ * @str: non-NULL string to be added to @set, in kmalloc'd memory
+ *
+ * On success, @str is now owned by @set and will be freed either
+ * by stringset_finalize() (if a content duplicate) or stringset_destroy().
+ * The stringset must have been allocated with sufficient capacity via
+ * stringset_alloc(). Strings must not be added after stringset_finalize()
+ * has been called. Callers must not hand the same pointer to stringset_add()
+ * more than once: ownership has already transferred, and a second add
+ * produces a double-free at stringset_destroy().
+ *
+ * Return:
+ * %true: @str is now a member of @set
+ * %false: @str could not be added (NULL, no capacity, or @set is
+ * already finalized)
+ */
+static inline __must_check bool
+stringset_add(struct stringset *set, char *str)
+{
+ if (WARN_ON_ONCE(set->ss_finalized))
+ return false;
+ if (!str || set->ss_count >= set->ss_capacity)
+ return false;
+ set->ss_strings[set->ss_count++] = str;
+ return true;
+}
+
+/**
+ * stringset_add_dup - Add a copy of a string to a stringset
+ * @set: An initialized stringset with available capacity
+ * @str: non-NULL string to be copied and added to @set
+ * @gfp: Memory allocation flags
+ *
+ * On success, @str will have been copied into a kmalloc'd
+ * buffer. The caller can release @str immediately. Strings must not
+ * be added after stringset_finalize() has been called.
+ *
+ * Return:
+ * %true: @str is now a member of @set
+ * %false: @str could not be added (NULL, no capacity, ENOMEM, or
+ * @set is already finalized)
+ */
+static inline __must_check bool
+stringset_add_dup(struct stringset *set, const char *str, gfp_t gfp)
+{
+ char *entry;
+
+ if (WARN_ON_ONCE(set->ss_finalized))
+ return false;
+ if (!str || set->ss_count >= set->ss_capacity)
+ return false;
+ entry = kstrdup(str, gfp);
+ if (!entry)
+ return false;
+ set->ss_strings[set->ss_count++] = entry;
+ return true;
+}
+
+/* Implemented in lib/stringset.c */
+void stringset_finalize(struct stringset *set);
+void stringset_destroy(struct stringset *set);
+bool stringset_is_member(const struct stringset *set, const char *str);
+bool stringset_copy(struct stringset *dest, const struct stringset *src, gfp_t gfp);
+bool stringset_intersection(const struct stringset *set1, const struct stringset *set2);
+
+#endif /* _LINUX_STRINGSET_H */
diff --git a/lib/Kconfig b/lib/Kconfig
index 1e42f5167c68..c8ba3f7e13d8 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -363,6 +363,18 @@ config ASSOCIATIVE_ARRAY
for more information.
+config STRINGSET
+ bool
+ help
+ A sorted set of strings, built once and then queried for
+ membership and intersection.
+
+ See:
+
+ Documentation/core-api/stringset.rst
+
+ for more information.
+
config CLOSURES
bool
diff --git a/lib/Makefile b/lib/Makefile
index a2afe954805c..cbed3e2e7cd3 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -61,6 +61,7 @@ obj-y += bcd.o sort.o parser.o debug_locks.o random32.o \
generic-radix-tree.o bitmap-str.o
obj-y += string_helpers.o
obj-y += hexdump.o
+obj-$(CONFIG_STRINGSET) += stringset.o
obj-$(CONFIG_TEST_HEXDUMP) += test_hexdump.o
obj-y += kstrtox.o
obj-$(CONFIG_FIND_BIT_BENCHMARK) += find_bit_benchmark.o
diff --git a/lib/stringset.c b/lib/stringset.c
new file mode 100644
index 000000000000..2f77b048ffc5
--- /dev/null
+++ b/lib/stringset.c
@@ -0,0 +1,190 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Stringsets - a sorted set of strings
+ *
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Author: Chuck Lever <chuck.lever@oracle.com>
+ */
+
+#include <linux/bug.h>
+#include <linux/export.h>
+#include <linux/sort.h>
+#include <linux/string.h>
+#include <linux/stringset.h>
+
+static int stringset_cmp(const void *a, const void *b)
+{
+ const char * const *sa = a;
+ const char * const *sb = b;
+
+ return strcmp(*sa, *sb);
+}
+
+/**
+ * stringset_finalize - Sort the stringset and remove duplicates
+ * @set: An initialized stringset that has been populated
+ *
+ * This must be called after all strings have been added and before
+ * calling stringset_is_member(), stringset_intersection(), or
+ * stringset_copy(). Duplicate strings are removed and their memory
+ * is freed.
+ */
+void stringset_finalize(struct stringset *set)
+{
+ unsigned int i, j;
+
+ if (WARN_ON_ONCE(set->ss_finalized))
+ return;
+ if (set->ss_count > 1) {
+ sort(set->ss_strings, set->ss_count, sizeof(char *),
+ stringset_cmp, NULL);
+
+ for (i = 0, j = 1; j < set->ss_count; j++) {
+ if (strcmp(set->ss_strings[i], set->ss_strings[j]) == 0)
+ kfree(set->ss_strings[j]);
+ else
+ set->ss_strings[++i] = set->ss_strings[j];
+ }
+ set->ss_count = i + 1;
+ }
+ set->ss_finalized = true;
+}
+EXPORT_SYMBOL_GPL(stringset_finalize);
+
+/**
+ * stringset_destroy - Release stringset resources
+ * @set: stringset to be destroyed
+ */
+void stringset_destroy(struct stringset *set)
+{
+ unsigned int i;
+
+ for (i = 0; i < set->ss_count; i++)
+ kfree(set->ss_strings[i]);
+ kfree(set->ss_strings);
+ stringset_init(set);
+}
+EXPORT_SYMBOL_GPL(stringset_destroy);
+
+/**
+ * stringset_is_member - Check if a string is already a member of a stringset
+ * @set: An initialized and finalized stringset to be checked
+ * @str: string to search for
+ *
+ * Uses binary search. The stringset must have been finalized first;
+ * a query on an unfinalized stringset warns and returns %false.
+ *
+ * Return:
+ * %true: if @str is a member of @set
+ * %false: if @str is not a member of @set, or @set is not finalized
+ */
+bool stringset_is_member(const struct stringset *set, const char *str)
+{
+ unsigned int low = 0, high = set->ss_count;
+
+ if (WARN_ON_ONCE(!set->ss_finalized))
+ return false;
+ /*
+ * Open-coded rather than bsearch() to call strcmp() directly,
+ * avoiding an indirect comparator call per search step.
+ */
+ while (low < high) {
+ unsigned int mid = low + (high - low) / 2;
+ int cmp = strcmp(str, set->ss_strings[mid]);
+
+ if (cmp == 0)
+ return true;
+ if (cmp < 0)
+ high = mid;
+ else
+ low = mid + 1;
+ }
+ return false;
+}
+EXPORT_SYMBOL_GPL(stringset_is_member);
+
+/**
+ * stringset_copy - Duplicate strings to another stringset
+ * @dest: An unallocated, initialized stringset to be filled
+ * @src: An initialized, finalized stringset to be copied from
+ * @gfp: Memory allocation flags
+ *
+ * @dest must be initialized -- via STRINGSET_INIT, DEFINE_STRINGSET,
+ * stringset_init(), or a prior stringset_destroy() -- and must not yet
+ * have storage allocated. An already-allocated @dest is a caller error:
+ * the copy warns, frees @dest's storage, and returns failure.
+ *
+ * @src must be finalized: a copy reproduces @src's sorted, queryable
+ * state, so copying an unfinalized stringset warns and returns failure.
+ *
+ * On success @dest is populated and ready for use; no call to
+ * stringset_finalize() is required. On failure @dest is left as an
+ * empty, finalized stringset, so callers can safely run queries
+ * against it without first checking the return value.
+ *
+ * Return:
+ * %true: All strings in @src were copied to @dest
+ * %false: A failure occurred; @dest is empty and finalized
+ */
+bool stringset_copy(struct stringset *dest, const struct stringset *src, gfp_t gfp)
+{
+ unsigned int i;
+
+ if (WARN_ON_ONCE(dest->ss_strings))
+ goto out_fail;
+ if (WARN_ON_ONCE(!src->ss_finalized))
+ goto out_fail;
+ if (src->ss_count && !stringset_alloc(dest, src->ss_count, gfp))
+ goto out_fail;
+ for (i = 0; i < src->ss_count; i++) {
+ char *entry = kstrdup(src->ss_strings[i], gfp);
+
+ if (!entry)
+ goto out_fail;
+ dest->ss_strings[dest->ss_count++] = entry;
+ }
+ /* src is finalized and thus sorted, so dest is too */
+ dest->ss_finalized = true;
+ return true;
+
+out_fail:
+ stringset_destroy(dest);
+ dest->ss_finalized = true;
+ return false;
+}
+EXPORT_SYMBOL_GPL(stringset_copy);
+
+/**
+ * stringset_intersection - Report if there are common strings
+ * @set1: An initialized and finalized stringset
+ * @set2: Another initialized and finalized stringset
+ *
+ * Uses merge-style comparison of two sorted arrays for O(N+M)
+ * complexity. Both stringsets must have been finalized first; a
+ * query on an unfinalized stringset warns and returns %false.
+ *
+ * Return:
+ * %true: @set1 and @set2 have at least one common string
+ * %false: @set1 and @set2 have no strings in common, or either
+ * stringset is not finalized
+ */
+bool stringset_intersection(const struct stringset *set1, const struct stringset *set2)
+{
+ unsigned int i = 0, j = 0;
+
+ if (WARN_ON_ONCE(!set1->ss_finalized) ||
+ WARN_ON_ONCE(!set2->ss_finalized))
+ return false;
+ while (i < set1->ss_count && j < set2->ss_count) {
+ int cmp = strcmp(set1->ss_strings[i], set2->ss_strings[j]);
+
+ if (cmp == 0)
+ return true;
+ if (cmp < 0)
+ i++;
+ else
+ j++;
+ }
+ return false;
+}
+EXPORT_SYMBOL_GPL(stringset_intersection);
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 03/10] lib: Add KUnit tests for the stringset data structure
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 01/10] handshake: Add tags to "done" downcall Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 02/10] lib: Add a "stringset" data structure Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 04/10] handshake: Add a session-tags argument to the TLS done callback Chuck Lever
` (6 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
The stringset data structure carries invariants that are easy to
regress silently: stringset_add() transfers ownership of the passed
pointer, stringset_finalize() both sorts and drops duplicate
strings, queries are valid only after finalization, and
stringset_copy() must produce a set that survives destruction of
its source. None of these are exercised today.
Add a KUnit suite that drives the supported usage pattern and
checks the observable results: capacity bounds and the once-only
allocation rule, the post-finalize sort and dedup, binary-search
membership at the array boundaries and in gaps, intersection over
overlapping, disjoint, and empty sets, and copy independence.
Built with KASAN, the dedup and copy cases become use-after-free
detectors. Under kmemleak, the ownership case catches a transfer
that never happened.
The misuse paths guarded by WARN_ON_ONCE() are intentionally left
out: tripping them taints the kernel, and KUnit has no facility to
mark a warning as expected.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
MAINTAINERS | 1 +
lib/Kconfig.debug | 6 +
lib/tests/Makefile | 1 +
lib/tests/stringset_kunit.c | 308 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 316 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index b977559f24f6..fecbbc7b3d60 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27019,6 +27019,7 @@ S: Maintained
F: Documentation/core-api/stringset.rst
F: include/linux/stringset.h
F: lib/stringset.c
+F: lib/tests/stringset_kunit.c
SUN HAPPY MEAL ETHERNET DRIVER
M: Sean Anderson <seanga2@gmail.com>
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 2d22638ec13a..4ae7922d90ef 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -2531,6 +2531,12 @@ config STRING_HELPERS_KUNIT_TEST
depends on KUNIT
default KUNIT_ALL_TESTS
+config STRINGSET_KUNIT_TEST
+ tristate "KUnit test the stringset data structure at runtime" if !KUNIT_ALL_TESTS
+ depends on KUNIT
+ select STRINGSET
+ default KUNIT_ALL_TESTS
+
config STRING_KUNIT_BENCH
bool "Benchmark string functions at runtime"
depends on STRING_KUNIT_TEST
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index df0537e1b91f..aabc70c40840 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -54,6 +54,7 @@ CFLAGS_stackinit_kunit.o += $(call cc-disable-warning, switch-unreachable)
obj-$(CONFIG_STACKINIT_KUNIT_TEST) += stackinit_kunit.o
obj-$(CONFIG_STRING_KUNIT_TEST) += string_kunit.o
obj-$(CONFIG_STRING_HELPERS_KUNIT_TEST) += string_helpers_kunit.o
+obj-$(CONFIG_STRINGSET_KUNIT_TEST) += stringset_kunit.o
obj-$(CONFIG_USERCOPY_KUNIT_TEST) += usercopy_kunit.o
obj-$(CONFIG_UTIL_MACROS_KUNIT) += util_macros_kunit.o
obj-$(CONFIG_RATELIMIT_KUNIT_TEST) += test_ratelimit.o
diff --git a/lib/tests/stringset_kunit.c b/lib/tests/stringset_kunit.c
new file mode 100644
index 000000000000..2a50a5c634d8
--- /dev/null
+++ b/lib/tests/stringset_kunit.c
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the stringset data structure.
+ *
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ */
+
+#include <kunit/resource.h>
+#include <kunit/test.h>
+#include <linux/stringset.h>
+
+KUNIT_DEFINE_ACTION_WRAPPER(stringset_destroy_action, stringset_destroy,
+ struct stringset *);
+
+/*
+ * Cleanup actions run after a failed assertion has ended the test
+ * thread, so the set cannot live on that thread's stack.
+ */
+static struct stringset *new_set(struct kunit *test)
+{
+ struct stringset *set;
+ int err;
+
+ set = kunit_kzalloc(test, sizeof(*set), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, set);
+ stringset_init(set);
+ err = kunit_add_action_or_reset(test, stringset_destroy_action, set);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ return set;
+}
+
+/* Build a populated (but not yet finalized) set from an array of literals. */
+static struct stringset *build_set(struct kunit *test,
+ const char * const *strs, unsigned int n)
+{
+ struct stringset *set = new_set(test);
+ unsigned int i;
+
+ KUNIT_ASSERT_TRUE(test, stringset_alloc(set, n, GFP_KERNEL));
+ for (i = 0; i < n; i++)
+ KUNIT_ASSERT_TRUE(test,
+ stringset_add_dup(set, strs[i], GFP_KERNEL));
+ return set;
+}
+
+static void stringset_init_empty(struct kunit *test)
+{
+ DEFINE_STRINGSET(set);
+
+ KUNIT_EXPECT_TRUE(test, stringset_is_empty(&set));
+ KUNIT_EXPECT_EQ(test, stringset_count(&set), 0u);
+ stringset_destroy(&set);
+}
+
+static void stringset_alloc_zero_is_empty_set(struct kunit *test)
+{
+ DEFINE_STRINGSET(set);
+
+ KUNIT_EXPECT_TRUE(test, stringset_alloc(&set, 0, GFP_KERNEL));
+ stringset_finalize(&set);
+ KUNIT_EXPECT_FALSE(test, stringset_is_member(&set, "anything"));
+ KUNIT_EXPECT_EQ(test, stringset_count(&set), 0u);
+ stringset_destroy(&set);
+}
+
+static void stringset_alloc_twice_fails(struct kunit *test)
+{
+ DEFINE_STRINGSET(set);
+
+ KUNIT_EXPECT_TRUE(test, stringset_alloc(&set, 4, GFP_KERNEL));
+ KUNIT_EXPECT_FALSE(test, stringset_alloc(&set, 4, GFP_KERNEL));
+ stringset_destroy(&set);
+}
+
+static void stringset_realloc_after_destroy(struct kunit *test)
+{
+ const char * const in[] = { "a", "b" };
+ struct stringset *set;
+
+ set = build_set(test, in, ARRAY_SIZE(in));
+ stringset_finalize(set);
+ stringset_destroy(set);
+ KUNIT_EXPECT_TRUE(test, stringset_alloc(set, 2, GFP_KERNEL));
+}
+
+static void stringset_add_capacity_exhausted(struct kunit *test)
+{
+ struct stringset *set = new_set(test);
+ char *overflow;
+
+ KUNIT_ASSERT_TRUE(test, stringset_alloc(set, 1, GFP_KERNEL));
+ KUNIT_EXPECT_TRUE(test, stringset_add_dup(set, "a", GFP_KERNEL));
+ KUNIT_EXPECT_FALSE(test, stringset_add_dup(set, "b", GFP_KERNEL));
+
+ /* The failed add does not transfer ownership, so free the string. */
+ overflow = kstrdup("b", GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, overflow);
+ KUNIT_EXPECT_FALSE(test, stringset_add(set, overflow));
+ kfree(overflow);
+
+ KUNIT_EXPECT_EQ(test, stringset_count(set), 1u);
+}
+
+static void stringset_add_null_rejected(struct kunit *test)
+{
+ DEFINE_STRINGSET(set);
+
+ KUNIT_ASSERT_TRUE(test, stringset_alloc(&set, 1, GFP_KERNEL));
+ KUNIT_EXPECT_FALSE(test, stringset_add(&set, NULL));
+ KUNIT_EXPECT_FALSE(test, stringset_add_dup(&set, NULL, GFP_KERNEL));
+ stringset_destroy(&set);
+}
+
+/*
+ * stringset_add takes ownership; destroy must free it. A missed
+ * transfer leaks the string, which leak checkers (kmemleak) catch.
+ */
+static void stringset_add_takes_ownership(struct kunit *test)
+{
+ struct stringset *set = new_set(test);
+ char *str;
+
+ KUNIT_ASSERT_TRUE(test, stringset_alloc(set, 1, GFP_KERNEL));
+ str = kstrdup("owned", GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, str);
+ KUNIT_EXPECT_TRUE(test, stringset_add(set, str));
+ stringset_finalize(set);
+}
+
+static void stringset_add_is_queryable(struct kunit *test)
+{
+ struct stringset *set = new_set(test);
+ char *str;
+
+ KUNIT_ASSERT_TRUE(test, stringset_alloc(set, 1, GFP_KERNEL));
+ str = kstrdup("owned", GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, str);
+ KUNIT_EXPECT_TRUE(test, stringset_add(set, str));
+ KUNIT_EXPECT_FALSE(test, stringset_is_empty(set));
+ stringset_finalize(set);
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(set, "owned"));
+}
+
+static void stringset_finalize_dedups(struct kunit *test)
+{
+ const char * const in[] = { "a", "b", "a", "b" };
+ struct stringset *set;
+
+ set = build_set(test, in, ARRAY_SIZE(in));
+ KUNIT_EXPECT_EQ(test, stringset_count(set), 4u);
+ stringset_finalize(set);
+ KUNIT_EXPECT_EQ(test, stringset_count(set), 2u);
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(set, "a"));
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(set, "b"));
+}
+
+/*
+ * Callers must not depend on iteration order, but the sort is what
+ * makes binary-search membership work, so check it directly here.
+ */
+static void stringset_finalize_sorts(struct kunit *test)
+{
+ const char * const in[] = { "charlie", "alpha", "bravo" };
+ const char *prev = NULL, *str;
+ struct stringset *set;
+ unsigned int i;
+
+ set = build_set(test, in, ARRAY_SIZE(in));
+ stringset_finalize(set);
+ stringset_for_each(set, i, str) {
+ if (prev)
+ KUNIT_EXPECT_LT(test, strcmp(prev, str), 0);
+ prev = str;
+ }
+}
+
+static void stringset_is_member_present_and_absent(struct kunit *test)
+{
+ const char * const in[] = { "b", "d", "f", "h" };
+ struct stringset *set;
+
+ set = build_set(test, in, ARRAY_SIZE(in));
+ stringset_finalize(set);
+ /* present: first, middle, last */
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(set, "b"));
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(set, "f"));
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(set, "h"));
+ /* absent: before first, in a gap, after last */
+ KUNIT_EXPECT_FALSE(test, stringset_is_member(set, "a"));
+ KUNIT_EXPECT_FALSE(test, stringset_is_member(set, "e"));
+ KUNIT_EXPECT_FALSE(test, stringset_is_member(set, "z"));
+}
+
+static void stringset_intersection_overlap(struct kunit *test)
+{
+ const char * const a[] = { "a", "c", "e" };
+ const char * const b[] = { "b", "c", "d" };
+ struct stringset *s1, *s2;
+
+ s1 = build_set(test, a, ARRAY_SIZE(a));
+ s2 = build_set(test, b, ARRAY_SIZE(b));
+ stringset_finalize(s1);
+ stringset_finalize(s2);
+ KUNIT_EXPECT_TRUE(test, stringset_intersection(s1, s2));
+}
+
+static void stringset_intersection_disjoint(struct kunit *test)
+{
+ const char * const a[] = { "a", "b", "c" };
+ const char * const b[] = { "x", "y", "z" };
+ struct stringset *s1, *s2;
+
+ s1 = build_set(test, a, ARRAY_SIZE(a));
+ s2 = build_set(test, b, ARRAY_SIZE(b));
+ stringset_finalize(s1);
+ stringset_finalize(s2);
+ KUNIT_EXPECT_FALSE(test, stringset_intersection(s1, s2));
+}
+
+static void stringset_intersection_with_empty(struct kunit *test)
+{
+ const char * const a[] = { "a", "b" };
+ struct stringset *s1;
+ DEFINE_STRINGSET(empty);
+
+ s1 = build_set(test, a, ARRAY_SIZE(a));
+ stringset_finalize(s1);
+ stringset_finalize(&empty);
+ KUNIT_EXPECT_FALSE(test, stringset_intersection(s1, &empty));
+ stringset_destroy(&empty);
+}
+
+/* Copy is deep: destroying the source must not disturb the copy. */
+static void stringset_copy_is_independent(struct kunit *test)
+{
+ const char * const in[] = { "one", "two", "three" };
+ struct stringset *src, *dst;
+
+ src = build_set(test, in, ARRAY_SIZE(in));
+ stringset_finalize(src);
+ dst = new_set(test);
+ KUNIT_ASSERT_TRUE(test, stringset_copy(dst, src, GFP_KERNEL));
+ KUNIT_EXPECT_EQ(test, stringset_count(dst), 3u);
+
+ stringset_destroy(src);
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(dst, "one"));
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(dst, "three"));
+}
+
+static void stringset_copy_empty_src(struct kunit *test)
+{
+ DEFINE_STRINGSET(src);
+ DEFINE_STRINGSET(dst);
+
+ stringset_finalize(&src);
+ KUNIT_EXPECT_TRUE(test, stringset_copy(&dst, &src, GFP_KERNEL));
+ KUNIT_EXPECT_EQ(test, stringset_count(&dst), 0u);
+ /* dst is finalized, so a query is well-defined and returns false */
+ KUNIT_EXPECT_FALSE(test, stringset_is_member(&dst, "x"));
+ stringset_destroy(&dst);
+ stringset_destroy(&src);
+}
+
+static void stringset_iteration_visits_all(struct kunit *test)
+{
+ const char * const in[] = { "a", "b", "c", "d" };
+ unsigned int i, seen = 0;
+ struct stringset *set;
+ char *str;
+
+ set = build_set(test, in, ARRAY_SIZE(in));
+ stringset_finalize(set);
+ stringset_for_each(set, i, str) {
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(set, str));
+ seen++;
+ }
+ KUNIT_EXPECT_EQ(test, seen, stringset_count(set));
+}
+
+static struct kunit_case stringset_test_cases[] = {
+ KUNIT_CASE(stringset_init_empty),
+ KUNIT_CASE(stringset_alloc_zero_is_empty_set),
+ KUNIT_CASE(stringset_alloc_twice_fails),
+ KUNIT_CASE(stringset_realloc_after_destroy),
+ KUNIT_CASE(stringset_add_capacity_exhausted),
+ KUNIT_CASE(stringset_add_null_rejected),
+ KUNIT_CASE(stringset_add_takes_ownership),
+ KUNIT_CASE(stringset_add_is_queryable),
+ KUNIT_CASE(stringset_finalize_dedups),
+ KUNIT_CASE(stringset_finalize_sorts),
+ KUNIT_CASE(stringset_is_member_present_and_absent),
+ KUNIT_CASE(stringset_intersection_overlap),
+ KUNIT_CASE(stringset_intersection_disjoint),
+ KUNIT_CASE(stringset_intersection_with_empty),
+ KUNIT_CASE(stringset_copy_is_independent),
+ KUNIT_CASE(stringset_copy_empty_src),
+ KUNIT_CASE(stringset_iteration_visits_all),
+ {}
+};
+
+static struct kunit_suite stringset_test_suite = {
+ .name = "stringset",
+ .test_cases = stringset_test_cases,
+};
+kunit_test_suite(stringset_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for the stringset data structure");
+MODULE_LICENSE("GPL");
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 04/10] handshake: Add a session-tags argument to the TLS done callback
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
` (2 preceding siblings ...)
2026-11-12 14:52 ` [PATCH net-next v2 03/10] lib: Add KUnit tests for the stringset " Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 05/10] handshake: Pick up session tags passed during the DONE downcall Chuck Lever
` (5 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
Session tags arriving in the DONE downcall have to reach handshake
consumers for authorization decisions. The completion callback is
the only path to a consumer, so carrying the tags there requires a
new argument on tls_done_func_t. That signature change ripples
through every existing TLS handshake consumer in the tree.
Add the @tags parameter ahead of the functional change. No consumer
reads it yet and the handshake core passes NULL, so behavior is
unchanged. Isolating the mechanical signature churn confines the
tag-collection patch that follows to the handshake core.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
Documentation/networking/tls-handshake.rst | 3 ++-
drivers/nvme/host/tcp.c | 3 ++-
drivers/nvme/target/tcp.c | 3 ++-
include/net/handshake.h | 16 +++++++++++++++-
net/handshake/tlshd.c | 5 +++--
net/sunrpc/svcsock.c | 4 +++-
net/sunrpc/xprtsock.c | 4 +++-
7 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/Documentation/networking/tls-handshake.rst b/Documentation/networking/tls-handshake.rst
index 4f7bc1087df9..7e9f4bd659b4 100644
--- a/Documentation/networking/tls-handshake.rst
+++ b/Documentation/networking/tls-handshake.rst
@@ -169,7 +169,8 @@ The synopsis of this function is:
.. code-block:: c
typedef void (*tls_done_func_t)(void *data, int status,
- key_serial_t peerid);
+ key_serial_t peerid,
+ const struct stringset *tags);
The consumer provides a cookie in the @ta_data field of the
tls_handshake_args structure that is returned in the @data parameter of
diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
index bc46e5f3eaa7..c792ac1c3c6d 100644
--- a/drivers/nvme/host/tcp.c
+++ b/drivers/nvme/host/tcp.c
@@ -1919,7 +1919,8 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
qid, queue->io_cpu);
}
-static void nvme_tcp_tls_done(void *data, int status, key_serial_t pskid)
+static void nvme_tcp_tls_done(void *data, int status, key_serial_t pskid,
+ const struct stringset *tags)
{
struct nvme_tcp_queue *queue = data;
struct nvme_tcp_ctrl *ctrl = queue->ctrl;
diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
index 8092ce4914b6..5064d1f6c28e 100644
--- a/drivers/nvme/target/tcp.c
+++ b/drivers/nvme/target/tcp.c
@@ -1865,7 +1865,8 @@ static int nvmet_tcp_tls_key_lookup(struct nvmet_tcp_queue *queue,
}
static void nvmet_tcp_tls_handshake_done(void *data, int status,
- key_serial_t peerid)
+ key_serial_t peerid,
+ const struct stringset *tags)
{
struct nvmet_tcp_queue *queue = data;
diff --git a/include/net/handshake.h b/include/net/handshake.h
index 8ebd4f9ed26e..c227d42bd767 100644
--- a/include/net/handshake.h
+++ b/include/net/handshake.h
@@ -10,6 +10,8 @@
#ifndef _NET_HANDSHAKE_H
#define _NET_HANDSHAKE_H
+#include <linux/stringset.h>
+
enum {
TLS_NO_KEYRING = 0,
TLS_NO_PEERID = 0,
@@ -17,8 +19,20 @@ enum {
TLS_NO_PRIVKEY = 0,
};
+/**
+ * typedef tls_done_func_t - TLS handshake completion callback
+ * @data: opaque context pointer set via tls_handshake_args.ta_data
+ * @status: zero on success, otherwise a negative errno
+ * @peerid: serial number of peer identity key, or TLS_NO_PEERID
+ * @tags: session tags assigned by the handshake agent
+ *
+ * The @tags pointer is valid only for the duration of this callback.
+ * A consumer requiring persistent access must copy the set with
+ * stringset_copy().
+ */
typedef void (*tls_done_func_t)(void *data, int status,
- key_serial_t peerid);
+ key_serial_t peerid,
+ const struct stringset *tags);
struct tls_handshake_args {
struct socket *ta_sock;
diff --git a/net/handshake/tlshd.c b/net/handshake/tlshd.c
index 7567150c2a4f..997edc3fe31c 100644
--- a/net/handshake/tlshd.c
+++ b/net/handshake/tlshd.c
@@ -26,7 +26,8 @@
struct tls_handshake_req {
void (*th_consumer_done)(void *data, int status,
- key_serial_t peerid);
+ key_serial_t peerid,
+ const struct stringset *tags);
void *th_consumer_data;
int th_type;
@@ -105,7 +106,7 @@ static void tls_handshake_done(struct handshake_req *req,
set_bit(HANDSHAKE_F_REQ_SESSION, &req->hr_flags);
treq->th_consumer_done(treq->th_consumer_data, status,
- treq->th_peerid[0]);
+ treq->th_peerid[0], NULL);
}
#if IS_ENABLED(CONFIG_KEYS)
diff --git a/net/sunrpc/svcsock.c b/net/sunrpc/svcsock.c
index 498c5096eef9..f21522dd51f2 100644
--- a/net/sunrpc/svcsock.c
+++ b/net/sunrpc/svcsock.c
@@ -414,13 +414,15 @@ static void svc_tcp_kill_temp_xprt(struct svc_xprt *xprt)
* @data: address of xprt to wake
* @status: status of handshake
* @peerid: serial number of key containing the remote peer's identity
+ * @tags: session tags assigned by the handshake agent
*
* If a security policy is specified as an export option, we don't
* have a specific export here to check. So we set a "TLS session
* is present" flag on the xprt and let an upper layer enforce local
* security policy.
*/
-static void svc_tcp_handshake_done(void *data, int status, key_serial_t peerid)
+static void svc_tcp_handshake_done(void *data, int status, key_serial_t peerid,
+ const struct stringset *tags)
{
struct svc_xprt *xprt = data;
struct svc_sock *svsk = container_of(xprt, struct svc_sock, sk_xprt);
diff --git a/net/sunrpc/xprtsock.c b/net/sunrpc/xprtsock.c
index 027b37de694b..ade473f1a312 100644
--- a/net/sunrpc/xprtsock.c
+++ b/net/sunrpc/xprtsock.c
@@ -2593,9 +2593,11 @@ static int xs_tcp_tls_finish_connecting(struct rpc_xprt *lower_xprt,
* @data: address of xprt to wake
* @status: status of handshake
* @peerid: serial number of key containing the remote's identity
+ * @tags: session tags assigned by the handshake agent
*
*/
-static void xs_tls_handshake_done(void *data, int status, key_serial_t peerid)
+static void xs_tls_handshake_done(void *data, int status, key_serial_t peerid,
+ const struct stringset *tags)
{
struct rpc_xprt *lower_xprt = data;
struct sock_xprt *lower_transport =
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 05/10] handshake: Pick up session tags passed during the DONE downcall
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
` (3 preceding siblings ...)
2026-11-12 14:52 ` [PATCH net-next v2 04/10] handshake: Add a session-tags argument to the TLS done callback Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 06/10] handshake: Add a kunit test for the completion gate Chuck Lever
` (4 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
Upper Layer Protocols such as NFS have no way to base access control
on TLS certificate characteristics. Parsing x.509 in the kernel
duplicates work user space libraries already do. The tlshd daemon can
evaluate certificates against administrator-defined policies and
assign tags indicating which policies a certificate satisfies.
Collect session tags from the DONE netlink downcall and pass them to
handshake consumers via their completion callback. NFSD, for example,
can then restrict export access to connections whose certificate
earned a particular tag, without parsing the certificate itself. The
tags are valid only while the callback runs. Release them as soon
as the callback returns, so a long-lived transport such as an NFS
connection does not hold them for the life of the socket.
The genl family runs DONE with parallel_ops, so concurrent downcalls
for the same socket can both reach the same handshake_req. Split
handshake_complete() into a try/finish pair. The handler collects
tags into a private stringset before taking the completion gate, and
the winner hands the set to handshake_finish_complete(). A duplicate
downcall is refused with -EBUSY. handshake_req_cancel() returns false
once the gate is taken, and svc_tcp_handshake() then waits
uninterruptibly for its callback. Collecting the tags first keeps the
handler's GFP_KERNEL allocations out of that wait.
The DONE downcall can now fail, where before it always returned
success. A tag the parser rejects completes the handshake with -EIO
and returns the parse error to tlshd. The extended ACK names the
rejected tag attribute. The request is complete on that path, so a
retry collects -EBUSY.
When the count of distinct tags exceeds HANDSHAKE_MAX_SESSIONTAGS, the
handler truncates the list, logs pr_warn_once(), and returns success.
Failing the DONE with -E2BIG would signal the daemon definitively, but
it would also tear down an otherwise usable handshake.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
Documentation/networking/tls-handshake.rst | 51 ++++++++
include/net/handshake.h | 31 ++++-
net/Kconfig | 1 +
net/handshake/handshake-test.c | 197 +++++++++++++++++++++++++++++
net/handshake/handshake.h | 9 ++
net/handshake/netlink.c | 110 +++++++++++++++-
net/handshake/request.c | 59 +++++++--
net/handshake/tlshd.c | 2 +-
8 files changed, 448 insertions(+), 12 deletions(-)
diff --git a/Documentation/networking/tls-handshake.rst b/Documentation/networking/tls-handshake.rst
index 7e9f4bd659b4..eb8cfe3367cb 100644
--- a/Documentation/networking/tls-handshake.rst
+++ b/Documentation/networking/tls-handshake.rst
@@ -201,6 +201,10 @@ The @peerid parameter contains the serial number of a key containing the
remote peer's identity or the value TLS_NO_PEERID if the session is not
authenticated.
+The @tags parameter points to a stringset containing session metadata
+assigned by the handshake agent. See the "Session Tags" section
+below for details on lifetime and safe access patterns.
+
A best practice is to close and destroy the socket immediately if the
handshake failed.
@@ -221,3 +225,50 @@ received message data is TLS record data or session metadata.
See tls.rst for details on how a kTLS consumer recognizes incoming
(decrypted) application data, alerts, and handshake packets once the
socket has been promoted to use the TLS ULP.
+
+
+Session Tags
+============
+
+When a TLS handshake completes successfully, the handshake agent may
+assign metadata tags to the session. These tags enable kernel consumers
+to make authorization decisions based on certificate characteristics
+without parsing x.509 certificates directly.
+
+The handshake agent evaluates the peer's certificate against
+administrator-defined policies and assigns tags indicating which
+policies the certificate satisfies. For example, an administrator
+might configure the agent to assign an "internal-servers" tag when a
+certificate's Issuer DN matches a particular corporate CA.
+
+Tags are delivered to the consumer via the @tags parameter of the
+completion callback. The stringset is valid only for the duration of the
+callback. Consumers needing persistent access must copy the stringset
+using stringset_copy() before returning.
+
+To check whether a session has a particular tag:
+
+.. code-block:: c
+
+ if (stringset_is_member(tags, "internal-servers")) {
+ /* Certificate matched the internal-servers policy */
+ }
+
+To check whether a session has any tag from a small fixed set:
+
+.. code-block:: c
+
+ if (stringset_is_member(tags, "admin") ||
+ stringset_is_member(tags, "operator")) {
+ /* Certificate matched admin or operator policy */
+ }
+
+When the required set is dynamic (e.g., parsed from an export option),
+construct a stringset and use stringset_intersection() to test for any
+overlap. See Documentation/core-api/stringset.rst for the
+initialization, add, and finalize sequence.
+
+If the handshake failed or no tags were assigned, the stringset is
+empty. The handshake layer always delivers a finalized stringset to
+the callback, so consumers may call stringset_is_member() and
+stringset_intersection() unconditionally without a separate guard.
diff --git a/include/net/handshake.h b/include/net/handshake.h
index c227d42bd767..ad96e92af04d 100644
--- a/include/net/handshake.h
+++ b/include/net/handshake.h
@@ -10,8 +10,36 @@
#ifndef _NET_HANDSHAKE_H
#define _NET_HANDSHAKE_H
+#include <linux/errno.h>
#include <linux/stringset.h>
+/*
+ * Bounds the cost of stringset_intersection() in consumer
+ * authorization checks.
+ */
+#define HANDSHAKE_MAX_SESSIONTAGS 64
+
+/**
+ * handshake_session_tag_len - Content length of a session tag payload
+ * @data: start of an NLA_STRING tag payload
+ * @len: payload length in bytes, including any trailing NUL
+ *
+ * A tag payload may arrive with or without a single trailing NUL, and
+ * the terminator is not part of the content. NLA_STRING cannot express
+ * "no NUL except as terminator," so callers validate the payload here.
+ *
+ * Return: content length in bytes, or -EINVAL if the payload is empty
+ * or carries an embedded NUL.
+ */
+static inline int handshake_session_tag_len(const char *data, size_t len)
+{
+ if (len > 0 && data[len - 1] == '\0')
+ len--;
+ if (len == 0 || memchr(data, '\0', len))
+ return -EINVAL;
+ return len;
+}
+
enum {
TLS_NO_KEYRING = 0,
TLS_NO_PEERID = 0,
@@ -24,7 +52,8 @@ enum {
* @data: opaque context pointer set via tls_handshake_args.ta_data
* @status: zero on success, otherwise a negative errno
* @peerid: serial number of peer identity key, or TLS_NO_PEERID
- * @tags: session tags assigned by the handshake agent
+ * @tags: session tags assigned by the handshake agent; never NULL,
+ * always finalized, and empty when no tags were assigned
*
* The @tags pointer is valid only for the duration of this callback.
* A consumer requiring persistent access must copy the set with
diff --git a/net/Kconfig b/net/Kconfig
index 76ab44aa439a..aab2d2f03d4d 100644
--- a/net/Kconfig
+++ b/net/Kconfig
@@ -93,6 +93,7 @@ source "net/xdp/Kconfig"
config NET_HANDSHAKE
bool
depends on SUNRPC || NVME_TARGET_TCP || NVME_TCP
+ select STRINGSET
default y
config NET_HANDSHAKE_KUNIT_TEST
diff --git a/net/handshake/handshake-test.c b/net/handshake/handshake-test.c
index 4b99acd9f9c7..21fccfa94d89 100644
--- a/net/handshake/handshake-test.c
+++ b/net/handshake/handshake-test.c
@@ -12,7 +12,9 @@
#include <net/sock.h>
#include <net/genetlink.h>
+#include <net/netlink.h>
#include <net/netns/generic.h>
+#include <net/handshake.h>
#include <uapi/linux/handshake.h>
#include "handshake.h"
@@ -516,6 +518,173 @@ static void handshake_req_destroy_test1(struct kunit *test)
KUNIT_EXPECT_PTR_EQ(test, handshake_req_destroy_test, req);
}
+static struct sk_buff *handshake_done_msg(struct kunit *test,
+ struct nlmsghdr **nlhp)
+{
+ struct sk_buff *skb;
+
+ skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ *nlhp = nlmsg_put(skb, 0, 0, HANDSHAKE_CMD_DONE, GENL_HDRLEN, 0);
+ KUNIT_ASSERT_NOT_NULL(test, *nlhp);
+ return skb;
+}
+
+/*
+ * handshake_get_sessiontags() leaves the collected set unfinalized,
+ * as the netlink handler does. Cases below that query membership
+ * finalize first, the way handshake_finish_complete() does before
+ * delivery to the consumer.
+ */
+static void handshake_sessiontags_basic(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+
+ skb = handshake_done_msg(test, &nlh);
+ nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "alpha");
+ nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "beta");
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
+ stringset_finalize(&tags);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 2);
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "alpha"));
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "beta"));
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
+static void handshake_sessiontags_none(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+
+ skb = handshake_done_msg(test, &nlh);
+ nla_put_u32(skb, HANDSHAKE_A_DONE_STATUS, 0);
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 0);
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
+static void handshake_sessiontags_trailing_nul(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+
+ /* A payload of 6 carries "gamma" plus its terminator. */
+ skb = handshake_done_msg(test, &nlh);
+ nla_put(skb, HANDSHAKE_A_DONE_TAG, 6, "gamma");
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
+ stringset_finalize(&tags);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 1);
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "gamma"));
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
+static void handshake_sessiontags_embedded_nul(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+
+ skb = handshake_done_msg(test, &nlh);
+ nla_put(skb, HANDSHAKE_A_DONE_TAG, 3, "a\0b");
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), -EINVAL);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 0);
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
+static void handshake_sessiontags_empty(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+
+ skb = handshake_done_msg(test, &nlh);
+ nla_put(skb, HANDSHAKE_A_DONE_TAG, 0, "");
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), -EINVAL);
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
+static void handshake_sessiontags_duplicate(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+
+ skb = handshake_done_msg(test, &nlh);
+ nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "dup");
+ nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "dup");
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 2);
+ stringset_finalize(&tags);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 1);
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "dup"));
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
+static void handshake_sessiontags_truncate(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+ unsigned int i;
+
+ skb = handshake_done_msg(test, &nlh);
+ for (i = 0; i < HANDSHAKE_MAX_SESSIONTAGS + 6; i++) {
+ char tag[16];
+
+ snprintf(tag, sizeof(tag), "tag%u", i);
+ nla_put_string(skb, HANDSHAKE_A_DONE_TAG, tag);
+ }
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), HANDSHAKE_MAX_SESSIONTAGS);
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
static struct kunit_case handshake_api_test_cases[] = {
{
.name = "req_alloc API fuzzing",
@@ -562,6 +731,34 @@ static struct kunit_case handshake_api_test_cases[] = {
.name = "req_destroy works",
.run_case = handshake_req_destroy_test1,
},
+ {
+ .name = "sessiontags basic",
+ .run_case = handshake_sessiontags_basic,
+ },
+ {
+ .name = "sessiontags none present",
+ .run_case = handshake_sessiontags_none,
+ },
+ {
+ .name = "sessiontags trailing NUL",
+ .run_case = handshake_sessiontags_trailing_nul,
+ },
+ {
+ .name = "sessiontags embedded NUL",
+ .run_case = handshake_sessiontags_embedded_nul,
+ },
+ {
+ .name = "sessiontags empty tag",
+ .run_case = handshake_sessiontags_empty,
+ },
+ {
+ .name = "sessiontags duplicate",
+ .run_case = handshake_sessiontags_duplicate,
+ },
+ {
+ .name = "sessiontags truncate at cap",
+ .run_case = handshake_sessiontags_truncate,
+ },
{}
};
diff --git a/net/handshake/handshake.h b/net/handshake/handshake.h
index da61cadd1ad3..11f57d09b511 100644
--- a/net/handshake/handshake.h
+++ b/net/handshake/handshake.h
@@ -10,6 +10,8 @@
#ifndef _INTERNAL_HANDSHAKE_H
#define _INTERNAL_HANDSHAKE_H
+#include <linux/stringset.h>
+
/* Per-net namespace context */
struct handshake_net {
spinlock_t hn_lock; /* protects next 3 fields */
@@ -36,6 +38,7 @@ struct handshake_req {
struct file *hr_file;
struct sock *hr_sk;
void (*hr_odestruct)(struct sock *sk);
+ struct stringset hr_tags;
/* Always the last field */
char hr_priv[];
@@ -77,6 +80,9 @@ int handshake_genl_notify(struct net *net, const struct handshake_proto *proto,
struct nlmsghdr *handshake_genl_put(struct sk_buff *msg,
struct genl_info *info);
struct handshake_net *handshake_pernet(struct net *net);
+#if IS_ENABLED(CONFIG_KUNIT)
+int handshake_get_sessiontags(struct stringset *tags, struct genl_info *info);
+#endif
/* request.c */
struct handshake_req *handshake_req_alloc(const struct handshake_proto *proto,
@@ -88,6 +94,9 @@ struct handshake_req *handshake_req_hash_lookup(struct sock *sk);
struct handshake_req *handshake_req_next(struct handshake_net *hn, int class);
int handshake_req_submit(struct socket *sock, struct handshake_req *req,
gfp_t flags);
+bool handshake_try_complete(struct handshake_req *req);
+void handshake_finish_complete(struct handshake_req *req, int status,
+ struct genl_info *info, struct stringset *tags);
void handshake_complete(struct handshake_req *req, int status,
struct genl_info *info);
bool handshake_req_cancel(struct sock *sk);
diff --git a/net/handshake/netlink.c b/net/handshake/netlink.c
index 73b8314d9010..b03e33f97e60 100644
--- a/net/handshake/netlink.c
+++ b/net/handshake/netlink.c
@@ -16,6 +16,7 @@
#include <net/sock.h>
#include <net/genetlink.h>
+#include <net/handshake.h>
#include <net/netns/generic.h>
#include <kunit/visibility.h>
@@ -130,11 +131,93 @@ int handshake_nl_accept_doit(struct sk_buff *skb, struct genl_info *info)
return err;
}
+static bool handshake_sessiontag_is_dup(const struct stringset *tags,
+ const struct nlattr *nla)
+{
+ unsigned int i;
+ char *tag;
+
+ stringset_for_each(tags, i, tag) {
+ if (!nla_strcmp(nla, tag))
+ return true;
+ }
+ return false;
+}
+
+/**
+ * handshake_get_sessiontags - Collect session tags from a DONE downcall
+ * @tags: initialized stringset to fill, left unfinalized on return
+ * @info: netlink message context for the downcall
+ *
+ * The caller retains ownership of @tags. Tags dropped under memory
+ * pressure, or truncated at HANDSHAKE_MAX_SESSIONTAGS, are not an
+ * error.
+ *
+ * Context: May sleep. Must be called before the completion gate is
+ * taken.
+ *
+ * Return: 0 on success, or a negative errno if a tag payload was
+ * malformed, in which case no tags are collected.
+ */
+VISIBLE_IF_KUNIT int handshake_get_sessiontags(struct stringset *tags,
+ struct genl_info *info)
+{
+ unsigned int count = 0;
+ struct nlattr *nla;
+ int rem;
+
+ nlmsg_for_each_attr_type(nla, HANDSHAKE_A_DONE_TAG, info->nlhdr,
+ GENL_HDRLEN, rem) {
+ if (handshake_session_tag_len(nla_data(nla), nla_len(nla)) < 0) {
+ NL_SET_BAD_ATTR(info->extack, nla);
+ return -EINVAL;
+ }
+ count++;
+ }
+ if (count == 0)
+ return 0;
+ if (count > HANDSHAKE_MAX_SESSIONTAGS)
+ count = HANDSHAKE_MAX_SESSIONTAGS;
+ if (!stringset_alloc(tags, count, GFP_KERNEL)) {
+ pr_warn_once("handshake: dropping session tags under memory pressure\n");
+ return 0;
+ }
+
+ nlmsg_for_each_attr_type(nla, HANDSHAKE_A_DONE_TAG, info->nlhdr,
+ GENL_HDRLEN, rem) {
+ char *tag;
+
+ /* A repeated tag does not count against the cap. */
+ if (handshake_sessiontag_is_dup(tags, nla))
+ continue;
+
+ /*
+ * At most HANDSHAKE_MAX_SESSIONTAGS tags are kept. A
+ * dropped tag can only narrow the consumer's
+ * authorization, never widen it.
+ */
+ if (stringset_count(tags) >= HANDSHAKE_MAX_SESSIONTAGS) {
+ pr_warn_once("handshake: more than %u session tags\n",
+ HANDSHAKE_MAX_SESSIONTAGS);
+ break;
+ }
+
+ tag = nla_strdup(nla, GFP_KERNEL);
+ if (!tag)
+ continue;
+ if (!stringset_add(tags, tag))
+ kfree(tag);
+ }
+ return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(handshake_get_sessiontags);
+
int handshake_nl_done_doit(struct sk_buff *skb, struct genl_info *info)
{
struct net *net = sock_net(skb->sk);
struct handshake_req *req;
struct socket *sock;
+ DEFINE_STRINGSET(tags);
int fd, status, err;
if (GENL_REQ_ATTR_CHECK(info, HANDSHAKE_A_DONE_SOCKFD))
@@ -158,10 +241,33 @@ int handshake_nl_done_doit(struct sk_buff *skb, struct genl_info *info)
status = -EIO;
if (info->attrs[HANDSHAKE_A_DONE_STATUS])
status = -(int)nla_get_u32(info->attrs[HANDSHAKE_A_DONE_STATUS]);
+ err = 0;
+ if (!status) {
+ int ret = handshake_get_sessiontags(&tags, info);
- handshake_complete(req, status, info);
+ /*
+ * A malformed tag casts doubt on the rest of the
+ * agent's tag set, so the consumer sees a failed
+ * handshake.
+ */
+ if (ret < 0) {
+ err = ret;
+ trace_handshake_cmd_done_err(net, req, sock->sk, err);
+ status = -EIO;
+ }
+ }
+
+ if (!handshake_try_complete(req)) {
+ trace_handshake_cmd_done_err(net, req, sock->sk, -EBUSY);
+ stringset_destroy(&tags);
+ sockfd_put(sock);
+ return -EBUSY;
+ }
+
+ /* handshake_finish_complete() takes ownership, so no destroy here. */
+ handshake_finish_complete(req, status, info, &tags);
sockfd_put(sock);
- return 0;
+ return err;
}
static unsigned int handshake_net_id;
diff --git a/net/handshake/request.c b/net/handshake/request.c
index ff739ee81cb8..72515e5e8cc2 100644
--- a/net/handshake/request.c
+++ b/net/handshake/request.c
@@ -80,6 +80,7 @@ static void handshake_req_destroy(struct handshake_req *req)
req->hr_proto->hp_destroy(req);
rhashtable_remove_fast(&handshake_rhashtbl, &req->hr_rhash,
handshake_rhash_params);
+ stringset_destroy(&req->hr_tags);
kfree(req);
}
@@ -125,6 +126,7 @@ struct handshake_req *handshake_req_alloc(const struct handshake_proto *proto,
return NULL;
INIT_LIST_HEAD(&req->hr_list);
+ stringset_init(&req->hr_tags);
req->hr_proto = proto;
return req;
}
@@ -321,20 +323,61 @@ int handshake_req_submit(struct socket *sock, struct handshake_req *req,
}
EXPORT_SYMBOL(handshake_req_submit);
-void handshake_complete(struct handshake_req *req, int status,
- struct genl_info *info)
+/**
+ * handshake_try_complete - Take the unique-completer gate
+ * @req: handshake request to complete
+ *
+ * Exactly one caller wins the gate and drives completion to the
+ * consumer. handshake_req_cancel() returns %false once the gate is
+ * taken, so a winner that sleeps first lets a concurrent cancel free
+ * callback data while the consumer callback is still pending.
+ *
+ * Context: The winner must reach handshake_finish_complete() without
+ * sleeping.
+ * Return: %true if the caller won the gate, %false otherwise.
+ */
+bool handshake_try_complete(struct handshake_req *req)
+{
+ return !test_and_set_bit(HANDSHAKE_F_REQ_COMPLETED, &req->hr_flags);
+}
+
+/**
+ * handshake_finish_complete - Deliver completion to the consumer
+ * @req: handshake request to complete
+ * @status: 0, or the negative errno to report to the consumer
+ * @info: netlink message context, or NULL if not from a downcall
+ * @tags: session tags to publish, or NULL; ownership passes to @req
+ *
+ * Context: The caller has won the gate via handshake_try_complete()
+ * and has not slept since.
+ */
+void handshake_finish_complete(struct handshake_req *req, int status,
+ struct genl_info *info, struct stringset *tags)
{
struct sock *sk = req->hr_sk;
struct net *net = sock_net(sk);
+ struct file *file = req->hr_file;
- if (!test_and_set_bit(HANDSHAKE_F_REQ_COMPLETED, &req->hr_flags)) {
- struct file *file = req->hr_file;
+ if (tags)
+ req->hr_tags = *tags;
- trace_handshake_complete(net, req, sk, status);
- req->hr_proto->hp_done(req, status, info);
+ trace_handshake_complete(net, req, sk, status);
+ /*
+ * Consumers may query hr_tags on any completion path, and
+ * stringset_is_member() WARNs on an unfinalized set.
+ */
+ stringset_finalize(&req->hr_tags);
+ req->hr_proto->hp_done(req, status, info);
+ stringset_destroy(&req->hr_tags);
- fput(file);
- }
+ fput(file);
+}
+
+void handshake_complete(struct handshake_req *req, int status,
+ struct genl_info *info)
+{
+ if (handshake_try_complete(req))
+ handshake_finish_complete(req, status, info, NULL);
}
EXPORT_SYMBOL_IF_KUNIT(handshake_complete);
diff --git a/net/handshake/tlshd.c b/net/handshake/tlshd.c
index 997edc3fe31c..a9c5e5bedced 100644
--- a/net/handshake/tlshd.c
+++ b/net/handshake/tlshd.c
@@ -106,7 +106,7 @@ static void tls_handshake_done(struct handshake_req *req,
set_bit(HANDSHAKE_F_REQ_SESSION, &req->hr_flags);
treq->th_consumer_done(treq->th_consumer_data, status,
- treq->th_peerid[0], NULL);
+ treq->th_peerid[0], &req->hr_tags);
}
#if IS_ENABLED(CONFIG_KEYS)
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 06/10] handshake: Add a kunit test for the completion gate
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
` (4 preceding siblings ...)
2026-11-12 14:52 ` [PATCH net-next v2 05/10] handshake: Pick up session tags passed during the DONE downcall Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-13 14:52 ` sashiko-bot
2026-11-12 14:52 ` [PATCH net-next v2 07/10] handshake: Advertise the session-tag cap to user space Chuck Lever
` (3 subsequent siblings)
9 siblings, 1 reply; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
The genl family runs DONE with parallel_ops, so duplicate or
concurrent DONE downcalls for the same socket can both reach the
same handshake_req. The split try/finish pair guarantees that
exactly one caller drives completion to the consumer's hp_done
callback. Ownership of the collected session tags can then pass to
the request.
Exercise the gate's exclusivity with a kunit case: only the first
handshake_try_complete() wins, and a later handshake_complete() on
the same request leaves hp_done fired exactly once.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/handshake/handshake-test.c | 112 ++++++++++++++++++++++++++++++++++++++++-
net/handshake/request.c | 2 +
2 files changed, 113 insertions(+), 1 deletion(-)
diff --git a/net/handshake/handshake-test.c b/net/handshake/handshake-test.c
index 21fccfa94d89..b6992d1fa4ce 100644
--- a/net/handshake/handshake-test.c
+++ b/net/handshake/handshake-test.c
@@ -465,6 +465,84 @@ static void handshake_req_cancel_test3(struct kunit *test)
fput(filp);
}
+static int handshake_try_complete_done_count;
+
+static void test_done_count_func(struct handshake_req *req, int status,
+ struct genl_info *info)
+{
+ handshake_try_complete_done_count++;
+}
+
+static struct handshake_proto handshake_req_alloc_proto_count = {
+ .hp_handler_class = HANDSHAKE_HANDLER_CLASS_TLSHD,
+ .hp_accept = test_accept_func,
+ .hp_done = test_done_count_func,
+};
+
+static void handshake_try_complete_test1(struct kunit *test)
+{
+ unsigned long fcount_before;
+ struct handshake_req *req, *next;
+ struct handshake_net *hn;
+ struct socket *sock;
+ struct file *filp;
+ struct net *net;
+ bool first, second;
+ int err;
+
+ /* Arrange */
+ handshake_try_complete_done_count = 0;
+
+ req = handshake_req_alloc(&handshake_req_alloc_proto_count, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, req);
+
+ err = __sock_create(&init_net, PF_INET, SOCK_STREAM, IPPROTO_TCP,
+ &sock, 1);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ filp = sock_alloc_file(sock, O_NONBLOCK, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, filp);
+ KUNIT_ASSERT_NOT_NULL(test, sock->sk);
+ sock->file = filp;
+
+ fcount_before = file_count(filp);
+ err = handshake_req_submit(sock, req, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, file_count(filp), fcount_before + 1);
+
+ net = sock_net(sock->sk);
+ hn = handshake_pernet(net);
+ KUNIT_ASSERT_NOT_NULL(test, hn);
+
+ /* Pretend to accept this request */
+ next = handshake_req_next(hn, HANDSHAKE_HANDLER_CLASS_TLSHD);
+ KUNIT_ASSERT_PTR_EQ(test, req, next);
+ /* Simulate FD_PREPARE() consuming the file reference handed
+ * off by handshake_req_next(); see handshake_nl_accept_doit().
+ */
+ fput(filp);
+
+ /* Act */
+ first = handshake_try_complete(req);
+ second = handshake_try_complete(req);
+ handshake_finish_complete(req, 0, NULL, NULL);
+ /* handshake_finish_complete() drops the request's pin on the file. */
+ KUNIT_EXPECT_EQ(test, file_count(filp), fcount_before);
+ /* handshake_complete() re-enters the gate via
+ * handshake_try_complete(). With the gate already taken,
+ * hp_done must not fire a second time.
+ */
+ handshake_complete(req, 0, NULL);
+
+ /* Assert */
+ KUNIT_EXPECT_TRUE(test, first);
+ KUNIT_EXPECT_FALSE(test, second);
+ KUNIT_EXPECT_EQ(test, handshake_try_complete_done_count, 1);
+ KUNIT_EXPECT_EQ(test, file_count(filp), fcount_before);
+
+ fput(filp);
+}
+
static struct handshake_req *handshake_req_destroy_test;
static void test_destroy_func(struct handshake_req *req)
@@ -651,7 +729,7 @@ static void handshake_sessiontags_duplicate(struct kunit *test)
info.nlhdr = nlh;
KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
- KUNIT_EXPECT_EQ(test, stringset_count(&tags), 2);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 1);
stringset_finalize(&tags);
KUNIT_EXPECT_EQ(test, stringset_count(&tags), 1);
KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "dup"));
@@ -660,6 +738,30 @@ static void handshake_sessiontags_duplicate(struct kunit *test)
kfree_skb(skb);
}
+static void handshake_sessiontags_duplicate_at_cap(struct kunit *test)
+{
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ DEFINE_STRINGSET(tags);
+ unsigned int i;
+
+ skb = handshake_done_msg(test, &nlh);
+ for (i = 0; i < HANDSHAKE_MAX_SESSIONTAGS; i++)
+ nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "dup");
+ nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "last");
+ nlmsg_end(skb, nlh);
+ info.nlhdr = nlh;
+
+ KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
+ KUNIT_EXPECT_EQ(test, stringset_count(&tags), 2);
+ stringset_finalize(&tags);
+ KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "last"));
+
+ stringset_destroy(&tags);
+ kfree_skb(skb);
+}
+
static void handshake_sessiontags_truncate(struct kunit *test)
{
struct genl_info info = {};
@@ -727,6 +829,10 @@ static struct kunit_case handshake_api_test_cases[] = {
.name = "req_cancel after done",
.run_case = handshake_req_cancel_test3,
},
+ {
+ .name = "try_complete gate is exclusive",
+ .run_case = handshake_try_complete_test1,
+ },
{
.name = "req_destroy works",
.run_case = handshake_req_destroy_test1,
@@ -755,6 +861,10 @@ static struct kunit_case handshake_api_test_cases[] = {
.name = "sessiontags duplicate",
.run_case = handshake_sessiontags_duplicate,
},
+ {
+ .name = "sessiontags duplicates at cap",
+ .run_case = handshake_sessiontags_duplicate_at_cap,
+ },
{
.name = "sessiontags truncate at cap",
.run_case = handshake_sessiontags_truncate,
diff --git a/net/handshake/request.c b/net/handshake/request.c
index 72515e5e8cc2..31a81b7cb7e9 100644
--- a/net/handshake/request.c
+++ b/net/handshake/request.c
@@ -340,6 +340,7 @@ bool handshake_try_complete(struct handshake_req *req)
{
return !test_and_set_bit(HANDSHAKE_F_REQ_COMPLETED, &req->hr_flags);
}
+EXPORT_SYMBOL_IF_KUNIT(handshake_try_complete);
/**
* handshake_finish_complete - Deliver completion to the consumer
@@ -372,6 +373,7 @@ void handshake_finish_complete(struct handshake_req *req, int status,
fput(file);
}
+EXPORT_SYMBOL_IF_KUNIT(handshake_finish_complete);
void handshake_complete(struct handshake_req *req, int status,
struct genl_info *info)
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 07/10] handshake: Advertise the session-tag cap to user space
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
` (5 preceding siblings ...)
2026-11-12 14:52 ` [PATCH net-next v2 06/10] handshake: Add a kunit test for the completion gate Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 08/10] SUNRPC: Copy the TLS session tags when they are available Chuck Lever
` (2 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
The kernel caps the number of session tags it accepts in a DONE
downcall at HANDSHAKE_MAX_SESSIONTAGS. tlshd has no way to learn
this cap. The value lives in include/net/handshake.h, not in the
UAPI header, so a daemon cannot size its tag list to it. An
over-cap list is truncated with one pr_warn_once per boot, which is
recoverable but easy to miss.
Carry the cap on every ACCEPT reply as
HANDSHAKE_A_ACCEPT_MAX_SESSION_TAGS, a u32 attribute populated by
the kernel. User space reads the value at ACCEPT time and can gate
its DONE-side tag list against it. A list longer than the cap then
becomes a user-space policy choice instead of a kernel-side
truncation.
An attribute lets the kernel raise the cap in a later release
without a new UAPI constant.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
Documentation/netlink/specs/handshake.yaml | 4 ++++
Documentation/networking/tls-handshake.rst | 8 ++++++++
include/uapi/linux/handshake.h | 1 +
net/handshake/tlshd.c | 5 +++++
4 files changed, 18 insertions(+)
diff --git a/Documentation/netlink/specs/handshake.yaml b/Documentation/netlink/specs/handshake.yaml
index 17521b159e89..f86490cd14c6 100644
--- a/Documentation/netlink/specs/handshake.yaml
+++ b/Documentation/netlink/specs/handshake.yaml
@@ -85,6 +85,9 @@ attribute-sets:
-
name: keyring
type: s32
+ -
+ name: max-session-tags
+ type: u32
-
name: done
attributes:
@@ -136,6 +139,7 @@ operations:
- certificate
- peername
- keyring
+ - max-session-tags
-
name: done
doc: Handler reports handshake completion
diff --git a/Documentation/networking/tls-handshake.rst b/Documentation/networking/tls-handshake.rst
index eb8cfe3367cb..ca8f06973fac 100644
--- a/Documentation/networking/tls-handshake.rst
+++ b/Documentation/networking/tls-handshake.rst
@@ -272,3 +272,11 @@ If the handshake failed or no tags were assigned, the stringset is
empty. The handshake layer always delivers a finalized stringset to
the callback, so consumers may call stringset_is_member() and
stringset_intersection() unconditionally without a separate guard.
+
+The stringset delivered to the consumer may contain fewer tags than
+the handshake agent assigned. The kernel caps the per-DONE tag
+count at HANDSHAKE_MAX_SESSIONTAGS, and individual tags within
+the cap may be dropped under memory pressure. The kernel reports
+the cap in every ACCEPT reply as
+HANDSHAKE_A_ACCEPT_MAX_SESSION_TAGS, so the agent can size its
+DONE-side tag list to it.
diff --git a/include/uapi/linux/handshake.h b/include/uapi/linux/handshake.h
index bbb9ea2fa4fb..a22393527342 100644
--- a/include/uapi/linux/handshake.h
+++ b/include/uapi/linux/handshake.h
@@ -52,6 +52,7 @@ enum {
HANDSHAKE_A_ACCEPT_CERTIFICATE,
HANDSHAKE_A_ACCEPT_PEERNAME,
HANDSHAKE_A_ACCEPT_KEYRING,
+ HANDSHAKE_A_ACCEPT_MAX_SESSION_TAGS,
__HANDSHAKE_A_ACCEPT_MAX,
HANDSHAKE_A_ACCEPT_MAX = (__HANDSHAKE_A_ACCEPT_MAX - 1)
diff --git a/net/handshake/tlshd.c b/net/handshake/tlshd.c
index a9c5e5bedced..ec050058805a 100644
--- a/net/handshake/tlshd.c
+++ b/net/handshake/tlshd.c
@@ -238,6 +238,11 @@ static int tls_handshake_accept(struct handshake_req *req,
goto out_cancel;
}
+ ret = nla_put_u32(msg, HANDSHAKE_A_ACCEPT_MAX_SESSION_TAGS,
+ HANDSHAKE_MAX_SESSIONTAGS);
+ if (ret < 0)
+ goto out_cancel;
+
ret = nla_put_u32(msg, HANDSHAKE_A_ACCEPT_AUTH_MODE,
treq->th_auth_mode);
if (ret < 0)
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 08/10] SUNRPC: Copy the TLS session tags when they are available
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
` (6 preceding siblings ...)
2026-11-12 14:52 ` [PATCH net-next v2 07/10] handshake: Advertise the session-tag cap to user space Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 09/10] NFSD: Implement export tagging Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 10/10] NFSD: Add allow_tags to the netlink export interface Chuck Lever
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
When a server handshake completes successfully, tlshd might provide
a set of TLS session tags. SUNRPC can save these within the svc_xprt.
NFSD can later use them to authorize or reject operations that target
NFS exports whose allow_tags list intersects those tags.
A second handshake on the same transport would destroy the saved tags
while other workers read them. svcauth_tls_accept() refuses AUTH_TLS
on a transport that already carries a TLS session, and
svc_tcp_handshake() rechecks the session flag under XPT_BUSY to close
the race with a handshake that completes concurrently.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
include/linux/sunrpc/svc_xprt.h | 7 ++++-
net/sunrpc/Kconfig | 1 +
net/sunrpc/svc_xprt.c | 11 +++++---
net/sunrpc/svcauth_unix.c | 14 +++++++++-
net/sunrpc/svcsock.c | 59 ++++++++++++++++++++++++++++++++++-------
5 files changed, 78 insertions(+), 14 deletions(-)
diff --git a/include/linux/sunrpc/svc_xprt.h b/include/linux/sunrpc/svc_xprt.h
index 2af222f3ea2c..e52d5d4e7941 100644
--- a/include/linux/sunrpc/svc_xprt.h
+++ b/include/linux/sunrpc/svc_xprt.h
@@ -9,6 +9,7 @@
#define SUNRPC_SVC_XPRT_H
#include <linux/sunrpc/svc.h>
+#include <linux/stringset.h>
struct module;
@@ -82,6 +83,7 @@ struct svc_xprt {
const struct cred *xpt_cred;
struct rpc_xprt *xpt_bc_xprt; /* NFSv4.1 backchannel */
struct rpc_xprt_switch *xpt_bc_xps; /* NFSv4.1 backchannel */
+ struct stringset xpt_handshake_tags; /* TLS session tags */
};
/* flag bits for xpt_flags */
@@ -101,7 +103,10 @@ enum {
XPT_KILL_TEMP, /* call xpo_kill_temp_xprt before closing */
XPT_CONG_CTRL, /* has congestion control */
XPT_HANDSHAKE, /* xprt requests a handshake */
- XPT_TLS_SESSION, /* transport-layer security established */
+ XPT_TLS_SESSION, /* transport-layer security established;
+ * also gates xpt_handshake_tags
+ * visibility and destruction
+ */
XPT_PEER_AUTH, /* peer has been authenticated */
XPT_PEER_VALID, /* peer has presented a filehandle that
* it has access to. It is NOT counted
diff --git a/net/sunrpc/Kconfig b/net/sunrpc/Kconfig
index e7808e5714dc..5a7250b42a24 100644
--- a/net/sunrpc/Kconfig
+++ b/net/sunrpc/Kconfig
@@ -2,6 +2,7 @@
config SUNRPC
tristate
depends on MULTIUSER
+ select STRINGSET
config SUNRPC_GSS
tristate
diff --git a/net/sunrpc/svc_xprt.c b/net/sunrpc/svc_xprt.c
index 08cfe8a31189..7a8cfc7c3d75 100644
--- a/net/sunrpc/svc_xprt.c
+++ b/net/sunrpc/svc_xprt.c
@@ -170,6 +170,8 @@ static void svc_xprt_free(struct kref *kref)
struct svc_xprt *xprt =
container_of(kref, struct svc_xprt, xpt_ref);
struct module *owner = xprt->xpt_class->xcl_owner;
+
+ stringset_destroy(&xprt->xpt_handshake_tags);
if (test_bit(XPT_CACHE_AUTH, &xprt->xpt_flags))
svcauth_unix_info_release(xprt);
put_cred(xprt->xpt_cred);
@@ -190,9 +192,12 @@ void svc_xprt_put(struct svc_xprt *xprt)
}
EXPORT_SYMBOL_GPL(svc_xprt_put);
-/*
- * Called by transport drivers to initialize the transport independent
- * portion of the transport instance.
+/**
+ * svc_xprt_init - initialize transport-independent fields of an xprt
+ * @net: Network namespace
+ * @xcl: Transport class
+ * @xprt: Transport to be initialized
+ * @serv: RPC service
*/
void svc_xprt_init(struct net *net, struct svc_xprt_class *xcl,
struct svc_xprt *xprt, struct svc_serv *serv)
diff --git a/net/sunrpc/svcauth_unix.c b/net/sunrpc/svcauth_unix.c
index a68fe44d1f62..958b565617c4 100644
--- a/net/sunrpc/svcauth_unix.c
+++ b/net/sunrpc/svcauth_unix.c
@@ -1100,7 +1100,8 @@ struct auth_ops svcauth_null = {
*
* Return values:
* %SVC_OK: Both credential and verifier are valid
- * %SVC_DENIED: Credential or verifier is not valid
+ * %SVC_DENIED: Credential or verifier is not valid, or a TLS
+ * session already exists on this transport
* %SVC_GARBAGE: Failed to decode credential or verifier
* %SVC_CLOSE: Temporary failure
*
@@ -1138,6 +1139,17 @@ svcauth_tls_accept(struct svc_rqst *rqstp)
return SVC_DENIED;
}
+ /*
+ * AUTH_TLS initiates a handshake, and a second handshake
+ * would destroy xpt_handshake_tags while other workers read
+ * them. This test can pass before a concurrent handshake
+ * completes; svc_tcp_handshake() rechecks under XPT_BUSY.
+ */
+ if (test_bit(XPT_TLS_SESSION, &xprt->xpt_flags)) {
+ rqstp->rq_auth_stat = rpc_autherr_badcred;
+ return SVC_DENIED;
+ }
+
/* Signal that mapping to nobody uid/gid is required */
cred->cr_uid = INVALID_UID;
cred->cr_gid = INVALID_GID;
diff --git a/net/sunrpc/svcsock.c b/net/sunrpc/svcsock.c
index f21522dd51f2..96a84b51328b 100644
--- a/net/sunrpc/svcsock.c
+++ b/net/sunrpc/svcsock.c
@@ -416,10 +416,10 @@ static void svc_tcp_kill_temp_xprt(struct svc_xprt *xprt)
* @peerid: serial number of key containing the remote peer's identity
* @tags: session tags assigned by the handshake agent
*
- * If a security policy is specified as an export option, we don't
- * have a specific export here to check. So we set a "TLS session
- * is present" flag on the xprt and let an upper layer enforce local
- * security policy.
+ * No export is available here to check against a security policy
+ * specified as an export option. The callback copies the session
+ * tags, sets a "TLS session is present" flag on the xprt, and
+ * leaves enforcement of local security policy to an upper layer.
*/
static void svc_tcp_handshake_done(void *data, int status, key_serial_t peerid,
const struct stringset *tags)
@@ -428,9 +428,35 @@ static void svc_tcp_handshake_done(void *data, int status, key_serial_t peerid,
struct svc_sock *svsk = container_of(xprt, struct svc_sock, sk_xprt);
if (!status) {
- if (peerid != TLS_NO_PEERID)
- set_bit(XPT_PEER_AUTH, &xprt->xpt_flags);
- set_bit(XPT_TLS_SESSION, &xprt->xpt_flags);
+ /*
+ * The tag copy can sleep. The xprt is pinned, and
+ * svc_tcp_handshake() waits for sk_handshake_done
+ * when it cannot cancel the handshake.
+ *
+ * XPT_TLS_SESSION left clear on copy failure makes
+ * svc_tcp_handshake() close the connection; a
+ * reconnect retries both the handshake and the copy.
+ */
+ if (stringset_copy(&xprt->xpt_handshake_tags, tags, GFP_KERNEL)) {
+ /*
+ * XPT_PEER_AUTH is set first so no reader
+ * observes XPT_TLS_SESSION without it:
+ * check_xprtsec_policy() would read that
+ * combination as anonymous TLS. The reverse
+ * is harmless; it reads as plaintext.
+ */
+ if (peerid != TLS_NO_PEERID)
+ set_bit(XPT_PEER_AUTH, &xprt->xpt_flags);
+ /*
+ * Order the tag stores and XPT_PEER_AUTH
+ * before XPT_TLS_SESSION. Pairs with the
+ * reader's test_bit_acquire(XPT_TLS_SESSION).
+ */
+ smp_wmb();
+ set_bit(XPT_TLS_SESSION, &xprt->xpt_flags);
+ } else {
+ pr_warn_ratelimited("svc: failed to copy TLS session tags\n");
+ }
}
clear_bit(XPT_HANDSHAKE, &xprt->xpt_flags);
complete_all(&svsk->sk_handshake_done);
@@ -441,6 +467,8 @@ static void svc_tcp_handshake_done(void *data, int status, key_serial_t peerid,
* svc_tcp_handshake - Perform a transport-layer security handshake
* @xprt: connected transport endpoint
*
+ * The handshake request is declined if the transport already
+ * carries a TLS session.
*/
static void svc_tcp_handshake(struct svc_xprt *xprt)
{
@@ -453,9 +481,21 @@ static void svc_tcp_handshake(struct svc_xprt *xprt)
};
int ret;
+ /*
+ * svcauth_tls_accept()'s XPT_TLS_SESSION test can race a
+ * handshake that completes just after it; recheck under
+ * XPT_BUSY before destroying the saved tags.
+ */
+ if (test_bit(XPT_TLS_SESSION, &xprt->xpt_flags)) {
+ clear_bit(XPT_HANDSHAKE, &xprt->xpt_flags);
+ set_bit(XPT_DATA, &xprt->xpt_flags);
+ svc_xprt_enqueue(xprt);
+ return;
+ }
+
trace_svc_tls_upcall(xprt);
- clear_bit(XPT_TLS_SESSION, &xprt->xpt_flags);
+ stringset_destroy(&xprt->xpt_handshake_tags);
init_completion(&svsk->sk_handshake_done);
/* Pin the transport across the asynchronous handshake callback. */
@@ -477,7 +517,8 @@ static void svc_tcp_handshake(struct svc_xprt *xprt)
goto out_close;
}
/* Cancellation lost to handshake_complete(): the
- * callback is in flight and should finish quickly.
+ * callback is in flight. Its tag copy can sleep in
+ * the allocator before the completion fires.
*/
wait_for_completion(&svsk->sk_handshake_done);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 09/10] NFSD: Implement export tagging
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
` (7 preceding siblings ...)
2026-11-12 14:52 ` [PATCH net-next v2 08/10] SUNRPC: Copy the TLS session tags when they are available Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 10/10] NFSD: Add allow_tags to the netlink export interface Chuck Lever
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
NFSD treats TLS peer identity as a boolean: a peer is authenticated
or it is not. Some deployments need finer authorization. A single
certificate may authenticate several distinct actors, and an
administrator may wish to grant each a different level of access.
Once a TLS handshake completes, tlshd hands the kernel a list of
tags for the session. When an export configures an allow_tags list,
grant access only when the session carries at least one matching
tag. An export with no allow_tags list continues to admit any
authenticated peer. Report each denial through the
nfsd_export_tags_denied tracepoint, with the client's address and
the request's XID.
tlshd issues session tags only for an mTLS handshake. Reject an
allow_tags list paired with any xprtsec mode other than mtls. The
administrator then states the combination explicitly, where a
default xprtsec setting would expose the export to plaintext or
anonymous-TLS peers.
A kernel that predates this patch ignores allow_tags and exports
the filesystem without the restriction. List an export's tags in
/proc/fs/nfsd/exports so user space can confirm the restriction
took effect.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
fs/nfsd/Kconfig | 1 +
fs/nfsd/export.c | 114 +++++++++++++++++++++++++++++++++++++++++++++---
fs/nfsd/export.h | 9 ++++
fs/nfsd/trace.h | 27 ++++++++++++
include/net/handshake.h | 5 +++
5 files changed, 151 insertions(+), 5 deletions(-)
diff --git a/fs/nfsd/Kconfig b/fs/nfsd/Kconfig
index ffb76761d6a8..14f694323dcb 100644
--- a/fs/nfsd/Kconfig
+++ b/fs/nfsd/Kconfig
@@ -12,6 +12,7 @@ config NFSD
select SUNRPC
select EXPORTFS
select NFS_COMMON
+ select STRINGSET
select NFS_ACL_SUPPORT if NFSD_V2_ACL
select NFS_ACL_SUPPORT if NFSD_V3_ACL
depends on MULTIUSER
diff --git a/fs/nfsd/export.c b/fs/nfsd/export.c
index e5a0f1ababe6..68f8c570ab0d 100644
--- a/fs/nfsd/export.c
+++ b/fs/nfsd/export.c
@@ -18,6 +18,7 @@
#include <linux/exportfs.h>
#include <linux/sunrpc/svc_xprt.h>
#include <net/genetlink.h>
+#include <net/handshake.h>
#include <uapi/linux/nfsd_netlink.h>
#include "nfsd.h"
@@ -629,6 +630,7 @@ static void svc_export_release(struct rcu_head *rcu_head)
struct svc_export *exp = container_of(rcu_head, struct svc_export,
ex_rcu);
+ stringset_destroy(&exp->ex_allow_tags);
nfsd4_fslocs_free(&exp->ex_fslocs);
export_stats_destroy(exp->ex_stats);
kfree(exp->ex_stats);
@@ -1287,6 +1289,52 @@ static int xprtsec_parse(char **mesg, char *buf, struct svc_export *exp)
return 0;
}
+static int tags_parse(char **mesg, char *buf, struct stringset *tags)
+{
+ unsigned int i, listsize;
+ int err;
+
+ /* more than one allow_tags */
+ if (tags->ss_finalized)
+ return -EINVAL;
+
+ err = get_uint(mesg, &listsize);
+ if (err)
+ return -EINVAL;
+ if (listsize == 0 || listsize > NFSD_MAX_ALLOW_TAGS)
+ return -EINVAL;
+ if (!stringset_alloc(tags, listsize, GFP_KERNEL))
+ return -ENOMEM;
+
+ for (i = 0; i < listsize; i++) {
+ int len;
+
+ len = qword_get(mesg, buf, PAGE_SIZE);
+ if (len <= 0 || len > HANDSHAKE_SESSION_TAG_MAX_LEN)
+ return -EINVAL;
+ if (strlen(buf) != len)
+ return -EINVAL;
+ if (!stringset_add_dup(tags, buf, GFP_KERNEL))
+ return -ENOMEM;
+ }
+ stringset_finalize(tags);
+
+ return 0;
+}
+
+/*
+ * tlshd issues session tags only for an mTLS handshake, so an
+ * allow_tags list is valid only with xprtsec=mtls alone.
+ * check_xprtsec_policy() enforces the same invariant at access time.
+ */
+static int check_allow_tags(const struct svc_export *exp)
+{
+ if (!stringset_is_empty(&exp->ex_allow_tags) &&
+ exp->ex_xprtsec_modes != NFSEXP_XPRTSEC_MTLS)
+ return -EINVAL;
+ return 0;
+}
+
static inline int
nfsd_uuid_parse(char **mesg, char *buf, unsigned char **puuid)
{
@@ -1348,6 +1396,7 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
exp.cd = cd;
exp.ex_devid_map = NULL;
exp.ex_xprtsec_modes = NFSEXP_XPRTSEC_ALL;
+ stringset_init(&exp.ex_allow_tags);
/* expiry */
err = get_expiry(&mesg, &exp.h.expiry_time);
@@ -1382,6 +1431,11 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
goto out3;
exp.ex_fsid = an_int;
+ /*
+ * A kernel that does not recognize "allow_tags" breaks
+ * out of this loop when it reaches that word, exporting
+ * the filesystem without the tag restriction.
+ */
while (qword_get(&mesg, buf, PAGE_SIZE) > 0) {
if (strcmp(buf, "fsloc") == 0)
err = fsloc_parse(&mesg, buf, &exp.ex_fslocs);
@@ -1391,6 +1445,8 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
err = secinfo_parse(&mesg, buf, &exp);
else if (strcmp(buf, "xprtsec") == 0)
err = xprtsec_parse(&mesg, buf, &exp);
+ else if (strcmp(buf, "allow_tags") == 0)
+ err = tags_parse(&mesg, buf, &exp.ex_allow_tags);
else
/* quietly ignore unknown words and anything
* following. Newer user-space can try to set
@@ -1401,6 +1457,10 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
goto out4;
}
+ err = check_allow_tags(&exp);
+ if (err)
+ goto out4;
+
err = check_export(&exp.ex_path, &exp.ex_flags, exp.ex_uuid);
if (err)
goto out4;
@@ -1443,6 +1503,7 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
} else
err = -ENOMEM;
out4:
+ stringset_destroy(&exp.ex_allow_tags);
nfsd4_fslocs_free(&exp.ex_fslocs);
kfree(exp.ex_uuid);
out3:
@@ -1468,6 +1529,22 @@ static int is_export_stats_file(struct seq_file *m)
return !strcmp(m->file->f_path.dentry->d_name.name, "export_stats");
}
+static void show_allow_tags(struct seq_file *m,
+ const struct stringset *tags)
+{
+ unsigned int i;
+ char *tag;
+
+ if (stringset_is_empty(tags))
+ return;
+ seq_puts(m, ",allow_tags=");
+ stringset_for_each(tags, i, tag) {
+ if (i)
+ seq_putc(m, ':');
+ seq_escape(m, tag, ",:() \t\n\\");
+ }
+}
+
static int svc_export_show(struct seq_file *m,
struct cache_detail *cd,
struct cache_head *h)
@@ -1514,6 +1591,7 @@ static int svc_export_show(struct seq_file *m,
}
}
show_secinfo(m, exp);
+ show_allow_tags(m, &exp->ex_allow_tags);
}
seq_puts(m, ")\n");
return 0;
@@ -1570,6 +1648,9 @@ static void export_update(struct cache_head *cnew, struct cache_head *citem)
new->ex_flavors[i] = item->ex_flavors[i];
}
new->ex_xprtsec_modes = item->ex_xprtsec_modes;
+ new->ex_allow_tags = item->ex_allow_tags;
+ /* Shallow move: @item is released once the update completes. */
+ stringset_init(&item->ex_allow_tags);
}
static struct cache_head *svc_export_alloc(void)
@@ -1590,6 +1671,8 @@ static struct cache_head *svc_export_alloc(void)
return NULL;
}
+ stringset_init(&i->ex_allow_tags);
+
return &i->h;
}
@@ -1798,6 +1881,9 @@ static struct svc_export *exp_find(struct cache_detail *cd,
* one exception is __fh_verify() since it has logic that may result in one
* or both of the helpers being skipped.
*
+ * When @exp carries an allow_tags list, the session's tags must
+ * intersect it.
+ *
* Return values:
* %nfs_ok if access is granted, or
* %nfserr_wrongsec if access is denied
@@ -1806,19 +1892,37 @@ __be32 check_xprtsec_policy(struct svc_export *exp, struct svc_rqst *rqstp)
{
struct svc_xprt *xprt = rqstp->rq_xprt;
+ /*
+ * A tagged export is satisfiable only by a matching mTLS
+ * session, so the plaintext and anonymous-TLS arms grant
+ * access only when the allow_tags list is empty.
+ */
if (exp->ex_xprtsec_modes & NFSEXP_XPRTSEC_NONE) {
- if (!test_bit(XPT_TLS_SESSION, &xprt->xpt_flags))
+ if (!test_bit(XPT_TLS_SESSION, &xprt->xpt_flags) &&
+ stringset_is_empty(&exp->ex_allow_tags))
return nfs_ok;
}
if (exp->ex_xprtsec_modes & NFSEXP_XPRTSEC_TLS) {
if (test_bit(XPT_TLS_SESSION, &xprt->xpt_flags) &&
- !test_bit(XPT_PEER_AUTH, &xprt->xpt_flags))
+ !test_bit(XPT_PEER_AUTH, &xprt->xpt_flags) &&
+ stringset_is_empty(&exp->ex_allow_tags))
return nfs_ok;
}
if (exp->ex_xprtsec_modes & NFSEXP_XPRTSEC_MTLS) {
- if (test_bit(XPT_TLS_SESSION, &xprt->xpt_flags) &&
- test_bit(XPT_PEER_AUTH, &xprt->xpt_flags))
- return nfs_ok;
+ /*
+ * test_bit_acquire() pairs with the smp_wmb() in
+ * svc_tcp_handshake_done(): a set XPT_TLS_SESSION
+ * guarantees the handshake tag stores are visible here.
+ */
+ if (test_bit_acquire(XPT_TLS_SESSION, &xprt->xpt_flags) &&
+ test_bit(XPT_PEER_AUTH, &xprt->xpt_flags)) {
+ if (stringset_is_empty(&exp->ex_allow_tags))
+ return nfs_ok;
+ if (stringset_intersection(&xprt->xpt_handshake_tags,
+ &exp->ex_allow_tags))
+ return nfs_ok;
+ trace_nfsd_export_tags_denied(rqstp, exp);
+ }
}
return nfserr_wrongsec;
}
diff --git a/fs/nfsd/export.h b/fs/nfsd/export.h
index 117fb28db1e0..e7ebf5abc594 100644
--- a/fs/nfsd/export.h
+++ b/fs/nfsd/export.h
@@ -7,6 +7,7 @@
#include <linux/sunrpc/cache.h>
#include <linux/percpu_counter.h>
+#include <linux/stringset.h>
#include <uapi/linux/nfsd/export.h>
#include <linux/nfs4.h>
@@ -47,6 +48,13 @@ struct exp_flavor_info {
u32 flags;
};
+/*
+ * Bounds the per-request cost of the stringset_intersection() in
+ * check_xprtsec_policy(). Independent of the per-handshake session
+ * tag cap, HANDSHAKE_MAX_SESSIONTAGS.
+ */
+#define NFSD_MAX_ALLOW_TAGS 64
+
/* Per-export stats */
enum {
EXP_STATS_FH_STALE,
@@ -78,6 +86,7 @@ struct svc_export {
struct rcu_head ex_rcu;
unsigned long ex_xprtsec_modes;
struct export_stats *ex_stats;
+ struct stringset ex_allow_tags;
};
/* an "export key" (expkey) maps a filehandlefragement to an
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index 2ae7f150a72c..e5c179fe28f8 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -467,6 +467,33 @@ TRACE_EVENT(nfsd_export_update,
)
);
+TRACE_EVENT(nfsd_export_tags_denied,
+ TP_PROTO(
+ const struct svc_rqst *rqstp,
+ const struct svc_export *exp
+ ),
+ TP_ARGS(rqstp, exp),
+ TP_STRUCT__entry(
+ __field(u32, xid)
+ __sockaddr(client, rqstp->rq_xprt->xpt_remotelen)
+ __string(path, exp->ex_path.dentry->d_name.name)
+ __string(auth_domain, exp->ex_client->name)
+ ),
+ TP_fast_assign(
+ const struct svc_xprt *xprt = rqstp->rq_xprt;
+
+ __entry->xid = be32_to_cpu(rqstp->rq_xid);
+ __assign_sockaddr(client, &xprt->xpt_remote,
+ xprt->xpt_remotelen);
+ __assign_str(path);
+ __assign_str(auth_domain);
+ ),
+ TP_printk("xid=0x%08x client=%pISpc path=%s domain=%s",
+ __entry->xid, __get_sockaddr(client),
+ __get_str(path), __get_str(auth_domain)
+ )
+);
+
DECLARE_EVENT_CLASS(nfsd_io_class,
TP_PROTO(struct svc_rqst *rqstp,
struct svc_fh *fhp,
diff --git a/include/net/handshake.h b/include/net/handshake.h
index ad96e92af04d..d04f0c0b9cba 100644
--- a/include/net/handshake.h
+++ b/include/net/handshake.h
@@ -12,10 +12,15 @@
#include <linux/errno.h>
#include <linux/stringset.h>
+#include <uapi/linux/handshake.h>
/*
* Bounds the cost of stringset_intersection() in consumer
* authorization checks.
+ *
+ * The per-tag byte limit, HANDSHAKE_SESSION_TAG_MAX_LEN, is
+ * enforced by the netlink policy generated from
+ * Documentation/netlink/specs/handshake.yaml.
*/
#define HANDSHAKE_MAX_SESSIONTAGS 64
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH net-next v2 10/10] NFSD: Add allow_tags to the netlink export interface
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
` (8 preceding siblings ...)
2026-11-12 14:52 ` [PATCH net-next v2 09/10] NFSD: Implement export tagging Chuck Lever
@ 2026-11-12 14:52 ` Chuck Lever
2026-11-13 14:52 ` sashiko-bot
9 siblings, 1 reply; 14+ messages in thread
From: Chuck Lever @ 2026-11-12 14:52 UTC (permalink / raw)
To: Donald Hunter, Jakub Kicinski, David S. Miller, Paolo Abeni,
Simon Horman, John Fastabend, Sabrina Dubroca, Keith Busch,
Jens Axboe, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Eric Dumazet
Cc: kernel-tls-handshake, netdev, linux-nvme, linux-nfs
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() left
ex_allow_tags empty, and check_xprtsec_policy() then granted access
to 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 rejects a
contradictory security policy with an explicit error at
configuration time rather than caching an export that can never
grant access.
A bare -EINVAL does not tell an administrator which tag in a
request was malformed, or that the export's xprtsec setting
conflicts with allow-tags. Pass the request's extended ACK into
nfsd_nl_parse_one_export(). The parser reports a malformed tag as the
bad attribute, and too many tags or an xprtsec setting other than mtls
alone as an error message.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
Documentation/netlink/specs/nfsd.yaml | 10 +++++
fs/nfsd/export.c | 80 +++++++++++++++++++++++++++++++++--
fs/nfsd/netlink.c | 4 +-
fs/nfsd/netlink.h | 3 +-
include/uapi/linux/nfsd_netlink.h | 1 +
tools/net/ynl/Makefile.deps | 3 +-
6 files changed, 94 insertions(+), 7 deletions(-)
diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index 9ae37bf3ea71..e58856b34b02 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: linux/handshake.h
-
type: flags
name: cache-type
@@ -297,6 +301,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 68f8c570ab0d..72b159769539 100644
--- a/fs/nfsd/export.c
+++ b/fs/nfsd/export.c
@@ -832,11 +832,66 @@ 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_allow_tags - parse allow-tags from netlink
+ * @exp: svc_export to fill in
+ * @attr: nested svc-export attribute holding the allow-tags entries
+ * @extack: extended ACK that reports the rejected attribute, or NULL
+ *
+ * On success @exp->ex_allow_tags is populated and finalized; on
+ * failure it can hold a partial set. The caller releases it with
+ * stringset_destroy() either way.
+ *
+ * Return: 0 on success or a negative errno.
+ */
+static int nfsd_nl_parse_allow_tags(struct svc_export *exp,
+ struct nlattr *attr,
+ struct netlink_ext_ack *extack)
+{
+ struct nlattr *tag_attr;
+ unsigned int count = 0;
+ int rem;
+
+ nla_for_each_nested_type(tag_attr, NFSD_A_SVC_EXPORT_ALLOW_TAGS,
+ attr, rem) {
+ if (handshake_session_tag_len(nla_data(tag_attr),
+ nla_len(tag_attr)) < 0) {
+ NL_SET_BAD_ATTR(extack, tag_attr);
+ return -EINVAL;
+ }
+ count++;
+ }
+ if (count > NFSD_MAX_ALLOW_TAGS) {
+ NL_SET_ERR_MSG(extack, "too many allow-tags");
+ return -EINVAL;
+ }
+ if (!stringset_alloc(&exp->ex_allow_tags, count, GFP_KERNEL))
+ return -ENOMEM;
+
+ 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)
+ return -ENOMEM;
+ if (!stringset_add(&exp->ex_allow_tags, tag)) {
+ kfree(tag);
+ return -ENOMEM;
+ }
+ }
+ stringset_finalize(&exp->ex_allow_tags);
+
+ return 0;
+}
/**
* nfsd_nl_parse_one_export - parse one svc_export entry from a netlink message
* @cd: cache_detail for the svc_export cache
* @attr: nested attribute containing svc-export fields
+ * @extack: extended ACK for error reporting, or NULL
*
* Parses one svc-export entry from a netlink message and updates the
* cache. Mirrors the logic in svc_export_parse().
@@ -844,16 +899,17 @@ static int check_export(const struct path *path, int *flags,
* Returns 0 on success or a negative errno.
*/
static int nfsd_nl_parse_one_export(struct cache_detail *cd,
- struct nlattr *attr)
+ struct nlattr *attr,
+ struct netlink_ext_ack *extack)
{
- 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;
@@ -994,6 +1050,20 @@ static int nfsd_nl_parse_one_export(struct cache_detail *cd,
}
}
+ /* allow-tags (multi-attr string) */
+ if (tb[NFSD_A_SVC_EXPORT_ALLOW_TAGS]) {
+ err = nfsd_nl_parse_allow_tags(&exp, attr, extack);
+ if (err)
+ goto out_uuid;
+ }
+
+ err = check_allow_tags(&exp);
+ if (err) {
+ NL_SET_ERR_MSG(extack,
+ "allow-tags needs xprtsec=mtls only");
+ goto out_uuid;
+ }
+
err = check_export(&exp.ex_path, &exp.ex_flags,
exp.ex_uuid);
if (err)
@@ -1028,6 +1098,7 @@ static int nfsd_nl_parse_one_export(struct cache_detail *cd,
}
out_uuid:
+ stringset_destroy(&exp.ex_allow_tags);
kfree(exp.ex_uuid);
out_fslocs:
nfsd4_fslocs_free(&exp.ex_fslocs);
@@ -1068,7 +1139,8 @@ int nfsd_nl_svc_export_set_reqs_doit(struct sk_buff *skb,
nlmsg_for_each_attr_type(attr, NFSD_A_SVC_EXPORT_REQS_REQUESTS,
info->nlhdr, GENL_HDRLEN, rem) {
- ret = nfsd_nl_parse_one_export(cd, (struct nlattr *)attr);
+ ret = nfsd_nl_parse_one_export(cd, (struct nlattr *)attr,
+ info->extack);
if (ret)
break;
}
diff --git a/fs/nfsd/netlink.c b/fs/nfsd/netlink.c
index 88a4a4ffcb7f..146c65575741 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 <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 027e2953db26..098514e7d1bf 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 <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 c125af0cc6a5..e4bd1fe69fff 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -185,6 +185,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)
diff --git a/tools/net/ynl/Makefile.deps b/tools/net/ynl/Makefile.deps
index f8850444d240..3a3fc10a77d6 100644
--- a/tools/net/ynl/Makefile.deps
+++ b/tools/net/ynl/Makefile.deps
@@ -33,7 +33,8 @@ CFLAGS_netdev:=$(call get_hdr_inc,_LINUX_NETDEV_H,netdev.h)
CFLAGS_nl80211:=$(call get_hdr_inc,__LINUX_NL802121_H,nl80211.h)
CFLAGS_nlctrl:=$(call get_hdr_inc,__LINUX_GENERIC_NETLINK_H,genetlink.h) \
$(call get_hdr_inc,__LINUX_NETLINK_H,netlink.h)
-CFLAGS_nfsd:=$(call get_hdr_inc,_LINUX_NFSD_NETLINK_H,nfsd_netlink.h)
+CFLAGS_nfsd:=$(call get_hdr_inc,_LINUX_NFSD_NETLINK_H,nfsd_netlink.h) \
+ $(call get_hdr_inc,_LINUX_HANDSHAKE_H,handshake.h)
CFLAGS_ovpn:=$(call get_hdr_inc,_LINUX_OVPN_H,ovpn.h)
CFLAGS_ovs_datapath:=$(call get_hdr_inc,__LINUX_OPENVSWITCH_H,openvswitch.h)
CFLAGS_ovs_flow:=$(call get_hdr_inc,__LINUX_OPENVSWITCH_H,openvswitch.h)
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* Re: [PATCH net-next v2 02/10] lib: Add a "stringset" data structure
2026-11-12 14:52 ` [PATCH net-next v2 02/10] lib: Add a "stringset" data structure Chuck Lever
@ 2026-11-13 14:52 ` sashiko-bot
0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-11-13 14:52 UTC (permalink / raw)
To: Chuck Lever; +Cc: linux-nvme, Jeff Layton, Anna Schumaker
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] lib: stringset: malformed kernel-doc Return sections break
htmldocs
--
commit 6b2f2a1d0a32f2df849790a6aef77082faf99b69
Author: Chuck Lever <cel@kernel.org>
lib: Add a "stringset" data structure
This commit introduces a sorted-array stringset data structure and API for
string membership testing and set intersection, along with its documentation.
> diff --git a/include/linux/stringset.h b/include/linux/stringset.h
> new file mode 100644
> index 0000000000000..fd41ea23f2b17
> --- /dev/null
> +++ b/include/linux/stringset.h
[ ... ]
> +/**
> + * stringset_add - Add a string to a stringset
> + * @set: An initialized stringset with available capacity
> + * @str: non-NULL string to be added to @set, in kmalloc'd memory
> + *
> + * On success, @str is now owned by @set and will be freed either
> + * by stringset_finalize() (if a content duplicate) or stringset_destroy().
> + * The stringset must have been allocated with sufficient capacity via
> + * stringset_alloc(). Strings must not be added after stringset_finalize()
> + * has been called. Callers must not hand the same pointer to stringset_add()
> + * more than once: ownership has already transferred, and a second add
> + * produces a double-free at stringset_destroy().
> + *
> + * Return:
> + * %true: @str is now a member of @set
> + * %false: @str could not be added (NULL, no capacity, or @set is
> + * already finalized)
> + */
[Severity: Low]
Does the Return section here (and in stringset_add_dup() and
stringset_intersection()) need bullet markers (* %true: / * %false:)?
When Documentation/core-api/stringset.rst includes these kernel-doc
comments during make htmldocs, kdoc_parser strips the two leading spaces
after Return:, leaving two unindented lines (%true: and %false:) followed
by an indented continuation line without a preceding blank line or list
marker, which causes docutils to emit an "Unexpected indentation" error.
In the remaining multi-line Return sections (stringset_alloc(),
stringset_is_empty(), stringset_is_member(), and stringset_copy()),
omitting the list markers also causes the %true and %false descriptions to
collapse into a single paragraph in the rendered documentation.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112-tls-session-tags-v2-0-a587a50de7fc@kernel.org?part=2
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH net-next v2 06/10] handshake: Add a kunit test for the completion gate
2026-11-12 14:52 ` [PATCH net-next v2 06/10] handshake: Add a kunit test for the completion gate Chuck Lever
@ 2026-11-13 14:52 ` sashiko-bot
0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-11-13 14:52 UTC (permalink / raw)
To: Chuck Lever; +Cc: linux-nvme, Jeff Layton, Anna Schumaker
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] handshake: unrelated sessiontags KUnit changes squashed into
commit
--
commit b879c6737fc8615a84b7321f23d468457911d409
Author: Chuck Lever <cel@kernel.org>
handshake: Add a kunit test for the completion gate
This commit exports handshake_try_complete() and handshake_finish_complete()
for KUnit and adds a test case verifying completion gate exclusivity, along
with updates to the session-tag duplicate KUnit tests.
> diff --git a/net/handshake/handshake-test.c b/net/handshake/handshake-test.c
> index 21fccfa94d897..b6992d1fa4ce4 100644
> --- a/net/handshake/handshake-test.c
> +++ b/net/handshake/handshake-test.c
[ ... ]
> @@ -651,7 +729,7 @@ static void handshake_sessiontags_duplicate(struct kunit *test)
> info.nlhdr = nlh;
>
> KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
> - KUNIT_EXPECT_EQ(test, stringset_count(&tags), 2);
> + KUNIT_EXPECT_EQ(test, stringset_count(&tags), 1);
> stringset_finalize(&tags);
> KUNIT_EXPECT_EQ(test, stringset_count(&tags), 1);
> KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "dup"));
> @@ -660,6 +738,30 @@ static void handshake_sessiontags_duplicate(struct kunit *test)
> kfree_skb(skb);
> }
>
> +static void handshake_sessiontags_duplicate_at_cap(struct kunit *test)
> +{
> + struct genl_info info = {};
> + struct nlmsghdr *nlh;
> + struct sk_buff *skb;
> + DEFINE_STRINGSET(tags);
> + unsigned int i;
> +
> + skb = handshake_done_msg(test, &nlh);
> + for (i = 0; i < HANDSHAKE_MAX_SESSIONTAGS; i++)
> + nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "dup");
> + nla_put_string(skb, HANDSHAKE_A_DONE_TAG, "last");
> + nlmsg_end(skb, nlh);
> + info.nlhdr = nlh;
> +
> + KUNIT_EXPECT_EQ(test, handshake_get_sessiontags(&tags, &info), 0);
> + KUNIT_EXPECT_EQ(test, stringset_count(&tags), 2);
> + stringset_finalize(&tags);
> + KUNIT_EXPECT_TRUE(test, stringset_is_member(&tags, "last"));
> +
> + stringset_destroy(&tags);
> + kfree_skb(skb);
> +}
[ ... ]
> @@ -755,6 +861,10 @@ static struct kunit_case handshake_api_test_cases[] = {
> .name = "sessiontags duplicate",
> .run_case = handshake_sessiontags_duplicate,
> },
> + {
> + .name = "sessiontags duplicates at cap",
> + .run_case = handshake_sessiontags_duplicate_at_cap,
> + },
> {
> .name = "sessiontags truncate at cap",
> .run_case = handshake_sessiontags_truncate,
[Severity: Low]
Were these session-tag duplicate KUnit changes meant to be folded into
the preceding commit "handshake: Pick up session tags passed during the
DONE downcall"?
In that commit, handshake_get_sessiontags() already skips duplicate tags
via handshake_sessiontag_is_dup(), so handshake_sessiontags_duplicate()
fails its stringset_count(&tags) == 2 check until this commit updates it,
and the commit message here only describes the completion gate test.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112-tls-session-tags-v2-0-a587a50de7fc@kernel.org?part=6
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH net-next v2 10/10] NFSD: Add allow_tags to the netlink export interface
2026-11-12 14:52 ` [PATCH net-next v2 10/10] NFSD: Add allow_tags to the netlink export interface Chuck Lever
@ 2026-11-13 14:52 ` sashiko-bot
0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-11-13 14:52 UTC (permalink / raw)
To: Chuck Lever; +Cc: linux-nvme, Jeff Layton, Anna Schumaker
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Medium] nfsd: NLA_STRING policy in fs/nfsd/netlink.c disagrees with
nfsd.yaml
--
commit 1059c4281c80865b99ac7732d9133036256073b9
Author: Chuck Lever <cel@kernel.org>
NFSD: Add allow_tags to the netlink export interface
This commit adds the allow-tags attribute to the NFSD netlink svc-export
attribute set and parses it in nfsd_nl_parse_one_export() to enforce mTLS
session tag policies.
> diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
> index 9ae37bf3ea717..e58856b34b020 100644
> --- a/Documentation/netlink/specs/nfsd.yaml
> +++ b/Documentation/netlink/specs/nfsd.yaml
[ ... ]
> @@ -297,6 +301,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/netlink.c b/fs/nfsd/netlink.c
> index 88a4a4ffcb7fd..146c655757410 100644
> --- a/fs/nfsd/netlink.c
> +++ b/fs/nfsd/netlink.c
[ ... ]
> @@ -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, },
> };
[Severity: Medium]
Was fs/nfsd/netlink.c edited by hand to use NLA_STRING for
NFSD_A_SVC_EXPORT_ALLOW_TAGS?
fs/nfsd/netlink.c is generated from Documentation/netlink/specs/nfsd.yaml,
where allow-tags is a genetlink string attribute without unterminated-ok.
Running tools/net/ynl/ynl-regen.sh will overwrite this entry with
.type = NLA_NUL_STRING, changing the kernel attribute validation policy on
the next regeneration.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112-tls-session-tags-v2-0-a587a50de7fc@kernel.org?part=10
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-11-13 14:52 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-12 14:52 [PATCH net-next v2 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 01/10] handshake: Add tags to "done" downcall Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 02/10] lib: Add a "stringset" data structure Chuck Lever
2026-11-13 14:52 ` sashiko-bot
2026-11-12 14:52 ` [PATCH net-next v2 03/10] lib: Add KUnit tests for the stringset " Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 04/10] handshake: Add a session-tags argument to the TLS done callback Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 05/10] handshake: Pick up session tags passed during the DONE downcall Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 06/10] handshake: Add a kunit test for the completion gate Chuck Lever
2026-11-13 14:52 ` sashiko-bot
2026-11-12 14:52 ` [PATCH net-next v2 07/10] handshake: Advertise the session-tag cap to user space Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 08/10] SUNRPC: Copy the TLS session tags when they are available Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 09/10] NFSD: Implement export tagging Chuck Lever
2026-11-12 14:52 ` [PATCH net-next v2 10/10] NFSD: Add allow_tags to the netlink export interface Chuck Lever
2026-11-13 14:52 ` sashiko-bot
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.