Hi,

Here are three small independent fixes against current master
(e5e52e33c). All three are about values from the configuration or the
CLI that were not checked. The patches are attached (git format-patch).

1/3 BUG/MINOR: tcpcheck: check the number of "hdr" in http-check send
The headers of an "http-check send" rule go into a stack array sized
by tune.http.maxhdr plus a terminator, without any bounds check.
With "tune.http.maxhdr 5" and five "hdr" arguments, "haproxy -c"
segfaults on a release build (ASan: dynamic-stack-buffer-overflow
WRITE). The default of 101 cannot be reached because of the 64-word
line limit. The rule is now rejected with "too many headers".
Backport to all stable versions (since 2.2).

2/3 BUG/MINOR: mjson: fix overread on a path ending with a backslash
plen1()/plen2() skip two characters on a backslash, so a json_query
path ending with one, e.g. json_query($.a\\), makes them read past
the trailing zero. ASan reports a heap-buffer-overflow READ in plen1()
on the first request. The upstream mjson code has the same bug.
Backport to all stable versions (since 2.4).

3/3 BUG/MINOR: stick-table: reject negative gpc/gpt array indexes
strtoul() accepts "-1" and wraps it, so data.gpc[-1] on the CLI and
sc-inc-gpc(-1,0)/sc-set-gpt(-1,0)/sc-add-gpc(-1,0) in the config are
accepted and then silently do nothing or fail as "index out of range".
There is no memory issue, since the index is checked before use. This
is the index counterpart of 1f1b701fe3. Backport to all stable
versions, the CLI part only to 3.2 and above.

Tested on e5e52e33c with an ASan+UBSan build and a release build
(USE_OPENSSL=1 USE_PCRE2=1 USE_ZLIB=1). On master, the reproducers for
1/3 and 2/3 trigger the ASan reports above (1/3 also segfaults on the
release build), and the invalid indexes of 3/3 are accepted. With the
patches, 1/3 and 3/3 give a config or CLI error, and 2/3 returns an
empty result with no ASan report. Paths with escaped separators
($.my\\.key) and array indexes still work. "haproxy -c" gives the same
result before and after on the examples/ configs and on the configs
embedded in the checks, stick-table, stickiness, converter and
sample_fetches reg-tests. I could not run the reg-tests themselves
because I do not have vtest here.

These were found with an LLM-Enhanced Static Analysis Engine and verified
manually.

Martin Mirchev
From 79afdca813f906a2d6d06105ab5b90e4e5cf6798 Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Fri, 30 Oct 2026 13:32:30 +0800
Subject: [PATCH 1/3] BUG/MINOR: tcpcheck: check the number of "hdr" in
 http-check send

parse_tcpcheck_send_http() collects the "hdr <name> <value>" arguments
of an "http-check send" rule into a stack array sized by
tune.http.maxhdr, followed by an IST_NULL terminator, but never checks
the number of headers against that size. When tune.http.maxhdr is
lowered and a rule sets as many headers as the limit, the terminator is
written past the end of the array, and each extra header writes one
more entry past it.

This happens while parsing the configuration. E.g. with:

    global
        tune.http.maxhdr 5

    backend b
        option httpchk
        http-check send meth GET uri /health hdr X-H1 v1 hdr X-H2 v2 \
                        hdr X-H3 v3 hdr X-H4 v4 hdr X-H5 v5
        server s1 127.0.0.1:1 check

"haproxy -c" crashes with a segfault on a release build, and ASan
reports a dynamic-stack-buffer-overflow WRITE in
parse_tcpcheck_send_http(). With the default value (101) it cannot be
reached because a config line is limited to 64 words.

Let's reject the rule with an error when there are too many headers,
keeping room for the terminator.

