java-topology/defects/openssl/patch/openssl-0004-store-cert-subjects-lhash.md

4.9 KiB

UNDF: UNDF-2026-000000491

openssl-0004: CWE-407 O(N²) CA name dedup in SSL_add_store_cert_subjects_to_stack

Severity: MEDIUM

Location

ssl/ssl_cert.cadd_uris_recursive() called from SSL_add_store_cert_subjects_to_stack()

Description

SSL_add_store_cert_subjects_to_stack() loads CA subject names from a URI store (e.g., a PKCS#11 token or directory) and deduplicates them into a STACK_OF(X509_NAME). It calls add_uris_recursive() for the actual loading.

add_uris_recursive() contains this pattern:

while (!OSSL_STORE_eof(ctx) && !OSSL_STORE_error(ctx)) {
    ...
    if (sk_X509_NAME_find(stack, xn) >= 0) {   /* O(N) linear scan */
        /* Duplicate */
        X509_NAME_free(xn);
    } else if (!sk_X509_NAME_push(stack, xn)) {
        ...
    }
}

sk_X509_NAME_find() calls internal_find(). When the stack is unsorted (which it is here — add_uris_recursive never calls sk_X509_NAME_sort()), internal_find() falls through to a linear scan:

if (!st->sorted) {
    for (i = 0; i < st->num; i++) {
        cmp_ret = cmp_with_thunk(st, &data, st->data + i);
        if (cmp_ret == 0) { ... return i; }
    }
    return -1;
}

Each call is O(N). Loading N certificates from the store requires N such calls, giving O(N²) total comparisons.

By contrast, both SSL_add_file_cert_subjects_to_stack and SSL_add_dir_cert_subjects_to_stack received an explicit LHASH fix (visible in the same file) that pre-populates an LHASH_OF(X509_NAME) for O(1) duplicate detection. SSL_add_store_cert_subjects_to_stack was left behind.

Complexity Before Fix

N (CA certs in store) Comparisons (O(N²))
100 ~5,050
500 ~125,250
1,000 ~500,500
5,000 ~12,502,500

Each comparison calls xname_sk_cmp → X509_NAME_cmp → X509_NAME_cmp_ex which allocates and DER-encodes the name — making each O(N) scan carry non-trivial constant work beyond the raw count.

Fix

Pass an LHASH_OF(X509_NAME) into add_uris_recursive() just as SSL_add_dir_cert_subjects_to_stack does for the per-file helper, replacing sk_X509_NAME_find() with lh_X509_NAME_retrieve().

--- a/ssl/ssl_cert.c
+++ b/ssl/ssl_cert.c
@@ -1022,7 +1022,8 @@ static int add_uris_recursive(STACK_OF(X509_NAME) *stack,
-static int add_uris_recursive(STACK_OF(X509_NAME) *stack,
-    const char *uri, int depth)
+static int add_uris_recursive(STACK_OF(X509_NAME) *stack,
+    LHASH_OF(X509_NAME) *name_hash,
+    const char *uri, int depth)
 {
     ...
     if (infotype == OSSL_STORE_INFO_NAME) {
         if (depth > 0)
-            ok = add_uris_recursive(stack, OSSL_STORE_INFO_get0_NAME(info),
-                depth - 1);
+            ok = add_uris_recursive(stack, name_hash,
+                OSSL_STORE_INFO_get0_NAME(info), depth - 1);
     } else if (infotype == OSSL_STORE_INFO_CERT) {
         ...
-        if (sk_X509_NAME_find(stack, xn) >= 0) {
+        if (lh_X509_NAME_retrieve(name_hash, xn) != NULL) {
             /* Duplicate. */
             X509_NAME_free(xn);
         } else if (!sk_X509_NAME_push(stack, xn)) {
             ...
+        } else {
+            lh_X509_NAME_insert(name_hash, xn);
         }
     }
     ...
 }

 int SSL_add_store_cert_subjects_to_stack(STACK_OF(X509_NAME) *stack,
     const char *store)
 {
     int (*oldcmp)(const X509_NAME *const *a, const X509_NAME *const *b)
         = sk_X509_NAME_set_cmp_func(stack, xname_sk_cmp);
+    LHASH_OF(X509_NAME) *name_hash = lh_X509_NAME_new(xname_hash, xname_cmp);
+    int ret;
+    X509_NAME *xn;
+    int idx, num;
+
+    if (name_hash == NULL) {
+        (void)sk_X509_NAME_set_cmp_func(stack, oldcmp);
+        return 0;
+    }
+
+    /* Pre-populate lhash with names already on the stack */
+    num = sk_X509_NAME_num(stack);
+    for (idx = 0; idx < num; idx++) {
+        xn = sk_X509_NAME_value(stack, idx);
+        lh_X509_NAME_insert(name_hash, xn);
+    }
+
-    int ret = add_uris_recursive(stack, store, 1);
+    ret = add_uris_recursive(stack, name_hash, store, 1);
+    lh_X509_NAME_free(name_hash);
     (void)sk_X509_NAME_set_cmp_func(stack, oldcmp);
     return ret;
 }

Complexity After Fix

O(N) total — each name is hashed once (O(1) amortized) during insertion. lh_X509_NAME_retrieve is O(1) amortized.

Speedup

N Before (comparisons) After (hash ops) Ratio
100 5,050 ~100 ~50x
500 125,250 ~500 ~250x
1000 500,500 ~1,000 ~500x

References

  • CWE-407: Inefficient Algorithmic Complexity
  • ssl/ssl_cert.cSSL_add_dir_cert_subjects_to_stack() (already fixed, same file)
  • crypto/stack/stack.cinternal_find() linear path when !st->sorted