]> git.kaiwu.me - haproxy.git/log
haproxy.git
2 weeks agoIMPORT: slz: update slz to version 1.4-dev3
Willy Tarreau [Wed, 28 Oct 2026 14:18:42 +0000 (15:18 +0100)]
IMPORT: slz: update slz to version 1.4-dev3

This refreshes the slz library to 1.4-dev3, bringing a number of
improvements:
  - x1.4 to x20 compression speed depending on input contents, due
    to better handling of non-compressible data (x20, think CSS/HTML
    with embedded images) and many other optimizations giving at
    least x1.4 at the base.

  - slightly higher default compressed ratio (46.34% vs 46.97%) and
    top compressed ratio (43.03% vs 45.33%) on silesia corpus, and
    23.36% vs 26.22% on linux kernel, which now compresses at 1 GB/s
    vs 600 MB/s before on a P-core of a Core Ultra 265k.

  - x2 to x3 decompression speed (mostly 2.5) depending on input
    stream, best gains on gzip input which was previously much slower
    since not as much in the target.

  - better support for crc32 on ARM (properly ignore older archs)

  - runtime detection of CPU support for PCLMULQDQ on x86_64

This version now passes all lzbench tests so it was the right moment
to update it. More updates to come soon (taking content-type into
account to adjust optimizations) still being worked on, but nothing
critical.

In order to ease updates, just like for xz or ceb, stubs were added
in src/ that directly load the original files from include/import.

2 weeks agoDOC: config: add missing entry for health checks
Willy Tarreau [Mon, 26 Oct 2026 18:05:03 +0000 (19:05 +0100)]
DOC: config: add missing entry for health checks

In 3.4, a new health checks section (12.9) was added by commit 6fbccae1ab
("MEDIUM: tcpcheck/server: Add healthcheck server keyword") but it didn't
get a global entry. Let's do it.

This can be backported to 3.4.

2 weeks agoBUG/MEDIUM: mux-spop: Don't include DALLOC/DFULL flags in SPOP_CF_DEM_BLOCK_ANY
Adam Szymanski [Wed, 28 Oct 2026 10:19:59 +0000 (11:19 +0100)]
BUG/MEDIUM: mux-spop: Don't include DALLOC/DFULL flags in SPOP_CF_DEM_BLOCK_ANY

SPOP_CF_DEM_BLOCK_ANY is documented as "aggregate of the demux flags above
except DALLOC/DFULL", like H2_CF_DEM_BLOCK_ANY and FCGI_CF_DEM_BLOCK_ANY, but
its value 0x7E0 also covers SPOP_CF_DEM_DALLOC (0x20) and SPOP_CF_DEM_DFULL
(0x40).

When a single recv() fills the demux buffer, spop_recv() sets
SPOP_CF_DEM_DFULL. spop_process() then skips spop_process_demux() because
the flag is part of SPOP_CF_DEM_BLOCK_ANY, and the only place clearing
SPOP_CF_DEM_DFULL is inside that skipped block. spop_recv_allowed() also
refuses to read. The connection stays stuck with a full dbuf forever: the
peer sees a zero TCP window, ACK frames are never parsed, every stream
attached to the connection times out, and new streams keep being attached
to it because it is still considered available.

This happens with pipelining when enough ACK frames are queued at once to
fill the 16kB demux buffer in one read (e.g. ACKs carrying ~1-3kB
variables), so "no option pipelining" in the spoe-agent section hides it.

The value must be 0x780 (MROOM|SALLOC|SFULL|TOOMANY).

Reproduced on HAProxy 3.4.7 with nbthread 4, pipelining enabled,
50 concurrent HTTP keep-alive clients, and a Go SPOA agent returning
ACK frames carrying approximately 3kB of variables. In three 10-second
test runs, the unpatched version left 3-5 connections stuck with
DEM_DFULL and produced 400-520 SPOE timeouts per run. With the fix,
no stuck connections or SPOE timeouts were observed.

This must be backported as far as 3.1.

Co-authored-by: ITGG <kontakt@itgg.eu>
2 weeks agoBUG/MINOR: server: free the sample expression when it is not usable on a server
Martin Mirchev [Wed, 28 Oct 2026 14:08:10 +0000 (22:08 +0800)]
BUG/MINOR: server: free the sample expression when it is not usable on a server

parse_srv_expr() parses the "sni" and "pool-conn-name" expressions of a
server. When the fetch cannot be used on a server connection (e.g. a
fetch that needs a response such as "res.hdr(host)"), it reports an error
and returns without releasing the expression it has just allocated.

At startup this is harmless since the process exits on the error, but the
same function is used by the "add server" CLI command. Each rejected
"add server ... sni <expr>" leaks the expression (about 200 bytes per
command, as reported by valgrind), so a script or an operator repeatedly
issuing such commands slowly increases the process' memory usage. Only
admin-level CLI users can trigger it.

This fixes GitHub issue #3547.

