From: Bernard Ladenthin <bernard.ladenthin@gmail.com>
To: akpm@linux-foundation.org
Cc: linux-kernel@vger.kernel.org, pablo@netfilter.org, fw@strlen.de,
netfilter-devel@vger.kernel.org, kunit-dev@googlegroups.com,
davem@davemloft.net,
Bernard Ladenthin <bernard.ladenthin@gmail.com>
Subject: [PATCH 2/4] lib/tests: add KUnit tests for the textsearch infrastructure
Date: Sun, 16 Aug 2026 19:05:38 +0200 [thread overview]
Message-ID: <20260816170541.3384-3-bernard.ladenthin@gmail.com> (raw)
In-Reply-To: <20260816170541.3384-1-bernard.ladenthin@gmail.com>
lib/textsearch.c and the algorithms registered with it have had no test
coverage since the infrastructure was added in 2005. Every bug found in
lib/ts_bm.c since then was found by inspection, or by a user hitting it in
production:
commit 3f330317ab49 ("[TEXTSEARCH]: Fix broken good shift array calculation in Boyer-Moore")
commit 3ffaa8c7c0f8 ("[TEXTSEARCH]: Fix Boyer Moore initialization bug")
commit aebb6a849cfe ("textsearch: fix Boyer-Moore text search bug")
commit 6f67fbf8192d ("lib/ts_bm: reset initial match offset for every block of text")
commit 9003ec6f7f39 ("lib/ts_bm: fix integer overflow in pattern length calculation")
Add a KUnit suite that runs the same cases against every algorithm taking
a plain byte-string pattern. All implementations are then held to the same
interface contract. The cases cover matches at the start, middle and end
of the text, the absence of a match, pattern accessors, rejection of
zero-length patterns, and that textsearch_next() advances and eventually
terminates.
Three of the five fixes listed above concern multi-block handling. The
suite therefore also drives the algorithms through a get_next_block() that
hands the text out in fixed-size chunks, the way skb_seq_read() does.
Those cases check what has to hold for any block layout. A match contained
in a single block is found. Every reported offset is a real match.
Iteration makes progress and terminates. Matches spanning a block boundary
are left alone, since ts_bm documents those as missed while ts_kmp finds
them.
ts_fsm is not covered. fsm_init() consumes an array of struct ts_fsm_token
rather than a byte string, so it cannot share these test vectors.
The loop in ts_next_advances is bounded. An algorithm that fails to
advance then reports a failure instead of hanging the test run.
Signed-off-by: Bernard Ladenthin <bernard.ladenthin@gmail.com>
---
This is my first kernel submission. Corrections on anything I got wrong in
the process are welcome.
lib/Kconfig.debug | 19 ++
lib/tests/Makefile | 1 +
lib/tests/textsearch_kunit.c | 327 +++++++++++++++++++++++++++++++++++
3 files changed, 347 insertions(+)
create mode 100644 lib/tests/textsearch_kunit.c
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 1244dcac2294..783cf6bf1469 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3531,6 +3531,25 @@ config GLOB_KUNIT_TEST
If unsure, say N
+config TEXTSEARCH_KUNIT_TEST
+ tristate "Textsearch infrastructure test" if !KUNIT_ALL_TESTS
+ depends on KUNIT
+ select TEXTSEARCH
+ select TEXTSEARCH_KMP
+ select TEXTSEARCH_BM
+ default KUNIT_ALL_TESTS
+ help
+ Enable this option to test the textsearch infrastructure at
+ runtime.
+
+ This test suite exercises lib/textsearch.c together with the
+ string-pattern algorithms registered with it. The same cases are
+ run against every algorithm, checking the reported match offsets
+ and that repeated searches over one buffer make progress and
+ terminate.
+
+ If unsure, say N
+
endif # RUNTIME_TESTING_MENU
config ARCH_USE_MEMTEST
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index 4ead57602eac..a2d0390c18d4 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_TEXTSEARCH_KUNIT_TEST) += textsearch_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/textsearch_kunit.c b/lib/tests/textsearch_kunit.c
new file mode 100644
index 000000000000..b8a79240366d
--- /dev/null
+++ b/lib/tests/textsearch_kunit.c
@@ -0,0 +1,327 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the textsearch infrastructure.
+ *
+ * The cases below are run against every string-pattern algorithm registered
+ * with lib/textsearch.c, so that all implementations are held to the same
+ * interface contract.
+ *
+ * ts_fsm is deliberately not covered: fsm_init() consumes an array of
+ * struct ts_fsm_token rather than a plain byte string, so it cannot share
+ * these test vectors.
+ */
+
+#include <kunit/test.h>
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/textsearch.h>
+
+static const char * const ts_algo_names[] = { "kmp", "bm" };
+
+static void ts_algo_desc(const char * const *algo, char *desc)
+{
+ strscpy(desc, *algo, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(ts_algo, ts_algo_names, ts_algo_desc);
+
+/*
+ * Build a configuration for the algorithm under test. Skips the case rather
+ * than failing it when the algorithm is not registered, so that a kernel
+ * built without, say, CONFIG_TEXTSEARCH_BM still reports cleanly.
+ */
+static struct ts_config *ts_conf_get(struct kunit *test, const char *pattern)
+{
+ const char *algo = *(const char * const *)test->param_value;
+ struct ts_config *conf;
+
+ conf = textsearch_prepare(algo, pattern, strlen(pattern),
+ GFP_KERNEL, TS_AUTOLOAD);
+ if (IS_ERR(conf))
+ kunit_skip(test, "algorithm \"%s\" not registered (%pe)",
+ algo, conf);
+
+ return conf;
+}
+
+static void ts_find_middle(struct kunit *test)
+{
+ static const char text[] = "We dance the funky chicken";
+ static const char pattern[] = "chicken";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ struct ts_state state;
+
+ KUNIT_EXPECT_EQ(test,
+ textsearch_find_continuous(conf, &state, text,
+ strlen(text)),
+ strlen(text) - strlen(pattern));
+
+ textsearch_destroy(conf);
+}
+
+static void ts_find_at_start(struct kunit *test)
+{
+ static const char text[] = "abcdefg";
+ static const char pattern[] = "abc";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ struct ts_state state;
+
+ KUNIT_EXPECT_EQ(test,
+ textsearch_find_continuous(conf, &state, text,
+ strlen(text)),
+ 0);
+
+ textsearch_destroy(conf);
+}
+
+static void ts_find_at_end(struct kunit *test)
+{
+ static const char text[] = "abcdefg";
+ static const char pattern[] = "efg";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ struct ts_state state;
+
+ KUNIT_EXPECT_EQ(test,
+ textsearch_find_continuous(conf, &state, text,
+ strlen(text)),
+ 4);
+
+ textsearch_destroy(conf);
+}
+
+static void ts_find_no_match(struct kunit *test)
+{
+ static const char text[] = "abcdefg";
+ static const char pattern[] = "xyz";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ struct ts_state state;
+
+ KUNIT_EXPECT_EQ(test,
+ textsearch_find_continuous(conf, &state, text,
+ strlen(text)),
+ UINT_MAX);
+
+ textsearch_destroy(conf);
+}
+
+/*
+ * textsearch_find() resets state->offset and textsearch_next() relies on the
+ * algorithm having advanced it past the match it just reported. An algorithm
+ * that leaves state->offset alone reports the same position forever.
+ */
+static void ts_next_advances(struct kunit *test)
+{
+ static const char text[] = "aaaa";
+ static const char pattern[] = "aa";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ unsigned int pos, prev;
+ struct ts_state state;
+ int i;
+
+ pos = textsearch_find_continuous(conf, &state, text, strlen(text));
+ KUNIT_ASSERT_EQ(test, pos, 0);
+
+ /* Bounded so that a non-advancing algorithm fails instead of hanging. */
+ for (i = 0; i < 8; i++) {
+ prev = pos;
+
+ pos = textsearch_next(conf, &state);
+ if (pos == UINT_MAX)
+ break;
+
+ KUNIT_ASSERT_GT_MSG(test, pos, prev,
+ "textsearch_next() reported %u after %u; it must advance past the previous match",
+ pos, prev);
+ }
+
+ KUNIT_EXPECT_EQ_MSG(test, pos, UINT_MAX,
+ "search did not terminate within 8 iterations");
+
+ textsearch_destroy(conf);
+}
+
+/* The full set of matches must be reported exactly once, in order. */
+static void ts_next_finds_all(struct kunit *test)
+{
+ static const char text[] = "xxABxxABxx";
+ static const char pattern[] = "AB";
+ static const unsigned int expect[] = { 2, 6 };
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ struct ts_state state;
+ unsigned int pos;
+ int i;
+
+ pos = textsearch_find_continuous(conf, &state, text, strlen(text));
+
+ for (i = 0; i < ARRAY_SIZE(expect); i++) {
+ KUNIT_ASSERT_EQ_MSG(test, pos, expect[i],
+ "match %d: expected offset %u, got %u",
+ i, expect[i], pos);
+ pos = textsearch_next(conf, &state);
+ }
+
+ KUNIT_EXPECT_EQ_MSG(test, pos, UINT_MAX,
+ "expected exactly %zu matches", ARRAY_SIZE(expect));
+
+ textsearch_destroy(conf);
+}
+
+/*
+ * A block source that hands the text out in fixed-size chunks, so that the
+ * algorithms are driven the way a non-linear skb drives them. Boundaries sit
+ * at multiples of @chunk, mirroring skb_seq_read().
+ */
+struct ts_chunk_state {
+ const char *data;
+ unsigned int len;
+ unsigned int chunk;
+};
+
+static unsigned int ts_get_chunk(unsigned int consumed, const u8 **dst,
+ struct ts_config *conf,
+ struct ts_state *state)
+{
+ struct ts_chunk_state *cs = (struct ts_chunk_state *)state->cb;
+ unsigned int end;
+
+ if (consumed >= cs->len)
+ return 0;
+
+ end = (consumed / cs->chunk + 1) * cs->chunk;
+ if (end > cs->len)
+ end = cs->len;
+
+ *dst = (const u8 *)cs->data + consumed;
+ return end - consumed;
+}
+
+static unsigned int ts_find_chunked(struct ts_config *conf,
+ struct ts_state *state, const char *text,
+ unsigned int len, unsigned int chunk)
+{
+ struct ts_chunk_state *cs = (struct ts_chunk_state *)state->cb;
+
+ BUILD_BUG_ON(sizeof(struct ts_chunk_state) > sizeof(state->cb));
+
+ conf->get_next_block = ts_get_chunk;
+ cs->data = text;
+ cs->len = len;
+ cs->chunk = chunk;
+
+ return textsearch_find(conf, state);
+}
+
+/*
+ * A match that lies entirely inside one block must be found no matter how the
+ * text is split up. Matches spanning a block boundary are deliberately not
+ * covered: ts_bm documents those as missed, ts_kmp finds them.
+ */
+static void ts_blocks_match_within_block(struct kunit *test)
+{
+ static const char text[] = "xxxxABCDxxxx";
+ static const char pattern[] = "ABCD";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ struct ts_state state;
+
+ /* chunk 4 puts "ABCD" exactly in the second block */
+ KUNIT_EXPECT_EQ_MSG(test,
+ ts_find_chunked(conf, &state, text,
+ strlen(text), 4),
+ 4, "match inside a single block must be found");
+
+ /* one block for the whole text must agree with the chunked run */
+ KUNIT_EXPECT_EQ(test,
+ ts_find_chunked(conf, &state, text, strlen(text),
+ strlen(text)),
+ 4);
+
+ textsearch_destroy(conf);
+}
+
+/* Iterating over a chunked buffer must terminate and must make progress. */
+static void ts_blocks_iteration_terminates(struct kunit *test)
+{
+ static const char text[] = "abababababab";
+ static const char pattern[] = "ab";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+ unsigned int chunk, pos, prev;
+ struct ts_state state;
+ int i;
+
+ for (chunk = 1; chunk <= strlen(text); chunk++) {
+ pos = ts_find_chunked(conf, &state, text, strlen(text), chunk);
+
+ for (i = 0; i < 32 && pos != UINT_MAX; i++) {
+ KUNIT_ASSERT_LE_MSG(test, pos + strlen(pattern),
+ strlen(text),
+ "chunk %u: reported match at %u runs past the text",
+ chunk, pos);
+ KUNIT_ASSERT_MEMEQ_MSG(test, text + pos, pattern,
+ strlen(pattern),
+ "chunk %u: offset %u is not a real match",
+ chunk, pos);
+ prev = pos;
+ pos = textsearch_next(conf, &state);
+ if (pos == UINT_MAX)
+ break;
+ KUNIT_ASSERT_GT_MSG(test, pos, prev,
+ "chunk %u: reported %u after %u",
+ chunk, pos, prev);
+ }
+
+ KUNIT_EXPECT_EQ_MSG(test, pos, UINT_MAX,
+ "chunk %u: search did not terminate", chunk);
+ }
+
+ textsearch_destroy(conf);
+}
+
+static void ts_get_pattern(struct kunit *test)
+{
+ static const char pattern[] = "chicken";
+ struct ts_config *conf = ts_conf_get(test, pattern);
+
+ KUNIT_EXPECT_EQ(test, textsearch_get_pattern_len(conf),
+ strlen(pattern));
+ KUNIT_EXPECT_MEMEQ(test, textsearch_get_pattern(conf), pattern,
+ strlen(pattern));
+
+ textsearch_destroy(conf);
+}
+
+/* textsearch_prepare() documents -EINVAL for a zero-length pattern. */
+static void ts_prepare_zero_len(struct kunit *test)
+{
+ const char *algo = *(const char * const *)test->param_value;
+ struct ts_config *conf;
+
+ conf = textsearch_prepare(algo, "", 0, GFP_KERNEL, TS_AUTOLOAD);
+ KUNIT_ASSERT_TRUE(test, IS_ERR(conf));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(conf), -EINVAL);
+}
+
+static struct kunit_case textsearch_test_cases[] = {
+ KUNIT_CASE_PARAM(ts_find_middle, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_find_at_start, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_find_at_end, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_find_no_match, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_next_advances, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_next_finds_all, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_blocks_match_within_block, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_blocks_iteration_terminates, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_get_pattern, ts_algo_gen_params),
+ KUNIT_CASE_PARAM(ts_prepare_zero_len, ts_algo_gen_params),
+ {}
+};
+
+static struct kunit_suite textsearch_test_suite = {
+ .name = "textsearch",
+ .test_cases = textsearch_test_cases,
+};
+
+kunit_test_suite(textsearch_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for the textsearch infrastructure");
+MODULE_LICENSE("GPL");
--
2.49.0.windows.1
next prev parent reply other threads:[~2026-08-16 17:06 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-16 17:05 [PATCH 0/4] lib/textsearch: fix ts_bm resume offset, add tests, two small cleanups Bernard Ladenthin
2026-08-16 17:05 ` [PATCH 1/4] lib/ts_bm: advance state->offset past the reported match Bernard Ladenthin
2026-08-16 20:37 ` Pablo Neira Ayuso
2026-08-16 17:05 ` Bernard Ladenthin [this message]
2026-08-16 17:05 ` [PATCH 3/4] textsearch: align ts_state.cb like skb->cb Bernard Ladenthin
2026-08-16 17:05 ` [PATCH 4/4] lib/ts_fsm: document that a match must consume the remaining data Bernard Ladenthin
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=20260816170541.3384-3-bernard.ladenthin@gmail.com \
--to=bernard.ladenthin@gmail.com \
--cc=akpm@linux-foundation.org \
--cc=davem@davemloft.net \
--cc=fw@strlen.de \
--cc=kunit-dev@googlegroups.com \
--cc=linux-kernel@vger.kernel.org \
--cc=netfilter-devel@vger.kernel.org \
--cc=pablo@netfilter.org \
/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 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.