]> git.kaiwu.me - haproxy.git/log
haproxy.git
7 days agoMINOR: http/hardening: implement hardening "reject-header-trailer-folding"
Willy Tarreau [Tue, 3 Nov 2026 15:52:07 +0000 (16:52 +0100)]
MINOR: http/hardening: implement hardening "reject-header-trailer-folding"

Rejects requests and responses which contain folded header fields or
trailer fields. Folding consists in having a single field span multiple
lines. This practice is obsoleted by modern HTTP standards, absolutely
not seen anymore in valid messages over the internet but might still be
present in a few antique internal services dealing with large cookies.
On untrusted networks, they are always associated with attacks trying
to inject headers into non-compliant components, and should be rejected
as a sign of break-in attempt. Note that HAProxy does not have problems
with them and systematically unfolds these messages before forwarding
them.

7 days agoMINOR: http/hardening: add "delete-all-trailer-fields"
Willy Tarreau [Tue, 3 Nov 2026 15:32:38 +0000 (16:32 +0100)]
MINOR: http/hardening: add "delete-all-trailer-fields"

Deletes from requests and responses any trailer field, regardless of
its content or syntax. This is done after syntactic checks though. The
vast majority of public facing applications do not use trailers because
they are not reliable (intermediaries are allowed to drop or fold them)
and their presence on public networks is generally associated with
attack attempts. It is generally safe to drop them by default on public
facing listeners, and even on servers meant to deliver public contents,
in order to avoid any information leakage. Note that gRPC services use
them.

7 days agoMINOR: http/hardening: add "delete-unregistered-trailer-fields"
Willy Tarreau [Tue, 3 Nov 2026 15:25:49 +0000 (16:25 +0100)]
MINOR: http/hardening: add "delete-unregistered-trailer-fields"

This is the same as for the headers, but this time for the trailers. Better
keep them separated as trailers are used even worse than headers these days.

Similarly as for the underscore, unwanted trailers are dropped early for
h1.

7 days agoMINOR: http/hardening: add "delete-unregistered-header-fields"
Willy Tarreau [Tue, 27 Oct 2026 15:29:23 +0000 (16:29 +0100)]
MINOR: http/hardening: add "delete-unregistered-header-fields"

Deletes from requests and responses any header field whose name starts
with 'x-' (upper or lower case). These header fields are not registered
with IANA and are not used across the internet by standard components.
They may appear in untrusted requests in attacks trying to exploit some
backend framework weaknesses however, as well as in old browsers that
were trying to work around known server bugs (e.G. "X-Pad"). While it
would be risky to block such requests, it is safe and even recommended
to delete such headers from inbound internet requests. It is preferable
not to do it on responses however when applications uses such header
fields to report information to HAProxy.

7 days agoMINOR: http/harderning: implement "delete-underscore-in-trailer-name"
Willy Tarreau [Tue, 3 Nov 2026 15:19:39 +0000 (16:19 +0100)]
MINOR: http/harderning: implement "delete-underscore-in-trailer-name"

This is the same as for the headers, but this time for the trailers. Better
keep them separated as trailers are used even worse than headers these days.

Note that in h1 we delete them when we detect them since we already have
the code for this, no need to index them and delete them later.

7 days agoMINOR: http/harderning: implement "delete-underscore-in-header-name"
Willy Tarreau [Tue, 27 Oct 2026 14:53:06 +0000 (15:53 +0100)]
MINOR: http/harderning: implement "delete-underscore-in-header-name"

Deletes from requests and responses any header field whose name
contains an underscore character ('_' or \x5C). While this character is
perfectly permitted, some application frameworks which create automatic
variables already turn the dash ('-') into an underscore in variable
names, leading to an ambiguity with the underscore which can result in
the same variable name. This could be abused to bypass some
access-control policy for example. No standard HTTP header uses this,
so it is recommended to enable on untrusted listeners. Note that this
applies only after "reject-underscore-in-header-name" was accepted.

7 days agoMINOR: http/hardening: implement hardening "reject-underscore-in-trailer-name"
Willy Tarreau [Tue, 3 Nov 2026 15:12:54 +0000 (16:12 +0100)]
MINOR: http/hardening: implement hardening "reject-underscore-in-trailer-name"

This is the same as for the headers, but this time for the trailers. Better
keep them separated as trailers are used even worse than headers these days.

7 days agoMINOR: http/hardening: implement hardening "reject-underscore-in-header-name"
Willy Tarreau [Tue, 27 Oct 2026 14:37:52 +0000 (15:37 +0100)]
MINOR: http/hardening: implement hardening "reject-underscore-in-header-name"

Rejects requests and responses in which one header field name contains
an underscore character ('_' or \x5C). This character is perfectly
permitted, but some application frameworks which create automatic
variables tend to already turn the dash ('-') into an underscore in
variable names, leading to an ambiguity with the underscore which can
result in the same variable name. This could be abused to bypass some
access-control policy for example. No standard HTTP header uses this,
so it is safe to enable on untrusted listeners.

7 days agoMINOR: http/hardening: implement hardening "reject-lowercase-method"
Willy Tarreau [Tue, 27 Oct 2026 14:16:23 +0000 (15:16 +0100)]
MINOR: http/hardening: implement hardening "reject-lowercase-method"

Rejects requests whose method contains a lower-case character. No
standard HTTP method uses lower-case characters and these ones are
essentially used in desynchronization attack scenarios, by trying to
exploit different case matching schemes along the chain. This is
recommended on untrusted listeners. This is implemented directly in the
h1 parser, as well as in http_method_has_forbidden_char() for h2 and
h3. H2 now uses this function instead of rolling its own loop.

7 days agoMINOR: http/hardening: implement hardening "reject-duplicate-host-header"
Willy Tarreau [Mon, 26 Oct 2026 18:53:06 +0000 (19:53 +0100)]
MINOR: http/hardening: implement hardening "reject-duplicate-host-header"

Rejects requests containing more than one "Host" header field, even if
valid and identical. This is recommended on untrusted listeners. Note that
even if not rejected, duplicate host header fields are folded into a
single one. This was done for h1/h2/h3 which all had the explicit
check to perform deduplication.

In h2 and h3 we also had to make sure not to count host+authority as a
duplicate, so that requires a host counter as a local var, but that's
cheap.

Note that h3 currently doesn't check that duplicate headers are strictly
equal, only the first one is kept.

7 days agoMINOR: http: add the "set http-hardening" CLI command
Willy Tarreau [Tue, 27 Oct 2026 16:43:44 +0000 (17:43 +0100)]
MINOR: http: add the "set http-hardening" CLI command

"set http-hardening <name> <option> {on|off}" enables or disables an option
of a profile at runtime, including the "all" keyword to change everything
at once, e.g. in an emergency. Options may also be specified by the bit
number that appears in -dKcfg or in "show http-hardening". The option is
looked up like in the section parser so that a suggestion is made on typos.
The profile is only read by the other threads, so it is updated while the
threads are isolated, which avoids any locking on the fast path. The
command is restricted to the admin level.

7 days agoMINOR: h3: pass the HTTP hardening checks to the h3s and h3c
Willy Tarreau [Fri, 30 Oct 2026 10:29:45 +0000 (11:29 +0100)]
MINOR: h3: pass the HTTP hardening checks to the h3s and h3c

This adds an http_hdng_chk into h3s. In h3_attach() we preset
it to the h3c's profile's mask, which is found during h3c initialization
from the connection side's target profile (from server or bind_conf). If
no hardening profile is found ("auto" or not set), then we check the
source address of the connection. If it's a non-local IPv4/v6, then
we use the untrusted profile, otherwise we fall back to the trusted one.

Contrary to h2, h3s is allocated before parsing (like h1s), so we can
place all pre-initialization in h3_attach() without having to preset any
fields on h3s before parsing: in h3_rcv_buf(), we just have to preset
and collect http_hdng_chk, like for h1 again.

7 days agoMINOR: mux-h2: pass the profile's HTTP hardening checks into the h2c and h2s
Willy Tarreau [Thu, 29 Oct 2026 16:14:35 +0000 (17:14 +0100)]
MINOR: mux-h2: pass the profile's HTTP hardening checks into the h2c and h2s

This adds an http_hardening profile pointer to h2c and an
http_hdng_chk into h2s. In h2_init() we preset the global hardening
profile depending on the side (note that the side might be reversed with
rhttp, but the pointer still points to a bind_conf or server so it remains
valid), and if no profile is found ("auto" or anything else) then we check
the source address to see if it's a non-local IPv4/v6, in which case we
use the untrusted profile, otherwise we fall back to the trusted one.

In h2s_new() we preset the h2s's hardening rules to the mask defined by
the profile pointed to by the h2c. This will be used as-is by the backend,
but in h2c_frt_handle_headers(), we have to preset the global hardening
profile to the h2c's mask before proceeding. If the stream is accepted
and created, we then copy what we collected to h2s for later consumption
(not implemented yet).

7 days agoMINOR: mux-h1: pass the profile's HTTP hardening checks into the h1c and h1s
Willy Tarreau [Thu, 29 Oct 2026 14:25:50 +0000 (15:25 +0100)]
MINOR: mux-h1: pass the profile's HTTP hardening checks into the h1c and h1s

This adds the pointer to the chosen http_hardening profile to h1c, and
sets an http_hdng_chk on h1s. In h1c_init() we simply use the
connection's target to get the hardening profile. It is either a server,
or a listener, in which case we access its bind_conf's profile. If none
is found, we check if we have an untruted source address and set it to
untrusted, otherwise we fall back to trusted. Then we can preset the
http_hdng_chk rules in h1s from the mask of the profile pointed
to by h1c.

For now the returned information, if set, are not exploited.

7 days agoMINOR: http: define http_hdng_chk for ambient hardening checks to be run
Willy Tarreau [Thu, 29 Oct 2026 14:14:45 +0000 (15:14 +0100)]
MINOR: http: define http_hdng_chk for ambient hardening checks to be run

In order to enforce HTTP hardening checks, we have to instrument functions
at many different layers of the HTTP parsing code. First tests show that
we might need to add extra parameters to numerous functions, just to
forward requirements and results.

A simpler, less elegant but much more efficient approach consists sharing
the parsing checks globally (local to the thread) so that anywhere in the
HTTP stack it is possible to know what checks are enforced. It is not
different from the global acceptable violations after all, except that
this has to be preset, consumed and cleared by the caller. The benefit
is that it will then be possible to define the checks before parsing,
and let any function anywhere in the chain participate to the operation,
including for reporting possible anomalies.

The http_harden(flag) function does the repetitive part of the check and
returning what final action was performed and should be easy to deploy.

7 days agoDOC: config: document the "http-hardening" section and server/bind options
Willy Tarreau [Mon, 26 Oct 2026 18:18:19 +0000 (19:18 +0100)]
DOC: config: document the "http-hardening" section and server/bind options

Add section 12.10 describing the "http-hardening" section, the implicit
"untrusted" and "trusted" profiles and the "all" keyword, and document the
"http-hardening" option of "bind" and "server" lines. For now we do'nt
have other examples aside "all" in the section but the goal is that its
usage now starts with a few options.

