]> git.kaiwu.me - haproxy.git/commitdiff
BUG/MEDIUM: cli: Fix parsing of big payloads with dynamically allocated buffer
authorChristopher Faulet <cfaulet@haproxy.com>
Wed, 14 Oct 2026 11:56:09 +0000 (13:56 +0200)
committerChristopher Faulet <cfaulet@haproxy.com>
Fri, 16 Oct 2026 17:07:18 +0000 (19:07 +0200)
Since the 3.4, a dedicated buffer is used for big payload on the CLI. When
the command line exceeds the buffer size because of a big payload, a
dedicated buffer is dynamically allocated to copy the payload. However, on
realloc, input data were moved in the dynamic buffer without parsing
them. This could prevent the end of the payload from being correctly
detected.

To fix the issue, instead of moving input data in the newly allocated
buffer, we retry the parsing.

But a payload line may be longer than tune.bufsize. In this case, only the
beginning of the line is present in the input buffer, and the parsing
always fails to find its end. The input buffer is never emptied and the
flag reporting a full input buffer is never cleared, so the peer is never
unblocked and the remaining part of the line is never received. The CLI
keeps looping on the same partial line, burning the CPU, and the command
is never executed. This was already the case before, when the input data
were moved in the dynamic buffer without parsing them: the part of a line
already received could be moved several times without progress.

Thus, when the parsing fails because the input buffer contains an
incomplete payload line, the part already received is moved into the
payload buffer, waiting for more data, and the full-input-buffer flag is
cleared. For this purpose, the beginning of the incomplete current payload
line is saved in the CLI context, and updated when the payload buffer is
reallocated. When a full line is received, the end of the payload is
searched in the whole assembled line: the end of the payload must also be
detected when the line marking it is itself received in several chunks.

This patch should fix the issue #3524. It must be backported to 3.4.

include/haproxy/applet-t.h
reg-tests/cli/payload_parsing.vtc [new file with mode: 0644]
src/cli.c

