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);