From ac2ee18b081483430b42c2cf35e423634aebbeb8 Mon Sep 17 00:00:00 2001
From: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Date: Thu, 30 Oct 2025 16:01:36 +0100
Subject: [PATCH 2/4] lib:replace: Add test for memset_explicit()

Upstream-Status: Backport [https://gitlab.com/samba-team/samba/-/commit/2b17c9816d4373eba365de803eec10435ea038d4]
Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andreas Schneider <asn@samba.org>
Signed-off-by: Khem Raj <raj.khem@gmail.com>
---
 lib/replace/tests/test_memset_explicit.c | 99 ++++++++++++++++++++++++
 lib/replace/wscript                      |  5 ++
 selftest/tests.py                        |  6 ++
 3 files changed, 110 insertions(+)
 create mode 100644 lib/replace/tests/test_memset_explicit.c

diff --git a/lib/replace/tests/test_memset_explicit.c b/lib/replace/tests/test_memset_explicit.c
new file mode 100644
index 0000000..4e56d7a
--- /dev/null
+++ b/lib/replace/tests/test_memset_explicit.c
@@ -0,0 +1,99 @@
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <setjmp.h>
+#include <cmocka.h>
+
+#include "lib/replace/replace.h"
+
+
+/*
+ * To check that a memset_explicit string is being memset when it
+ * appears unused, we meed to be sneaky in our check -- otherwise the
+ * check counts as a use.
+ *
+ * We are sneaky by using a function that seens to take an int
+ * argument which is really a pointer, and we hide that it is a
+ * pointer by masking it.
+ *
+ * For these tests we don't use talloc because the talloc magic gets
+ * in the way a little bit.
+ */
+
+#define MASK 0x12345678
+
+__attribute__((noinline))
+static void check_memset_explicit(intmax_t p, const char *expected, size_t len)
+{
+	size_t i;
+	char *secret = (char *) (p ^ MASK);
+	for (i = 0; i < len; i++) {
+		assert_int_equal(secret[i], expected[i]);
+	}
+}
+
+
+__attribute__((noinline))
+static char *get_secret(off_t offset)
+{
+	char * secret = malloc(7 + offset);
+	memset(secret, 0, 7 + offset);
+	memcpy(secret + offset, "secret", 7);
+	/* avoiding *this* being elided */
+	print_message("secret is '%s'\n", secret);
+	asm("");
+	return secret;
+}
+
+
+static void test_memset_explicit(void ** state)
+{
+	uintptr_t p;
+	char zeros[7] = {0};
+	char *secret = get_secret(0);
+	p = ((uintptr_t)secret) ^ MASK;
+	memset_explicit(secret, 'o', 3);
+	check_memset_explicit(p, "oooret", 7);
+	memset_explicit(secret, 0, 7);
+	check_memset_explicit(p, zeros, 7);
+	free(secret);
+}
+
+static void test_memset_explicit_double_alloc(void ** state)
+{
+	size_t i, found;
+	uintptr_t p, q;
+	char *secret = get_secret(20);
+	p = (uintptr_t)secret ^ MASK;
+	memset_explicit(secret, 'x', 23);
+	free(secret);
+	/*
+	 * Now we malloc the same size again, and hope we got the
+	 * block we just freed.
+	 */
+	found = 0;
+	for (i = 0; i < 1000; i++) {
+		secret = malloc(27);
+		q = (uintptr_t)secret ^ MASK;
+		if (q == p) {
+			q = (uintptr_t)(secret + 20) ^ MASK;
+			check_memset_explicit(q, "xxxret", 7);
+			found ++;
+		}
+		free(secret);
+	}
+	print_message("found freed pointer %zu/1000 times \n",
+		found);
+}
+
+int main(void)
+{
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(test_memset_explicit),
+		cmocka_unit_test(test_memset_explicit_double_alloc),
+	};
+	if (! isatty(1)) {
+		cmocka_set_message_output(CM_OUTPUT_SUBUNIT);
+	}
+	return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/lib/replace/wscript b/lib/replace/wscript
index 574740a..9acebd3 100644
--- a/lib/replace/wscript
+++ b/lib/replace/wscript
@@ -964,6 +964,11 @@ def build(bld):
                      deps='replace replace-test',
                      install=False)
 
+    bld.SAMBA_BINARY('test_memset_explicit',
+                     source='tests/test_memset_explicit.c',
+                     deps='cmocka replace',
+                     for_selftest=True)
+
     # build replacements for stdint.h and stdbool.h if needed
     bld.SAMBA_GENERATOR('replace_stdint_h',
                         rule='cp ${SRC} ${TGT}',
diff --git a/selftest/tests.py b/selftest/tests.py
index deb3c0b..3131905 100644
--- a/selftest/tests.py
+++ b/selftest/tests.py
@@ -414,6 +414,12 @@ plantestsuite("samba.unittests.smb1cli_session", "none",
 plantestsuite("samba.unittests.smb_util_translate", "none",
               [os.path.join(bindir(), "default/libcli/smb/test_util_translate")])
 
+plantestsuite(
+    "samba.unittests.memset_explicit",
+    "none",
+    [os.path.join(bindir(), "default/lib/replace/test_memset_explicit")],
+)
+
 plantestsuite("samba.unittests.talloc_keep_secret", "none",
               [os.path.join(bindir(), "default/lib/util/test_talloc_keep_secret")])
 
