mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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


  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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®