This should be backported to all stable versions. The code dates back to
2.2 and commit e5870d872b ("MAJOR: checks: Implement HTTP check using
tcp-check rules").
---
 src/tcpcheck.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/tcpcheck.c b/src/tcpcheck.c
index 1e245e0c8..7861218f8 100644
--- a/src/tcpcheck.c
+++ b/src/tcpcheck.c
@@ -3090,6 +3090,13 @@ struct tcpcheck_rule *parse_tcpcheck_send_http(char **args, int cur_arg, struct
 				 strcasecmp(args[cur_arg+1], "transfer-encoding") == 0)
 				goto skip_hdr;
 
+			/* keep room for the terminating IST_NULL entry */
+			if (i >= global.tune.max_http_hdr - 1) {
+				memprintf(errmsg, "too many headers (tune.http.maxhdr is %d)",
+					  global.tune.max_http_hdr);
+				goto error;
+			}
+
 			hdrs[i].n = ist(args[cur_arg + 1]);
 			hdrs[i].v = ist(args[cur_arg + 2]);
 			i++;
-- 
2.50.1

From e9f65512bcec62fc7e88367bf77f4c3e85c4613b Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Fri, 30 Oct 2026 13:32:48 +0800
Subject: [PATCH 2/3] BUG/MINOR: mjson: fix overread on a path ending with a
 backslash

plen1() and plen2() measure a JSON path segment and skip two characters
when they see a backslash, so that an escaped '.' or '[' is not taken as
a separator. When the path ends with a backslash, the second skipped
character is the trailing zero, and the loop goes on reading past the end
of the string.

The path comes from the json_query converter's argument, which is
stored in an exactly sized allocation. With:

    http-request set-var(txn.out) req.fhdr(json),json_query($.a\\)

(the config parser turns "\\" into "\", so the path is "$.a\"), any
request carrying a JSON object in the "json" header makes plen1() read
past the end of the 5-byte path. ASan reports a heap-buffer-overflow
READ of size 1 in plen1() called from sample_conv_json_query(). Paths
without a trailing backslash are not affected.

Let's only skip the character following a backslash when it is not the
end of the string, in both functions. The bug is still present in the
upstream mjson project.

This should be backported to all stable versions. The code came with
mjson in 2.4.
---
 src/mjson.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/mjson.c b/src/mjson.c
index 603947d72..97a6f34b0 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -176,7 +176,7 @@ struct msjon_get_data {
 static int plen1(const char *s) {
   int i = 0, n = 0;
   while (s[i] != '\0' && s[i] != '.' && s[i] != '[')
-    n++, i += s[i] == '\\' ? 2 : 1;
+    n++, i += (s[i] == '\\' && s[i + 1] != '\0') ? 2 : 1;
   // printf("PLEN: s: [%s], [%.*s] => %d\n", s, i, s, n);
   return n;
 }
@@ -184,7 +184,7 @@ static int plen1(const char *s) {
 static int plen2(const char *s) {
   int i = 0, __attribute__((unused)) n = 0;
   while (s[i] != '\0' && s[i] != '.' && s[i] != '[')
-    n++, i += s[i] == '\\' ? 2 : 1;
+    n++, i += (s[i] == '\\' && s[i + 1] != '\0') ? 2 : 1;
   // printf("PLEN: s: [%s], [%.*s] => %d\n", s, i, s, n);
   return i;
 }
-- 
2.50.1

From 5cc67f103578520844306accf595b3394f2e9e88 Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Fri, 30 Oct 2026 13:33:07 +0800
Subject: [PATCH 3/3] BUG/MINOR: stick-table: reject negative gpc/gpt array
 indexes

The array index of gpc, gpc_rate and gpt is parsed with strtoul(), which
accepts a leading '-' and negates the result. Thus an index of -1 is
silently turned into a huge unsigned value instead of being rejected:

  - on the CLI, "set table <t> key <k> data.gpc[-1] 7" fails with "index
    out of range in this data array", exactly like data.gpc[4294967295],
    instead of "Unknown data type" as for data.gpc[q]. "show table <t>
    data.gpc[-1] gt 0" is accepted as a filter and matches nothing.

  - in the configuration, "sc-inc-gpc(-1,0)", "sc-set-gpt(-1,0)" and
    "sc-add-gpc(-1,0)" pass "haproxy -c", and the action then does
    nothing at runtime since stktable_data_ptr_idx() finds the index out
    of range.

There is no memory access issue since the index is checked against the
array size before use, but the invalid value should be reported where it
is parsed. Let's require the index to start with a digit, both in
stktable_get_data_type_idx() and in the sc-inc-gpc, sc-set-gpt and
sc-add-gpc action parsers. This is the index counterpart of commit
1f1b701fe3 ("BUG/MINOR: stick-table: reject negative stick counter IDs in
sc-* actions").

This should be backported to all stable versions. The CLI part only
applies to 3.2 and above where stktable_get_data_type_idx() was added,
sc-add-gpc() appeared in 2.8, and the other actions in 2.5.
---
 src/stick_table.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/stick_table.c b/src/stick_table.c
index dbb59f7e0..dbe1c80dd 100644
--- a/src/stick_table.c
+++ b/src/stick_table.c
@@ -1996,6 +1996,8 @@ int stktable_get_data_type_idx(char *name, unsigned int *idx)
 
 		/* we've got a match */
 		name += stop + 1;
+		if (!isdigit((unsigned char)*name))
+			return -1; // bad value (strtoul() would accept "-1")
 		*idx = strtoul(name, &ret, 10);
 		if (ret == name || *ret != ']')
 			return -1; // bad value
@@ -2902,7 +2904,7 @@ static enum act_parse_ret parse_inc_gpc(const char **args, int *arg, struct prox
 	if (*cmd_name == '(') {
 		cmd_name++; /* skip the '(' */
 		rule->arg.gpc.idx = strtoul(cmd_name, &error, 10); /* Convert stick table id. */
-		if (*error != ',') {
+		if (!isdigit((unsigned char)*cmd_name) || *error != ',') {
 			memprintf(err, "Missing gpc ID '%s'. Expects sc-inc-gpc(<GPC ID>,<Track ID>)", args[*arg-1]);
 			return ACT_RET_PRS_ERR;
 		}
@@ -3132,7 +3134,7 @@ static enum act_parse_ret parse_set_gpt(const char **args, int *arg, struct prox
 	if (*cmd_name == '(') {
 		cmd_name++; /* skip the '(' */
 		rule->arg.gpt.idx = strtoul(cmd_name, &error, 10); /* Convert stick table id. */
-		if (*error != ',') {
+		if (!isdigit((unsigned char)*cmd_name) || *error != ',') {
 			memprintf(err, "Missing gpt ID '%s'. Expects sc-set-gpt(<GPT ID>,<Track ID>)", args[*arg-1]);
 			return ACT_RET_PRS_ERR;
 		}
@@ -3325,7 +3327,7 @@ static enum act_parse_ret parse_add_gpc(const char **args, int *arg, struct prox
 	}
 	cmd_name++; /* skip the '(' */
 	rule->arg.gpc.idx = strtoul(cmd_name, &error, 10); /* Convert stick table id. */
-	if (*error != ',') {
+	if (!isdigit((unsigned char)*cmd_name) || *error != ',') {
 		memprintf(err, "Missing gpc ID. Expects %s(<GPC ID>,<Track ID>)", args[*arg-1]);
 		return ACT_RET_PRS_ERR;
 	}
-- 
2.50.1

Reply via email to