Also take this opportunity for defining the meaning of "trusted" and
"untrusted" in this context.

7 days agoMINOR: tools: add is_ip_untrusted() to check for non-local addresses
Willy Tarreau [Tue, 3 Nov 2026 13:57:53 +0000 (14:57 +0100)]
MINOR: tools: add is_ip_untrusted() to check for non-local addresses

The function will return true for non-local ipv4/ipv6 addresses only.
Some are between the two (benchmark, doc, examples, cgn etc) and are
intentionally reported as non-local hence untrusted as well, the
rationale being that they're normally not deployed for production so
they shouldn't be taken for trusted local addresses. This will be
used in future commits.

7 days agoMINOR: server: add the "http-hardening" server keyword
Willy Tarreau [Mon, 26 Oct 2026 17:47:28 +0000 (18:47 +0100)]
MINOR: server: add the "http-hardening" server keyword

The new "http-hardening" server keyword sets the HTTP hardening profile
that applies to the responses coming from the server and to the requests
sent to it. The server now holds a pointer to the profile, which may be
NULL if unset or explicitly set to "auto". It is inherited from
default-server, server templates and "from" like any other setting, and
may be set at runtime with "add server". The profile must have been
declared before.

7 days agoMINOR: listener: add the "http-hardening" bind keyword
Willy Tarreau [Mon, 26 Oct 2026 17:39:23 +0000 (18:39 +0100)]
MINOR: listener: add the "http-hardening" bind keyword

The new "http-hardening" bind keyword sets the HTTP hardening profile that
applies to the connections accepted by the bind line. The bind_conf now
keeps a pointer to the profile set by the keyword, or NULL if not set or
if explicitly set to "auto".

7 days agoMINOR: cli/http: add the "show http-hardening" CLI command
Willy Tarreau [Mon, 26 Oct 2026 17:21:33 +0000 (18:21 +0100)]
MINOR: cli/http: add the "show http-hardening" CLI command

Without argument, "show http-hardening" lists all known profiles, including
the implicit "untrusted" and "trusted" ones when they were not declared (they
are then marked "(implicit)"). With a profile name, it shows each known
single-bit hardening option of this profile, along with its number (bit
position plus 1), direction among "req", "res", "-" (for bidir) and the
status ("on" or "off"), so that it is easy to know what is enabled.
Keywords covering a set of options such as "all" are not shown.

7 days agoMINOR: cfgparse: dump the "http-hardening" keywords with -dKcfg
Willy Tarreau [Mon, 26 Oct 2026 17:08:40 +0000 (18:08 +0100)]
MINOR: cfgparse: dump the "http-hardening" keywords with -dKcfg

The keywords of the "http-hardening" section are not registered via the
cfg_kw_list mechanism but through their own table in http.c, so they were
missing from the output of "-dKcfg". Let's have cfg_dump_registered_keywords()
report them by calling dump_http_hardening_kws() once the other sections
were dumped. The function also reports the option numbers for pure options
(those which are not composite).

7 days agoMINOR: http: support default hardening names "untrusted" and "trusted"
Willy Tarreau [Mon, 26 Oct 2026 19:41:09 +0000 (20:41 +0100)]
MINOR: http: support default hardening names "untrusted" and "trusted"

And also reserve "auto" for future use. Both "untrusted" and "trusted"
have a global storage and are available all the time. They're just not
registered in the tree by default so that we'll find them once missed
in the tree lookup. If a section with these names is created, it
inherits their settings. This will allow new default settings to
change over time, with minimal maintenance effort for the users.

They are preset to the default permissive rules.

7 days agoMINOR: cfgparse/http: add a simple parser for new section "http-fingerprint"
Willy Tarreau [Mon, 26 Oct 2026 16:43:22 +0000 (17:43 +0100)]
MINOR: cfgparse/http: add a simple parser for new section "http-fingerprint"

Just like the resolvers parser, this is a whole section parser. It handles
both the http-fingerprint keyword that creates the section, and all
registered hardening options supported there. The format is:

    http-fingerprint <name> [from <origin>]
         <option> [on|off]
         ...

A section may only be declared once, and the origin must already exist if
mentioned (like with defaults). Like with http-violations, keywords are
resolved using fuzzy matching so that an approaching name will suggest
the correct one.

For now, no keywords beyond "all" are declared, and nothing is done with
these sections.

7 days agoMINOR: http: introduce the notion of http-hardening flags and sections
Willy Tarreau [Mon, 26 Oct 2026 16:07:13 +0000 (17:07 +0100)]
MINOR: http: introduce the notion of http-hardening flags and sections

The goal here will be to declare http-hardening sections which enumerate
controls that can be relaxed or enforced. The control names will be stored
in a table just like the http-violations, associated with a mask. They
will possibly permit to have names for sets that cover multiple bits. For
now only 32 bits are planned, covering both front/back and req/resp. This
should be sufficient for a start and we'll see once the limit is reached
what's the best way to proceed.

One difficulty here is to reason in terms of what is not permitted.
So the way to think about it is to associate each verb to an action
to be performed, such as reject-xxx, delete-xxx, adjust-xxx, block-xxx.

7 days agoMINOR: h1: indicate the trailers state to h1_parse_msg_tlrs()
Willy Tarreau [Tue, 3 Nov 2026 17:49:11 +0000 (18:49 +0100)]
MINOR: h1: indicate the trailers state to h1_parse_msg_tlrs()

In order to parse H1 trailers, h1_parse_msg_tlrs() builds a local dummy
h1m that sets H1_MF_HDRS_ONLY and calls h1_headers_to_hdr_list() which
directly jumps to plain headers parsing. However, this latter function
has no way to adjust its error processing on header or trailer because
the information regarding the fact that headers or trailers are being
parsed isn't present. Currently this is not a problem but it will become
in order to refine error processing to have separate rules for headers
and trailers.

This patch sets the dummy h1m's state to H1_MSG_TRAILERS so that the
called function can safely rely on it. All other callers pass a real
message with a valid state, so it will be possible to check h1m->state
during error processing to figure if the function is in the process of
parsing trailers or headers.

7 days agoMINOR: http: use ist_find_http_ctl() instead of ist_find_ctl()
Willy Tarreau [Wed, 4 Nov 2026 16:37:11 +0000 (17:37 +0100)]
MINOR: http: use ist_find_http_ctl() instead of ist_find_ctl()

At a few places we were using ist_find_ctl() as an accelerator to locate
possibly forbidden CTL chars, but that one doesn't detect the DEL char,
so now that we have the new ist_find_http_ctl(), let's use it instead.
This is not tagged as a bug because nothing at this point is made with
this char so this doesn't make a difference at this point.

7 days agoMINOR: ist: add ist_find_outside() to find offending characters
Willy Tarreau [Wed, 4 Nov 2026 10:28:25 +0000 (11:28 +0100)]
MINOR: ist: add ist_find_outside() to find offending characters

For some syntax checks it's interesting to find offending characters
that are outside an interval. Unfortunately for now we only limit
outselves to searching control chars < 0x20 but that's not always
sufficient. Let's just implement a fast scan for characters outside a
range (min and max may not be more than 0x7f apart though). Just like
ist_find_range(), it reads one word at a time and finishes per byte
when found or on false positives. It's optimized for non-matching
inputs.

7 days agoMINOR: ist: add ist_find_http_ctl() to match http CTL chars
Willy Tarreau [Wed, 4 Nov 2026 16:29:02 +0000 (17:29 +0100)]
MINOR: ist: add ist_find_http_ctl() to match http CTL chars