index 06e4a6442a2b9588cbea4847826756621b6cd050..ff41fdf82a34765d0bc5e50aea63f6dceec453cf 100644 (file)
@@ -129,6 +129,7 @@ struct appctx {
                int severity_output;    /* used within the cli_io_handler to format severity output of informational feedback */
                int level;              /* the level of CLI which can be lowered dynamically */
                char *payload_pat;      /* Pointer to the payload pattern. NULL if no payload */
+               char *payload_line;        /* Beginning of the incomplete current payload line, NULL if none */
                uint32_t max_payload_sz;/* Max size allowed for dynamic payload. 0 if not allowed */
                uint32_t anon_key;      /* the key to anonymise with the hash in cli */
                int (*io_handler)(struct appctx *appctx);  /* used within the cli_io_handler when st0 = CLI_ST_CALLBACK */
diff --git a/reg-tests/cli/payload_parsing.vtc b/reg-tests/cli/payload_parsing.vtc
new file mode 100644 (file)
index 0000000..8a359a4
--- /dev/null
@@ -0,0 +1,142 @@
+varnishtest "Test CLI payloads with long lines and multiple input refills"
+
+# Two aspects of the CLI payload parsing are tested here, with a small
+# tune.bufsize (1024 bytes) to force the payload buffer to be dynamically
+# allocated and the input to be received in several refills:
+#
+# 1. A payload line may be longer than tune.bufsize. The part of the line
+#    already received must be moved into the payload buffer while waiting
+#    for the remaining part, and the peer must be unblocked, otherwise the
+#    CLI keeps looping on the same partial line forever, burning the CPU,
+#    and the command is never executed.
+#
+# 2. A payload made of several lines shorter than tune.bufsize, but bigger
+#    than the payload buffer, must keep being parsed line by line after the
+#    payload buffer was enlarged, so that the end of the payload is still
+#    detected and the command is executed (issue #3524).
+#
+# A third payload mixes lines shorter and longer than tune.bufsize, to be
+# sure the parsing resumes correctly after a line reassembled from
+# multiple input refills.
+#
+# A fourth payload uses a customized pattern to mark the end of the
+# payload, sent in several chunks: the end of the payload must be detected
+# in the line reassembled from the beginning of the pattern received as an
+# incomplete line and its end received later.
+
+feature ignore_unknown_macro
+
+shell {
+    echo "k0 v0" > ${tmpdir}/map
+}
+
+haproxy h1 -conf {
+    global
+        tune.bufsize 1024
+    stats socket "${tmpdir}/h1/stats" level admin
+
+    defaults
+        mode http
+        timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-header X-V %[req.hdr(x-t),map("${tmpdir}/map")]
+} -start
+
+# 1. A single payload line of 4003 bytes, longer than tune.bufsize, sent
+#    in chunks. If the line is not properly reassembled, the CLI is stuck
+#    and never closes the connection.
+client c1 -connect ${h1_cli_sock} {
+    send "add map ${tmpdir}/map <<"
+    send "\n"
+    send "k1 "
+    send_n 2000 "a"
+    send_n 2000 "b"
+    send "\n"
+    send "\n"
+    expect_close
+} -run
+
+# 2. A payload made of 4 lines of 250 bytes, bigger than the initial
+#    payload buffer: the end of the payload is received after the payload
+#    buffer was enlarged and must still be detected.
+client c2 -connect ${h1_cli_sock} {
+    send "add map ${tmpdir}/map <<"
+    send "\n"
+    send "k2 a"
+    send_n 246 "a"
+    send "\n"
+    send "k3 b"
+    send_n 246 "b"
+    send "\n"
+    send "k4 c"
+    send_n 246 "c"
+    send "\n"
+    send "k5 d"
+    send_n 246 "d"
+    send "\n"
+    send "\n"
+    expect_close
+} -run
+
+# 3. A payload mixing lines shorter and longer than tune.bufsize. The
+#    parsing must resume correctly after a reassembled line.
+client c3 -connect ${h1_cli_sock} {
+    send "add map ${tmpdir}/map <<"
+    send "\n"
+    send "k6 "
+    send_n 1500 "e"
+    send_n 1500 "f"
+    send "\n"
+    send "k7 g"
+    send_n 246 "g"
+    send "\n"
+    send "k8 "
+    send_n 1500 "h"
+    send_n 1500 "i"
+    send "\n"
+    send "\n"
+    expect_close
+} -run
+
+# 4. A payload with a customized end pattern, split across two input
+#    refills: the CLI first receives "12", an incomplete line, then "3\n"
+#    completing the end pattern. The end of the payload must be detected.
+#    If it is not, the command is never executed and the CLI is stuck.
+client c4 -connect ${h1_cli_sock} {
+    send "add map ${tmpdir}/map <<123"
+    send "\n"
+    send "k9 v9"
+    send "\n"
+    send "12"
+    delay 0.5
+    send "3"
+    send "\n"
+    expect_close
+} -run
+
+
+haproxy h1 -cli {
+    # The value of the long lines cannot be displayed, the output is
+    # truncated by the trash chunk size. But the entry must be found,
+    # which proves the payload line was properly reassembled, the end of
+    # the payload detected and the command executed.
+    send "get map ${tmpdir}/map k1"
+    expect ~ "type=str, case=sensitive, found=yes,.*key=\"k1\"[^,]*"
+    send "get map ${tmpdir}/map k2"
+    expect ~ "type=str, case=sensitive, found=yes,.*key=\"k2\", value=\"aaa.*"
+    send "get map ${tmpdir}/map k5"
+    expect ~ "type=str, case=sensitive, found=yes,.*key=\"k5\", value=\"ddd.*"
+    send "get map ${tmpdir}/map k6"
+    expect ~ "type=str, case=sensitive, found=yes,.*key=\"k6\"[^,]*"
+    send "get map ${tmpdir}/map k7"
+    expect ~ "type=str, case=sensitive, found=yes,.*key=\"k7\", value=\"ggg.*"
+    send "get map ${tmpdir}/map k8"
+    expect ~ "type=str, case=sensitive, found=yes,.*key=\"k8\"[^,]*"
+    send "get map ${tmpdir}/map k9"
+    expect ~ "type=str, case=sensitive, found=yes,.*key=\"k9\", value=\"v9\", type=\"str\""
+}
index 463d5193b2c9913a93f4d99d3abf7c875e353cca..c1e9d29e93ff159433e169ad7ecb9b0ce6665f98 100644 (file)
--- a/src/cli.c
+++ b/src/cli.c
@@ -1002,6 +1002,7 @@ int cli_init(struct appctx *appctx)
        appctx->st0 = CLI_ST_PARSE_CMDLINE;
        appctx->cli_ctx.level = bind_conf->level;
        appctx->cli_ctx.payload_pat = NULL;
+       appctx->cli_ctx.payload_line = NULL;
        appctx->cli_ctx.cmdline = NULL;
        appctx->cli_ctx.payload = BUF_NULL;
        appctx->cli_ctx.max_payload_sz = global.tune.cli_max_payload_sz;
@@ -1015,12 +1016,16 @@ int cli_init(struct appctx *appctx)
 int cli_try_realloc_payload(struct appctx *appctx, struct buffer *buf, size_t new_size)
 {
        char *old, *new;
+       unsigned int payload_line_offset = 0;
 
        BUG_ON(new_size == 0);
 
        if (new_size > appctx->cli_ctx.max_payload_sz)
                new_size = appctx->cli_ctx.max_payload_sz;
 
+       if (appctx->cli_ctx.payload_line)
+               payload_line_offset = appctx->cli_ctx.payload_line - b_orig(buf);
+
        old = (appctx->st1 & APPCTX_CLI_ST1_DYN_PAYLOAD) ? b_orig(buf) : NULL;
        new = my_realloc2(old, new_size);
        if (!new) {
@@ -1033,6 +1038,12 @@ int cli_try_realloc_payload(struct appctx *appctx, struct buffer *buf, size_t ne
                memcpy(new, b_orig(buf), b_data(buf));
                appctx->st1 |= APPCTX_CLI_ST1_DYN_PAYLOAD;
        }
+
+       /* The payload buffer may have been reallocated, the beginning of the
+        * current payload line, if any, must be updated accordingly.
+        */
+       if (appctx->cli_ctx.payload_line)
+               appctx->cli_ctx.payload_line = new + payload_line_offset;
        *buf = b_make(new, new_size, 0, b_data(buf));
        return 0;
 }
@@ -1061,6 +1072,7 @@ int cli_parse_cmdline(struct appctx *appctx)
                if (!b_data(&appctx->inbuf))
                        break;
 
+       retry:
                /* payload doesn't take escapes nor does it end on semi-colons,
                 * so we use the regular getline. Normal mode however must stop
                 * on LFs and semi-colons that are not prefixed by a backslash.
@@ -1076,7 +1088,6 @@ int cli_parse_cmdline(struct appctx *appctx)
                        else
                                len = b_getline(&appctx->inbuf, 0, b_data(&appctx->inbuf), str, b_room(buf) - 1);
                }
-
                if (!len) {
                        if (!b_room(buf) || (b_data(&appctx->inbuf) > b_room(buf) - 1)) {
                                if (!(appctx->st1 & APPCTX_CLI_ST1_PAYLOAD)) {
@@ -1096,7 +1107,17 @@ int cli_parse_cmdline(struct appctx *appctx)
                                        goto error;
                                }
 
-                               /* A bigger payload buffer was allocated, wait for more data */
+                               /* A bigger payload buffer was allocated, retry to parse input buffer */
+                               goto retry;
+                       }
+                       /* The input buffer contains the part of a payload line
+                        * and the remaining part is still to be received. Copy
+                        * it to make some space.
+                        */
+                       if (appctx->st1 & APPCTX_CLI_ST1_PAYLOAD) {
+                               /* save the beginning of the current payload line */
+                               if (!appctx->cli_ctx.payload_line)
+                                       appctx->cli_ctx.payload_line = b_tail(buf);
                                b_xfer(buf, &appctx->inbuf, b_data(&appctx->inbuf));
                                applet_fl_clr(appctx, APPCTX_FL_INBLK_FULL);
                        }
@@ -1191,7 +1212,17 @@ int cli_parse_cmdline(struct appctx *appctx)
 
                        /* look for a pattern at the end of the payload
                         * (take care to exclude the last character because it is a \n)
+                        *
+                        * <str> may be the end of a line received in several part.
+                        * If the beginning of the current payload line was remembered,
+                        * the pattern is searched in the whole assembled line.
                         */
+                       if (appctx->cli_ctx.payload_line) {
+                               str = appctx->cli_ctx.payload_line;
+                               len = b_tail(buf) - str;
+                               appctx->cli_ctx.payload_line = NULL;
+                       }
+
                        if (len-1 == strlen(appctx->cli_ctx.payload_pat)) {
                                if (strncmp(str, appctx->cli_ctx.payload_pat, len-1) == 0) {
                                        /* end of payload was reached, rewind before the previous \n, if any, and replace it by a \0
@@ -1423,6 +1454,7 @@ void cli_io_handler(struct appctx *appctx)
                                if (appctx->st1 & APPCTX_CLI_ST1_DYN_PAYLOAD)
                                        free(b_orig(&appctx->cli_ctx.payload));
                                appctx->cli_ctx.payload_pat = NULL;
+                               appctx->cli_ctx.payload_line = NULL;
                                appctx->cli_ctx.cmdline = NULL;
                                appctx->cli_ctx.payload = BUF_NULL;
                                appctx->st1 &= ~(APPCTX_CLI_ST1_LASTCMD|APPCTX_CLI_ST1_DYN_PAYLOAD);