Manu Nicolas [Wed, 2 Sep 2026 10:09:14 +0000 (10:09 +0000)]
MEDIUM: resolvers: retry truncated UDP responses over TCP
RFC 1123 and RFC 2181 recommend retrying a truncated DNS response
over a transport capable of carrying a larger message. HAProxy instead
treats TC as a resolver error unless the nameserver is configured to
use TCP for every query.
Give unprefixed IPv4, IPv6 and hostname nameservers both a datagram and
a stream server. Queries use UDP first and the existing stream code
opens TCP connections lazily. Explicit datagram prefixes remain
UDP-only, explicit stream prefixes remain TCP-only, and nameservers
loaded from resolv.conf gain the default fallback behavior.
When a valid UDP response is truncated, rebuild its query over TCP with
the same ID, question type and nameserver. Taking the type from the
response preserves late A or AAAA responses received after a query-type
switch. Queueing the fallback refreshes the retry deadline without
consuming a retry or initiating another A/AAAA switch.
A truncated SRV response can still be partially processed when no TCP
fallback can be initiated for that response. This preserves existing
behavior for explicit UDP and TCP nameservers and processes a partial
SRV response received during TCP fallback. Other truncated TCP
responses follow the normal error path, and no TCP response triggers
another fallback.
Failed queue operations use the existing send-error path, while
connection failures are handled by the resolver timeout and retry path.
Add a tcp_fallback counter to the CLI, resolver statistics and
Prometheus output. It counts fallback queries successfully queued to the
stream server.
Manu Nicolas [Tue, 1 Sep 2026 10:44:50 +0000 (10:44 +0000)]
MEDIUM: resolvers: accept full-size DNS messages over streams
accepted_payload_size is both the EDNS UDP advertisement and the
response size limit. A complete stream response larger than this value
is therefore discarded even though DNS stream framing can carry it. It
also makes it impossible to advertise a conservative UDP size while
accepting the complete response after a transport switch.
The stream code already rejects messages above the DNS protocol limit
and buffers the message before the resolver checks its size. Accepting
up to DNS_MAX_MSG_SIZE in the resolver therefore does not increase the
stream allocation.
Keep accepted_payload_size as the UDP advertisement and receive limit,
and use the protocol maximum for stream responses. Configurations that
used it to reject large stream responses will now accept those valid
messages.
Manu Nicolas [Tue, 1 Sep 2026 10:44:48 +0000 (10:44 +0000)]
MINOR: dns: select the nameserver server type explicitly
DNS send, receive and response functions currently infer which server
to use from the non-null member of a nameserver. This is unambiguous
while a nameserver owns only one server, but prevents one logical
nameserver from exposing both datagram and stream servers.
Pass a dns_server_type through these interfaces and add predicates for
readable type checks. Existing queries still select the same server, so
this patch does not change runtime behavior.
Manu Nicolas [Wed, 2 Sep 2026 10:08:45 +0000 (10:08 +0000)]
CLEANUP: dns: use a single maximum DNS message size
The DNS code defines separate maximum message sizes for datagram and
stream processing, but both are the DNS protocol limit of 65535 bytes.
The duplicate names make common response buffers look
transport-specific and could allow the values to diverge.
The request ring size also has a TCP-specific name even though both
datagram and stream servers use it.
Replace the message limits with DNS_MAX_MSG_SIZE and rename the ring
limit to DNS_MSG_RING_MAX_SIZE. This does not change any limit or
runtime behavior.
Manu Nicolas [Tue, 1 Sep 2026 10:44:47 +0000 (10:44 +0000)]
MINOR: tools: report explicit address type selection
str2sa_range() reports the resulting socket and transport types through
net_addr_type. It does not tell callers whether these types came from
defaults or from an explicit prefix. Callers that need to distinguish
the two must duplicate the parser prefix list, which can fall out of
sync.
Add explicit_type and set it for prefixes that select a socket or
transport type. Family-only prefixes leave it unset. This has no effect
on existing callers.
Manu Nicolas [Tue, 1 Sep 2026 10:44:45 +0000 (10:44 +0000)]
CLEANUP: resolvers: stop clearing the DNS response buffer
resolv_process_responses() clears the whole response buffer before
each receive. The receive operation supplies the message length and
all parser accesses are bounded by the corresponding end pointer, so
no byte beyond the received message is observed.
The answer count is checked before the response question is parsed.
Most DNS servers return a truncated response without answers, so such
a response is classified as ANCOUNT_ZERO before the TC flag and
question type are examined. It increments the wrong counter and hides
the actual response status.
Move the empty-answer check after question parsing and TC handling.
Truncated SRV responses keep their existing partial-answer behavior.
BUG/MEDIUM: resolvers: Use the right server to init DNS streams
During TCP nameservers parsing, the wrong server was selected to initialize
the DNS stream. After the server addition, the first server of the proxy was
selected instead of the last one. So when several TCP nameservers was
defined, only the first one was properly initialized. All others was using
the server created for the first one.
The regression was introduced when the server insertion in the proxy's list
was inverted. The change was not made on the resolvers part. The
proxy_last_server() function must be used instead.
BUN/MINOR: hlua: reset hlua_body variable after per-thread files loading
hlua_body variable was introduced to know if the files loading was passed or
not and emit warnings if some operations are performed during runtime. In
particular, it was used to detect calls to core.use_native_mailers_config()
during runtime.
However, the hlua_body variable was reset too early in the post-init
function, before the per-thread files loading. Because of this issue,
loading mailers.lua script using 'lua-load-per-thread' directive led to the
following error during haproxy startup:
... use_native_mailers_config: not available outside of body context
To fix the issue, this variable is now reset at the end of the function.
This patch must be backported as far as 3.3. It should fix the issue #3487.
Add -fcgi (protocol selector) and --fcgi-app <str> (raw passthrough
into a generated "fcgi-app haload" section, mainly for "docroot").
The FCGI filter is attached from hld_cfg_finalize() (post-check),
not hld_pre_check(): servers don't exist yet at pre-check time, and
HAProxy's own postparser then wrongly reports no FCGI server found.
"set-param"/"pass-header" have no effect (no struct stream to run
them); use -H ":fcgi-<param>:<value>" instead.
MINOR: mux-fcgi: also skip sc_opposite() for haload streams
fcgi_set_default_param() already falls back to conn_src()/conn_dst()
for check streams (struct check, sc_check()). haload streams (struct
hldstream, sc_hldstream()) need the same fallback.
This is a preparation for the FCGI support into haload.
REGTESTS: ssl: add verifyhost SAN vs CN matching test
Add a VTC test to check that backend certificate verification in
ssl_sock_srv_verifycbk() adheres to RFC 6125:
1. A certificate with a matching SAN dNSName is accepted.
2. A legacy certificate without any SAN extension falls back to the
Subject CN and is accepted.
3. A certificate with a non-matching SAN dNSName is rejected, even if
its Subject CN matches the expected hostname.
The test uses dedicated static certificates in reg-tests/ssl/certs/
(verifyhost_ca.crt and associated leaf certificates) so that
certificates do not need to be generated at runtime.
MEDIUM: ssl: do not fall back to Subject CN when a SAN dNSName is present
Historically, ssl_sock_srv_verifycbk() checked the certificate's
subjectAltName (SAN) extension first, but fell back to checking the
Subject Common Name (CN) whenever no SAN matched the expected hostname
(from SNI or "verifyhost").
However, per RFC 6125 section 6.4.4:
https://datatracker.ietf.org/doc/html/rfc6125#section-6.4.4
"As noted, a client MUST NOT seek a match for a reference identifier
of CN-ID if the presented identifiers include a DNS-ID, SRV-ID,
URI-ID, or any application-specific identifier types supported by the
client."
This patch changes the behavior by tracking whether any dNSName entry was
encountered in the SAN extension and skipping the Subject CN check entirely
if one was present, even if none of the SAN entries matched. Fallback to CN
is only preserved for legacy certificates lacking a dNSName SAN.
Willy Tarreau [Mon, 31 Aug 2026 12:14:16 +0000 (14:14 +0200)]
IMPORT: slz: get rid of the bogus USE_64BIT_QUEUE and check siezof(long) instead
The USE_64BIT_QUEUE was set when it was suggested to rely on 64-bit
operations on the queue and dump 32 bits at once (thus never keeping
more than 32 bits in the queue). It was also used to decide whether
the queue was stored as u32 or u64. However, this check in the public
part (slz.h) depends on a definition in the private part (slz-prv.h)
that cannot be seen by application code. Luckily, slz.c includes its
headers in the wrong order so it gets the public part before the private
one, and uses the same layout, which is why it was not detected earlier.
Let's just get rid of this USE_64BIT_QUEUE definition. It's not accurate
and cannot be matched between public and private code. Let's instead rely
on __SIZEOF_LONG__ which conveys the info we need: is the CPU efficient
when working on 64-bit integers.
The comments were adjusted to explain the difference and why it's not
dramatic to fall back to the 32-bit queue (in effect it's around a 1%
perf loss).
Willy Tarreau [Sun, 23 Aug 2026 10:42:34 +0000 (12:42 +0200)]
IMPORT: uslz: do not replay the header-detection bytes on the raw deflate path
When a stream turns out to be raw deflate and the first two bytes had to
be accumulated into hdr_detect.buf across calls, those bytes are replayed
to uslz_decode_block() from a local array, after which the code did:
That rewinds to the start of the caller's buffer, but the loop that
filled hdr_detect.buf consumed from that same buffer, so the bytes just
replayed were handed to the decoder a second time. Saving the real
in_ptr across the replay fixes it; in_top never moved, and is saved with
it only for symmetry.
The problem happens whenever a raw deflate stream is fed in chunks of 1
or 2 bytes, since 3 or more takes the fast path at "in_top - in_ptr > 2"
and never enters this branch. Feeding 5000 bytes of source through
uslz_decode() one byte at a time failed with USLZ_DECODE_E_GEN_HUFF and
no output at all: the duplicated bytes shift the bit stream, so the first
dynamic block's code lengths no longer form a valid tree. gzip and zlib
streams are unaffected, they leave this branch by a different location.
The issue was spotted by Qwen-3.8-27B. This fix must be backported to
1.3.
Dragan Dosen [Mon, 31 Aug 2026 22:43:56 +0000 (22:43 +0000)]
BUG/MEDIUM: filters: check the filter name before comparing it
compile_filter_sequence_elt() looks a filter up by name with strcmp(),
without checking whether the filter configuration has a name. Only the
filters declared with the "filter" keyword get one, assigned by
parse_filter(). The filters which the configuration inserts on its own
never go through that function and leave the name NULL, so haproxy can
crash while parsing a configuration which combines a "filter-sequence"
line with any of those, for example:
frontend f
bind :8080
compression algo gzip
filter-sequence request non-existent
Let's skip the filters which have no name. They cannot be referenced in
a "filter-sequence" line anyway, so the lookup now fails and reports
"invalid filter name".
This should be backported to all branches which have "filter-sequence".
CLEANUP: quic: remove the unused packet number of the key phase structure
struct quic_tls_kp has a <pn> member, set by quic_tls_rotate_keys() and
initialized by quic_tls_kp_init(), which is not read anywhere: the previous key
phase secrets are now detected with <ctx>. This patch removes it.
BUG/MINOR: quic: late packets of the first key phase are dropped
qc_pkt_decrypt() tests <qc->ku.prv_rx.pn> to know whether the previous key
phase secrets are usable. This number comes from <tls_ctx->rx.pn>, which is
only set on a key phase change and is thus still 0 during the first key phase.
After the first key update the test always fails and every late packet of the
initial key phase is dropped instead of being decrypted with the previous keys.
Test <qc->ku.prv_rx.ctx> instead: it is NULL before the first key update, and
set after it.
This bug was introduced by commit a7d2c0946 ("MINOR: quic: Enable the Key
Update process").
BUG/MEDIUM: quic: crash on key update phase change after a failed one
qc_pkt_decrypt() decrypts the first packet of a new key phase with
<qc->ku.nxt_rx.ctx> without checking it. quic_tls_key_update() leaves it NULL
when the derivation fails on a memory allocation error, and its caller only
dropped the packet, so the next key phase change crashed the process.
Closing the connection would be excessive: <tls_ctx->rx.ctx> is still valid and
the current key phase works. The next keys are now checked where they are used
and derived again if missing. <qc->ku.nxt_tx.ctx> is checked too, as
quic_tls_rotate_keys() moves it to <tls_ctx->tx.ctx>. If the derivation fails
again the packet is dropped and the peer retransmits it. A packet which
decrypted correctly is no longer dropped either.
This bug was introduced by commit a7d2c0946 ("MINOR: quic: Enable the Key
Update process").
BUG/MINOR: payload: bound ClientHello extension lists by the extension length
smp_fetch_ssl_supported_groups(), smp_fetch_ssl_sigalgs(),
smp_fetch_ssl_keyshare_groups() and smp_fetch_ssl_hello_sni() all check
the length of the list carried by their extension against
<list_len> > hs_len - 6
where <hs_len> is what remains of the *whole* extension block, not of the
extension being parsed. The list lives in the extension body, so the
bound has to be <ext_len> - 2. All four sites already reject ext_len < 2
beforehand, so the new bound cannot go negative, and since ext_len is
itself checked against hs_len - 4 a few lines above, it is strictly
tighter than the current one.
With the loose check, an extension whose body holds nothing but the
2-byte list length is accepted while announcing a list that runs into the
extensions that follow, and the fetch returns bytes belonging to another
extension:
For req.ssl_sni the effect is the most visible, since name_type, name_len
and the hostname are then all read from the neighbouring extension, and a
hostname is reported that appears in no server_name entry.
Those reads all stay within the ClientHello, whose bytes are supplied by
the client anyway, so this is neither an out-of-bounds access nor an
information disclosure: the returned sample ends at worst on the last
byte of the extension block. It does however let a client decouple what
use-server/use_backend rules and logs observe from what it really
announced, on a ClientHello that a conformant TLS stack would reject.
A reg-test covering, for each of the four fetches, a well-formed
extension and an over-announced list is added.
The req.ssl_supported_groups, req.ssl_sigalgs and req.ssl_keyshare_groups
parts should be backported as far as 3.2, where those fetches appeared
with commit fa063a9e7 ("MINOR: sample: add 4 new sample fetches for
clienthello parsing"). The req.ssl_sni part is much older, it dates back
to commit b6672b547 ("MINOR: acl: add support for TLS server name
matching using SNI") in 1.5-dev8, so backports to branches older than 3.2
need to keep that hunk only.
BUG/MINOR: payload: fix the cipher_len bound check in smp_client_hello_parse()
smp_client_hello_parse() validates the CipherSuite length of a TLS
ClientHello with:
ext_len > hs_len
where <hs_len> is the number of ClientHello bytes remaining, counted from
<data>, which at that point still points at the 2-byte cipher_len field
itself. The CLIENTHELLO_CIPHERSUITE exit path returns that very <data>
together with *len = cipher_len, and smp_fetch_ssl_cipherlist() then
publishes <data + 2> over <cipher_len> bytes. The cipher suites therefore
have to fit *after* the length field, so the check must be:
ext_len > hs_len - 2
Without this, a ClientHello announcing a cipher_len one or two bytes
larger than what its handshake message can hold is accepted, and
req.ssl_cipherlist returns up to 2 bytes read past the end of the
ClientHello. When the ClientHello is the last thing in the buffer, those
2 bytes are leftovers of a previous connection, which then leak to
whatever logs or routes on req.ssl_cipherlist:
# 45-byte ClientHello announcing cipher_len = 10 for 8 cipher bytes
cipherlist=130213031301009fcafe <- "cafe" sent after the TLS record
cipherlist=130213031301009f4142 <- nothing sent after the ClientHello
The CLIENTHELLO_EXTENSIONS path was not affected; it only survived by
accident, because it re-checks "hs_len < 2" after subtracting
2 + ext_len.
A reg-test exercising both a well-formed cipher_len and an over-announced
one is added.
This must be backported as far as 3.2, where req.ssl_cipherlist was
introduced by commit fa063a9e7 ("MINOR: sample: add 4 new sample fetches
for clienthello parsing").
BUG/MINOR: quic: fix zero value of tune.quic.sec.stateless-reset-rate
quic_stateless_reset_rate_init() split <quic_tune.sec_stateless_reset_rate>
across threads as (rate + tid) / nbthread, which can round down to 0 when
<rate> is lower than <nbthread>. send_stateless_reset() reads a 0 value as
"no limit", so a low <rate> silently disabled rate limiting on some
threads instead of restricting it further.
Fix it by giving every thread the same value, rounded up so it can never
reach 0: (rate + nbthread - 1) / nbthread. Updated the doc accordingly.
This bug was introduced by commit 297321e3e ("MINOR: quic: add
tune.quic.sec.stateless-reset-rate"), only present in this dev branch,
so no backport is needed.
MINOR: haload: enable per-thread-group FD tables when available
Add <_GNU_SOURCE> so <CLONE_FILES> is visible from <sched.h>. When it
is, generate "expose-experimental-directives" and "tune.fd.tables
per-thread-group" in the global section.
REGTESTS: decomp: Add more scripts to test HTTP/TCP decompression
* decompression/http.vtc: response and request decompression in HTTP.
* decompression/tcp.vtc: raw TCP streams decompression.
* decompression/http_recompression.vtc: decompression then recompression.
Willy Tarreau [Fri, 21 Aug 2026 08:24:54 +0000 (10:24 +0200)]
REG-TESTS: decompression: check the response header rewriting
Covers two of the fixes in this series: an unrelated "Vary" value that is a
prefix of "accept-encoding" ("Vary: Accept") must survive, a strong ETag
must be weakened since the representation changed, and a content coding
which is only a prefix of a configured algorithm ("Content-Encoding: g")
must not engage decompression.
The case-insensitive comparison of the coding is not covered: vtest always
emits its own lower-cased Content-Encoding next to the one given with -hdr,
so the response carries both and the test would pass either way.
MEDIUM: filters: implicitly enable "decomp-req" and "decomp-res" when decompression options are used
If the "decomp-req" and/or "decomp-res" filters are not declared
explicitly but decompression options are set for them, then we
detect that the options relevant for one or the other were set and
we enable the filter automatically. This eases the configuration for
users who forget to declare the relevant decompression filters, as
we already do for compression ones.
However, when multiple filters are involved on a single proxy, it is
highly recommended to declare filters explicitly in order to avoid
surprises and this way it is easier to control in which order they
are executed. See also "filter-sequence" to help control executing
order.
For now we only support some decompression algos by leveraging libslz
builtin library. But it can be confusing for users building haproxy
against zlib (with USE_ZLIB). For such cases, we now raise an explicit
error telling that the algos are not available because they require
haproxy to be built using USE_SLZ.
REG-TESTS: decompression: add basic decompression test
For now only basic HTTP decompression with a small payload is tested,
because TCP decompression is not trivial to test with VTEST, and a HTTP
decompression test with a big payload may eventually come later, provided
it is not too slow.
However, both request and response gzip decompression is tested. Other
compression methods are not tested because it is not the intend of
this test to check the decompression library itself, only haproxy
behavior.
MINOR: flt-decomp: add support for HTTP decompression
Following the previous patch, now we make use of the HTX API to support
HTTP decompression (when proxy is in "mode http"). The decompression
filter works in the similar way as the compression one, except that it
performs decompression instead of compression. As such, we can now use
HAproxy to perform end to end compression: compress input request,
forward it to a distant haproxy node, and let the other haproxy node
decompress request before forwarding it to servers, then do the opposite
operation for the response. This may enable bandwidth optimization when
it matters and the CPU overhead that comes with it is acceptable (check
out libslz writeups to know more about the performance impact).
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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!
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).
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.
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.
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.
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.
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:
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.
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.
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().
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>).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
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.
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.
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")
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.
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.
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).
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.
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.
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.