Commit fcf8134aa for clamav.net
commit fcf8134aab5d01335860d583d62ffcd65f77aba9
Author: Valerie Snyder <valsnyde@cisco.com>
Date: Tue Aug 18 21:38:17 2026 -0400
Fix: Reject repeated AC replacement heads
The repeated-prefix selector ranked candidate windows by occurrences of
the original repeated byte. A signature such as ffffff000000 could
therefore replace an ff trie head with an all-zero trie head and recreate
the low-selectivity behavior this optimization is intended to avoid.
Require shifted windows to contain at least two distinct bytes. Add a
matcher regression that verifies the transition window is selected and
that the complete signature still matches.
Validation:
- make -j12 check_clamav
- CK_RUN_CASE=matchers ./build/unit_tests/check_clamav
- git clang-format --diff HEAD -- libclamav/matcher-ac.c
unit_tests/check_matchers.c
diff --git a/libclamav/matcher-ac.c b/libclamav/matcher-ac.c
index 7b474a291..2329fd6bb 100644
--- a/libclamav/matcher-ac.c
+++ b/libclamav/matcher-ac.c
@@ -318,7 +318,10 @@ static bool ac_select_repeated_prefix_exact_window(const uint16_t *pattern, uint
}
}
- if (!valid || repeated_count >= depth) {
+ /* Replacing one repeated trie head with another only changes which
+ * common byte can produce excessive candidates. Require the shifted
+ * window to contain a transition, regardless of the repeated value. */
+ if (!valid || repeated_count >= depth || distinct_count < 2) {
continue;
}
diff --git a/unit_tests/check_matchers.c b/unit_tests/check_matchers.c
index eabf4b8ba..e5af0f7ae 100644
--- a/unit_tests/check_matchers.c
+++ b/unit_tests/check_matchers.c
@@ -241,6 +241,49 @@ START_TEST(test_ac_scanbuff)
}
END_TEST
+START_TEST(test_ac_repeated_prefix_does_not_shift_to_repeated_window)
+{
+ static const unsigned char data[] = {0xff, 0xff, 0xff, 0x00, 0x00, 0x00};
+ struct cli_ac_data mdata;
+ struct cli_ac_patt *pattern;
+ struct cli_matcher *root;
+ cl_error_t status;
+
+ root = ctx.engine->root[0];
+ ck_assert_msg(root != NULL, "root == NULL");
+ root->ac_only = 1;
+
+#ifdef USE_MPOOL
+ root->mempool = mpool_create();
+#endif
+ status = cli_ac_init(root, CLI_DEFAULT_AC_MINDEPTH, CLI_DEFAULT_AC_MAXDEPTH, 1);
+ ck_assert_msg(status == CL_SUCCESS, "cli_ac_init() failed");
+
+ status = cli_add_content_match_pattern(root, "RepeatedPrefixTransition", "ffffff000000", 0, 0, 0, "*", NULL, 0);
+ ck_assert_msg(status == CL_SUCCESS, "cli_add_content_match_pattern failed");
+ ck_assert_uint_eq(root->ac_patterns, 1);
+
+ pattern = root->ac_pattable[0];
+ ck_assert_ptr_nonnull(pattern);
+ ck_assert_uint_eq(pattern->prefix_length[0], 2);
+ ck_assert_uint_eq(pattern->depth, CLI_DEFAULT_AC_MAXDEPTH);
+ ck_assert_uint_eq(pattern->pattern[0] & 0xff, 0xff);
+ ck_assert_uint_eq(pattern->pattern[1] & 0xff, 0x00);
+ ck_assert_uint_eq(pattern->pattern[2] & 0xff, 0x00);
+
+ status = cli_ac_buildtrie(root);
+ ck_assert_msg(status == CL_SUCCESS, "cli_ac_buildtrie() failed");
+ status = cli_ac_initdata(&mdata, root->ac_partsigs, 0, 0, CLI_DEFAULT_AC_TRACKLEN);
+ ck_assert_msg(status == CL_SUCCESS, "cli_ac_initdata() failed");
+
+ status = cli_ac_scanbuff(data, sizeof(data), &virname, NULL, NULL, root, &mdata, 0, 0, NULL, AC_SCAN_VIR, NULL);
+ ck_assert_msg(status == CL_VIRUS, "cli_ac_scanbuff() failed");
+ ck_assert_msg(!strncmp(virname, "RepeatedPrefixTransition", strlen("RepeatedPrefixTransition")), "Incorrect signature matched in cli_ac_scanbuff()\n");
+
+ cli_ac_freedata(&mdata);
+}
+END_TEST
+
START_TEST(test_ac_scanbuff_allscan)
{
struct cli_ac_data mdata;
@@ -584,6 +627,7 @@ Suite *test_matchers_suite(void)
suite_add_tcase(s, tc_matchers);
tcase_add_checked_fixture(tc_matchers, setup, teardown);
tcase_add_test(tc_matchers, test_ac_scanbuff);
+ tcase_add_test(tc_matchers, test_ac_repeated_prefix_does_not_shift_to_repeated_window);
tcase_add_test(tc_matchers, test_ac_scanbuff_ex);
tcase_add_test(tc_matchers, test_bm_scanbuff);
tcase_add_test(tc_matchers, test_pcre_scanbuff);