Hi,

Please find attached two patches for resolv_str_to_dn_label().

The function does not limit the label and name sizes nor reject null
bytes, so the strdup()ed encoded name can be shorter than the
hostname_dn_len kept along with it, and resolv_pick_resolution() /
resolv_build_query() then read past the end of the allocation. With
"do-resolve" fed from request data, a client can trigger it.

The first patch makes the function reject labels over 63 bytes, names
over 255 characters and null bytes. It should be backported to all
stable branches. The second one adds a "resolv_dn_label" unit test
(with a tests/unit/resolvers/resolvers.sh entry), kept separate since
the unit test framework does not exist in older branches.

Tested with an ASan build with -DDEBUG_UNIT: the unit test fails
without the fix and passes with it, and the other unit tests give the
same results as before.

Thanks,
Alejandro Pacheco
From 27427692b92d30a1bd93a9f50c9dbf8d236cdcca Mon Sep 17 00:00:00 2001
From: Alejandro Pacheco <[email protected]>
Date: Wed, 14 Oct 2026 15:44:14 +0200
Subject: [PATCH 1/2] BUG/MEDIUM: resolvers: only encode valid names in
 resolv_str_to_dn_label()
To: [email protected]
Cc: Willy Tarreau <[email protected]>

resolv_str_to_dn_label() stores each label length in a single byte but
does not check the label nor the name size, nor that the name contains
no null byte. Every caller then strdup()s the encoded name while keeping
the returned length as hostname_dn_len, which is later used to memcmp()
and memcpy() the name (resolv_pick_resolution(), resolv_build_query()).
With a label of 256 bytes (a length byte of 0) or a null byte in the
input, the strdup()ed copy is shorter than hostname_dn_len and these
functions read past the end of the allocation. The "do-resolve" action
passes a sample of request data (e.g. the Host header) to this function,
so this can be triggered by a client.

Labels longer than DNS_MAX_LABEL_SIZE (63) bytes, names longer than
DNS_MAX_NAME_SIZE (255) characters (the limit resolv_hostname_validation()
already applies) and null bytes are now rejected, so the encoded name is
always a string of exactly the returned length. Such names could not be
resolved anyway: a label of 64 to 255 bytes was encoded with a length
byte that DNS reserves for compression pointers or other label types.

This should be backported to all stable branches.
---
 src/resolvers.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/resolvers.c b/src/resolvers.c
index 24f9915f5..c0a6e2896 100644
--- a/src/resolvers.c
+++ b/src/resolvers.c
@@ -1920,7 +1920,10 @@ int resolv_dn_label_to_str(const char *dn, int dn_len, char *str, int str_len)
  * this allocated buffer size must be passed in <dn_len>.
  *
  * In case of error, -1 is returned, otherwise, the number of bytes copied in
- * <dn> (excluding the terminating null byte).
+ * <dn> (excluding the terminating null byte). Labels must be at most
+ * DNS_MAX_LABEL_SIZE bytes long, the name at most DNS_MAX_NAME_SIZE characters
+ * long (as resolv_hostname_validation() checks) and contain no null byte, so
+ * that the result is a string of exactly the returned length.
  */
 int resolv_str_to_dn_label(const char *str, int str_len, char *dn, int dn_len)
 {
@@ -1946,6 +1949,8 @@ int resolv_str_to_dn_label(const char *str, int str_len, char *dn, int dn_len)
 			offset = i+1;
 			continue;
 		}
+		if (!str[i] || i - offset >= DNS_MAX_LABEL_SIZE || i >= DNS_MAX_NAME_SIZE)
+			return -1;
 		dn[i+1] = tolower((unsigned char)str[i]);
 	}
 	dn[offset] = i - offset;
-- 
2.48.1

From fd38e29faadae3128be54f1d45087480d2d0273d Mon Sep 17 00:00:00 2001
From: Alejandro Pacheco <[email protected]>
Date: Wed, 14 Oct 2026 15:44:14 +0200
Subject: [PATCH 2/2] TESTS: resolvers: add a unit test for
 resolv_str_to_dn_label()
