From: Chuck Lever <cel@kernel.org>
To: Donald Hunter <donald.hunter@gmail.com>,
Jakub Kicinski <kuba@kernel.org>,
"David S. Miller" <davem@davemloft.net>,
Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
John Fastabend <john.fastabend@gmail.com>,
Sabrina Dubroca <sd@queasysnail.net>,
Keith Busch <kbusch@kernel.org>, Jens Axboe <axboe@kernel.dk>,
Christoph Hellwig <hch@lst.de>, Sagi Grimberg <sagi@grimberg.me>,
Chaitanya Kulkarni <kch@nvidia.com>,
Chuck Lever <cel@kernel.org>, Jeff Layton <jlayton@kernel.org>,
NeilBrown <neil@brown.name>,
Olga Kornievskaia <okorniev@redhat.com>,
Dai Ngo <Dai.Ngo@oracle.com>, Tom Talpey <tom@talpey.com>,
Eric Dumazet <edumazet@kernel.org>
Cc: kernel-tls-handshake@lists.linux.dev, netdev@vger.kernel.org,
linux-nvme@lists.infradead.org, linux-nfs@vger.kernel.org
Subject: [PATCH net-next v3 03/10] lib: Add KUnit tests for the stringset data structure
Date: Fri, 13 Nov 2026 14:22:01 -0500 [thread overview]
Message-ID: <20261113-tls-session-tags-v3-3-91271964cdb0@kernel.org> (raw)
In-Reply-To: <20261113-tls-session-tags-v3-0-91271964cdb0@kernel.org>
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 | 311 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 319 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 9b058bb60b0f..71a4ffe39c53 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27028,6 +27028,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..6e814b1faffb
--- /dev/null
+++ b/lib/tests/stringset_kunit.c
@@ -0,0 +1,311 @@
+// 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;
+ bool added;
+
+ KUNIT_ASSERT_TRUE(test, stringset_alloc(set, 1, GFP_KERNEL));
+ KUNIT_ASSERT_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);
+ added = stringset_add(set, overflow);
+ KUNIT_EXPECT_FALSE(test, added);
+ if (!added)
+ 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
next prev parent reply other threads:[~2026-11-13 19:22 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 19:21 [PATCH net-next v3 00/10] Deliver TLS session tags to upper-layer consumers (NFSD) Chuck Lever
2026-11-13 19:21 ` [PATCH net-next v3 01/10] handshake: Add tags to "done" downcall Chuck Lever
2026-11-13 19:22 ` [PATCH net-next v3 02/10] lib: Add a "stringset" data structure Chuck Lever
2026-11-13 19:22 ` Chuck Lever [this message]
2026-11-13 19:22 ` [PATCH net-next v3 04/10] handshake: Add a session-tags argument to the TLS done callback Chuck Lever
2026-11-13 19:22 ` [PATCH net-next v3 05/10] handshake: Pick up session tags passed during the DONE downcall Chuck Lever
2026-11-13 19:22 ` [PATCH net-next v3 06/10] handshake: Add a kunit test for the completion gate Chuck Lever
2026-11-13 19:22 ` [PATCH net-next v3 07/10] handshake: Advertise the session-tag cap to user space Chuck Lever
2026-11-13 19:22 ` [PATCH net-next v3 08/10] SUNRPC: Copy the TLS session tags when they are available Chuck Lever
2026-11-13 19:22 ` [PATCH net-next v3 09/10] NFSD: Implement export tagging Chuck Lever
2026-11-13 19:22 ` [PATCH net-next v3 10/10] NFSD: Add allow_tags to the netlink export interface Chuck Lever
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261113-tls-session-tags-v3-3-91271964cdb0@kernel.org \
--to=cel@kernel.org \
--cc=Dai.Ngo@oracle.com \
--cc=axboe@kernel.dk \
--cc=davem@davemloft.net \
--cc=donald.hunter@gmail.com \
--cc=edumazet@kernel.org \
--cc=hch@lst.de \
--cc=horms@kernel.org \
--cc=jlayton@kernel.org \
--cc=john.fastabend@gmail.com \
--cc=kbusch@kernel.org \
--cc=kch@nvidia.com \
--cc=kernel-tls-handshake@lists.linux.dev \
--cc=kuba@kernel.org \
--cc=linux-nfs@vger.kernel.org \
--cc=linux-nvme@lists.infradead.org \
--cc=neil@brown.name \
--cc=netdev@vger.kernel.org \
--cc=okorniev@redhat.com \
--cc=pabeni@redhat.com \
--cc=sagi@grimberg.me \
--cc=sd@queasysnail.net \
--cc=tom@talpey.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox