From: Markus Koschany <apo@debian.org>
Date: Tue, 26 May 2026 07:05:41 +0200
Subject: security announcement May 2026

Bug-Upstream: https://bugzilla.samba.org/show_bug.cgi?id=16018
---
 buildtools/wafsamba/samba_abi.py                   |    5 +-
 buildtools/wafsamba/samba_autoconf.py              |    8 +
 docs-xml/smbdotconf/printing/printcommand.xml      |    7 +-
 .../smbdotconf/security/checkpasswordscript.xml    |   10 +-
 lib/util/samba_util.h                              |   11 +-
 lib/util/substitute.c                              |  480 ++++++++-
 lib/util/substitute.h                              |   58 +-
 lib/util/tests/test_string_sub.c                   | 1043 ++++++++++++++++++++
 lib/util/util_str.c                                |   38 +
 lib/util/util_str_escape.c                         |   10 +-
 lib/util/wscript_build                             |    7 +
 selftest/tests.py                                  |    4 +
 source3/lib/substitute.c                           |    2 +
 source3/lib/substitute_generic.c                   |   81 +-
 source3/nmbd/wscript_build                         |    5 +
 source3/printing/print_generic.c                   |  108 +-
 source3/rpc_server/samr/srv_samr_chgpasswd.c       |  110 ++-
 source3/rpc_server/samr/srv_samr_nt.c              |    8 +
 source3/rpc_server/samr/srv_samr_util.h            |    5 +
 source3/torture/test_rpc_samr.c                    |  358 +++++++
 source3/utils/testparm.c                           |   21 +
 source4/nbt_server/wins/winsserver.c               |   38 +-
 22 files changed, 2273 insertions(+), 144 deletions(-)
 create mode 100644 lib/util/tests/test_string_sub.c
 create mode 100644 source3/torture/test_rpc_samr.c

diff --git a/buildtools/wafsamba/samba_abi.py b/buildtools/wafsamba/samba_abi.py
index bf82fc5..d8a8829 100644
--- a/buildtools/wafsamba/samba_abi.py
+++ b/buildtools/wafsamba/samba_abi.py
@@ -41,8 +41,9 @@ def normalise_signature(sig):
 
 def normalise_varargs(sig):
     '''cope with older versions of gdb'''
-    sig = re.sub(',\s\.\.\.', '', sig)
-    return sig
+    sig = re.sub(r',\s\.\.\.', '', sig)
+    # Make sure we compare bytes and not strings
+    return bytes(sig, encoding='utf-8').decode('unicode_escape')
 
 
 def parse_sigs(sigs, abi_match):
diff --git a/buildtools/wafsamba/samba_autoconf.py b/buildtools/wafsamba/samba_autoconf.py
index 276b887..ca02186 100644
--- a/buildtools/wafsamba/samba_autoconf.py
+++ b/buildtools/wafsamba/samba_autoconf.py
@@ -767,6 +767,8 @@ def SAMBA_CONFIG_H(conf, path=None):
                         testflags=True)
         conf.ADD_CFLAGS('-Werror=strict-overflow -Wstrict-overflow=2',
                         testflags=True)
+        conf.ADD_CFLAGS('-Wno-error=enum-int-mismatch',
+                        testflags=True)
 
         conf.ADD_CFLAGS('-Wformat=2 -Wno-format-y2k', testflags=True)
         conf.ADD_CFLAGS('-Wno-format-zero-length', testflags=True)
@@ -787,6 +789,12 @@ int main(void) {
                 conf.env['EXTRA_CFLAGS'] = []
             conf.env['EXTRA_CFLAGS'].extend(TO_LIST("-Werror=format"))
 
+        if CHECK_CFLAGS(conf, ["-Wno-error=array-bounds"]):
+            conf.define('HAVE_WNO_ERROR_ARRAY_BOUNDS', 1)
+
+        if CHECK_CFLAGS(conf, ["-Wno-error=stringop-overflow"]):
+            conf.define('HAVE_WNO_ERROR_STRINGOP_OVERFLOW', 1)
+
         if not Options.options.disable_warnings_as_errors:
             conf.ADD_NAMED_CFLAGS('PICKY_CFLAGS', '-Werror -Wno-error=deprecated-declarations', testflags=True)
             conf.ADD_NAMED_CFLAGS('PICKY_CFLAGS', '-Wno-error=tautological-compare', testflags=True)
diff --git a/docs-xml/smbdotconf/printing/printcommand.xml b/docs-xml/smbdotconf/printing/printcommand.xml
index c84e45f..d708287 100644
--- a/docs-xml/smbdotconf/printing/printcommand.xml
+++ b/docs-xml/smbdotconf/printing/printcommand.xml
@@ -21,8 +21,11 @@
     <para>%p - the appropriate printer 
     name</para>
 
-    <para>%J - the job 
-    name as transmitted by the client.</para>
+    <para>%J - the job name as transmitted by the client,
+    but with dangerous characters being replaced by _.
+    You should use single quotes (directly) around %J, e.g. '%J',
+    see CVE-2026-4480 for more details.
+    </para>
 
     <para>%c - The number of printed pages
     of the spooled job (if known).</para>
diff --git a/docs-xml/smbdotconf/security/checkpasswordscript.xml b/docs-xml/smbdotconf/security/checkpasswordscript.xml
index 18aa2c6..dd162d8 100644
--- a/docs-xml/smbdotconf/security/checkpasswordscript.xml
+++ b/docs-xml/smbdotconf/security/checkpasswordscript.xml
@@ -20,8 +20,8 @@
 
     <itemizedlist>
 	<listitem><para>
-	SAMBA_CPS_ACCOUNT_NAME is always present and contains the sAMAccountName of user,
-	the is the same as the %u substitutions in the none AD DC case.
+	SAMBA_CPS_ACCOUNT_NAME is always present and contains the sAMAccountName of user.
+	It is the same as the '%u' substitutions in the non AD DC case.
 	</para></listitem>
 
 	<listitem><para>
@@ -33,6 +33,12 @@
 	</para></listitem>
     </itemizedlist>
 
+    <para>Even on a non AD DC SAMBA_CPS_ACCOUNT_NAME is the preferred way to access the
+    account name, as it contains the raw value provided by the client. If that's not
+    possible you should use single quotes (directly) around %u, e.g. /path/to/somescript '%u',
+    see CVE-2026-4408 for more details.
+    </para>
+
     <para>Note: In the example directory is a sample program called <command moreinfo="none">crackcheck</command>
     that uses cracklib to check the password quality.</para>
 
diff --git a/lib/util/samba_util.h b/lib/util/samba_util.h
index e788bea..db337ed 100644
--- a/lib/util/samba_util.h
+++ b/lib/util/samba_util.h
@@ -299,7 +299,16 @@ _PUBLIC_ bool set_boolean(const char *boolean_string, bool *boolean);
 _PUBLIC_ bool conv_str_bool(const char * str, bool * val);
 
 /**
- * Convert a size specification like 16K into an integral number of bytes. 
+ * Returns a pointer to the first invalid character in name.
+ *
+ * Passing a NULL pointer as name is not allowed!
+ *
+ * This returns NULL for a valid account name.
+ **/
+_PUBLIC_ const char *strstr_for_invalid_account_characters(const char *name);
+
+/**
+ * Convert a size specification like 16K into an integral number of bytes.
  **/
 _PUBLIC_ bool conv_str_size_error(const char * str, uint64_t * val);
 
diff --git a/lib/util/substitute.c b/lib/util/substitute.c
index 2d88e97..280dd86 100644
--- a/lib/util/substitute.c
+++ b/lib/util/substitute.c
@@ -22,8 +22,11 @@
 */
 
 #include "replace.h"
+#include "system/locale.h"
 #include "debug.h"
 #ifndef SAMBA_UTIL_CORE_ONLY
+#include "lib/util/fault.h"
+#include "lib/util/talloc_stack.h"
 #include "charset/charset.h"
 #else
 #include "charset_compat.h"
@@ -35,6 +38,37 @@
  * @brief Substitute utilities.
  **/
 
+static inline
+char mask_unsafe_character(char in,
+			   bool is_last,
+			   bool allow_trailing_dollar,
+			   const char *unsafe_characters,
+			   char safe_out)
+{
+	const char *unsafe = NULL;
+
+	if (unsafe_characters == NULL) {
+		return in;
+	}
+
+	/* allow a trailing $ (as in machine accounts) */
+	if (allow_trailing_dollar && is_last && in == '$') {
+		return in;
+	}
+
+	if (iscntrl(in)) {
+		return safe_out;
+	}
+
+	unsafe = strchr(unsafe_characters, in);
+	if (unsafe != NULL) {
+		return safe_out;
+	}
+
+	/* ok */
+	return in;
+}
+
 /**
  Substitute a string for a pattern in another string. Make sure there is
  enough room!
@@ -42,15 +76,17 @@
  This routine looks for pattern in s and replaces it with
  insert. It may do multiple replacements or just one.
 
- Any of " ; ' $ or ` in the insert string are replaced with _
+ Any of STRING_SUB_UNSAFE_CHARACTERS and any character
+ caught by calling iscntrl() in the insert string are replaced with _
+
  if len==0 then the string cannot be extended. This is different from the old
  use of len==0 which was for no length checks to be done.
 **/
 
-static void string_sub2(char *s,const char *pattern, const char *insert, size_t len,
-			bool remove_unsafe_characters, bool replace_once,
-			bool allow_trailing_dollar)
+void string_sub(char *s, const char *pattern, const char *insert, size_t len)
 {
+	const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS;
+	char safe_character = '_';
 	char *p;
 	size_t ls, lp, li, i;
 
@@ -77,53 +113,25 @@ static void string_sub2(char *s,const char *pattern, const char *insert, size_t
 			memmove(p+li,p+lp,strlen(p+lp)+1);
 		}
 		for (i=0;i<li;i++) {
-			switch (insert[i]) {
-			case '$':
-				/* allow a trailing $
-				 * (as in machine accounts) */
-				if (allow_trailing_dollar && (i == li - 1 )) {
-					p[i] = insert[i];
-					break;
-				}
-				FALL_THROUGH;
-			case '`':
-			case '"':
-			case '\'':
-			case ';':
-			case '%':
-			case '\r':
-			case '\n':
-				if ( remove_unsafe_characters ) {
-					p[i] = '_';
-					/* yes this break should be here
-					 * since we want to fall throw if
-					 * not replacing unsafe chars */
-					break;
-				}
-				FALL_THROUGH;
-			default:
-				p[i] = insert[i];
-			}
+			/*
+			 * Without allow_trailing_dollar we don't
+			 * need to calculate is_last...
+			 */
+			const bool is_last = false;
+			const bool allow_trailing_dollar = false;
+
+			p[i] = mask_unsafe_character(insert[i],
+						     is_last,
+						     allow_trailing_dollar,
+						     unsafe_characters,
+						     safe_character);
 		}
 		s = p + li;
 		ls = ls + li - lp;
 
-		if (replace_once)
-			break;
 	}
 }
 
-void string_sub_once(char *s, const char *pattern,
-		const char *insert, size_t len)
-{
-	string_sub2( s, pattern, insert, len, true, true, false );
-}
-
-void string_sub(char *s,const char *pattern, const char *insert, size_t len)
-{
-	string_sub2( s, pattern, insert, len, true, false, false );
-}
-
 /**
  * Talloc'ed version of string_sub
  */
@@ -209,3 +217,387 @@ _PUBLIC_ void all_string_sub(char *s,const char *pattern,const char *insert, siz
 		ls = ls + li - lp;
 	}
 }
+
+/*
+ * Internal guts of talloc_string_sub and talloc_all_string_sub.
+ * talloc version of string_sub2.
+ */
+
+bool realloc_string_sub_raw(char **_string,
+			    const char *pattern,
+			    const char *insert,
+			    bool replace_once,
+			    bool allow_trailing_dollar,
+			    const char *unsafe_characters,
+			    char safe_character)
+{
+	char *p = NULL;
+	char *s = NULL;
+	char *string = NULL;
+	ssize_t ls,lp,li,ld, i;
+
+	if (!insert || !pattern || !*pattern || !_string|| !*_string) {
+		return false;
+	}
+
+	s = string = *_string;
+
+	ls = (ssize_t)strlen(s);
+	lp = (ssize_t)strlen(pattern);
+	li = (ssize_t)strlen(insert);
+	ld = li - lp;
+
+	while ((p = strstr_m(s,pattern))) {
+		if (ld > 0) {
+			ptrdiff_t offset = PTR_DIFF(s,string);
+			string = talloc_realloc(NULL, string, char, ls + ld + 1);
+			if (!string) {
+				DBG_ERR("out of memory(realloc)!\n");
+				return false;
+			}
+			*_string = string;
+			p = string + offset + (p - s);
+		}
+		if (li != lp) {
+			memmove(p+li,p+lp,strlen(p+lp)+1);
+		}
+		for (i=0; i < li; i++) {
+			bool is_last = (i == li - 1);
+
+			p[i] = mask_unsafe_character(insert[i],
+						     is_last,
+						     allow_trailing_dollar,
+						     unsafe_characters,
+						     safe_character);
+		}
+		s = p + li;
+		ls += ld;
+
+		if (replace_once) {
+			break;
+		}
+	}
+	return true;
+}
+
+char *talloc_string_sub2(TALLOC_CTX *mem_ctx,
+			 const char *src,
+			 const char *pattern,
+			 const char *insert,
+			 bool remove_unsafe_characters,
+			 bool replace_once,
+			 bool allow_trailing_dollar)
+{
+	const char *unsafe_characters = NULL;
+	char safe_character = '\0';
+	char *string = NULL;
+	bool ok;
+
+	if (!insert || !pattern || !*pattern || !src) {
+		return NULL;
+	}
+
+	if (remove_unsafe_characters) {
+		unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS;
+		safe_character = '_';
+	}
+
+	string = talloc_strdup(mem_ctx, src);
+	if (string == NULL) {
+		DBG_ERR("out of memory, talloc_strdup(src)!\n");
+		return NULL;
+	}
+
+	ok = realloc_string_sub_raw(&string,
+				    pattern,
+				    insert,
+				    replace_once,
+				    allow_trailing_dollar,
+				    unsafe_characters,
+				    safe_character);
+	if (!ok) {
+		TALLOC_FREE(string);
+		DBG_ERR("out of memory, realloc_string_sub_raw()!\n");
+		return NULL;
+	}
+
+	return string;
+}
+
+/* Same as string_sub, but returns a talloc'ed string */
+
+char *talloc_string_sub(TALLOC_CTX *mem_ctx,
+			const char *src,
+			const char *pattern,
+			const char *insert)
+{
+	return talloc_string_sub2(mem_ctx, src, pattern, insert,
+			true, false, false);
+}
+
+char *talloc_all_string_sub(TALLOC_CTX *ctx,
+				const char *src,
+				const char *pattern,
+				const char *insert)
+{
+	return talloc_string_sub2(ctx, src, pattern, insert,
+			false, false, false);
+}
+
+#ifndef SAMBA_UTIL_CORE_ONLY
+
+bool talloc_string_sub_mixed_quoting(const char *full_cmd, char variable_char)
+{
+	/*
+	 * Try to make sure talloc_string_sub_unsafe()
+	 * won't return NULL, instead talloc_stackframe_pool()
+	 * would panic
+	 */
+	size_t cmd_len = full_cmd != NULL ? strlen(full_cmd) : 0;
+	size_t pool_size = 512 + cmd_len;
+	TALLOC_CTX *frame = talloc_stackframe_pool(pool_size);
+	char *cmd = NULL;
+	bool modified = false;
+	bool masked = false;
+	bool mixed_fallback = false;
+
+	cmd = talloc_string_sub_unsafe(frame,
+				       full_cmd,
+				       variable_char,
+				       "U",  /* unsafe_value */
+				       "'\"%", /* unsafe_characters */
+				       '_',    /* safe_character */
+				       "F",  /* fallback_value */
+				       &modified,
+				       &masked,
+				       &mixed_fallback);
+	if (cmd == NULL) {
+		mixed_fallback = false;
+	}
+	TALLOC_FREE(frame);
+	return mixed_fallback;
+}
+
+char *talloc_string_sub_unsafe(TALLOC_CTX *mem_ctx,
+			       const char *orig_cmd,
+			       char variable_char,
+			       const char *unsafe_value,
+			       const char *unsafe_characters,
+			       char safe_character,
+			       const char *fallback_value,
+			       bool *_modified,
+			       bool *_masked,
+			       bool *_mixed_fallback)
+{
+	TALLOC_CTX *frame = talloc_stackframe();
+	const char variable[3] =
+		{ '%', variable_char, '\0' };
+	const char variable_s_quoted[5] =
+		{ '\'', '%', variable_char, '\'', '\0' };
+	const char variable_d_quoted[5] =
+		{ '"', '%', variable_char, '"', '\0' };
+	char *cmd = NULL;
+	char *masked_value = NULL;
+	char *quoted_value = NULL;
+	bool has_s_quotes;
+	bool has_d_quotes;
+	bool has_variable;
+	bool has_variable_s_quoted;
+	bool has_variable_d_quoted;
+	bool modified = false;
+	bool masked = false;
+	bool mixed_fallback = false;
+	bool ok;
+
+	/*
+	 * The unsafe_characters argument should contain
+	 * single and double quotes.
+	 * Otherwise We can't safely handle this.
+	 */
+	SMB_ASSERT(unsafe_characters != NULL);
+	SMB_ASSERT(strchr(unsafe_characters, '\'') != NULL);
+	SMB_ASSERT(strchr(unsafe_characters, '"') != NULL);
+	SMB_ASSERT(strchr(unsafe_characters, '%') != NULL);
+
+	cmd = talloc_strdup(mem_ctx, orig_cmd);
+	if (cmd == NULL) {
+		TALLOC_FREE(frame);
+		return NULL;
+	}
+	cmd = talloc_steal(frame, cmd);
+
+	has_variable = strstr(orig_cmd, variable) != NULL;
+	if (!has_variable) {
+		/*
+		 * Nothing to do...
+		 */
+		goto done;
+	}
+	modified = true;
+
+	/*
+	 * Replace all unsafe characters as well as control
+	 * characters.
+	 *
+	 * Note that we start with masked_value = "%u"
+	 * and then replace "%u" with unsafe_value,
+	 * as a result we have a masked version of
+	 * unsafe_value.
+	 *
+	 * And don't allow option injected like
+	 *
+	 * '-h value'
+	 * '--help value'
+	 *
+	 */
+	masked_value = talloc_strdup(frame, variable);
+	if (masked_value == NULL) {
+		goto nomem;
+	}
+	ok = realloc_string_sub_raw(&masked_value,
+				    variable,
+				    unsafe_value,
+				    false, /* replace_once */
+				    false, /* allow_trailing_dollar */
+				    unsafe_characters,
+				    safe_character);
+	if (!ok) {
+		goto nomem;
+	}
+	if (masked_value[0] == '-') {
+		masked_value[0] = safe_character;
+	}
+	masked = strcmp(masked_value, unsafe_value) != 0;
+
+retry:
+
+	has_s_quotes = strchr(cmd, '\'') != NULL;
+	has_d_quotes = strchr(cmd, '"') != NULL;
+	has_variable = strstr(cmd, variable) != NULL;
+	has_variable_s_quoted = strstr(cmd, variable_s_quoted) != NULL;
+	has_variable_d_quoted = strstr(cmd, variable_d_quoted) != NULL;
+
+	if (has_variable_s_quoted) {
+		/*
+		 * In smb.conf we have something like
+		 *
+		 * some script = /usr/bin/script '%u'
+		 *
+		 * It is safe to replace '%u' (or '%J' etc, depending
+		 * on variable_char) with '<masked_value>' if
+		 * masked_value does not contain single quotes. We
+		 * have checked that.
+		 */
+
+		if (quoted_value == NULL) {
+			quoted_value = talloc_asprintf(frame, "'%s'",
+						       masked_value);
+			if (quoted_value == NULL) {
+				goto nomem;
+			}
+		}
+
+		ok = realloc_string_sub_raw(&cmd,
+					    variable_s_quoted,
+					    quoted_value,
+					    false, /* replace_once */
+					    false, /* allow_trailing_dollar */
+					    NULL,  /* unsafe_characters */
+					    '\0'); /* safe_character */
+		if (!ok) {
+			goto nomem;
+		}
+
+		goto retry;
+	}
+
+	if (has_variable_d_quoted && !has_s_quotes) {
+		/*
+		 * replace the "%u"
+		 *
+		 * some script = /usr/bin/script "%u"
+		 *
+		 * with '%u' and try the '%u' -> 'variable' substitution
+		 * again.
+		 */
+
+		ok = realloc_string_sub_raw(&cmd,
+					    variable_d_quoted,
+					    variable_s_quoted,
+					    false, /* replace_once */
+					    false, /* allow_trailing_dollar */
+					    NULL,  /* unsafe_characters */
+					    '\0'); /* safe_character */
+		if (!ok) {
+			goto nomem;
+		}
+
+		goto retry;
+	}
+
+	if (has_variable && !has_s_quotes && !has_d_quotes) {
+		/*
+		 * In this case:
+		 *
+		 * some script = /usr/bin/script %u
+		 *
+		 * we can safely substitute %u -> '%u' and try the
+		 * single quote test again.
+		 */
+
+		ok = realloc_string_sub_raw(&cmd,
+					    variable,
+					    variable_s_quoted,
+					    false, /* replace_once */
+					    false, /* allow_trailing_dollar */
+					    NULL,  /* unsafe_characters */
+					    '\0'); /* safe_character */
+		if (!ok) {
+			goto nomem;
+		}
+
+		goto retry;
+	}
+
+	if (has_variable) {
+		/*
+		 * There are single or double quotes, but not tightly
+		 * bound around a %u.
+		 *
+		 * Or there's a mix of single and double quotes.
+		 *
+		 * We just use a generic fallback value.
+		 * and let the caller warn about this
+		 * and give the admin a hind to fix the smb.conf
+		 * option.
+		 */
+		mixed_fallback = true;
+
+		ok = realloc_string_sub_raw(&cmd,
+					    variable,
+					    fallback_value,
+					    false, /* replace_once */
+					    false, /* allow_trailing_dollar */
+					    NULL,  /* unsafe_characters */
+					    '\0'); /* safe_character */
+		if (!ok) {
+			goto nomem;
+		}
+	}
+
+done:
+	*_modified = modified;
+	*_masked = masked;
+	*_mixed_fallback = mixed_fallback;
+	cmd = talloc_steal(mem_ctx, cmd);
+	TALLOC_FREE(frame);
+	return cmd;
+
+nomem:
+	*_modified = false;
+	*_masked = false;
+	*_mixed_fallback = false;
+	TALLOC_FREE(frame);
+	return NULL;
+}
+#endif /* ! SAMBA_UTIL_CORE_ONLY */
diff --git a/lib/util/substitute.h b/lib/util/substitute.h
index 5ba469c..c322732 100644
--- a/lib/util/substitute.h
+++ b/lib/util/substitute.h
@@ -26,6 +26,8 @@
 
 #include <talloc.h>
 
+#define STRING_SUB_UNSAFE_CHARACTERS "$`\"';%|&<>"
+
 /**
  Substitute a string for a pattern in another string. Make sure there is
  enough room!
@@ -33,15 +35,14 @@
  This routine looks for pattern in s and replaces it with
  insert. It may do multiple replacements.
 
- Any of " ; ' $ or ` in the insert string are replaced with _
+ Any of STRING_SUB_UNSAFE_CHARACTERS (see above) and any character
+ caught by calling iscntrl() in the insert string are replaced with _
+
  if len==0 then the string cannot be extended. This is different from the old
  use of len==0 which was for no length checks to be done.
 **/
 void string_sub(char *s,const char *pattern, const char *insert, size_t len);
 
-void string_sub_once(char *s, const char *pattern,
-		     const char *insert, size_t len);
-
 char *string_sub_talloc(TALLOC_CTX *mem_ctx, const char *s,
 			const char *pattern, const char *insert);
 
@@ -53,4 +54,53 @@ char *string_sub_talloc(TALLOC_CTX *mem_ctx, const char *s,
 **/
 void all_string_sub(char *s,const char *pattern,const char *insert, size_t len);
 
+/*
+ * If unsafe_characters is NULL all characters are allowed,
+ * if unsafe_characters is not NULL all characters caught
+ * by iscntrl() are also replaced by safe_character.
+ *
+ * *_string might be reallocated!
+ *
+ * On error *_string may still be reallocated and
+ * may contain partial replacements.
+ */
+bool realloc_string_sub_raw(char **_string,
+			    const char *pattern,
+			    const char *insert,
+			    bool replace_once,
+			    bool allow_trailing_dollar,
+			    const char *unsafe_characters,
+			    char safe_character);
+
+char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src,
+			const char *pattern,
+			const char *insert,
+			bool remove_unsafe_characters,
+			bool replace_once,
+			bool allow_trailing_dollar);
+char *talloc_string_sub(TALLOC_CTX *mem_ctx,
+			const char *src,
+			const char *pattern,
+			const char *insert);
+char *talloc_all_string_sub(TALLOC_CTX *ctx,
+				const char *src,
+				const char *pattern,
+				const char *insert);
+
+#ifndef SAMBA_UTIL_CORE_ONLY
+bool talloc_string_sub_mixed_quoting(const char *full_cmd, char variable_char);
+
+char *talloc_string_sub_unsafe(TALLOC_CTX *mem_ctx,
+			       const char *orig_cmd,
+			       char variable_char,
+			       const char *unsafe_value,
+			       const char *unsafe_characters,
+			       char safe_character,
+			       const char *fallback_value,
+			       bool *_modified,
+			       bool *_masked,
+			       bool *_mixed_fallback);
+
+#endif /* ! SAMBA_UTIL_CORE_ONLY */
+
 #endif /* _SAMBA_SUBSTITUTE_H_ */
diff --git a/lib/util/tests/test_string_sub.c b/lib/util/tests/test_string_sub.c
new file mode 100644
index 0000000..d6bf904
--- /dev/null
+++ b/lib/util/tests/test_string_sub.c
@@ -0,0 +1,1043 @@
+#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);
+}
diff --git a/lib/util/util_str.c b/lib/util/util_str.c
index 3356df3..f532dc4 100644
--- a/lib/util/util_str.c
+++ b/lib/util/util_str.c
@@ -346,3 +346,41 @@ _PUBLIC_ int memcmp_const_time(const void *s1, const void *s2, size_t n)
 
 	return sum != 0;
 }
+
+_PUBLIC_ const char *strstr_for_invalid_account_characters(const char *name)
+{
+	/*
+	 * Return a pointer to the first invalid character in the
+	 * sAMAccountName, or NULL if the whole name is valid.
+	 *
+	 * The rules here are based on
+	 *
+	 * https://social.technet.microsoft.com/wiki/contents/articles/11216.active-directory-requirements-for-creating-objects.aspx
+	 */
+	size_t i;
+
+	for (i = 0; name[i] != '\0'; i++) {
+		uint8_t c = name[i];
+		const char *p = NULL;
+
+		if (iscntrl(c)) {
+			return &name[i];
+		}
+
+		p = strchr("\"[]:;|=+*?<>/\\,", c);
+		if (p != NULL) {
+			return &name[i];
+		}
+	}
+
+	if (i == 0) {
+		return &name[i];
+	}
+
+	if (name[i - 1] == '.') {
+		i -= 1;
+		return &name[i];
+	}
+
+	return NULL;
+}
diff --git a/lib/util/util_str_escape.c b/lib/util/util_str_escape.c
index 93cdd8d..4a2bd2e 100644
--- a/lib/util/util_str_escape.c
+++ b/lib/util/util_str_escape.c
@@ -17,7 +17,9 @@
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-#include "includes.h"
+#include "replace.h"
+#include "system/locale.h"
+#include "lib/util/debug.h"
 #include "lib/util/util_str_escape.h"
 
 
@@ -25,9 +27,9 @@
  * Calculate the encoded length of a character for log_escape
  *
  */
-static size_t encoded_length(char c)
+static size_t encoded_length(unsigned char c)
 {
-	if (c != '\\' &&  c > 0x1F) {
+	if (c != '\\' && !iscntrl(c)) {
 		return 1;
 	} else {
 		switch (c) {
@@ -78,7 +80,7 @@ char *log_escape(TALLOC_CTX *frame, const char *in)
 	c = in;
 	e = encoded;
 	while (*c) {
-		if (*c != '\\' && *c > 0x1F) {
+		if (*c != '\\' && !iscntrl((unsigned char)(*c))) {
 			*e++ = *c++;
 		} else {
 			switch (*c) {
diff --git a/lib/util/wscript_build b/lib/util/wscript_build
index 5a8a049..ba68063 100644
--- a/lib/util/wscript_build
+++ b/lib/util/wscript_build
@@ -317,3 +317,10 @@ else:
                      deps='cmocka replace talloc samba-util',
                      local_include=False,
                      for_selftest=True)
+
+    bld.SAMBA_BINARY('test_string_sub',
+                      source='tests/test_string_sub.c',
+                      deps='''cmocka replace talloc samba-util
+                      ''',
+                      local_include=False,
+                      for_selftest=True)
diff --git a/selftest/tests.py b/selftest/tests.py
index eb3a07f..8e4f87b 100644
--- a/selftest/tests.py
+++ b/selftest/tests.py
@@ -404,6 +404,8 @@ plantestsuite("samba.unittests.util", "none",
               [os.path.join(bindir(), "default/lib/util/test_util")])
 plantestsuite("samba.unittests.memcache", "none",
               [os.path.join(bindir(), "default/lib/util/test_memcache")])
+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",
@@ -416,6 +418,8 @@ plantestsuite("samba.unittests.test_registry_regfio", "none",
               [os.path.join(bindir(), "default/source3/test_registry_regfio")])
 plantestsuite("samba.unittests.test_oLschema2ldif", "none",
               [os.path.join(bindir(), "default/source4/utils/oLschema2ldif/test_oLschema2ldif")])
+plantestsuite("samba.unittests.test_rpc_samr", "none",
+              [os.path.join(bindir(), "test_rpc_samr")])
 if have_heimdal_support and not using_system_gssapi:
     plantestsuite("samba.unittests.auth.heimdal_gensec_unwrap_des", "none",
               [valgrindify(os.path.join(bindir(), "test_heimdal_gensec_unwrap_des"))])
diff --git a/source3/lib/substitute.c b/source3/lib/substitute.c
index 833af10..2ba0ba7 100644
--- a/source3/lib/substitute.c
+++ b/source3/lib/substitute.c
@@ -406,6 +406,7 @@ char *talloc_sub_basic(TALLOC_CTX *mem_ctx,
 	}
 
 	tmp_ctx = talloc_stackframe();
+	a_string = talloc_steal(tmp_ctx, a_string);
 
 	for (s = a_string; (p = strchr_m(s, '%')); s = a_string + (p - b)) {
 
@@ -565,6 +566,7 @@ error:
 	TALLOC_FREE(a_string);
 
 done:
+	a_string = talloc_steal(mem_ctx, a_string);
 	TALLOC_FREE(tmp_ctx);
 	return a_string;
 }
diff --git a/source3/lib/substitute_generic.c b/source3/lib/substitute_generic.c
index 0498cd0..bd35e53 100644
--- a/source3/lib/substitute_generic.c
+++ b/source3/lib/substitute_generic.c
@@ -42,71 +42,38 @@ char *realloc_string_sub2(char *string,
 			bool remove_unsafe_characters,
 			bool allow_trailing_dollar)
 {
-	char *p, *in;
-	char *s;
-	ssize_t ls,lp,li,ld, i;
+	const char *unsafe_characters = NULL;
+	char safe_character = '\0';
+	bool ok;
 
 	if (!insert || !pattern || !*pattern || !string || !*string)
 		return NULL;
 
-	s = string;
+	if (remove_unsafe_characters) {
+		unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS;
+		safe_character = '_';
+	}
 
-	in = talloc_strdup(talloc_tos(), insert);
-	if (!in) {
-		DEBUG(0, ("realloc_string_sub: out of memory!\n"));
+	ok = realloc_string_sub_raw(&string,
+				    pattern,
+				    insert,
+				    false,  /* replace_once */
+				    allow_trailing_dollar,
+				    unsafe_characters,
+				    safe_character);
+	if (!ok) {
+		DBG_ERR("out of memory, realloc_string_sub_raw()!\n");
+		/*
+		 * The calling convention of realloc_string_sub2()
+		 * is very strange regarding stale string pointers.
+		 *
+		 * It is assumed the given string was allocated
+		 * on talloc_tos(), so we just don't touch
+		 * it at all here...
+		 */
 		return NULL;
 	}
-	ls = (ssize_t)strlen(s);
-	lp = (ssize_t)strlen(pattern);
-	li = (ssize_t)strlen(insert);
-	ld = li - lp;
-	for (i=0;i<li;i++) {
-		switch (in[i]) {
-			case '$':
-				/* allow a trailing $
-				 * (as in machine accounts) */
-				if (allow_trailing_dollar && (i == li - 1 )) {
-					break;
-				}
-				FALL_THROUGH;
-			case '`':
-			case '"':
-			case '\'':
-			case ';':
-			case '%':
-			case '\r':
-			case '\n':
-				if ( remove_unsafe_characters ) {
-					in[i] = '_';
-					break;
-				}
-				FALL_THROUGH;
-			default:
-				/* ok */
-				break;
-		}
-	}
 
-	while ((p = strstr_m(s,pattern))) {
-		if (ld > 0) {
-			int offset = PTR_DIFF(s,string);
-			string = talloc_realloc(NULL, string, char, ls + ld + 1);
-			if (!string) {
-				DEBUG(0, ("realloc_string_sub: "
-					"out of memory!\n"));
-				talloc_free(in);
-				return NULL;
-			}
-			p = string + offset + (p - s);
-		}
-		if (li != lp) {
-			memmove(p+li,p+lp,strlen(p+lp)+1);
-		}
-		memcpy(p, in, li);
-		s = p + li;
-		ls += ld;
-	}
-	talloc_free(in);
 	return string;
 }
 
diff --git a/source3/nmbd/wscript_build b/source3/nmbd/wscript_build
index 843c900..09a3e21 100644
--- a/source3/nmbd/wscript_build
+++ b/source3/nmbd/wscript_build
@@ -1,5 +1,9 @@
 #!/usr/bin/env python
 
+nmbd_cflags = ''
+if bld.CONFIG_SET('HAVE_WNO_ERROR_STRINGOP_OVERFLOW'):
+    nmbd_cflags = '-Wno-error=stringop-overflow'
+
 bld.SAMBA3_BINARY('nmbd',
                  source='''
                  asyncdns.c
@@ -30,6 +34,7 @@ bld.SAMBA3_BINARY('nmbd',
                  nmbd_workgroupdb.c
                  nmbd_synclists.c
                  ''',
+                 cflags=nmbd_cflags,
                  deps='''
                  talloc
                  tevent
diff --git a/source3/printing/print_generic.c b/source3/printing/print_generic.c
index 0059fad..c669580 100644
--- a/source3/printing/print_generic.c
+++ b/source3/printing/print_generic.c
@@ -18,6 +18,7 @@
 */
 
 #include "includes.h"
+#include "lib/util/util_str_escape.h"
 #include "printing.h"
 #include "smbd/proto.h"
 
@@ -205,6 +206,52 @@ static int generic_queue_get(const char *printer_name,
 	return qcount;
 }
 
+static const char *replace_print_cmd_J(TALLOC_CTX *mem_ctx,
+				       const char *orig_cmd,
+				       const char *unsafe_jobname,
+				       const char *fallback_jobname)
+{
+	char *cmd = NULL;
+	bool modified = false;
+	bool masked = false;
+	bool mixed_fallback = false;
+
+	/*
+	 * This replaces unsafe characters with '_'.
+	 * We also mask forward and backslash here.
+	 *
+	 * Then it replaces %J with an single quoted
+	 * version of the masked jobname or it falls
+	 * back to fallback_jobname is the print command
+	 * uses strange mixed quoting.
+	 */
+
+#define JOBNAME_UNSAFE_CHARACTERS \
+	STRING_SUB_UNSAFE_CHARACTERS "/\\"
+
+	cmd = talloc_string_sub_unsafe(mem_ctx,
+				       orig_cmd,
+				       'J',
+				       unsafe_jobname,
+				       JOBNAME_UNSAFE_CHARACTERS,
+				       '_',
+				       fallback_jobname,
+				       &modified,
+				       &masked,
+				       &mixed_fallback);
+	if (cmd == NULL) {
+		return NULL;
+	}
+
+	/*
+	 * The caller already checked talloc_string_sub_mixed_quoting()
+	 * and warned the admin, so we don't check mixed_fallback
+	 * here
+	 */
+
+	return cmd;
+}
+
 /****************************************************************************
  Submit a file for printing - called from print_job_end()
 ****************************************************************************/
@@ -220,11 +267,12 @@ static int generic_job_submit(int snum, struct printjob *pjob,
 	char *print_directory = NULL;
 	char *wd = NULL;
 	char *p = NULL;
-	char *jobname = NULL;
+	const char *print_cmd = NULL;
 	TALLOC_CTX *ctx = talloc_tos();
 	fstring job_page_count, job_size;
 	print_queue_struct *q;
 	print_status_struct status;
+	const char *jobname = "No Document Name";
 
 	/* we print from the directory path to give the best chance of
            parsing the lpq output */
@@ -253,24 +301,49 @@ static int generic_job_submit(int snum, struct printjob *pjob,
 		return -1;
 	}
 
-	jobname = talloc_strdup(ctx, pjob->jobname);
-	if (!jobname) {
-		ret = -1;
-		goto out;
+	if (pjob->jobname[0] != '\0') {
+		jobname = pjob->jobname;
 	}
-	jobname = talloc_string_sub(ctx, jobname, "'", "_");
-	if (!jobname) {
-		ret = -1;
-		goto out;
+
+	print_cmd = lp_print_command(snum);
+	if (print_cmd != NULL) {
+		const char *invalid_jobname = "__CVE-2026-4480_FallbackJobname__";
+
+		if (talloc_string_sub_mixed_quoting(print_cmd, 'J')) {
+			/*
+			 * The admin used a strange mixture of
+			 * single and double quotes, fallback
+			 * to InvalidDocumentName and warn about
+			 * it, so that the admin can adjust to
+			 * the use single quotes directly around %J,
+			 * e.g. '%J'.
+			 */
+			jobname = invalid_jobname;
+			D_WARNING("CVE-2026-4480: printer %s "
+				  "strange quoting in 'print command', "
+				  "falling back to jobname=%s, "
+				  "use testparm to fix the configuration\n",
+				  lp_printername(talloc_tos(), lp_sub, snum),
+				  invalid_jobname);
+		}
+
+		print_cmd = replace_print_cmd_J(ctx,
+						print_cmd,
+						jobname,
+						invalid_jobname);
+		if (!print_cmd) {
+			ret = -1;
+			goto out;
+		}
 	}
+
 	slprintf(job_page_count, sizeof(job_page_count)-1, "%d", pjob->page_count);
 	slprintf(job_size, sizeof(job_size)-1, "%lu", (unsigned long)pjob->size);
 
 	/* send it to the system spooler */
 	ret = print_run_command(snum, lp_printername(talloc_tos(), lp_sub, snum), True,
-			lp_print_command(snum), NULL,
+			print_cmd, NULL,
 			"%s", p,
-			"%J", jobname,
 			"%f", p,
 			"%z", job_size,
 			"%c", job_page_count,
@@ -291,9 +364,14 @@ static int generic_job_submit(int snum, struct printjob *pjob,
 		int i;
 		for (i = 0; i < ret; i++) {
 			if (strcmp(q[i].fs_file, p) == 0) {
+				char *le_jobname =
+					log_escape(talloc_tos(), jobname);
+
 				pjob->sysjob = q[i].sysjob;
 				DEBUG(5, ("new job %u (%s) matches sysjob %d\n",
-					  pjob->jobid, jobname, pjob->sysjob));
+					  pjob->jobid, le_jobname, pjob->sysjob));
+
+				TALLOC_FREE(le_jobname);
 				break;
 			}
 		}
@@ -301,8 +379,12 @@ static int generic_job_submit(int snum, struct printjob *pjob,
 		ret = 0;
 	}
 	if (pjob->sysjob == -1) {
+		char *le_jobname = log_escape(talloc_tos(), jobname);
+
 		DEBUG(2, ("failed to get sysjob for job %u (%s), tracking as "
-			  "Unix job\n", pjob->jobid, jobname));
+			  "Unix job\n", pjob->jobid, le_jobname));
+
+		TALLOC_FREE(le_jobname);
 	}
 
 
diff --git a/source3/rpc_server/samr/srv_samr_chgpasswd.c b/source3/rpc_server/samr/srv_samr_chgpasswd.c
index cb9837e..7ab4489 100644
--- a/source3/rpc_server/samr/srv_samr_chgpasswd.c
+++ b/source3/rpc_server/samr/srv_samr_chgpasswd.c
@@ -54,6 +54,7 @@
 #include "passdb.h"
 #include "auth.h"
 #include "lib/util/sys_rw.h"
+#include "lib/util/util_str_escape.h"
 
 #include "lib/crypto/gnutls_helpers.h"
 #include <gnutls/gnutls.h>
@@ -1001,27 +1002,118 @@ static bool check_passwd_history(struct samu *sampass, const char *plaintext)
 /***********************************************************
 ************************************************************/
 
+NTSTATUS check_password_complexity_internal(TALLOC_CTX *tosctx,
+					    const char *orig_cmd,
+					    const char *username,
+					    char **cmd_out)
+{
+	const char *fallback_username = "__CVE-2026-4408_FallbackUsername__";
+	const char *inv = NULL;
+	char *cmd = NULL;
+	bool modified = false;
+	bool masked = false;
+	bool mixed_fallback = false;
+
+	*cmd_out = NULL;
+
+	if (username == NULL) {
+		return NT_STATUS_INVALID_USER_PRINCIPAL_NAME;
+	}
+
+	/*
+	 * This catches invalid characters in account names
+	 * which might be problematic passing to a shell script.
+	 */
+	inv = strstr_for_invalid_account_characters(username);
+	if (inv != NULL) {
+		char *le_username = log_escape(tosctx, username);
+
+		DBG_WARNING("username '%s' has invalid or dangerous characters\n",
+			    le_username);
+
+		TALLOC_FREE(le_username);
+
+		return NT_STATUS_INVALID_USER_PRINCIPAL_NAME;
+	}
+
+	/*
+	 * This masks the remaining unsafe characters which
+	 * are not already caught by strstr_for_invalid_account_characters()
+	 * with '_'.
+	 *
+	 * Then it replaces %u with an single quoted
+	 * and/or shell escaped version of the masked username.
+	 */
+	cmd = talloc_string_sub_unsafe(tosctx,
+				       orig_cmd,
+				       'u',
+				       username,
+				       STRING_SUB_UNSAFE_CHARACTERS,
+				       '_',
+				       fallback_username,
+				       &modified,
+				       &masked,
+				       &mixed_fallback);
+	if (cmd == NULL) {
+		return NT_STATUS_NO_MEMORY;
+	}
+
+	/*
+	 * Now warn about unexpected values
+	 */
+
+	if (mixed_fallback) {
+		D_WARNING("CVE-2026-4408: "
+			  "strange quoting in 'check password script', "
+			  "falling back to replace %%u with %s, "
+			  "use testparm to fix the configuration\n",
+			  fallback_username);
+		D_WARNING("CVE-2026-4408: "
+			  "You should use '%%u', or SAMBA_CPS_ACCOUNT_NAME "
+			  "inside of 'check password script'.\n");
+	} else if (masked) {
+		char *le_username = log_escape(tosctx, username);
+
+		D_WARNING("CVE-2026-4408: "
+			  "replaced %%u with masked value instead of: %s\n",
+			  le_username);
+		D_WARNING("CVE-2026-4408: "
+			  "You should use SAMBA_CPS_ACCOUNT_NAME inside "
+			  "'check password script' instead of %%u.\n");
+
+		TALLOC_FREE(le_username);
+	}
+
+	*cmd_out = cmd;
+	return NT_STATUS_OK;
+}
+
+
 NTSTATUS check_password_complexity(const char *username,
 				   const char *fullname,
 				   const char *password,
 				   enum samPwdChangeReason *samr_reject_reason)
 {
+	int check_ret;
+	NTSTATUS status;
 	TALLOC_CTX *tosctx = talloc_tos();
 	const struct loadparm_substitution *lp_sub =
 		loadparm_s3_global_substitution();
-	int check_ret;
-	char *cmd;
+	const char *orig_cmd = NULL;
+	char *cmd = NULL;
 
-	/* Use external script to check password complexity */
-	if ((lp_check_password_script(tosctx, lp_sub) == NULL)
-	    || (*(lp_check_password_script(tosctx, lp_sub)) == '\0')){
+	orig_cmd = lp_check_password_script(tosctx, lp_sub);
+	if (orig_cmd == NULL || orig_cmd[0] == '\0') {
 		return NT_STATUS_OK;
 	}
 
-	cmd = talloc_string_sub(tosctx, lp_check_password_script(tosctx, lp_sub), "%u",
-				username);
-	if (!cmd) {
-		return NT_STATUS_PASSWORD_RESTRICTION;
+	/* note we don't use 'fullname' or 'password' here */
+	status = check_password_complexity_internal(tosctx,
+						    orig_cmd,
+						    username,
+						    &cmd);
+	if (!NT_STATUS_IS_OK(status)) {
+		return status;
 	}
 
 	check_ret = setenv("SAMBA_CPS_ACCOUNT_NAME", username, 1);
diff --git a/source3/rpc_server/samr/srv_samr_nt.c b/source3/rpc_server/samr/srv_samr_nt.c
index 5ffc333..b2e6d4a 100644
--- a/source3/rpc_server/samr/srv_samr_nt.c
+++ b/source3/rpc_server/samr/srv_samr_nt.c
@@ -7122,6 +7122,14 @@ NTSTATUS _samr_ValidatePassword(struct pipes_struct *p,
 		return NT_STATUS_ACCESS_DENIED;
 	}
 
+	if (lp_server_role() <= ROLE_DOMAIN_MEMBER) {
+		/*
+		 * We only want this on DCs
+		 */
+		p->fault_state = DCERPC_FAULT_ACCESS_DENIED;
+		return NT_STATUS_ACCESS_DENIED;
+	}
+
 	if (r->in.level < 1 || r->in.level > 3) {
 		return NT_STATUS_INVALID_INFO_CLASS;
 	}
diff --git a/source3/rpc_server/samr/srv_samr_util.h b/source3/rpc_server/samr/srv_samr_util.h
index c0c4808..9a3d691 100644
--- a/source3/rpc_server/samr/srv_samr_util.h
+++ b/source3/rpc_server/samr/srv_samr_util.h
@@ -74,6 +74,11 @@ NTSTATUS pass_oem_change(char *user, const char *rhost,
 			 uchar password_encrypted_with_nt_hash[516],
 			 const uchar old_nt_hash_encrypted[16],
 			 enum samPwdChangeReason *reject_reason);
+
+NTSTATUS check_password_complexity_internal(TALLOC_CTX *mem_ctx,
+					    const char *_orig_cmd,
+					    const char *username,
+					    char **cmd_out);
 NTSTATUS check_password_complexity(const char *username,
 				   const char *fullname,
 				   const char *password,
diff --git a/source3/torture/test_rpc_samr.c b/source3/torture/test_rpc_samr.c
new file mode 100644
index 0000000..8d4f398
--- /dev/null
+++ b/source3/torture/test_rpc_samr.c
@@ -0,0 +1,358 @@
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <setjmp.h>
+#include <sys/stat.h>
+#include <cmocka.h>
+#include "includes.h"
+#include "talloc.h"
+#include "libcli/util/ntstatus.h"
+#include "../librpc/gen_ndr/samr.h"
+#include "rpc_server/samr/srv_samr_util.h"
+
+/* set SAMR_DEBUG_VERBOSE to true to print more. */
+#define SAMR_DEBUG_VERBOSE true
+
+#if SAMR_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;
+	NTSTATUS result_code;
+};
+
+static struct cmd_expansion expansions[] = {
+	{
+		"/bin/echo '%u'",
+		"bob",
+		"/bin/echo 'bob'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"bob",
+		"/bin/echo 'bob'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"bob'",
+		"/bin/echo 'bob_'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"bob\'",
+		"/bin/echo 'bob_'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"bob'''",
+		"/bin/echo 'bob___'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"bob*",
+		NULL,
+		NT_STATUS_INVALID_USER_PRINCIPAL_NAME
+	},
+	{
+		"/bin/echo %u",
+		"bob\"",
+		NULL,
+		NT_STATUS_INVALID_USER_PRINCIPAL_NAME
+	},
+	{
+		"/bin/echo '%u",
+		"bob bob bob",
+		"/bin/echo '__CVE-2026-4408_FallbackUsername__",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo \"%u\"",
+		" ",
+		"/bin/echo ' '",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo \"--uu=%u\"",
+		"bob",
+		"/bin/echo \"--uu=__CVE-2026-4408_FallbackUsername__\"",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo \"--uu=%u\"",
+		"bob !0",
+		"/bin/echo \"--uu=__CVE-2026-4408_FallbackUsername__\"",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"!0",
+		"/bin/echo '!0'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo \"--uu=%u\"",
+		"bob \\",
+		NULL,
+		NT_STATUS_INVALID_USER_PRINCIPAL_NAME
+	},
+	{
+		"/bin/echo --uu='%u'",
+		"bob >> x",
+		NULL,
+		NT_STATUS_INVALID_USER_PRINCIPAL_NAME
+	},
+	{
+		"/bin/echo '--uu=%u\"",
+		"bob",
+		"/bin/echo '--uu=__CVE-2026-4408_FallbackUsername__\"",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu='%u'",
+		"bob",
+		"/bin/echo --uu='bob'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu'=%u'",
+		"bob",
+		"/bin/echo --uu'=__CVE-2026-4408_FallbackUsername__'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu'=%u'",
+		"`ls`",
+		"/bin/echo --uu'=__CVE-2026-4408_FallbackUsername__'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu'=%u'",
+		"$(ls)",
+		"/bin/echo --uu'=__CVE-2026-4408_FallbackUsername__'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu='%u'",
+		"$(ls)",
+		"/bin/echo --uu='_(ls)'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu=\"'%u'\"",
+		"bob",
+		"/bin/echo --uu=\"'bob'\"",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu='%u' --yy='%u' '%u' %u",
+		"bob",
+		"/bin/echo --uu='bob' --yy='bob' 'bob' __CVE-2026-4408_FallbackUsername__",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo --uu=%u%u'' %user 50%u",
+		"bob",
+		"/bin/echo --uu=__CVE-2026-4408_FallbackUsername____CVE-2026-4408_FallbackUsername__'' __CVE-2026-4408_FallbackUsername__ser 50__CVE-2026-4408_FallbackUsername__",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"!!",
+		"/bin/echo '!!'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		">xxx",
+		NULL,
+		NT_STATUS_INVALID_USER_PRINCIPAL_NAME
+	},
+	{
+		"/bin/echo %u",
+		"\\",
+		NULL,
+		NT_STATUS_INVALID_USER_PRINCIPAL_NAME
+	},
+	{
+		"/bin/echo %u",
+		"3",
+		"/bin/echo '3'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo '%u'",
+		"3$",
+		"/bin/echo '3_'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo '%u'",
+		"comp$",
+		"/bin/echo 'comp_'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo '%u'",
+		"3$3",
+		"/bin/echo '3_3'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo '%u'",
+		"q $3",
+		"/bin/echo 'q _3'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -s '%u' %u",
+		"āāā",
+		"/bin/echo -s 'āāā' __CVE-2026-4408_FallbackUsername__",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -s '%u' %u",
+		"-āāā",
+		"/bin/echo -s '_āāā' __CVE-2026-4408_FallbackUsername__",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -s %u",
+		"āāā",
+		"/bin/echo -s 'āāā'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -s %u",
+		"a -a",
+		"/bin/echo -s 'a -a'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -s=%u %u",
+		"ā -a",
+		"/bin/echo -s='ā -a' 'ā -a'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -s=\"%u %u\"",
+		"ā -a",
+		"/bin/echo -s=\"__CVE-2026-4408_FallbackUsername__ __CVE-2026-4408_FallbackUsername__\"",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -m='fridge' %u",
+		"ā -x -ß",
+		"/bin/echo -m='fridge' __CVE-2026-4408_FallbackUsername__",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo -m='fridge' %u",
+		"-ā -a",
+		"/bin/echo -m='fridge' __CVE-2026-4408_FallbackUsername__",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"-n",
+		"/bin/echo '_n'",
+		NT_STATUS_OK
+	},
+	{
+		"/bin/echo %u",
+		"o'clock",
+		"/bin/echo 'o_clock'",
+		NT_STATUS_OK
+	},
+};
+
+static void test_expansions(void **state)
+{
+	TALLOC_CTX *mem_ctx = *state;
+	size_t i;
+
+	for (i = 0; i < ARRAY_SIZE(expansions); i++) {
+		struct cmd_expansion t = expansions[i];
+		char *result_cmd = NULL;
+		NTSTATUS status;
+
+		status = check_password_complexity_internal(mem_ctx,
+							    t.lp_cmd,
+							    t.username,
+							    &result_cmd);
+		if (NT_STATUS_IS_OK(t.result_code) && NT_STATUS_IS_OK(status)) {
+			int cmp;
+
+			cmp = strcmp(t.result_cmd, result_cmd);
+			if (cmp == 0) {
+				debug_message("[%zu] «%s» «%s»   ->   «%s», nstatus %s; AS EXPECTED\n",
+					      i, t.lp_cmd,
+					      t.username,
+					      result_cmd,
+					      nt_errstr(status));
+			} else {
+				debug_message("[%zu] «%s» «%s», nstatus %s; "
+					      "expected   «%s»   got  «%s»\033[1;31m BAD! \033[0m\n",
+					      i, t.lp_cmd,
+					      t.username,
+					      nt_errstr(status),
+					      t.result_cmd,
+					      result_cmd);
+			}
+			assert_int_equal(cmp, 0);
+		} else if (NT_STATUS_EQUAL(status, t.result_code)) {
+			debug_message("[%zu] «%s» «%s», nstatus %s FAILED AS EXPECTED\n",
+				      i, t.lp_cmd,
+				      t.username,
+				      nt_errstr(status));
+		} else {
+			debug_message("[%zu] «%s» «%s» -> «%s», nstatus %s; "
+				      "EXPECTED result «%s»   ntstatus %s; \033[1;31m BAD! \033[0m\n",
+				      i, t.lp_cmd,
+				      t.username,
+				      result_cmd,
+				      nt_errstr(status),
+				      t.result_cmd,
+				      nt_errstr(t.result_code));
+			assert_int_equal(true, false);
+		}
+	}
+	debug_message("ALL correct\n");
+}
+
+int main(void)
+{
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(test_expansions),
+	};
+	if (!isatty(1)) {
+		cmocka_set_message_output(CM_OUTPUT_SUBUNIT);
+	}
+	return cmocka_run_group_tests(tests,
+				      setup_talloc_context,
+				      teardown_talloc_context);
+}
diff --git a/source3/utils/testparm.c b/source3/utils/testparm.c
index 2d717f1..6d264e9 100644
--- a/source3/utils/testparm.c
+++ b/source3/utils/testparm.c
@@ -214,6 +214,7 @@ static int do_global_checks(void)
 	const char *socket_options;
 	const struct loadparm_substitution *lp_sub =
 		loadparm_s3_global_substitution();
+	const char *check_pw_script = NULL;
 
 	if (lp_security() >= SEC_DOMAIN && !lp_encrypt_passwords()) {
 		fprintf(stderr, "ERROR: in 'security=domain' mode the "
@@ -525,6 +526,18 @@ static int do_global_checks(void)
 		ret = 1;
 	}
 
+
+	check_pw_script = lp_check_password_script(talloc_tos(), lp_sub);
+	if (talloc_string_sub_mixed_quoting(check_pw_script, 'u')) {
+		fprintf(stderr,
+			"WARNING: You are using 'check password script' "
+			"with mixed quoting and %%u.\n"
+			"CVE-2026-4408 changed the way %%u substitution works. \n"
+			"You should use the SAMBA_CPS_ACCOUNT_NAME "
+			"environment variable exported to the script, or\n"
+			"at least use single quotes (directly) around '%%u'.\n\n");
+	}
+
 	return ret;
 }
 
@@ -622,6 +635,14 @@ static void do_per_share_checks(int s)
 			"parameter is ignored when using CUPS libraries.\n\n",
 			lp_servicename(talloc_tos(), lp_sub, s));
 	}
+	if (talloc_string_sub_mixed_quoting(lp_print_command(s), 'J')) {
+		fprintf(stderr,
+			"WARNING: Service %s defines a 'print command' "
+			"with mixed quoting and %%J.\n"
+			"CVE-2026-4480 changed the way %%J substitution works.\n"
+			"You should use single quotes (directly) around '%%J'.\n\n",
+			lp_servicename(talloc_tos(), lp_sub, s));
+	}
 
 	vfs_objects = lp_vfs_objects(s);
 	if (vfs_objects && str_list_check(vfs_objects, "fruit")) {
diff --git a/source4/nbt_server/wins/winsserver.c b/source4/nbt_server/wins/winsserver.c
index 98789c0..0a1f75e 100644
--- a/source4/nbt_server/wins/winsserver.c
+++ b/source4/nbt_server/wins/winsserver.c
@@ -459,16 +459,34 @@ static void nbtd_winsserver_register(struct nbt_name_socket *nbtsock,
 	struct nbtd_interface *iface = talloc_get_type(nbtsock->incoming.private_data,
 						       struct nbtd_interface);
 	struct wins_server *winssrv = iface->nbtsrv->winssrv;
-	struct nbt_name *name = &packet->questions[0].name;
+	struct nbt_name *name = NULL;
 	struct winsdb_record *rec;
 	uint8_t rcode = NBT_RCODE_OK;
-	uint16_t nb_flags = packet->additional[0].rdata.netbios.addresses[0].nb_flags;
-	const char *address = packet->additional[0].rdata.netbios.addresses[0].ipaddr;
+	struct nbt_res_rec *additional = NULL;
+	uint16_t nb_flags;
+	const char *address = NULL;
+	struct nbt_rdata_address *addresses = NULL;
 	bool mhomed = ((packet->operation & NBT_OPCODE) == NBT_OPCODE_MULTI_HOME_REG);
-	enum wrepl_name_type new_type = wrepl_type(nb_flags, name, mhomed);
+	enum wrepl_name_type new_type;
 	struct winsdb_addr *winsdb_addr = NULL;
 	bool duplicate_packet;
 
+	NBTD_ASSERT_PACKET(packet, src, packet->qdcount > 0);
+	NBTD_ASSERT_PACKET(packet, src, packet->arcount > 0);
+
+	name = &packet->questions[0].name;
+	additional = packet->additional;
+
+	NBTD_ASSERT_PACKET(packet,
+			   src,
+			   additional[0].rdata.netbios.length > 0);
+
+	addresses = additional[0].rdata.netbios.addresses;
+
+	nb_flags = addresses[0].nb_flags;
+	address = addresses[0].ipaddr;
+	new_type = wrepl_type(nb_flags, name, mhomed);
+
 	/*
 	 * as a special case, the local master browser name is always accepted
 	 * for registration, but never stored, but w2k3 stores it if it's registered
@@ -728,13 +746,17 @@ static void nbtd_winsserver_query(struct loadparm_context *lp_ctx,
 	struct nbtd_interface *iface = talloc_get_type(nbtsock->incoming.private_data,
 						       struct nbtd_interface);
 	struct wins_server *winssrv = iface->nbtsrv->winssrv;
-	struct nbt_name *name = &packet->questions[0].name;
+	struct nbt_name *name = NULL;
 	struct winsdb_record *rec;
 	struct winsdb_record *rec_1b = NULL;
 	const char **addresses;
 	const char **addresses_1b = NULL;
 	uint16_t nb_flags = 0;
 
+	NBTD_ASSERT_PACKET(packet, src, packet->qdcount > 0);
+
+	name = &packet->questions[0].name;
+
 	if (name->type == NBT_NAME_MASTER) {
 		goto notfound;
 	}
@@ -870,11 +892,15 @@ static void nbtd_winsserver_release(struct nbt_name_socket *nbtsock,
 	struct nbtd_interface *iface = talloc_get_type(nbtsock->incoming.private_data,
 						       struct nbtd_interface);
 	struct wins_server *winssrv = iface->nbtsrv->winssrv;
-	struct nbt_name *name = &packet->questions[0].name;
+	struct nbt_name *name = NULL;
 	struct winsdb_record *rec;
 	uint32_t modify_flags = 0;
 	uint8_t ret;
 
+	NBTD_ASSERT_PACKET(packet, src, packet->qdcount > 0);
+
+	name = &packet->questions[0].name;
+
 	if (name->type == NBT_NAME_MASTER) {
 		goto done;
 	}