This must be backported to all stable versions starting with 3.0, where
the code was introduced by commit 91001422b4 ("MINOR: server: generalize
sni expr parsing").

2 weeks agoDOC: configuration: clarify SCT support by TLS library and version
William Lallemand [Tue, 27 Oct 2026 15:13:40 +0000 (15:13 +0000)]
DOC: configuration: clarify SCT support by TLS library and version

List the TLS versions and libraries supporting SCT delivery, and indicate
that QUIC is unsupported.

This should be backported with the SCTL fixes.

2 weeks agoBUG/MINOR: ssl: support SCTL in TLS 1.3 Certificate messages
William Lallemand [Tue, 27 Oct 2026 15:13:40 +0000 (15:13 +0000)]
BUG/MINOR: ssl: support SCTL in TLS 1.3 Certificate messages

SSL_CTX_add_server_custom_ext() only sends SCT lists in TLS 1.2 and earlier
ServerHello messages. Use SSL_CTX_add_custom_ext() on vanilla OpenSSL 1.1.1
and later to support TLS 1.3 Certificate messages as well. Retain the legacy
API for other libraries and older OpenSSL versions.

Send SCTs only on the leaf Certificate entry when requested by the client
and present for the selected certificate.

Keep SCT delivery disabled for QUIC: valgrind reports a use-after-free in
native OpenSSL 3.5.5 when SSL_set_SSL_CTX() merges transport parameters with
custom extensions. Also skip loading SCT lists into QUIC contexts,
including on AWS-LC.

Verified exact SCT payloads in TLS 1.2 and TLS 1.3 with a two-entry chain,
SNI isolation, omission without a client request, QUIC omission, and clean
config-check/deinit under valgrind.

This must be backported to all supported versions.

2 weeks agoBUG/MINOR: ssl: fix a double-free of the .sctl buffer with several contexts
William Lallemand [Tue, 27 Oct 2026 14:32:47 +0000 (14:32 +0000)]
BUG/MINOR: ssl: fix a double-free of the .sctl buffer with several contexts

ssl_sock_load_sctl() stores the ckch's .sctl buffer in SSL_CTX ex_data,
whose free callback releases it when the context is destroyed. The ckch
also frees this buffer, causing a double-free during deinit or "haproxy -c",
even with a single bind and certificate. Sharing the certificate across
contexts adds further frees of the same buffer.

Give each context its own copy, leaving the original owned by the ckch.
AWS-LC already copies the list through its native API.

This bug was introduced in 2.1 by commit 0dfae6c315 ("MINOR: ssl: load sctl
from buf OR from a file") and must be backported to all versions since then.

2 weeks agoBUG/MEDIUM: ssl: support SCTL with AWS-LC
William Lallemand [Thu, 22 Oct 2026 15:12:18 +0000 (15:12 +0000)]
BUG/MEDIUM: ssl: support SCTL with AWS-LC

AWS-LC owns extension 18 and rejects server custom extension registration.
The previous commit skips that registration, leaving .sctl files ignored.

Use SSL_CTX_set_signed_cert_timestamp_list(), available in all AWS-LC
releases, to set the serialized SCT list. AWS-LC copies the list and handles
context switches. Enable HAVE_SSL_SCTL for AWS-LC despite its missing
SN_ct_cert_scts OID.

AWS-LC also validates the list. Report a rejected list as a warning and omit
the extension, allowing the certificate to remain usable.

Tested with AWS-LC 4.2.0 and 3.0.0: TLS 1.2 sends the exact SCT payload for
the selected certificate with a sidecar and omits it for another certificate
on the same bind.

This must be backported to 3.0, the first version supporting AWS-LC.

2 weeks agoBUG/MEDIUM: ssl: fix an out-of-bounds read on SSL_CTX switch with a .sctl file
William Lallemand [Thu, 22 Oct 2026 12:26:54 +0000 (12:26 +0000)]
BUG/MEDIUM: ssl: fix an out-of-bounds read on SSL_CTX switch with a .sctl file

A certificate's .sctl sidecar registers extension 18 only on that
certificate's SSL_CTX. Connections start on initial_ctx without this
extension, then switch contexts in the ClientHello callback.

OpenSSL sizes the received-extensions array from the initial context but
parses it using the selected context's extension count. An unauthenticated
ClientHello selecting a certificate with a .sctl file can therefore trigger
an out-of-bounds read in tls_parse_all_extensions(). Depending on the heap
contents, the parser may also write past the array and read adjacent data.

Register the extension in the same order on every switchable frontend
context. The add callback omits it when the selected context has no SCT
list. Skip QUIC contexts because native OpenSSL's SSL_set_SSL_CTX() reads
freed memory when merging the transport parameters extension with custom
extensions. Skip registration on AWS-LC, which owns extension 18.

Workaround: remove "sctl" from "ssl-load-extra-files" or remove the .sctl files.

This should be backported to all supported versions.

Reported-by: Finlay Moss <fmoss2003@gmail.com>
2 weeks agoBUG/MEDIUM: ssl-gencert: fix a NULL dereference with QUIC listeners
Frederic Lecaille [Wed, 28 Oct 2026 10:26:02 +0000 (11:26 +0100)]
BUG/MEDIUM: ssl-gencert: fix a NULL dereference with QUIC listeners

ssl_sock_generate_certificate_from_conn() retrieves the connection from
the SSL object through ssl_app_data_index. A QUIC SSL object only
carries the quic_conn, stored under ssl_qc_app_data_index, so the
connection is always NULL there and conn_get_dst() dereferences a NULL
pointer.

This function is called from ssl_sock_switchctx_cbk() when the
ClientHello has no SNI and 'generate-certificates' is set on the bind
line. Any client that omits the server_name extension therefore kills
the worker (SIGSEGV), with no skill required: connecting to an IP
address is enough, since RFC 6066 forbids IP literals in SNI.

The bug is not specific to the OpenSSL compatibility layer. It
reproduces with USE_QUIC=1 whether or not USE_QUIC_OPENSSL_COMPAT is
set: the callback is registered the same way (HAVE_SSL_CLIENT_HELLO_CB)
and the ex_data assignment in qc_ssl_sess_init() is identical. It was
reproduced on a quictls build (USE_QUIC=1 alone, no 'limited-quic'
needed) and on an OpenSSL build with USE_QUIC_OPENSSL_COMPAT=1, both
crashing in ssl_sock_generate_certificate_from_conn().

Use ssl_sock_get_conn(), which handles both the connection and the
quic_conn, and fall back on the quic_conn's local address, the QUIC
counterpart of conn->dst, when there is no connection. When no address
is available, return 0 so that the caller keeps its default certificate
selection instead of crashing.

This must be backported to all stable versions.

Reported-by: Martin Mirchev <mirchevmartin2203@gmail.com>
2 weeks agoBUG/MINOR: connection: check the IP header length in NetScaler CIP
Martin Mirchev [Tue, 27 Oct 2026 06:17:35 +0000 (14:17 +0800)]
BUG/MINOR: connection: check the IP header length in NetScaler CIP

conn_recv_netscaler_cip() takes the TCP ports at ip_hl * 4 bytes after the
start of the IPv4 header without checking ip_hl nor that the announced
header length covers them:

  - with ip_hl < 5 the ports are taken from the IPv4 header itself (e.g.
    ip_hl=0 gives source port 16384 from the version/IHL byte);
  - with a header length shorter than ip_hl * 4 + 4 (e.g. ip_hl=15 and a
    40-byte header) they are taken from the payload or from bytes past the
    received data, which are leftovers of the previous use of the trash
    buffer, possibly from another connection.

The same applies to IPv6 with a header length shorter than 44 bytes. The
reads stay inside the trash buffer, so this is not a memory safety issue,
but the session addresses end up with wrong ports. Let's reject such
headers with CO_ER_CIP_BAD_PROTO.

This should be backported to all stable versions.

2 weeks agoBUG/MINOR: connection: check the NetScaler CIP header length against received data
Martin Mirchev [Tue, 27 Oct 2026 02:43:46 +0000 (10:43 +0800)]
BUG/MINOR: connection: check the NetScaler CIP header length against received data

conn_recv_netscaler_cip() only checks that the announced header length
<hdr_len> is not larger than the whole peeked data, which includes the
8 (legacy) or 12 (standard) byte CIP prefix. The header is then consumed
with a single recv() of prefix + hdr_len bytes into the trash chunk. With
hdr_len close to tune.bufsize (e.g. 16384 with the default bufsize), this
recv() writes up to 12 bytes past the end of the trash buffer.

Make sure the announced header fits in the data received after the prefix
before going further.

CIP is only accepted from trusted peers, so this is not a security issue,
but a sender misbehaving that way must not corrupt memory.

This should be backported to all stable versions.

2 weeks agoCLEANUP: resolvers: don't close the nameserver socket twice on deinit
Martin Mirchev [Tue, 27 Oct 2026 06:29:44 +0000 (14:29 +0800)]
CLEANUP: resolvers: don't close the nameserver socket twice on deinit

resolvers_destroy() calls fd_delete() on the UDP socket of each nameserver,
which already closes it, then calls close() on the same FD again. This
second close() fails with EBADF (visible with strace on soft-stop), and
could close an unrelated FD if the number were reused in between. Let's
just drop the redundant close().

This came with resolvers_destroy() in 2.7 (commit e606c84fe "MINOR:
resolvers: resolvers_destroy() deinit and free a resolver"). There is no
need to backport it, though it does not hurt.

2 weeks agoBUG/MINOR: stick-table: reject negative stick counter IDs in sc-* actions
Martin Mirchev [Tue, 27 Oct 2026 06:37:07 +0000 (14:37 +0800)]
BUG/MINOR: stick-table: reject negative stick counter IDs in sc-* actions

The parsers of sc-inc-gpc, sc-inc-gpc0/1, sc-add-gpc, sc-set-gpt and
sc-set-gpt0 read the stick counter ID with strtol() into a signed int and
only check it against tune.stick-counters. A negative ID such as in
"sc-inc-gpc(0,-2)" or "sc-set-gpt0(-1)" is thus accepted, and at runtime
the actions read &s->stkctr[-2] or &sess->stkctr[-1], before the start of
the stick counters array. ASan reports a heap-buffer-overflow READ in
stkctr_entry() called from the action, and a release build may crash or
use whatever lies there as a stick-table entry pointer to update it.

The sc_* sample fetches are not affected since they convert the ID to an
unsigned int before the check, and track-sc* parses an unsigned number.

Let's also reject negative IDs. A reg-test is added.

This should be backported to all stable branches. sc-add-gpc() appeared
in 2.8 and the others are older.

2 weeks agoBUG/MINOR: cfgparse-listen: reject negative capture lengths
Martin Mirchev [Tue, 27 Oct 2026 06:37:07 +0000 (14:37 +0800)]
BUG/MINOR: cfgparse-listen: reject negative capture lengths

"capture request header", "capture response header" and "capture cookie"
read their length with atol() and never check it, unlike "declare capture"
and "http-request capture" which reject lengths <= 0. A negative length,
or one that does not fit in an int such as 4294967295, is thus accepted,
and the first matching message crashes the process:

  - for headers, the capture pool is created with size len+1 (0 for -1),
    then http_capture_headers() calls isttrim(v, h->len), which converts
    -1 to SIZE_MAX and does not trim anything, so the whole header value
    is copied into the tiny pool object. ASan reports a heap-buffer-overflow
    WRITE from http_capture_headers(), and a release build usually dies
    soon after with a pool tag mismatch, a malloc() corruption abort or a
    SIGSEGV;

  - for the cookie, http_manage_client_side_cookies() clamps log_len to
    capture_len (-1) and calls memcpy() with (size_t)-1 (ASan:
    negative-size-param).

Let's reject negative lengths and lengths that do not fit in an int at
parsing time. A length of zero is still accepted since it is harmless
(it captures an empty string) and some configurations may rely on it. A
reg-test is added.

This should be backported to all stable branches. The parser was moved
to proxy_parse_capture() in 3.5-dev, older branches have the same atol()
calls in cfg_parse_listen().

2 weeks agoBUG/MINOR: tools: make strl2irc() and strl2llrc() reject a lone minus sign
Martin Mirchev [Tue, 27 Oct 2026 03:36:58 +0000 (11:36 +0800)]
BUG/MINOR: tools: make strl2irc() and strl2llrc() reject a lone minus sign

strl2irc() and strl2llrc() are documented to accurately detect any error
(truncated string, invalid chars, overflows). But when the string is made
of a single '-', the negative branch skips the sign, the digit loop does not
run since there is nothing left, and the function returns success with 0.

As a result, a lone "-" is silently taken as 0 wherever these functions are
used to validate a number:
  - "uid -" and "gid -" in the global section set uid/gid 0 (root)
    instead of emitting the usual "is not a number" warning. When started
    as root, this also suppresses the warning about running as root
    without "user"/"uid", so the process silently keeps root privileges;
  - "set server <b>/<s> check-port -" (and "agent-port -") on the CLI
    sets port 0 instead of failing with "provided port is not an
    integer";
  - req.hdr_ip() accepts "1.2.3.4:-" as a valid address and port;
  - an integer ACL pattern ":-" is taken as ":0", and a "-" value in the
    CLI "set table ... data.<type> -" command stores 0.

Let's return an error when the sign is not followed by any digit. A
reg-test checks that "uid -" and "gid -" now produce the warning (made
fatal with -dW).

This has been there since the functions were introduced, so the fix may be
backported to all stable branches.
[wt: better not backport as it could break configs running by accident]

2 weeks agoBUG/MINOR: thread: don't turn reversed thread ranges into "even"
Martin Mirchev [Tue, 27 Oct 2026 03:36:58 +0000 (11:36 +0800)]
BUG/MINOR: thread: don't turn reversed thread ranges into "even"

parse_thread_set() checks that the last thread number of a range is within
the permitted limits, but not that it is greater than or equal to the first
one. When storing the set, "max < min" is the internal encoding of "all"
(max=0), "odd" (max=-1) and "even" (max=-2), so any range written in
reverse order such as "thread 4-2" or "thread 1/4-2" on a "bind" line was
silently accepted and stored as "even" (memset 0xaa). With "nbthread 4",
"thread 4-2" binds the listener to threads 2 and 4 instead of threads 2 to
4, and "thread 4-1" also gives threads 2 and 4. There is no warning, so the
only visible effect is that some threads never accept connections on that
listener, which is easy to miss and hard to diagnose.

The other range parsers ("cpu-map", "process" via parse_process_number(),
and the cpuset parser) swap a reversed range, so let's do the same here.
This keeps configurations that rely on such ranges working, with the threads
the user wrote. A reg-test is added to check that "2-4", "4-2" and "1/4-2"
give the same thread mask.

This bug was introduced with the thread_set API in 2.8 (commit bef43dfa6
"MINOR: thread: add a simple thread_set API"), so this fix may be backported
to all stable branches down to 2.8.

2 weeks agoBUG/MINOR: h3: reset HTX message after trailers parsing error
Amaury Denoyelle [Tue, 27 Oct 2026 14:42:04 +0000 (15:42 +0100)]
BUG/MINOR: h3: reset HTX message after trailers parsing error

HTTP/3 trailers parsing was adapted by the following patch to rely on
the common function http_trailers_to_htx().

  25a11d345425be08cb01fec0eae5dfe8b423ee57
  MEDIUM: h3: rely on the generic trailers parser

Along this change, h3_trailers_to_htx() error path has been updated to
regroup failure with or without glitch via two labels. However, it
bypasses HTX message reset, leaving the stream buffer appearing as full
and possibly containing a partial trailers conversion.

Fix this by restoring the single unified function end. Return value and
glitch are directly adjusted in the switch statement from
http_trailers_to_htx() result.

No need to backport.

2 weeks agoBUG/MINOR: mux_quic: fix stream elasticity dec on qcm_init() failure
Amaury Denoyelle [Tue, 27 Oct 2026 15:09:28 +0000 (16:09 +0100)]
BUG/MINOR: mux_quic: fix stream elasticity dec on qcm_init() failure

On qcm_init() early failure, QCC is freed via qcc_release(). This
function always decrements reserved streams from the thread group pool
when stream elasticity is active for frontend connections.

This causes an issue as qcm_init() may fail before accounting the
committed streams: this is the case for <pacing_task> allocation
failure. Thread group counter is thus invalid after the extra decrement
and may cause an underflow and wrap.

Fix this by moving pacing task allocation later in qcm_init() so that
streams accounting is now performed before any failure path.

This must be backported up to 3.4.

2 weeks agoBUG/MINOR: mux_quic: fix ALPN retrieval from server
Amaury Denoyelle [Tue, 27 Oct 2026 14:11:34 +0000 (15:11 +0100)]
BUG/MINOR: mux_quic: fix ALPN retrieval from server

On backend side, ALPN may be retrieved from the server when 0-RTT is
used. This requires the server lock to protect concurrent modification
of the server access.

This code was badly written as server ALPN was still accessed outside of
the lock. Fix this by copying the server ALPN in a local string before
releasing the lock.

This also fixes the detection of an empty server ALPN to avoid an
unnecessary call to quic_alpn_to_app_ops().

This must be backported up to 3.4.

2 weeks agoBUG/MINOR: mux_quic: fix possible BUG_ON() after frame alloc failure
Amaury Denoyelle [Tue, 27 Oct 2026 10:01:13 +0000 (11:01 +0100)]
BUG/MINOR: mux_quic: fix possible BUG_ON() after frame alloc failure

Reset the list of frames to send at the end of qcc_io_send() if at least
one QCS instance was moved to the qcs_failed list and is reinserted into
the send list. This is mandatory to ensure frame generation will be
performed again for the failed QCS instances.

This issue is detected as it triggers a BUG_ON() crash on the second
qcc_io_send() call, as the send list will contain the previously failed
QCS after qcc_send_frames() despite not being blocked on flow control or
buffer allocation.

This bug occurs after qcs_send() failure, which is either due to the
connection currently being subscribed for send after a transient network
error or after a frame allocation failure under memory pressure.
However, the first case cannot occur as a transient error will not leave
any QCS in failed list. Thus, this fix is only relevant for memory
allocation failure, which explains its minor severity.

This must be backported up to 3.2 after a period of observation. For
older releases, a local frame list is used instead in qcc_io_send().

2 weeks agoBUG/MEDIUM: mux_quic: do not reset timeout after shut
Amaury Denoyelle [Wed, 28 Oct 2026 09:38:29 +0000 (10:38 +0100)]
BUG/MEDIUM: mux_quic: do not reset timeout after shut

When a connection is shutting down, MUX QUIC uses shut-timeout if
defined as a final timeout value. However, the timer was reset every
time qcc_refresh_timeout() was called. In effect, a connection could
stay indefinitely due to repeated activity from the peer.

Fix this by ensuring shut-timeout is only set once in
qcc_refresh_timeout(). The value is not updated if QCC task timeout is
already set.

Note that shut-timeout could be completely ignored if set to a greater
value than the default client/server timeout. However, as the docs
imply, this is not expected, as it is used to ensure a closing
connection will quickly end.

This must be backported up to 2.8.

2 weeks agoBUG/MAJOR: mux_quic: remove data not yet sent on reset stream
Amaury Denoyelle [Mon, 26 Oct 2026 10:21:32 +0000 (11:21 +0100)]
BUG/MAJOR: mux_quic: remove data not yet sent on reset stream

When a stream is reset, it is expected that no more data will be
emitted. Thus, prepared data not yet sent is subtracted from the
connection flow control. However, nothing is performed for the data
itself, which may have important side effects.

Most of the time, this issue is invisible as RESET_STREAM frame is
finally emitted, QCS instance is fully closed and removed from the send
list, ready for its deletion. However, an issue is possible if the read
channel is not yet closed and output has been blocked for whatever
reason. If output is unblocked before the read channel closure, QCS may
emit new data STREAM frames, triggering a BUG_ON() on stream flow
control in qcm_ctrl_send(). This assertion reveals a possible violation
of RFC 9000 3.3 (STREAM frame sent in "Reset Sent" state) and of the
final size already advertised by RESET_STREAM frame.

The proper fix to this issue is to also clear prepared data when stream
is reset, along with the flow control subtraction. This guarantees that
no more STREAM frames will be emitted after stream reset, as any
potential snd_buf stream layer callback will drain content.

This must be backported to all stable releases. Note that qcs_tx_buf()
is available since 3.4, needs adjustment for previous releases.

Many thanks to "Sungho Park (@gigio1023)" for its report and analysis of
the issue.

2 weeks agoMINOR: filters: Pre-initialize the instances in check_config_validity()
Christopher Faulet [Wed, 28 Oct 2026 08:01:51 +0000 (09:01 +0100)]
MINOR: filters: Pre-initialize the instances in check_config_validity()

The filter instances were pre-checked by a callback registered at the
pre-check stage, flt_precheck_instances_all(). This was annoying for
the modules adding instances after the configuration parsing, from
their own pre-check callback: the pre-check callbacks run before
check_config_validity(), so the instances they add are never
finalized.

The pre-check of all instances now runs from check_config_validity(),
before the servers are initialized: the default filter mode is
applied, then every instance is finalized by the parse callback of its
class, and the filter-enable/filter-disable and filter-sequence
directives are applied, as before. The pre-check callbacks may add
instances when the whole configuration was parsed, they are then
pre-initialized like the ones declared by the configuration.

The functions are renamed accordingly: flt_precheck_instances_all()
becomes flt_preinit_instances_all(), now exported, and
flt_precheck_instances(), flt_precheck_class_instances() and
flt_precheck_instance() respectively become flt_preinit_instances(),
flt_preinit_class_instances() and flt_preinit_instance().

2 weeks agoDOC: configuration: document library compatibility for pha
William Lallemand [Fri, 23 Oct 2026 10:56:53 +0000 (10:56 +0000)]
DOC: configuration: document library compatibility for pha

Add a Compatibility line to the pha server keyword documentation, following
the format introduced by commit 18003c561 (DOC: configuration: document
library compatibility for TLS keywords).

Document the minimum OpenSSL version and the lack of support in AWS-LC.

2 weeks agoDOC: configuration: document library compatibility for TLS keywords
William Lallemand [Tue, 13 Oct 2026 20:28:12 +0000 (20:28 +0000)]
DOC: configuration: document library compatibility for TLS keywords

Several TLS configuration keywords describe their OpenSSL requirements in
prose without a standardized Compatibility line, leaving AWS-LC support
unclear to readers.

Add Compatibility lines for the default bind and server ciphersuites,
bind and server ciphersuites, and TLS key logging. These require OpenSSL
1.1.1 or newer and AWS-LC 1.22.0 or newer (API 22). Replace the certificate
compression support sentence with a Compatibility line requiring OpenSSL
3.2.0 and explicitly stating that AWS-LC is unsupported.

Leave keywords requiring OpenSSL versions below 1.1.1 unchanged. This is
a documentation-only change; no runtime behavior is modified.

This may be backported to maintained branches containing the corresponding
keywords and AWS-LC support (as far as 3.0).

2 weeks agoDOC: config: Fix documentation about dot character in variable names
Christopher Faulet [Tue, 27 Oct 2026 13:20:17 +0000 (14:20 +0100)]
DOC: config: Fix documentation about dot character in variable names

In the section 2.8 (Variables), the doc character ('.') was not listed in
the set of characters allowed in a variable name. However it is an
error. The dot character is allowed. It cannot be used in the scope. But it
is allowed in the name.

The bug was introduced with the commit b643fbb1a ("DOC: config: Add a
dedicated section about variables"), when the section 2.8 was added.

This patch could be backported as far as 2.8.

2 weeks agoBUG/MEDIUM: applet: check buffer sizes before HTX transfer
Adam Crawford [Mon, 26 Oct 2026 04:21:40 +0000 (23:21 -0500)]
BUG/MEDIUM: applet: check buffer sizes before HTX transfer

The appctx_htx_snd_buf() fast path uses b_xfer() when the destination
HTX message is empty and the source contains a complete HTX message.
However, it does not verify that both buffers have the same capacity.

When tune.bufsize.large is configured and wait-for-body uses
use-large-buffer, the channel buffer may be larger than the applet
input buffer. The fast path can then attempt to transfer more data
than the destination can hold, triggering a fatal bounds check and
terminating the worker process.

Only use the fast path when the source and destination buffers have
the same capacity. Otherwise, use the existing HTX block-wise transfer
path.

[cf: Must be backported to 3.4]

2 weeks agoMINOR: filters: Document the public helpers missing from the filters guide
Christopher Faulet [Tue, 27 Oct 2026 12:45:40 +0000 (13:45 +0100)]
MINOR: filters: Document the public helpers missing from the filters guide

The filters internals guide did not reference several public functions
of the filter API, mainly the ones handling the configuration during
the parsing and the post-parsing stages. They are now described, at the
place where a filter developer needs them:

  * filter_find_class() looks a filter class up by name, in the class
    registry section (3.2);

  * flt_conf_add(), flt_conf_insert() and flt_conf_delete() manage the
    three configuration orders of a legacy filter configuration, in the
    legacy keyword parser section (3.2.2), with the constraint on the
    FLT_CFG_FL_EXPLICIT flag;

  * flt_use_legacy_filter() tests the filter mode of a proxy during the
    parsing, in the same section, with the constraint not to cache its
    result before the post-parsing stage;

  * flt_find_legacy_conf() searches the legacy filter configurations
    during the parsing, whatever the filter mode, in the section about
    referencing a filter configuration from other directives (3.7);

  * flt_has_explicit_config() detects an explicit filter declaration,
    in the same section.

2 weeks agoMINOR: flt-http-comp: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 10:54:35 +0000 (11:54 +0100)]
MINOR: flt-http-comp: Stop to explicitly loop on the proxy's filter_configs list

The "compression", "comp-req" and "comp-res" filter keyword parsers
scanned the filter_configs list of the proxy by hand to detect an
already declared compression filter configuration, matching on the
filter ids.

The parsers now use flt_find_conf(), the supported way to find a filter
configuration, which walks the right list depending on the filter mode of
the proxy. The "compression" duplicate check resolves the two possible
configurations with two successive lookups, one per direction.

2 weeks agoMINOR: flt-decomp: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 10:51:14 +0000 (11:51 +0100)]
MINOR: flt-decomp: Stop to explicitly loop on the proxy's filter_configs list

The "decomp-req" and "decomp-res" filter keyword parsers scanned the
filter_configs list of the proxy by hand to detect an already declared
decompression filter configuration, matching on the filter id.

The parsers now use flt_find_conf(), the supported way to find a filter
configuration, which walks the right list depending on the filter mode
of the proxy. As a side effect, the message of the "decomp-res" duplicate
error now names the right filter.

2 weeks agoMINOR: bwlim: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 10:50:33 +0000 (11:50 +0100)]
MINOR: bwlim: Stop to explicitly loop on the proxy's filter_configs list

The bandwidth limitation filter keyword parsers scanned the
filter_configs list of the proxy by hand to detect an already declared
"bwlim-in" or "bwlim-out" configuration, matching on the filter ids and
the limitation name.

The parsers now use flt_find_conf() with a match callback, the supported
way to find a filter configuration, which walks the right list depending
on the filter mode of the proxy. The duplicate check matches both filter
ids at once, with a single lookup.

2 weeks agoMINOR: fcgi-app: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 09:48:34 +0000 (10:48 +0100)]
MINOR: fcgi-app: Stop to explicitly loop on the proxy's filter_configs list

