Implements a new "kill" admin CLI command. This allows stopping other
running commands.
This patch is implemented in two parts. First, the "kill" parser, which
loops over CLI jobs and sets the CLI_CMD_FL_KILLED flag on the targeted
ones.
The other change is in cli_io_handler() so that each targeted job reacts
to the CLI_CMD_FL_KILLED flag. If set, normal CLI flow is interrupted
and the io_release handler is called. Any release pending message is
suppressed and "Killed." is displayed to notify the user.
Currently, "kill" can only stop uninterruptible commands. This must
still be specified as a mandatory argument to support future extensions
of the command.
MINOR: sink: add timeout argument for "show events"
Define a new "-u" argument for the CLI show events command. This allows
activating the uninterruptible execution mode for
cli_io_handler_show_ring(). The inactivity delay is directly derived
from the configured stats timeout.
MINOR: ring: support inactivity timeout in show ring CLI handler
This patch implements uninterruptible mode of execution when monitoring
a ring via the CLI. The handler continues to run despite the detection
of the CLI input closure.
The command is still stopped if a send error occurs while reporting new
events. Also, the socket timeout is reused as an inactivity timeout,
refreshed each time new events are detected. The command is stopped if
there is nothing to report during this delay.
Amaury Denoyelle [Thu, 29 Oct 2026 14:24:59 +0000 (15:24 +0100)]
MINOR: cli: implement uninterruptible mode for wait
Define a new argument "-u" for the CLI wait command. This allows
executing wait in uninterruptible mode, which is convenient when using
utilities such as socat.
The new argument is parsed in cli_parse_wait(). If present, the command
will be started as an uninterruptible CLI job and will be accounted for
as such, unless the uninterruptible jobs limit is reached.
Amaury Denoyelle [Thu, 29 Oct 2026 14:38:59 +0000 (15:38 +0100)]
MINOR: cli: implement basic mechanism for uninterruptible jobs
Interactive CLI commands will be interrupted when the CLI input
is disconnected. This is a safety measure to ensure a long running
command won't consume unnecessarily a CLI connection slot without any
means of shutting it down.
A disconnection is detected by the closure of the remote write side as
this is the only way to detect it when there is nothing to send.
However, this causes issues with utilities such as socat running with a
timeout which perform a half-close on their write channel when the
command has been sent. For example, it is currently impossible to
execute "wait" with socat, unless the special option "ignoreeof" is set.
Introduce a new concept of CLI uninterruptible jobs. This mode of
execution will only be available to long-running commands with a
dedicated argument. Under this mode of execution, the execution will
continue despite the remote write channel being already closed.
A limit of concurrent uninterruptible jobs is set to 3 to ensure they
won't exhaust the default CLI maxconn of 10.
MINOR: cli: store active CLI sessions in a global list
Implement a global list of the currently active CLI sessions. This is
sound as since only one CLI frontend exists per HAProxy instance (via
"stats socket" for a worker, or alternatively a master CLI via -S for a
master process).
Each session is appended in the CLI_ST_INIT state at the beginning of
cli_io_handler() and removed in cli_release_handler().
This will allow to implement new CLI commands which can interact with
other CLI sessions.
Define a new helper cli_reset_cmd() which is used to reset the CLI
context between each executed command. This will become useful to
introduce new advanced CLI mechanisms.
BUG/MEDIUM: ssl: don't use MSG_MORE for kTLS control records
ha_ssl_write() also applies the application's SEND_MORE hint to kTLS
control records. Linux rejects TLS_SET_RECORD_TYPE with MSG_MORE with
EINVAL. If application data are still buffered, the failed close_notify
leaves the response tail unsent when the socket closes.
Fix that by just not using MSG_MORE for control records.
BUG/MEDIUM: mux-h2: also consider the fctl_list as pending data for the timeout
The previous commit 87cf2a6b ("BUG/MEDIUM: mux-h2: consider the send list
as pending data for the timeout") was incomplete: a stream blocked on the
connection window (H2_SF_BLK_MFCTL) may also be orphaned by h2_detach()
after its stream connector left, in which case it sits on the fctl_list
and not on the send_list. Nothing of its frame was emitted yet (typically
an ES/RST frame via a WANT_SHUTW, or the remaining fast-forward bytes of
its iobuf), so br_data() on the mux buffer can be zero.
Such a connection was still considered idle and, on a backend connection,
was armed with no timeout at all while not being reusable: the orphan is
still in streams_by_id so it is not moved to the idle list either. It
then relied on the peer to send a WINDOW_UPDATE to ever wake up, which a
stalled peer never does.
The blocked_list is deliberately not considered: streams blocked on their
own stream window (H2_SF_BLK_SFCTL) are never orphaned by h2_detach(), so
this list is always empty once the last stream connector detached.
This matches the original h2c_may_expire() from before commit 3439583dd
("MEDIUM: mux-h2: slightly relax timeout management rules") which
considered both lists. mux-fcgi is not affected, it has no flow-control
window and no fctl list.
This could be backported as far as 2.6, along with 87cf2a6b.
Signed-off-by: Christopher Faulet <cfaulet@haproxy.com>
MINOR: mux-fcgi: don't add a closed connection to an idle list on detach
In fcgi_detach(), the test deciding whether the connection may be moved to
an idle list checks FCGI_CF_EOS, FCGI_CF_ERR_PENDING, FCGI_CF_ERROR and
FCGI_CF_KEEP_CONN, but not the connection state. A connection that already
switched to FCGI_CS_CLOSED may thus be inserted in the session server list
or in the server's idle pool, while fcgi_avail_streams() reports no
available stream for it. The stream immediately before, in the same
function, does test FCGI_CS_CLOSED.
This can currently only happen with a private connection, since
srv_add_to_idle_list() already rejects connections without available
streams, and it is not harmful either way: session_get_conn() also
requires an available stream, so such a connection is never reused. But
it is still wrong to park it, since it prevents the session from
dropping it and forces a useless idle counter increment.
Note that fcgi_conn_is_dead() must not be used here: it also requires
FCGI_CF_KEEP_CONN to be cleared or FCGI_CF_ERROR to be set, so it does not
cover every FCGI_CS_CLOSED connection, and the non-private branch below
clears conn->owner before parking the connection.
MINOR: mux-fcgi: check for a dead connection before refreshing the timeout
fcgi_detach() refreshed the connection timeout right after the stream was
destroyed, then evaluated fcgi_conn_is_dead() only at the end of the
function, on the paths that did not already return. So a connection about to
be released was first armed and requeued via task_queue(), then
fcgi_release() cleared its task context and woke it up again for nothing.
Let's evaluate fcgi_conn_is_dead() first, right after the stream is
destroyed, and return early when the connection must be released. The check
that closed the function is then redundant and is removed, and the timeout
is only refreshed for connections that survive.
This only reorders statements: fcgi_conn_update_timeout() writes the task's
expiration date, the FCGI_CF_IDL_PING-like state and the timer queue, none
of which fcgi_conn_is_dead() reads. It also matches the structure mux-h2
just got. The move is safe with regards to the idle lists, since the
branches below only set conn->owner, the session and idle list entries and
the transport idle state, which the predicate does not look at either.
Note that this was only correct once the previous commit removed the
conn->owner test from fcgi_conn_is_dead(): the non-private branch below
clears conn->owner before parking the connection, so evaluating the
predicate first would otherwise report every connection as dead.
BUG/MEDIUM: mux-fcgi: ignore conn->owner when deciding if a connection is dead
fcgi_conn_is_dead() reports a connection as dead when it has no stream left
and conn->owner is NULL, on the assumption that an ownerless connection has
nobody left to take care of it.
That assumption no longer holds. Idle backend connections are stored in the
server, reachable through a hash, so they are no longer attached to a
session and may legitimately have conn->owner NULL while still being
perfectly usable. This is exactly the situation mux-h2 hit: the first stream
on a fresh connection sets conn->owner, but a later stream picking that
connection from the idle pool does not, so the connection is destroyed the
next time its last stream detaches instead of being returned to the
pool. Commit a0541f5d2 ("BUG/MEDIUM: mux-h2: ignore conn->owner when
deciding if a connection is dead") removed the test from h2c_is_dead() for
that reason; do the same here.
The test was also actively harmful in fcgi_detach(): the non-private branch
clears conn->owner and then calls fcgi_conn_is_dead(), so the predicate was
true by construction at that point, which would have prevented returning the
connection to the server's idle pool at all. It only worked because
detach() tests the flags and FCGI_CF_KEEP_CONN first and never reaches the
tail for such a connection.
Note that mux-fcgi has no reverse-connection support, so unlike mux-h2 there
is no conn_is_reverse() companion test to remove.
This should be backported wherever a0541f5d2 was backported, so as far as 2.8.
BUG/MEDIUM: mux-fcgi: detect an early connection error during the handshake
fcgi_conn_is_dead() treats a connection with FCGI_CF_ERROR as dead, but not
one with FCGI_CF_ERR_PENDING. The latter is set by fcgi_send() when the
transport reports an error, and is only turned into FCGI_CF_ERROR once
FCGI_CF_EOS is also set, i.e. once a read0 was seen. A connection whose
transport failed while the GET_VALUES handshake was still in progress
carries FCGI_CF_ERR_PENDING alone, and is therefore considered alive, so
nothing closes it: fcgi_send() bails out immediately on the flag,
fcgi_recv() is not allowed to run either (fcgi_recv_allowed() refuses when
the wait event is subscribed for SEND only... in practice nothing makes
progress any more), and h_detach()'s idle-list branch excludes it so it is
not parked either. It is only released when the idle-connection purge
reaches it.
This is the same situation mux-h2 fixed in commit 21fb6bdab ("BUG/MEDIUM:
mux-h2: Be able to detect connection error during handshake"), which reports
a connection as dead when H2_CF_ERR_PENDING is set before any frame was
received. Let's do the same here: a connection with FCGI_CF_ERR_PENDING
whose state is still below FCGI_CS_RECORD_H has not exchanged a single FCGI
record yet, so it cannot be reused and is dropped.
The state threshold differs from mux-h2: there, H2_CS_FRAME_H means "waiting
for the next frame header" and is reached before any frame is processed,
while FCGI_CS_RECORD_H is the state where records may be exchanged, the
three states below it covering only the GET_VALUES handshake.
This should be backported to all supported versions.
BUG/MEDIUM: mux-fcgi: arm the connection timeout only when data remains
The commit "BUG/MEDIUM: mux-fcgi: do not enforce the timeout on connections
with streams" made the connection timeout inactive while stream connectors
are attached, since the stream layer is then responsible for timeouts. It
has to become active again once the last stream detaches, which detach()
does not do in all cases: when the stream is blocked waiting for room in the
mux buffer, the function returns early without touching the timeout, and
when the connection is moved to an idle list, it returns before the refresh
at the end of the function.
In both cases the connection stays without any timeout. When a client aborts
a large POST to a FastCGI server that does not read it, the connection with
its full mux buffer is put in the server's idle list and is only closed
later by the purge of the idle connections, instead of after "timeout
server".
Arming "timeout server" on every connection without a stream is wrong
though: a connection with nothing left to send is idle and reusable, and
belongs to the idle connection pool for its lifetime. Arming the server
timeout there closes it while it sits in the idle list, defeating connection
reuse, unlike idle backend H1 and H2 connections which are left without a
mux timeout.
So the update is moved to fcgi_conn_update_timeout(), which now tells the
cases apart, and is called once right after the stream is destroyed, before
the connection may be moved to an idle list. The refresh that used to sit at
the end of the function is dropped: everything between the two points only
manipulates session and idle lists, so the second call recomputed the very
same expiration date.
- streams attached: no connection timeout, the stream layer knows
whether it waits for the server or for the client;
- nothing attached, but data pending in the mux buffer or streams still
queued in the send list (blocked on a missing mux buffer, their bytes
still in their own buffer): the regular data timeout, so that the
data gets a chance to be drained and the connection closed after it;
- nothing attached, nothing to send, connection closing: the shutdown
timeout, otherwise nothing would close it;
- nothing attached, nothing to send, connection alive: no timeout, it
is an idle connection.
tick_add_ifset() replaces tick_add() so an unset timeout cannot arm an
already expired timer.
It is related to GitHub issue #3558 and should be backported along with the
mentionned commit to all supported versions.
BUG/MEDIUM: mux-h2: refresh the connection timeout before parking it idle
h2_detach() refreshes the connection timeout in two places only: in the
early return taken when the stream is blocked, and at the very end of
the function. The branches that move the connection to an idle list
(the session's private list or the server's idle pool) return before
reaching the tail, so a connection detached with data still queued in
the mux buffer is parked without any timeout and nothing drains it. The
timeout used to be refreshed for idle-ping only, which left every other
case aside.
The refresh is now performed right after h2s_destroy(), before the
connection may be given away, and the redundant call at the end of the
function is dropped: everything between the two points only manipulates
session and idle lists, so it recomputed the very same expiration date.
h2c_update_timeout() already returns early when there is no task, so no
guard is needed at the call site.
The dead-connection check is moved ahead of the refresh, so that a
connection about to be released is not armed and requeued just before
h2_release() clears its task context. This only reorders statements:
h2c_update_timeout() touches neither the stream map nor the flags and
states h2c_is_dead() looks at, so the predicate gives the same result at
both places. Since a dead connection now returns early, the
"!h2c_is_dead(h2c)" test that guarded the idle-list branches, and the
h2c_is_dead() check that closed the function, are both redundant and are
removed. Commit 59c599f3f ("BUG/MEDIUM: mux-h2: make sure not to move a
dead connection to idle") is thus enforced structurally rather than by a
duplicated test.
BUG/MEDIUM: mux-h2: consider the send list as pending data for the timeout
When no stream connector is attached anymore, h2c_update_timeout() only
looks at the mux buffer to decide whether the connection still has data
to send, in which case it must be armed with the regular data timeout.
Streams may however still be queued in the send list while their data
sits in their own iobuf: they are blocked on a missing mux buffer, so
br_data() on the mux buffer is zero even though some bytes remain to be
sent. Such a connection is then considered idle and armed with neither
the data timeout nor anything else.
Let's also consider the send list in the condition, as it was part of
the original h2c_may_expire() before commit 3439583dd ("MEDIUM: mux-h2:
slightly relax timeout management rules") reduced it to the attached
stream count, with only br_data() being put back afterwards. This
matches what fcgi_conn_update_timeout() does.
Willy Tarreau [Mon, 9 Nov 2026 10:12:01 +0000 (11:12 +0100)]
BUILD: debug: fix _COUNT undefinied with USE_OBSOLETE_LINKER
We recently introduced the COUNT() macro with commit 29925e59a7 ("MINOR:
debug: add a COUNT() macro which doesn't take a condition"). However it
relies on a _COUNT() macro that is not defined when USE_OBSOLETE_LINKER
is used, causing build failures visible in the CI with ASAN builds.
Let's just add it. No backport needed.
Willy Tarreau [Sun, 8 Nov 2026 11:03:45 +0000 (12:03 +0100)]
BUG/MEDIUM: compression: use a trash chunk as large as the buffer for the output
Since commit a0b37fb47 ("BUG/MEDIUM: compression: fix incompatibility
between compression and large-buffers"), slz_encode() writes directly into
the output chunk passed by the compression filter, which is the global
trash, sized like regular buffers. But with large buffers, a DATA block may
be larger than this trash. The input is then cut to the trash size by the
test added in commit f82ace414 ("MEDIUM: compression: Be sure to never
compress more than a chunk at once"), leaving no room for the deflater's
overhead. On incompressible data, the first call emits the input plus 5
bytes for the stored block plus the 10 bytes of the gzip header, which
overflows the trash:
FATAL: bug condition "b->data + count > b->size" matched at include/haproxy/buf.h:395
#0 b_add (b=..., count=32783) at include/haproxy/buf.h:395
#1 comp_rfc195x_add_data (..., in_len=32768, out=...) at src/compression.c:404
This was seen in production on a PDF served with 32 kB buffers and
"http-response wait-for-body use-large-buffer", where the body formed a
37 kB DATA block. Without DEBUG_STRICT=2, the overflow goes unnoticed.
The output replaces the input in the HTX, so the right size for the output
chunk is the one of the buffer it comes from. It is now obtained with
get_best_trash_chunk(), which returns a large trash chunk for large buffers
and the regular one otherwise. htx_compression_buffer_init() still ensures
that the HTX has the room for the deflater's overhead, so a DATA block
plus this overhead always fits in such a chunk. A side effect is that the
data of a large buffer are now compressed in a single call, as intended by
the use of large buffers for compression.
The reg-test compression/large_buffers.vtc reproduces the crash with
unpatched versions built with DEBUG_STRICT=2.
This must be backported wherever a0b37fb47 is backported.
BUG/MINOR: server: fix a leak when completing a named default-server
A named default-server may be defined over several lines, each line
completing the previous one, as documented and as exercised by
reg-tests/server/from_keyword.vtc. In _srv_parse_init(), the name is
duplicated with strdup() before being looked up in defsrv_by_name.
When the lookup finds the existing default-server, the copy is neither
stored nor freed, so every line after the first one leaks it.
The leak only happens at configuration load and is limited to the name
length plus one byte per line, so the impact is minimal, but it is
reported by LeakSanitizer, e.g. "haproxy -c" built with
-fsanitize=address on a configuration defining the same
"default-server name other ..." on several lines.
Fix it by freeing the copy when an existing default-server is found,
since that one already owns its name. The code path is already covered
by reg-tests/server/from_keyword.vtc, so no new regression test is
added.
This was introduced with named default-servers by commit 507f790af
("MEDIUM: proxy: implement named default-server") in 3.5-dev4, so no
backport is needed.
BUG/MINOR: applet: accept a temporarily owned tid in appctx_init()
appctx_init() makes sure the appctx is initialized on the thread it is bound
to, and checks it with a BUG_ON() which tolerates an unbound task (tid ==
-1), since an appctx may be created without a target thread and left to
whichever thread handles it first. That tolerance was added by commit 4f7c26cbb ("BUG/MINOR: applet: Don't trigger BUG_ON if the tid is not on
appctx init") for 2.8, at a time when a task's thread ID was either a real
thread number or -1.
Since commit 8b6d8f5e4 ("MINOR: tasks: Add __task_get_new_tid_field()"), the
scheduler encodes the temporary ownership of a shared task differently: when
a thread takes ownership of a task whose tid is -1, it sets -2 - <thread>
instead, so that no other thread can grab it before it is queued or
run. That value is still "not bound" but designates a specific owner, and it
is what task_wakeup() and task_queue() now set through
__task_get_new_tid_field(). The task code already knows about it:
__tasklet_wakeup_here() carries the very same triple test, and
task_set_thread() has an explicit early return for "t->tid == -2 - tid" when
the requested thread is -1.
appctx_init() did not, so it would trip the BUG_ON() on a task that happens
to be in that transient state, instead of adopting the current thread as
task_set_thread() is about to do. Let's accept -2 - tid here too, so that
the three states handled by task_set_thread() are all accepted: unbound,
bound to the current thread, or temporarily owned by the current thread.
This is not reachable today: every appctx is created with an explicit target
thread (appctx_new_here() or appctx_new_on() with a valid thread number), so
its task tid is never -1 and never -2 - tid, and appctx_new_anywhere()
currently has no caller. It is nonetheless a latent bug: any future user of
appctx_new_anywhere(), which is the intended way to start an appctx on a
thread subset, would hit the BUG_ON() as soon as the task is woken up before
being initialized.
The "-2 - tid" encoding was introduced in 3.5-dev1, so no backport needed.
Willy Tarreau [Fri, 6 Nov 2026 15:17:00 +0000 (16:17 +0100)]
CLEANUP: debug/tree-wide: replace COUNT_IF(1, ...) with COUNT()
This patch replaces 28 occurrences of COUNT_IF(1, msg) with COUNT(msg).
This makes the counters output more readable, the code as well, and even
saves a few bytes (148) in the executable.
Willy Tarreau [Fri, 6 Nov 2026 15:11:47 +0000 (16:11 +0100)]
MINOR: debug: add a COUNT() macro which doesn't take a condition
Most of the COUNT_IF() calls are placed in locations where they're not
part of a condition, but we just want to count one pass at a specific
location (e.g. branch of an "if" or after a label, etc). In such cases
it's systematically written as COUNT_IF(1, ...), but it's ugly, adds
bytes in the code, and needlessly clobbers the output.
Let's introduce a new variant COUNT() which doesn't require a condition
and only counts the number of passes. This is possible since COUNT_GLITCH
was introduced and already unlocked the ability to write messages without
conditions, so let's just exploit this.
BUG/MEDIUM: quic: fix loss time selection in quic_loss_pktns()
quic_loss_pktns() must return the packet number space with the earliest
loss time. But an unset loss time (TICK_ETERNITY, i.e. 0) could win the
selection and hide a real loss time.
Indeed, since a55acf993 ("BUG/MINOR: quic: ticks comparison without
ticks API use"), the candidate loss time is compared with
tick_is_lt(). An unset candidate (0) then beats any armed loss time T
whenever (0 - T) wraps to a negative value, that is to say whenever
T lies in the low half of the 32-bit tick circle. c7c87641 ("MINOR:
quic: Wrong packet number space selection in quic_loss_pktns()") had
already fixed the same problem for the naive comparison in 2021, but
the guard was inverted by 1f6cf181 in the same year.
The bug is not a rare calendar coincidence: clock.c forces the internal
time to wrap BOOT_TIME_WRAP_SEC (20) seconds after boot, after which
now_ms starts over from zero and lives in the low half of its 32-bit
circle for about 24.9 days. Thus, on every instance restarted less
than ~25 days ago, the loss time of the Initial packet number space
could be discarded. The Initial packets in flight were then never
declared lost, their loss time was never honored, and the PTO branch
kept arming probe requests on both Initial and Handshake spaces
without end, as long as the Handshake packets never reached the
client: with the Handshake probe flag cleared without emitting
(quic_tx.c), the server entered a tight timer loop sending padded
Initial PINGs over and over without ever retransmitting the Handshake
CRYPTO data. A conforming client breaks this loop within about one
second by sending the anti-deadlock Handshake probe it must arm when
it holds Handshake keys but no Handshake packet of its own was
acknowledged; a loop lasting minutes was only observed with a client
missing this behavior.
Fix the selection by considering only the spaces with an armed loss
time. If none is armed, the function returns the first space of the
list, and both callers already check tick_isset() on the returned
loss time, so nothing else changes.
Thank you to @jbevemyr for having reported this issue in GH #3552.
This could be backported as far as 2.6 where the tick_is_lt() call
was introduced.
BUG/MEDIUM: mux-fcgi: do not enforce the timeout on connections with streams
The FCGI connection task is rearmed with "timeout server" on each pass
in fcgi_process(), and fcgi_timeout_task() kills the connection when it
expires, even when streams are still attached to it. With a slow
client, HAProxy stops reading from the FastCGI server once the buffers
are full, so nothing happens on the FCGI connection for a while, and
the connection is killed in the middle of the response. The response is
truncated, the log reports "SD--", and the FastCGI server gets a reset
while it is blocked in write().
Whether the stream waits for the server or for the client is only known
by the stream layer, which already enforces "timeout server" when the
server does not answer. So, as done for the H2 mux by commit c2ea47fb1
("BUG/MEDIUM: mux-h2: do not enforce timeout on long connections", for
GitHub issue #308), the connection timeout is now only enforced when no
stream connector is attached anymore: fcgi_conn_may_expire() mirrors
h2c_may_expire(), fcgi_process() leaves the task unarmed while streams
are attached, and fcgi_timeout_task() does not kill the connection in
this case.
A FastCGI server which does not answer at all still gets a 504 after
"timeout server", reported as "sH--", as before.
This fixes GitHub issue #3558. It should be backported to all supported
versions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
Adam Crawford [Thu, 5 Nov 2026 23:32:47 +0000 (17:32 -0600)]
BUG/MEDIUM: filters: avoid NULL block access in decompression
The decompression filter may end up with a NULL block pointer when
processing an empty request. If no replacement block is provided,
htx_get_next_blk() is called with this NULL pointer, causing HAProxy
to crash.
Only advance to the next block if the current block pointer is valid.
Willy Tarreau [Thu, 5 Nov 2026 17:14:02 +0000 (18:14 +0100)]
MEDIUM: h1/violations: restrict valid version strings to "1.0" and "1.1"
The "invalid-version-string" HTTP violation only permitted to use
versions which are not made of exactly one digit, a dot, and one digit.
But the only versions where such a version is advertised are 1.0 and
1.1, and after them no other version is advertised. This means that the
only valid tokens are exactly 1.0 and 1.1. There's no point in accepting
"0.9", "1.2" nor "2.0" for example, and while haproxy handles them
carefully, some components only look at the second digit, or apply "&1"
to it resulting in 1.2 becoming 1.0 etc. Better be strict on this and
block these ones unless the violation is explicitly accepted. No
visible effect is expected given that the vast majority of servers and
proxies already do not accept other versions.
The index of the gpc, gpc_rate and gpt arrays is never checked where it
is parsed, so invalid values are silently accepted:
- the sc-inc-gpc, sc-set-gpt and sc-add-gpc actions parse it with
strtoul(), which accepts a sign and wraps "-1" to UINT_MAX, and
which returns 0 for an empty index, so "sc-inc-gpc(,0)" updates
gpc[0]. Values of 100 and above are accepted as well.
- the sample fetches and converters taking an index (sc_get_gpc,
sc_get_gpt, sc_gpc_rate, sc_inc_gpc, sc_clr_gpc, sc1_clr_gpc and the
src_* and table_* variants) take it as a signed integer without any
check, so sc_get_gpc(-1,0) or table_gpc(100,t) pass "haproxy -c" and
never return anything.
- on the CLI, "data.gpc[-1]" is taken as index 4294967295, so "set
table" reports "index out of range in this data array" instead of
rejecting the name as for "data.gpc[q]", and the "show table" and
"clear table" filters accept it and match no entry, as they do for
any index beyond the table's array.
Since the index is stored as an unsigned int, values above UINT_MAX are
even truncated: "sc-inc-gpc(4294967296,0)" increments gpc[0] and
sc_get_gpc(4294967296,0) returns gpc[0], and the same happens on the
CLI. There is no memory access issue since stktable_data_ptr_idx()
checks the index against the array size before use, but these errors
should be reported where the index is parsed. The documentation states
that <idx> is an integer between 0 and 99, which is
STKTABLE_MAX_DT_ARRAY_SIZE - 1.
This patch adds stktable_parse_array_idx(), which requires the index to
start with a digit and to be lower than STKTABLE_MAX_DT_ARRAY_SIZE, and
uses it in stktable_get_data_type_idx() and in the three action parsers.
The fetches and converters get a new argument check function which does
the same verification on their first argument. Finally, the CLI table
filters now reject an index beyond the array size of the table, as "set
table" already does. The table is not known when the actions, fetches
and converters are parsed, so they can only be checked against the
maximum array size.
This is the index counterpart of commit 1f1b701fe3 ("BUG/MINOR:
stick-table: reject negative stick counter IDs in sc-* actions").
Configurations using an invalid index are now rejected, but such an
index never worked, or silently used gpc[0]/gpt[0].
This should be backported to all stable versions where the gpc/gpt
arrays exist (2.5 and above). The CLI parts only apply to 3.2 and above
where stktable_get_data_type_idx() was added, and sc-add-gpc() appeared
in 2.8.
Alex Joseph [Tue, 3 Nov 2026 21:19:51 +0000 (21:19 +0000)]
MINOR: tcpcheck: add post-80-sha2 mode to mysql-check for caching_sha2_password
MySQL 8.0 made caching_sha2_password the default authentication plugin
and MySQL 8.4 disables mysql_native_password by default (it is removed in
9.0). None of the existing mysql-check modes works with a
caching_sha2_password check user, and each of them generates an aborted
connection on the server for every check:
- pre-41 / post-41 do not set CLIENT_PLUGIN_AUTH, so the server rejects
the handshake with error 1251 ("Client does not support
authentication protocol requested by server") and the server is
marked DOWN.
- post-80 announces mysql_native_password, so the server answers with
an AuthSwitchRequest (0xFE) to caching_sha2_password. The check
accepts it as a valid reply since it is not an error packet, and
reports the server UP. But the COM_QUIT pipelined after the handshake
response is read by the server as the auth switch reply, and the
handshake is aborted with error 1156 ("Got packets out of order").
In both cases the server counts a connection error for the HAProxy host
(Aborted_connects, and the host cache handshake errors when
skip_name_resolve is OFF, which is the MySQL default, and the HAProxy
address resolves forward and backward). Once the host reaches max_connect_errors
(100 by default), the server blocks it with ER_HOST_IS_BLOCKED (1129)
and refuses every connection from it, including proxied client traffic,
until FLUSH HOSTS is issued. With post-80 this happens while the check
reported the server as healthy.
This patch adds a "post-80-sha2" mode which sets CLIENT_PLUGIN_AUTH and
announces caching_sha2_password with an empty auth response. For a
password-less caching_sha2_password account the server authenticates in
a single round trip and replies with an OK packet, then the pipelined
COM_QUIT closes the session cleanly, so no connection error is recorded:
CREATE USER 'haproxy'@'%' IDENTIFIED WITH caching_sha2_password BY '';
option mysql-check user haproxy post-80-sha2
Tested against MySQL 8.4.13 (skip_name_resolve=OFF,
max_connect_errors=10) and Cloud SQL for MySQL 8.4.11 with a
caching_sha2_password check user, one check every 2s: with post-80-sha2
the check is L7OK and neither Aborted_connects nor the host cache error
counters move. With post-41 or post-80 every check adds one aborted
connection and one host cache handshake error, and the HAProxy host is
blocked (1129) after 10 checks, i.e. 20 seconds.
The documentation and the mysql-check regtest are updated accordingly.
Signed-off-by: Alex Joseph <alex.lurthu@gmail.com>
Martin Mirchev [Fri, 30 Oct 2026 05:32:30 +0000 (13:32 +0800)]
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").
BUG/MEDIUM: spoe: stop waiting on a dead producer channel
spoe_process_event() interrupts a pending event processing when the frontend
connector reports an error or an early close: there is no point waiting for
an agent reply if the stream is being torn down from the client side.
The check was only made on the frontend connector. With an event bound to
the response channel (on-http-response), a server error reported while the
processing is pending leaves the SPOE context in SENDING_MSGS/ WAITING_ACK
with the processing timeout armed on res->analyse_exp, and nothing can wake
it up anymore.
Test the error on the producer of the channel the event is bound to: scb
in addition to scf for response-side events, the scf checks remaining
valid whatever the direction. For the response side, only SC_FL_ERROR is
relevant: an early server close with "option abortonclose" is a
request-side semantic which is already handled on the request path. The
context then concludes with the INTERRUPT error, which lets the response
analyser proceed and deliver whatever response was already received.
This is the SPOE part of github issue #3559: with a filter engine bound to
on-http-response, an H2 backend answering early and resetting its stream
while the request body is still being uploaded used to leave the stream
spinning in process_stream() until the watchdog killed the worker. The
send-spoe-group action is covered by the http-ana fix, as it is called from
the http-response rule engine.
BUG/MINOR: http-ana: also set FINAL on http-response actions on server error
http_res_get_intercept_rule() stops http-response actions from yielding once
the client side is dead: when the frontend connector reports an error or an
early close, the action is called with ACT_OPT_FINAL, so a yield is refused
and the caller receives an HTTP_RULE_RES_ERROR instead of being left waiting
forever.
The server side was missing: when the backend connector reports an error
while an http-response action (e.g. send-spoe-group) is yielding, the action
kept waiting for a wakeup that would only ever come from the server it just
lost, and the response was stuck in the channel until the client timed out.
Add the missing server-side condition: an SC_FL_ERROR on the backend
connector also sets ACT_OPT_FINAL, so http-response actions get a final
chance to conclude when the server dies. Combined with the previous commit,
the "server answered early and reset its stream while the action was
pending" case from github issue #3559 now lets the action conclude with an
error instead of hanging the response.
This patch should be backported to all supported versions.
BUG/MEDIUM: stream: reset analyse_exp when analysers are stripped on error
When the client or the server connector reports an error or a timeout that
no analyser handled, process_stream() strips all analysers from the
corresponding channel to let the error propagate, except the flt_end one.
When this happens while an analyser was yielding with the channel's
analyse_exp timer armed (e.g. an SPOE event or a Lua action waiting on an
external party), the stripped analysers can no longer manage nor reset this
timer. If it already expired, update_exp_and_leave() refuses to clear it as
long as the AN_*_FLT_END bit is kept for the two-channel filter
synchronisation, so t->expire is set to a past date, the task is requeued in
the past and process_stream() spins in a tight loop until the watchdog kills
the worker.
The request-side retry path (http_ana.c, http_req_retry) already resets
res->analyse_exp right after the same strip; do the same on both error
propagation paths of process_stream() so that an armed analyse timer can
never survive the analysers it was armed for.
This is the first commit of the series fixing github issue #3559. It must be
backported to all stable versions
Willy Tarreau [Thu, 5 Nov 2026 07:54:06 +0000 (08:54 +0100)]
MINOR: http/hardening: turn it to a macro and count matches with COUNT_IF()
Thanks to COUNT_IF() it is quite easy to count the number of times the
condition for a given hardening flag was met, and the number of times it
was enforced. Let's replace the function by a macro for this. In order
to avoid counting multiple times the same match (e.g. many CTL chars in
a header value), the macro takes care of checking both the found and the
rules flags. This way we have at least one found per enforcement, and at
least one per distinct match but not necessarily more if no enforcement
is configured.
Willy Tarreau [Thu, 5 Nov 2026 07:51:27 +0000 (08:51 +0100)]
BUG/MINOR: http/hardening: don't count unregistered hdr/tlr too early
In 3.5-dev11, the ability to remove unregistered headers and trailers
(X-*) was added with commits 4b9b6044b2 ("MINOR: http/hardening: add
"delete-unregistered-header-fields"") and b5fd464f0a ("MINOR:
http/hardening: add "delete-unregistered-trailer-fields""). There is
a small mistake there, which is the the http_harden() call is made
before checking if the situation was met, resulting in the flags being
set for each header/trailer. For now this doesn't have any impact but
it will when reporting is implemented. Let's fix that by only calling
http_harden() after the header syntax was checked. No backport is
needed.
BUG/MINOR: fd: allow fd_write_frag_line() to run before fdtab allocation
With traces enabled on the command line using "-dt ssl:developer:clean",
a trace event is emitted during the configuration check if an SSL bind
line uses "generate-certificates".
check_config_validity() calls ->prepare_bind_conf() for every bind line.
For SSL, this is ssl_sock_prepare_bind_conf(). When "generate-certificates"
is set, that function checks that a default certificate exists, and calls
ssl_sock_choose_sni_ctx() for this. This happens long before
init_pollers() is called, so fdtab is still NULL. fd_write_frag_line()
reads fdtab[fd].state to take the per-fd lock and to test FD_INITIALIZED,
and the process segfaults as soon as the trace is written to stderr.
fd_write_frag_line() has read fdtab since 931d8b79a (v2.1), but no trace
could be emitted that early at that time. The crash became possible with 105c1ca13 (v3.2-dev13), which added traces to ssl_sock_choose_sni_ctx();
ssl_sock_prepare_bind_conf() has called this function from the
configuration check since b80635a7e (v3.0).
At this stage the process is still single-threaded, so the per-fd lock is
not needed. Skip the fdtab accesses when fdtab is not allocated yet, and
call writev() directly.
Owais Lone [Tue, 3 Nov 2026 10:29:01 +0000 (10:29 +0000)]
REGTESTS: protobuf: cover the ungrpc message realignment
Regression test for 3a319e14c ("BUG/MINOR: sample: realign the ungrpc
converter on message boundaries").
grpc.vtc already walks a body of two gRPC messages, but the lookup
succeeds in its first message, so the cursor is left at the message end
and the bug does not show: that file passes both before and after the
fix. Reaching the defect needs a lookup that fails part-way through the
first message, which a length-delimited field whose wire type does not
match the requested one produces.
The body is 23 bytes and holds field 1 twice: length-delimited in the
first message, with a payload that reads as a gRPC header once the
cursor is misaligned, and as a varint 9999 in the second one.
ungrpc(1,int64) must return 9999. Before the fix it returned 1337, a
field of no message of the body.
Checked against 3a319e14c (passes) and its parent 1f114e4c4 (fails,
returning 1337).
CLEANUP: quic: remove the duplicate qc_prep_hdshk_fast_retrans() prototype
This function is declared in quic_retransmit.h, which is included both by
quic_tx.c and quic_rx.c, its only two callers. The second declaration in
quic_tx.h is therefore useless.
CLEANUP: quic: drop the intermediate packet tree pointer in the fast retransmissions
In qc_prep_fast_retrans() and qc_prep_hdshk_fast_retrans(), the local
<pkts> pointer is assigned once and then only used once, as argument of
eb64_first(). Dereference the packet number space tree directly instead,
which removes five lines without changing the behaviour.
CLEANUP: quic: remove the dead encryption level variable in qc_prep_hdshk_fast_retrans()
<qel> was assigned once to qc->iel and never modified afterwards, so
every "qel == iqel" test was always true. Use <iqel> directly for the
packet number space and for the pto_probe increment, test the
anti-amplification condition positively, and unwrap the requeue
condition.
The <start> label has had no goto target since this function stopped
looping over encryption levels, remove it.
CLEANUP: quic: use QUIC_PACKET_PN_MAXLEN instead of the magic 4
Both fast retransmission functions account for the packet number increase
when comparing the size of the packets to be retransmitted with the
anti-amplification limit. They use a plain 4 for this, while
quic_conn-t.h defines QUIC_PACKET_PN_MAXLEN for this exact purpose, as
already used in quic_tx.c for the header protection sample.
Replace the magic value in qc_prep_fast_retrans() and
qc_prep_hdshk_fast_retrans().
CLEANUP: quic: remove a redundant test in the fast retransmission lookups
qc_pkt_with_only_acked_frms() returns 1 for a packet without any frame,
since its loop is not entered and it falls back on the final return 1.
The LIST_ISEMPTY(&pkt->frms) test combined with it is therefore
redundant, both in qc_prep_fast_retrans() and in
qc_prep_hdshk_fast_retrans().
CLEANUP: quic: fix and complete the comments in quic_retransmit.c
Three comments are inaccurate:
- qc_prep_fast_retrans() is documented as taking a <qel> argument, but
it has been replaced by <pktns> for a while;
- the packet lookup loop of qc_prep_fast_retrans() is said to skip the
coalesced packets, while QUIC_FL_TX_PACKET_COALESCED is not tested
there at all. Only the Handshake lookup loop skips them;
- qc_pkt_with_only_acked_frms() is documented as returning 1 for a
packet made only of acknowledged frames, but it also returns 1 for a
packet without any frame. qc_prep_fast_retrans() relies on this to
skip the empty packets.
Three comment blocks are added where the intent is not obvious: the
<tmp> list used by qc_dup_pkt_frms() to prepend the duplicated frames
ahead of the pending ones, the <pkt> variable which is deliberately not
reset before the <start> label so that a second probe datagram is built
from the same frames when no other packet may be retransmitted, and the
Initial packet number space which may still be probed with a PING frame
when the anti-amplification limit allows to send a full-size datagram.
CLEANUP: quic: compute the STREAM frame end offset once in qc_stream_frm_is_acked()
The end offset of a STREAM frame, frm->offset + frm->len, was recomputed
four times in qc_stream_frm_is_acked(): once in the BUG_ON() and three
times in the offset comparisons. Store it in a local frm_end variable, as
already done for the FIN bit flag.
Willy Tarreau [Wed, 4 Nov 2026 22:22:17 +0000 (23:22 +0100)]
[RELEASE] Released version 3.5-dev11
Released version 3.5-dev11 with the following main changes :
- BUILD: sym: Fix build on recent glibc
- BUG/MINOR: checks/log: force implicit UDP checks as TCP for log backend
- CLEANUP: sym: factor out the short object name extraction
- MINOR: sym: use the symbol table to resolve a DSO name
- BUG/MINOR: errors: reset diagnostic buffers after thread cleanup
- BUG/MINOR: sample: fix NULL dereference in the ltime/utime converters
- BUILD: Makefile: extend the capabilities of EXTRA_MAKE
- MINOR: thread: add EXTRAS_LOCK so addons can name their locks
- BUG/MINOR: debug: fix "debug dev fd" crash on unknown address families
- BUILD: Makefile: allow external makefiles to provide build targets
- BUG/MEDIUM: ssl: skip TLS 1.3 suites in the FIPS TLS 1.2 cipher check
- REGTESTS: ssl: skip AWS-LC FIPS rejected crypto in ciphersuites/curves tests
- CI: github: add AWS-LC FIPS testing workflow
- BUILD: github: run FreeBSD CI on every branch
- CI: github: cache VTest binary in FreeBSD workflow
- BUG/MEDIUM: proxy: fix stick table expired entries cleanup on stopping
- BUG/MINOR: http-ana: fix response compression for applets
- DOC: configuration: Clarify wait-for-body doc for the response side
- MINOR: http-act: Add 'no-fail' option on 'wait-for-body' action
- BUG/MEDIUM: leastconn: reset the smallest key when backups take over
- CLEANUP: quic: remove duplicated key phase pointer assignments
- MINOR: quic: use the right trace event in quic_tls_key_update()
- CLEANUP: quic: remove duplicated rx offset initialization in quic_cstream_new()
- CLEANUP: quic: remove the unused tx part of struct quic_cstream
- CLEANUP: quic: remove unused tx crypto size and offset counters
- BUG/MINOR: quic: fix the header protection key length dump with AWS-LC
- CLEANUP: quic: remove the unused key parameter from quic_tls_decrypt()
- CLEANUP: quic: remove the unused count and flags from struct quic_tls_kp
- CLEANUP: quic: remove the stale initial_salt declaration
- CLEANUP: quic: remove duplicated quic_cstream_new() declaration
- CLEANUP: quic: remove the unneeded quic_rx.h include from quic_tls.c
- CLEANUP: quic: update the outdated comment of quic_tls_derive_keys()
- BUG/MINOR: sample: realign the ungrpc converter on message boundaries
- BUG/MEDIUM: compression: fix incompatibility between compression and large-buffers
- CLEANUP: tools: make inetaddr_host_lim*() usable on untrusted inputs
- BUG/MINOR: flt-http-comp: check the block returned by htx_replace_blk_value()
- BUG/MINOR: compression: pass the configured level to slz
- OPTIM: flt-http-comp: only flush the compressor after the last DATA block
- IMPORT: slz: update slz to version 1.4-dev4
- MINOR: compression: pass the content type to the algorithms' init function
- MINOR: flt-http-comp: size the compression reserve using SLZ_MAX_OVERHEAD()
- REGTESTS: compression: check hard to compress contents and nearly full buffers
- BUG/MEDIUM: mux-h1: don't drop pending output on a short content-length
- BUG/MINOR: filters: Reject id-less implicit instances of multi-instance classes
- BUG/MINOR: ssl/ckch: free the new instance when ckch_inst_rebuild() fails
- BUG/MEDIUM: ssl/cli: reset the ongoing transaction when "commit ssl cert" is interrupted
- BUG/MEDIUM: ssl/cli: don't unlock the certificate lock after a failed trylock
- BUG/MINOR: httpclient/lua: fix stack overread when copying the headers
- BUG/MINOR: httpclient/lua: raise an error on too many request headers
- BUG/MINOR: resolvers: validate TXT RDATA before reading
- MINOR: stream: add per-stream memory slots
- BUG/MINOR: haterm: fix the response body phase on short writes
- BUG/MEDIUM: haload: restore request accounting after user timeouts
- BUG/MEDIUM: ssl: disable kTLS on OpenSSL versions affected by a known bug
- DOC: config: add missing empty line before tune.ssl.ktls-unsafe
- BUG/MINOR: ist: make ist_find_range() accept non-zero min boundaries
- MINOR: ist: add ist_find_http_ctl() to match http CTL chars
- MINOR: ist: add ist_find_outside() to find offending characters
- MINOR: http: use ist_find_http_ctl() instead of ist_find_ctl()
- MINOR: h1: indicate the trailers state to h1_parse_msg_tlrs()
- MINOR: http: introduce the notion of http-hardening flags and sections
- MINOR: cfgparse/http: add a simple parser for new section "http-fingerprint"
- MINOR: http: support default hardening names "untrusted" and "trusted"
- MINOR: cfgparse: dump the "http-hardening" keywords with -dKcfg
- MINOR: cli/http: add the "show http-hardening" CLI command
- MINOR: listener: add the "http-hardening" bind keyword
- MINOR: server: add the "http-hardening" server keyword
- MINOR: tools: add is_ip_untrusted() to check for non-local addresses
- DOC: config: document the "http-hardening" section and server/bind options
- MINOR: http: define http_hdng_chk for ambient hardening checks to be run
- MINOR: mux-h1: pass the profile's HTTP hardening checks into the h1c and h1s
- MINOR: mux-h2: pass the profile's HTTP hardening checks into the h2c and h2s
- MINOR: h3: pass the HTTP hardening checks to the h3s and h3c
- MINOR: http: add the "set http-hardening" CLI command
- MINOR: http/hardening: implement hardening "reject-duplicate-host-header"
- MINOR: http/hardening: implement hardening "reject-lowercase-method"
- MINOR: http/hardening: implement hardening "reject-underscore-in-header-name"
- MINOR: http/hardening: implement hardening "reject-underscore-in-trailer-name"
- MINOR: http/harderning: implement "delete-underscore-in-header-name"
- MINOR: http/harderning: implement "delete-underscore-in-trailer-name"
- MINOR: http/hardening: add "delete-unregistered-header-fields"
- MINOR: http/hardening: add "delete-unregistered-trailer-fields"
- MINOR: http/hardening: add "delete-all-trailer-fields"
- MINOR: http/hardening: implement hardening "reject-header-trailer-folding"
- MINOR: h1: rearrange the header value parser to better deal with CTL chars
- MINOR: http: speed up http_header_has_forbidden_char() to make it work alone
- MINOR: http: don't call ist_find_http_ctl() before up http_header_has_forbidden_char()
- MEDIUM: http/hardening: implement hardening "reject-ctl-in-header-trailer"
- MINOR: http/hardening: implement hardening "reject-content-length-with-te"
- MINOR: http/hardening: implement hardening "reject-content-length-leading-zero"
- MINOR: http/hardening: add aliases "all-reject" and "all-delete"
- MEDIUM: http: enable a few important hardening rules on untrusted connections
- REGTESTS: http-messaging: test the http-hardening checks on HTTP/1 headers
- REGTESTS: http-messaging: test the http-hardening checks on HTTP/2 headers
- REGTESTS: http-messaging: test the http-hardening checks on trailers
- REGTESTS: http-messaging: test http-hardening profiles and their CLI
Willy Tarreau [Wed, 4 Nov 2026 17:11:10 +0000 (18:11 +0100)]
REGTESTS: http-messaging: test http-hardening profiles and their CLI
This is an LLM-generated reg-test covering hardening config and CLI.
This covers the declaration and selection of http-hardening profiles, using
a few checks that are easy to observe on HTTP/1 requests:
- the implicit "trusted" profile is the one used over the loopback when
no profile or "auto" is set on the bind line, and both implicit
profiles may be redefined;
- "from" inherits the settings of another profile, and keywords are
applied in order ("all on" followed by an exception);
- "all-reject" and "all-delete" only cover their own family;
- a server inherits its profile from default-server, and "auto" resets
it to "trusted";
- "show http-hardening" lists the profiles (implicit ones being marked
as such) and dumps one profile's options, and "set http-hardening"
changes an option by name or number, with immediate effect on both
bind and server lines, and its error messages;
- the configuration errors: undeclared or forward-referenced profile on
bind and server lines, reserved name "auto", duplicate profile, unknown
or self parent, unknown keyword with a suggestion, invalid value.
Since the "untrusted" profile now enables a few rejections by default, a
redefined "untrusted" profile starts with reject-lowercase-method, so the
test must expect a lower case method to be rejected there. This is now
checked explicitly, as well as the default contents of the implicit
"untrusted" and "trusted" profiles as reported by "show http-hardening",
and their inheritance by "from".
Willy Tarreau [Wed, 4 Nov 2026 17:09:18 +0000 (18:09 +0100)]
REGTESTS: http-messaging: test the http-hardening checks on trailers
This is an LLM-generated reg-test covering hardening checks on trailers.
These are tricky because we don't have anything beyond TCP mode with
content inspection to check them.
Each trailer-related check gets its own profile, applied to a bind line for
request trailers and to a server line for response trailers, with both
HTTP/1 and HTTP/2 on the receiving side. A profile enabling header checks
verifies that these ones do not affect trailers.
vtest cannot parse HTTP/1 trailers, so the request trailers are forwarded in
HTTP/1 to a TCP frontend of the same instance, which waits for the whole
chunked message, then switches to HTTP and replies with the list of fields
it received. Response trailers are delivered to HTTP/2 clients, and the
response body is waited for so that a rejected trailer results in a 502.
Covered: underscore in trailer name (reject and delete), unregistered
trailer fields, deletion of all trailers, folding, CTL and DEL in values.
Willy Tarreau [Wed, 4 Nov 2026 17:08:51 +0000 (18:08 +0100)]
REGTESTS: http-messaging: test the http-hardening checks on HTTP/2 headers
This is an LLM-generated reg-test covering HTTP/2 hardening checks.
This is the HTTP/2 counterpart of http_hardening_h1.vtc for the checks that
apply to HTTP/2 (folding and Transfer-Encoding cannot appear there). A
rejected request must cause a stream error, except for an invalid method
which results in a 400 like other method errors, and a rejected response a
502. It also checks that a Host header accompanying an identical :authority
is not considered as a duplicate Host header.
Willy Tarreau [Wed, 4 Nov 2026 17:07:55 +0000 (18:07 +0100)]
REGTESTS: http-messaging: test the http-hardening checks on HTTP/1 headers
This is an LLM-generated reg-test covering HTTP/1 hardening checks.
Each header-related check gets its own profile, applied to a bind line for
requests and to a server line for responses, and a profile with everything
disabled shows that each offending message is accepted by default. The
frontend replies itself with the method, the header names and the value
of a header it saw, so that deletions are observed after the H1 parser.
Covered: duplicate Host, lower case method, underscore in header name (reject,
delete, and reject having precedence over delete), unregistered header
fields, folding (after the colon, and after a value with SP or HT), CTL and
DEL in values (with HT, SP and obs-text still accepted, and CTL found past
the first words), Content-Length with Transfer-Encoding, and Content-Length
with leading zeroes. Rejected requests must return a 400 and rejected
responses a 502.
Willy Tarreau [Wed, 4 Nov 2026 17:17:48 +0000 (18:17 +0100)]
MEDIUM: http: enable a few important hardening rules on untrusted connections
For connections coming from non-local IP addresses, we now enable these
hardening rules by default, which should not cause any trouble to
existing applications and which are essentially used to build attacks
when seen in public:
This will cause error 400 or 502 depending on the side they are triggered.
In HTTP/1 an error will be captured. We still don't have a capture of what
matched but that's a start.
Willy Tarreau [Wed, 4 Nov 2026 15:21:34 +0000 (16:21 +0100)]
MINOR: http/hardening: add aliases "all-reject" and "all-delete"
These aliases group all reject-* and all delete-* respectively into one
alias, allowing to simplify general cleanups on certain boundaries, or
to fix issues in emergency.
Rejects requests and responses having a Content-Length header field
whose value contains a leading zero before a digit. Some client and
server applications tend to parse this as an octal value giving a
different body size (e.g. "0300" = 192). This can be used in attacks
trying to desynchronize a client or server from the rest of the
communication, and for example for cache poisionning attacks. These may
seldom be seen on legacy applications but not on standard compoennts
found on the internet, so it is generally safe to reject them on public
facing listeners.
Rejects requests and responses having both a Content-Length header
field and a Transfer-Encoding header field. While these are permitted
by HTTP/1.1 (Transfer-Encoding wins), it can cause problems when the
message first passed through an HTTP/1.0 gateway that does not
recognize Transfer-Encoding, and can desynchronize this gateway from
the rest of the communication. These are still seldom seen in legacy
client or server applications, sometimes in relation with compression
and/or caching. While HAProxy properly normalizes them, it is generally
safe to reject them on public facing listeners.
Rejects requests and responses which contain control characters in
their header or trailer field values (codes 0-8,10-31,127). These ones
are forbidden but have long experienced a certain tolerance among
implementations, which has encouraged some rare applications to use
them as record delimiters for example. No standard component produces
them on the public internet, except attacks. By default, HAProxy only
blocks the strictly required ones (0,10,13) and lets other ones pass,
which can make certain backend applications trigger connection errors.
As such, it is both safe and recommended to block them on public facing
listeners. See also the has_ctl() converter used for detecting them.
Willy Tarreau [Wed, 4 Nov 2026 10:59:00 +0000 (11:59 +0100)]
MINOR: http: don't call ist_find_http_ctl() before up http_header_has_forbidden_char()
Now that the function was sped up to skip non-offending bytes, we don't
need ist_find_ctl() anymore. And since it would block us from finding
certain chars, let's now get rid of it.
Willy Tarreau [Wed, 4 Nov 2026 09:58:53 +0000 (10:58 +0100)]
MINOR: http: speed up http_header_has_forbidden_char() to make it work alone
The function reads one char at a time and is systematically hidden behind
a first call to ist_find_http_ctl() to speed up the lookup of control chars.
Unfortunately, this construct doesn't permit to find other control chars
such as 0x7F. Better make http_header_has_forbidden_char() self-sustaining,
but for this we need to implement the speedup there. For this, we start
with a loop using is_char8_outside(0x20,0x7e) that allows to fly over most
non-offending bytes (SP, VCHAR). Only HT (9) and obs-text (0x80..0xFF) will
exit early, which is not a problem given that they are extremely rare in
header values. After that we continue the loop to search for offending
bytes starting from the block of 4 or 8 that triggered.
Willy Tarreau [Wed, 4 Nov 2026 07:52:44 +0000 (08:52 +0100)]
MINOR: h1: rearrange the header value parser to better deal with CTL chars
The header value parser only made a special case of NUL, CR and LF in
its core loop, which doesn't ease detection of other CTL chars. Let's
first slightly rearrange the loop to make the special case of chars below
0x20 first. The checks were only moved inside a new "if" block but nothing
was changed. Use diff -b to better see.
Rejects requests and responses which contain folded header fields or
trailer fields. Folding consists in having a single field span multiple
lines. This practice is obsoleted by modern HTTP standards, absolutely
not seen anymore in valid messages over the internet but might still be
present in a few antique internal services dealing with large cookies.
On untrusted networks, they are always associated with attacks trying
to inject headers into non-compliant components, and should be rejected
as a sign of break-in attempt. Note that HAProxy does not have problems
with them and systematically unfolds these messages before forwarding
them.
Deletes from requests and responses any trailer field, regardless of
its content or syntax. This is done after syntactic checks though. The
vast majority of public facing applications do not use trailers because
they are not reliable (intermediaries are allowed to drop or fold them)
and their presence on public networks is generally associated with
attack attempts. It is generally safe to drop them by default on public
facing listeners, and even on servers meant to deliver public contents,
in order to avoid any information leakage. Note that gRPC services use
them.
This is the same as for the headers, but this time for the trailers. Better
keep them separated as trailers are used even worse than headers these days.
Similarly as for the underscore, unwanted trailers are dropped early for
h1.
Deletes from requests and responses any header field whose name starts
with 'x-' (upper or lower case). These header fields are not registered
with IANA and are not used across the internet by standard components.
They may appear in untrusted requests in attacks trying to exploit some
backend framework weaknesses however, as well as in old browsers that
were trying to work around known server bugs (e.G. "X-Pad"). While it
would be risky to block such requests, it is safe and even recommended
to delete such headers from inbound internet requests. It is preferable
not to do it on responses however when applications uses such header
fields to report information to HAProxy.
This is the same as for the headers, but this time for the trailers. Better
keep them separated as trailers are used even worse than headers these days.
Note that in h1 we delete them when we detect them since we already have
the code for this, no need to index them and delete them later.
Deletes from requests and responses any header field whose name
contains an underscore character ('_' or \x5C). While this character is
perfectly permitted, some application frameworks which create automatic
variables already turn the dash ('-') into an underscore in variable
names, leading to an ambiguity with the underscore which can result in
the same variable name. This could be abused to bypass some
access-control policy for example. No standard HTTP header uses this,
so it is recommended to enable on untrusted listeners. Note that this
applies only after "reject-underscore-in-header-name" was accepted.
This is the same as for the headers, but this time for the trailers. Better
keep them separated as trailers are used even worse than headers these days.
Rejects requests and responses in which one header field name contains
an underscore character ('_' or \x5C). This character is perfectly
permitted, but some application frameworks which create automatic
variables tend to already turn the dash ('-') into an underscore in
variable names, leading to an ambiguity with the underscore which can
result in the same variable name. This could be abused to bypass some
access-control policy for example. No standard HTTP header uses this,
so it is safe to enable on untrusted listeners.
Rejects requests whose method contains a lower-case character. No
standard HTTP method uses lower-case characters and these ones are
essentially used in desynchronization attack scenarios, by trying to
exploit different case matching schemes along the chain. This is
recommended on untrusted listeners. This is implemented directly in the
h1 parser, as well as in http_method_has_forbidden_char() for h2 and
h3. H2 now uses this function instead of rolling its own loop.
Rejects requests containing more than one "Host" header field, even if
valid and identical. This is recommended on untrusted listeners. Note that
even if not rejected, duplicate host header fields are folded into a
single one. This was done for h1/h2/h3 which all had the explicit
check to perform deduplication.
In h2 and h3 we also had to make sure not to count host+authority as a
duplicate, so that requires a host counter as a local var, but that's
cheap.
Note that h3 currently doesn't check that duplicate headers are strictly
equal, only the first one is kept.
Willy Tarreau [Tue, 27 Oct 2026 16:43:44 +0000 (17:43 +0100)]
MINOR: http: add the "set http-hardening" CLI command
"set http-hardening <name> <option> {on|off}" enables or disables an option
of a profile at runtime, including the "all" keyword to change everything
at once, e.g. in an emergency. Options may also be specified by the bit
number that appears in -dKcfg or in "show http-hardening". The option is
looked up like in the section parser so that a suggestion is made on typos.
The profile is only read by the other threads, so it is updated while the
threads are isolated, which avoids any locking on the fast path. The
command is restricted to the admin level.
Willy Tarreau [Fri, 30 Oct 2026 10:29:45 +0000 (11:29 +0100)]
MINOR: h3: pass the HTTP hardening checks to the h3s and h3c
This adds an http_hdng_chk into h3s. In h3_attach() we preset
it to the h3c's profile's mask, which is found during h3c initialization
from the connection side's target profile (from server or bind_conf). If
no hardening profile is found ("auto" or not set), then we check the
source address of the connection. If it's a non-local IPv4/v6, then
we use the untrusted profile, otherwise we fall back to the trusted one.
Contrary to h2, h3s is allocated before parsing (like h1s), so we can
place all pre-initialization in h3_attach() without having to preset any
fields on h3s before parsing: in h3_rcv_buf(), we just have to preset
and collect http_hdng_chk, like for h1 again.
Willy Tarreau [Thu, 29 Oct 2026 16:14:35 +0000 (17:14 +0100)]
MINOR: mux-h2: pass the profile's HTTP hardening checks into the h2c and h2s
This adds an http_hardening profile pointer to h2c and an
http_hdng_chk into h2s. In h2_init() we preset the global hardening
profile depending on the side (note that the side might be reversed with
rhttp, but the pointer still points to a bind_conf or server so it remains
valid), and if no profile is found ("auto" or anything else) then we check
the source address to see if it's a non-local IPv4/v6, in which case we
use the untrusted profile, otherwise we fall back to the trusted one.
In h2s_new() we preset the h2s's hardening rules to the mask defined by
the profile pointed to by the h2c. This will be used as-is by the backend,
but in h2c_frt_handle_headers(), we have to preset the global hardening
profile to the h2c's mask before proceeding. If the stream is accepted
and created, we then copy what we collected to h2s for later consumption
(not implemented yet).
Willy Tarreau [Thu, 29 Oct 2026 14:25:50 +0000 (15:25 +0100)]
MINOR: mux-h1: pass the profile's HTTP hardening checks into the h1c and h1s
This adds the pointer to the chosen http_hardening profile to h1c, and
sets an http_hdng_chk on h1s. In h1c_init() we simply use the
connection's target to get the hardening profile. It is either a server,
or a listener, in which case we access its bind_conf's profile. If none
is found, we check if we have an untruted source address and set it to
untrusted, otherwise we fall back to trusted. Then we can preset the
http_hdng_chk rules in h1s from the mask of the profile pointed
to by h1c.
For now the returned information, if set, are not exploited.
Willy Tarreau [Thu, 29 Oct 2026 14:14:45 +0000 (15:14 +0100)]
MINOR: http: define http_hdng_chk for ambient hardening checks to be run
In order to enforce HTTP hardening checks, we have to instrument functions
at many different layers of the HTTP parsing code. First tests show that
we might need to add extra parameters to numerous functions, just to
forward requirements and results.
A simpler, less elegant but much more efficient approach consists sharing
the parsing checks globally (local to the thread) so that anywhere in the
HTTP stack it is possible to know what checks are enforced. It is not
different from the global acceptable violations after all, except that
this has to be preset, consumed and cleared by the caller. The benefit
is that it will then be possible to define the checks before parsing,
and let any function anywhere in the chain participate to the operation,
including for reporting possible anomalies.
The http_harden(flag) function does the repetitive part of the check and
returning what final action was performed and should be easy to deploy.
Willy Tarreau [Mon, 26 Oct 2026 18:18:19 +0000 (19:18 +0100)]
DOC: config: document the "http-hardening" section and server/bind options
Add section 12.10 describing the "http-hardening" section, the implicit
"untrusted" and "trusted" profiles and the "all" keyword, and document the
"http-hardening" option of "bind" and "server" lines. For now we do'nt
have other examples aside "all" in the section but the goal is that its
usage now starts with a few options.
Also take this opportunity for defining the meaning of "trusted" and
"untrusted" in this context.
Willy Tarreau [Tue, 3 Nov 2026 13:57:53 +0000 (14:57 +0100)]
MINOR: tools: add is_ip_untrusted() to check for non-local addresses
The function will return true for non-local ipv4/ipv6 addresses only.
Some are between the two (benchmark, doc, examples, cgn etc) and are
intentionally reported as non-local hence untrusted as well, the
rationale being that they're normally not deployed for production so
they shouldn't be taken for trusted local addresses. This will be
used in future commits.
Willy Tarreau [Mon, 26 Oct 2026 17:47:28 +0000 (18:47 +0100)]
MINOR: server: add the "http-hardening" server keyword
The new "http-hardening" server keyword sets the HTTP hardening profile
that applies to the responses coming from the server and to the requests
sent to it. The server now holds a pointer to the profile, which may be
NULL if unset or explicitly set to "auto". It is inherited from
default-server, server templates and "from" like any other setting, and
may be set at runtime with "add server". The profile must have been
declared before.
Willy Tarreau [Mon, 26 Oct 2026 17:39:23 +0000 (18:39 +0100)]
MINOR: listener: add the "http-hardening" bind keyword
The new "http-hardening" bind keyword sets the HTTP hardening profile that
applies to the connections accepted by the bind line. The bind_conf now
keeps a pointer to the profile set by the keyword, or NULL if not set or
if explicitly set to "auto".
Willy Tarreau [Mon, 26 Oct 2026 17:21:33 +0000 (18:21 +0100)]
MINOR: cli/http: add the "show http-hardening" CLI command
Without argument, "show http-hardening" lists all known profiles, including
the implicit "untrusted" and "trusted" ones when they were not declared (they
are then marked "(implicit)"). With a profile name, it shows each known
single-bit hardening option of this profile, along with its number (bit
position plus 1), direction among "req", "res", "-" (for bidir) and the
status ("on" or "off"), so that it is easy to know what is enabled.
Keywords covering a set of options such as "all" are not shown.
Willy Tarreau [Mon, 26 Oct 2026 17:08:40 +0000 (18:08 +0100)]
MINOR: cfgparse: dump the "http-hardening" keywords with -dKcfg
The keywords of the "http-hardening" section are not registered via the
cfg_kw_list mechanism but through their own table in http.c, so they were
missing from the output of "-dKcfg". Let's have cfg_dump_registered_keywords()
report them by calling dump_http_hardening_kws() once the other sections
were dumped. The function also reports the option numbers for pure options
(those which are not composite).
Willy Tarreau [Mon, 26 Oct 2026 19:41:09 +0000 (20:41 +0100)]
MINOR: http: support default hardening names "untrusted" and "trusted"
And also reserve "auto" for future use. Both "untrusted" and "trusted"
have a global storage and are available all the time. They're just not
registered in the tree by default so that we'll find them once missed
in the tree lookup. If a section with these names is created, it
inherits their settings. This will allow new default settings to
change over time, with minimal maintenance effort for the users.
Willy Tarreau [Mon, 26 Oct 2026 16:43:22 +0000 (17:43 +0100)]
MINOR: cfgparse/http: add a simple parser for new section "http-fingerprint"
Just like the resolvers parser, this is a whole section parser. It handles
both the http-fingerprint keyword that creates the section, and all
registered hardening options supported there. The format is:
A section may only be declared once, and the origin must already exist if
mentioned (like with defaults). Like with http-violations, keywords are
resolved using fuzzy matching so that an approaching name will suggest
the correct one.
For now, no keywords beyond "all" are declared, and nothing is done with
these sections.
Willy Tarreau [Mon, 26 Oct 2026 16:07:13 +0000 (17:07 +0100)]
MINOR: http: introduce the notion of http-hardening flags and sections
The goal here will be to declare http-hardening sections which enumerate
controls that can be relaxed or enforced. The control names will be stored
in a table just like the http-violations, associated with a mask. They
will possibly permit to have names for sets that cover multiple bits. For
now only 32 bits are planned, covering both front/back and req/resp. This
should be sufficient for a start and we'll see once the limit is reached
what's the best way to proceed.
One difficulty here is to reason in terms of what is not permitted.
So the way to think about it is to associate each verb to an action
to be performed, such as reject-xxx, delete-xxx, adjust-xxx, block-xxx.
Willy Tarreau [Tue, 3 Nov 2026 17:49:11 +0000 (18:49 +0100)]
MINOR: h1: indicate the trailers state to h1_parse_msg_tlrs()
In order to parse H1 trailers, h1_parse_msg_tlrs() builds a local dummy
h1m that sets H1_MF_HDRS_ONLY and calls h1_headers_to_hdr_list() which
directly jumps to plain headers parsing. However, this latter function
has no way to adjust its error processing on header or trailer because
the information regarding the fact that headers or trailers are being
parsed isn't present. Currently this is not a problem but it will become
in order to refine error processing to have separate rules for headers
and trailers.
This patch sets the dummy h1m's state to H1_MSG_TRAILERS so that the
called function can safely rely on it. All other callers pass a real
message with a valid state, so it will be possible to check h1m->state
during error processing to figure if the function is in the process of
parsing trailers or headers.
Willy Tarreau [Wed, 4 Nov 2026 16:37:11 +0000 (17:37 +0100)]
MINOR: http: use ist_find_http_ctl() instead of ist_find_ctl()
At a few places we were using ist_find_ctl() as an accelerator to locate
possibly forbidden CTL chars, but that one doesn't detect the DEL char,
so now that we have the new ist_find_http_ctl(), let's use it instead.
This is not tagged as a bug because nothing at this point is made with
this char so this doesn't make a difference at this point.
Willy Tarreau [Wed, 4 Nov 2026 10:28:25 +0000 (11:28 +0100)]
MINOR: ist: add ist_find_outside() to find offending characters
For some syntax checks it's interesting to find offending characters
that are outside an interval. Unfortunately for now we only limit
outselves to searching control chars < 0x20 but that's not always
sufficient. Let's just implement a fast scan for characters outside a
range (min and max may not be more than 0x7f apart though). Just like
ist_find_range(), it reads one word at a time and finishes per byte
when found or on false positives. It's optimized for non-matching
inputs.
Willy Tarreau [Wed, 4 Nov 2026 16:29:02 +0000 (17:29 +0100)]
MINOR: ist: add ist_find_http_ctl() to match http CTL chars
Some tests in the HTTP code involve ist_find_ctl() hoping to find any
HTTP CTL char, but CTL for ist is 0x00..0x1F only (and documented as
such so we won't change it). For HTTP, this also includes 0x7F which
these tests are missing.
It's not realistic to do a second full scan on those data for something
that never happens. Better introduce a new function which scans for
0x00..0x1F,0x7F, that's much more efficient. It's constructed exactly
like the existing one with a extra check for 0x7F by adding 1 to
generate a carry in this case as well. It's expected to cost roughly
2 extra cycles per word, which is OK.
Willy Tarreau [Wed, 4 Nov 2026 10:09:24 +0000 (11:09 +0100)]
BUG/MINOR: ist: make ist_find_range() accept non-zero min boundaries
The way ist_find_range() is currently constructed can make it miss the
lower bound. For example, checking "abcdefgh1jklmnop" against min='0'
and max='9' misses the '1' in the middle because the check only sees
bytes that are below (max-min), not those between min and max (the
min value is not subtracted before the comparison). It's OK in the
byte-at-a-time check however.
Let's just simplify the operation to make it work more like the
is_char8_below_opt() and is_char8_above_opt() functions. The idea
is to check (x-min) and (max-x) and combine them to check for the
carry. However there's still the case of testing that all bytes are
outside the interval (we don't want to miss one). The trick here
is to or the left term with the carry so that we keep the carry for
bytes outside and it only disappears when all bytes are ouside the
range. That way the loop stops when there's one carry, indicating
at least one byte within range, or one which had bit 7 previously
turned on (which was already mentioned as the exception), in which
case we finish one byte at a time. The only restriction is that
min <= max <= 0x7f, but that's already how the function was meant
to be used.
This can be backported, though it will not fix any bug because at
the moment, all callers pass 0 to the min value and are not affected.
BUG/MEDIUM: ssl: disable kTLS on OpenSSL versions affected by a known bug
With vanilla OpenSSL, a blocked kTLS record keeps referencing the caller's
buffer and the library retries from it, even though
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is set; the H2 fast-forward reuses that
buffer, so streams get corrupted. Upstream fixed this in 3.4.6 and 3.5.7, so
disable kTLS on older versions and report it, with "tune.ssl.ktls-unsafe" to
opt in anyway.
BUG/MEDIUM: haload: restore request accounting after user timeouts
The user path drops expired streams without releasing their active-stream
count or their tot_rconns slot, unlike the stream-error path. Once -r is
reached with fewer completions, running_tasks never falls back to zero and
haload hangs until it is killed.
Release both there, matching the error path; the timeout is still counted
as an error. This was introduced with haload in 3.5-dev, no backport needed.
BUG/MINOR: haterm: fix the response body phase on short writes
hstream_add_ff_data() computed the offset of the next body byte as
(req_size - len) % modulo, where <len> is the length offered by the mux
for that call. A short write or a partially filled output buffer makes it
smaller than the number of bytes still to send, so the offset no longer
matched what had already been emitted: the repeating pattern jumped and
the peer received a corrupted body, which happened as soon as a proxy read
the response with its own buffer boundaries. Use hs->to_write instead.
Introduced with ecf36f2ca ("MEDIUM: haterm: Add support for 0-copy data
forwading and option to disable it"), present only in the development
branch, so no backport is needed.
Add stream_reserve_memslot() to reserve pointer-aligned per-stream storage
during single-threaded initialization, before streams exist. Access the
reserved slots with stream_get_memslot().
Reserved storage is zeroed at stream creation, preserved for the stream's
lifetime and released on destruction.
maxml [Wed, 4 Nov 2026 10:16:46 +0000 (10:16 +0000)]
BUG/MINOR: resolvers: validate TXT RDATA before reading
maxml reported on the public HAProxy mailing list that the DNS response
validator reads the first TXT character-string length byte before checking
that the record contains any RDATA.
When a TXT answer has RDLENGTH zero and ends exactly at bufend, the generic
RDATA bounds check accepts the empty range. The TXT parser then reads
reader[0] with reader == bufend, before rejecting the record. Short-circuit
evaluation of the subsequent condition does not protect this earlier read.
The report includes a minimized 31-byte DNS response which triggers an
AddressSanitizer one-byte heap-buffer-overflow with an exactly sized
libFuzzer input. All 13 original AFL++ crash inputs reach the same read.
The production caller uses a DNS_MAX_MSG_SIZE + 1 stack buffer and parses
at most DNS_MAX_MSG_SIZE bytes, so this read remains within the allocation.
The loaded value is discarded when the zero-length check fails. No
production crash or information disclosure was demonstrated.
Check that RDATA contains at least one byte before reading its first
character-string length. A valid empty TXT string remains accepted: its
RDLENGTH is one and its character-string length byte is zero.
This was introduced by e418e828aa ("MINOR: resolvers: basic TXT record
implementation"). It should be backported to 3.4. Older stable branches
are not affected.
Martin Mirchev [Fri, 30 Oct 2026 05:49:13 +0000 (13:49 +0800)]
BUG/MINOR: httpclient/lua: raise an error on too many request headers
When a Lua httpclient request has more header values than
tune.http.maxhdr, hlua_httpclient_table_to_hdrs() pops the inner
lua_next() key and value and returns NULL. But the key and value of the
outer lua_next() loop on the headers table stay on the stack, so the
caller's lua_next() on the argument table gets the wrong key and the
request fails with a confusing error:
runtime error: invalid key to 'next' from [C]: in method 'get'
The header values already duplicated with istdup() are also leaked.
Let's free them and raise an explicit Lua error instead, as the caller
does for the other invalid arguments:
runtime error: too many headers (tune.http.maxhdr is 101)
The function is now marked __LJMP and its call wrapped in MAY_LJMP().
This should be backported where 2db801c63 ("BUG/MINOR: hlua: fix stack
overflow in httpclient headers conversion") was backported. Before 3.5
the function is in src/hlua.c.
Martin Mirchev [Fri, 30 Oct 2026 05:45:12 +0000 (13:45 +0800)]
BUG/MINOR: httpclient/lua: fix stack overread when copying the headers
hlua_httpclient_table_to_hdrs() collects the header values of a Lua
httpclient request into a stack array of tune.http.maxhdr entries, then
copies hdr_num + 1 entries into the allocated result, although the
array never contains a terminator: the result is allocated with calloc()
and its last entry is set to IST_NULL right after the copy.
Since commit 2db801c63 ("BUG/MINOR: hlua: fix stack overflow in
httpclient headers conversion") hdr_num may reach tune.http.maxhdr, and
the copy then reads one entry past the end of the array. E.g. with the
default value:
local v = {}
for i = 1, 101 do v[i] = "v" .. i end
hc:get{ url = "http://127.0.0.1:8080/", headers = { ["x-many"] = v } }
ASan reports a dynamic-stack-buffer-overflow READ of size 3264 (102
entries) in hlua_httpclient_table_to_hdrs(). With fewer values, the
extra entry is uninitialized stack, which is then overwritten.
Let's only copy the hdr_num collected entries.
This should be backported to all stable versions with the Lua
httpclient (2.5 and above), on top of 2db801c63 where it was
backported. Before 3.5 the function is in src/hlua.c.
BUG/MEDIUM: ssl/cli: don't unlock the certificate lock after a failed trylock
When ckch_store_load_payload() fails to acquire ckch_lock, it jumps to
the common cleanup path, which unconditionally releases the lock. This
unlocks another operation's lock and returns success because errcode is
still zero. When the actual owner subsequently unlocks, the lock state
underflows and later certificate commands remain blocked.
Return ERR_ALERT | ERR_FATAL immediately on failed lock acquisition. No
temporary resources have been allocated at this point, so cleanup is
unnecessary.
This regression was introduced in 3.4-dev11 by commit efe6c97488, which
extracted ckch_store_load_payload() from the CLI parser. This fix should
be backported to 3.4 only. Versions 3.3 and older return directly on
failed lock acquisition and are not affected.
Chris Staite [Mon, 2 Nov 2026 10:00:44 +0000 (10:00 +0000)]
BUG/MEDIUM: ssl/cli: reset the ongoing transaction when "commit ssl cert" is interrupted
"commit ssl cert" builds the new instances in several calls of its I/O
handler: it yields every 10 instances, and whenever the output buffer is
full. If the applet is released in between, for example because the CLI
client closed the connection, cli_release_commit_cert() frees the new
ckch_store of the transaction, but ckchs_transaction.new_ckchs,
ckchs_transaction.old_ckchs and ckchs_transaction.path still reference
it. The next "set ssl cert", "commit ssl cert", "abort ssl cert" or
"show ssl cert" on this certificate then uses the freed store, which may
crash the process or corrupt memory.
Only an admin of the CLI can trigger it, but it doesn't take much: a
certificate used by more than 10 instances (e.g. in a crt-list loaded by
several bind lines) and a client which doesn't wait for the end of the
commit are enough.
The release function is only called with a new store still attached when
the transaction is being abandoned, the success path having consumed it
in CERT_ST_INSERT, so the transaction is reset there. On the error path,
CERT_ST_FIN already reset it and doing it again is harmless.
"commit ssl ca-file" and "commit ssl crl-file" seem to follow the same
pattern in cli_release_commit_cafile() and cli_release_commit_crlfile(),
this will be addressed separately.
This should be backported to all stable branches. Before 3.4, the release
function is the same but frees ctx->new_ckchs itself instead of calling
ckch_store_update_cleanup().
Chris Staite [Mon, 2 Nov 2026 10:00:56 +0000 (10:00 +0000)]
BUG/MINOR: ssl/ckch: free the new instance when ckch_inst_rebuild() fails
ckch_inst_rebuild() creates the new ckch_inst of a "commit ssl cert" with
its sni_ctx and SSL_CTX, then prepares the SSL_CTX of the server or of
the bind line. When this preparation fails it returns an error to
ckch_store_rebuild_instances() which only returns -1, so the new instance,
which is not linked anywhere yet, is leaked with its SSL_CTXs, and with
everything they reference (the certificate, the chain, the key, the CA
file links).
The impact is a memory leak for each failed commit, which requires an
admin to commit a certificate that doesn't fit its bind or server lines.
Free the instance with ckch_inst_free() and reset the pointer on these
two error paths. The instance is not in any tree nor list at this point,
which ckch_inst_free() handles. The first error path, after
ckch_inst_new_load_store() or ckch_inst_new_load_srv_store(), is already
fine since these functions never leave an instance behind on failure.
This should be backported to all stable branches, the function exists
since 2.5.
BUG/MINOR: filters: Reject id-less implicit instances of multi-instance classes
An id is mandatory when a filter class allows several instances per proxy,
and this was verified when a filter-config directive was parsed. But it was
not when an implicit instance was created by flt_add_implicit_instance()
called from another directive: for such a class, an instance without id
could not be distinguished from a second one.
Like for an explicit declaration, the implicit declaration is now reported
and ignored if no id is provided for such a class. All the in-tree
directives creating implicit instances of a multi-instance class (the
cache-store rules) already pass the cache name as id, so nothing changes for
existing configurations.
The filters documentation was updated accordingly.
Olivier Houchard [Fri, 30 Oct 2026 15:04:33 +0000 (16:04 +0100)]
BUG/MEDIUM: mux-h1: don't drop pending output on a short content-length
When a fixed-length message is truncated and output data is still pending
in the H1 output buffer, h1s_destroy() flags the connection in error
because we got less data than expected, and so h1_send() will refuse to
send the remaining data, and not everything will be transmitted.
Now, when a truncated message is detected while output data is still
pending, H1C_F_ERROR is not set immediately, we just avoid adding it to
the idle pool, and the connection will be closed later when everything
has been sent.
To release the connection once the output buffer is drained, the
condition in h1_process() that only matched delayed silent shutdowns is
generalized to any connection in CLOSING state with an empty output
buffer. This covers both silent shutdowns and the short content-length
case. The connection is still not released while a stream remains
attached, since h1_close() keeps it in CLOSING until the stream is
detached.