From d6054f0016db05fb5c82177ddbd0a4e8331059a1 Mon Sep 17 00:00:00 2001
From: Alexander Sosedkin <asosedkin@redhat.com>
Date: Wed, 4 Feb 2026 20:03:49 +0100
Subject: [PATCH] x509/name_constraints: name_constraints_node_list_intersect
 over sorted

Fixes: #1773
Fixes: GNUTLS-SA-2026-02-09-2
Fixes: CVE-2025-14831

Signed-off-by: Alexander Sosedkin <asosedkin@redhat.com>

Upstream-Status: Backport [https://gitlab.com/gnutls/gnutls/-/commit/d6054f0016db05fb5c82177ddbd0a4e8331059a1]
CVE: CVE-2025-14831
Signed-off-by: Vijay Anusuri <vanusuri@mvista.com>
---
 lib/x509/name_constraints.c | 347 ++++++++++++++----------------------
 1 file changed, 135 insertions(+), 212 deletions(-)

diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c
index d1006eb..04722bd 100644
--- a/lib/x509/name_constraints.c
+++ b/lib/x509/name_constraints.c
@@ -446,13 +446,6 @@ name_constraints_node_add_copy(gnutls_x509_name_constraints_t nc,
 					     src->name.data, src->name.size);
 }
 
-// for documentation see the implementation
-static int name_constraints_intersect_nodes(
-	gnutls_x509_name_constraints_t nc,
-	const struct name_constraints_node_st *node1,
-	const struct name_constraints_node_st *node2,
-	struct name_constraints_node_st **intersection);
-
 /*-
  * _gnutls_x509_name_constraints_is_empty:
  * @nc: name constraints structure
@@ -716,129 +709,143 @@ typedef char assert_ipaddr[(GNUTLS_SAN_IPADDRESS <= GNUTLS_SAN_MAX) ? 1 : -1];
 static int name_constraints_node_list_intersect(
 	gnutls_x509_name_constraints_t nc,
 	struct name_constraints_node_list_st *permitted,
-	const struct name_constraints_node_list_st *permitted2,
+	struct name_constraints_node_list_st *permitted2,
 	struct name_constraints_node_list_st *excluded)
 {
-	struct name_constraints_node_st *tmp;
-	int ret, type, used;
-	struct name_constraints_node_list_st removed = { .data = NULL,
-							 .size = 0,
-							 .capacity = 0 };
+	struct name_constraints_node_st *nc1, *nc2;
+	struct name_constraints_node_list_st result = { 0 };
+	struct name_constraints_node_list_st unsupp2 = { 0 };
+	enum name_constraint_relation rel;
+	unsigned type;
+	int ret = GNUTLS_E_SUCCESS;
+	size_t i, j, p1_unsupp = 0, p2_unsupp = 0;
+	type_bitmask_t universal_exclude_needed = 0;
+	type_bitmask_t types_in_p1 = 0, types_in_p2 = 0;
 	static const unsigned char universal_ip[32] = { 0 };
 
-	/* bitmask to see if we need to add universal excluded constraints
-	 * (see phase 3 for details) */
-	type_bitmask_t types_with_empty_intersection = 0;
-
 	if (permitted->size == 0 || permitted2->size == 0)
-		return 0;
+		return GNUTLS_E_SUCCESS;
 
