From 208bbf8cfda200deaeddfad77e4b43d54e692ba5 Mon Sep 17 00:00:00 2001
From: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Date: Wed, 20 Oct 2021 17:09:21 +1300
Subject: [PATCH 157/266] CVE-2020-25722 s4/dsdb modules: add
 dsdb_get_expected_new_values()

This function collects a superset of all the new values for the specified
attribute that could result from an ldb add or modify message.

In most cases -- where there is a single add or modify -- the exact set
of added values is returned, and this is done reasonably efficiently
using the existing element. Where it gets complicated is when there are
multiple elements for the same attribute in a message. Anything added
before a replace or delete will be included in these results but may not
end up in the database if the message runs its course. Examples:

   sequence           result
1. ADD                the element is returned (exact)
2. REPLACE            the element is returned (exact)
3. ADD, ADD           both elements are concatenated together (exact)
4. ADD, REPLACE       both elements are concatenated together (superset)
5. REPLACE, ADD       both elements are concatenated together (exact)
6. ADD, DEL, ADD      adds are concatenated together (superset)
7. REPLACE, REPLACE   both concatenated (superset)
8. DEL, ADD           last element is returned (exact)

Why this? In the past we have treated dsdb_get_single_valued_attr() as if
it returned the complete set of possible database changes, when in fact it
only returned the last non-delete. That is, it could have missed values
in examples 3-7 above.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=14876

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>

Conflict:NA
Reference:https://gitlab.com/samba-team/samba/-/commit/208bbf8cfda200deaeddfad77e4b43d54e692ba5

---
 source4/dsdb/samdb/ldb_modules/util.c | 121 ++++++++++++++++++++++++++
 1 file changed, 121 insertions(+)

diff --git a/source4/dsdb/samdb/ldb_modules/util.c b/source4/dsdb/samdb/ldb_modules/util.c
index 9519ecfa928..da152e4d754 100644
--- a/source4/dsdb/samdb/ldb_modules/util.c
+++ b/source4/dsdb/samdb/ldb_modules/util.c
@@ -1441,6 +1441,127 @@ void dsdb_req_chain_debug(struct ldb_request *req, int level)
 	talloc_free(s);
 }
 
+/*
+ * Get all the values that *might* be added by an ldb message, as a composite
+ * ldb element.
+ *
+ * This is useful when we need to check all the possible values against some
+ * criteria.
+ *
+ * In cases where a modify message mixes multiple ADDs, DELETEs, and REPLACES,
+ * the returned element might contain more values than would actually end up
+ * in the database if the message was run to its conclusion.
+ *
+ * If the operation is not LDB_ADD or LDB_MODIFY, an operations error is
+ * returned.
+ *
+ * The returned element might not be new, and should not be modified or freed
+ * before the message is finished.
+ */
+
+int dsdb_get_expected_new_values(TALLOC_CTX *mem_ctx,
+				 const struct ldb_message *msg,
+				 const char *attr_name,
+				 struct ldb_message_element **el,
+				 enum ldb_request_type operation)
+{
+	unsigned int i;
+	unsigned int el_count = 0;
+	unsigned int val_count = 0;
+	struct ldb_val *v = NULL;
+	struct ldb_message_element *_el = NULL;
+	*el = NULL;
+
+	if (operation != LDB_ADD && operation != LDB_MODIFY) {
+		DBG_ERR("inapplicable operation type: %d\n", operation);
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+
+	/* count the adding or replacing elements */
+	for (i = 0; i < msg->num_elements; i++) {
+		if (ldb_attr_cmp(msg->elements[i].name, attr_name) == 0) {
+			unsigned int tmp;
+			if ((operation == LDB_MODIFY) &&
+			    (LDB_FLAG_MOD_TYPE(msg->elements[i].flags)
+						== LDB_FLAG_MOD_DELETE)) {
+				continue;
+			}
+			el_count++;
+			tmp = val_count + msg->elements[i].num_values;
+			if (unlikely(tmp < val_count)) {
+				DBG_ERR("too many values for one element!");
+				return LDB_ERR_OPERATIONS_ERROR;
+			}
+			val_count = tmp;
+		}
+	}
+	if (el_count == 0) {
+		/* nothing to see here */
+		return LDB_SUCCESS;
+	}
+
+	if (el_count == 1 || val_count == 0) {
+		/*
+		 * There is one effective element, which we can return as-is,
+		 * OR there are only elements with zero values -- any of which
+		 * will do.
+		 */
+		for (i = 0; i < msg->num_elements; i++) {
+			if (ldb_attr_cmp(msg->elements[i].name, attr_name) == 0) {
+				if ((operation == LDB_MODIFY) &&
+				    (LDB_FLAG_MOD_TYPE(msg->elements[i].flags)
+				     == LDB_FLAG_MOD_DELETE)) {
+					continue;
+				}
+				*el = &msg->elements[i];
+				return LDB_SUCCESS;
+			}
+		}
+	}
+
+	_el = talloc_zero(mem_ctx, struct ldb_message_element);
+	if (_el == NULL) {
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+	_el->name = attr_name;
+
+	if (val_count == 0) {
+		/*
+		 * Seems unlikely, but sometimes we might be adding zero
+		 * values in multiple separate elements. The talloc zero has
+		 * already set the expected values = NULL, num_values = 0.
+		 */
+		*el = _el;
+		return LDB_SUCCESS;
+	}
+
+	_el->values = talloc_array(_el, struct ldb_val, val_count);
+	if (_el->values == NULL) {
+		talloc_free(_el);
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+	_el->num_values = val_count;
+
+	v = _el->values;
+
+	for (i = 0; i < val_count; i++) {
+		if (ldb_attr_cmp(msg->elements[i].name, attr_name) == 0) {
+			if ((operation == LDB_MODIFY) &&
+			    (LDB_FLAG_MOD_TYPE(msg->elements[i].flags)
+						== LDB_FLAG_MOD_DELETE)) {
+				continue;
+			}
+			memcpy(v,
+			       msg->elements[i].values,
+			       msg->elements[i].num_values);
+			v += msg->elements[i].num_values;
+		}
+	}
+
+	*el = _el;
+	return LDB_SUCCESS;
+}
+
 /*
  * Gets back a single-valued attribute by the rules of the DSDB triggers when
  * performing a modify operation.
-- 
2.23.0

