Commit e59165a1e3 for openssl.org

commit e59165a1e3df2f1cbe223406ea4fb40025e54d8a
Author: Niels Provos <provos@gmail.com>
Date:   Tue May 12 09:44:12 2026 -0700

    crypto/x509: replace O(N^2) RFC 3779 canonicalisation merge with linear sweep

    ASIdentifierChoice_canonize and IPAddressOrRanges_canonize previously
    merged adjacent entries with an in-place loop that called
    sk_..._delete() after each merge, making the merge O(N^2) due to the
    per-merge stack shift.

    Replace the merge with a single linear sweep using a write index
    distinct from the read cursor: mergeable entries fold into the
    previous output's upper bound in O(1), non-mergeable entries are slid
    forward into the write slot, and the source slot is set to NULL so the
    ASN.1 free machinery cannot double-free on a subsequent abort.
    Canonicalisation is now O(N log N) overall, bounded by the existing
    sort.

    Mixed-state-on-error safety is provided by the caller's normal
    teardown path: OPENSSL_sk_pop unlinks without freeing, OPENSSL_sk_set
    replaces without freeing the displaced value, and
    ossl_asn1_item_embed_free no-ops on NULL slots, so returning early on
    an inner failure leaves the stack in a state that the choice's normal
    free path handles cleanly.

    New regression tests in test/v3ext.c at N=8192 cover the all-merge,
    no-merge, interleaved (slide-forward), range-merge,
    overlap-mid-sweep, and inverted-range-mid-sweep paths; the
    mixed-state teardown invariant is exercised under ASan + UBSan.

    Reviewed-by: Paul Dale <paul.dale@oracle.com>
    Reviewed-by: Bob Beck <beck@openssl.org>
    MergeDate: Fri Jul  3 19:20:01 2026
    (Merged from https://github.com/openssl/openssl/pull/31147)

diff --git a/crypto/x509/v3_addr.c b/crypto/x509/v3_addr.c
index 1e0d94babf..bdaa74a0d1 100644
--- a/crypto/x509/v3_addr.c
+++ b/crypto/x509/v3_addr.c
@@ -760,6 +760,7 @@ int X509v3_addr_is_canonical(IPAddrBlocks *addr)
         aors = f->ipAddressChoice->u.addressesOrRanges;
         if (sk_IPAddressOrRange_num(aors) == 0)
             return 0;
+
         for (j = 0; j < sk_IPAddressOrRange_num(aors) - 1; j++) {
             IPAddressOrRange *a = sk_IPAddressOrRange_value(aors, j);
             IPAddressOrRange *b = sk_IPAddressOrRange_value(aors, j + 1);
@@ -814,78 +815,106 @@ int X509v3_addr_is_canonical(IPAddrBlocks *addr)

 /*
  * Whack an IPAddressOrRanges into canonical form.
+ *
+ * After the initial sort, the merge runs as a single linear sweep
+ * over the list using a write index.  Adjacent entries are folded
+ * into the previous output by replacing it with a freshly built
+ * merged range; both old entries are then freed and the source slot
+ * is left NULL so the asn1 free machinery does not double-free on a
+ * subsequent abort.  Total cost is O(N log N) sort + O(N) merge,
+ * with no stack deletes inside the loop.
  */
 static int IPAddressOrRanges_canonize(IPAddressOrRanges *aors,
     const unsigned afi)
 {
-    int i, j, length = length_from_afi(afi);
+    int length = length_from_afi(afi);
+    int read, write = 0, n;

-    /*
-     * Sort the IPAddressOrRanges sequence.
-     */
     sk_IPAddressOrRange_sort(aors);
+    n = sk_IPAddressOrRange_num(aors);

     /*
-     * Clean up representation issues, punt on duplicates or overlaps.
+     * Error paths below all `return 0` directly.  Slots at
+     * [write..read-1] are NULL (from earlier iterations) and slots at
+     * [read..n-1] still hold their original entries; the caller's
+     * normal teardown walks the whole stack and frees each non-NULL
+     * slot safely, so leaving the stack in this mixed state is sound.
      */
-    for (i = 0; i < sk_IPAddressOrRange_num(aors) - 1; i++) {
-        IPAddressOrRange *a = sk_IPAddressOrRange_value(aors, i);
-        IPAddressOrRange *b = sk_IPAddressOrRange_value(aors, i + 1);
-        unsigned char a_min[ADDR_RAW_BUF_LEN], a_max[ADDR_RAW_BUF_LEN];
-        unsigned char b_min[ADDR_RAW_BUF_LEN], b_max[ADDR_RAW_BUF_LEN];
+    for (read = 0; read < n; read++) {
+        IPAddressOrRange *cur = sk_IPAddressOrRange_value(aors, read);
+        unsigned char c_min[ADDR_RAW_BUF_LEN], c_max[ADDR_RAW_BUF_LEN];

-        if (!extract_min_max(a, a_min, a_max, length) || !extract_min_max(b, b_min, b_max, length))
+        if (!extract_min_max(cur, c_min, c_max, length))
             return 0;

         /*
-         * Punt inverted ranges.
+         * Punt inverted range.
          */
-        if (memcmp(a_min, a_max, length) > 0 || memcmp(b_min, b_max, length) > 0)
+        if (memcmp(c_min, c_max, length) > 0)
             return 0;

-        /*
-         * Punt overlaps.
-         */
-        if (memcmp(a_max, b_min, length) >= 0)
-            return 0;
+        if (write > 0) {
+            IPAddressOrRange *prev = sk_IPAddressOrRange_value(aors,
+                write - 1);
+            unsigned char p_min[ADDR_RAW_BUF_LEN], p_max[ADDR_RAW_BUF_LEN];
+            unsigned char c_min_minus_one[ADDR_RAW_BUF_LEN];
+            int j;

-        /*
-         * Merge if a and b are adjacent.  We check for
-         * adjacency by subtracting one from b_min first.
-         */
-        for (j = length - 1; j >= 0 && b_min[j]-- == 0x00; j--)
-            ;
-        if (memcmp(a_max, b_min, length) == 0) {
-            IPAddressOrRange *merged;
+            if (!extract_min_max(prev, p_min, p_max, length))
+                return 0;

-            if (!make_addressRange(&merged, a_min, b_max, length))
+            /*
+             * Reject overlap with the previous accepted entry.
+             */
+            if (memcmp(p_max, c_min, length) >= 0)
                 return 0;
-            (void)sk_IPAddressOrRange_set(aors, i, merged);
-            (void)sk_IPAddressOrRange_delete(aors, i + 1);
-            IPAddressOrRange_free(a);
-            IPAddressOrRange_free(b);
-            --i;
-            continue;
-        }
-    }

-    /*
-     * Check for inverted final range.
-     */
-    j = sk_IPAddressOrRange_num(aors) - 1;
-    {
-        IPAddressOrRange *a = sk_IPAddressOrRange_value(aors, j);
+            /*
+             * Adjacency test: does c_min - 1 equal p_max?  Work on a
+             * scratch copy so the original c_min stays intact for use
+             * as the lower bound if we end up keeping cur.
+             */
+            memcpy(c_min_minus_one, c_min, length);
+            for (j = length - 1;
+                j >= 0 && c_min_minus_one[j]-- == 0x00;
+                j--)
+                ;
+            if (memcmp(p_max, c_min_minus_one, length) == 0) {
+                IPAddressOrRange *merged;

-        if (a != NULL && a->type == IPAddressOrRange_addressRange) {
-            unsigned char a_min[ADDR_RAW_BUF_LEN], a_max[ADDR_RAW_BUF_LEN];
+                if (!make_addressRange(&merged, p_min, c_max, length))
+                    return 0;
+                /*
+                 * Replace prev with merged, free the originals, and
+                 * NULL the source slot so the stack does not retain a
+                 * second reference to cur.
+                 */
+                (void)sk_IPAddressOrRange_set(aors, write - 1, merged);
+                IPAddressOrRange_free(prev);
+                IPAddressOrRange_free(cur);
+                (void)sk_IPAddressOrRange_set(aors, read, NULL);
+                continue;
+            }
+        }

-            if (!extract_min_max(a, a_min, a_max, length))
-                return 0;
-            if (memcmp(a_min, a_max, length) > 0)
-                return 0;
+        /*
+         * Keep cur.  Slide it forward into the write slot if we have
+         * fallen behind, and NULL the source slot to avoid duplicate
+         * ownership.
+         */
+        if (write != read) {
+            (void)sk_IPAddressOrRange_set(aors, write, cur);
+            (void)sk_IPAddressOrRange_set(aors, read, NULL);
         }
+        write++;
     }

+    /*
+     * Compaction succeeded: every slot at [write..n-1] is NULL, so
+     * popping the tail leaves the canonicalised list at [0..write-1].
+     */
+    while (sk_IPAddressOrRange_num(aors) > write)
+        (void)sk_IPAddressOrRange_pop(aors);
     return 1;
 }

diff --git a/crypto/x509/v3_asid.c b/crypto/x509/v3_asid.c
index 8470e0f134..c00ded15f9 100644
--- a/crypto/x509/v3_asid.c
+++ b/crypto/x509/v3_asid.c
@@ -347,13 +347,22 @@ int X509v3_asid_is_canonical(ASIdentifiers *asid)

 /*
  * Whack an ASIdentifierChoice into canonical form.
+ *
+ * After the initial sort, the merge runs as a single linear sweep
+ * over the list using a write index.  Each entry is examined once;
+ * adjacent / mergeable entries extend the previous output's upper
+ * bound in O(1) and the source slot is left NULL so the asn1 free
+ * machinery does not double-free on a subsequent abort.  Total cost
+ * is O(N log N) sort + O(N) merge, with no stack deletes inside the
+ * loop.
  */
 static int ASIdentifierChoice_canonize(ASIdentifierChoice *choice)
 {
     ASN1_INTEGER *a_max_plus_one = NULL;
     ASN1_INTEGER *orig;
     BIGNUM *bn = NULL;
-    int i, ret = 0;
+    int read, write = 0, n;
+    int ret = 0;

     /*
      * Nothing to do for empty element or inheritance.
@@ -370,112 +379,135 @@ static int ASIdentifierChoice_canonize(ASIdentifierChoice *choice)
     }

     /*
-     * We have a non-empty list.  Sort it.
+     * Sort the list, then merge in a single sweep using a write index.
      */
     sk_ASIdOrRange_sort(choice->u.asIdsOrRanges);
+    n = sk_ASIdOrRange_num(choice->u.asIdsOrRanges);

-    /*
-     * Now check for errors and suboptimal encoding, rejecting the
-     * former and fixing the latter.
-     */
-    for (i = 0; i < sk_ASIdOrRange_num(choice->u.asIdsOrRanges) - 1; i++) {
-        ASIdOrRange *a = sk_ASIdOrRange_value(choice->u.asIdsOrRanges, i);
-        ASIdOrRange *b = sk_ASIdOrRange_value(choice->u.asIdsOrRanges, i + 1);
-        ASN1_INTEGER *a_min = NULL, *a_max = NULL, *b_min = NULL, *b_max = NULL;
+    for (read = 0; read < n; read++) {
+        ASIdOrRange *cur = sk_ASIdOrRange_value(choice->u.asIdsOrRanges, read);
+        ASN1_INTEGER *c_min = NULL, *c_max = NULL;

-        if (!extract_min_max(a, &a_min, &a_max)
-            || !extract_min_max(b, &b_min, &b_max))
+        if (!extract_min_max(cur, &c_min, &c_max))
             goto done;

         /*
-         * Make sure we're properly sorted (paranoia).
+         * Punt inverted range.
          */
-        if (!ossl_assert(ASN1_INTEGER_cmp(a_min, b_min) <= 0))
+        if (ASN1_INTEGER_cmp(c_min, c_max) > 0)
             goto done;

-        /*
-         * Punt inverted ranges.
-         */
-        if (ASN1_INTEGER_cmp(a_min, a_max) > 0 || ASN1_INTEGER_cmp(b_min, b_max) > 0)
-            goto done;
+        if (write > 0) {
+            ASIdOrRange *prev = sk_ASIdOrRange_value(choice->u.asIdsOrRanges,
+                write - 1);
+            ASN1_INTEGER *p_min = NULL, *p_max = NULL;

-        /*
-         * Check for overlaps.
-         */
-        if (ASN1_INTEGER_cmp(a_max, b_min) >= 0) {
-            ERR_raise(ERR_LIB_X509V3, X509V3_R_EXTENSION_VALUE_ERROR);
-            goto done;
-        }
+            if (!extract_min_max(prev, &p_min, &p_max))
+                goto done;

-        /*
-         * Calculate a_max + 1 to check for adjacency.
-         */
-        if ((bn == NULL && (bn = BN_new()) == NULL) || ASN1_INTEGER_to_BN(a_max, bn) == NULL || !BN_add_word(bn, 1)) {
-            ERR_raise(ERR_LIB_X509V3, ERR_R_BN_LIB);
-            goto done;
-        }
+            /*
+             * Make sure we're properly sorted (paranoia).
+             */
+            if (!ossl_assert(ASN1_INTEGER_cmp(p_min, c_min) <= 0))
+                goto done;

-        if ((a_max_plus_one = BN_to_ASN1_INTEGER(bn, orig = a_max_plus_one)) == NULL) {
-            a_max_plus_one = orig;
-            ERR_raise(ERR_LIB_X509V3, ERR_R_ASN1_LIB);
-            goto done;
-        }
+            /*
+             * Reject overlap with the previous accepted entry.
+             */
+            if (ASN1_INTEGER_cmp(p_max, c_min) >= 0) {
+                ERR_raise(ERR_LIB_X509V3, X509V3_R_EXTENSION_VALUE_ERROR);
+                goto done;
+            }

-        /*
-         * If a and b are adjacent, merge them.
-         */
-        if (ASN1_INTEGER_cmp(a_max_plus_one, b_min) == 0) {
-            ASRange *r;
-            switch (a->type) {
-            case ASIdOrRange_id:
-                if ((r = OPENSSL_malloc(sizeof(*r))) == NULL)
-                    goto done;
-                r->min = a_min;
-                r->max = b_max;
-                a->type = ASIdOrRange_range;
-                a->u.range = r;
-                break;
-            case ASIdOrRange_range:
-                ASN1_INTEGER_free(a->u.range->max);
-                a->u.range->max = b_max;
-                break;
+            /*
+             * Calculate p_max + 1 to check for adjacency.
+             */
+            if ((bn == NULL && (bn = BN_new()) == NULL)
+                || ASN1_INTEGER_to_BN(p_max, bn) == NULL
+                || !BN_add_word(bn, 1)) {
+                ERR_raise(ERR_LIB_X509V3, ERR_R_BN_LIB);
+                goto done;
             }
-            switch (b->type) {
-            case ASIdOrRange_id:
-                b->u.id = NULL;
-                break;
-            case ASIdOrRange_range:
-                b->u.range->max = NULL;
-                break;
+            if ((a_max_plus_one = BN_to_ASN1_INTEGER(bn,
+                     orig = a_max_plus_one))
+                == NULL) {
+                a_max_plus_one = orig;
+                ERR_raise(ERR_LIB_X509V3, ERR_R_ASN1_LIB);
+                goto done;
+            }
+
+            /*
+             * If prev and cur are adjacent, fold cur into prev.
+             */
+            if (ASN1_INTEGER_cmp(a_max_plus_one, c_min) == 0) {
+                ASRange *r;
+
+                switch (prev->type) {
+                case ASIdOrRange_id:
+                    if ((r = OPENSSL_malloc(sizeof(*r))) == NULL)
+                        goto done;
+                    r->min = p_min;
+                    r->max = c_max;
+                    prev->type = ASIdOrRange_range;
+                    prev->u.range = r;
+                    break;
+                case ASIdOrRange_range:
+                    ASN1_INTEGER_free(prev->u.range->max);
+                    prev->u.range->max = c_max;
+                    break;
+                }
+                /*
+                 * Detach c_max from cur so freeing cur does not free
+                 * the value we just transferred to prev.
+                 */
+                switch (cur->type) {
+                case ASIdOrRange_id:
+                    cur->u.id = NULL;
+                    break;
+                case ASIdOrRange_range:
+                    cur->u.range->max = NULL;
+                    break;
+                }
+                ASIdOrRange_free(cur);
+                /*
+                 * NULL the source slot so any later teardown does not
+                 * walk a freed pointer.  We do not advance `write`.
+                 */
+                (void)sk_ASIdOrRange_set(choice->u.asIdsOrRanges, read, NULL);
+                continue;
             }
-            ASIdOrRange_free(b);
-            (void)sk_ASIdOrRange_delete(choice->u.asIdsOrRanges, i + 1);
-            i--;
-            continue;
         }
-    }

-    /*
-     * Check for final inverted range.
-     */
-    i = sk_ASIdOrRange_num(choice->u.asIdsOrRanges) - 1;
-    {
-        ASIdOrRange *a = sk_ASIdOrRange_value(choice->u.asIdsOrRanges, i);
-        ASN1_INTEGER *a_min, *a_max;
-        if (a != NULL && a->type == ASIdOrRange_range) {
-            if (!extract_min_max(a, &a_min, &a_max)
-                || ASN1_INTEGER_cmp(a_min, a_max) > 0)
-                goto done;
+        /*
+         * Keep cur.  Slide it forward into the write slot if we have
+         * fallen behind, and NULL the source slot to avoid duplicate
+         * ownership.
+         */
+        if (write != read) {
+            (void)sk_ASIdOrRange_set(choice->u.asIdsOrRanges, write, cur);
+            (void)sk_ASIdOrRange_set(choice->u.asIdsOrRanges, read, NULL);
         }
+        write++;
     }

-    /* Paranoia */
-    if (!ossl_assert(ASIdentifierChoice_is_canonical(choice)))
-        goto done;
-
     ret = 1;

 done:
+    /*
+     * On success every slot at [write..n-1] is NULL, so popping the
+     * tail leaves the canonicalised list at [0..write-1].  On error we
+     * leave the tail untouched; the slots are either NULL (from earlier
+     * iterations) or original entries the loop never reached, both of
+     * which the caller's ASIdentifierChoice_free path handles safely.
+     */
+    if (ret) {
+        while (sk_ASIdOrRange_num(choice->u.asIdsOrRanges) > write)
+            (void)sk_ASIdOrRange_pop(choice->u.asIdsOrRanges);
+        /* Paranoia */
+        if (!ossl_assert(ASIdentifierChoice_is_canonical(choice)))
+            ret = 0;
+    }
+
     ASN1_INTEGER_free(a_max_plus_one);
     BN_free(bn);
     return ret;
diff --git a/test/v3ext.c b/test/v3ext.c
index 2e852804ee..e2b6441197 100644
--- a/test/v3ext.c
+++ b/test/v3ext.c
@@ -417,6 +417,595 @@ static int test_ext_syntax(void)
     return testresult;
 }

+/*
+ * Number of entries the large-canonize regression tests construct.
+ * Chosen well above the legacy 4096 cap and large enough that the
+ * previous O(N^2) merge would be visibly slow under any sanitiser
+ * configuration, while still small enough to keep CI cost negligible
+ * with the linear merge.
+ */
+#define V3EXT_TEST_LARGE_N 8192
+
+/*
+ * Build an ASIdentifiers extension containing V3EXT_TEST_LARGE_N
+ * adjacent single integers (1, 2, 3, ...), exercise canonize, and
+ * verify that the entire list collapses to one ASIdOrRange_range and
+ * that the post-canonize result reports canonical.  Stresses the
+ * linear merge path that replaced the quadratic in-place delete.
+ */
+static int test_asid_large_canonize_merge(void)
+{
+    ASIdentifiers *asid = NULL;
+    ASN1_INTEGER *val = NULL;
+    int i;
+    int testresult = 0;
+
+    if (!TEST_ptr(asid = ASIdentifiers_new()))
+        goto err;
+
+    for (i = 0; i < V3EXT_TEST_LARGE_N; i++) {
+        if (!TEST_ptr(val = ASN1_INTEGER_new())
+            || !TEST_true(ASN1_INTEGER_set_int64(val, (int64_t)(i + 1))))
+            goto err;
+        if (!TEST_true(X509v3_asid_add_id_or_range(asid, V3_ASID_ASNUM,
+                val, NULL)))
+            goto err;
+        /* Ownership of val transferred on success. */
+        val = NULL;
+    }
+
+    if (!TEST_int_eq(sk_ASIdOrRange_num(asid->asnum->u.asIdsOrRanges),
+            V3EXT_TEST_LARGE_N))
+        goto err;
+
+    if (!TEST_true(X509v3_asid_canonize(asid)))
+        goto err;
+
+    /* The whole list must collapse to a single merged range [1, N]. */
+    if (!TEST_int_eq(sk_ASIdOrRange_num(asid->asnum->u.asIdsOrRanges), 1))
+        goto err;
+    {
+        ASIdOrRange *aor = sk_ASIdOrRange_value(asid->asnum->u.asIdsOrRanges,
+            0);
+        int64_t got_min = 0, got_max = 0;
+
+        if (!TEST_ptr(aor) || !TEST_int_eq(aor->type, ASIdOrRange_range))
+            goto err;
+        if (!TEST_true(ASN1_INTEGER_get_int64(&got_min, aor->u.range->min))
+            || !TEST_int64_t_eq(got_min, 1)
+            || !TEST_true(ASN1_INTEGER_get_int64(&got_max, aor->u.range->max))
+            || !TEST_int64_t_eq(got_max, (int64_t)V3EXT_TEST_LARGE_N))
+            goto err;
+    }
+
+    if (!TEST_int_eq(X509v3_asid_is_canonical(asid), 1))
+        goto err;
+
+    testresult = 1;
+err:
+    ASN1_INTEGER_free(val);
+    ASIdentifiers_free(asid);
+    return testresult;
+}
+
+/*
+ * Build an ASIdentifiers extension containing V3EXT_TEST_LARGE_N
+ * non-mergeable single integers (1, 3, 5, ...).  After canonize the
+ * list must be unchanged in length, every entry must remain an id
+ * with its original value (none silently merged or transformed), and
+ * is_canonical must report canonical -- i.e. the large list is
+ * accepted on its merits with no arbitrary cap kicking in.
+ */
+static int test_asid_large_canonize_no_merge(void)
+{
+    ASIdentifiers *asid = NULL;
+    ASN1_INTEGER *val = NULL;
+    int i;
+    int testresult = 0;
+
+    if (!TEST_ptr(asid = ASIdentifiers_new()))
+        goto err;
+
+    for (i = 0; i < V3EXT_TEST_LARGE_N; i++) {
+        if (!TEST_ptr(val = ASN1_INTEGER_new())
+            || !TEST_true(ASN1_INTEGER_set_int64(val, (int64_t)(2 * i + 1))))
+            goto err;
+        if (!TEST_true(X509v3_asid_add_id_or_range(asid, V3_ASID_ASNUM,
+                val, NULL)))
+            goto err;
+        val = NULL;
+    }
+
+    if (!TEST_true(X509v3_asid_canonize(asid)))
+        goto err;
+
+    if (!TEST_int_eq(sk_ASIdOrRange_num(asid->asnum->u.asIdsOrRanges),
+            V3EXT_TEST_LARGE_N))
+        goto err;
+
+    /*
+     * Every entry must still be a single id with its original value;
+     * adjacent ids should NOT have been merged.
+     */
+    for (i = 0; i < V3EXT_TEST_LARGE_N; i++) {
+        ASIdOrRange *aor = sk_ASIdOrRange_value(asid->asnum->u.asIdsOrRanges,
+            i);
+        int64_t got = 0;
+
+        if (!TEST_ptr(aor)
+            || !TEST_int_eq(aor->type, ASIdOrRange_id)
+            || !TEST_true(ASN1_INTEGER_get_int64(&got, aor->u.id))
+            || !TEST_int64_t_eq(got, (int64_t)(2 * i + 1)))
+            goto err;
+    }
+
+    if (!TEST_int_eq(X509v3_asid_is_canonical(asid), 1))
+        goto err;
+
+    testresult = 1;
+err:
+    ASN1_INTEGER_free(val);
+    ASIdentifiers_free(asid);
+    return testresult;
+}
+
+/*
+ * Build an IPAddrBlocks with a single IPv4 family carrying
+ * V3EXT_TEST_LARGE_N adjacent /32 host prefixes starting at 1.0.0.1
+ * (deliberately offset by one from the prefix-aligned 1.0.0.0 so the
+ * merged span [1.0.0.1, 1.0.32.0] cannot be expressed as a single
+ * prefix and stays an IPAddressOrRange_addressRange).  After canonize
+ * the family must contain exactly one IPAddressOrRange of type
+ * addressRange covering the whole block, and is_canonical must
+ * accept it.  Stresses the linear merge path in v3_addr.c.
+ */
+static int test_addr_large_canonize_merge(void)
+{
+    IPAddrBlocks *addr = NULL;
+    int i;
+    int testresult = 0;
+
+    if (!TEST_ptr(addr = sk_IPAddressFamily_new_null()))
+        goto end;
+
+    for (i = 0; i < V3EXT_TEST_LARGE_N; i++) {
+        unsigned char ip[4];
+        unsigned int v = 0x01000001u + (unsigned int)i; /* 1.0.0.1 + i */
+
+        ip[0] = (unsigned char)((v >> 24) & 0xFF);
+        ip[1] = (unsigned char)((v >> 16) & 0xFF);
+        ip[2] = (unsigned char)((v >> 8) & 0xFF);
+        ip[3] = (unsigned char)(v & 0xFF);
+        if (!TEST_true(X509v3_addr_add_prefix(addr, IANA_AFI_IPV4, NULL,
+                ip, 32)))
+            goto end;
+    }
+
+    if (!TEST_true(X509v3_addr_canonize(addr)))
+        goto end;
+
+    if (!TEST_int_eq(sk_IPAddressFamily_num(addr), 1))
+        goto end;
+    {
+        IPAddressFamily *f = sk_IPAddressFamily_value(addr, 0);
+        IPAddressOrRange *aor;
+        unsigned char got_min[4], got_max[4];
+        unsigned int expected_max = 0x01000001u + V3EXT_TEST_LARGE_N - 1;
+        unsigned char want_min[4] = { 0x01, 0x00, 0x00, 0x01 };
+        unsigned char want_max[4];
+
+        want_max[0] = (unsigned char)((expected_max >> 24) & 0xFF);
+        want_max[1] = (unsigned char)((expected_max >> 16) & 0xFF);
+        want_max[2] = (unsigned char)((expected_max >> 8) & 0xFF);
+        want_max[3] = (unsigned char)(expected_max & 0xFF);
+
+        if (!TEST_ptr(f)
+            || !TEST_int_eq(f->ipAddressChoice->type,
+                IPAddressChoice_addressesOrRanges)
+            || !TEST_int_eq(sk_IPAddressOrRange_num(
+                                f->ipAddressChoice->u.addressesOrRanges),
+                1))
+            goto end;
+
+        aor = sk_IPAddressOrRange_value(f->ipAddressChoice->u.addressesOrRanges,
+            0);
+        if (!TEST_ptr(aor)
+            || !TEST_int_eq(aor->type, IPAddressOrRange_addressRange))
+            goto end;
+        if (!TEST_int_eq(X509v3_addr_get_range(aor, IANA_AFI_IPV4,
+                             got_min, got_max, sizeof(got_min)),
+                4)
+            || !TEST_mem_eq(got_min, 4, want_min, 4)
+            || !TEST_mem_eq(got_max, 4, want_max, 4))
+            goto end;
+    }
+
+    if (!TEST_int_eq(X509v3_addr_is_canonical(addr), 1))
+        goto end;
+
+    testresult = 1;
+end:
+    sk_IPAddressFamily_pop_free(addr, IPAddressFamily_free);
+    return testresult;
+}
+
+/*
+ * Interleaved merge / no-merge pattern, exercising the slide-forward
+ * arm of the linear-sweep compaction in ASIdentifierChoice_canonize.
+ *
+ * We build pairs of adjacent integers separated by gaps: (1, 2), (5, 6),
+ * (9, 10), ...  Each pair merges to a single range, so the canonical
+ * output has exactly V3EXT_TEST_LARGE_N / 2 entries.  Critically, after
+ * the second element of every pair merges into the first the merge
+ * loop's `write` index falls behind `read`; the *next* (non-mergeable)
+ * pair-start must then be slid forward into slot `write` and the source
+ * slot at `read` must be NULL'd.  This is the path with no coverage in
+ * the all-merge or all-no-merge tests.
+ */
+static int test_asid_interleaved_canonize(void)
+{
+    ASIdentifiers *asid = NULL;
+    ASN1_INTEGER *val = NULL;
+    int i;
+    int expected = V3EXT_TEST_LARGE_N / 2;
+    int testresult = 0;
+
+    if (!TEST_ptr(asid = ASIdentifiers_new()))
+        goto err;
+
+    for (i = 0; i < V3EXT_TEST_LARGE_N; i++) {
+        /* Pair starts at 4*p, then 4*p+1; the next pair starts at 4*(p+1). */
+        int pair = i / 2;
+        int within = i % 2;
+        int64_t v = 4 * (int64_t)pair + within + 1;
+
+        if (!TEST_ptr(val = ASN1_INTEGER_new())
+            || !TEST_true(ASN1_INTEGER_set_int64(val, v)))
+            goto err;
+        if (!TEST_true(X509v3_asid_add_id_or_range(asid, V3_ASID_ASNUM,
+                val, NULL)))
+            goto err;
+        val = NULL;
+    }
+
+    if (!TEST_true(X509v3_asid_canonize(asid)))
+        goto err;
+
+    if (!TEST_int_eq(sk_ASIdOrRange_num(asid->asnum->u.asIdsOrRanges),
+            expected))
+        goto err;
+
+    /* Every entry must now be a 2-wide range (the merge result of a pair). */
+    for (i = 0; i < expected; i++) {
+        ASIdOrRange *aor = sk_ASIdOrRange_value(asid->asnum->u.asIdsOrRanges, i);
+
+        if (!TEST_ptr(aor) || !TEST_int_eq(aor->type, ASIdOrRange_range))
+            goto err;
+    }
+
+    if (!TEST_int_eq(X509v3_asid_is_canonical(asid), 1))
+        goto err;
+
+    testresult = 1;
+err:
+    ASN1_INTEGER_free(val);
+    ASIdentifiers_free(asid);
+    return testresult;
+}
+
+/*
+ * IP-address counterpart to test_asid_interleaved_canonize: pairs of
+ * adjacent /32 prefixes separated by a gap of 2, so each pair merges
+ * but the next pair-start must be slid forward.
+ */
+static int test_addr_interleaved_canonize(void)
+{
+    IPAddrBlocks *addr = NULL;
+    int i;
+    int expected = V3EXT_TEST_LARGE_N / 2;
+    int testresult = 0;
+
+    if (!TEST_ptr(addr = sk_IPAddressFamily_new_null()))
+        goto end;
+
+    for (i = 0; i < V3EXT_TEST_LARGE_N; i++) {
+        unsigned char ip[4];
+        unsigned int pair = (unsigned int)(i / 2);
+        unsigned int within = (unsigned int)(i % 2);
+        unsigned int v = 0x01000000u + 4u * pair + within;
+
+        ip[0] = (unsigned char)((v >> 24) & 0xFF);
+        ip[1] = (unsigned char)((v >> 16) & 0xFF);
+        ip[2] = (unsigned char)((v >> 8) & 0xFF);
+        ip[3] = (unsigned char)(v & 0xFF);
+        if (!TEST_true(X509v3_addr_add_prefix(addr, IANA_AFI_IPV4, NULL,
+                ip, 32)))
+            goto end;
+    }
+
+    if (!TEST_true(X509v3_addr_canonize(addr)))
+        goto end;
+
+    if (!TEST_int_eq(sk_IPAddressFamily_num(addr), 1))
+        goto end;
+    {
+        IPAddressFamily *f = sk_IPAddressFamily_value(addr, 0);
+
+        if (!TEST_ptr(f)
+            || !TEST_int_eq(f->ipAddressChoice->type,
+                IPAddressChoice_addressesOrRanges)
+            || !TEST_int_eq(sk_IPAddressOrRange_num(
+                                f->ipAddressChoice->u.addressesOrRanges),
+                expected))
+            goto end;
+    }
+
+    if (!TEST_int_eq(X509v3_addr_is_canonical(addr), 1))
+        goto end;
+
+    testresult = 1;
+end:
+    sk_IPAddressFamily_pop_free(addr, IPAddressFamily_free);
+    return testresult;
+}
+
+/*
+ * Trigger an overlap-detection error partway through the linear
+ * merge.  The first V3EXT_TEST_LARGE_N / 2 entries are adjacent and
+ * mergeable; entry K is a duplicate of entry K-1 (overlap).  The
+ * canonize call must return 0, and the caller's normal teardown of
+ * the choice must safely free the stack -- some slots hold merged
+ * results, some hold NULL (from earlier merges), and some hold
+ * originals that the loop never reached.  ASan / UBSan-instrumented
+ * builds will catch any double-free or use-after-free in the
+ * teardown that the mixed-state-on-error invariant claims to avoid.
+ */
+static int test_asid_canonize_error_midsweep(void)
+{
+    ASIdentifiers *asid = NULL;
+    ASN1_INTEGER *val = NULL;
+    int i;
+    int n = V3EXT_TEST_LARGE_N;
+    int k = n / 2;
+    int testresult = 0;
+
+    if (!TEST_ptr(asid = ASIdentifiers_new()))
+        goto err;
+
+    for (i = 0; i < n; i++) {
+        /*
+         * Entries 1..k are 1,2,...,k (all adjacent).
+         * Entry k+1 duplicates entry k (overlap, triggers the error).
+         * Remaining entries are far away so they sort after the overlap.
+         */
+        int64_t v;
+
+        if (i < k)
+            v = (int64_t)i + 1;
+        else if (i == k)
+            v = (int64_t)k; /* duplicate, overlap */
+        else
+            v = (int64_t)i + 1000000; /* far suffix, untouched */
+
+        if (!TEST_ptr(val = ASN1_INTEGER_new())
+            || !TEST_true(ASN1_INTEGER_set_int64(val, v)))
+            goto err;
+        if (!TEST_true(X509v3_asid_add_id_or_range(asid, V3_ASID_ASNUM,
+                val, NULL)))
+            goto err;
+        val = NULL;
+    }
+
+    /* canonize must reject overlap. */
+    if (!TEST_int_eq(X509v3_asid_canonize(asid), 0))
+        goto err;
+
+    /*
+     * Successful return below relies on ASIdentifiers_free walking
+     * the partially-compacted stack without UAF or double-free.
+     * Under ASan / UBSan that walk is the actual test.
+     */
+    testresult = 1;
+err:
+    ASN1_INTEGER_free(val);
+    ASIdentifiers_free(asid);
+    return testresult;
+}
+
+/*
+ * Trigger an overlap-detection error partway through the linear merge
+ * in IPAddressOrRanges_canonize.  Construct a list whose first half is
+ * adjacent and mergeable, with a duplicate at position k that hits the
+ * overlap check after a series of merges has driven write < read.
+ * The canonize call must return 0, and the family's normal teardown
+ * (sk_IPAddressFamily_pop_free) must safely walk the partially
+ * compacted stack -- ASan / UBSan catches any double-free or UAF the
+ * mixed-state-on-error invariant would otherwise miss.  Because the
+ * v3_addr.c canonize uses direct `return 0` rather than a `done:`
+ * cleanup label, the teardown invariant for this file is different
+ * from the asid path and warrants its own coverage.
+ */
+static int test_addr_canonize_error_midsweep(void)
+{
+    IPAddrBlocks *addr = NULL;
+    int i;
+    int n = V3EXT_TEST_LARGE_N;
+    int k = n / 2;
+    int testresult = 0;
+
+    if (!TEST_ptr(addr = sk_IPAddressFamily_new_null()))
+        goto end;
+
+    for (i = 0; i < n; i++) {
+        unsigned char ip[4];
+        unsigned int v;
+
+        if (i < k)
+            v = 0x01000000u + (unsigned int)i; /* adjacent /32 prefixes */
+        else if (i == k)
+            v = 0x01000000u + (unsigned int)(k - 1); /* duplicate, overlap */
+        else
+            v = 0x02000000u + (unsigned int)i; /* far suffix, untouched */
+
+        ip[0] = (unsigned char)((v >> 24) & 0xFF);
+        ip[1] = (unsigned char)((v >> 16) & 0xFF);
+        ip[2] = (unsigned char)((v >> 8) & 0xFF);
+        ip[3] = (unsigned char)(v & 0xFF);
+        if (!TEST_true(X509v3_addr_add_prefix(addr, IANA_AFI_IPV4, NULL,
+                ip, 32)))
+            goto end;
+    }
+
+    /* canonize must reject overlap. */
+    if (!TEST_int_eq(X509v3_addr_canonize(addr), 0))
+        goto end;
+
+    /*
+     * Successful return below relies on sk_IPAddressFamily_pop_free
+     * walking the partially-compacted aors stack without UAF or
+     * double-free.  Under ASan / UBSan that walk is the actual test.
+     */
+    testresult = 1;
+end:
+    sk_IPAddressFamily_pop_free(addr, IPAddressFamily_free);
+    return testresult;
+}
+
+/*
+ * Exercise the merge arm where `cur` is itself a range (rather than a
+ * single integer), hitting the `case ASIdOrRange_range` detach branch
+ * in ASIdentifierChoice_canonize.  Build adjacent 2-wide ranges
+ * [1,2], [3,4], [5,6], ... which all fold into a single big range
+ * [1, 2 * V3EXT_TEST_LARGE_N].
+ */
+static int test_asid_range_merge_canonize(void)
+{
+    ASIdentifiers *asid = NULL;
+    ASN1_INTEGER *minv = NULL, *maxv = NULL;
+    int i;
+    int testresult = 0;
+
+    if (!TEST_ptr(asid = ASIdentifiers_new()))
+        goto err;
+
+    for (i = 0; i < V3EXT_TEST_LARGE_N; i++) {
+        if (!TEST_ptr(minv = ASN1_INTEGER_new())
+            || !TEST_true(ASN1_INTEGER_set_int64(minv,
+                (int64_t)(2 * i + 1)))
+            || !TEST_ptr(maxv = ASN1_INTEGER_new())
+            || !TEST_true(ASN1_INTEGER_set_int64(maxv,
+                (int64_t)(2 * i + 2))))
+            goto err;
+        if (!TEST_true(X509v3_asid_add_id_or_range(asid, V3_ASID_ASNUM,
+                minv, maxv)))
+            goto err;
+        minv = maxv = NULL;
+    }
+
+    if (!TEST_true(X509v3_asid_canonize(asid)))
+        goto err;
+
+    if (!TEST_int_eq(sk_ASIdOrRange_num(asid->asnum->u.asIdsOrRanges), 1))
+        goto err;
+    {
+        ASIdOrRange *aor = sk_ASIdOrRange_value(asid->asnum->u.asIdsOrRanges,
+            0);
+        int64_t got_min = 0, got_max = 0;
+
+        if (!TEST_ptr(aor) || !TEST_int_eq(aor->type, ASIdOrRange_range))
+            goto err;
+        /*
+         * The merged range must cover [1, 2 * V3EXT_TEST_LARGE_N];
+         * incorrect max-propagation would still leave a single range
+         * but with a truncated upper bound.
+         */
+        if (!TEST_true(ASN1_INTEGER_get_int64(&got_min, aor->u.range->min))
+            || !TEST_int64_t_eq(got_min, 1)
+            || !TEST_true(ASN1_INTEGER_get_int64(&got_max, aor->u.range->max))
+            || !TEST_int64_t_eq(got_max, (int64_t)(2 * V3EXT_TEST_LARGE_N)))
+            goto err;
+    }
+
+    if (!TEST_int_eq(X509v3_asid_is_canonical(asid), 1))
+        goto err;
+
+    testresult = 1;
+err:
+    ASN1_INTEGER_free(minv);
+    ASN1_INTEGER_free(maxv);
+    ASIdentifiers_free(asid);
+    return testresult;
+}
+
+/*
+ * Trigger the inverted-range guard partway through the linear merge
+ * in ASIdentifierChoice_canonize.  The first half of the list is
+ * well-formed adjacent integers; entry k is an explicitly inverted
+ * range (min = 1000, max = 100).  X509v3_asid_add_id_or_range does
+ * not validate min <= max for ranges, so the bad entry is admitted
+ * into the list, and canonize must detect it on the sweep.  The
+ * teardown under ASan / UBSan verifies that the early-exit path
+ * leaves the asIdsOrRanges stack in a freeable state.
+ *
+ * The addr-side counterpart of this branch (v3_addr.c:849) is not
+ * reachable through the public API: make_addressRange refuses to
+ * construct an inverted IPAddressOrRange in the first place.  The
+ * guard remains as defence against a DER-decoded extension that
+ * carries inverted min/max bit strings; exercising it from C would
+ * require hand-building an IPAddressOrRange, which would bind the
+ * test to internal ASN.1 layout.
+ */
+static int test_asid_canonize_inverted_midsweep(void)
+{
+    ASIdentifiers *asid = NULL;
+    ASN1_INTEGER *val = NULL, *minv = NULL, *maxv = NULL;
+    int i;
+    int n = V3EXT_TEST_LARGE_N;
+    int k = n / 2;
+    int testresult = 0;
+
+    if (!TEST_ptr(asid = ASIdentifiers_new()))
+        goto err;
+
+    for (i = 0; i < n; i++) {
+        if (i == k) {
+            /* Inverted range: min=1000, max=100. */
+            if (!TEST_ptr(minv = ASN1_INTEGER_new())
+                || !TEST_true(ASN1_INTEGER_set_int64(minv, 1000))
+                || !TEST_ptr(maxv = ASN1_INTEGER_new())
+                || !TEST_true(ASN1_INTEGER_set_int64(maxv, 100)))
+                goto err;
+            if (!TEST_true(X509v3_asid_add_id_or_range(asid, V3_ASID_ASNUM,
+                    minv, maxv)))
+                goto err;
+            minv = maxv = NULL;
+        } else {
+            int64_t v = (i < k) ? (int64_t)i + 1
+                                : (int64_t)i + 1000000; /* far suffix */
+
+            if (!TEST_ptr(val = ASN1_INTEGER_new())
+                || !TEST_true(ASN1_INTEGER_set_int64(val, v)))
+                goto err;
+            if (!TEST_true(X509v3_asid_add_id_or_range(asid, V3_ASID_ASNUM,
+                    val, NULL)))
+                goto err;
+            val = NULL;
+        }
+    }
+
+    /* canonize must reject the inverted entry. */
+    if (!TEST_int_eq(X509v3_asid_canonize(asid), 0))
+        goto err;
+
+    testresult = 1;
+err:
+    ASN1_INTEGER_free(val);
+    ASN1_INTEGER_free(minv);
+    ASN1_INTEGER_free(maxv);
+    ASIdentifiers_free(asid);
+    return testresult;
+}
+
 static int test_addr_subset(void)
 {
     int i;
@@ -480,6 +1069,15 @@ int setup_tests(void)
     ADD_TEST(test_ext_syntax);
     ADD_TEST(test_addr_fam_len);
     ADD_TEST(test_addr_subset);
+    ADD_TEST(test_asid_large_canonize_merge);
+    ADD_TEST(test_asid_large_canonize_no_merge);
+    ADD_TEST(test_addr_large_canonize_merge);
+    ADD_TEST(test_asid_interleaved_canonize);
+    ADD_TEST(test_addr_interleaved_canonize);
+    ADD_TEST(test_asid_canonize_error_midsweep);
+    ADD_TEST(test_addr_canonize_error_midsweep);
+    ADD_TEST(test_asid_range_merge_canonize);
+    ADD_TEST(test_asid_canonize_inverted_midsweep);
 #endif /* OPENSSL_NO_RFC3779 */
     return 1;
 }