-	/* Phase 1
-	 * For each name in PERMITTED, if a PERMITTED2 does not contain a name
-	 * with the same type, move the original name to REMOVED.
-	 * Do this also for node of unknown type (not DNS, email, IP) */
-	for (size_t i = 0; i < permitted->size;) {
-		struct name_constraints_node_st *t = permitted->data[i];
-		const struct name_constraints_node_st *found = NULL;
-
-		for (size_t j = 0; j < permitted2->size; j++) {
-			const struct name_constraints_node_st *t2 =
-				permitted2->data[j];
-			if (t->type == t2->type) {
-				// check bounds (we will use 't->type' as index)
-				if (t->type > GNUTLS_SAN_MAX || t->type == 0) {
-					gnutls_assert();
-					ret = GNUTLS_E_INTERNAL_ERROR;
-					goto cleanup;
-				}
-				// note the possibility of empty intersection for this type
-				// if we add something to the intersection in phase 2,
-				// we will reset this flag back to 0 then
-				type_bitmask_set(types_with_empty_intersection,
-						 t->type);
-				found = t2;
-				break;
-			}
+	/* make sorted views of the arrays */
+	ret = ensure_sorted(permitted);
+	if (ret < 0) {
+		gnutls_assert();
+		goto cleanup;
+	}
+	ret = ensure_sorted(permitted2);
+	if (ret < 0) {
+		gnutls_assert();
+		goto cleanup;
+	}
+
+	/* deal with the leading unsupported types first: count, then union */
+	while (p1_unsupp < permitted->size &&
+	       !is_supported_type(permitted->sorted_view[p1_unsupp]->type))
+		p1_unsupp++;
+	while (p2_unsupp < permitted2->size &&
+	       !is_supported_type(permitted2->sorted_view[p2_unsupp]->type))
+		p2_unsupp++;
+	if (p1_unsupp) { /* copy p1 unsupported type pointers into result */
+		result.data = gnutls_calloc(
+			p1_unsupp, sizeof(struct name_constraints_node_st *));
+		if (!result.data) {
+			ret = GNUTLS_E_MEMORY_ERROR;
+			gnutls_assert();
+			goto cleanup;
 		}
+		memcpy(result.data, permitted->sorted_view,
+		       p1_unsupp * sizeof(struct name_constraints_node_st *));
+		result.size = result.capacity = p1_unsupp;
+		result.dirty = true;
+	}
+	if (p2_unsupp) { /* union will make deep copies from p2 */
+		unsupp2.data = permitted2->sorted_view; /* so, just alias */
+		unsupp2.size = unsupp2.capacity = p2_unsupp;
+		unsupp2.dirty = false; /* we know it's sorted */
+		unsupp2.sorted_view = permitted2->sorted_view;
+		ret = name_constraints_node_list_union(nc, &result, &unsupp2);
+		if (ret < 0) {
+			gnutls_assert();
+			goto cleanup;
+		}
+	}
 
-		if (found != NULL && is_supported_type(t->type)) {
-			/* move node from PERMITTED to REMOVED */
-			ret = name_constraints_node_list_add(&removed, t);
-			if (ret < 0) {
-				gnutls_assert();
-				goto cleanup;
-			}
-			/* remove node by swapping */
-			if (i < permitted->size - 1)
-				permitted->data[i] =
-					permitted->data[permitted->size - 1];
-			permitted->size--;
-			permitted->dirty = true;
-			continue;
+	/* with that out of the way, pre-compute the supported types we have */
+	for (i = p1_unsupp; i < permitted->size; i++) {
+		type = permitted->sorted_view[i]->type;
+		if (type < 1 || type > GNUTLS_SAN_MAX) {
+			ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+			goto cleanup;
+		}
+		type_bitmask_set(types_in_p1, type);
+	}
+	for (j = p2_unsupp; j < permitted2->size; j++) {
+		type = permitted2->sorted_view[j]->type;
+		if (type < 1 || type > GNUTLS_SAN_MAX) {
+			ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+			goto cleanup;
 		}
-		i++;
+		type_bitmask_set(types_in_p2, type);
 	}
+	/* universal excludes might be needed for types intersecting to empty */
+	universal_exclude_needed = types_in_p1 & types_in_p2;
+
+	/* go through supported type NCs and intersect in a single pass */
+	i = p1_unsupp;
+	j = p2_unsupp;
+	while (i < permitted->size || j < permitted2->size) {
+		nc1 = (i < permitted->size) ? permitted->sorted_view[i] : NULL;
+		nc2 = (j < permitted2->size) ? permitted2->sorted_view[j] :
+					       NULL;
+		rel = compare_name_constraint_nodes(nc1, nc2);
 
-	/* Phase 2
-	 * iterate through all combinations from PERMITTED2 and PERMITTED
-	 * and create intersections of nodes with same type */
-	for (size_t i = 0; i < permitted2->size; i++) {
-		const struct name_constraints_node_st *t2 = permitted2->data[i];
-
-		// current PERMITTED2 node has not yet been used for any intersection
-		// (and is not in REMOVED either)
-		used = 0;
-		for (size_t j = 0; j < removed.size; j++) {
-			const struct name_constraints_node_st *t =
-				removed.data[j];
-			// save intersection of name constraints into tmp
-			ret = name_constraints_intersect_nodes(nc, t, t2, &tmp);
-			if (ret < 0) {
-				gnutls_assert();
-				goto cleanup;
-			}
-			used = 1;
-			// if intersection is not empty
-			if (tmp !=
-			    NULL) { // intersection for this type is not empty
-				// check bounds
-				if (tmp->type > GNUTLS_SAN_MAX ||
-				    tmp->type == 0) {
-					gnutls_free(tmp);
-					return gnutls_assert_val(
-						GNUTLS_E_INTERNAL_ERROR);
-				}
-				// we will not add universal excluded constraint for this type
-				type_bitmask_clr(types_with_empty_intersection,
-						 tmp->type);
-				// add intersection node to PERMITTED
-				ret = name_constraints_node_list_add(permitted,
-								     tmp);
-				if (ret < 0) {
-					gnutls_assert();
-					goto cleanup;
-				}
-			}
+		switch (rel) {
+		case NC_SORTS_BEFORE:
+			assert(nc1 != NULL); /* comparator-guaranteed */
+			/* if nothing to intersect with, shallow-copy nc1 */
+			if (!type_bitmask_in(types_in_p2, nc1->type))
+				ret = name_constraints_node_list_add(&result,
+								     nc1);
+			i++; /* otherwise skip nc1 */
+			break;
+		case NC_SORTS_AFTER:
+			assert(nc2 != NULL); /* comparator-guaranteed */
+			/* if nothing to intersect with, deep-copy nc2 */
+			if (!type_bitmask_in(types_in_p1, nc2->type))
+				ret = name_constraints_node_add_copy(
+					nc, &result, nc2);
+			j++; /* otherwise skip nc2 */
+			break;
+		case NC_INCLUDED_BY: /* add nc1, shallow-copy */
+			assert(nc1 != NULL && nc2 != NULL); /* comparator */
+			type_bitmask_clr(universal_exclude_needed, nc1->type);
+			ret = name_constraints_node_list_add(&result, nc1);
+			i++;
+			break;
+		case NC_INCLUDES: /* pick nc2, deep-copy */
+			assert(nc1 != NULL && nc2 != NULL); /* comparator */
+			type_bitmask_clr(universal_exclude_needed, nc2->type);
+			ret = name_constraints_node_add_copy(nc, &result, nc2);
+			j++;
+			break;
+		case NC_EQUAL: /* pick whichever: nc1, shallow-copy */
+			assert(nc1 != NULL && nc2 != NULL); /* loop condition */
+			type_bitmask_clr(universal_exclude_needed, nc1->type);
+			ret = name_constraints_node_list_add(&result, nc1);
+			i++;
+			j++;
+			break;
 		}
-		// if the node from PERMITTED2 was not used for intersection, copy it to DEST
-		// Beware: also copies nodes other than DNS, email, IP,
-		//       since their counterpart may have been moved in phase 1.
-		if (!used) {
-			ret = name_constraints_node_add_copy(nc, permitted, t2);
-			if (ret < 0) {
-				gnutls_assert();
-				goto cleanup;
-			}
+		if (ret < 0) {
+			gnutls_assert();
+			goto cleanup;
 		}
 	}
 
-	/* Phase 3
-	 * For each type: If we have empty permitted name constraints now
-	 * and we didn't have at the beginning, we have to add a new
-	 * excluded constraint with universal wildcard
-	 * (since the intersection of permitted is now empty). */
+	/* finishing touch: add universal excluded constraints for types where
+	 * both lists had constraints, but all intersections ended up empty */
 	for (type = 1; type <= GNUTLS_SAN_MAX; type++) {
-		if (!type_bitmask_in(types_with_empty_intersection, type))
+		if (!type_bitmask_in(universal_exclude_needed, type))
 			continue;
 		_gnutls_hard_log(
 			"Adding universal excluded name constraint for type %d.\n",
@@ -871,14 +878,24 @@ static int name_constraints_node_list_intersect(
 				goto cleanup;
 			}
 			break;
-		default: // do nothing, at least one node was already moved in phase 1
-			break;
+		default: /* unsupported type; should be unreacheable */
+			ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+			goto cleanup;
 		}
 	}
-	ret = GNUTLS_E_SUCCESS;
 
+	gnutls_free(permitted->data);
+	gnutls_free(permitted->sorted_view);
+	permitted->data = result.data;
+	permitted->sorted_view = NULL;
+	permitted->size = result.size;
+	permitted->capacity = result.capacity;
+	permitted->dirty = true;
+
+	result.data = NULL;
+	ret = GNUTLS_E_SUCCESS;
 cleanup:
-	gnutls_free(removed.data);
+	name_constraints_node_list_clear(&result);
 	return ret;
 }
 
@@ -1254,100 +1271,6 @@ static unsigned email_matches(const gnutls_datum_t *name,
 	return rel == NC_EQUAL || rel == NC_INCLUDED_BY;
 }
 
-/*-
- * name_constraints_intersect_nodes:
- * @nc1: name constraints node 1
- * @nc2: name constraints node 2
- * @_intersection: newly allocated node with intersected constraints,
- *		 NULL if the intersection is empty
- *
- * Inspect 2 name constraints nodes (of possibly different types) and allocate
- * a new node with intersection of given constraints.
- *
- * Returns: On success, %GNUTLS_E_SUCCESS (0) is returned, otherwise a negative error value.
- -*/
-static int name_constraints_intersect_nodes(
-	gnutls_x509_name_constraints_t nc,
-	const struct name_constraints_node_st *node1,
-	const struct name_constraints_node_st *node2,
-	struct name_constraints_node_st **_intersection)
-{
-	// presume empty intersection
-	struct name_constraints_node_st *intersection = NULL;
-	const struct name_constraints_node_st *to_copy = NULL;
-	enum name_constraint_relation rel;
-
-	*_intersection = NULL;
-
-	if (node1->type != node2->type) {
-		return GNUTLS_E_SUCCESS;
-	}
-	switch (node1->type) {
-	case GNUTLS_SAN_DNSNAME:
-		rel = compare_dns_names(&node1->name, &node2->name);
-		switch (rel) {
-		case NC_EQUAL: // equal means doesn't matter which one
-		case NC_INCLUDES: // node2 is more specific
-			to_copy = node2;
-			break;
-		case NC_INCLUDED_BY: // node1 is more specific
-			to_copy = node1;
-			break;
-		case NC_SORTS_BEFORE: // no intersection
-		case NC_SORTS_AFTER: // no intersection
-			return GNUTLS_E_SUCCESS;
-		}
-		break;
-	case GNUTLS_SAN_RFC822NAME:
-		rel = compare_emails(&node1->name, &node2->name);
-		switch (rel) {
-		case NC_EQUAL: // equal means doesn't matter which one
-		case NC_INCLUDES: // node2 is more specific
-			to_copy = node2;
-			break;
-		case NC_INCLUDED_BY: // node1 is more specific
-			to_copy = node1;
-			break;
-		case NC_SORTS_BEFORE: // no intersection
-		case NC_SORTS_AFTER: // no intersection
-			return GNUTLS_E_SUCCESS;
-		}
-		break;
-	case GNUTLS_SAN_IPADDRESS:
-		rel = compare_ip_ncs(&node1->name, &node2->name);
-		switch (rel) {
-		case NC_EQUAL: // equal means doesn't matter which one
-		case NC_INCLUDES: // node2 is more specific
-			to_copy = node2;
-			break;
-		case NC_INCLUDED_BY: // node1 is more specific
-			to_copy = node1;
-			break;
-		case NC_SORTS_BEFORE: // no intersection
-		case NC_SORTS_AFTER: // no intersection
-			return GNUTLS_E_SUCCESS;
-		}
-		break;
-	default:
-		// for other types, we don't know how to do the intersection, assume empty
-		return GNUTLS_E_SUCCESS;
-	}
-
-	// copy existing node if applicable
-	if (to_copy != NULL) {
-		*_intersection = name_constraints_node_new(nc, to_copy->type,
-							   to_copy->name.data,
-							   to_copy->name.size);
-		if (*_intersection == NULL)
-			return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR);
-		intersection = *_intersection;
-
-		assert(intersection->name.data != NULL);
-	}
-
-	return GNUTLS_E_SUCCESS;
-}
-
 /*
  * Returns: true if the certification is acceptable, and false otherwise.
  */
-- 
2.43.0