Some tests in the HTTP code involve ist_find_ctl() hoping to find any
HTTP CTL char, but CTL for ist is 0x00..0x1F only (and documented as
such so we won't change it). For HTTP, this also includes 0x7F which
these tests are missing.

It's not realistic to do a second full scan on those data for something
that never happens. Better introduce a new function which scans for
0x00..0x1F,0x7F, that's much more efficient. It's constructed exactly
like the existing one with a extra check for 0x7F by adding 1 to
generate a carry in this case as well. It's expected to cost roughly
2 extra cycles per word, which is OK.

7 days agoBUG/MINOR: ist: make ist_find_range() accept non-zero min boundaries
Willy Tarreau [Wed, 4 Nov 2026 10:09:24 +0000 (11:09 +0100)]
BUG/MINOR: ist: make ist_find_range() accept non-zero min boundaries

The way ist_find_range() is currently constructed can make it miss the
lower bound. For example, checking "abcdefgh1jklmnop" against min='0'
and max='9' misses the '1' in the middle because the check only sees
bytes that are below (max-min), not those between min and max (the
min value is not subtracted before the comparison). It's OK in the
byte-at-a-time check however.

Let's just simplify the operation to make it work more like the
is_char8_below_opt() and is_char8_above_opt() functions. The idea
is to check (x-min) and (max-x) and combine them to check for the
carry. However there's still the case of testing that all bytes are
outside the interval (we don't want to miss one). The trick here
is to or the left term with the carry so that we keep the carry for
bytes outside and it only disappears when all bytes are ouside the
range. That way the loop stops when there's one carry, indicating
at least one byte within range, or one which had bit 7 previously
turned on (which was already mentioned as the exception), in which
case we finish one byte at a time. The only restriction is that
min <= max <= 0x7f, but that's already how the function was meant
to be used.

This can be backported, though it will not fix any bug because at
the moment, all callers pass 0 to the min value and are not affected.

7 days agoDOC: config: add missing empty line before tune.ssl.ktls-unsafe
Willy Tarreau [Wed, 4 Nov 2026 21:09:47 +0000 (22:09 +0100)]
DOC: config: add missing empty line before tune.ssl.ktls-unsafe

The word wouldn't have been indexed without it. No backport is needed,
this is just 3.5.

7 days agoBUG/MEDIUM: ssl: disable kTLS on OpenSSL versions affected by a known bug
Olivier Houchard [Wed, 4 Nov 2026 12:42:28 +0000 (13:42 +0100)]
BUG/MEDIUM: ssl: disable kTLS on OpenSSL versions affected by a known bug

With vanilla OpenSSL, a blocked kTLS record keeps referencing the caller's
buffer and the library retries from it, even though
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is set; the H2 fast-forward reuses that
buffer, so streams get corrupted. Upstream fixed this in 3.4.6 and 3.5.7, so
disable kTLS on older versions and report it, with "tune.ssl.ktls-unsafe" to
opt in anyway.

To be backported to 3.3 and 3.4.

7 days agoBUG/MEDIUM: haload: restore request accounting after user timeouts
Olivier Houchard [Mon, 2 Nov 2026 15:57:00 +0000 (16:57 +0100)]
BUG/MEDIUM: haload: restore request accounting after user timeouts

The user path drops expired streams without releasing their active-stream
count or their tot_rconns slot, unlike the stream-error path. Once -r is
reached with fewer completions, running_tasks never falls back to zero and
haload hangs until it is killed.

Release both there, matching the error path; the timeout is still counted
as an error. This was introduced with haload in 3.5-dev, no backport needed.

7 days agoBUG/MINOR: haterm: fix the response body phase on short writes
Olivier Houchard [Wed, 4 Nov 2026 15:14:28 +0000 (16:14 +0100)]
BUG/MINOR: haterm: fix the response body phase on short writes

hstream_add_ff_data() computed the offset of the next body byte as
(req_size - len) % modulo, where <len> is the length offered by the mux
for that call. A short write or a partially filled output buffer makes it
smaller than the number of bytes still to send, so the offset no longer
matched what had already been emitted: the repeating pattern jumped and
the peer received a corrupted body, which happened as soon as a proxy read
the response with its own buffer boundaries. Use hs->to_write instead.

Introduced with ecf36f2ca ("MEDIUM: haterm: Add support for 0-copy data
forwading and option to disable it"), present only in the development
branch, so no backport is needed.

7 days agoMINOR: stream: add per-stream memory slots
William Lallemand [Fri, 30 Oct 2026 11:56:50 +0000 (12:56 +0100)]
MINOR: stream: add per-stream memory slots

Add stream_reserve_memslot() to reserve pointer-aligned per-stream storage
during single-threaded initialization, before streams exist. Access the
reserved slots with stream_get_memslot().

Reserved storage is zeroed at stream creation, preserved for the stream's
lifetime and released on destruction.

7 days agoBUG/MINOR: resolvers: validate TXT RDATA before reading
maxml [Wed, 4 Nov 2026 10:16:46 +0000 (10:16 +0000)]
BUG/MINOR: resolvers: validate TXT RDATA before reading

maxml reported on the public HAProxy mailing list that the DNS response
validator reads the first TXT character-string length byte before checking
that the record contains any RDATA.

When a TXT answer has RDLENGTH zero and ends exactly at bufend, the generic
RDATA bounds check accepts the empty range. The TXT parser then reads
reader[0] with reader == bufend, before rejecting the record. Short-circuit
evaluation of the subsequent condition does not protect this earlier read.

The report includes a minimized 31-byte DNS response which triggers an
AddressSanitizer one-byte heap-buffer-overflow with an exactly sized
libFuzzer input. All 13 original AFL++ crash inputs reach the same read.

The production caller uses a DNS_MAX_MSG_SIZE + 1 stack buffer and parses
at most DNS_MAX_MSG_SIZE bytes, so this read remains within the allocation.
The loaded value is discarded when the zero-length check fails. No
production crash or information disclosure was demonstrated.

Check that RDATA contains at least one byte before reading its first
character-string length. A valid empty TXT string remains accepted: its
RDLENGTH is one and its character-string length byte is zero.

This was introduced by e418e828aa ("MINOR: resolvers: basic TXT record
implementation"). It should be backported to 3.4. Older stable branches
are not affected.

8 days agoBUG/MINOR: httpclient/lua: raise an error on too many request headers
Martin Mirchev [Fri, 30 Oct 2026 05:49:13 +0000 (13:49 +0800)]
BUG/MINOR: httpclient/lua: raise an error on too many request headers

When a Lua httpclient request has more header values than
tune.http.maxhdr, hlua_httpclient_table_to_hdrs() pops the inner
lua_next() key and value and returns NULL. But the key and value of the
outer lua_next() loop on the headers table stay on the stack, so the
caller's lua_next() on the argument table gets the wrong key and the
request fails with a confusing error:

    runtime error: invalid key to 'next' from [C]: in method 'get'

The header values already duplicated with istdup() are also leaked.

Let's free them and raise an explicit Lua error instead, as the caller
does for the other invalid arguments:

    runtime error: too many headers (tune.http.maxhdr is 101)

The function is now marked __LJMP and its call wrapped in MAY_LJMP().

This should be backported where 2db801c63 ("BUG/MINOR: hlua: fix stack
overflow in httpclient headers conversion") was backported. Before 3.5
the function is in src/hlua.c.

8 days agoBUG/MINOR: httpclient/lua: fix stack overread when copying the headers
Martin Mirchev [Fri, 30 Oct 2026 05:45:12 +0000 (13:45 +0800)]
BUG/MINOR: httpclient/lua: fix stack overread when copying the headers

hlua_httpclient_table_to_hdrs() collects the header values of a Lua
httpclient request into a stack array of tune.http.maxhdr entries, then
copies hdr_num + 1 entries into the allocated result, although the
array never contains a terminator: the result is allocated with calloc()
and its last entry is set to IST_NULL right after the copy.

Since commit 2db801c63 ("BUG/MINOR: hlua: fix stack overflow in
httpclient headers conversion") hdr_num may reach tune.http.maxhdr, and
the copy then reads one entry past the end of the array. E.g. with the
default value:

    local v = {}
    for i = 1, 101 do v[i] = "v" .. i end
    hc:get{ url = "http://127.0.0.1:8080/", headers = { ["x-many"] = v } }

ASan reports a dynamic-stack-buffer-overflow READ of size 3264 (102
entries) in hlua_httpclient_table_to_hdrs(). With fewer values, the
extra entry is uninitialized stack, which is then overwritten.

Let's only copy the hdr_num collected entries.

This should be backported to all stable versions with the Lua
httpclient (2.5 and above), on top of 2db801c63 where it was
backported. Before 3.5 the function is in src/hlua.c.

8 days agoBUG/MEDIUM: ssl/cli: don't unlock the certificate lock after a failed trylock
William Lallemand [Tue, 3 Nov 2026 16:53:09 +0000 (16:53 +0000)]
BUG/MEDIUM: ssl/cli: don't unlock the certificate lock after a failed trylock

When ckch_store_load_payload() fails to acquire ckch_lock, it jumps to
the common cleanup path, which unconditionally releases the lock. This
unlocks another operation's lock and returns success because errcode is
still zero. When the actual owner subsequently unlocks, the lock state
underflows and later certificate commands remain blocked.

Return ERR_ALERT | ERR_FATAL immediately on failed lock acquisition. No
temporary resources have been allocated at this point, so cleanup is
unnecessary.

This regression was introduced in 3.4-dev11 by commit efe6c97488, which
extracted ckch_store_load_payload() from the CLI parser. This fix should
be backported to 3.4 only. Versions 3.3 and older return directly on
failed lock acquisition and are not affected.

8 days agoBUG/MEDIUM: ssl/cli: reset the ongoing transaction when "commit ssl cert" is interrupted
Chris Staite [Mon, 2 Nov 2026 10:00:44 +0000 (10:00 +0000)]
BUG/MEDIUM: ssl/cli: reset the ongoing transaction when "commit ssl cert" is interrupted

"commit ssl cert" builds the new instances in several calls of its I/O
handler: it yields every 10 instances, and whenever the output buffer is
full. If the applet is released in between, for example because the CLI
client closed the connection, cli_release_commit_cert() frees the new
ckch_store of the transaction, but ckchs_transaction.new_ckchs,
ckchs_transaction.old_ckchs and ckchs_transaction.path still reference
it. The next "set ssl cert", "commit ssl cert", "abort ssl cert" or
"show ssl cert" on this certificate then uses the freed store, which may
crash the process or corrupt memory.

Only an admin of the CLI can trigger it, but it doesn't take much: a
certificate used by more than 10 instances (e.g. in a crt-list loaded by
several bind lines) and a client which doesn't wait for the end of the
commit are enough.

The release function is only called with a new store still attached when
the transaction is being abandoned, the success path having consumed it
in CERT_ST_INSERT, so the transaction is reset there. On the error path,
CERT_ST_FIN already reset it and doing it again is harmless.

"commit ssl ca-file" and "commit ssl crl-file" seem to follow the same
pattern in cli_release_commit_cafile() and cli_release_commit_crlfile(),
this will be addressed separately.

This should be backported to all stable branches. Before 3.4, the release
function is the same but frees ctx->new_ckchs itself instead of calling
ckch_store_update_cleanup().

8 days agoBUG/MINOR: ssl/ckch: free the new instance when ckch_inst_rebuild() fails
Chris Staite [Mon, 2 Nov 2026 10:00:56 +0000 (10:00 +0000)]
BUG/MINOR: ssl/ckch: free the new instance when ckch_inst_rebuild() fails

ckch_inst_rebuild() creates the new ckch_inst of a "commit ssl cert" with
its sni_ctx and SSL_CTX, then prepares the SSL_CTX of the server or of
the bind line. When this preparation fails it returns an error to
ckch_store_rebuild_instances() which only returns -1, so the new instance,
which is not linked anywhere yet, is leaked with its SSL_CTXs, and with
everything they reference (the certificate, the chain, the key, the CA
file links).

The impact is a memory leak for each failed commit, which requires an
admin to commit a certificate that doesn't fit its bind or server lines.

Free the instance with ckch_inst_free() and reset the pointer on these
two error paths. The instance is not in any tree nor list at this point,
which ckch_inst_free() handles. The first error path, after
ckch_inst_new_load_store() or ckch_inst_new_load_srv_store(), is already
fine since these functions never leave an instance behind on failure.

This should be backported to all stable branches, the function exists
since 2.5.

8 days agoBUG/MINOR: filters: Reject id-less implicit instances of multi-instance classes
Christopher Faulet [Tue, 3 Nov 2026 08:09:19 +0000 (09:09 +0100)]
BUG/MINOR: filters: Reject id-less implicit instances of multi-instance classes

An id is mandatory when a filter class allows several instances per proxy,
and this was verified when a filter-config directive was parsed. But it was
not when an implicit instance was created by flt_add_implicit_instance()
called from another directive: for such a class, an instance without id
could not be distinguished from a second one.

Like for an explicit declaration, the implicit declaration is now reported
and ignored if no id is provided for such a class. All the in-tree
directives creating implicit instances of a multi-instance class (the
cache-store rules) already pass the cache name as id, so nothing changes for
existing configurations.

The filters documentation was updated accordingly.

No backport needed.

8 days agoBUG/MEDIUM: mux-h1: don't drop pending output on a short content-length
Olivier Houchard [Fri, 30 Oct 2026 15:04:33 +0000 (16:04 +0100)]
BUG/MEDIUM: mux-h1: don't drop pending output on a short content-length

When a fixed-length message is truncated and output data is still pending
in the H1 output buffer, h1s_destroy() flags the connection in error
because we got less data than expected, and so h1_send() will refuse to
send the remaining data, and not everything will be transmitted.

Now, when a truncated message is detected while output data is still
pending, H1C_F_ERROR is not set immediately, we just avoid adding it to
the idle pool, and the connection will be closed later when everything
has been sent.

To release the connection once the output buffer is drained, the
condition in h1_process() that only matched delayed silent shutdowns is
generalized to any connection in CLOSING state with an empty output
buffer. This covers both silent shutdowns and the short content-length
case. The connection is still not released while a stream remains
attached, since h1_close() keeps it in CLOSING until the stream is
detached.

This should be backported where 75dd2c18dda29f9893574ef982ddf66de16f0408
has been backported, up to 2.8.

8 days agoREGTESTS: compression: check hard to compress contents and nearly full buffers
Willy Tarreau [Sat, 31 Oct 2026 08:21:13 +0000 (09:21 +0100)]
REGTESTS: compression: check hard to compress contents and nearly full buffers

Add reg-tests/compression/output_margin_*.vtc, with a Lua service in
output_margin.lua, which sends bodies that are incompressible or barely
compressible (random bytes, bytes that cost 9 bits each with the fixed
tree, random data mixed with text, random data with short matches, runs
of 9-bit literals separated by 4-byte matches) or repetitive. The service
serves the cases of the set named in the request in order, so that the
vtest clients just loop over the same request. Each response must be
compressed with gzip, and is decompressed by the client, which checks the
gzip trailer's CRC and length, then compares the body length to the one
announced in x-len.

The tests are split into several files so that vtest runs them in
parallel, and tagged slow:
  - output_margin_{whole,buf,small,odd}.vtc send all patterns through 4 kB
    buffers, whole or by calls of 4000, 1 to 64 or 1 to 20000 bytes;
  - output_margin_full_{random,run51}.vtc use 16 kB buffers and
    tune.maxrewrite 32, so that the buffer may be almost full when the
    filter compresses it, with bodies of 16088 to 16116 bytes sent in one
    call. With the default maxrewrite, the channel always keeps far more
    room than the deflater could ever need, which is why this does not
    happen with default settings. These sizes still leave enough room for
    the deflater, so they must be compressed as well (responses of 16144
    bytes or more are not compressed at all with this setup).

Before the fixes, the last two crashed haproxy in comp_http_payload()
(htx_get_blk_type() on the NULL block returned by htx_replace_blk_value())
for all sizes from 16086 to 16118 bytes, which is the crash reported since
3.5-dev10. Each file takes about as long as a trivial test (0.25 to 0.3s
without ASAN here, 0.33 to 0.42s with ASAN).

8 days agoMINOR: flt-http-comp: size the compression reserve using SLZ_MAX_OVERHEAD()
Willy Tarreau [Sun, 1 Nov 2026 15:36:20 +0000 (16:36 +0100)]
MINOR: flt-http-comp: size the compression reserve using SLZ_MAX_OVERHEAD()

htx_compression_buffer_init() reserves room for the deflater's output based
on what it would emit as stored blocks. libslz now publishes in slz.h the
room each call to slz_encode() needs beyond its input length,
SLZ_MAX_OVERHEAD(len), which includes the stored blocks, the end of the
stream, the bits queued by previous calls and the up to 40 bytes its fixed
huffman encoding may lag behind stored blocks. Let's use it so that the
reserve follows slz's own settings instead of a local estimate.

All the DATA blocks of a pass are queued by the slz backend and compressed
in a single call when the last one is flushed, so the call covers at most
the whole buffer. The reserve is thus based on the buffer size, which keeps
it constant (52 bytes for buffers up to 64 kB), plus 14 bytes for the finish
(queue, last block and gzip trailer, a flush needs 10) and 10 bytes for the
gzip header: 76 bytes for 16 kB buffers.

The previous computation is kept for zlib, which publishes no such bound,
and when linking with an older external libslz which doesn't define the
macro. In all cases the result of htx_replace_blk_value() remains checked
since commit 5ab337485 ("BUG/MEDIUM: flt-http-comp: check the block
returned by htx_replace_blk_value()"). Crafted bodies made of runs of
literals >= 144 separated by short references, as well as random data,
sent in chunks of 256 bytes to 64 kB, all decompress correctly.

This is not a bug fix, only a way to better tie haproxy to slz's settings,
and it must not be backported.

8 days agoMINOR: compression: pass the content type to the algorithms' init function
Willy Tarreau [Sun, 1 Nov 2026 21:33:14 +0000 (22:33 +0100)]
MINOR: compression: pass the content type to the algorithms' init function

Now that we have slz 1.4-dev4 with support for text-aware compression
using an alternate huffman tree, let's implement support for enabling it.
In order to permit this, the algorithms' init() function now takes a new
argument, <ctype>, which is either COMP_CT_GENERIC (0) for unknown or
binary contents, or COMP_CT_TEXT (1) for text contents.

comp_prepare_compress_request() and select_compression_response_header()
set it to COMP_CT_TEXT when the Content-Type header starts with "text/",
regardless of the "compression type" lists. The config-time checks of the
algorithms use COMP_CT_GENERIC.

The slz algorithms select SLZ_CT_TEXT after their init for text contents.
libslz has no dedicated macro for the content types, but they were
introduced right after SLZ_MAX_OVERHEAD(), which is thus used to detect an
external libslz that supports them. zlib and identity ignore the argument.

On an 81 kB HTML file, the response is 22.0 kB when served as text/html
instead of 25.4-27.1 kB when served as application/octet-stream at level 1
(the size depends on how the server sends the data).

8 days agoIMPORT: slz: update slz to version 1.4-dev4
Willy Tarreau [Tue, 3 Nov 2026 08:14:08 +0000 (09:14 +0100)]
IMPORT: slz: update slz to version 1.4-dev4

This refreshes the slz library to 1.4-dev4, fixing a crash bug and
bringing a small number of improvements:
  - faster (vectorizable) adler32 checksum calculation for zlib
    ("deflate" token in accept-encoding)
  - small performance improvements on compression (size and speed)
  - support for a special compression scheme for text (~-15...-20%)

8 days agoOPTIM: flt-http-comp: only flush the compressor after the last DATA block
Willy Tarreau [Sun, 1 Nov 2026 13:39:14 +0000 (14:39 +0100)]
OPTIM: flt-http-comp: only flush the compressor after the last DATA block

comp_http_payload() used to call the compressor's flush() after each DATA
block, which emits a synchronization point (an EOB and an empty stored
block, up to 5 bytes) and closes the current huffman block each time.
Worse, with slz, and before recent commit a0b37fb470 ("BUG/MEDIUM:
compression: fix incompatibility between compression and large-buffers"),
add_data() only queued the data and flush() is where they were compressed,
so each block was compressed on its own, without being able to reference
the previous ones, since slz keeps no context between calls.

Now flush() is only called when no other DATA block follows in this
pass, so that the client still gets everything while we're waiting for
more data. With zlib, which compresses in add_data(), the last bits are
simply kept for the next block. With slz, all DATA blocks of the pass
are queued (the first one by reference, the following ones copied into
a contiguous buffer, as add_data() already does) and compressed at once
with their whole context. The first block's data are still referenced
after the block is removed or emptied, which is fine since this does not
touch the data area, and they are copied when the second block is added.

The difference is only visible when several DATA blocks are processed
in the same pass, e.g. a 1 MB HTML response made of 256-byte chunks
shrinks from 405057 to 402030 bytes (-0.7%) at level 1.

8 days agoBUG/MINOR: compression: pass the configured level to slz
Willy Tarreau [Sun, 1 Nov 2026 20:23:40 +0000 (21:23 +0100)]
BUG/MINOR: compression: pass the configured level to slz

Since its integration in 1.6 with commit 418b8c0c4 ("MAJOR: compression:
integrate support for libslz"), the slz backend only knew levels 0 and 1,
so the init functions forced the level to 0 or 1 using !!level, and the
rate limiting code only raised the level back up to 1. Since slz 1.4-dev3
was imported in commit 32582812a ("IMPORT: slz: update slz to version
1.4-dev3"), slz supports levels 1 to 3 (higher ones behave as 3), but
tune.comp.maxlevel was still capped to 1.

Let's pass the configured level to slz's init functions, and let the rate
limiting code raise the level back up to tune.comp.maxlevel.

Note that configurations with tune.comp.maxlevel above 1 will now use the
higher slz levels, which compress better but consume more CPU.

This must be backported wherever commit 32582812a is backported.

8 days agoBUG/MINOR: flt-http-comp: check the block returned by htx_replace_blk_value()
Willy Tarreau [Sat, 31 Oct 2026 07:57:19 +0000 (08:57 +0100)]
BUG/MINOR: flt-http-comp: check the block returned by htx_replace_blk_value()

comp_http_payload() replaces the value of a DATA block with the deflater's
output and directly passes the result to htx_get_next_blk(), without checking
it. htx_replace_blk_value() returns NULL when htx_prepare_blk_expansion()
finds not enough room for the new value (delta > htx_free_space()). Normally
this cannot happen, but it seldom does during development or improvements
on the compression libraries, and causes crashes that could just be turned
into errors. The decompression code already performs the test, so let's do
the same and stay safe in front of such occasional early bugs. It's not
necessary to backport this since it doesn't trigger with stable code.

8 days agoCLEANUP: tools: make inetaddr_host_lim*() usable on untrusted inputs
Willy Tarreau [Tue, 3 Nov 2026 07:49:52 +0000 (08:49 +0100)]
CLEANUP: tools: make inetaddr_host_lim*() usable on untrusted inputs

inetaddr_host_lim() and inetaddr_host_lim_ret() were introduced in 1.4.4
by commit d54bbdce87 ("[MINOR] add very fast IP parsing functions") as
fast parsers for some specific use cases. They expect a delimiter after
the IP address (cr/lf/nul/whatever, in fact, anything not a digit), and
this is how they're currently used. As noted by Md. Raisul Islam Mobin,
they could also be misused by external code not aware of this limitation
which by then saved a few CPU cycles per address at an era where they
were still precious, so let's test the end pointer before reading the
digit to make it more future-proof. If this speed still matters, these
ones could probably be rewritten to benefit from automatic vectorization
on SIMD-capable machines.

8 days agoBUG/MEDIUM: compression: fix incompatibility between compression and large-buffers
Willy Tarreau [Mon, 2 Nov 2026 17:42:14 +0000 (18:42 +0100)]
BUG/MEDIUM: compression: fix incompatibility between compression and large-buffers

The SLZ compression being stateless, it doesn't keep data between calls
and must be called with the largest blocks at once to be efficient. When
it was added, muxes were not there yet and chunks were parsed inside the
HTTP forwarding analyzer, which would call comp->add_data() for each chunk
of data. In case of small chunks, that would just not compress, so a local
buffer was added and data were temporarily stored there and effectively
compressed when calling ->flush().

But with the introduction of large buffers, it became a problem because
the locally allocated buffer can be too small if the data to be compressed
comes from a large one. One solution to this is to also pre-allocate a
large buffer, but since there needs to be one per thread and that some
users might use extremely large buffers on large systems, that could be
extremely expensive in terms of memory.

A better solution is to just simplify all of this as this complexity is
no longer needed at the era of muxes which extract chunks and make a
large HTX data block with them. That's what this patch does, and the
functions become as simple as their zlib counterparts, no longer require
a local per-thread buffer, and will be even faster thanks to the removal
of a long-unneeded extra copy.

This fix will need to be backported to 3.4 where large buffers were
introduced, maybe after a period of observation. An alternate fix in
the mean time could be to reject configs having both compression and
large buffers.

8 days agoBUG/MINOR: sample: realign the ungrpc converter on message boundaries
Frederic Lecaille [Tue, 3 Nov 2026 06:47:54 +0000 (07:47 +0100)]
BUG/MINOR: sample: realign the ungrpc converter on message boundaries

sample_conv_ungrpc() walks the GRPC messages of the body with two
variables: <pos>, the read cursor, and <grpc_left>, the bytes left.
They are supposed to describe the same position.

When protobuf_field_lookup() fails, it does not always leave <pos> at the
end of the message. For a length-delimited field it stops on the value,
after the key and the length. But the loop still subtracts the whole
message length from <grpc_left>, without moving <pos>. The cursor and the
counter then describe different parts of the input, and the next
iteration reads a "gRPC header" from the previous message's payload.

A 23-byte body shows it:

  00 0000000a 0a 08 00 00 00 00 03 08 b9 0a 00 00000003 08 8f 4e

ungrpc(1,int64) returns 1337, which is the value of the first message's
field. The second message holds the real field 1 = 9999 but is never
reached. The counter can also over-count, and skip real messages.

Save the end of the message rigght after the "grpc_left < left"
check, then set <pos> to it when the lookup fails, so the next iteration
starts on a real message boundary.

Nothing changes on the success path, nor for fully parseable bodies,
where the lookup already stops at the message end. The multi-message walk
is intended for streaming bodies and is kept.

This is not a memory-safety issue: <pos> and the counter advance by the
header together, and the lookup moves forward by at most the message
length, which is the same amount removed from the counter.

This is also not a policy bypass like a4f5eea841 ("BUG/MEDIUM: protobuf:
fix nested path bypass in field lookup"). There, a flat sibling field
could satisfy a nested path that the configuration explicitly
restricted, so the client gained a capability it did not have. Here the
client writes the whole body and can already obtain any value with a
single well-formed message. The bug only makes the proxy read a value
that is the field of no message of the body, while the backend reads
another one. Hence MINOR and not MEDIUM.

The loop was written this way when the converter was added in 2.0
(1fceee8316) and did not change since 5f33f85ce8, apart from the
unaligned access fix. Not a regression. Backport to 2.0 and above.

Thank you to Owais Lone for having reported this bug and provided this
patch.

Must be backported to all stable versions.

Reported-by: Owais Lone <thesecguy>
8 days agoCLEANUP: quic: update the outdated comment of quic_tls_derive_keys()
Frederic Lecaille [Thu, 29 Oct 2026 14:38:16 +0000 (15:38 +0100)]
CLEANUP: quic: update the outdated comment of quic_tls_derive_keys()

The comment still refers to a <ctx> parameter which no longer exists and
claims that the derived key, IV and header protection key have the same
size, which the code below contradicts. Describe the actual derivation
from RFC 9001 5.1 instead.

8 days agoCLEANUP: quic: remove the unneeded quic_rx.h include from quic_tls.c
Frederic Lecaille [Thu, 29 Oct 2026 14:33:23 +0000 (15:33 +0100)]
CLEANUP: quic: remove the unneeded quic_rx.h include from quic_tls.c

None of the declarations of haproxy/quic_rx.h is used by quic_tls.c.
Let's remove the include.

8 days agoCLEANUP: quic: remove duplicated quic_cstream_new() declaration
Frederic Lecaille [Thu, 29 Oct 2026 14:26:55 +0000 (15:26 +0100)]
CLEANUP: quic: remove duplicated quic_cstream_new() declaration

The function prototype is declared twice in include/haproxy/quic_conn.h.
Let's remove the second one.

8 days agoCLEANUP: quic: remove the stale initial_salt declaration
Frederic Lecaille [Thu, 29 Oct 2026 14:23:12 +0000 (15:23 +0100)]
CLEANUP: quic: remove the stale initial_salt declaration

No symbol named initial_salt exists anymore: the Initial salts are the
per-version initial_salt_draft_29, initial_salt_v1 and initial_salt_v2,
reached through the initial_salt member of struct quic_version. Let's
remove the leftover declaration.

8 days agoCLEANUP: quic: remove the unused count and flags from struct quic_tls_kp
Frederic Lecaille [Thu, 29 Oct 2026 14:20:21 +0000 (15:20 +0100)]
CLEANUP: quic: remove the unused count and flags from struct quic_tls_kp

The count and flags members of struct quic_tls_kp are only initialized by
quic_tls_kp_init() and read nowhere: the key phase state is kept in the pn
member of the TLS secrets and in the flags member of the TLS context.
Let's remove them.

8 days agoCLEANUP: quic: remove the unused key parameter from quic_tls_decrypt()
Frederic Lecaille [Thu, 29 Oct 2026 14:08:28 +0000 (15:08 +0100)]
CLEANUP: quic: remove the unused key parameter from quic_tls_decrypt()

The key parameter of quic_tls_decrypt() and quic_tls_decrypt2() is never
read: the key is installed in the cipher context by
quic_tls_rx_ctx_init(), only the IV differs per packet. Dead since the
cipher contexts became reusable (commit f4605748f).

8 days agoBUG/MINOR: quic: fix the header protection key length dump with AWS-LC
Frederic Lecaille [Thu, 29 Oct 2026 13:49:16 +0000 (14:49 +0100)]
BUG/MINOR: quic: fix the header protection key length dump with AWS-LC

Under QUIC_AEAD_API, tls_hp() returns EVP_CIPHER_CHACHA20, which is an
EVP_AEAD cast to EVP_CIPHER. Applied to it, EVP_CIPHER_key_length() reads
an unrelated field and returns 1 instead of 32, so
quic_tls_keys_hexdump() only dumps one byte of the header protection key.
quic_tls_derive_keys() has the same computation but is guarded by
 #ifdef QUIC_AEAD_API. Move that special case to a helper in quic_tls.h
and use it for both.

This only affects the trace output. Measured on AWS-LC 5.9.0: 1 instead
of 32 for ChaCha20, AES-128 and AES-256 unaffected.

Should be backported to all stable versions even if only the QUIC traces
are impacted.

8 days agoCLEANUP: quic: remove unused tx crypto size and offset counters
Frederic Lecaille [Thu, 29 Oct 2026 13:28:28 +0000 (14:28 +0100)]
CLEANUP: quic: remove unused tx crypto size and offset counters

The sz and offset members of the tx crypto substructure of
quic_enc_level are written but never read: the buffered CRYPTO data are
located with the bufs array and the sz member of each quic_crypto_buf.
Let's remove them.

8 days agoCLEANUP: quic: remove the unused tx part of struct quic_cstream
Frederic Lecaille [Thu, 29 Oct 2026 13:14:16 +0000 (14:14 +0100)]
CLEANUP: quic: remove the unused tx part of struct quic_cstream

struct quic_cstream carries a tx substructure with offset, sent_offset
and buf members, inherited from the qcs object it was derived from to
handle CRYPTO streams. Nothing ever reads them: the TX side of the
CRYPTO stream is handled by the tx crypto buffers of its encryption
level for the data, and by the stream descriptor attached to the
cstream. Only quic_cstream_new() sets the three members and no reader
exists anywhere in src/ or include/.

They were already unread when they were introduced (commit 7e3f7c47e
"MINOR: quic: New quic_cstream object implementation"), so let's remove
them along with their initialization.

8 days agoCLEANUP: quic: remove duplicated rx offset initialization in quic_cstream_new()
Frederic Lecaille [Thu, 29 Oct 2026 10:51:33 +0000 (11:51 +0100)]
CLEANUP: quic: remove duplicated rx offset initialization in quic_cstream_new()

<cs->rx.offset> is set to 0 twice in a row. Let's drop the second useless
assignment.

8 days agoMINOR: quic: use the right trace event in quic_tls_key_update()
Frederic Lecaille [Thu, 29 Oct 2026 10:47:16 +0000 (11:47 +0100)]
MINOR: quic: use the right trace event in quic_tls_key_update()

The next RX/TX secret lengths are reported with QUIC_EV_CONN_SPPKTS,
whose handler expects a <struct quic_tx_packet *>, while the rest of the
function uses QUIC_EV_CONN_KP event. TRACE_PRINTF() bypasses the source
callback through <&trace_no_cb>, so nothing crashed: the message was
just filtered under the wrong event.

8 days agoCLEANUP: quic: remove duplicated key phase pointer assignments
Frederic Lecaille [Thu, 29 Oct 2026 10:44:51 +0000 (11:44 +0100)]
CLEANUP: quic: remove duplicated key phase pointer assignments

<nxt_rx> and <nxt_tx> are already initialized from <qc->ku> at
declaration time in <quic_tls_key_update()>, and <qc->ku> is not
modified in between, so the two assignments placed after TRACE_ENTER()
are dead duplicates.

9 days agoBUG/MEDIUM: leastconn: reset the smallest key when backups take over
Aleksandar Lazic [Mon, 2 Nov 2026 17:00:00 +0000 (18:00 +0100)]
BUG/MEDIUM: leastconn: reset the smallest key when backups take over

Since commit 9fe72bba3 ("MAJOR: leastconn; Revamp the way servers are
ordered."), the lookup starts at the smallest key recorded in
lbprm.lb_seq, and this key is shared by the active and the backup
trees. Under load it follows the keys of the active servers.

When the last active server goes down or into maintenance, the key is
only reset if it equals the key of that server, which is often not the
case under load. With "option allbackups" the backup tree is then
looked up from that key, and the idle backup servers, whose key is 0,
are not found. fwlc_get_next_server() returns NULL, the streams are
queued in the backend, and without maxconn nothing dequeues them: every
request ends with a 503 after "timeout queue", although a backup server
is up. It only recovers when a server is queued at a lower key again.
Without "option allbackups" the first backup server is used directly
and the problem does not happen.

Fix it by resetting the smallest key when the last active server leaves
the tree.

This fixes GitHub issue #3555. It can be reproduced with an active and
a backup server, "balance leastconn" and "option allbackups", by
setting the active server to maintenance under load.

This should be backported as far as 3.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
9 days agoMINOR: http-act: Add 'no-fail' option on 'wait-for-body' action
Christopher Faulet [Mon, 2 Nov 2026 20:20:02 +0000 (21:20 +0100)]
MINOR: http-act: Add 'no-fail' option on 'wait-for-body' action

With this option, the "wait-for-body" action does not return an error at the
end of the timeout but continues the message analysis with the body received
so far, instead of responding with a 408 "Request Timeout" error to the
client on the request side or a 504 "Gateway Timeout" error on the response
side. The remaining body bytes, if any, are streamed to the server or to the
client once received.

9 days agoDOC: configuration: Clarify wait-for-body doc for the response side
Christopher Faulet [Mon, 2 Nov 2026 20:24:31 +0000 (21:24 +0100)]
DOC: configuration: Clarify wait-for-body doc for the response side

The documentation was too request-oriented and did not mention the 504
"Gateway Timeout" error returned to the client when the timeout occurs while
waiting for a response body. It is now side-neutral and explicitly mentions
the 408 and the 504 errors returned on timeout, depending on the side.

9 days agoBUG/MINOR: http-ana: fix response compression for applets
Willy Tarreau [Mon, 2 Nov 2026 16:01:34 +0000 (17:01 +0100)]
BUG/MINOR: http-ana: fix response compression for applets

In 3.5-dev6, commit 1a99a7c0f4 ("MINOR: flt-http-comp: Check request
headers for response compression earlier") was needed to make sure
that processing of the accept-encoding request header happened in the
suitable order when both compression offload and decompression are
used at the same time. While theoretically it's no longer needed now
with the updated filter sequences merged into -dev9, it still uncovered
an interesting vestigal construct that we could carbon-14 date to 2014
in 1.5-dev24, precisely with this commit: 70730dddbd ("MEDIUM: http:
enable analysers to have keep-alive on stats").

By then, applets were not yet autonomous objects outside of the stream,
but were entirely part of the stream and were not much compatible with
analyzer processing, with a few exceptions, and the stream (by then
called a session) was reset to a pre-parsing state after each response
to process the next one, resulting in great difficulties explaining
why only select analyzers were kept and the other ones dropped.

In 3.5 all of this is old history and it's a bit surprising and even
refreshing to find living witnesses of this antique era, having managed
to both adapt to and resist natural evolution.

What happened with the 3.5-dev6 patch above is that function
select_compression_request_header() is no longer called by the
compression code during requests, and is expected to be called by the
rest of the natural request processing flow. But the antique code that
only keeps a subset of analyzers didn't care about ANA_REQ_HTTP_INNER
by then since applets didn't need that, and since it's the only place
which can still make sure that this function is called, the result
is that the accept-encoding header is no longer inspected for applets,
because either it was a frontend and the flag was already missing, or
it was a backend and that flag which was set for the backend was reset
by that code. Because of this, compression of applet responses (stats,
cache, promex, Lua services) is silently disabled since 3.5-dev6.

Upon careful inspecting, it turns out that all flags that were added
by the antique code (AN_REQ_FLT_HTTP_HDRS, AN_REQ_HTTP_XFER_BODY etc)
are all properly set by http_process_common() which is called on
AN_REQ_HTTP_INNER. Thus the fix consists in removing all that old
code to stop purging flags, and only enabling AN_REQ_HTTP_INNER when
applets are branched from frontends, so that the flag is always set,
and that the rest of the processing is properly performed in both
cases.

With this patch, sending curl --compressed to a config with only
a frontend or only a backend containing "stats uri /" properly
returns compressed contents again.

No backport is needed since it only affects 3.5-dev6 (3.4 and older
still work fine thanks to the code that the patch above dropped).

Incidentally, regtest legacy-implicit-order.vtc needed to be fixed
because it was written after the change above, when the cache's
output wasn't compressed by the existing rules. Now with the fix,
the cache's output gets compressed again, so adding a separate
proxy for this allows to distinguish the request processing.

9 days agoBUG/MEDIUM: proxy: fix stick table expired entries cleanup on stopping
Amaury Denoyelle [Mon, 2 Nov 2026 13:34:42 +0000 (14:34 +0100)]
BUG/MEDIUM: proxy: fix stick table expired entries cleanup on stopping

Since the following patch, proxy internal management task is only
created for frontends or if a stick-table is present. This notably
serves to purge expired entries from tables on soft-stop.

  a4a17eb36698c927de1c3171236fcc5a6997721a
  OPTIM/MINOR: proxy: do not init proxy management task if unused

However, the condition was incorrectly written, as it is never true for
the stick-table case. This is because table's <current> member is always
zero at parse time as the stick-table is still empty. Thus, the task is
not allocated for backends even if stick-table is declared. This results
in worker processes potentially leaving expired entries in stick-tables.

The error originates from reusing the condition from manage_proxy(),
which performs the stick-table cleanup. In this context it makes sense
to check <current> value.

Fix the condition to ensure the task is allocated when a stick-table is
declared. Checking the proxy's <table> member is sufficient for that.

This fixes github issue #3554.

This must be backported up to 3.4.

9 days agoCI: github: cache VTest binary in FreeBSD workflow
William Lallemand [Mon, 2 Nov 2026 10:42:06 +0000 (10:42 +0000)]
CI: github: cache VTest binary in FreeBSD workflow

The FreeBSD job rebuilds VTest2 from source on every run. The shared
setup-vtest action cannot be used directly because it runs on the Ubuntu
host and would produce a Linux binary rather than a FreeBSD one.

Restore and save the workspace-relative vtest directory on the host with
actions/cache. Build into that directory inside the guest and explicitly
enable copyback so the resulting FreeBSD binary is available to the save
step after the VM exits.

Use the same release and architecture variables for the VM and cache key.
Keep the FreeBSD namespace separate from other jobs, and include the
upstream VTest SHA, build-script hash and a manual version suffix to
invalidate incompatible entries. Skip both cache steps if the SHA lookup
returns no output.

Probe the cached binary with -V and rebuild if it is missing or unusable.
Print the selected version and pass its absolute path to the full
regression suite. Save only after the VM step succeeds and the restore
was not an exact hit; retain the existing build and failure diagnostics.

9 days agoBUILD: github: run FreeBSD CI on every branch
William Lallemand [Mon, 2 Nov 2026 10:59:34 +0000 (10:59 +0000)]
BUILD: github: run FreeBSD CI on every branch

The FreeBSD workflow was the only workflow in .github/workflows to
restrict itself to the master and next branches. Pull request branches,
merged topic branches and tags were therefore never built or tested on
FreeBSD, unlike macOS and Alpine, which are part of the push-triggered
VTest matrix. As a result changes touching platform specific code could
be merged without any FreeBSD validation.

Drop the branches filter so the workflow triggers on every push, as the
VTest, Windows and Contrib workflows already do. The existing job
condition still limits the run to the haproxy repository, or to a manual
workflow_dispatch, so forks are unaffected.

9 days agoCI: github: add AWS-LC FIPS testing workflow
William Lallemand [Fri, 23 Oct 2026 11:51:51 +0000 (11:51 +0000)]
CI: github: add AWS-LC FIPS testing workflow

The FIPS branches of AWS-LC were covered by the VTest matrix, but only
built in non-FIPS mode: nothing exercised the FIPS-specific code paths in
HAProxy, in particular the compliance checks in src/fips.c which reject
non-approved ciphers, curves, signature algorithms and TLS versions at
configuration time.  A regression there is invisible until someone builds
HAProxy against an AWS-LC FIPS library.

Add a nightly workflow that builds AWS-LC with FIPS=1 and runs the VTest
and unit test suites against it, in the same way the other SSL library
workflows do.  It tests three sources, resolved through
determine_aws_lc():

  - AWS-LC 3.x, the fips-2024-09-27 branch;
  - AWS-LC 4.x, the fips-2025-09-12-lts branch;
  - the latest AWS-LC release tag, whose FIPS module is the one currently
    submitted for validation, so that the newest code is covered too
    instead of only the two maintenance branches.

Branch specs resolve to the head commit of the branch, which is passed as
AWS_LC_VERSION and downloaded as an immutable archive by build-ssl.sh, so
a run is reproducible and the build cache is only renewed when the branch
advances.  The resolved release and commit are both part of the cache
key, matching the version-specific identity used by the VTest job so the
two workflows never share a half-populated cache entry.

The workflow only runs on the upstream repository, or on manual dispatch
from a fork, like the other SSL library workflows.

Note that the AWS-LC latest release is not expected to be identical to
either FIPS branch; it is built in FIPS mode for its own FIPS module
version, which is what the extra job is meant to detect early.

9 days agoREGTESTS: ssl: skip AWS-LC FIPS rejected crypto in ciphersuites/curves tests
William Lallemand [Fri, 23 Oct 2026 11:50:51 +0000 (11:50 +0000)]
REGTESTS: ssl: skip AWS-LC FIPS rejected crypto in ciphersuites/curves tests

Four reg-tests deliberately configure cryptographic material that
HAProxy's FIPS compliance checks reject when the SSL library runs in FIPS
mode, so they can never start a configuration there:

  - ssl_ciphersuites.vtc and quic/ssl_ciphersuites.vtc configure the
    TLS_CHACHA20_POLY1305_SHA256 ciphersuite, which is not FIPS-approved;
  - ssl_curves_selection.vtc, quic/ssl_curves_selection.vtc and
    ssl_curve_name.vtc, quic/ssl_curve_name.vtc configure the X25519
    group, which is likewise rejected.

Each then fails while parsing the configuration rather than on the
behaviour under test, which is why ssl_fips_check_ciphersuites(),
ssl_fips_check_curves() and their callers in src/ssl_sock.c exist in the
first place; the rejection is the expected outcome of the check, not a
defect in the test.

Skip those files with the fips_mode() config condition predicate already
used elsewhere in reg-tests/ssl (see "REGTESTS: ssl: skip tests broken by
FIPS mode"), so that they keep running against every non-FIPS SSL library
including OpenSSL and plain AWS-LC, and are only skipped when FIPS is
actually active.

9 days agoBUG/MEDIUM: ssl: skip TLS 1.3 suites in the FIPS TLS 1.2 cipher check
Chris Staite [Sun, 1 Nov 2026 19:57:37 +0000 (19:57 +0000)]
BUG/MEDIUM: ssl: skip TLS 1.3 suites in the FIPS TLS 1.2 cipher check

With AWS-LC in FIPS mode, ssl_fips_check_ciphers() walks the list returned
by SSL_CTX_get_ciphers() and rejects any entry whose key exchange is not
ECDHE (or DHE). But AWS-LC documents that this list includes the TLS 1.3
cipher suites as well, and for these SSL_CIPHER_get_kx_nid() returns
NID_kx_any since TLS 1.3 suites do not specify the key exchange. Every
TLS 1.3 suite is thus reported as a non-FIPS cipher, including the
FIPS-approved ones that are set by default:

  FIPS mode active but non-FIPS cipher(s) configured:
  'TLS_AES_128_GCM_SHA256', 'TLS_AES_256_GCM_SHA384'.

Since the internal httpclient server is checked as well, HAProxy refuses
to start in FIPS mode with any configuration, even one without a single
SSL setting. This was observed with AWS-LC FIPS 4.0.0.

TLS 1.3 suites are already validated against their own list by
ssl_fips_check_ciphersuites(), so ssl_fips_check_ciphers() must only
consider TLS 1.2 ciphers. Skip the ciphers whose minimum protocol version
is TLS 1.3 or above. SSL_CIPHER_get_min_version() was preferred over
testing for NID_kx_any because it expresses the intent directly rather
than relying on a side effect of how TLS 1.3 suites are described. Non-FIPS
TLS 1.2 ciphers (e.g. AES128-SHA or ECDHE-RSA-CHACHA20-POLY1305) and
non-FIPS TLS 1.3 suites (e.g. TLS_CHACHA20_POLY1305_SHA256) are still
rejected.

This bug was introduced in 3.5-dev3 by commit 9e2809508 ("MEDIUM: ssl: add
FIPS TLS 1.2 cipher check for AWS-LC"), no backport is needed.

12 days agoBUILD: Makefile: allow external makefiles to provide build targets
William Lallemand [Fri, 30 Oct 2026 09:57:52 +0000 (10:57 +0100)]
BUILD: Makefile: allow external makefiles to provide build targets

Let EXTRA_MAKE fragments set EXTRA_TARGET_BUILD to a nonempty value to
provide their own build and cleanup targets instead of building HAProxy.
Keep the common option handling and regression-test targets available.

Evaluate the selector after global compiler and linker options have been
initialized so a fragment can install its alternate build rules there.

12 days agoBUG/MINOR: debug: fix "debug dev fd" crash on unknown address families
Amaury Denoyelle [Fri, 30 Oct 2026 09:53:08 +0000 (10:53 +0100)]
BUG/MINOR: debug: fix "debug dev fd" crash on unknown address families

"debug dev fd" formats the local and peer addresses of each fd with
sa2str() and then replaces non-printable characters in the result. It
returns NULL for address families other than AF_INET, AF_INET6, AF_UNIX
and some AF_CUST_*. For supported address families, it can also return
NULL in case of a memory allocation failure.

Only an admin in expert mode can run the command.

Let's skip the cleanup and print "?" when sa2str() fails.

This fixes GitHub issue #3546.

This must be backported to all stable versions. In fact, strlen()
without guard was introduced in 3.1, which guarantees a crash in case of
an unsupported address. For older releases, only an unsafe
chunk_appendf() is present which may cause issues.

12 days agoMINOR: thread: add EXTRAS_LOCK so addons can name their locks
William Lallemand [Fri, 30 Oct 2026 10:54:43 +0000 (11:54 +0100)]
MINOR: thread: add EXTRAS_LOCK so addons can name their locks

Provide an unconditional EXTRAS_LOCK label so addons can name their
locks using the existing lock debugging and statistics infrastructure.
This avoids adding addon-specific lock labels or build-option guards to
the core enum. Report the label as EXTRAS in lock statistics.

12 days agoBUILD: Makefile: extend the capabilities of EXTRA_MAKE
William Lallemand [Thu, 29 Oct 2026 22:46:35 +0000 (22:46 +0000)]
BUILD: Makefile: extend the capabilities of EXTRA_MAKE

Allow external Makefile.mk loaded through EXTRA_MAKE to extend the main
build without adding addon-specific logic to the root Makefile.

Provide the following extension points:

  - EXTRA_USE_OPTS registers additional USE_* switches. Check unknown
    options after loading the fragments so ERR=1 accepts these switches.
  - EXTRA_CFLAGS and EXTRA_LDFLAGS append compiler and linker flags.
  - EXTRA_INCLUDES adds headers to the build dependencies so changes to
    addon headers trigger recompilation.
  - EXTRA_LATE_HOOK evaluates addon logic after the object lists and
    compiler options have been collected, for example to generate
    metadata from the final build options.
  - EXTRA_INSTALL and EXTRA_UNINSTALL add targets to the install and
    uninstall prerequisites.

Reuse the existing EXTRA_OBJS variable for objects supplied by addons.
Keep all as the default goal so rules in an external fragment do not
change what a plain make invocation builds.

12 days agoBUG/MINOR: sample: fix NULL dereference in the ltime/utime converters
Martin Mirchev [Wed, 28 Oct 2026 14:33:16 +0000 (22:33 +0800)]
BUG/MINOR: sample: fix NULL dereference in the ltime/utime converters

conv_time_common() returns NULL when alloc_trash_chunk() fails, and when
the format string expanded for "%N" does not fit in the trash chunk. Its
four callers, sample_conv_us_ltime(), sample_conv_ms_ltime(),
sample_conv_us_utime() and sample_conv_ms_utime(), dereference the
result without checking it.

A configuration using one of these converters with a long format, e.g.
ms_ltime("%N%N%N...") expanding to more than bufsize, is accepted by
"haproxy -c", and the first request evaluating it crashes the process
with a NULL dereference. A failed allocation at runtime has the same
effect with an ordinary format.

Let's make the converter fail instead, like on its other error paths.

This fixes GitHub issue #3545.

This must be backported to all stable versions up to 3.0, as commit
5e63e1636 introduced the first version of conv_time_common() with a
possible NULL return.

13 days agoBUG/MINOR: errors: reset diagnostic buffers after thread cleanup
William Lallemand [Tue, 13 Oct 2026 14:13:03 +0000 (14:13 +0000)]
BUG/MINOR: errors: reset diagnostic buffers after thread cleanup

A live worker frees its thread-local diagnostic buffers before EXTRA_OBJS
teardown, whose deinitializers can still emit diagnostics.

ha_free() releases the allocation and clears only the area pointer. It
leaves size, head and data unchanged, while b_is_null() tests size rather
than area. A previously allocated buffer therefore still looks allocated
with a NULL area. Later diagnostics skip lazy allocation and can crash
when accessing the buffer.

Reset both usermsgs_buf and usermsgs_ctx.str to BUF_NULL after freeing
their areas. This clears the stale size and contents metadata so later
diagnostics can allocate fresh buffers. The post-deinit cleanup releases
any buffers reallocated during EXTRA_OBJS teardown.

This could be backported in every maintained versions.

13 days agoMINOR: sym: use the symbol table to resolve a DSO name
Olivier Houchard [Wed, 28 Oct 2026 17:51:13 +0000 (18:51 +0100)]
MINOR: sym: use the symbol table to resolve a DSO name

resolve_dso_name() only used dladdr() to find the object containing an
address. Our in-process symbol table also knows the owning object, is
lock-free and async-signal-safe, and also works on static builds where
dladdr() may not. Try it first and keep dladdr() as a fallback for
addresses that are not covered by any symbol, or when the table has not
been built yet.

13 days agoCLEANUP: sym: factor out the short object name extraction
Olivier Houchard [Wed, 28 Oct 2026 17:51:06 +0000 (18:51 +0100)]
CLEANUP: sym: factor out the short object name extraction

dump_sym_lookup() and resolve_dso_name() both keep the part of an object's
path between the last '/' and the first following '.'. Introduce
append_obj_name() for it and use it in both places.

13 days agoBUG/MINOR: checks/log: force implicit UDP checks as TCP for log backend
Aurelien DARRAGON [Thu, 29 Oct 2026 12:47:11 +0000 (13:47 +0100)]
BUG/MINOR: checks/log: force implicit UDP checks as TCP for log backend

As described in GH #3528, declaring an UDP log server with "check"
keyword, without specifying check port would result in haproxy
starting a check using TCP oriented API while the underlying address
family was implicit datagram. It would either cause the check to
always fail, or even segfault on some systems where the network API
from the operating system doesn't expect DGRAM family with stream oriented
functions such as "connect()".

To fix the issue, although we do support UDP log endpoints, there is no
UDP checks, and we don't offer ICMP checks, so let's just fallback to
TCP protocol when we detect the address family is DGRAM when starting
a check from a log backend.

The documentation was updated to describe this pitfall.

Many thanks to GH user @ssavant2 for filing a bug report and providing
useful reproducer.

It may be backported to 3.0.

2 weeks agoBUILD: sym: Fix build on recent glibc
Olivier Houchard [Wed, 28 Oct 2026 16:05:19 +0000 (17:05 +0100)]
BUILD: sym: Fix build on recent glibc

With a recent glibc, as found on Ubuntu 26.04, with the default locale,
sort and comm do not always agree on the ordering of all characters,
notably punctuation characters, and comm will thus fail when reading the
output of sort, producing a warning, and prevent the minidebug section
from being created.
Just force them to use the C locale, where this is not a problem.

2 weeks ago[RELEASE] Released version 3.5-dev10 v3.5-dev10
Willy Tarreau [Wed, 28 Oct 2026 14:43:02 +0000 (15:43 +0100)]
[RELEASE] Released version 3.5-dev10

Released version 3.5-dev10 with the following main changes :
    - BUG/MEDIUM: stats-file: shm-stats-file be counters not working anymore
    - BUG/MINOR: quic: rearm packet on packet received even non ack-eliciting
    - REGTESTS: use HAPROXY_TEST_TIMEOUT for wait in purge srv/be
    - BUILD: sym: Make sure minidebug works correctly with most environments
    - BUILD: sym: Use dl_iterate_phdr() for more OSes.
    - BUILD: sym: Get it to build and work on various OSes
    - BUILD: sym: Make sure __always_inline always is inline.
    - BUILD: sym: Build properly with gold as a linker
    - BUILD: sym: Keep a temporary binary without .gnu_debugdata
    - BUILD: sym: Don't add a minidebug section to haterm and haload
    - MINOR: quic: use the listener for Retry and Version Negotiation emission
    - MINOR: quic: define a function to emit a datagram on a listener socket
    - BUG/MEDIUM: quic: use datagram destination as source for Retry
    - BUG/MINOR: quic: use datagram destination as source for stateless reset
    - BUG/MINOR: quic: use datagram destination as source for version negotiation
    - REGTESTS: quic: check Retry source address on a wildcard listener
    - BUG/MINOR: quic: handle EINTR on emission of packets without connection
    - MINOR: quic: rename confusing qc_purge_txbuf() to qc_flush_txbuf()
    - BUG/MEDIUM: mux_quic: prevent uaf on fctl frame after send error
    - BUG/MINOR: mux_quic: prevent RS/SS on unistream with hq-interop
    - BUG/MINOR: check: do not inspect errno if conn FDLESS
    - BUG/MINOR: mux_quic: do not crash on SE_SHR_DRAIN shut
    - MEDIUM: quic: implement the AEAD confidentiality limit
    - OPTIM: quic: use native HKDF APIs where available
    - TESTS: quic: add HKDF unit tests using RFC 5869 vectors
    - BUG/MEDIUM: peers: keep the earliest sync task deadline
    - DOC: config: explain per ACL line limitations
    - MINOR: quic: use the same HP encryption helpers for RX and TX
    - BUG/MINOR: quic: valid NEW_TOKEN connections are counted as half-open
    - BUG/MEDIUM: cli: Fix parsing of big payloads with dynamically allocated buffer
    - BUG/MINOR: filters: fix silent compression disable with legacy "filter compression" first
    - MINOR: filters: Support the global side in filter_place_class()
    - OPTIMIZE: stream: Stop holding a reference on the frontend during stream_free()
    - BUG/MEDIUM: mux-h1: Don't close the connection on a payload parsing error
    - BUG/MEDIUM: h1: make the Connection/Upgrade headers cleanup idempotent
    - BUG/MINOR: h1: keep accepting unsafe violations after a parser reset
    - BUG/MEDIUM: h1: don't rewrite the Upgrade header when not cleaning headers
    - BUG/MEDIUM: h1-htx: don't stall on messages ending exactly at the buffer's end
    - BUG/MEDIUM: mux-h1: don't truncate the response when closing after draining
    - BUG/MEDIUM: mux-h1: don't truncate responses on soft-stop
    - BUG/MINOR: h1: don't use uninitialized start line pointers when restarting
    - BUG/MINOR: mux-h1: only reset the session's timing info on the frontend side
    - BUG/MINOR: mux-h1: fix the open streams counter on stream creation failure
    - BUG/MINOR: mux-h1: wake the connection up when a pending error can be sent
    - BUG/MINOR: mux-h1: don't dereference a NULL opposite sedesc in h1_fastfwd()
    - BUG/MEDIUM: h1: reject CONNECT request targets other than "host:port"
    - BUG/MEDIUM: mux-h1: don't lose the end of the previous chunk in h1_make_chunk()
    - BUG/MEDIUM: mux-h1: don't stall when draining more than a buffer of data
    - BUG/MINOR: mux-h1: reset the content-length/chunking state after a 1xx response
    - BUG/MINOR: mux-h1: don't keep fast-forwarding disabled for the next streams
    - BUG/MINOR: mux-h1: don't report a 408 error on timeout of a closed connection
    - BUG/MEDIUM: mux-h1: don't swap buffers when transferring data with a reserved offset
    - BUG/MINOR: mux-h1: report payload parsing glitches to the session
    - BUG/MEDIUM: mux-h1: don't lose error responses once a content-length was parsed
    - BUG/MEDIUM: h1: build a missing Host header from the authority of the URI
    - BUG/MINOR: h1: reject CONNECT authorities with an empty host
    - BUG/MINOR: h1: forget a previous pass' results when parsing headers only
    - BUG/MEDIUM: mux-h1: don't send chunked responses to HTTP/1.0 clients
    - BUG/MINOR: mux-h1: convert the <from> argument of h1-case-adjust to lower case
    - BUG/MEDIUM: h1: reject absolute http(s) URIs with an empty host
    - BUG/MINOR: mux-h1: only rely on the client's method for bodyless responses
    - BUG/MINOR: mux-h1: search the websocket key in the right message
    - BUG/MINOR: mux-h1: don't forward request trailers named like server name header
    - BUG/MEDIUM: mux-h1: don't spread-close idle conns with idle-close-on-response
    - BUG/MINOR: mux-h1: Don't subscribe for receives once read0 or error received
    - BUILD: makefile: better support git worktrees
    - BUG/MINOR: sym: don't crash on images linked at a fixed base
    - CI: github: run several weekly/monthly jobs daily
    - DOC: remove the AWS-LC CI image
    - BUG/MEDIUM: resolvers: reject too long labels when encoding a name
    - CI: github: fix VTest cache paths in container jobs
    - BUG/MAJOR: mux-quic: do not crash on connection error after glitch threshold
    - BUG/MAJOR: mux-h2: split absolute URIs like the HTTP analysers do on output
    - BUG/MEDIUM: h3: split absolute URIs like the HTTP analysers do on output
    - BUG/MINOR: filters: free the error message of the filter-config parsers
    - MINOR: filters: initialize the filter classes before the register stage
    - BUG/MEDIUM: bwlim: never burst more than the pending data in input
    - MINOR: filters: Move cache-store after decompression in global classes list
    - BUG/MINOR: sample: Fix wrong 'base64urldec' return value check in 'jwt_member_query'
    - BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on SETTINGS parsing
    - BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on GOAWAY parsing
    - BUG/MEDIUM: h3: fix BUG_ON() crash with glitch after QPACK dec err
    - MINOR: h3: remove dead code on h3_rcv_buf() error path
    - BUG/MINOR: h3: fix risk of BUG_ON() crash with glitch on unknown frame
    - BUG/MINOR: h3: fix BUG_ON() crash with glitch on htx_set_eom() failure
    - REGTESTS: filters: Fix the global filters order as reported on the CLI
    - Revert "BUG/MAJOR: mux-quic: do not crash on connection error after glitch threshold"
    - MEDIUM: filters: automatically order the implicit core filters in legacy mode
    - DOC: filters: document the automatic ordering of the legacy core filters
    - MEDIUM: filters: eval the backend instances first on the response path
    - MEDIUM: filters: eval the legacy backend filters first on the response path
    - BUG/MINOR: flt-decomp: Report error about filter order in lagacy mode only
    - BUG/MEDIUM: h3: fix memory corruption on trailers parsing realign
    - BUG/MINOR: mux_quic: wake streams on soft-stop using listener socket
    - BUG/MINOR: h3: enforce maxrewrite on request headers
    - BUG/MINOR: h3: fix crash with maxrewrite on response headers
    - BUG/MINOR: cfgparse/peers: warn and prevent undefined behavior when peers is missing bind info
    - BUG/MEDIUM: task: do not destroy twice the tasks already killed in mworker_cleantasks()
    - BUG/MEDIUM: mux_quic: fix crash on QCS double remote close
    - BUG/MEDIUM: mux_quic: ensure FIN is reported after Rx data transfer
    - MINOR: thread: split out thread_bind_to_cpu_map()
    - OPTIM: fd: Make each thread group's FD table NUMA-local
    - OPTIM: fd: make each thread group's polled_mask NUMA-local
    - MINOR: ssl: Add new 'pha' server option
    - BUILD: thread: fix builds without thread support
    - DOC: config: add missing acme.scheduler in the global index
    - MINOR: tools: add find_word_index_in_array() to find a keyword
    - MINOR: http/config: add acceptable-http-re{quest,sponse}-violations
    - MINOR: http: add one control per existing HTTP violation
    - MEDIUM: http: add the "short-status-code" violation exception
    - BUG/MINOR: jwt: don't truncate binary HMAC secrets at the first NULL byte
    - BUG/MINOR: jws: fix ECDSA_SIG leak in jws_b64_signature()
    - BUG/MINOR: jwe: only accept exact "alg" and "enc" names in JWE tokens
    - BUG/MINOR: jwe: reject non-empty encrypted keys with "dir" and "ECDH-ES"
    - BUG/MINOR: jwe: only match the exact "ALL" word in jwt.decrypt_{alg,enc}_list
    - BUG/MINOR: jwt: enforce the ECDSA signature length in jwt_verify
    - BUG/MINOR: mux-quic: prevent send corruption on buf realign
    - BUG/MEDIUM: mux_quic: fix FIN received with an already known offset
    - BUG/MINOR: quic: fix "show quic" filtered connection
    - BUG/MINOR: mux_quic: fix MAX_STREAMS limit
    - BUG/MINOR: mux_quic: fix overflow on avail_streams
    - MINOR: cli/server: speed up proxy/server purge via the CLI
    - BUG/MINOR: promex: don't loop forever when a module skips a time series
    - BUG/MINOR: http-ana: call the http_reset filter callback on L7 retries
    - DOC: filters: channel_start_analyze is called again on L7 retries
    - BUG/MEDIUM: server: Don't use the cached ALPN for websocket
    - BUG/MAJOR: h3: always check :path for all scheme values
    - BUG/MEDIUM: h3: prevent conn failure on trailers decoding without space
    - BUG/MINOR: h3: do not retry on trailers too large
    - BUG/MINOR: h3: do not wait after Tx realloc failure
    - BUG/MEDIUM: connection: don't leak the fd of an accepted connection
    - BUG/MINOR: hlua/filters: Track the payload phase per channel
    - BUG/MEDIUM: hlua/filters: make HTTPMessage.eom/may_recv relative to the filter's data
    - MINOR: hlua/filters: Add HTTPMessage.closed() to check if input is closed
    - BUG/MINOR: hlua/filters: Restrict HTTPMessage.set_eom/unset_eom to visible data
    - BUG/MEDIUM: hlua/filters: Make Channel.may_recv relative to the filter's data
    - BUG/MEDIUM: hlua/filters: Never arm the analysis timer for the lua wake-ups
    - BUG/MEDIUM: filters: Don't propagate a close while a filter waits on a channel
    - BUG/MEDIUM: deviceatlas: isolate threads while swapping the reloaded atlas
    - BUG/MINOR: deviceatlas: refresh the user-agent evidence id after a reload
    - BUG/MINOR: deviceatlas: ignore the scheduler's update in progress marker
    - BUG/MINOR: deviceatlas: use the result's property type when printing
    - BUG/MINOR: deviceatlas: consume a scheduled update before loading it
    - BUG/MEDIUM: peers: do not keep a dict entry cached when its update was not sent
    - BUG/MEDIUM: peers: fix byte order of last_get on updates without identifier
    - BUG/MINOR: peers: forget the appctx when the peer session init fails
    - BUG/MINOR: peers: don't lose the peer's version when the hello is fragmented
    - BUG/MINOR: peers: reject over-long varints in intdecode()
    - BUG/MINOR: peers: account incoming sessions in the per-thread applet counters
    - BUG/MINOR: peers: compare the received expiration date as unsigned
    - BUG/MINOR: peers: don't use remote table definitions from a previous session
    - BUG/MINOR: peers: forget the remote table when ignoring an unknown key type
    - BUG/MEDIUM: listener: enforce rate-limit sessions across thread groups
    - BUG/MINOR: hlua: unlock Patref before add_bulk type error
    - MINOR: filters: Set the class flags on filter class registration
    - MINOR: filters: Deduce the default filter API from the implied filters
    - MINOR: filters: Warn about ignored legacy filters for a proxy
    - MINOR: filters: Declare a proxy as using the legacy API before parsing filter directive
    - MINOR: filters: Add a function to remove a filter conf from all proxy lists
    - MINOR: filters: Add a function to find a legacy filter configuration during parsing
    - MINOR: cache: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: fcgi-app: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: bwlim: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: flt-decomp: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: flt-http-comp: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: filters: Document the public helpers missing from the filters guide
    - BUG/MEDIUM: applet: check buffer sizes before HTX transfer
    - DOC: config: Fix documentation about dot character in variable names
    - DOC: configuration: document library compatibility for TLS keywords
    - DOC: configuration: document library compatibility for pha
    - MINOR: filters: Pre-initialize the instances in check_config_validity()
    - BUG/MAJOR: mux_quic: remove data not yet sent on reset stream
    - BUG/MEDIUM: mux_quic: do not reset timeout after shut
    - BUG/MINOR: mux_quic: fix possible BUG_ON() after frame alloc failure
    - BUG/MINOR: mux_quic: fix ALPN retrieval from server
    - BUG/MINOR: mux_quic: fix stream elasticity dec on qcm_init() failure
    - BUG/MINOR: h3: reset HTX message after trailers parsing error
    - BUG/MINOR: thread: don't turn reversed thread ranges into "even"
    - BUG/MINOR: tools: make strl2irc() and strl2llrc() reject a lone minus sign
    - BUG/MINOR: cfgparse-listen: reject negative capture lengths
    - BUG/MINOR: stick-table: reject negative stick counter IDs in sc-* actions
    - CLEANUP: resolvers: don't close the nameserver socket twice on deinit
    - BUG/MINOR: connection: check the NetScaler CIP header length against received data
    - BUG/MINOR: connection: check the IP header length in NetScaler CIP
    - BUG/MEDIUM: ssl-gencert: fix a NULL dereference with QUIC listeners
    - BUG/MEDIUM: ssl: fix an out-of-bounds read on SSL_CTX switch with a .sctl file
    - BUG/MEDIUM: ssl: support SCTL with AWS-LC
    - BUG/MINOR: ssl: fix a double-free of the .sctl buffer with several contexts
    - BUG/MINOR: ssl: support SCTL in TLS 1.3 Certificate messages
    - DOC: configuration: clarify SCT support by TLS library and version
    - BUG/MINOR: server: free the sample expression when it is not usable on a server
    - BUG/MEDIUM: mux-spop: Don't include DALLOC/DFULL flags in SPOP_CF_DEM_BLOCK_ANY
    - DOC: config: add missing entry for health checks
    - IMPORT: slz: update slz to version 1.4-dev3

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().