The "fcgi-app" filter keyword parser and the implicit "use-fcgi-app"
declaration resolution both scanned the filter_configs list of the proxy
by hand to find a previously declared FCGI filter configuration, matching
on the filter id and the application name.

The parsers now use flt_find_conf() with a match callback, the supported
way to find a filter configuration, which walks the right list depending
on the filter mode of the proxy. An explicit "fcgi-app" declaration now
rejects the replacement of another explicit declaration instead of any
one found, the implicit configuration it replaces is removed with
flt_conf_delete(), and the one-fcgi-app-per-backend check of
"use-fcgi-app" resolves the FCGI instance directly with
flt_find_instance() instead of a manual loop on the configurations.

2 weeks agoMINOR: cache: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 09:48:00 +0000 (10:48 +0100)]
MINOR: cache: Stop to explicitly loop on the proxy's filter_configs list

The "cache" filter keyword parser and the implicit "cache-store"
declaration resolution both scanned the filter_configs list of the proxy
by hand to find a previously declared cache filter configuration. The
lookup filters were inlined in the loops, matching on the filter id and
the cache name.

The parsers now use flt_find_conf() with a match callback, the supported
way to find a filter configuration, which walks the right list depending
on the filter mode of the proxy. The implicit cache-store instance is
added directly with its class, without a name lookup through the class
registry, and the implicit configuration replaced by an explicit
declaration is removed with flt_conf_delete().

2 weeks agoMINOR: filters: Add a function to find a legacy filter configuration during parsing
Christopher Faulet [Tue, 27 Oct 2026 13:07:10 +0000 (14:07 +0100)]
MINOR: filters: Add a function to find a legacy filter configuration during parsing

flt_find_conf() resolves a filter configuration through the filter mode
of the proxy: from the filter instances in the new mode, from the legacy
configurations otherwise. A directive parsing its own configuration
during the configuration parsing cannot use it: the mode of the proxy is
not known yet, it is deduced later from the implied filters, and in the
new mode the legacy configurations of the filters that registered no
class are ignored after the post-parsing stage.

flt_find_legacy_conf() is the parsing-time counterpart of
flt_find_conf(): it always searches the legacy filter configurations of
the proxy, in declaration order, whatever the filter mode. Like
flt_find_conf(), it filters the candidates by filter id, with an
optional match callback. It must only be used during the configuration
parsing, before the mode is deduced: the check callbacks of the rules
keep resolving their configuration with flt_find_conf(), once the
instances were finalized.

2 weeks agoMINOR: filters: Add a function to remove a filter conf from all proxy lists
Christopher Faulet [Tue, 27 Oct 2026 09:47:21 +0000 (10:47 +0100)]
MINOR: filters: Add a function to remove a filter conf from all proxy lists

A legacy filter configuration is linked in three lists of its proxy: the
global order with the 'list' member and the request and response orders
with the 'by_req' and 'by_res' members. Removing such a configuration
meant unlinking the three members by hand, in every filter supporting an
implicit declaration replaced by an explicit one.

The new flt_conf_delete() helper does it at once, as the counterpart of
flt_conf_add() and flt_conf_insert(). It will be used by the filters
parsing an explicit declaration that replaces an implicit one.

2 weeks agoMINOR: filters: Declare a proxy as using the legacy API before parsing filter directive
Christopher Faulet [Tue, 27 Oct 2026 10:14:17 +0000 (11:14 +0100)]
MINOR: filters: Declare a proxy as using the legacy API before parsing filter directive

The "filter" directive declares a legacy filter configuration on the
proxy being parsed, which selects the legacy filter mode: the
PR_FL_FILTER_LEGACY flag is set on the proxy. Until now the flag was set
at the end of the parsing, after the filter keyword was resolved and its
configuration produced.

The filter keyword parsers may call flt_find_conf() to resolve a
previously declared configuration, or may add an implicit instance with
flt_add_implicit_instance(). Both rely on the filter mode of the proxy
to walk the right configuration list: without the flag they would
inspect the filter instances instead of the legacy filter
configurations. The keyword parser of a filter is called while the
directive is being parsed, so the mode must be known before, not after.

The flag is now set as soon as the "filter" directive is identified, so
the keyword parsers always see the mode their own directive selects.

2 weeks agoMINOR: filters: Warn about ignored legacy filters for a proxy
Christopher Faulet [Tue, 27 Oct 2026 10:15:51 +0000 (11:15 +0100)]
MINOR: filters: Warn about ignored legacy filters for a proxy

A proxy without explicit filter declaration now gets its filter mode
deduced from the filters implied by its other directives. When it implies
filters of both kinds, it uses the filter instance mode and the
class-less ones are ignored: their configurations are legacy filter
configurations with no filter instance, so they are never evaluated and
the features they implement silently stop working on that proxy.

Such a proxy is now reported at startup, with a warning per ignored
configuration, naming the configuration and reminding the two ways to
have it evaluated: declare the filter explicitly with the "filter"
directive, which selects the legacy mode for the whole proxy, or set the
"filter-api downgrade-to-legacy" global option.

The relation between a legacy configuration and the instances of its
filter relies on the callbacks table of the filter: a filter supporting
both modes produces configurations with the same table from its legacy
keyword parser and from its class parse callback (see
flt_parse_instance_legacy()), so an instance with the same table means
the filter declared its class. The check runs after the instances are
finalized, when their configurations exist, and before the class
references are released by the flattening.

All the filters supported in-tree register their classes, so only the
third-party filters relying on an implicit declaration are concerned.
The documentation is updated accordingly, in the filters internals guide
and in the configuration manual.

