]> git.kaiwu.me - haproxy.git/log
haproxy.git
2 months agoMINOR: flt-http-comp: Check request headers for response compression earlier
Christopher Faulet [Fri, 28 Aug 2026 11:44:43 +0000 (13:44 +0200)]
MINOR: flt-http-comp: Check request headers for response compression earlier

When the response compression is enabled, the request headers are tested
earlier now, on post-analysis of AN_REQ_HTTP_INNER. It will be mandatory to
be mixed with the HTTP decompression.

2 months agoMEDIUM: flt-decomp: implement decomp-req and decomp-res filters
Aurelien DARRAGON [Wed, 12 Aug 2026 15:11:31 +0000 (17:11 +0200)]
MEDIUM: flt-decomp: implement decomp-req and decomp-res filters

Initial filter implementation for decompressing gzip/zlib streams by
leveraging libslz/uslz API.

For now both request and response filters only work for TCP proxies
(with "mode tcp" set). While this is currently not possible to
compress TCP streams using the existing compression filters, thus
making the TCP decompression less useful than the upcoming HTTP
decompression, basic tcp decompression implementation helped during
development to stress test the libslz decompression part without
falling in the haproxy HTX API pitfalls straight away. However it
turns out the TCP decompression may find some use-cases, so let's
keep that for now.

With TCP decompression now supported, we have all the code
infrastructure needed to perform HTTP decompression, which will come
in a later patch.

2 months agoMINOR: (de)compression: add decompression infrastructure
Aurelien DARRAGON [Wed, 12 Aug 2026 09:58:52 +0000 (11:58 +0200)]
MINOR: (de)compression: add decompression infrastructure

Add basic decompression infrastructure to haproxy compression API, which
will be used in upcoming commits to implement TCP and HTTP decompression.

For now decompression exclusively leverages uslz (slz decompression API).

