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