To: [email protected]
Cc: Willy Tarreau <[email protected]>

The "resolv_dn_label" unit test checks that resolv_str_to_dn_label()
accepts labels of up to 63 bytes and names of up to 255 characters,
rejects longer ones and null bytes, and that the encoded name is a
string of exactly the returned length. It is run by "make unit-tests"
on builds with -DDEBUG_UNIT.
---
 src/resolvers.c                   | 51 +++++++++++++++++++++++++++++++
 tests/unit/resolvers/resolvers.sh | 18 +++++++++++
 2 files changed, 69 insertions(+)
 create mode 100755 tests/unit/resolvers/resolvers.sh

diff --git a/src/resolvers.c b/src/resolvers.c
index c0a6e2896..02dd5f87d 100644
--- a/src/resolvers.c
+++ b/src/resolvers.c
@@ -4361,3 +4361,54 @@ static struct promex_module promex_resolver_module = {
 INITCALL1(STG_REGISTER, promex_register_module, &promex_resolver_module);
 
 #endif
+
+/* resolv_str_to_dn_label() must only produce labels of at most 63 bytes, names
+ * of at most 255 characters, and an encoded name which is a string of exactly
+ * the returned length.
+ */
+int resolv_dn_label_unittest(int argc, char **argv)
+{
+	char str[600], dn[600];
+	int ret = 1, len;
+
+	len = resolv_str_to_dn_label("www.HAProxy.org.", 16, dn, sizeof(dn));
+	if (len != 16 || memcmp(dn, "\3www\7haproxy\3org", 17) != 0)
+		goto out;
+
+	/* a 63-byte label is valid */
+	memset(str, 'a', 63);
+	memcpy(str + 63, ".org", 4);
+	len = resolv_str_to_dn_label(str, 67, dn, sizeof(dn));
+	if (len != 68 || dn[0] != 63 || strlen(dn) != len)
+		goto out;
+
+	/* a 64-byte label is invalid */
+	memset(str, 'a', 64);
+	memcpy(str + 64, ".org", 4);
+	if (resolv_str_to_dn_label(str, 68, dn, sizeof(dn)) != -1)
+		goto out;
+
+	/* a 256-byte label is invalid (its length does not fit in one byte) */
+	memset(str, 'a', 256);
+	memcpy(str + 256, ".org", 4);
+	if (resolv_str_to_dn_label(str, 260, dn, sizeof(dn)) != -1)
+		goto out;
+
+	/* a null byte in the name is invalid */
+	if (resolv_str_to_dn_label("www.ha\0proxy.org", 16, dn, sizeof(dn)) != -1)
+		goto out;
+
+	/* a 255-byte name is valid, a 256-byte one is not */
+	for (len = 0; len < 300; len += 10)
+		memcpy(str + len, "aaaaaaaaa.", 10);
+	len = resolv_str_to_dn_label(str, 255, dn, sizeof(dn));
+	if (len != 256 || strlen(dn) != len)
+		goto out;
+	if (resolv_str_to_dn_label(str, 256, dn, sizeof(dn)) != -1)
+		goto out;
+
+	ret = 0;
+ out:
+	return ret;
+}
+REGISTER_UNITTEST("resolv_dn_label", resolv_dn_label_unittest);
diff --git a/tests/unit/resolvers/resolvers.sh b/tests/unit/resolvers/resolvers.sh
new file mode 100755
index 000000000..c3a53bb67
--- /dev/null
+++ b/tests/unit/resolvers/resolvers.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+check() {
+	${HAPROXY_PROGRAM} -vv | grep -E '^Unit tests list :' | grep -q "resolv_dn_label"
+}
+
+run() {
+	${HAPROXY_PROGRAM} -U resolv_dn_label
+}
+
+case "$1" in
+	"check")
+		check
+	;;
+	"run")
+		run
+	;;
+esac
-- 
2.48.1

Reply via email to