As such it supports the following decompression options:

 - uslz-auto: this is automatic format detection from libslz/uslz: it is
   able to guess if the stream is gzip or zlib (deflate) one, and if it
   is none of the two, it fallsback on raw-reflate.
 - gzip: if the stream is valid gzip, it will be decompressed, else the
   filter will refuse to decompress the stream.
 - "deflate": if the stream is valid deflate (zlib), it will be
   decompressed, else the filter will refuse to decompress the stream.
 - "raw-deflate": the stream will be assumed to be raw deflate (deflate
   without enveloppe, also known as (rfc1951), the filter will try to
   decompress it unless the stream is invalid.

2 months agoMINOR: buf: make b_rep_blk() wrapping-proof
Aurelien DARRAGON [Wed, 12 Aug 2026 09:39:54 +0000 (11:39 +0200)]
MINOR: buf: make b_rep_blk() wrapping-proof

b_rep_blk() was not versatile because there were a lot of cases
(including wrapping resulting from the insertion or deletion)
that were not supported, this actually made the function not
very useful.

Since there a no more known usages of this API function in the code, but
yet the decompression filters could make use of it in upcoming commits
let's revisit the function by leveraging b_move() to properly support
edge cases such as wrapping.

Now the function is pretty versatile and can safely be used to expand
as well as to shrink buffer when replacing existing data with new data.

There is still one known limitation to this day, <pos> and <end> have
to be contiguous (not interrupted by wrapping buffer), but that could
be improved in the future if needed.

The function assumes the caller already checked that inserted data will
fit in the buffer (removed data also taken into account).

2 months agoBUG/MEDIUM: server: Fix server-template with default-server
Olivier Houchard [Fri, 28 Aug 2026 15:04:51 +0000 (17:04 +0200)]
BUG/MEDIUM: server: Fix server-template with default-server

Commit c81044a8b moved the call for srv_settings_cpy() from
_srv_parse_init() to parse_server(), but in doing so, it will use 1 as
the last argument if the new server is a server template. This is,
however, wrong, srv_settings_cpy() expect 1 if the source is a
server template, not the destination. Doing that will lead to an
uninitialized server, if there were settings to copy from another
server.
This fixes using server templates with default-server.

2 months agoMEDIUM: servers: Do not allow reverse HTTP with per-tg fd tables
Olivier Houchard [Fri, 28 Aug 2026 13:54:09 +0000 (15:54 +0200)]
MEDIUM: servers: Do not allow reverse HTTP with per-tg fd tables

We can't steal the connection of other thread groups with per-thread
group file descriptor tables, so disallow its usage if there's a server
that uses reverse HTTP, as requests could fail because we would not have
enough connections.

2 months agoBUG/MEDIUM: mux-spop: Properly handle parsing of split HELLO/DISCONNECT frames
Christopher Faulet [Fri, 28 Aug 2026 08:24:08 +0000 (10:24 +0200)]
BUG/MEDIUM: mux-spop: Properly handle parsing of split HELLO/DISCONNECT frames

The parsing of HELLO/DISCONNECT frames did not properly handle frames split
into several packets. In that case, the frame header parsing was not
bypassed once received and consumed when waiting for the payload. So when
the frame payload was finally received, the demux tried to re-read the frame
headers, triggering this way a parsing error. It was especially an issue on
HELLO frame, because this led to reject the SPOP connexion.

Now, the HELLO/DISCONNECT frames header is only parsed onced. If the demux
must wait for the frame payload, the frame header parsing is skipped to
properly parse the frame payload.

Note it was only an issue for HELLO/DISCONNECT frames.

This patch should fix the issue #3478. It must be backported as far as 3.2.

2 months agoOPTIM: server: remember the lowest known unused server ID
Willy Tarreau [Wed, 26 Aug 2026 16:40:45 +0000 (18:40 +0200)]
OPTIM: server: remember the lowest known unused server ID

Tiburce Giroux reported that adding many servers without an explicit
ID took time in O(N^2) for N servers due to the lookup of the lowest
unused ID. Instead, let's just store this value after finding it, and
restart from there upon next "add server". In a test with 20k servers
the delay went down from 14.4 seconds to 66 milliseconds, or 218 times
faster. And it takes 408ms to add 50000 servers (~120k srv/s).

In practice we store the ID of the place to start searching from, so
typically id+1 after a successful addition at the last position. Upon
deletion we just reset the ID to the deleted server if it was before
so that next addition starts searching from there again. Thanks to
this, the lowest known hole is always the one allocated.

It would make sense to backport this to 3.4 since it's being increasingly
used in a dynamic way.

2 months agoBUG/MINOR: cache: Seed cache primary hash
Remi Tricot-Le Breton [Wed, 26 Aug 2026 15:08:26 +0000 (17:08 +0200)]
BUG/MINOR: cache: Seed cache primary hash

Since only the first 32bits of the primary hash built in sha1_hosturi
are used as key in the cache tree and we reject entries when a hash
collision happens, it would be possible for an attacher to fill a
specific cache entry with "wrong" data and prevent actual caching.
Since a cache seed was already built for secondary key hashes we might
as well use it for primary hashes as well.

Reported-by: Claude (ANT-2026-9WRFEVJ3)
This patch should be backported to all stable branches.

2 months agoBUG/MINOR: cache: Manage collisions on primary key
Remi Tricot-Le Breton [Tue, 18 Aug 2026 16:01:23 +0000 (18:01 +0200)]
BUG/MINOR: cache: Manage collisions on primary key

When storing an entry in the cache, we calculate a hash (sha1) of the
host and uri which is 20 bytes long but we only use the first 4 bytes of
this hash as key for the eb32 tree. The tree itself is then vulnerable
to collisions which might even be forced since the sha1 is not seeded.
Since all the "vary" logic was added to the cache, dup entries have been
allowed so colliding entries would then be inserted in the tree
indefinitely and only the first one would be retrieved in the use_cache
operation.
This patch checks that in case of collision the full hashes match, and
if not the newly inserted entry is removed.

Reported-by: Claude (ANT-2026-9WRFEVJ3)
This patch should be backported to all stable branches.

2 months agoMINOR: haload: add -P to report ttfb/ttlb percentiles
Frederic Lecaille [Wed, 26 Aug 2026 15:26:00 +0000 (17:26 +0200)]
MINOR: haload: add -P to report ttfb/ttlb percentiles

Mostly ported from the same option in h1load: a compact 16-bit float
encoding (uf16_t) buckets ttfb/ttlb samples per thread, merged and
printed as percentiles at the end of the run.

2 months agoBUG/MINOR: haload: fix hld_new_usr()'s error path
Frederic Lecaille [Wed, 26 Aug 2026 11:57:00 +0000 (13:57 +0200)]
BUG/MINOR: haload: fix hld_new_usr()'s error path

When <usr>, <t>, or <sess> failed to allocate, the error path always
dereferenced ->urls, even when <usr> itself hadn't been allocated
yet. It also never freed <sess> on any failure, leaking it.

<usr> is now only allocated once <t> and <sess> are confirmed valid,
so ->urls is only ever touched once <usr> is fully set up. The error
path frees <sess> too, on every failure.

2 months agoMINOR: haload: always track and resend cookies per user
Frederic Lecaille [Wed, 26 Aug 2026 08:43:27 +0000 (10:43 +0200)]
MINOR: haload: always track and resend cookies per user

Capture every Set-Cookie header from a response, and send them back
as a Cookie header on that user's later requests. This is always on,
not behind an option: haload should behave like a real client here.

Cookies are kept per user, not per URL: they stay across a
reconnect, and are shared between the URLs that user hits, the same
way a real browser would.

http_extract_cookie_value() (src/http.c) is reused to parse the value
part (it already handles quotes and spaces correctly); only finding
the name comes first, since that function needs the name to already
be known.

2 months agoMINOR: haload: add -T to add a think time after each response
Frederic Lecaille [Tue, 25 Aug 2026 13:55:36 +0000 (15:55 +0200)]
MINOR: haload: add -T to add a think time after each response

Add -T <time>, like h1load's option of the same name: after a
response, wait <time> ms (with a bit of random jitter) before sending
the next request, instead of sending it right away. This simulates a
real user pausing between actions.

-T is positional, like -post or -head: it applies to the URLs that
follow it on the command line, not to all of them.

If -R is also set, the wait used is the larger of the two: the rate
limit and the think time never fight each other, whichever asks for
more waiting wins.

-ee takes over as soon as an error is detected: -T is ignored while it
is still probing, and applies again once probing is done and the
normal load resumes.

2 months agoCLEANUP: sink: initialize and finalize the implicit forward proxy
Willy Tarreau [Wed, 26 Aug 2026 13:47:54 +0000 (15:47 +0200)]
CLEANUP: sink: initialize and finalize the implicit forward proxy

sink_new_from_logger() builds a forward proxy by hand from a POST_CHECK
hook, that is after check_config_validity() has walked over
sink_proxies_list. As a result, contrary to the proxy of a "ring"
section, that one was neither passed to proxy_init_per_thr() nor to
proxy_finalize(), and kept running with px->per_tgrp left NULL, no
load-balancing setup at all (lbprm.algo, lbprm.ops and the lbprm lock
were never initialized), and its server left in SRV_ST_STOPPED with a
zero effective weight.

It still works surprisingly well but for how long... Let's just make
sure the freshly created server is completely initialized like any
other one so as to avoid susprises in the future like the max-reuse
one.

This shouldn't be backported as it might reveal other issues in older
code.

2 months agoBUG/MEDIUM: sink: do not create one implicit ring per logger copy
Willy Tarreau [Wed, 26 Aug 2026 13:34:31 +0000 (15:34 +0200)]
BUG/MEDIUM: sink: do not create one implicit ring per logger copy

A "log tcp@..." directive gets its implicit ring created from
sink_resolve_logger_buffer(), which calls sink_new_from_logger() as soon
as the target has an address (to differentiate it from log ring@...).

However, one case was overlooked: when using "log global", the same log
target is inherited everywhere, and the current code doesn't check for
this and systematically tries to recreates the sink. This seems to be
the case since 2.9 with commit a9b185f34e ("MEDIUM: log: introduce log
target"), which added dup_log_target().

sink_new_buf() returns the pre-existing sink when a buffer of that name
already exists, and all the copies carry the same generated "ring#N"
name, so the second and later copies did not create a new sink: they
added one more server to the very same one, then sink_finalize()
registered it and sink_init_forward() replaced sink->forward_task,
(leaking the previous task along with the freshly allocated forward
proxy BTW but that's secondary).

The result for a "log tcp@" in the global section with a single frontend
was 4 forwarding servers on one 16 kB ring (due to ocsp and http-client
also referencing the global logger), hence 4 connection attempts to the
log server, each receiving a full copy of every message. Worse, when more
connections than acceptable by the server would be reached, extraneous
connections fail and are repeatedly attempted, as can be seen with tcpdump
showing lots of SYN/RST pairs, and the extraneous servers that cannot be
served remain attached to the ring, preventing it from being recycled.
Because of this, running tests with a single-connection server like netcat
would only show 16 kB received (a single buffer) before stalling for
roughly as many times as there are references to the global logger, then
would not receive anything anymore until haproxy is reloaded. This would
also likely cause log duplication on the servers sometimes (e.g. after
reconnecting, where another server would start from its recovery point).

Interestingly, placing the same directive in a defaults section did not
show the bug, because only the frontend's copy is resolved there, and
the internal proxies only duplicate the loggers of the global section.

Let's lookup the sink by its ring name first and only create it when it
does not exist yet, just like the explicit ring case does a few lines
below. Ring names are generated once per parsed directive, and dup_logger()
copies the name rather than regenerating it, so distinct directives keep
one forwarding server each even when they point to the same address with
different parameters, while pure copies now correctly share the same
ring.

This must be backported to 3.0.

2 months agoBUG/MEDIUM: sink: initialize the settings of the implicit log server
Willy Tarreau [Wed, 26 Aug 2026 13:26:35 +0000 (15:26 +0200)]
BUG/MEDIUM: sink: initialize the settings of the implicit log server

TCP logs currently work fairly badly due to connections closing
immediately after logs are sent (or at best a full buffer of logs).
This started to happen in 3.1 with commit 237849c911 ("MEDIUM: sink:
"max-reuse" support for sink servers") thow that commit is not faulty,
the issue stems to the complex server creation chain.

What happens is that max_reuse >= 0 means the max-reuse setting is
enable, and the sink server creation in sink_new_from_logger() creates
a server but doesn't initialize it. Other servers are not exactly
initialized, they're preset from defaults which are initialized. But
here we don't inherit defaults so we must call srv_settings_init(),
otherwise max_reuse remains zero and connections are closed after each
message.

This must be backported to 3.2.

2 months agoBUG/MEDIUM: flt-http-comp: Block HTX_FL_EOM flag till the end of compression
Christopher Faulet [Wed, 26 Aug 2026 10:02:58 +0000 (12:02 +0200)]
BUG/MEDIUM: flt-http-comp: Block HTX_FL_EOM flag till the end of compression

Because the compression filter is generating a new payload and especially
because data may be blocked in its internal context, the EOM flag must be
removed from the HTX message and only set again at the end of the
compression.

It is important to be sure the end of message is not detected too early.
Indeed, it may happen if some data are blocked in the filter context but all
data in the message must be fully forwarded. In that case, we have a message
with no input at all and the EOM flag. It is the condition to consider the
end of the message was reached. This could lead to truncate the message.

To fix the issue, the compression filter now remove systematically the EOM
flag from the HTX message. But it save the information in its context,
thanks to the COMP_STATE_EOM_SEEN flag. This way, at the end of the
compression, it can set it again.

This patch could be backported to supported version. Not sure the bug was
ever been observed. But it is theorically possible.

2 months agoBUG/MINOR: flt-http-comp: Don't read next block to detect end of data
Christopher Faulet [Wed, 26 Aug 2026 08:32:59 +0000 (10:32 +0200)]
BUG/MINOR: flt-http-comp: Don't read next block to detect end of data

During the compression, on a DATA block, the next blocks was tested to know
if the end of data was reached. However, it is not valid to do so because a
prior filter may have limited the data forwarding to the current block to
inject more DATA blocks later for instance. Because of this bug, we could
detect the end of the payload too early, hijacking this way processing of
prior filters.

This test was added as an optimization, to save a loop on the HTX
message. So it can be safely removed.

For now, it is not possible to hit the bug with in-house filters. But a lua
filter modifiying the payload could trigger it. And it will be an issue if
the compression filter is chained with the decompression one (once
added). So let's fix it now.

This patch must be backported to all supported versions.

2 months agoBUG/MINOR: fcgi-app: allow explicit filter declaration with non-cache/non-compression...
Austin Kauffman [Sat, 15 Aug 2026 01:34:39 +0000 (18:34 -0700)]
BUG/MINOR: fcgi-app: allow explicit filter declaration with non-cache/non-compression filters

fcgi_flt_check() in src/fcgi-app.c unconditionally rejects any filter that
isn't compression, cache, or fcgi-app from coexisting with use-fcgi-app in
the same backend section, regardless of whether an explicit
`filter fcgi-app <name>` declaration is present. This contradicts the
documentation (section 9.5 of configuration.txt), which states that an
explicit `filter fcgi-app` declaration should allow other filters to
coexist.

The cache filter's equivalent check (cache_store_check in src/cache.c)
correctly uses a CACHE_FLT_F_IMPLICIT_DECL flag to only reject implicit
declarations when other filters are present. The fcgi-app check was missing
this flag, which appears to be an oversight.

This patch adds a FCGI_FLT_F_IMPLICIT_DECL flag to fcgi_flt_conf, mirroring
the cache filter's pattern:

  - The flag is set when use-fcgi-app creates an implicit filter
    (proxy_parse_use_fcgi_app).
  - The flag is cleared when filter fcgi-app explicitly declares the filter
    (parse_fcgi_flt), whether reusing an existing implicit config or
    creating a new one.
  - fcgi_flt_check only errors when the flag is set (i.e., the filter is
    implicit), matching cache_store_check's behavior.

This allows Lua-based filters (lua.resp_transform, lua.compress for
brotli/zstd) and other third-party filters to coexist with FastCGI backends
when an explicit `filter fcgi-app` declaration is present, as documented.

This should be backported to all versions that have the fcgi-app filter.

2 months agoBUG/MEDIUM: cache: ignore cache on redundant origin/referer
Willy Tarreau [Sun, 16 Aug 2026 06:09:32 +0000 (08:09 +0200)]
BUG/MEDIUM: cache: ignore cache on redundant origin/referer

The cache's Vary header processing to consider secondary keys used only
the first occurrence of Origin and Referer. These header are single-valued
so this is not supposed to be a problem, however the server's behavior
when dealing with more than one will not necessarily be predictable, so
we'd rather just bypass the cache when facing such requests.

default_normalizer() now returns -1 if a second occurrence is found,
making http_request_build_secondary_key() return a failure and never
match from secondary keys. This way it doesn't affect the standard case
and doesn't take risks with border-line ones.

The vary regtest was completed with various combinations.

Reported-by: Claude (ANT-2026-FQEGP4YG)
This should be backported to all stable versions.

2 months agoMEDIUM: ring: attach readers under the tail lock instead of isolating
Willy Tarreau [Tue, 25 Aug 2026 15:53:59 +0000 (17:53 +0200)]
MEDIUM: ring: attach readers under the tail lock instead of isolating

ring_dispatch_messages() isolates the thread to grab the initial reader
offset, as introduced by commit 31b93b40b ("MEDIUM: ring: protect the
initialization of the initial reader offset"), whose message already
noted that the tail lock could be used instead.

Doing it that way removes a trap for the callers: isolating from a
function that may be called with a lock held deadlocks against any
thread spinning on that lock, which is what happened with the sink
forward applet. Only a writer holding the tail lock may move the head,
so taking that same lock is sufficient to perform the initialization
while we attach.

Contrary to the isolation, this does not exclude a writer that released
the tail lock and is still copying its payload, so the readers count
byte may transiently be seen as RING_WRITING_SIZE if we're unlucky,
but the existing retry loop on values above RING_MAX_READERS already
handles that case.

This also makes attaching a reader a lot cheaper even if that has
never been a problem.

Note that this only aims at making the code more robust and tolerant
to accidental abuse, it's not a reason for calling
ring_dispatch_messages() under a lock anyway!

2 months agoMEDIUM: threads: always abort when calling thread_isolated() under a lock
Willy Tarreau [Tue, 25 Aug 2026 16:21:36 +0000 (18:21 +0200)]
MEDIUM: threads: always abort when calling thread_isolated() under a lock

A bug was found in _sink_forward_io_handler() where we call
ring_dispatch_messages() under an unneeded look, and that one calls
thread_isolate() on first call. Needless to say that when we're unlucky
it really deadlocks.

There is never ever any valid case for calling thread_isolate() with a
lock held, and all such instances MUST be addressed. In addition we must
not call it under isolation either. Let's just add two BUG_ON() for this
that display exactly the problem so that next time a developer creates
such an accidental construct, it's instantly discovered.

All tests pass, but better not backport this, as it's essentially meant
to help developers, it would not bring any benefit to end users (the
watchdog just addresses this case as well one second later if this
happens).

2 months agoBUG/MEDIUM: sink: do not hold the sft lock around ring_dispatch_messages()
Willy Tarreau [Tue, 25 Aug 2026 15:53:45 +0000 (17:53 +0200)]
BUG/MEDIUM: sink: do not hold the sft lock around ring_dispatch_messages()

Attaching a reader to a ring is done under thread_isolate(), which was
added by commit 31b93b40b ("MEDIUM: ring: protect the initialization of
the initial reader offset"). But _sink_forward_io_handler() calls
ring_dispatch_messages() with the sft lock held, where on the very first
call for a given sft, a thread isolates itself while holding that lock!
Needles to say that if a second thread tries to emit a lock during that
tiny time frame, it's a guaranted deadlock!

The lock is not needed around that call: sft->ofs and sft->e_processed
are only ever accessed from this applet, which is bound to a single
thread, and both lists involved are mt_lists that protect themselves.
What the lock really protects is sft->appctx, whose publication by
process_sink_forward() is asynchronous, and that is already dealt with
by sink_forward_session_init(), which runs before the first call to the
I/O handler. So let's simply take the lock after the dispatch. In fact
that lock is a leftover from 2.2's commit 494c505703 ("MEDIUM: ring:
add server statement to forward messages from a ring") which duplicated
the dispatching code inside the function's body, so when this was cleaned
up in 3.0 with commit c262442b1a ("MEDIUM: sink: move the generic ring
forwarder code use ring_dispatch_messages()"), it was not noticed that
this lock could now cause harm.

This should be backported to 3.0.

2 months agoBUG/MEDIUM: connection: fix an infinite loop in the fc_pp_tlv() fetch
Willy Tarreau [Fri, 21 Aug 2026 15:59:02 +0000 (17:59 +0200)]
BUG/MEDIUM: connection: fix an infinite loop in the fc_pp_tlv() fetch

fc_pp_tlv() is a multi-value fetch: it reports one TLV per call, sets
SMP_F_NOT_LAST and stores the TLV it returned in smp->ctx.p so that the next
call resumes from there. But it resumes *on* that TLV rather than after it,
and list_for_each_entry_from() starts on the element it is given, so the same
TLV matches again and is returned again, with SMP_F_NOT_LAST set again.

The ACL engine asks for the next occurrence as long as the fetch reports one
(the "fetch_next" retry in acl_exec_cond()), so an ACL whose pattern does not
match a TLV which is present never terminates: the thread spins at 100% and
the request is never answered. A configuration as simple as

    http-request deny if { fc_pp_tlv(0xE0) -m str nomatch }

hangs as soon as a client sends that TLV on any frontend accepting PROXY
protocol v2.

The same mistake also means the fetch never walked the list at all: only the
first matching TLV was ever visible, so a rule matching any later occurrence
of a repeated TLV silently never matched.

Resume from the element following the one returned previously. When it was
the last one, the resulting pointer is the list head and the loop stops right
away, as it should.

The regtest now sends the same TLV type three times and matches on the last
one, and checks that a pattern matching none of the occurrences terminates.
Without this patch that test hangs until the client times out.

This should fix GitHub issue #3482. Thanks to Annika Wickert for reporting
it!

This should be backported to all stable branches where fc_pp_tlv() exists.

2 months agoMEDIUM: mux-h2: don't report EOM on a short content-length message
Olivier Houchard [Tue, 25 Aug 2026 15:10:28 +0000 (17:10 +0200)]
MEDIUM: mux-h2: don't report EOM on a short content-length message

If we received an end of stream, but we ended up getting less than
Content-Length, then do not broadcast EOM, we never got the whole
message.
This is more hardening, to make sure everything is consistent across the
whole stream.

This should be backported up to 2.8.

2 months agoBUG/MEDIUM: mux-h1: close the connection on a short content-length
Olivier Houchard [Thu, 13 Aug 2026 18:58:18 +0000 (20:58 +0200)]
BUG/MEDIUM: mux-h1: close the connection on a short content-length

When a fixed-length message completes while curr_len is still non-zero,
fewer bytes than announced were sent and the peer keeps waiting, so
reusing the connection desynchronizes the next request (request
smuggling). In h1s_destroy(), when either the request or the response
still expects data, flag the connection on error, unless the message is
legitimately bodyless. It is then closed instead of being returned to
the idle pool.

This can be reproduced with an H2 frontend, that will send less data
than announced before sending an end of stream, to an H1 backend that
replies before it received the whole request.

This should be backported up to 2.8.

Many thanks to truff@projet7.org for reporting the issue and providing
an initial fix.

2 months agoBUILD: ssl: avoid a potential null-dereference warning on OpenSSL 1.0.2
William Lallemand [Tue, 25 Aug 2026 10:26:17 +0000 (10:26 +0000)]
BUILD: ssl: avoid a potential null-dereference warning on OpenSSL 1.0.2

On OpenSSL 1.0.2, X509_CRL_get_issuer() is a raw macro expanding to
(x)->crl->issuer instead of an opaque accessor function, so gcc's
inliner sees straight through to the dereference in
ssl_ctx_hash_store(). It cannot prove that the X509_CRL returned by
X509_OBJECT_get0_X509_CRL() is non-NULL, and flags it under
-Wnull-dereference, which "make ERR=1" turns into a hard error:

    x509.h:605:45: error: potential null pointer dereference [-Werror=null-dereference]

Add an explicit NULL check on crl before using it, the same way
ssl_ctx_hash_X509() already guards against a NULL certificate a few
lines above. This only affects builds against OpenSSL 1.0.2, where the
macro form is used; newer OpenSSL versions hide the access behind a
real function and never triggered the warning.

This must be backported in every stable branches along with the sid_ctx
fixes.

2 months agoMINOR: haload: keep old-scheduled users under -ee's floor rate
Frederic Lecaille [Mon, 24 Aug 2026 17:14:06 +0000 (19:14 +0200)]
MINOR: haload: keep old-scheduled users under -ee's floor rate

A user with its next send already scheduled before -ee starts still
fires at the old, unlimited pace. Nothing re-paces it once the ramp
begins. A mass failure could still cause a burst of new connections
right when the floor kicks in.

Each user now remembers the last -ee generation it checked in with.
On its first wakeup during a new ramp, it checks for room under the
current rate before sending. If there is none, it retries after a
short, fixed delay, and checks again. A user created during the ramp
already has the right generation, so replacements are not affected:
-ee's doubling still only depends on their own successes.

This check does not reuse hld_usr_schedule()'s wait: that one grows
with the number of idle users on the thread, and right after a mass
failure that number can be huge. It also never gets recomputed later,
so a user could end up waiting several minutes with no way back. A
short, fixed retry avoids this, since the user is checked again each
time instead of trusting one old estimate.

2 months agoCLEANUP: haload: factor out two more duplicated -ee blocks
Frederic Lecaille [Mon, 24 Aug 2026 14:07:42 +0000 (16:07 +0200)]
CLEANUP: haload: factor out two more duplicated -ee blocks

hld_usr_schedule() and hld_rate_task() had the same four lines
to reset a thread's rate history on a new -ee ramp generation. Move
this into hld_thr_sync_ramp_gen().

hld_usr_schedule() and the budget check added to hld_usr_task() had
the same throttle max computation. Move this into
hld_thr_rate_max(), built on top of hld_thr_rate_share().

2 months agoCLEANUP: haload: factor out the per-thread rate share computation
Frederic Lecaille [Mon, 24 Aug 2026 10:14:04 +0000 (12:14 +0200)]
CLEANUP: haload: factor out the per-thread rate share computation

hld_usr_schedule(), hld_usr_task(), and hld_rate_task() each had the
same three lines to compute this thread's exact share of a rate
(throttle-aware, or not). Move this into hld_thr_rate_share() and call
it from all three places instead.

Also fix a typo in hld_usr_schedule()'s comment (<urs> -> <usr>).

2 months agoBUG/MINOR: haload: -ee could burst past its floor when many users fail at once
Frederic Lecaille [Mon, 24 Aug 2026 10:03:39 +0000 (12:03 +0200)]
BUG/MINOR: haload: -ee could burst past its floor when many users fail at once

On a large, near-simultaneous outage (e.g. -u 10000 on several
threads), -ee's 1 req/s floor could be blown past. A burst of several
thousand requests could fire right when the ramp starts. This defeats
the whole point of probing gently.

Two separate gaps caused this:

  - hld_rate_task() creates replacement users after a kill. It only
    counted a new user once it later connected, not when it was
    created. Many kills in a row wake this task up again and again,
    before any replacement has had time to run. Each time, it still
    saw an empty counter, and created one more than it should. It now
    counts a new user right when it decides to create it.

  - hld_rate_task() has a "no rate limit, fill directly" path. It is
    used to refill users after a kill when -R is not set. It only
    checked arg_rate. On a mass failure, another thread can already be
    inside hld_err_trigger(), with its new arg_rate not visible here
    yet. This path would then still fire, and fill the whole thread's
    quota at once. It now also checks err_ramp_pending. This flag is
    set first by hld_err_trigger(). Seeing it at 0 means no trigger is
    running.

2 months agoBUG/MINOR: haload: split the rate exactly across threads
Frederic Lecaille [Fri, 21 Aug 2026 16:26:38 +0000 (18:26 +0200)]
BUG/MINOR: haload: split the rate exactly across threads

hld_usr_schedule() and hld_rate_task() gave every thread ceil(rate /
arg_thrd), so a low rate on a machine with many threads was quietly
served several times over (e.g. rate=1 on 200 threads gave ~200 req/s
instead of 1). Use the same exact split already used elsewhere in
haload (thread <tid> gets (rate + tid) / arg_thrd): most threads then
get 0 and only <rate> of them get 1, so the total always matches
<rate> regardless of the thread count.

A thread with no budget now just checks back in 1s instead of being
bumped to 1 by the old max ? max : 1 fallback.

2 months agoMINOR: quic: add tune.quic.sec.stateless-reset-rate
Frederic Lecaille [Thu, 20 Aug 2026 16:23:18 +0000 (18:23 +0200)]
MINOR: quic: add tune.quic.sec.stateless-reset-rate

RFC 9000 10.3 allows sending a Stateless Reset for any packet that can't
be matched to a connection. A flood of garbage UDP datagrams can turn this
into an unwitting amplifier, wasting CPU and bandwidth on replies.

Add tune.quic.sec.stateless-reset-rate to cap the total number of
Stateless Reset packets sent per second across all threads (default:
1000). It is not scoped to the frontend side: a Stateless Reset can be
sent for any unmatched packet, regardless of which side received it.
Each thread keeps its own THREAD_LOCAL freq_ctr and gets its own share
of the total, computed once at thread startup: thread <tid> gets
(N + tid) / nbthread, so the shares always add up to exactly N (no
rounding overshoot.

Tested by flooding a listener with short-header packets carrying random
DCIDs: the number of replies received matches the configured rate rather
than the number of packets sent, with normal traffic unaffected when well
under the limit.

2 months agoMINOR: haload: add -ee to probe and ramp back up after an error
Frederic Lecaille [Thu, 20 Aug 2026 13:53:58 +0000 (15:53 +0200)]
MINOR: haload: add -ee to probe and ramp back up after an error

-e stops the whole run on the first error. -ee is a softer variant:
on the first error, it resets all counters and drops to 1 conn/req
per second per URL, then doubles that rate every second, as long as
requests keep succeeding, until 100 successes per URL have been seen.
It then resumes the load asked for on the command line (uncapped, or
the original -R value if one was given).

The floor and target are expressed per URL, not per thread: with a
per-thread scaling, a machine with many threads would start the probe
far from gently. The doubling isn't capped at the original -R value on
the way up either; only the success count ends the ramp. It never
speeds up while the target is still failing, and reacts immediately to
the first sign of recovery rather than waiting for the next scheduled
check. A later, separate outage can trigger -ee again once a previous
ramp has completed.

A connection error means the affected user has no connection left
anyway, so it is killed rather than rescheduled for a retry. This lets
hld_rate_task() take over and create its replacement under the current
ramp rate, rechecking every 100ms instead of trusting a single,
possibly stale, wait time computed by hld_usr_schedule() (which, with
a large -u and a low rate, could grow to several minutes and would
never be reconsidered even as -ee doubles the rate afterwards). For
this to work:
  - hld_rate_task() is now created for every thread whenever -ee is
    used, even without -R (it otherwise only exists if -R is set, so
    -ee alone would have nothing to recreate users with).
  - it no longer destroys itself once the target concurrency is
    reached; under -ee it stays alive, idle, so it can be woken up
    later to refill after a kill. It still checks all_usr_stop_asap
    and destroys itself for good once set, so an idle task never
    prevents the process from exiting cleanly at the end of a test.
  - once the main task's own initial ramp-up is over (usr_cnt >=
    arg_usr), a rate of 0 (no limit) is treated as "fill directly",
    not "zero budget": otherwise, once -ee hands the rate back to 0
    after a successful probe, nothing could ever refill a later kill.
  - hld_usr_release() keeps the per-thread live user count accurate
    and wakes the rate task after a kill.

Each user also remembers the -ee generation it was created in. Only
successes from users created during the current ramp count towards
the 100 needed to end it: a pre-existing user that was never affected
by the error succeeding again does not mean the probe itself worked,
and counting it would let a few leftover in-flight requests, or an
unrelated majority of still-healthy users, satisfy the target almost
instantly while the target is still actually down.

2 months agoBUG/MINOR: haload: make -e react to request timeouts too
Frederic Lecaille [Thu, 20 Aug 2026 13:38:36 +0000 (15:38 +0200)]
BUG/MINOR: haload: make -e react to request timeouts too

-e only triggered on connection/protocol errors, not on
requests that silently timed out waiting for a response. Now that
timeouts are correctly counted as errors (previous commit), make them
trigger -e the same way any other error does.

2 months agoBUG/MINOR: haload: count timed-out requests as errors, not successes
Frederic Lecaille [Thu, 20 Aug 2026 13:35:27 +0000 (15:35 +0200)]
BUG/MINOR: haload: count timed-out requests as errors, not successes

In hld_usr_task(), requests reaped from the expiration list (no full
response received within -w) were counted as tot_done (success)
instead of tot_perr (failure). This made haload silently report
timeouts as successful requests, skewing tot_req/err in the live
summary and the final report.

2 months agoMINOR: haload: add -get/-head/-post options to select the HTTP method
Frederic Lecaille [Wed, 19 Aug 2026 18:36:58 +0000 (20:36 +0200)]
MINOR: haload: add -get/-head/-post options to select the HTTP method

Only GET (or HEAD globally via -I) could be sent before. Add -get,
-head and -post <size> as positional options, like -h(0|1|2|2c|3), to
pick the method and, for POST, the body size, per URL.

The method and body size are stored per URL path (struct hld_path) so
distinct paths sharing the same server address keep their own
settings. The POST body is filled from a static buffer, streamed
across several output buffers for large sizes, with a matching
Content-Length header.

Remove -I, superseded by -head. Update doc/haload.txt.

2 months agoIMPORT: slz/uslz: support SLZ_FMT_NONE format and force its value to 0
Aurelien DARRAGON [Fri, 21 Aug 2026 16:01:40 +0000 (18:01 +0200)]
IMPORT: slz/uslz: support SLZ_FMT_NONE format and force its value to 0

For library users that rely on SLZ_FMT_* (through uslz_init_fmt()), it is
useful to have a default value that does nothing and can be compared
against, as users will typically store the fmt in a dedicated variable or
within a structure which will be zero-initialized. Having the SLZ_FMT_NONE
alias is also useful to compare a stored fmt value against it to know
whether or not it was set to an other value than the default one.

As a bonus, uslz_init_fmt() also supports the SLZ_FMT_NONE instead of
returning an error, this way, uslz_init_fmt() can be used in place of
uslz_init() even when no compression is needed, which can help write
more generic code for library users where they just need to pass the fmt
value as argument and let the library to its job.

This is libslz upstream commit 4adc28e64716f3db5df9a1edc6eeb9234a37f6ad

2 months agoOPTIM: log/tools: switch from function pointer design to dual mode HEX/BIN design...
Aurelien DARRAGON [Fri, 21 Aug 2026 13:34:54 +0000 (15:34 +0200)]
OPTIM: log/tools: switch from function pointer design to dual mode HEX/BIN design for CBOR encoding

Time has shown that only the HEX/BIN mode for cbor encoding were relevant
so far. And the current design based on function pointers was proved
to be a lot CPU consuming, reducing logging performances by a significant
amount (even when binary CBOR was used) compared to logs without encoding.

In order to try to recover as much logging performance as possible,
especially for the CBOR binary encoder, let's switch to a simpler design
where all cbor encoding functions are proposed in 2 distincts variants:
one for binary encoding, and the other for hex (ie: ascii compatible)
encoding. The cost of that is a code that contains a little bit of
duplication, although this was limited thanks to the use of static
inline helpers where possible to group common code. The benefit is that
no function pointers are used anymore during the encoding chain, making
the branches more predictable for CPU predictions and compiler
optimizations. To further improve the performance in CBOR binary mode
a special case was performed in cbor_encode_uint64_prefix_bin():
as suggested by Willy, for x86_64 systems we can actually directly
write int64 using memcpy() since bits are already in the correct order.

2 months ago[RELEASE] Released version 3.5-dev5 v3.5-dev5
Willy Tarreau [Fri, 21 Aug 2026 09:50:56 +0000 (11:50 +0200)]
[RELEASE] Released version 3.5-dev5

Released version 3.5-dev5 with the following main changes :
    - MINOR: config: support "all" and "none" on "tune.defaults.purge"
    - BUG/MINOR: proxy: release watcher in various commands on CLI abort
    - BUG/MINOR: proxy: release watcher in "show default-server" on CLI abort
    - REGTESTS: proxy: complete "del backend" test
    - BUG/MEDIUM: hpack: encode long methods and schemes using the long form
    - BUG/MINOR: stats-file: reject tgid 0 when preloading shm objects
    - BUG/MINOR: hlua: use a local buffer to format the socket addresses
    - DEV: haring: bound the ring geometry to the size of the file
    - BUG/MEDIUM: mux-fcgi: check the room left before appending the index
    - BUG/MEDIUM: spoe: clear the applet pointer when the applet fails to start
    - BUG/MINOR: mux-fcgi: don't call fcgi_strm_destroy() on a NULL stream
    - BUG/MEDIUM: http-ana: don't crash on "keep-query" in a response redirect
    - BUG/MEDIUM: stick-tables: use the same bucket for string keys with a NUL
    - BUG/MEDIUM: sock: bound the recvmsg() length when receiving old sockets
    - BUG/MEDIUM: http-ana: check the cookie rewrite result before moving the offsets
    - BUG/MINOR: mux-fcgi: sanitize the STDERR records before logging them
    - MINOR: log/tools: fix ambiguous comments for some log encoding helpers
    - BUG/MEDIUM: log: always reserve room for trailing 0 when using CBOR encoding helpers
    - BUG/MEDIUM: jwe: validate the secret length against the algorithm of the token
    - BUG/MINOR: jwt: don't take an extra reference on the certificate public key
    - BUG/MEDIUM: ssl: require a full-length AEAD tag when decrypting with AES-GCM
    - BUG/MEDIUM: acme: don't delete a NULL token from the map
    - BUG/MINOR: ssl: reject server certificate names containing a NUL byte
    - BUG/MINOR: acme: restrict the permissions of the generated account key
    - MINOR: backend: Do not always allow takeover across thread groups
    - MINOR: backend: Deprecate tune.takeover-other-tg-connections.
    - DOC: Document limitations of tune.fd.tables
    - MEDIUM: pipes: Never allocate more than maxpipes
    - BUG/MEDIUM: fd: release the port range entry in host byte order
    - BUG/MINOR: connection: reserve the whole CRC32C TLV before saving its pointer
    - BUG/MEDIUM: session: don't release a reversed connection twice on error
    - BUG/MINOR: lb-chash: bound the walk when the saved cursor changed tree
    - BUG/MINOR: debug: only dump the trace once in __BUG_ON_ONCE()
    - BUG/MINOR: mux-h2: strip the userinfo when deriving :authority for a server
    - BUG/MEDIUM: quic: prevent out-of-bound read on wrapping CRYPTO content
    - MINOR: quic: stress CRYPTO Rx buffer wrapping
    - BUG/MINOR: quic: drop multiple Retry on same connection
    - BUG/MEDIUM: lua: resume Channel:send() from the unsent part of the string
    - BUG/MEDIUM: hlua_fcn: ensure systematic bref cleanup for patref list iterator
    - DOC: config: clarify req.ssl_sni
    - MINOR: ssl: add ssl_c_policies sample fetch
    - REGTESTS: ssl: add ssl_c_policies test
    - IMPORT: slz: update slz to version 1.3.0
    - IMPORT: slz: do not read past the end of the input around the match loop
    - IMPORT: slz: fix the documented worst case size of flush() and finish()
    - IMPORT: slz: use the exact switch cost for the last literals of a block
    - IMPORT: slz: bound the bits wasted by the 9-bit literals
    - IMPORT: slz: do not append a block to an already finished stream
    - IMPORT: slz: fix the adler32 accumulators signedness on 32-bit
    - IMPORT: slz: avoid undefined shifts when building the word byte by byte
    - IMPORT: slz: clarify that the size promise applies to the stream, not to a call
    - IMPORT: slz/uslz: fix incorrect sign extension in shift when reading the adler32 trailer
    - IMPORT: slz/uslz: don't reset the drain offset when nothing was decoded
    - IMPORT: slz/uslz: don't lose the bytes consumed by the header fast paths
    - IMPORT: slz/uslz: make the gzip FEXTRA field resumable
    - IMPORT: slz/uslz: widen crc_flush, it overflowed on large stored blocks
    - IMPORT: slz/uslz: verify the trailer checksum before reporting completion
    - IMPORT: slz/uslz: decode all the members of a multi-member gzip stream
    - IMPORT: slz/uslz: add a method to impose an envelope format (gzip or zlib)
    - IMPORT: slz/uslz: inline the bit reader and the huffman decoders
    - IMPORT: slz/uslz: defer the output bookkeeping to a checkpoint
    - BUG/MEDIUM: cache: retain the primary or secondary entry only when detaching its row
    - BUG/MINOR: cache: do not retain imcomplete or stripped secondary entry
    - BUG/MEDIUM: cache: do not release an entry under the cache read lock
    - BUG/MINOR: wurfl: fix memory leak of information list and patch strings at deinit
    - IMPORT: slz: prevent build compilation error due to unused __slz_make_crc_table()
    - BUG/MINOR: ssl: reject an embedded NUL in the ssl_*_dn(entry) fetches
    - BUG/MINOR: ssl: reject an embedded NUL in the full-DN ssl_*_dn() fetches
    - BUG/MINOR: quic: avoid a division by zero in the BBR pacing interval
    - BUG/MINOR: server: fix memory leak on "default-server" parsing failures
    - BUG/MINOR: h3: fix potential NULL pointer dereference in _h3_trace_header()
    - BUG/MINOR: hq_interop: fix potential NULL dereference in _hq_trace_http()
    - BUG/MINOR: server: fix off-by-one error when parsing and copying source port range
    - BUG/MEDIUM: http: fix authority parsing for absolute-form URI with empty path
    - DOC: security: clarify that only up-to-date versions may get securty reports
    - MINOR: sample: add new converter has_ctl() to detect control characters
    - BUG/MEDIUM: bwlim: fix a stick-table entry leak in shared mode
    - MEDIUM: ssl: add fips_mode() config condition predicate
    - MINOR: ssl: report FIPS mode in -vv for OpenSSL >= 3.0 too
    - REGTESTS: ssl: skip tests broken by FIPS mode
    - REGTESTS: checks: skip tcp-check-client-hello.vtc under FIPS mode
    - DEBUG: cli: unstatify cli_process_cmdline()
    - CLEANUP: debug: move ha_backtrace_to_stderr() declaration to bug.h
    - CLEANUP: debug: reorder definitions in bug.h
    - MINOR: debug: check the match count in __BUG_ON_ONCE() and not in complain()
    - DEBUG: cli: add a "debug dev abort" debugging command
    - DEBUG: deduplicate __ABORT_NOW() between DEBUG_USE_ABORT and ha_crash_now()
    - MINOR: debug: move printing of the hint to ha_backtrace_to_stderr()
    - MINOR: debug: use bit fields for the debug type and fatality
    - MINOR: debug: only emit and count warnings when explicitly requested
    - CLEANUP: debug: remove the now unused counter argument to complain()
    - MINOR: debug: make the complain() function start to complete the message
    - MEDIUM: debug: make the complain() function print the prefix
    - MEDIUM: debug: use \x1e (RS) to delimit the condition from vaargs in DBG_COUNT()
    - MEDIUM: debug: use \x1e (RS) to delimit the condition from vaargs in complain()
    - MEDIUM: debug: make the complain() function print the suffix
    - MINOR: debug: pass the type to the __BUG_ON*() macros
    - MINOR: debug: unify __BUG_ON() and __BUG_ON_ONCE()
    - MINOR: debug: avoid the break in __BUG_ON()
    - CLEANUP: debug: remove the unneeded do { } while (0) on __BUG_ON()
    - MINOR: debug: make a pair of __BUG_ON() for modern and obsolete linkers
    - MINOR: debug: rely on the __dbg_cnt_ counter for _BUG_ON_ONCE()
    - MINOR: debug: store the caller's details in debug_count
    - MINOR: debug: prepare complain() to be used in multiple ways
    - MEDIUM: debug: add a new version of complain() that takes a debug_count struct
    - MEDIUM: debug: do not dump a context-less backtrace in ABORT_NOW()
    - CLEANUP: debug: make __BUG_ON() use abort_with_line()
    - MEDIUM: debug: merge the counter and message emission into complain()
    - MINOR: compiler: add a function attribute to disable tail calls
    - MINOR: debug: force complain's backtrace to be a tail jump instead of a tail call
    - BUG/MINOR: payload: fix handshake length off-by-4 in ssl_hello_sni/alpn
    - MINOR: payload: make smp_fetch_ssl_hello_sni() use smp_client_hello_parse()
    - MINOR: payload: make smp_fetch_ssl_hello_alpn() use smp_client_hello_parse()
    - MINOR: ssl-gencert: use a 64-bit hash for the LRU cache key
    - MINOR: ssl-gencert: enforce a stronger seed for the LRU cache key
    - BUG/MINOR: spoe: check snprintf() return value in spoe_set_var/spoe_unset_var
    - BUG/MINOR: qpack: missing shift count check in qpack_get_varint() (UB)
    - BUG/MEDIUM: pollers: create the poller pipes in builds without threads
    - BUG/MEDIUM: tasks: don't type-pun state and tid in builds without threads
    - BUG/MINOR: log: fix double-free error when error in parse_loger occurs
    - BUG/MINOR: uri-auth: never risk to dereference a NULL on error path
    - BUG/MINOR: mux-h2: harden h2_dump_h2s_info() against potentially null h2s->sd
    - BUILD: protocol: silence -Warray-bounds in single-group builds
    - CI: github: add a TARGET=generic job in matrix.py
    - BUG/MINOR: conn: Do not check 'sess_el' list on frontend connections in __trace_enabled
    - BUG/MINOR: ssl: Fix leak of X509_NAME in traces
    - BUG/MINOR: ssl: release the previous client cert reference at depth > 0
    - BUG/MEDIUM: ssl: Fix unprotected 'ssl_sock_choose_sni_ctx' calls
    - BUG/MEDIUM: ssl: enforce tune.ssl.lifetime across TLS1.3 session renewals
    - BUG/MINOR: ssl: apply tune.ssl.lifetime to TLS1.3 sessions on BoringSSL/AWS-LC
    - BUILD: ssl: disable the TLS1.3 session timeout clamp on wolfSSL
    - BUG/MEDIUM: ssl: isolate TLS session resumption per X509 server certificate
    - BUG/MINOR: ssl: isolate TLS session resumption per crt-list filter
    - BUG/MEDIUM: ssl: isolate TLS session resumption per authentication policy
    - BUG/MEDIUM: tasks: don't type-pun state and tid in builds without threads
    - BUG/MEDIUM: pollers: really create the poller pipes in builds without threads
    - REGTESTS: don't rely on the stick-table dump order in the ipmask test
    - CLEANUP: payload: remove dead code in smp_client_hello_parse()
    - BUG/MINOR: haproxy: fix out-of-scope tmpdir usage in do_chroot()
    - BUG/MINOR: ssl/cli: fix frontend-not-found detection in 'show ssl sni -f'
    - MINOR: backend: Do not bother stopping if there is only one thread group
    - REGTESTS: converters: use connection: close in the has_ctl() test
    - BUG/MINOR: resolvers: accept fields at the response boundary
    - BUG/MINOR: dns: schedule idle task after setting its thread
    - CLEANUP: dns: centralize nameserver transport cleanup
    - MINOR: ssl: Forbid using ssl-mode-async with per tgroup fd tables
    - MINOR: hlua: Forbid lua-load with per-tgroup fd tables
    - MINOR: addons/51d: Forbid use with per tgroup fd tables
    - MINOR: addons/wurfl: Forbid use with per tgroup fd tables
    - MINOR: addons/deviceatlas: Forbid use with per tgroup fd tables
    - BUG/MEDIUM: sink: pre-initialize the implicit log forwarding server

2 months agoBUG/MEDIUM: sink: pre-initialize the implicit log forwarding server
Willy Tarreau [Wed, 19 Aug 2026 15:24:14 +0000 (17:24 +0200)]
BUG/MEDIUM: sink: pre-initialize the implicit log forwarding server

Since commit 368d01361a ("MEDIUM: server: add and use srv_init() function")
a "log tcp@127.0.0.1:514" line in the global section causes a segfault at
boot, without even trying to connect anywhere.

In this case the target is a ring, which relies on a server to forward
its data. That server is created by sink_new_from_logger(), which is
reached from postresolve_loggers() in REGISTER_POST_CHECK, so it runs
after check_config_validity() has walked all_servers to call srv_preinit()
on every known servers, and after the REGISTER_POST_SERVER_CHECK hooks.
The server therefore never gets initialized, srv->per_thr and srv->per_tgrp
are left NULL, and the first forwarding session dies in stream_set_target()
on per_tgrp[tgid - 1].nb_strm.

sink_new_from_logger() used to call srv_init_per_thr() itself before
the change above, srv_init() has since been split, and the per-thread
part is now srv_preinit(), so let's just call that from there.

Note that the same ordering means these servers also miss srv_postinit(),
so their shared counters stay NULL. They are not used with sinks so we
probably don't care and would better not touch that now, but it's worth
keeping in mind.

This should be backported to 3.3.

2 months agoMINOR: addons/deviceatlas: Forbid use with per tgroup fd tables
Olivier Houchard [Wed, 19 Aug 2026 14:16:20 +0000 (16:16 +0200)]
MINOR: addons/deviceatlas: Forbid use with per tgroup fd tables

It is not known if deviceatlas uses its own file descriptors, so for the time
being, forbid its usage with tune.fd.tables per-thread-group.

2 months agoMINOR: addons/wurfl: Forbid use with per tgroup fd tables
Olivier Houchard [Wed, 19 Aug 2026 14:15:52 +0000 (16:15 +0200)]
MINOR: addons/wurfl: Forbid use with per tgroup fd tables

It is not known if wurfl uses its own file descriptors, so for the time
being, forbid its usage with tune.fd.tables per-thread-group.

2 months agoMINOR: addons/51d: Forbid use with per tgroup fd tables
Olivier Houchard [Wed, 19 Aug 2026 14:14:51 +0000 (16:14 +0200)]
MINOR: addons/51d: Forbid use with per tgroup fd tables

It is not known if 51d uses its own file descriptors, so for the time
being, forbid its usage with tune.fd.tables per-thread-group.

2 months agoMINOR: hlua: Forbid lua-load with per-tgroup fd tables
Olivier Houchard [Wed, 19 Aug 2026 14:10:50 +0000 (16:10 +0200)]
MINOR: hlua: Forbid lua-load with per-tgroup fd tables

lua-load may be used to create file descriptors that it expects to be
global to the process, which is not true file tune.fd.tables
per-thread-group, so forbid it.

2 months agoMINOR: ssl: Forbid using ssl-mode-async with per tgroup fd tables
Olivier Houchard [Wed, 19 Aug 2026 14:09:41 +0000 (16:09 +0200)]
MINOR: ssl: Forbid using ssl-mode-async with per tgroup fd tables

ssl-mode-async requires file descriptors global to the process, which is
not true with tune.fd.tables per-thread-group, so just forbid it.

2 months agoCLEANUP: dns: centralize nameserver transport cleanup
Manu Nicolas [Tue, 18 Aug 2026 17:24:48 +0000 (17:24 +0000)]
CLEANUP: dns: centralize nameserver transport cleanup

Several resolver configuration error paths open-code nameserver
transport cleanup. They also assume that a nameserver owns either a
datagram transport or a stream transport, but never both.

Add dns_nameserver_deinit() and use it from these error paths. The
helper releases each transport's configuration-time allocations
independently and clears its pointer. No functional change is intended.

2 months agoBUG/MINOR: dns: schedule idle task after setting its thread
Manu Nicolas [Tue, 18 Aug 2026 17:24:47 +0000 (17:24 +0000)]
BUG/MINOR: dns: schedule idle task after setting its thread

Commit b8a311f49 ("MEDIUM: dns: Stick the TCP nameserver tasks to the
resolvers' thread") assigns each stream nameserver task to the resolver
thread during configuration finalization. However, dns_stream_init()
has already queued the idle task by then.

task_set_thread() removes the task from its old wait queue and queues
it again after changing its owner. The current thread still belongs to
the old owner, which violates wait-queue ownership. Strict builds hit
the task->tid assertion while finalizing a configuration containing a
TCP nameserver.

Leave the idle task unqueued during stream initialization. Once
finalization has set all three stream task affinities, wake the idle
task so it schedules its expiration on the resolver thread.

No backport is needed. The offending commit is only present in the
development branch.

2 months agoBUG/MINOR: resolvers: accept fields at the response boundary
Manu Nicolas [Tue, 18 Aug 2026 17:24:45 +0000 (17:24 +0000)]
BUG/MINOR: resolvers: accept fields at the response boundary

resolv_validate_dns_response() treats <bufend> as an exclusive end
pointer. Its fixed-width field checks nevertheless reject
"reader + 2 == bufend", although reader[0] and reader[1] are both
within the response.

Use a strict greater-than comparison for every two-byte DNS header and
question field. No complete successful answer is affected, but this
corrects the parser contract and lets later checks classify minimal
responses instead of rejecting them early as invalid.

This may be backported as far as 2.4.

2 months agoREGTESTS: converters: use connection: close in the has_ctl() test
Willy Tarreau [Wed, 19 Aug 2026 09:17:23 +0000 (11:17 +0200)]
REGTESTS: converters: use connection: close in the has_ctl() test

The reg-test used to test has_ctl() introduced by commit 33374957db
("MINOR: sample: add new converter has_ctl() to detect control characters")
is flaky due to the server not using "connection: close", so under load
it randomly shows "HTTP header incomplete". Let's add the close like in
many other similar tests involving server reuse.

This should be backported wherever the converter above is backported.

2 months agoMINOR: backend: Do not bother stopping if there is only one thread group
Olivier Houchard [Tue, 18 Aug 2026 15:42:03 +0000 (17:42 +0200)]
MINOR: backend: Do not bother stopping if there is only one thread group

We forbid sharing idle connection across thread groups if there is
per-thread group file descriptor tables, and that will cause an error,
but don't bother doing that if after all, there is only one thread
group.

2 months agoBUG/MINOR: ssl/cli: fix frontend-not-found detection in 'show ssl sni -f'
William Lallemand [Tue, 18 Aug 2026 15:16:54 +0000 (15:16 +0000)]
BUG/MINOR: ssl/cli: fix frontend-not-found detection in 'show ssl sni -f'

Coverity (CID 1664029, GH #3466) points out that `px == NULL` after the
list_for_each_entry loop in cli_parse_show_sni() can never be true:
list_for_each_entry's loop variable, when the list is exhausted without
a break, is a container_of-derived pointer computed from the list
head's address, never NULL. This left the "-f <bogus-frontend>" case
silently falling through instead of erroring: the bug pre-dates
9993f29007, which fixed the found case (adding the missing break) but
kept comparing the wrong thing for the not-found case. Track "found"
explicitly instead.

Add reg-tests/ssl/show_ssl_sni_filter.vtc to cover the -f option,
including the not-found error case.

This must be backported to 3.2.

2 months agoBUG/MINOR: haproxy: fix out-of-scope tmpdir usage in do_chroot()
William Lallemand [Tue, 18 Aug 2026 14:41:20 +0000 (14:41 +0000)]
BUG/MINOR: haproxy: fix out-of-scope tmpdir usage in do_chroot()

Coverity (CID 1660133, GH #3470) reports that dir may point to the
block-local tmpdir[] array after it has gone out of scope, since dir
is read again in the "Cannot chroot()" alert past the end of the
"chroot auto" if-block. Move tmpdir's declaration to function scope
so its lifetime covers every use of dir.

Must be backported to 3.4.

2 months agoCLEANUP: payload: remove dead code in smp_client_hello_parse()
William Lallemand [Tue, 18 Aug 2026 14:27:34 +0000 (14:27 +0000)]
CLEANUP: payload: remove dead code in smp_client_hello_parse()

Remove dead code reported by Coverity (CID 1660132, GH #3471). bleft
is already guaranteed to be >= hs_len >= 49 by the two preceding
checks (bleft < 5 / hs_len < 49 / bleft < hs_len), so the
`if (bleft < 1)` check right after can never be true.

2 months agoREGTESTS: don't rely on the stick-table dump order in the ipmask test
Olivier Houchard [Tue, 18 Aug 2026 12:49:03 +0000 (14:49 +0200)]
REGTESTS: don't rely on the stick-table dump order in the ipmask test

"show table" dumps the entries bucket by bucket, so their order depends
on CONFIG_HAP_TBL_BUCKETS, which is 1 in builds without threads (e.g.
TARGET=generic) and 8 or more with them. The test hardcoded the order
seen with multiple buckets and failed on single-bucket builds.

Check the header and each of the three entries with a separate request
instead, so the order doesn't matter anymore.

2 months agoBUG/MEDIUM: pollers: really create the poller pipes in builds without threads
Olivier Houchard [Tue, 18 Aug 2026 12:31:00 +0000 (14:31 +0200)]
BUG/MEDIUM: pollers: really create the poller pipes in builds without threads

Commit c5f41118c ("BUG/MEDIUM: tasks: don't type-pun state and tid in
builds without threads") was mangled when committed: instead of the task
fix its message describes, its content accidentally reverted commit
cc0c18d82 ("BUG/MEDIUM: pollers: create the poller pipes in builds
without threads"), moving the fd_precreate_poller_pipes() call back into
setup_extra_threads(), an empty stub without threads. Builds made with
USE_THREAD unset (e.g. TARGET=generic) thus abort again on startup on
BUG_ON(fdtab[fd].owner) when inserting the poller pipes.

Move the call back to haproxy.c, right before setup_extra_threads(), so
that all builds share the same path.

2 months agoBUG/MEDIUM: tasks: don't type-pun state and tid in builds without threads
Olivier Houchard [Fri, 14 Aug 2026 16:59:44 +0000 (18:59 +0200)]
BUG/MEDIUM: tasks: don't type-pun state and tid in builds without threads

__task_set_state_and_tid() updates t->state and t->tid at once through
an uint64_t cast, which violates strict aliasing. With threads the
__atomic builtins keep the compiler conservative, but without them the
accesses are plain, and gcc -O2 legitimately keeps using a stale
t->state: the CAS always fails and _task_wakeup() spins forever on the
first request until the watchdog kills the process.

Without threads there is no concurrency to protect against, so just
compare and assign the two fields directly.

This is what commit c5f41118c01db4babcf6919f5eae4ce308adcfef should have
been.

2 months agoBUG/MEDIUM: ssl: isolate TLS session resumption per authentication policy
William Lallemand [Tue, 18 Aug 2026 07:26:53 +0000 (07:26 +0000)]
BUG/MEDIUM: ssl: isolate TLS session resumption per authentication policy

The session id context OpenSSL uses to allow or reject
session resumption) was so far scoped only to the frontend's own
certificate. Two SSL_CTXs presenting the same certificate but different
client-certificate policies (verify mode, ca-file, crl-file,
ca/crt-ignore-err) still shared that sid_ctx, so a session or ticket
obtained on a weakly-verified listener could be resumed on another
listener enforcing stricter mutual TLS, without ever re-presenting a
certificate.

Fix this by also hashing the verify mode, every CA certificate and CRL
loaded in the SSL_CTX's X509_STORE (ssl_ctx_hash_store()), and the
ca/crt-ignore-err bitfields into sid_ctx. CA certificates are hashed
the same way as leaf certificate (issuer/subject/serial); CRLs are
hashed by issuer and CRL Number extension.

Must be backported to every stable branch.

Reported-by: Yiyi Wang <wangyiyi25@mails.tsinghua.edu.cn>
Reported-by: Chris Jarrett-Davies <chrisjd@openai.com>
2 months agoBUG/MINOR: ssl: isolate TLS session resumption per crt-list filter
William Lallemand [Tue, 18 Aug 2026 07:10:49 +0000 (07:10 +0000)]
BUG/MINOR: ssl: isolate TLS session resumption per crt-list filter

ssl_set_session_id_context() folds the frontend's own leaf certificate
identity into sid_ctx, but a crt-list can point several entries at the
exact same certificate file, distinguished only by their SNI filter,
e.g.:

    cert.pem foo.example.com
    cert.pem bar.example.com

Each entry gets its own SSL_CTX/ckch_inst, but since both share the same
certificate, they still end up with an identical sid_ctx, and a session
established while connecting to "foo.example.com" can be resumed under
"bar.example.com" (or any other filter sharing that certificate) without
the servername callback/SNI match ever running again on the resumed
handshake.

Fix the issue by adding a ckch_inst parameter in order to hash the
filters provided by the crt-list.

Must be backported in every stable branches.

2 months agoBUG/MEDIUM: ssl: isolate TLS session resumption per X509 server certificate
William Lallemand [Fri, 14 Aug 2026 12:02:42 +0000 (12:02 +0000)]
BUG/MEDIUM: ssl: isolate TLS session resumption per X509 server certificate

The shared SSL session cache (stateful TLS tickets) was using the same
context ID (SHCTX_APPNAME) across all SSL_CTX in the process. It allowed
a client to resume from one bind line to another even if the SSL_CTX
wasn't using the same key/cert pair.

Stateless TLS tickets are less impacted since a random tls-ticket-key is
automatically generated by SSL_CTX, only bind lines with the same
tls-ticket-keys would have the problem.

This patch fixes the issue by setting a session ID derived from the X509
certificate,a SHA-256 digest of the SHCTX_APPNAME. plus the Issuer,
Subject and Serial of the frontend's own leaf certificate
(ssl_ctx_hash_leaf_X509()

Two SSL_CTX serving different certificates now get different sid_ctx
values, so OpenSSL's own resumption check actually has something to
compare and rejects cross-certificate resumption instead of always
matching.

This must be backported in every stable release.

2 months agoBUILD: ssl: disable the TLS1.3 session timeout clamp on wolfSSL
William Lallemand [Tue, 18 Aug 2026 12:01:55 +0000 (12:01 +0000)]
BUILD: ssl: disable the TLS1.3 session timeout clamp on wolfSSL

wolfSSL's OpenSSL compat layer declares wolfSSL_get_SSL_CTX() (aka
SSL_get_SSL_CTX()) taking a non-const WOLFSSL*, unlike OpenSSL's
const-correct SSL_get_SSL_CTX(const SSL *ssl). Calling it from
ssl_sock_infocbk(), which only has a const SSL *, fails the build:

  src/ssl_sock.c:1772:60: error: passing 'const SSL *' (aka 'const
  struct WOLFSSL *') to parameter of type 'WOLFSSL *' (aka 'struct
  WOLFSSL *') discards qualifiers
  [-Werror,-Wincompatible-pointer-types-discards-qualifiers]

Beyond the build failure, the clamp itself doesn't appear to behave
correctly against wolfSSL's TLS1.3 session/ticket handling, so skip
the whole thing on wolfSSL builds instead of only working around the
compile error.

Must be backported in every stable branches along with 9f1d590
("BUG/MEDIUM: ssl: enforce tune.ssl.lifetime across TLS1.3 session
renewals")

2 months agoBUG/MINOR: ssl: apply tune.ssl.lifetime to TLS1.3 sessions on BoringSSL/AWS-LC
William Lallemand [Tue, 18 Aug 2026 11:32:48 +0000 (11:32 +0000)]
BUG/MINOR: ssl: apply tune.ssl.lifetime to TLS1.3 sessions on BoringSSL/AWS-LC

SSL_CTX_set_timeout() is documented on BoringSSL/AWS-LC as covering only
TLS 1.2 (or earlier) sessions. TLS 1.3 sessions on these libs have their
own, separate lifetime knob, SSL_CTX_set_session_psk_dhe_timeout(), which
HAProxy never called. As a result, "tune.ssl.lifetime" silently had no
effect on TLS 1.3 sessions when built against BoringSSL/AWS-LC, which
instead defaulted to those libs' hardcoded 2-day PSK/DHE timeout.

Also call SSL_CTX_set_session_psk_dhe_timeout() with the same configured
value when tune.ssl.lifetime is set, on BoringSSL/AWS-LC builds.

Note there is still a separate, hardcoded 7-day authentication timeout
underneath this on these libs, with no public API to override it, so the
effective cap there becomes min(tune.ssl.lifetime, 7 days) rather than an
exact match to tune.ssl.lifetime.

Must be backported in every stable branches.

2 months agoBUG/MEDIUM: ssl: enforce tune.ssl.lifetime across TLS1.3 session renewals
William Lallemand [Tue, 18 Aug 2026 11:27:17 +0000 (13:27 +0200)]
BUG/MEDIUM: ssl: enforce tune.ssl.lifetime across TLS1.3 session renewals

On TLS 1.3, OpenSSL (and most derivatives) hands out a new session
ticket on every resumption, bumping that session's creation time to
"now" while leaving its timeout untouched. Each renewal therefore gets
a fresh, full "tune.ssl.lifetime" window instead of inheriting what's
left of the original one, so a client resuming just before each
ticket's window closes can keep a session alive indefinitely, well past
its original full handshake's deadline and past any point where a
client certificate would need to be re-verified for expiration or
revocation.

Fix this in ssl_sock_infocbk()'s SSL_CB_ACCEPT_LOOP handler: once per
handshake, clamp the session's remaining budget to what's actually left
of its ORIGINAL lifetime, by rewriting its creation time to "now" and
shrinking its timeout by however much real time has elapsed since it
was created (capped to the currently configured "tune.ssl.lifetime", in
case it was lowered since the session was first issued). Since
OpenSSL's own per-hop duplication only touches the creation time and
leaves whatever timeout is already on the object alone, the shrunk
timeout carries forward from hop to hop instead of resetting, so the
chain still eventually hits its original deadline.

This fix does not carry over to BoringSSL/AWS-LC: on those libs,
it need a different fix instead, handled separately.

Must be backported in every stable branches.

2 months agoBUG/MEDIUM: ssl: Fix unprotected 'ssl_sock_choose_sni_ctx' calls
Remi Tricot-Le Breton [Mon, 17 Aug 2026 13:26:24 +0000 (15:26 +0200)]
BUG/MEDIUM: ssl: Fix unprotected 'ssl_sock_choose_sni_ctx' calls

The 'ssl_sock_choose_sni_ctx' function accesses the sni tree and it
mostly covered by a read/write lock which was not taken around two of
its uses. One of them could be reached by using the SSL-related CLI
commands and the other one when the generate-cert mechanism is used. So
even if it's in a not that common use case, a race could still happen
and end up crashing.

Note that this requires that the read lock is properly released
before calling ssl_sock_generate_certificate() so as to avoid a
possible read-lock recursion (it was verified that this is safe).

This patch should be backported up to 3.0.

Reported-by: Claude (ANT-2026-VFR5JH3R)
2 months agoBUG/MINOR: ssl: release the previous client cert reference at depth > 0
Remi Tricot-Le Breton [Thu, 6 Aug 2026 07:36:48 +0000 (09:36 +0200)]
BUG/MINOR: ssl: release the previous client cert reference at depth > 0

The verify callback stores a reference to the client's certificate so that
it can still be reported in logs when the verification fails. At depth 0
it correctly releases the reference that may have been stored by an
earlier call before replacing it, but the depth > 0 branch, which does the
same for chain certificates, overwrote the stored pointer without
releasing it first.

For deployments using "ca-ignore-err", a client presenting a chain that
triggers more than one ignored CA-level error therefore leaks one X509
object per extra error, on every single handshake. Since the client fully
controls the chain it presents and the rate at which it connects, memory
grows without bound until the process is killed.

Let's free the previous reference in that branch too, as the depth 0
branch does.

This was introduced in 2.5 by commit 74f6ab6e8 ("MEDIUM: ssl: Keep a
reference to the client's certificate for use in logs"). It must be
backported to all stable versions.

Reported-by: Claude (ANT-2026-6WW7AT7V)
2 months agoBUG/MINOR: ssl: Fix leak of X509_NAME in traces
Remi Tricot-Le Breton [Mon, 17 Aug 2026 16:05:52 +0000 (18:05 +0200)]
BUG/MINOR: ssl: Fix leak of X509_NAME in traces

When X509_NAME_oneline() is called without a buffer, it allocates one
which was never free'd in the SSL traces' code. Rather than free it, we
might as well use a trash to avoid an allocation.

This was introduced in 3.2 by commit 4a8fa28e3 ("MINOR: ssl: Add traces
around SSL_do_handshake call"). It must be backported to 3.2.

Reported-by: Claude (ANT-2026-4P18WC9H)
2 months agoBUG/MINOR: conn: Do not check 'sess_el' list on frontend connections in __trace_enabled
Remi Tricot-Le Breton [Mon, 17 Aug 2026 15:48:09 +0000 (17:48 +0200)]
BUG/MINOR: conn: Do not check 'sess_el' list on frontend connections in __trace_enabled

When called on a frontend connection, the 'sess_el' list that is checked
in '__trace_enabled' was never initialized (see 'conn_backend_init' and
'conn_frontend_init').

This patch can be backported to all stable branches.

2 months agoCI: github: add a TARGET=generic job in matrix.py
William Lallemand [Fri, 14 Aug 2026 16:15:28 +0000 (18:15 +0200)]
CI: github: add a TARGET=generic job in matrix.py

Add a TARGET=generic job which compiles with openssl.

2 months agoBUILD: protocol: silence -Warray-bounds in single-group builds
Olivier Houchard [Fri, 14 Aug 2026 16:59:44 +0000 (18:59 +0200)]
BUILD: protocol: silence -Warray-bounds in single-group builds

When MAX_TGROUPS is 1 (e.g. built without threads), gcc warns that the
ha_tgroup_ctx[] accesses in protocol_grab_foreign_receivers() may be out
of bounds, as it cannot know that nbtgroups < 2 makes them unreachable.

2 months agoBUG/MINOR: mux-h2: harden h2_dump_h2s_info() against potentially null h2s->sd
Willy Tarreau [Fri, 14 Aug 2026 15:56:44 +0000 (17:56 +0200)]
BUG/MINOR: mux-h2: harden h2_dump_h2s_info() against potentially null h2s->sd

Not really sure if this can happen but since the code already takes care
of testing the sd, better make sure everywhere it's dereferenced it's
tested. And h2s_sc() does dereference h2s->sd which is later tested in
that function.

Under normal circumstances it's fine (when called from the stream via
h2_show_fd()) but it's less certain when dumping FDs via h2_show_fd()
which is called by the CLI's "show fd" debugging command. Let's just
guard all h2s_sc() under an sd test then.

Thanks to Ilya for reporting this Coverity finding in GH issue #3475.

This can be backported wherever it applies.

2 months agoBUG/MINOR: uri-auth: never risk to dereference a NULL on error path
Willy Tarreau [Fri, 14 Aug 2026 15:48:14 +0000 (17:48 +0200)]
BUG/MINOR: uri-auth: never risk to dereference a NULL on error path

The fix in commit 2dfbc311a8 ("BUG/MINOR: uri-auth: avoid leaks on
initialization error") added *root = NULL, but the functions support
being called with root == NULL, though nothing does it in the whole
code. At least it triggers Coverity (see github issue #3476 and #3477).
So let's fix it.

No backport is needed since it cannot happen in the current code base.

2 months agoBUG/MINOR: log: fix double-free error when error in parse_loger occurs
Kirill Furman [Fri, 14 Aug 2026 13:10:02 +0000 (16:10 +0300)]
BUG/MINOR: log: fix double-free error when error in parse_loger occurs

Passing incorrect argments in parse_logger() which contains sample and
some values, value of logger->lb.smp_rgs will equal to smp_rgs and then,
if goto error: occurs we firstly free(smp_rgs) and after it
free(logger->lb.smp_rgs) inside free_logger() function causes double-free.
It has been reported in GH #3458

Fix it by checking the value of logger->lb.smp_rgs inside error:
statement in parse_logger(), if it equal to smp_rgs - just assign
NULL to logger->lb.smp_rgs.

This fix should be backported to versions 2.6 and upper.

2 months agoBUG/MEDIUM: tasks: don't type-pun state and tid in builds without threads
Olivier Houchard [Fri, 14 Aug 2026 14:33:10 +0000 (16:33 +0200)]
BUG/MEDIUM: tasks: don't type-pun state and tid in builds without threads

__task_set_state_and_tid() updates t->state and t->tid at once through
an uint64_t cast, which violates strict aliasing. With threads the
__atomic builtins keep the compiler conservative, but without them the
accesses are plain, and gcc -O2 legitimately keeps using a stale
t->state: the CAS always fails and _task_wakeup() spins forever on the
first request until the watchdog kills the process.

Without threads there is no concurrency to protect against, so just
compare and assign the two fields directly.

2 months agoBUG/MEDIUM: pollers: create the poller pipes in builds without threads
Olivier Houchard [Fri, 14 Aug 2026 14:33:10 +0000 (16:33 +0200)]
BUG/MEDIUM: pollers: create the poller pipes in builds without threads

Commit 5f548bc8e ("MEDIUM: pollers: Create the poller pipes before we
create the thread") creates them from setup_extra_threads(), which is an
empty stub when built without threads (e.g. TARGET=generic). The pipes
then stay uninitialized and each thread inserts fd 0 twice, hitting
BUG_ON(fdtab[fd].owner != NULL) at boot.

Move the call to haproxy.c just before setup_extra_threads() so that all
builds share the same path.

2 months agoBUG/MINOR: qpack: missing shift count check in qpack_get_varint() (UB)
Frederic Lecaille [Fri, 14 Aug 2026 13:26:05 +0000 (15:26 +0200)]
BUG/MINOR: qpack: missing shift count check in qpack_get_varint() (UB)

Parsing QPACK varints with excessive continuation bytes increases shift
by 7 on each iteration. When shift reaches 63 or higher, evaluating:

    (v & 127) > (limit - ret) >> shift

executes a bitwise shift on a 64-bit integer with an out-of-range count,
which is Undefined Behavior in C.

Fix this by jumping to too_large label if shift >= 63 before the shift runs.

Thank you to @OneZ3r0 for having reported this issue.

Should be backported to all versions.

2 months agoBUG/MINOR: spoe: check snprintf() return value in spoe_set_var/spoe_unset_var
William Lallemand [Fri, 14 Aug 2026 09:39:12 +0000 (09:39 +0000)]
BUG/MINOR: spoe: check snprintf() return value in spoe_set_var/spoe_unset_var

snprintf() returns the length it would have written, not what actually
fit in varname[64]. A SET-VAR/UNSET-VAR name long enough to overflow
that buffer left len oversized, causing an out-of-bounds stack read in
the variable-name validation and hashing in vars_fill_desc().

Reject the action instead of proceeding on truncation: an oversized
name would otherwise be silently used under a different, truncated
name, which could let two distinct long names collide on the same
63-byte prefix.

This should be backported to all stable branches.

Reported-by: Aisle Research
Reported-by: Oyvind Albrigtsen <oalbrigt@redhat.com>
2 months agoMINOR: ssl-gencert: enforce a stronger seed for the LRU cache key
William Lallemand [Fri, 14 Aug 2026 09:07:12 +0000 (09:07 +0000)]
MINOR: ssl-gencert: enforce a stronger seed for the LRU cache key

The generated-certificate LRU cache key is seeded with the process
start time (time(NULL)).

This is not considered a bug per se: 'generate-certificates' is
generally deployed in MITM/interception contexts where the operator
already controls both ends of the connection. Still, relying on the
process start time for this seed is fragile, so switch to
ha_random64(), which is seeded at boot from /dev/urandom and
OpenSSL's RAND_bytes().

2 months agoMINOR: ssl-gencert: use a 64-bit hash for the LRU cache key
William Lallemand [Fri, 14 Aug 2026 09:06:34 +0000 (09:06 +0000)]
MINOR: ssl-gencert: use a 64-bit hash for the LRU cache key

The generated-certificate LRU cache indexes its entries with a 64-bit
key (lru64), but ssl_gencert.c was only feeding it a 32-bit XXH32
hash of the requested SNI. Use XXH3 (64-bit) instead, so the full key
space of the underlying cache is actually used.

2 months agoMINOR: payload: make smp_fetch_ssl_hello_alpn() use smp_client_hello_parse()
William Lallemand [Thu, 13 Aug 2026 16:42:50 +0000 (16:42 +0000)]
MINOR: payload: make smp_fetch_ssl_hello_alpn() use smp_client_hello_parse()

Same rationale as the previous commit for smp_fetch_ssl_hello_sni():
avoid duplicating the ClientHello parsing already factored out in
smp_client_hello_parse(). No behavior change.

2 months agoMINOR: payload: make smp_fetch_ssl_hello_sni() use smp_client_hello_parse()
William Lallemand [Thu, 13 Aug 2026 16:42:06 +0000 (16:42 +0000)]
MINOR: payload: make smp_fetch_ssl_hello_sni() use smp_client_hello_parse()

Avoid duplicating the ClientHello parsing already factored out in
smp_client_hello_parse(), like the other req_ssl_*/ssl_* fetches
already do. No behavior change.

2 months agoBUG/MINOR: payload: fix handshake length off-by-4 in ssl_hello_sni/alpn
William Lallemand [Thu, 13 Aug 2026 15:52:25 +0000 (15:52 +0000)]
BUG/MINOR: payload: fix handshake length off-by-4 in ssl_hello_sni/alpn

smp_fetch_ssl_hello_sni() and smp_fetch_ssl_hello_alpn() each have
their own copy of the ClientHello preamble parser instead of using the
shared smp_client_hello_parse(). The handshake length ("hs_len") is
compared against "bleft" before "bleft" is decremented for the 4-byte
handshake header (msg_type + 3-byte length), so a ClientHello claiming
a body up to 4 bytes larger than what was actually sent is still
accepted as complete. Every subsequent length-derived bound inherits
this same 4-byte over-count, letting the final read (SNI hostname or
ALPN protocol name) run up to 4 bytes past the received buffer and
disclose uninitialized memory through req.ssl_sni / req.ssl_alpn.

This is the same issue already fixed in smp_client_hello_parse() by
commit 2653936510 ("BUG/MINOR: payload: fix the handshake length
bounds check smp_client_hello_parse()"), just never ported to these
two functions' own duplicated preamble. This applies the same fix:
"data += 4" now comes with "bleft -= 4" before the length check
instead of after it, and the record-layer length is checked as soon
as the handshake is entered.

This must be backported to all supported versions.

2 months agoMINOR: debug: force complain's backtrace to be a tail jump instead of a tail call
Willy Tarreau [Tue, 11 Aug 2026 11:56:44 +0000 (13:56 +0200)]
MINOR: debug: force complain's backtrace to be a tail jump instead of a tail call

We can make complain_with_dbg() and complain() disappear from the
backtrace by forcing tail call elimination on these functions, since
ha_backtrace_to_stderr() is called last and with a compatible return
code.

Thanks to this, backtraces are clean again:

  WARNING: check condition "one > zero" matched at src/debug.c:1170 (please report to developers)
  This was triggered on purpose from the CLI 'debug dev check' command.
    call trace(13):
    |       0x6ea2c0 <09 9a 00 e8 b0 ef ff ff]: debug_parse_cli_check+0x40/0x42 > complain_with_dbg
    |       0x69724a <bd a8 fd ff ff 41 ff d0]: main+0x25154a
    |       0x69863c <00 00 00 e8 44 e9 ff ff]: cli_io_handler+0x54c/0xdee > main+0x251280
    |       0x710830 <00 0f 29 45 c0 ff 52 18]: task_process_applet+0x340/0xd84
    |       0x7b595b <00 00 4c 89 d7 41 ff d1]: run_tasks_from_lists+0x43b/0xbdb
    |       0x7b651a <8d 7d b0 e8 06 f0 ff ff]: process_runnable_tasks+0x41a/0xaae > run_tasks_from_lists
    |       0x70344e <01 00 00 e8 b2 2c 0b 00]: run_poll_loop+0xae/0x522 > process_runnable_tasks
    |       0x703b4f <00 00 00 e8 51 f8 ff ff]: run_thread_poll_loop+0x27f/0x4ba > run_poll_loop
    |       0x44703f <46 b2 00 e8 91 c8 2b 00]: main+0x133f/0x1d75 > run_thread_poll_loop

2 months agoMINOR: compiler: add a function attribute to disable tail calls
Willy Tarreau [Tue, 11 Aug 2026 13:14:31 +0000 (15:14 +0200)]
MINOR: compiler: add a function attribute to disable tail calls

In some cases we want a tail call to be turned into a jump, which is
sometimes referred to as "tail call elimination". Gcc and clang have
options for this, so let's add an attribute "eliminate_tail_calls" that
works with both.

2 months agoMEDIUM: debug: merge the counter and message emission into complain()
Willy Tarreau [Tue, 11 Aug 2026 11:43:46 +0000 (13:43 +0200)]
MEDIUM: debug: merge the counter and message emission into complain()

The event counter in debug mode moved to complain_with_dbg(). This
ensures that the deferencing of the counter, the atomic ops, the
condition to skip the emission are all centralized and are no longer
inline in the BUG_ON() location.

The trace emission that was now systematic after calls to complain*()
now moved to the shared _complain(), where it is emitted based on the
elements present in the details passed to the function (fatal/warning).

This saves another 56kB, so we're at -193kB compared to initial work.
The total preprocessor output is now ~7% smaller in bytes than initial
work, and ~10% in words, and builds ~7% faster (7.05s vs 7.55s before
on a 64-core EPYC). The part of the reasonable cleanup is now done.

However the complain_with_dbg function now appears in the caller's
stack as tail jumps are not always done, but we have plans for this.

2 months agoCLEANUP: debug: make __BUG_ON() use abort_with_line()
Willy Tarreau [Tue, 11 Aug 2026 15:20:21 +0000 (17:20 +0200)]
CLEANUP: debug: make __BUG_ON() use abort_with_line()

ABORT_NOW() with no arg is strictly equivalent to abort_with_line(__LINE__)
so let's do it explicitly, not only it clarifies the code, but it
also reduces the size of the developed macros, which does count,
considering the number of BUG_ON() in the code.

2 months agoMEDIUM: debug: do not dump a context-less backtrace in ABORT_NOW()
Willy Tarreau [Tue, 11 Aug 2026 12:20:45 +0000 (14:20 +0200)]
MEDIUM: debug: do not dump a context-less backtrace in ABORT_NOW()

Originally before 2.4, ABORT_NOW() was used to instantly stop the program
with as little damage as possible in order to help debug it, keeping
registers intact.

This was modified in 2.4-dev6 by commit 5baf4fe31ad ("MEDIUM: debug:
now always print a backtrace on CRASH_NOW() and friends") because the
same macro was shared with BUG_ON() and we didn't have the backtrace.
But by doing this we lost the ability to debug the precise crash
location.

Later in 3.0, we added support for an extra contextual info with commit
d417863828 ("MINOR: debug: support passing an optional message in
ABORT_NOW()"). This became even more fishy because at this point,
ABORT_NOW() called empty would only emit a backtrace, while when
passed an argument, it would emit "ABORT at <file>:<line>: <arg>",
losing the registers despite what the comment would say.

Now that we can type call places, it becomes possible to fix it again
so that ABORT_NOW() honors its two promises:
  - no register mangling when called with no argument ;
  - location + backtrace + message when called with an argument.

This patch simply conditions the call to ha_backtrace_to_stderr() to
the presence of an argument, without touching the abort_with_line()
call. It's done using an extra if to minimize the diff as it's only
temporary. For BUG_ON(), the trace calls are factored before the call
to an argument-less ABORT_NOW() since that one will not print the
message anymore.

2 months agoMEDIUM: debug: add a new version of complain() that takes a debug_count struct
Willy Tarreau [Tue, 11 Aug 2026 09:21:50 +0000 (11:21 +0200)]
MEDIUM: debug: add a new version of complain() that takes a debug_count struct

complain_with_dbg() takes a debug_count struct in argument from which
it retrieves everything it needs:
  - details
  - type
  - file name
  - line number
  - description
  - vaargs

This is done by making the common _complain() able to decode such elements
from a debug_count when presented. complain() passes a NULL there and uses
its msg, while complain_with_dbg() passes the debug_count and no msg. This
allows us to get rid of all the pre-made redundant messages and to save
70 kB of code. We're now around 136 kB under the initial size.

2 months agoMINOR: debug: prepare complain() to be used in multiple ways
Willy Tarreau [Tue, 11 Aug 2026 09:13:14 +0000 (11:13 +0200)]
MINOR: debug: prepare complain() to be used in multiple ways

We'll intrument the function to do a bit more, but all call paths will
not be able to support this, so let's first turn it to a general one
as _complain() and have complain() just call it. Better not inline them,
we don't want to duplicate the footprint for something rarely used.

2 months agoMINOR: debug: store the caller's details in debug_count
Willy Tarreau [Tue, 11 Aug 2026 08:04:15 +0000 (10:04 +0200)]
MINOR: debug: store the caller's details in debug_count

There's a 8-bit hole left in the struct and that's exactly what we'll
need to store the details. In fact we're only using 6 bits now and
they're a bit wasted, so we could even compact them to be merged with
the type later if needed.

2 months agoMINOR: debug: rely on the __dbg_cnt_ counter for _BUG_ON_ONCE()
Willy Tarreau [Tue, 11 Aug 2026 07:48:29 +0000 (09:48 +0200)]
MINOR: debug: rely on the __dbg_cnt_ counter for _BUG_ON_ONCE()

Since we already have a counter implemented when using DBG_COUNT(),
let's rely on this counter instead of declaring a local static one.
We always increment using a fetch_add() now, but regardless, this
reduces the code by 2 kB.

2 months agoMINOR: debug: make a pair of __BUG_ON() for modern and obsolete linkers
Willy Tarreau [Tue, 11 Aug 2026 07:43:13 +0000 (09:43 +0200)]
MINOR: debug: make a pair of __BUG_ON() for modern and obsolete linkers

The _BUG_ON() and _BUG_ON_ONCE() calls used to systematically call
__DBG_COUNT() first, then to call __BUG_ON(). Let's have a variant of
__BUG_ON() for each linker mode, that is merged with __DBG_COUNT().
The modern linker one now performs both the counting and processing
of the event at once.

For now the "once" aspect is still dependent on the expression-local
counter so that the code is exactly the same as before. The code size
didn't change.

2 months agoCLEANUP: debug: remove the unneeded do { } while (0) on __BUG_ON()
Willy Tarreau [Thu, 13 Aug 2026 14:51:11 +0000 (16:51 +0200)]
CLEANUP: debug: remove the unneeded do { } while (0) on __BUG_ON()

The macro is exclusively called by the _BUG_ON*() macros so it cannot
appear anywhere else, no need to clutter the emitted syntax for nothing,
seeing the output of the preprocessor on these parts is totally scary
already.

2 months agoMINOR: debug: avoid the break in __BUG_ON()
Willy Tarreau [Tue, 11 Aug 2026 07:32:02 +0000 (09:32 +0200)]
MINOR: debug: avoid the break in __BUG_ON()

Since the "break" statement irritates gcc, let's resort to the
opposite expression involving a compount expression to declare the
static counter. It seems to be doing the job well, and we're back
to the previous size (-63 kB from original).

2 months agoMINOR: debug: unify __BUG_ON() and __BUG_ON_ONCE()
Willy Tarreau [Mon, 10 Aug 2026 18:07:01 +0000 (20:07 +0200)]
MINOR: debug: unify __BUG_ON() and __BUG_ON_ONCE()

We can do that thanks to the debug type. The line counter moved into an
if () block conditioned by the type so it continues to emit nothing for
regular bugs.

Surprizingly, the code grew by 1 kB. It's apparently caused by having
an impossible "if" statement containing a "break". Splitting the macro
in two to put the BUG_ON_ONCE() callers aside showed that having only
"if (0) break;" in the block was sufficient to make gcc deoptimize it
and result in this same growth. This was observed with 9.5. Let's
consider this a temporary limitation and keep it this way.

2 months agoMINOR: debug: pass the type to the __BUG_ON*() macros
Willy Tarreau [Tue, 11 Aug 2026 06:58:20 +0000 (08:58 +0200)]
MINOR: debug: pass the type to the __BUG_ON*() macros

We now pass DBG_BUG or DBG_BUG_ONCE to the __BUG_ON*() macros, so that
we can later exploit this info to merge them.

2 months agoMEDIUM: debug: make the complain() function print the suffix
Willy Tarreau [Mon, 10 Aug 2026 16:44:41 +0000 (18:44 +0200)]
MEDIUM: debug: make the complain() function print the suffix

The suffix, such as "please contact the developer" that is printed on
each warning, with its optional variant "not crashing ..." can be entirely
computed by complain() based on the flags description. This way we can drop
that from the macros and further shrink the messages.

The code size reduced by 6.5 kB.

2 months agoMEDIUM: debug: use \x1e (RS) to delimit the condition from vaargs in complain()
Willy Tarreau [Mon, 10 Aug 2026 17:27:01 +0000 (19:27 +0200)]
MEDIUM: debug: use \x1e (RS) to delimit the condition from vaargs in complain()

This is the same principle as for DBG_COUNT() but applied to complain().
For now it doesn't bring any particular benefit but we needed to know where
the end of the string is in order to continue to turn other fields to
variables.

2 months agoMEDIUM: debug: use \x1e (RS) to delimit the condition from vaargs in DBG_COUNT()
Willy Tarreau [Mon, 10 Aug 2026 17:02:28 +0000 (19:02 +0200)]
MEDIUM: debug: use \x1e (RS) to delimit the condition from vaargs in DBG_COUNT()

The principle here is to reduce the reliance on multiple pointers in the
debug_count struct while still keeping a delimitation between records.
For this, we use the ASCII record separator character (\x1e or RS) that
is not present in the rest of our messages and has no reason to land there
by accident.

This slightly reduces the code size (2kB) but the purpose is to prepare
a next step where these definitions will benefit the BUG_ON*() macros.

2 months agoMEDIUM: debug: make the complain() function print the prefix
Willy Tarreau [Mon, 10 Aug 2026 15:49:39 +0000 (17:49 +0200)]
MEDIUM: debug: make the complain() function print the prefix

The prefix ("ABORT at", "FATAL:", "WARNING:") as well as the second level
("bug condition", ...) are entirely determined by the details bit field,
so there is no reason to have to build this string in the caller, instead
we can let complain() build it entirely from the details into the local
iovec, so let's do that.

The prefix is now dropped from the whole call chain as it's no longer
useful. This further reduces the code size by 17.5 kB.