From: Yeonggi Kim Date: Tue, 6 Oct 2026 11:29:10 +0000 (+0900) Subject: BUG/MEDIUM: cli: fix the last command detection on a full command line X-Git-Tag: v3.5-dev9~106 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=6a82757242fc3c5ba37b393220620cea8ca8b53f;p=haproxy.git BUG/MEDIUM: cli: fix the last command detection on a full command line Since 20ec1de214 ("MAJOR: cli: Refacor parsing and execution of pipelined commands"), a whole command line is copied into a chunk before its commands are executed, and the last command is detected by comparing its end with the tail of this chunk: end_of_cmdline = (... : b_tail(appctx->cli_ctx.cmdline)-1); ... if (end == end_of_cmdline) { appctx->st1 |= APPCTX_CLI_ST1_LASTCMD; The chunk is a trash chunk of tune.bufsize bytes, and b_getdelim() fills it up to its last byte when the line, including its final LF, is exactly tune.bufsize bytes long. b_tail() then wraps to the beginning of the area, and the last command is never detected. Once it is executed, the words of the line, separated by NUL bytes by the first parsing, are parsed again one by one as if more commands were pending, and b_del() removes more bytes than the chunk holds, which makes b_data() underflow. This never ends: the CLI sends "Unknown command" errors, keeping a thread busy for as long as the client reads them. A build with DEBUG_STRICT=2 dies instead on the BUG_ON_HOT() in b_del(). With one byte less, the line is processed normally, and with one byte more, it is rejected as too big. The same happens when several commands separated by semicolons fill the chunk, or with a line sent through the master CLI which becomes exactly tune.bufsize bytes long once the master added its prefixes (e.g. "expert-mode on -;"). Reproducer, with "tune.bufsize 1024" and a "stats socket": printf 'show version%1011s\n' '' | socat - /tmp/haproxy.sock The command line chunk is allocated empty by alloc_trash_chunk(), and is only filled by cli_parse_cmdline(), from its beginning, before any of its commands is processed. cli_process_cmdline() then only removes data from its head. So the data never wrap, and the end of the command line is now computed from its head and the amount of data, which also works when the chunk is full. This must be backported as far as 3.2. On 3.3 and 3.2, the end of the command line is tested with "end+1 == b_tail(appctx->cli_ctx.cmdline)", which must become "end+1 == orig + b_data(appctx->cli_ctx.cmdline)". --- diff --git a/src/cli.c b/src/cli.c index 5bdc5a260..21a23a1fb 100644 --- a/src/cli.c +++ b/src/cli.c @@ -817,7 +817,13 @@ int cli_process_cmdline(struct appctx *appctx) orig = p = b_head(appctx->cli_ctx.cmdline); end = p + strlen(p); - end_of_cmdline = (appctx->cli_ctx.payload_pat ? appctx->cli_ctx.payload_pat - strlen(PAYLOAD_PATTERN) : b_tail(appctx->cli_ctx.cmdline)-1); + /* The command line chunk is filled from its beginning and never wraps. + * Don't use b_tail() to find its end, it wraps to the beginning of the + * area when the command line fills the whole chunk. + */ + end_of_cmdline = (appctx->cli_ctx.payload_pat + ? appctx->cli_ctx.payload_pat - strlen(PAYLOAD_PATTERN) + : orig + b_data(appctx->cli_ctx.cmdline) - 1); /* * Get pointers on words. * One extra slot is reserved to store a pointer on a null byte.