2 weeks agoMINOR: filters: Deduce the default filter API from the implied filters
Christopher Faulet [Mon, 26 Oct 2026 10:16:59 +0000 (11:16 +0100)]
MINOR: filters: Deduce the default filter API from the implied filters

Until now, a proxy with no explicit filter declaration (neither "filter" nor
"filter-*" directives) always used the new filter API, except when the
legacy API was globally forced with "filter-api downgrade-to-legacy". But a
third-party filter relying on an implicit declaration only, and not
registering a filter class, was silently ignored in the new API: such a
filter declares a legacy filter configuration, without any class, hence
without any instance.

Now the API used by a proxy without explicit filter declaration is deduced
from what its other directives imply. A proxy with filter configurations and
no filter instance uses the legacy API: the implied filters declared no
class, so they are invisible to the new API. A proxy with filter instances
uses the new API, where the class-less filters it may also imply are
ignored. The explicit declarations and the "filter-api downgrade-to-legacy"
option keep precedence, as before.

All the filters supported in-tree register their classes and are
instantiated in the new API, so they are unaffected: a proxy implying only
in-tree filters always has instances, and keeps using the new API.  The
change only makes the class-less third-party filters work again without the
global downgrade, and it is the first step towards making the new API the
only one.

The documentation is updated accordingly, in the filters internals guide and
in the configuration manual, for the "filter-api" option and the filter mode
selection.

2 weeks agoMINOR: filters: Set the class flags on filter class registration
Christopher Faulet [Mon, 26 Oct 2026 09:41:57 +0000 (10:41 +0100)]
MINOR: filters: Set the class flags on filter class registration

The classes registered with filter_register_class() or
filter_register_class_full() could not set their flags, in particular
FLT_CLS_FL_MULTI: the flags passed to flt_init_class() were always 0, so a
class of an external module was always limited to a single instance per
proxy, even with an id.

filter_register_class() and filter_register_class_full() now take a
'flags' argument, passed to flt_init_class() as for the internal classes.
The class flags are the FLT_CLS_FL_* flags, only FLT_CLS_FL_MULTI for now,
set when several instances of the class are allowed in the same proxy.

The filters documentation is updated accordingly, in the class
registration section and in the filter instance mode conversion guide.
The example in the comment of filter_init_classes() is updated too.

2 weeks agoBUG/MINOR: hlua: unlock Patref before add_bulk type error
Kelly Kane [Sun, 25 Oct 2026 01:03:03 +0000 (01:03 +0000)]
BUG/MINOR: hlua: unlock Patref before add_bulk type error

Patref:add_bulk checks table entries while holding the pattern reference
write lock. luaL_checktype() raises on a non-string entry and bypasses
the unlock, blocking later updates. Check types without raising while
locked, then unlock before reporting the original Lua error.

Signed-off-by: Kelly Kane <kelly@hawknetworks.com>
It should be backported to 3.2.

2 weeks agoBUG/MEDIUM: listener: enforce rate-limit sessions across thread groups
Manu Nicolas [Tue, 20 Oct 2026 11:29:55 +0000 (11:29 +0000)]
BUG/MEDIUM: listener: enforce rate-limit sessions across thread groups

Since session rates are accounted per thread group, the "rate-limit
sessions" check sums, for each group, the remaining budget computed
against the full configured limit. A frontend therefore keeps accepting
connections as long as any group stays below the limit:

  - with evenly loaded groups, the frontend accepts up to the limit
    multiplied by the number of thread groups;
  - when some groups receive no traffic, e.g. with "bind ... thread
    1/all" and several thread groups, the limit is never enforced.

No explicit thread group setting is needed to be affected. Since 3.3,
the default "performance" cpu-policy creates one thread group per CCX
when neither "nbthread" nor "thread-groups" is set, and binds are
spread over all groups. On machines with several CCX, a frontend with
no thread-related setting may thus accept several times its configured
session rate. Setting "thread-groups 1" works around the issue.

With "rate-limit sessions 100" and three thread groups, a local test
measured about 270 sessions/s on a bind using all groups, and more than
21000 sessions/s on a bind restricted to the first group.

The limit is meant to apply to the frontend as a whole. Let's sum the
per-group session totals first and compare the result with the limit,
both for the number of sessions that may still be accepted and for the
delay before the next one. Two helpers, proxy_fe_sess_remain() and
proxy_fe_sess_delay(), now do this for all callers. The same test then
measures 100 sessions/s in both cases.

A reg-test is provided: with a bind restricted to the first of two
thread groups and a limit of one session per second, the second session
must wait for the rate to drop and see fe_sess_rate at 1, not 2.

