From b90c06cddd9d485c10915bc08fcf1b4beaa065ee Mon Sep 17 00:00:00 2001
From: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Date: Sat, 9 May 2026 22:02:47 +1200
Subject: [PATCH 16/27] CVE-2026-4480/CVE-2026-4408: lib/util: add
 test_string_sub unittests

This demonstrates the logic of talloc_string_sub_{mixed_quoting,unsafe}()

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

Pair-Programmed-With: Stefan Metzmacher <metze@samba.org>

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Signed-off-by: Stefan Metzmacher <metze@samba.org>
---
 lib/util/tests/test_string_sub.c | 1044 ++++++++++++++++++++++++++++++
 lib/util/wscript_build           |    6 +
 selftest/tests.py                |    2 +
 3 files changed, 1052 insertions(+)
 create mode 100644 lib/util/tests/test_string_sub.c

--- /dev/null
+++ b/lib/util/tests/test_string_sub.c
@@ -0,0 +1,1044 @@
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <setjmp.h>
+#include <sys/stat.h>
+#include "replace.h"
+#include <cmocka.h>
+#include "talloc.h"
+
+#include "../substitute.h"
+
+/* set _DEBUG_VERBOSE to print more. */
+#define _DEBUG_VERBOSE
+
+#ifdef _DEBUG_VERBOSE
+#define debug_message(...) print_message(__VA_ARGS__)
+#else
+#define debug_message(...) /* debug_message */
+#endif
+
+
+static int setup_talloc_context(void **state)
+{
+	TALLOC_CTX *mem_ctx = talloc_new(NULL);
+	*state = mem_ctx;
+	return 0;
+}
+
+static int teardown_talloc_context(void **state)
+{
+	TALLOC_CTX *mem_ctx = *state;
+	TALLOC_FREE(mem_ctx);
+	return 0;
+}
+
+struct cmd_expansion {
+	const char *lp_cmd;
+	const char *username;
+	const char *result_cmd;
+	bool modified;
+	bool masked;
+	bool mixed_fallback;
+};
+
+static void _test_talloc_string_sub_unsafe(void **state,
+					   struct cmd_expansion expansions[],
+					   size_t n_expansions,
+					   const char *unsafe_characters)
+{
+	TALLOC_CTX *mem_ctx = *state;
+	size_t i;
+
+	for (i = 0; i < n_expansions; i++) {
+		struct cmd_expansion t = expansions[i];
+		char *result_cmd = NULL;
+		bool masked;
+		bool mixed_fallback;
+		bool modified;
+		bool flags_correct;
+		bool mixed;
+		int cmp;
+
+		mixed = talloc_string_sub_mixed_quoting(t.lp_cmd, 'u');
+
+		result_cmd = talloc_string_sub_unsafe(mem_ctx,
+						      t.lp_cmd,
+						      'u',
+						      t.username,
+						      unsafe_characters,
+						      '_',
+						      "FallbackUsername",
+						      &modified,
+						      &masked,
+						      &mixed_fallback);
+		assert_ptr_not_equal(result_cmd, NULL);
+		assert_ptr_not_equal(t.result_cmd, NULL);
+
+		cmp = strcmp(t.result_cmd, result_cmd);
+		flags_correct = (modified == t.modified &&
+				 masked == t.masked &&
+				 mixed_fallback == t.mixed_fallback);
+
+		if (cmp == 0) {
+			debug_message("[%zu] «%s» «%s»   ->   «%s»; AS EXPECTED\n",
+				      i, t.lp_cmd,
+				      t.username,
+				      result_cmd);
+		} else {
+			debug_message("[%zu] «%s» «%s»; "
+				      "expected   [%zu] «%s»   got  [%zu] «%s»\033[1;31m BAD! \033[0m\n",
+				      i, t.lp_cmd,
+				      t.username,
+				      strlen(t.result_cmd), t.result_cmd,
+				      strlen(result_cmd), result_cmd);
+		}
+		assert_int_equal(cmp, 0);
+		if (!flags_correct) {
+			debug_message("[%zu] ", i);
+#define _FLAG(x) debug_message((t. x  == x) ? "%s: %s √; ":		\
+			       "%s \033[1;31m expected %s \033[0m; ",	\
+			       #x, t.x ? "true": "false");
+			_FLAG(modified);
+			_FLAG(masked);
+			_FLAG(mixed_fallback);
+			debug_message("\n");
+		}
+		assert_int_equal(flags_correct, true);
+		if (mixed_fallback != mixed) {
+			debug_message("[%zu] %s mixed \033[1;31m expected %s \033[0m; ",
+				      i, t.lp_cmd,
+				      mixed_fallback ? "true": "false");
+		}
+		assert_int_equal(mixed_fallback, mixed);
+#undef _FLAG
+	}
+	debug_message("ALL correct\n");
+}
+
+static void test_talloc_string_sub_unsafe(void **state)
+{
+	const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS;
+
+	static struct cmd_expansion expansions[] = {
+		{
+			"/bin/echo \"bob'",
+			"bob",
+			"/bin/echo \"bob'",
+			false,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"bob",
+			"/bin/echo 'bob'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob",
+			"/bin/echo 'bob'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob'",
+			"/bin/echo 'bob_'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob'''",
+			"/bin/echo 'bob___'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob\'",
+			"/bin/echo 'bob_'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo '%u",
+			"bob bob bob",
+			"/bin/echo 'FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo \"%u\"",
+			" ",
+			"/bin/echo ' '",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"--uu=%u\"",
+			"bob",
+			"/bin/echo \"--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo \"--uu=%u\"",
+			"bob !0",
+			"/bin/echo \"--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo %u",
+			"!0",
+			"/bin/echo '!0'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"--uu=%u\"",
+			"bob \\",
+			"/bin/echo \"--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"bob >> x",
+			"/bin/echo --uu='bob __ x'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo '--uu=%u\"",
+			"bob",
+			"/bin/echo '--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"bob",
+			"/bin/echo --uu='bob'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo --uu'=%u'",
+			"bob",
+			"/bin/echo --uu'=FallbackUsername'",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu'=%u'",
+			"`ls`",
+			"/bin/echo --uu'=FallbackUsername'",
+			true,
+			true,
+			true,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"u%u%u%u%u",
+			"/bin/echo --uu='u_u_u_u_u'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"$(ls)",
+			"/bin/echo --uu='_(ls)'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"`ls`",
+			"/bin/echo --uu='_ls_'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo --uu='1' %u",
+			"`ls`",
+			"/bin/echo --uu='1' FallbackUsername",
+			true,
+			true,
+			true,
+		},
+		{
+			"/bin/echo --uu=\"'%u'\"",
+			"bob",
+			"/bin/echo --uu=\"'bob'\"",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo --uu='%u' --yy='%u' '%u' %u",
+			"bob",
+			"/bin/echo --uu='bob' --yy='bob' 'bob' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu=%u%u%u'' %user 50%u",
+			"bob",
+			"/bin/echo --uu=FallbackUsernameFallbackUsernameFallbackUsername'' FallbackUsernameser 50FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo %u",
+			"!!",
+			"/bin/echo '!!'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			">xxx",
+			"/bin/echo '_xxx'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"3",
+			"/bin/echo '3'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"3$",
+			"/bin/echo '3_'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"comp$",
+			"/bin/echo 'comp_'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"3$3",
+			"/bin/echo '3_3'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"q $3",
+			"/bin/echo 'q _3'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo '%u",
+			"q $3",
+			"/bin/echo 'FallbackUsername",
+			true,
+			true,
+			true,
+		},
+		{
+			"/bin/echo -s '%u' %u",
+			"āāā",
+			"/bin/echo -s 'āāā' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo -s '%u' %u",
+			"-āāā",
+			"/bin/echo -s '_āāā' FallbackUsername",
+			true,
+			true,
+			true,
+		},
+		{
+			"/bin/echo -s %u",
+			"āāā",
+			"/bin/echo -s 'āāā'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo -s %u",
+			"a -a",
+			"/bin/echo -s 'a -a'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo -s=%u %u",
+			"ā -a",
+			"/bin/echo -s='ā -a' 'ā -a'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo -s=\"%u %u\"",
+			"ā -a",
+			"/bin/echo -s=\"FallbackUsername FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo -m='fridge' %u",
+			"ā  -ß",
+			"/bin/echo -m='fridge' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo -m='fridge' %u",
+			"-ā -a",
+			"/bin/echo -m='fridge' FallbackUsername",
+			true,
+			true,
+			true,
+		},
+		{
+			"/bin/echo %u",
+			"-n",
+			"/bin/echo '_n'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"o'clock",
+			"/bin/echo 'o_clock'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo \"bob'",
+			"bob",
+			"/bin/echo \"bob'",
+			false,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"%u\"",
+			"%u",
+			"/bin/echo '_u'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo \"$(ls)\"",
+			"%u",
+			"/bin/echo \"$(ls)\"",
+			false,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"\\",
+			"/bin/echo '\\'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"\\",
+			"/bin/echo '\\'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"%u\"",
+			"\\",
+			"/bin/echo '\\'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"%u\" %u",
+			"\\",
+			"/bin/echo '\\' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo '%u' \"%u\" %u",
+			"\\",
+			"/bin/echo '\\' \"FallbackUsername\" FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo '%u' \"%u\"",
+			"bob",
+			"/bin/echo 'bob' \"FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+	};
+
+	_test_talloc_string_sub_unsafe(state,
+				       expansions,
+				       ARRAY_SIZE(expansions),
+				       unsafe_characters);
+}
+
+static void test_talloc_string_sub_unsafe_minimal_unsafe_chars(void **state)
+{
+	const char *unsafe_characters = "\"'%";
+
+	static struct cmd_expansion expansions[] = {
+		{
+			"/bin/echo \"bob'",
+			"bob",
+			"/bin/echo \"bob'",
+			false,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"bob",
+			"/bin/echo 'bob'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob",
+			"/bin/echo 'bob'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob'",
+			"/bin/echo 'bob_'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob'''",
+			"/bin/echo 'bob___'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"bob\'",
+			"/bin/echo 'bob_'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo '%u",
+			"bob bob bob",
+			"/bin/echo 'FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo \"%u\"",
+			" ",
+			"/bin/echo ' '",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"--uu=%u\"",
+			"bob",
+			"/bin/echo \"--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo \"--uu=%u\"",
+			"bob !0",
+			"/bin/echo \"--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo %u",
+			"!0",
+			"/bin/echo '!0'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"--uu=%u\"",
+			"bob \\",
+			"/bin/echo \"--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"bob >> x",
+			"/bin/echo --uu='bob >> x'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '--uu=%u\"",
+			"bob",
+			"/bin/echo '--uu=FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"bob",
+			"/bin/echo --uu='bob'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo --uu'=%u'",
+			"bob",
+			"/bin/echo --uu'=FallbackUsername'",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu'=%u'",
+			"`ls`",
+			"/bin/echo --uu'=FallbackUsername'",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"u%u%u%u%u",
+			"/bin/echo --uu='u_u_u_u_u'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"$(ls)",
+			"/bin/echo --uu='$(ls)'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo --uu='%u'",
+			"`ls`",
+			"/bin/echo --uu='`ls`'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo --uu='1' %u",
+			"`ls`",
+			"/bin/echo --uu='1' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu=\"'%u'\"",
+			"bob",
+			"/bin/echo --uu=\"'bob'\"",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo --uu='%u' --yy='%u' '%u' %u",
+			"bob",
+			"/bin/echo --uu='bob' --yy='bob' 'bob' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo --uu=%u%u%u'' %user 50%u",
+			"bob",
+			"/bin/echo --uu=FallbackUsernameFallbackUsernameFallbackUsername'' FallbackUsernameser 50FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo %u",
+			"!!",
+			"/bin/echo '!!'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			">xxx",
+			"/bin/echo '>xxx'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"3",
+			"/bin/echo '3'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"3$",
+			"/bin/echo '3$'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"comp$",
+			"/bin/echo 'comp$'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"3$3",
+			"/bin/echo '3$3'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"q $3",
+			"/bin/echo 'q $3'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u",
+			"q $3",
+			"/bin/echo 'FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo -s '%u' %u",
+			"āāā",
+			"/bin/echo -s 'āāā' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo -s '%u' %u",
+			"-āāā",
+			"/bin/echo -s '_āāā' FallbackUsername",
+			true,
+			true,
+			true,
+		},
+		{
+			"/bin/echo -s %u",
+			"āāā",
+			"/bin/echo -s 'āāā'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo -s %u",
+			"a -a",
+			"/bin/echo -s 'a -a'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo -s=%u %u",
+			"ā -a",
+			"/bin/echo -s='ā -a' 'ā -a'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo -s=\"%u %u\"",
+			"ā -a",
+			"/bin/echo -s=\"FallbackUsername FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo -m='fridge' %u",
+			"ā  -ß",
+			"/bin/echo -m='fridge' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo -m='fridge' %u",
+			"-ā -a",
+			"/bin/echo -m='fridge' FallbackUsername",
+			true,
+			true,
+			true,
+		},
+		{
+			"/bin/echo %u",
+			"-n",
+			"/bin/echo '_n'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"o'clock",
+			"/bin/echo 'o_clock'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo \"bob'",
+			"bob",
+			"/bin/echo \"bob'",
+			false,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"%u\"",
+			"%u",
+			"/bin/echo '_u'",
+			true,
+			true,
+			false,
+		},
+		{
+			"/bin/echo \"$(ls)\"",
+			"%u",
+			"/bin/echo \"$(ls)\"",
+			false,
+			false,
+			false,
+		},
+		{
+			"/bin/echo %u",
+			"\\",
+			"/bin/echo '\\'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo '%u'",
+			"\\",
+			"/bin/echo '\\'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"%u\"",
+			"\\",
+			"/bin/echo '\\'",
+			true,
+			false,
+			false,
+		},
+		{
+			"/bin/echo \"%u\" %u",
+			"\\",
+			"/bin/echo '\\' FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo '%u' \"%u\" %u",
+			"\\",
+			"/bin/echo '\\' \"FallbackUsername\" FallbackUsername",
+			true,
+			false,
+			true,
+		},
+		{
+			"/bin/echo '%u' \"%u\"",
+			"bob",
+			"/bin/echo 'bob' \"FallbackUsername\"",
+			true,
+			false,
+			true,
+		},
+	};
+
+	_test_talloc_string_sub_unsafe(state,
+				       expansions,
+				       ARRAY_SIZE(expansions),
+				       unsafe_characters);
+}
+
+static void test_talloc_string_sub_unsafe_all_mixes(void **state)
+{
+	const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS;
+	size_t i;
+
+	for (i = 0; i < 32; i++) {
+		char in[100] = { 0, };
+		char out[100] = { 0, };
+		struct cmd_expansion expansions[] = {
+			{
+				in,
+				"bob",
+				out,
+				true,
+				false,
+				false,
+			},
+		};
+		bool vsq = i & 1;
+		bool vdq = i & 2;
+		bool v = i & 4;
+		bool sq = i & 8;
+		bool dq = i & 16;
+		char *inp = in;
+		char *outp = out;
+		if (vsq) {
+			inp = stpcpy(inp, "'%u' ");
+			outp = stpcpy(outp, "'bob' ");
+			debug_message("vsq ");
+		}
+		if (vdq) {
+			inp = stpcpy(inp, "\"%u\" ");
+			outp = stpcpy(outp, (vsq || sq) ? "\"FallbackUsername\" " : "'bob' ");
+			debug_message("vdq ");
+			if (vsq || sq) {
+				expansions[0].mixed_fallback = true;
+			}
+		}
+		if (v) {
+			inp = stpcpy(inp, "%u ");
+			outp = stpcpy(outp, (vsq || vdq || sq || dq) ? "FallbackUsername " : "'bob' ");
+			debug_message("v ");
+			if (vsq || vdq || sq || dq) {
+				expansions[0].mixed_fallback = true;
+			}
+		}
+		if (sq) {
+			inp = stpcpy(inp, "' ");
+			outp = stpcpy(outp, "' ");
+			debug_message("sq ");
+		}
+		if (dq) {
+			inp = stpcpy(inp, "\" ");
+			outp = stpcpy(outp, "\" ");
+			debug_message("dq ");
+		}
+		debug_message("(i: %zu)\n", i);
+		*inp = '\0';
+		*outp = '\0';
+		expansions[0].modified = strcmp(in, out) != 0;
+
+		_test_talloc_string_sub_unsafe(state,
+					       expansions,
+					       ARRAY_SIZE(expansions),
+					       unsafe_characters);
+	}
+}
+
+
+int main(void)
+{
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(test_talloc_string_sub_unsafe),
+		cmocka_unit_test(test_talloc_string_sub_unsafe_minimal_unsafe_chars),
+		cmocka_unit_test(test_talloc_string_sub_unsafe_all_mixes),
+	};
+	if (!isatty(1)) {
+		cmocka_set_message_output(CM_OUTPUT_SUBUNIT);
+	}
+	return cmocka_run_group_tests(tests,
+				      setup_talloc_context,
+				      teardown_talloc_context);
+}
--- a/lib/util/wscript_build
+++ b/lib/util/wscript_build
@@ -362,3 +362,9 @@ else:
                      deps='cmocka replace samba-util',
                      local_include=False,
                      for_selftest=True)
+
+    bld.SAMBA3_BINARY('test_string_sub',
+                      source='tests/test_string_sub.c',
+                      deps='''cmocka replace talloc samba-util
+                      ''',
+                      for_selftest=True)
--- a/selftest/tests.py
+++ b/selftest/tests.py
@@ -419,6 +419,8 @@ plantestsuite("samba.unittests.memcache"
               [os.path.join(bindir(), "default/lib/util/test_memcache")])
 plantestsuite("samba.unittests.sys_rw", "none",
               [os.path.join(bindir(), "default/lib/util/test_sys_rw")])
+plantestsuite("samba.unittests.test_string_sub", "none",
+              [os.path.join(bindir(), "test_string_sub")])
 plantestsuite("samba.unittests.ntlm_check", "none",
               [os.path.join(bindir(), "default/libcli/auth/test_ntlm_check")])
 plantestsuite("samba.unittests.gnutls", "none",