This was introduced in 3.3 by commit 16eb0fab3 ("MAJOR: counters:
dispatch counters over thread groups"), so this must be backported to
3.3.

2 weeks agoBUG/MINOR: peers: forget the remote table when ignoring an unknown key type
Frederic Lecaille [Fri, 23 Oct 2026 16:18:36 +0000 (18:18 +0200)]
BUG/MINOR: peers: forget the remote table when ignoring an unknown key type

Commit 1696cfaa1 ("BUG/MEDIUM: peers: enforce check on incoming table key
type") makes peer_treat_definemsg() ignore a table definition with an
unknown key type. But contrary to all other paths ignoring a definition,
it leaves p->remote_table set to the local table matched by name. The
update messages which follow are thus not ignored but decoded using the
local table's key type and applied to it, while they were built for a
different key type. This is precisely the situation the commit wanted to
cover (a newer peer supporting new key types).

Let's reset p->remote_table as the other paths do.

HAProxy never emits such a value: peer_net_key_type[] only covers the five
supported key types, so a compliant peer stays below PEER_KT_TYPES. This is
therefore latent today, and becomes reachable the day a new key type is
added, when a newer peer talks to an older one in a mixed-version cluster.

This must be backported to all stable versions.

2 weeks agoBUG/MINOR: peers: don't use remote table definitions from a previous session
Frederic Lecaille [Fri, 23 Oct 2026 16:01:06 +0000 (18:01 +0200)]
BUG/MINOR: peers: don't use remote table definitions from a previous session

The remote table ID and the list of data types announced by the remote
peer in table definition messages are stored in the shared table and are
never reset when the session ends. Since remote_id defaults to 0, a table
switch message designating table ID 0 selects any table the peer never
defined, and after a reconnection, a table switch message referencing a
table ID defined during the previous session selects it with the previous
session's data types, without the checks performed on definition messages
(e.g. the key type).

In both cases the update that follows is still decoded: the entry key is
inserted into the local table even when no data type is announced, so such
a peer can create entries carrying only a key and no value. On a small
table this can evict legitimate entries.

Let's reset the remote definitions when the session ends, and never match
table ID 0 in switch messages.

Reproducibility: both issues require a received "stick-table switch"
message (type 131). HAProxy has never emitted this message: since the
peers protocol 2.0 was introduced in 2015 (b3971ab06), the function in
charge of building it, peer_prepare_switchmsg(), actually emits a table
definition message (type 130), and doc/peers.txt documents type 131 as
unused. However, the message is part of the documented protocol and its
type is still assigned, so a third-party or modified implementation may
well emit it, possibly in good faith, since its name and semantics
describe exactly a table selection by ID. It is therefore reachable by a
non-HAProxy peer, but not by any version of HAProxy itself.

Note that the statement in the original report about a conforming peer
which "always defines a table before switching to it" does not hold
either, as HAProxy never switches at all.

This must be backported as far as 3.0.

2 weeks agoBUG/MINOR: peers: compare the received expiration date as unsigned
Frederic Lecaille [Fri, 23 Oct 2026 15:39:13 +0000 (17:39 +0200)]
BUG/MINOR: peers: compare the received expiration date as unsigned

The expiration date received in timed update messages is an unsigned
32-bit value on the wire, but it is stored into an int and compared with
the table's expire as a signed value. A value of 2^31 or more thus becomes
negative, is not capped to the table's expire, and results in an entry
expiring in the past, hence purged immediately, instead of being capped to
the table's expiration delay.

Let's compare it as unsigned.

Note that only a buggy or malicious peer sending such a value can reach this: a
compliant peer always sends tick_remain() of its own entry, which cannot
exceed its table's expire and is therefore below 2^31. The effect is
limited to the entry carried by the message, which never lives: it is
purged at the next expiration pass. But since peers are meant to keep
identical tables on every node, this lets such a peer make a chosen key
disappear prematurely on its neighbours, which is a divergence rather
than a local drop.

This can be backported to all stable versions.

2 weeks agoBUG/MINOR: peers: account incoming sessions in the per-thread applet counters
Frederic Lecaille [Fri, 23 Oct 2026 09:46:36 +0000 (11:46 +0200)]
BUG/MINOR: peers: account incoming sessions in the per-thread applet counters

The per-thread applet counters of a peers section are used to pick the
least loaded thread when creating an outgoing peer session. They are
incremented in peer_session_create() for outgoing sessions only, but
decremented in __peer_session_deinit() for all sessions, including
incoming ones. Each incoming session thus makes the counter of its thread
drift towards negative values, and outgoing sessions then tend to be
created on the threads which handled incoming ones, defeating the load
balancing.

Must be backported as far as 3.0.

2 weeks agoBUG/MINOR: peers: reject over-long varints in intdecode()
Frederic Lecaille [Fri, 23 Oct 2026 08:32:21 +0000 (10:32 +0200)]
BUG/MINOR: peers: reject over-long varints in intdecode()

intdecode() shifts continuation bytes by 7 more bits for as long as they
have their stop bit set, without any upper bound on the shift count. A
64-bit value never needs more than 10 bytes, i.e. a shift of 60, but a
malformed message can carry a varint whose 10th continuation byte would
be shifted by 67. Shifting a 64-bit value by 64 or more is undefined
behavior, reported by UBSan, and yields a garbage value.

Check the shift count along with the end of buffer before consuming the
byte, so that such an encoding is rejected. Values up to 2^64-1, which is
encoded on exactly 10 bytes, are still accepted.

Only a buggy or malicious peer sending a malformed varint can reach this:
a compliant peer never emits more than 10 bytes, and peer sessions are
expected to run on trusted networks. The decoded value is later
range-checked against the message bounds, so no memory safety issue is
known; this is UB hardening.

This is long-standing and can be backported to all stable versions.

2 weeks agoBUG/MINOR: peers: don't lose the peer's version when the hello is fragmented
Frederic Lecaille [Thu, 22 Oct 2026 09:34:58 +0000 (11:34 +0200)]
BUG/MINOR: peers: don't lose the peer's version when the hello is fragmented

The peers applet parses the protocol version from the first line of the
hello message in the GETVERSION state. However, maj_ver and min_ver are
local variables which are reset on each pass through the handler's state
machine.

If the host or peer line is not available yet, the handler returns and
resumes later in GETHOST or GETPEER, with the version lost. As a result,
PEER_F_DWNGRD is not updated and retains its previous value. A peer
announcing protocol version 2.0 may then receive timed updates it does not
support, while a 2.1 peer may receive updates without expiration.

Preserve the minor version in appctx->st1 until the peer is identified,
then use it to set or clear PEER_F_DWNGRD and reset st1. This field is
otherwise only used for error status codes, which error paths overwrite
before entering EXIT.

Since GETPEER can only be reached after a valid version has been parsed,
there is no need for an unknown-version sentinel. Remove the now unused
initialization of maj_ver and min_ver.

This has been there since 1.7 with commit 523cc9e85 ("MEDIUM: peers: Fix a
peer stick-tables synchronization issue."). This fix should be backported
to all stable versions.

2 weeks agoBUG/MINOR: peers: forget the appctx when the peer session init fails
Frederic Lecaille [Wed, 21 Oct 2026 16:26:09 +0000 (18:26 +0200)]
BUG/MINOR: peers: forget the appctx when the peer session init fails

peer_session_create() assigns the new appctx to peer->appctx and accounts
it in the per-thread applet counters, then the session is initialized by
peer_session_init() when the appctx first runs. If this initialization
fails (e.g. memory allocation failure), the appctx is freed by
appctx_free_on_early_error() without the release callback being called, so
peer->appctx keeps pointing to freed memory: the sync task then never
tries to reconnect this peer (it believes a session exists), a later
incoming connection from this peer or a soft-stop would call
peer_session_forceshutdown() on the freed appctx, and the applet counter
remains incremented.

Let's make peer_session_init() detach the appctx from the peer on error,
release the applet counter, and wake the sync task up so that a new
connection is attempted once the reconnect timer expires.

This can be backported to all stable versions where peer_session_init()
exists (2.6 and above).

2 weeks agoBUG/MEDIUM: peers: fix byte order of last_get on updates without identifier
Frederic Lecaille [Tue, 20 Oct 2026 14:12:36 +0000 (16:12 +0200)]
BUG/MEDIUM: peers: fix byte order of last_get on updates without identifier

Since commit f12252c7a ("BUG/MEDIUM: peers: Fix update message parsing
during a full resync"), peer_treat_updatemsg() computes the update ID
in a local variable whose byte order depends on the message type.
Explicit identifiers are read in network byte order, while implicit
identifiers are computed in host byte order as st->last_get + 1.

Outside full resynchronization, htonl() is then applied to this variable
in both cases before storing it in st->last_get. This correctly converts
explicit identifiers, since htonl() and ntohl() perform the same
transformation, but incorrectly byte-swaps implicit identifiers on
little-endian machines.

The incorrect identifier is sent in ACK messages and stored by the
sender as st->update. This value initializes the transmission cursors
after a reconnection, so its corruption may cause updates to be skipped
or unnecessarily resent. A subsequent update carrying an explicit
identifier restores the correct last_get value, which can hide the
issue when inspecting the final state after a burst of updates.

This can be reproduced by completing the initial resynchronization,
then sending an UPDATE with identifier 199 followed by an INCUPDATE.
On a little-endian receiver, last_get and the acknowledged identifier
become 3355443200 (0xc8000000) instead of 200. A regression test exercising
this sequence fails without the fix and passes with it.

Convert explicit identifiers to host byte order immediately after
reading them, then store the resulting identifier without conversion.
Leave the full-resynchronization guard unchanged.

This must be backported as far as 3.3.

2 weeks agoBUG/MEDIUM: peers: do not keep a dict entry cached when its update was not sent
Frederic Lecaille [Tue, 20 Oct 2026 12:40:53 +0000 (14:40 +0200)]
BUG/MEDIUM: peers: do not keep a dict entry cached when its update was not sent

When an update message carries a server_key which is not yet in the peer's
dictionary TX cache, peer_prepare_updatemsg() inserts it into the cache and
encodes the full value. But if the message does not fit in the applet's
output buffer, applet_putblk() returns -1 and the message is simply dropped,
to be built again on the next call since last_pushed was not advanced. At
this point the entry is found in the TX cache, so only its ID is emitted,
and the remote peer, which never received the value, resolves this ID to a
NULL (or stale) entry in its RX cache. The server_key of this entry is then
lost on the remote side, as well as for all subsequent updates referencing
the same server for the whole session's lifetime, or it may even point to
another server whose value previously occupied the same slot.

This is easily observed during a resync of a table storing server_key with a
few servers having long names: the first update carrying a new server name
that does not fit in the buffer is retried without its value. With a debug
print on the receiver, ~100 entries out of 400 were received with an unknown
dict ID in such a test.

Let's record the node newly inserted in the TX cache in the update params,
and remove it from the cache when the message could not be emitted, so that
the next attempt sends the full value again.

The issue exists since the dict cache was introduced in 2.0 with commit
8d78fa7de ("MINOR: peers: Make peers protocol support new "server_name" data
type."), so this should be backported to all supported versions.

2 weeks agoBUG/MINOR: deviceatlas: consume a scheduled update before loading it
David Carlier [Tue, 20 Oct 2026 14:59:06 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: consume a scheduled update before loading it

The update flag in the shared map was only cleared after a successful
reload. When the published data file could not be opened, compiled or
loaded, every following request retried it: an unreadable file produced
one alert per request, flooding the logs and lowering the throughput,
and a file failing to compile was compiled again on each request. This
is the case for example since the automatic chroot became the default,
as the data file path is no longer reachable at run time.

The flag is now cleared as soon as the path has been read, so a failing
file is reported once and retried only when the scheduler publishes a
new one. This also avoids losing an update published while the previous
one was being compiled.

This should be backported to lower branches.

2 weeks agoBUG/MINOR: deviceatlas: use the result's property type when printing
David Carlier [Tue, 20 Oct 2026 14:59:05 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: use the result's property type when printing

da_haproxy() took the property type from the atlas catalogue, but a result
may carry a different type for a property, e.g. a client-side property from
the cookie. The typed getter then failed and the value was printed empty.
The type is now read from the result with da_getproptype().

This should be backported to lower branches.

2 weeks agoBUG/MINOR: deviceatlas: ignore the scheduler's update in progress marker
David Carlier [Tue, 20 Oct 2026 14:59:04 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: ignore the scheduler's update in progress marker

The scheduler sets the first byte of the shared map to '0' while it
rewrites the data file path. Any non-zero byte was taken as a pending
update, so a request could read a partial path and emit a spurious
"invalid path" alert. '0' is now treated as no update, as the nginx
module does.

This should be backported to lower branches.

2 weeks agoBUG/MINOR: deviceatlas: refresh the user-agent evidence id after a reload
David Carlier [Tue, 20 Oct 2026 14:59:03 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: refresh the user-agent evidence id after a reload

The user-agent evidence id used by da-csv-conv was only computed at init.
Ids belong to the data file, so after a hot-reload of a file with a
different header list, da-csv-conv could pass a stale id and return wrong
or empty properties until restart.

This should be backported to lower branches.

2 weeks agoBUG/MEDIUM: deviceatlas: isolate threads while swapping the reloaded atlas
David Carlier [Tue, 20 Oct 2026 14:59:02 +0000 (15:59 +0100)]
BUG/MEDIUM: deviceatlas: isolate threads while swapping the reloaded atlas

When da_haproxy_checkinst() hot-reloads the atlas, the old instance was
closed and its image freed under dadwsch_lock only, while lookups on other
threads take no lock. With several threads, a request could search a freed
image or a partially copied atlas and crash. In addition, the other threads
spun on the lock during the whole compilation of the new data file, long
enough for the watchdog to kill the process with large files.

The lock is now only tried, so other threads keep serving with the current
atlas during the compilation. Once the new atlas is ready the lock is
released, the swap is done between thread_isolate() and thread_release(),
and the old image is freed afterwards.

This should be backported to lower branches.

v2: release dadwsch_lock before thread_isolate(), which must not be
called with a lock held.

2 weeks agoBUG/MEDIUM: filters: Don't propagate a close while a filter waits on a channel
Christopher Faulet [Fri, 23 Oct 2026 16:33:29 +0000 (18:33 +0200)]
BUG/MEDIUM: filters: Don't propagate a close while a filter waits on a channel

When a filter asks to wait during the analysis of a channel, with
filter.WAIT, all the data it holds are kept in the buffer, waiting to be
processed when it is called back. But a close could be propagated to the
other side in the meantime: when a shutdown or an abort was received on
the channel, the close was forwarded to the peer side, for instance a
backend EOF was immediately forwarded to the client. The stream could
then terminate before the filter resumed, and the held data were lost,
typically a complete response held by lua filters waiting for their
wake-up, delivered with no framing and delimited by the backend close.

So now, when a filter asks to wait, the close is no longer propagated:
like the connection on the request side, the channel keeps the
CF_AUTO_CLOSE flag cleared while a filter waits on it. The flag is
restored by the analyzers on each pass, so the close is simply deferred
until the filter resumes, and nothing changes for the streams without
filters waiting.

The "lua-eof" reg-test, introduced to record this race and marked broken
until it was fixed, now passes and is un-broken. It exercises both the
legacy filter chains and the filter instance mode, with a keep-alive
connection reuse.

This patch should be backported to all supported version after a perido of
observation.

2 weeks agoBUG/MEDIUM: hlua/filters: Never arm the analysis timer for the lua wake-ups
Christopher Faulet [Fri, 23 Oct 2026 14:52:40 +0000 (16:52 +0200)]
BUG/MEDIUM: hlua/filters: Never arm the analysis timer for the lua wake-ups

A lua filter may set a wake-up date from a callback, with
filter.wake_time(), to be called back later to continue its processing.
This date was armed on the analysis timer of the filtered channel. But this
timer is a real timer, shared by all the filters and the analyzers attached
to the channel, and it is single-owner: it must only be manipulated by one
entity at a time, which consumes its own expiry.  Setting a wake-up date on
it was thus wrong. In practice, the date was systematically preserved when
it was not expired, another filter or an analyzer may rely on it, especially
during the message analysis, when the analyzers use it as a blocking timer
(the "pause" action for instance). But the value could be left armed after
the filter was done with the channel: when it expired later, without any
owner, nothing could reset or rearm it. The stream task was then woken up on
the expired timer and looped forever rearming it, a busy loop until the
watchdog killed the process. This may happen long after the filter finished
its processing, for instance a wake-up date armed during the payload
analysis may be left behind when the payload analysis ends, while the
analysis of the other channel is still in progress.

The wake-up date is not a real timer, only an upper bound on the wait
duration: the filter must be woken up at most at this date, earlier is fine,
for instance on a data event. So now it is armed on the stream task
expiration date instead, which is not a real timer either, it is only used
to wake up the task at the lowest armed date. The stream task expiration
date is always recomputed from all its sources, so nothing can be left
behind on it. The lua filters remember nothing.

But a wake-up on the stream task alone does not report any event on the
channels. So, when the task is woken up because a timer expired, the
check_timeouts callback of the lua filters is called: it reports a read
event on the channels filtered by the lua filter when the wake-up date
expired, so its analyzers and its filters are called back. Otherwise, the
wake-up date is re-armed on the stream task expiration date, in case it was
dropped from it.

This way, the analysis timers are only used by their owners, and the lua
filters get their wake-ups without interfering with them. The
http-request/response actions, which also support act.wake_time(), continue
to arm the analysis timer: they are called back by the analyzers of the
ruleset they belong to, which consume their own expiry on this timer.

This patch should be backported to all supported versions.

2 weeks agoBUG/MEDIUM: hlua/filters: Make Channel.may_recv relative to the filter's data
Christopher Faulet [Fri, 23 Oct 2026 07:23:28 +0000 (09:23 +0200)]
BUG/MEDIUM: hlua/filters: Make Channel.may_recv relative to the filter's data

Channel.may_recv() tested the whole channel to know if more data may be
received, ignoring the data visible by the calling filter. But when several
Lua filters are chained on a channel and an earlier one holds back some data
(for instance to rewrite the body), the channel input may be closed while
the current filter still waits for data held upstream. In that case, the
function must return true, because the held data can still be exposed to
the filter, even if nothing will be received on the connection anymore.

Now, like HTTPMessage.eom(), HTTPMessage.may_recv() and
HTTPMessage.closed(), the result is relative to the data visible by the
current filter: the function always returns true if an earlier filter
holds back data.

This patch should be backported to all supported versions.

2 weeks agoBUG/MINOR: hlua/filters: Restrict HTTPMessage.set_eom/unset_eom to visible data
Christopher Faulet [Fri, 23 Oct 2026 07:23:28 +0000 (09:23 +0200)]
BUG/MINOR: hlua/filters: Restrict HTTPMessage.set_eom/unset_eom to visible data

HTTPMessage.set_eom() and HTTPMessage.unset_eom() manipulate the EOM flag
of the whole HTX message, without checking the data visible by the calling
filter. But when several Lua filters are chained on a channel and an
earlier one holds back some data (for instance to rewrite the body), the
end of the message may be reached by this filter while the current one
still waits for data held upstream. In that case, setting the EOM would
duplicate it, and unsetting it could not remove the flag from the data
held by the earlier filter.

Now both functions can only be called when all data of the message are
visible by the current filter, like HTTPMessage.eom(),
HTTPMessage.may_recv() and HTTPMessage.closed(). Otherwise an error is
raised. The error message of HTTPMessage.unset_eom() was also fixed: it
referred to the 'set_eom' function.

This patch should be backported to all supported versions.

2 weeks agoMINOR: hlua/filters: Add HTTPMessage.closed() to check if input is closed
Christopher Faulet [Fri, 23 Oct 2026 07:23:28 +0000 (09:23 +0200)]
MINOR: hlua/filters: Add HTTPMessage.closed() to check if input is closed

There is no way for a Lua filter to know if the input of the HTTP message
it is filtering is closed, i.e. if no more data will be received on the
corresponding channel because the end of the stream was reached or an
error was detected.

HTTPMessage.eom() is not suitable to detect that. It relies on the message
framing and is only true when the end of the message was properly
reported. For instance, when a truncated request is received, no end of
message is reported but the input is nonetheless closed.

HTTPMessage.may_recv() is not suitable either because, as its name
implies, it only tells if more data may still be received at this
point. An unfinished message whose input is closed returns false.

Conversely, HTTPMessage.closed() returns true as soon as the input is
closed, whatever the message framing state. However, as for
HTTPMessage.eom(), the result is relative to the data visible by the
current filter. It is only true when all data were already exposed to
it. So if an earlier filter holds back some data, the function returns
false until these data are visible.

2 weeks agoBUG/MEDIUM: hlua/filters: make HTTPMessage.eom/may_recv relative to the filter's...
Christopher Faulet [Tue, 20 Oct 2026 12:46:51 +0000 (12:46 +0000)]
BUG/MEDIUM: hlua/filters: make HTTPMessage.eom/may_recv relative to the filter's data

HTTPMessage.eom() and HTTPMessage.may_recv() rely on the whole HTX message
to know if more data are expected or not, by testing if the end of message
was reached. However when several Lua filters are chained on a channel and
an earlier one holds back some data (for instance to rewrite the body), the
next ones may perform a test on data it should not see. This could lead to
unexpected behavior, mainly by disclosing too early the end of the message
to next layer.

Now an help function is used to know if all data are visible or
not. HTTPMessage.eom() is always false is all data are not visible by the
current filter and HTTPMessage.may_recv() always true in that case.

This should be backported to all supported versions but it will have to be
adapted because the HTX API was changed in 3.5.

2 weeks agoBUG/MINOR: hlua/filters: Track the payload phase per channel
Christopher Faulet [Thu, 22 Oct 2026 13:32:03 +0000 (15:32 +0200)]
BUG/MINOR: hlua/filters: Track the payload phase per channel

HLUA_FLT_CTX_FL_PAYLOAD is used to know if a filter is in its payload
forwarding stage, when its http_payload or tcp_payload callback is
called, because the data of a channel may only be manipulated in this
context. But this was a per-context flag, shared by both channels,
while the payload phase is specific to a channel. Thus, when a filter
processed the request payload, a forbidden operation could be performed
on the response data while it was analyzed, and reciprocally.

Now the payload phase is tracked per channel, with a dedicated flag for
the request and the response. All the data manipulation functions now
check the flag of the channel on which they operate. The flags are set
or cleared by the payload and analysis callbacks of the corresponding
channel. In particular, start_analyze only clears the flag of the
channel being analyzed, so starting the analysis of a channel no longer
resets the payload phase of the other one.

2 weeks agoBUG/MEDIUM: connection: don't leak the fd of an accepted connection
Christopher Faulet [Thu, 22 Oct 2026 08:12:11 +0000 (10:12 +0200)]
BUG/MEDIUM: connection: don't leak the fd of an accepted connection

When session_accept_fd() fails in conn_prepare(), e.g. because the SSL
connection limit (maxsslconn) is reached, conn_ctrl_init() hasn't run yet
and CO_FL_CTRL_READY isn't set. conn_ctrl_close() then cannot close the fd,
leaving it open. Additional incoming connections on the same listener keep
accumulating leaked fds, even after the SSL connections are released, and
may ultimately exhaust maxsock, also preventing non-SSL frontends from
accepting.

Fix this by closing the fd in conn_ctrl_close() when it hasn't been
registered yet, unless the connection is fdless.

Many thanks to Adam Crawford for the report.

This should be backported to all supported versions.

2 weeks agoBUG/MINOR: h3: do not wait after Tx realloc failure
Amaury Denoyelle [Fri, 23 Oct 2026 09:22:48 +0000 (11:22 +0200)]
BUG/MINOR: h3: do not wait after Tx realloc failure

The following commit implements for QUIC transcoders a realloc operation
when a smallbuf is not sufficient to encode headers. This is available
via qcc_realloc_stream_txbuf() function.

  0d6112b40b20f9eea0cf38dc304bc1155b673588
  MINOR: mux-quic: retry after small buf alloc failure

However, an alloc failure of this function was considered as a transient
error, with 0 returned in this case. Fix this so that it is now a fatal
error which will trigger the connection closure, which is the correct
handling for memory allocation issues in QUIC MUX layer.

This must be backported up to 3.2. Note that the chunk relative to
h3_req_headers_send() is specific for QUIC backend usage and must only
be picked on 3.3 and above.

2 weeks agoBUG/MINOR: h3: do not retry on trailers too large
Amaury Denoyelle [Fri, 23 Oct 2026 08:44:41 +0000 (10:44 +0200)]
BUG/MINOR: h3: do not retry on trailers too large

The following patch adds the ability for the QUIC transcoder to release
a buffer if there is not enough space in it and allocate a new one
instead. This is mostly used during DATA and trailers encoding.

  cd22200d239773a851c6b78e722864fa5103af6a
  MEDIUM: mux-quic: release Tx buf on too small room

For trailers encoding it may cause an infinite loop if the content is
larger than a whole buffer. In this case, transcoder will release and
allocate a new buffer without interruption before the watchdog is
triggered.

Fix this by extending h3_resp_trailers_send(). The buffer is now
released only when it contains residual not yet sent or acknowledged
data. If the buffer is empty, trailers are too large and an error is
returned, which will close the connection with INTERNAL_ERROR code. This
method was in fact already implemented in h3_resp_headers_send().

This must be backported up to 3.0.

2 weeks agoBUG/MEDIUM: h3: prevent conn failure on trailers decoding without space
Amaury Denoyelle [Thu, 22 Oct 2026 13:36:02 +0000 (15:36 +0200)]
BUG/MEDIUM: h3: prevent conn failure on trailers decoding without space

HTTP/3 trailers are decoded in function h3_trailers_to_htx(). If this
fails due to a lack of space, a negative value is returned, resulting in
a connection closed with INTERNAL_ERROR. This is incorrect as it could
have occurred because of data not yet consumed in HTX output buffer.

One solution would be to temporarily block demux when such situation is
encountered. However, it is not easily implementable now since the
introduction of http_trailers_to_htx() which is shared with HTTP/2
parser. A value HTTP_PRS_TOO_MANY can be caused either due to a
transient lack of space. However, it is also used to report a number of
decoded trailers exceeding tune.http.maxhdr configuration which is a
fatal error.

HTTP/2 parser does not have the same problem as it only starts decoding
of trailers when output HTX buffer is empty. This is largely due to the
fact that HPACK decoding cannot be rolled back after a transient error
as it can have side effects related to dynamic table decoding. HTTP/3 is
not concerned as QPACK dynamic table capability is not implemented.

Thus, apply the same approach in HTTP/3: trailers decoding will be
started only after HTX output buffer is emptied. Flag QC_SF_DEM_FULL is
set to temporarily block demux if needed and 0 is returned from
h3_trailers_to_htx(). Decoding will be retried after a successful read
from the stream layer.

This solution is in fact already used for HEADERS when dealing with
intermediaries HTTP responses. Its impact is minimal given that trailers
usage in H3 is rare. Also, it will simplify a future QPACK dynamic table
implementation.

This must be backported up to 2.8.

2 weeks agoBUG/MAJOR: h3: always check :path for all scheme values
Amaury Denoyelle [Thu, 22 Oct 2026 15:28:38 +0000 (17:28 +0200)]
BUG/MAJOR: h3: always check :path for all scheme values

Update HTTP/3 parser so that :path pseudo-header is always checked to
reject any invalid values.

Previously, this was only performed if scheme is HTTPS or HTTP.
HAProxy's HTTP/3 is only associated with an HTTP-compatible protocol on
the other side, so removes the unnecessary condition before verifying
path conformity.

This must be backported up to 2.6.

2 weeks agoBUG/MEDIUM: server: Don't use the cached ALPN for websocket
Olivier Houchard [Fri, 16 Oct 2026 15:05:11 +0000 (17:05 +0200)]
BUG/MEDIUM: server: Don't use the cached ALPN for websocket

When using websockets, we may end up using a different ALPN than the one
we usually use with the server, so do not reuse the cached ALPN, if any,
and do not cache the one we used for that connection. Otherwise, we may
end up with websocket connections that will attempt to use the wrong
mux, and end in error.
Not making any attempt at caching the ALPN means we can't easily use
0RTT with websockets, but I do not believe this is a big problem.

This will hopefully help with github issue #3473.

This should be backported up to 3.3.

2 weeks agoDOC: filters: channel_start_analyze is called again on L7 retries
Aleksandar Lazic [Wed, 21 Oct 2026 20:25:34 +0000 (22:25 +0200)]
DOC: filters: channel_start_analyze is called again on L7 retries

The filters documentation says that the flt_start_analyze and
flt_end_analyze analyzers are called only once per stream. On a L7
retry, the response analyzers are set again when the connection is
established, so channel_start_analyze is called a second time on the
response channel, without a channel_end_analyze call in between. It is
preceded by an http_reset call on the response.

Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
2 weeks agoBUG/MINOR: http-ana: call the http_reset filter callback on L7 retries
Aleksandar Lazic [Wed, 21 Oct 2026 20:25:18 +0000 (22:25 +0200)]
BUG/MINOR: http-ana: call the http_reset filter callback on L7 retries

The filters documentation says that the http_reset callback is called
when the request is sent again to the server for a L7 retry, but it was
only called for the 1xx informational responses. It was documented with
the L7 retries in commit a254a37ad ("MEDIUM: streams: Add the ability to
retry a request on L7 failure."), but never called on this path.

do_l7_retry() now calls it for the response, once sc_reset_endp()
succeeded so that the retry is certain, and before the L7 buffer swap,
while the failed response is still in the buffer and txn->status is
set, like for the 1xx responses. The request is sent again as is from
the L7 buffer and its callbacks are not called again, so it is not
reset. The documentation now tells it.

This patch could be backported to all stable versions.

Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
2 weeks agoBUG/MINOR: promex: don't loop forever when a module skips a time series
Aleksandar Lazic [Tue, 20 Oct 2026 18:42:11 +0000 (20:42 +0200)]
BUG/MINOR: promex: don't loop forever when a module skips a time series

The fill_ts() callback of a promex module may return 0 to skip the
current time series. In this case, promex_dump_module_metrics() jumps
back to the beginning of the loop on the time series with "continue",
without calling next_ts(). So fill_ts() is called again for the same
time series, forever, until the watchdog kills the process. The "next"
label, which retrieves the next time series, was obviously meant to be
used there, but it is never reached by a jump.

The modules of HAProxy itself never skip a time series with a valid
configuration (the stick-table module only skips the tables with a
null size), but an external module doing so triggers the issue at the
first scrape of its metrics.

The issue was introduced by commit 5f875414e ("MEDIUM: promex: Dump
metrics of registered modules with a way to filter them"). This patch
must be backported as far as 3.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
2 weeks agoMINOR: cli/server: speed up proxy/server purge via the CLI
Willy Tarreau [Thu, 22 Oct 2026 13:52:29 +0000 (15:52 +0200)]
MINOR: cli/server: speed up proxy/server purge via the CLI

The following recent commits implemented support for purging servers
and backends from the CLI:

  0c6d8ee23fa5 ("MEDIUM: proxy: implement purge backend")
  d4d8a762a0fd ("MEDIUM: server: implement "purge all servers" CLI")
  410719abdc07 ("MEDIUM: server: implement "purge server" CLI")

At the moment, when it is detected that it is not possible to complete
the purge, a background task is scheduled to retry every second. However
tests show that most of the time the operation completes almost instantly
(typically less than 20ms in local tests), so it's a bit sad to wait for
a full second in this case. In addition, it requires to apply a longer
client timeout to the VTC than any previous test.

Ideally it would be nice to use a capped exponential growth for retries,
but even without going this far, a nice first approach that brings
immediate benefit is to wait a very short time before the first retry,
while keeping the 1-second delay for subsequent ones. It's easy to do
because the code that arms the initial retry is performed in the command
pasing function while the next retries are in the task itself. That's
what this commit does. Now most of these operations will be performed
in either 0.05s or 1.05s instead of 1s. And regtests are happy again.

2 weeks agoBUG/MINOR: mux_quic: fix overflow on avail_streams
Amaury Denoyelle [Thu, 22 Oct 2026 12:53:59 +0000 (14:53 +0200)]
BUG/MINOR: mux_quic: fix overflow on avail_streams

Function qcc_fctl_avail_streams() return the number of streams which can
still be opened regarding the limit set by the peer.

The return value is a 32 bits integer, used to store the substraction
result. This may overflow as flow control limit is encoded on 64 bits,
which could result either in blocked transfers, or in spurrious error as
mandatory unistreams could not be opened.

Fix this by using a temporary 64 bits value, and cap it to INT_MAX if
needed. This is sufficient as values larger than INT_MAX is out of a
real case usage.

This must be backported up to 2.8.

2 weeks agoBUG/MINOR: mux_quic: fix MAX_STREAMS limit
Amaury Denoyelle [Thu, 22 Oct 2026 12:22:32 +0000 (14:22 +0200)]
BUG/MINOR: mux_quic: fix MAX_STREAMS limit

A peer may emit a MAX_STREAMS frame to increase the window of opened
streams. Such frame is parsed via qcc_recv_max_streams() since 3.3 as it
is mandatory for backend support.

This patch fixes the maximum value authorized in MAX_STREAMS. As
specified by RFC 9000, it cannot exceed 2^60, the maximum number of
streams of a given type, as stream IDs are encoded on 62 bits with the 2
least significant bits reserved for the type.

This must be backported up to 3.3.

2 weeks agoBUG/MINOR: quic: fix "show quic" filtered connection
Amaury Denoyelle [Thu, 22 Oct 2026 09:13:19 +0000 (11:13 +0200)]
BUG/MINOR: quic: fix "show quic" filtered connection

CLI "show quic" output can be filtered to only a single connection entry
by specifying its address as argument. However, loop iteration and
filtering was incorrectly implemented, and in effect only the first
connection of each thread could be dumped this way.

Fix this by adjusting iteration on "show quic" handler. The basic idea
is similar. If <ctx.ptr> is set, it does not dump anything for
non-matching entries. As soon as the filtered entry is dumped, the
iteration is interrupted.

This must be backported up to 3.0.

2 weeks agoBUG/MEDIUM: mux_quic: fix FIN received with an already known offset
Amaury Denoyelle [Thu, 22 Oct 2026 08:04:23 +0000 (10:04 +0200)]
BUG/MEDIUM: mux_quic: fix FIN received with an already known offset

The following patch improves STREAM FIN handling on received frames by
reducing the invokation of the parser. It requires to detect the case
where FIN is received standalone without any data,

  7dd1eec2b10bd13f05c792ec0e053c19aac952ee
  MINOR: mux-quic: refine reception of standalone STREAM FIN

This introduces an issue when STREAM FIN is received with an already
known offset. In this case, FIN is not considered standalone. However,
as the offset was already handled, parser is skipped and this prevents
closure of the stream Rx channel until its timeout.

Fix this by marking FIN as standalone after skipping already received
offset. Thus, if <len> is reduced to 0 in qcc_recv(), <fin_standalone>
will be set even if it was received with a payload.

This must be backported up to 3.2, which introduced the above patch.

3 weeks agoBUG/MINOR: mux-quic: prevent send corruption on buf realign
Amaury Denoyelle [Wed, 21 Oct 2026 13:47:59 +0000 (15:47 +0200)]
BUG/MINOR: mux-quic: prevent send corruption on buf realign

For QUIC MUX emission, frames prepared for sending are not rebuilt each
time qcc_io_send() is called. This was first implemented for pacing and
the principle was generalized by the following patch, so the frame list
is now preserved if emission is interrupted on pacing, transient
transport error or flow control limitation.

  14710b5e6bf76834343d58db22e00b72590b16fe
  MEDIUM/OPTIM: mux-quic: do not rebuild frms list on every send

It is necessary to reset the frames list if some action occured during
an emission in several steps, for example when new data from upper layer
is available for sending or flow control limit is extended.

Prepared frames directly point to each QCS Tx buffer. Thus if buf
realign is performed, related frame will point to an incorrect location
in the buffer, resulting in an invalid data transfer. Note that this
issue is most of the time adverted by the fact that frames list is
resetted via qcc_send_stream() which most of the time directly follow a
realign operation.

Fix this by refreshing the frames list also during Tx buf realign. This
is simply implemented via a call to qcc_clear_frms(). It is currently
not possible to only clear the single QCS frame, as next emission would
only use already generated frames from other streams.

This must be backported up to 3.2.

3 weeks agoBUG/MINOR: jwt: enforce the ECDSA signature length in jwt_verify
Remi Tricot-Le Breton [Tue, 20 Oct 2026 08:04:50 +0000 (10:04 +0200)]
BUG/MINOR: jwt: enforce the ECDSA signature length in jwt_verify

RFC 7518 section 3.4 states that an ES256 signature MUST be a 64-octet
sequence made of R and S each on 32 bytes, and that validation fails
otherwise (96 bytes for ES384 and 132 for ES512). jwt_verify() and
jwt_verify_cert() accept any even length and split it in two halves, so
that R and S may be prefixed with any number of zero bytes.
This can be reproduced by taking a valid ES256 token, decoding its
signature, inserting a zero byte before each 32-byte half, and encoding it
again: the converter still returns 1.

Let's check that the signature has exactly the length mandated by the
algorithm in convert_ecdsa_sig(), and return JWT_VRFY_INVALID_TOKEN (-3)
otherwise, as was already done for odd lengths. A reg-test is provided.

This fix should be backported to all supported versions.

3 weeks agoBUG/MINOR: jwe: only match the exact "ALL" word in jwt.decrypt_{alg,enc}_list
Remi Tricot-Le Breton [Tue, 20 Oct 2026 08:04:49 +0000 (10:04 +0200)]
BUG/MINOR: jwe: only match the exact "ALL" word in jwt.decrypt_{alg,enc}_list

The parsing of the special 'ALL' case in "jwt.decrypt_alg_list" and
"jwt.decrypt_enc_list" did not check the actual string length so a too
short string could be wrongly treated as 'ALL'.

This was introduced in 3.4 by commit f82a242c8 ("MINOR: jwe: Add option to
enable/disable algorithms or encryption algorithms for jwt_decrypt"), so
the fix must be backported to 3.4.

3 weeks agoBUG/MINOR: jwe: reject non-empty encrypted keys with "dir" and "ECDH-ES"
Remi Tricot-Le Breton [Tue, 20 Oct 2026 08:04:48 +0000 (10:04 +0200)]
BUG/MINOR: jwe: reject non-empty encrypted keys with "dir" and "ECDH-ES"

RFC 7516 section 5.2 step 10 requires that when direct encryption ("dir")
or direct key agreement ("ECDH-ES") are used, the JWE Encrypted Key part
of the token is empty. The jwt_decrypt_jwk() converter with "dir", and
both jwt_decrypt_jwk() and jwt_decrypt_cert() with "ECDH-ES", silently
ignore this part.

This can be reproduced by inserting any base64url string (e.g. "AAAA")
between the first two dots of a "dir" or "ECDH-ES" token: the converters
still return the decrypted contents.

Let's reject the token when the encrypted key is not empty for these
algorithms.

This fix must be backported to 3.4.

3 weeks agoBUG/MINOR: jwe: only accept exact "alg" and "enc" names in JWE tokens
Remi Tricot-Le Breton [Tue, 20 Oct 2026 08:04:47 +0000 (10:04 +0200)]
BUG/MINOR: jwe: only accept exact "alg" and "enc" names in JWE tokens

The 'alg' and 'enc' parser does not check the value length so a too
short value would still be seen as the full one ("A1" instead of
"A128CBC-HS256" for instance).

This was introduced in 3.4 by commit 416b87d5d ("MINOR: jwe: Add new
jwt_decrypt_secret converter"), so the fix must be backported to 3.4.

3 weeks agoBUG/MINOR: jws: fix ECDSA_SIG leak in jws_b64_signature()
Remi Tricot-Le Breton [Tue, 20 Oct 2026 08:04:46 +0000 (10:04 +0200)]
BUG/MINOR: jws: fix ECDSA_SIG leak in jws_b64_signature()

In 'jws_b64_signature' we call 'd2i_ECDSA_SIG' which creates an
ECDSA_SIG object that was never freed.

This was introduced in 3.2 by commit 3abb428fc ("MINOR: jws: implement
JWS signing"), so the fix must be backported to 3.2.

3 weeks agoBUG/MINOR: jwt: don't truncate binary HMAC secrets at the first NULL byte
Remi Tricot-Le Breton [Tue, 20 Oct 2026 08:04:45 +0000 (10:04 +0200)]
BUG/MINOR: jwt: don't truncate binary HMAC secrets at the first NULL byte

When jwt_verify() is used with an HMAC algorithm and its secret is passed
via a variable, the secret is truncated at its first NUL byte. RFC 7518
recommends random HMAC keys at least as long as the hash, which are
commonly distributed base64-encoded and fed to haproxy through something
such as "str(<b64>),b64dec" into a variable. About one random 32-byte key
out of eight contains a NUL byte. With such a key, all legitimately signed
tokens are rejected (traffic impact for the site), and tokens signed with
only the bytes located before the first NUL are accepted instead, which
for a NUL in the first bytes of the key means that a client can forge
valid tokens after a trivial brute force. Configurations passing the
secret as a string constant are not affected since the config parser
cannot produce a NUL byte there.

This can be reproduced with this config:

    http-request set-var(txn.key) str(YQBiY2RlZg==),b64dec   # "a\0bcdef"
    http-request return status 200 hdr x-res "%[req.hdr(x-tok),jwt_verify(HS256,txn.key)]"

A token signed with "a\0bcdef" returns 0 and one signed with "a" returns 1.

This fix should be backported to all supported versions.

3 weeks agoMEDIUM: http: add the "short-status-code" violation exception
Willy Tarreau [Wed, 21 Oct 2026 06:51:18 +0000 (08:51 +0200)]
MEDIUM: http: add the "short-status-code" violation exception

Now by default we reject status codes shorter than 3 digits, though
this exception permits to continue to accept them. There is no valid
reason for accepting these and no known client currently supports
them, but it has been supported due to historical reasons that are
long outdated.

It is expected that noone will notice, and may be candidate for
backporting only to the latest LTS version after some observation
time.

3 weeks agoMINOR: http: add one control per existing HTTP violation
Willy Tarreau [Tue, 20 Oct 2026 13:21:51 +0000 (15:21 +0200)]
MINOR: http: add one control per existing HTTP violation

Following the previous patch, this one extends every place where a
check for relaxing protocol violations was permitted with a specific
tunable that can individually be enabled or disabled, so that it is
now possible to selectively enable a single (or a few) exceptions
without granting other ones.

The following ones are covered here, per direction:

 "fragment-in-url",                HTTP_VIO_URL_FRAG,         HTTP_DIR_REQ
 "non-ascii-in-url",               HTTP_VIO_URL_NON_ASCII,    HTTP_DIR_REQ
 "long-status-code",               HTTP_VIO_STATUS_LONG,      HTTP_DIR_RES
 "non-token-in-header-name",       HTTP_VIO_HDR_NAME_NON_TOK, HTTP_DIR_BOTH
 "nul-in-header-value",            HTTP_VIO_HDR_VALUE_NUL,    HTTP_DIR_BOTH
 "inconsistent-transfer-encoding", HTTP_VIO_TE_BAD,           HTTP_DIR_RES
 "unparsable-authority",           HTTP_VIO_AUTH_UNPARSABLE,  HTTP_DIR_REQ
 "invalid-connect-authority",      HTTP_VIO_AUTH_CONNECT_INV, HTTP_DIR_REQ
 "empty-authority-host",           HTTP_VIO_AUTH_EMPTY_HOST,  HTTP_DIR_REQ
 "mismatch-authority-host",        HTTP_VIO_AUTH_HOST_DIFF,   HTTP_DIR_REQ
 "invalid-version-string",         HTTP_VIO_VERSION_STRING,   HTTP_DIR_BOTH
 "invalid-websocket-key",          HTTP_VIO_WEBSOCKET_KEY,    HTTP_DIR_BOTH

Most of the time the check consisted in appending this after err_pos < -1:

     || !(global.acc_hreq_vio & (1U << HTTP_VIO_...))

For the long status code, the code was slightly reordered because the
check for the lenient case was performed first and now we must do the
strict check first (but the logic is unchanged, both were exclusive
already). The logic remains: if strict mode (default) or relaxed without
acceptance for long headers, we reject beyond 3 digits. And in any case
we continue to reject values that don't fit in 16 bits.

The doc was updated with descriptions of what each keyword corresponds
to, associated risks, and examples.

This is not much intrusive and might possibly be backported to latest
LTS if users want it.

3 weeks agoMINOR: http/config: add acceptable-http-re{quest,sponse}-violations
Willy Tarreau [Tue, 20 Oct 2026 13:03:21 +0000 (15:03 +0200)]
MINOR: http/config: add acceptable-http-re{quest,sponse}-violations

Proxies support HTTP violations relaxation in requests via option
accept-unsafe-violations-in-http-request and in responses via option
accept-unsafe-violations-in-http-response. The problem is that over
time the list of exceptions has grown quite a bit, and that a simple
application that requires to relax one tiny non-dangerous feature
such as enabling raw utf-8 in URLs, we end up enabling many other
things that can be much more dangerous.

This patch introduces a new pair of global settings that permit to
declare which of the HTTP violations are acceptable when a proxy
wants to use them. It's expected that such violations are rare
enough so that a single global setting to adjust them all process-
wide is sufficient.

What the code does is that it introduces a bit field for the request
and the response in the global section, with each bit indicating that
one violation is acceptable, and it permits via the global keyword to
replace the whole list, add an entry or remove an entry. By default,
for backwards compatibility, all exceptions are allowed, but it's
expected that most of the time users will just set one, or will keep
the defaults except some of the most dangerous ones.

For now only the declaration and parsing are handled, and only "none"
and "all" are supported. The keyword parser supports fuzzy matching
and will propose approximating names when one is misspelled.

Subsequent patches will add the options themselves.

3 weeks agoMINOR: tools: add find_word_index_in_array() to find a keyword
Willy Tarreau [Mon, 19 Oct 2026 16:22:49 +0000 (18:22 +0200)]
MINOR: tools: add find_word_index_in_array() to find a keyword

This will be used to find a word among a set in an array of
arbitrary size and layout provided that the words are always
accessible via a pointer at a fixed location in each item. The
goal here is to simplify call places which want to match a word
among a list, find the associated entry, and propose a list of
possible matches.

This will return the following values depending on the looked up
word:
  0 = not found
  1 = found a different one which looks like a candidate
  2 = found exact match

When non-zero is returned, an index pointer will designate the
associated entry in the array so that the caller can find the
context of the sought word, or at least report the mostly similar
one in the error message, like this:

    size_t idx;

    match = find_word_index_in_array(kw_array, sizeof(kw_array[0]), -1,
                                     offsetof(struct keyword, name), word, &idx);
    if (match != 2) {
            memprintf(err, "Invalid keyword '%s'.", word);
            if (match == 1)
                    memprintf(err, "%s Maybe you meant '%s' ?", *err, kw_array[idx].name);
            goto fail;
    }

3 weeks agoDOC: config: add missing acme.scheduler in the global index
Willy Tarreau [Wed, 21 Oct 2026 09:53:15 +0000 (11:53 +0200)]
DOC: config: add missing acme.scheduler in the global index

The "acme.scheduler" global directive was added in 3.2 by commit
e7574cd5f0 ("MINOR: acme: add the global option 'acme.scheduler'")
but forgot to update the keyword index, so let's do it now. This
patch can be backported to 3.2.

3 weeks agoBUILD: thread: fix builds without thread support
Olivier Houchard [Wed, 21 Oct 2026 14:55:49 +0000 (16:55 +0200)]
BUILD: thread: fix builds without thread support

Commit 4312f13a2 ("OPTIM: fd: Make each thread group's FD table NUMA-local")
added an unconditional call to thread_bind_to_cpu_map() from init_pollers(),
but the helper is only defined with USE_THREAD. Building with TARGET=generic
thus fails with an implicit declaration and an undefined reference.

Add a no-op stub alongside the other non-threaded helpers.

3 weeks agoMINOR: ssl: Add new 'pha' server option
Remi Tricot-Le Breton [Tue, 20 Oct 2026 14:16:40 +0000 (16:16 +0200)]
MINOR: ssl: Add new 'pha' server option

This backend option allows post handshake authentication for the given
backend. This simply consists in adding a "post_handshake_auth"
extension in the ClientHello that tells the server that PHA is
supported. We only enable it for backends that have an explicit
certificate provided so that this certificate gets always used in
subsequent CertificateRequest messages sent by the server.
This does not work with AWS-LC (PHA unsupported) or WolfSSL which would
support it but only when WolfSSL is built with
WOLFSSL_POST_HANDSHAKE_AUTH defined (disabled by default).

3 weeks agoOPTIM: fd: make each thread group's polled_mask NUMA-local
Olivier Houchard [Tue, 20 Oct 2026 13:25:19 +0000 (15:25 +0200)]
OPTIM: fd: make each thread group's polled_mask NUMA-local

The polled_mask array was left aside the fdtab's NUMA placement:
its slices are not padded to the page size, so a page can be shared
between two groups, and calloc() zeroes it eagerly on the boot
thread when the allocation is served from the heap. Allocate it
with mmap() and pad the slices as the fdtab: nothing is copied
nor zeroed, the pages are born zeroed and each group's masks are
written by its own threads, bound before they first touch them, so
the lazy faulting places them on the right node by itself.

3 weeks agoOPTIM: fd: Make each thread group's FD table NUMA-local
Olivier Houchard [Thu, 30 Jul 2026 17:42:17 +0000 (19:42 +0200)]
OPTIM: fd: Make each thread group's FD table NUMA-local

With per-thread-group FD tables, the tables are allocated and zeroed,
hence faulted in, by the main thread, and the kernel's first-touch
placement then places every group's table on the main thread's NUMA
node, making the other groups' fdtab accesses, including the atomics
done for every FD event, run on remote memory.

Allocate the tables with mmap() instead, so that their pages are only
faulted in when they are first written, and make these first writes
come from the right CPUs: the boot thread is bound to group 0's CPUs
before any FD is inserted, and to each other group's CPUs around that
group's copy of the boot table, so that every table stays local to its
own node. Each group's slice is padded to the kernel's page size so
that no page is shared between two groups. Only matters with
per-group FD tables.

3 weeks agoMINOR: thread: split out thread_bind_to_cpu_map()
Olivier Houchard [Wed, 7 Oct 2026 12:41:12 +0000 (14:41 +0200)]
MINOR: thread: split out thread_bind_to_cpu_map()

set_thread_cpu_affinity() binds the current thread to the CPUs that
the cpu_map assigns to it. The next commit will need to bind to
another thread's CPUs, around the first touch of an allocation, so
split a thread_bind_to_cpu_map(tgrp, ltid) helper out of it, binding
the current thread to the CPUs assigned to local thread <ltid> of
thread group <tgrp> (0-based). set_thread_cpu_affinity() keeps its
master-process check and delegates to the new function.

3 weeks agoBUG/MEDIUM: mux_quic: ensure FIN is reported after Rx data transfer
Amaury Denoyelle [Tue, 20 Oct 2026 13:48:15 +0000 (15:48 +0200)]
BUG/MEDIUM: mux_quic: ensure FIN is reported after Rx data transfer

Multiple Rx buffers may be allocated per QCS instance. Each buffer is
parsed separately. If necessary, data may be transferred from the next
buffer when the parser is blocked in the middle of a frame.

This operation causes an issue if only two buffers remains and all data
with FIN are already received. In this case, QCS is not yet remotely
closed during qcc_recv(). If all content from the next buffer is
transferred, close is still not set. This prevents HTX EOM from being
set, which result in a stream closed after timeout.

Fix this by calling qcs_close_remote() in qcs_transfer_rx_data() if the
above scenario described is encountered. This is sufficient to produce
HTX EOM signal as parsing will be restarted via a new call to app_ops
rcv_buf with <fin> parameter now set.

If the next buffer has been emptied but is not truncated, it is still
reinserted in the rxbuf tree. This could result in a second
qcs_close_remote() call for the same QCS which is forbidden. Thus, this
patch depends on the below fix to prevent this situation.

  1dcaae1caf62b97789e77acb18c7b22753391d45
  BUG/MEDIUM: mux_quic: fix crash on QCS double remote close

This should be backported up to 3.2 after a period of observation, as
multiple Rx buffers were implemented there.

3 weeks agoBUG/MEDIUM: mux_quic: fix crash on QCS double remote close
Amaury Denoyelle [Wed, 21 Oct 2026 07:34:57 +0000 (09:34 +0200)]
BUG/MEDIUM: mux_quic: fix crash on QCS double remote close

A QCS instance must only be closed remotely once. A BUG_ON() enforces
it, so that FIN signalling to the upper layer is correct.

There exists a possible case of a double remote closure after data
transfer between two adjacent buffers on parser blocking. This only
happens if the next buffer is emptied and a standalone FIN is then
received. First qcs_close_remote() will be called during the empty
STREAM FIN reception. This is called again on qcc_decode_qcs() after
consuming data when dealing with the empty remaining buffer.

This patch fixes this by preventing the second call to
qcs_close_remote() in case an empty buffer is detected.

This bug is not easily reproducible as it is highly dependent on the
precise order of these different operations.

This must be backported up to 3.2, as this is the first release which
support multiple QCS Rx buffers.

3 weeks agoBUG/MEDIUM: task: do not destroy twice the tasks already killed in mworker_cleantasks()
William Lallemand [Wed, 21 Oct 2026 09:45:32 +0000 (09:45 +0000)]
BUG/MEDIUM: task: do not destroy twice the tasks already killed in mworker_cleantasks()

Since 9228fabe34 ("BUG/MEDIUM: task: Do not destroy a task that is not ours
in task_destroy"), task_destroy() calls task_kill() for a task that is not
owned by the current thread. task_kill() is not idempotent: it starts with
BUG_ON(state & TASK_KILLED), so a task which was already flagged as killed
while still being linked in a run or wait queue cannot be destroyed again.

On the 3.0 and 2.8 branches this is reachable by simply starting haproxy in
master-worker mode with a localpeer, a peers section and a program section:
the forked program child calls mworker_cleanlisteners(), which destroys the
peers sync task, then mworker_cleantasks() walks the run queue and destroys
it a second time, aborting the child with SIGILL before it reaches execvp().
The program section no longer exists on this branch, but the underlying
invariant is still violated: a task flagged TASK_KILLED by an earlier
task_kill() is deliberately left in its queue (that is how the scheduler
knows it must free it on dequeue), so mworker_cleantasks() can always stumble
upon one. Make the walk robust instead of relying on the callers never
handing it a doomed task.

Fix this by ignoring the tasks that are already flagged TASK_KILLED when
walking the queues from mworker_cleantasks(). Those tasks don't need to be
destroyed again as they're already doomed: the scheduler frees them as
soon as it dequeues them (see run_tasks_from_lists()). The check is applied
to every queue walked there (the shared run queue, and the per-thread run
and timer queues).

This must be backported to all stable branches.

3 weeks agoBUG/MINOR: cfgparse/peers: warn and prevent undefined behavior when peers is missing...
Aurelien DARRAGON [Thu, 8 Oct 2026 18:04:31 +0000 (20:04 +0200)]
BUG/MINOR: cfgparse/peers: warn and prevent undefined behavior when peers is missing bind info

As described in GH #3516, declaring a peers section containing the local
peer server (with server syntax) without the matching 'bind' diretive
causes haproxy to crash since 879c78c. However, as Amaury demonstrated,
the config is invalid/incomplete: it makes no sense to declare the local
peer server without the matching bind parameters. Before this patch
haproxy would let the config pass and the result was that the peers
proxy was created without any listeners. While it somehow didn't crash
in the past (perhaps the low level code was robust enough), from a
peers logic point of view it results in the peers proxy doing nothing.

Instead of silently failing (and now risking crashes upon reload), let's
proactively report a fatal error to the user that such config is
incomplete.

Many thanks to @alexandret-ik for the detailed report.
It should be backported to 3.4, and may be relevant to olders versions
but in this case replace the ha_alert() which ha_notice() so existing
configs that used to start (despite not valid from peers point of view)
don't break.

3 weeks agoBUG/MINOR: h3: fix crash with maxrewrite on response headers
Amaury Denoyelle [Tue, 20 Oct 2026 12:18:32 +0000 (14:18 +0200)]
BUG/MINOR: h3: fix crash with maxrewrite on response headers

This patch is similar to the previous one. This time it enforces HTTP
headers maxrewrite setting on responses parsing from the server to the
client. Thus, this change is related to the backend side handling.

As a side effect, it also prevents a BUG_ON() crash on
qcs_http_rcv_buf(). Indeed, maxrewrite is also used by the stream layer
to reduce the destination buffer. Thus, htx_xfer() may be interrupted
due to a lack of space after setting HTX_FL_PARSING_ERROR flag. This
triggers the BUG_ON() in qcs_http_rcv_buf(). This does not happen on
frontend side with requests as HTX buffer is passed to the newly created
stream instance without rcv_buf invocation.

This should be backported up to 3.3.

3 weeks agoBUG/MINOR: h3: enforce maxrewrite on request headers
Amaury Denoyelle [Tue, 20 Oct 2026 09:45:19 +0000 (11:45 +0200)]
BUG/MINOR: h3: enforce maxrewrite on request headers

Muxes are responsible to ensure that at least maxrewrite space remains
in the stream buffer after headers decoding. This was missing in HTTP/3
decoder.

Add an extra check on maxrewrite size after H3 HEADERS request decoding.
The connection is closed with INTERNAL_ERROR if on excess. A future
patch may implement stream-level error instead.

This should be backported up to 2.6.