]> git.kaiwu.me - haproxy.git/log
haproxy.git
2 months agoREGTESTS: log: check that the +cbor output passes the logged bytes through
Willy Tarreau [Wed, 2 Sep 2026 16:58:17 +0000 (18:58 +0200)]
REGTESTS: log: check that the +cbor output passes the logged bytes through

The encoding described in section 8.6 deliberately does not apply to the
cbor encoding, whose default output form is already made of hex digits and
whose "+bin" form emits delimited binary blocks. Nothing checked that so
far, so a well-intentioned change could reintroduce an encoding there
without anyone noticing.

Check that a byte outside the printable range, a UTF-8 sequence and a '#'
all reach the CBOR text string untouched, that a binary sample goes to a
byte string with its byte preserved, and that the "+bin" form leaves the
value alone as well.

2 months agoDOC: config: mention that section 8.6 does not apply to the cbor encoding
Willy Tarreau [Thu, 3 Sep 2026 04:53:11 +0000 (06:53 +0200)]
DOC: config: mention that section 8.6 does not apply to the cbor encoding

Section 8.6 of the config manual explains that the characters outside
the 32..126 range are not emitted as-is in the logs but encoded as a '#'
followed by their two hex digits, so that a log line cannot upset a
terminal.

However, this doesn't apply to cbor output, which can come as a surprise
to the reader. The reason is that cbor output already emits all bytes as
hex chars by default (hence the protection is granted), but supports raw
mode with "+bin" in which case the output will be consumed by a suitable
cbor parser and not by a terminal. In such a case we want the output to
remain verbatim and certainly not to be escaped. So let's update the doc
to explain this.

2 months agoREGTESTS: log: check the escaping performed by the +json option
Willy Tarreau [Thu, 3 Sep 2026 05:05:35 +0000 (07:05 +0200)]
REGTESTS: log: check the escaping performed by the +json option

Check that a control character, a byte above the printable range and the
two characters which JSON requires to be escaped all come out escaped, and
that nothing outside 32..126 reaches the ring. The test fails without the
previous commit.

2 months agoBUG/MEDIUM: log: encode the whole non-printable range in the +json output
Willy Tarreau [Wed, 2 Sep 2026 16:30:22 +0000 (18:30 +0200)]
BUG/MEDIUM: log: encode the whole non-printable range in the +json output

The +json encoder only escaped the double quote and the backslash, which
left two problems behind. First, RFC 8259 #7 requires every character
below 0x20 to be escaped, so a control character coming from a request,
such as a TAB, made the record invalid JSON:

  log-format "{\"hdr\":%{+json}[capture.req.hdr(0)]}"

  $ jq . < record
  parse error: Invalid string: control characters from U+0000 through U+001F must be escaped at line 1, column 8

Second, RFC 8259 #8.1 requires the text to be valid UTF-8, while a header
value is made of arbitrary bytes. A byte such as 0xAA, or a sequence such
as 0xC0 0x41, is perfectly legal on the wire and made the record
undecodable:

  $ python3 -m json.tool < record
  'utf-8' codec can't decode byte 0xaa in position 8: invalid start byte

In both cases a client can invalidate the log records of its own requests,
and possibly a whole batch depending on how the collector reacts to a
parse error.

Section 8.6 of the configuration manual makes it pretty clear that only
characters from 32 to 126 are emitted as-is, everything else being
encoded as "#" plus two hex digits, "#" itself included so that the
result is unambiguous. There is no reason for the json encoding to bypass
this rule especially when json decoders may face problems, so let's tag
that whole range in json_escape_map and escape it, using the two-character
form for the characters which have one and the six-character one for the
others.

This also protects readers from a possible misinterpretation of some
UTF-8 sequences (e.g. direction change) that could fool a reader on
screen if rendered as-is. Now it will be unabmbiguous since bytes will
be logged one at a time as is already done for the plain text logs.

This should be backported after some observation time to 3.0, where +json
was introduced by commit 3f7c8387c0 ("MINOR: log: add +json encoding
option").

2 months agoBUG/MINOR: tools: support the documented NULL in cbor_encode_bytes_prefix_bin()
Willy Tarreau [Tue, 1 Sep 2026 16:29:58 +0000 (18:29 +0200)]
BUG/MINOR: tools: support the documented NULL in cbor_encode_bytes_prefix_bin()

Both cbor_encode_bytes_prefix_bin() and cbor_encode_text_bin() document
that "if <bytes> is NULL, then only the <prefix> (with length) will be
emitted", and the hex variants do implement it, their copy loop being
guarded with "while (bytes && it < len)". The bin one does not: it calls
memcpy() unconditionally, so passing NULL dereferences it and crashes,
and it advances <start> by <len> for bytes it never wrote.

No caller passed NULL so far, which is why this remained unnoticed, but
better fix it before it gets used, as it's trivial.

No backport needed.

2 months agoREGTESTS: log: check which log-format tags escape control characters
Willy Tarreau [Tue, 1 Sep 2026 14:23:08 +0000 (16:23 +0200)]
REGTESTS: log: check which log-format tags escape control characters

It is not obvious which log-format construct protects the log against
control characters coming from the request, and "+E" is easily believed
to be the one, while it is in fact the RFC5424 structured-data escaping
and only covers '"', '\' and ']' (see rfc5424_escape_map).

The test logs one same captured header value, containing a raw ESC and a
']', through three different tags, and shows that:

  smp |A?[31m]B|        %[capture.req.hdr(0)]      nothing escaped
  esc |A?[31m\]B|       %{+E}[capture.req.hdr(0)]  ']' escaped, not the ESC
  hdr |{A#1B[31m]B}|    %hr                        ESC encoded as "#1B"

Only %hr and %r, which use hdr_encode_map and url_encode_map, encode the
0x00-0x1f and 0x7f-0xff ranges. A sample expression emits them verbatim
whatever the options, so a config using %[...] to log request data puts
control characters in the logs and in the rings.

The dump is read with "show events -s" so that a control character which
made it to the ring shows up as a '?', which avoids embedding one into
this file. The test does not depend on the delimiter fix from the two
previous commits since it does not use "-0".

2 months agoREGTESTS: log: add a test for "show events -s"
Willy Tarreau [Tue, 1 Sep 2026 13:53:06 +0000 (15:53 +0200)]
REGTESTS: log: add a test for "show events -s"

The commit that added the "-s" option to "show events" was marked as hard
to test, the difficulty being to get control characters into a ring in
the first place. It is in fact easy from the configuration alone, without
involving a client at all: "Gw==", "Cg==" and "Cf==" are the base64
encodings of ESC, LF and TAB, so a log-format that passes them through
b64dec emits raw control characters into the ring, exactly like a log
field does when it is emitted without "+E".

The test then checks the three behaviours that matter:
  - the default output remains byte-for-byte faithful: the raw LF is
    still there and turns a single record into two lines,
  - "-s" replaces the ESC and the LF with '?', and the TAB with a space,
    so a record can no longer forge another one nor drive the terminal,
  - "-0 -s" still delimits the records, which the previous commit fixed.

The last check fails without that fix, and the whole test fails on any
version predating the "-s" option since it reports an unknown option.

2 months agoMINOR: ring: add "-s" flag to "show events" to sanitize the output
Willy Tarreau [Sat, 15 Aug 2026 18:43:58 +0000 (20:43 +0200)]
MINOR: ring: add "-s" flag to "show events" to sanitize the output

"show events" and the other ring dumps (show startup-logs, show errors)
emit each ring message verbatim via applet_append_line(). Rings can hold
attacker-influenced contents (forwarded syslog lines, log-format output
emitted without "+E") that contains ESC, CR, LF or BS. Because the CLI
writes the result straight to the user's terminal, such a dump can for
example inject ANSI escape sequences and rewrite the screen, BS and
visually erase some fields on the screen, or even use UNICODE code points
that switch the writing direction from LTR to RTL and conversely.

This patch adds an opt-in "-s" flag to "show events", in order to sanitize
displayed strings. When set, each event is copied into the thread-local
trash chunk and every byte is sanitised before being appended: whitespaces
(spaces and tabs) are printed as a space, any other control character
(ESC, CR, LF, NUL, ...) as well as non-ASCII as '?', and everything else
verbatim. The behaviour is selected by a new RING_WF_SANITIZE watch flag
that switches cli_io_handler_show_ring() to a dedicated appender
(show_ring_append_escaped()) instead of applet_append_line().

The sanitisation is open-coded rather than reusing existing functions
which would stop at NUL. Here we must walk exactly <len> bytes and keep
the output length identical to the input so that offsets stay valid.

The flag is opt-in on purpose: the primary use of "show events" is a
byte-for-byte tail of the log ring piped into grep/awk/parser, or even
the dataplane API, and escaping by default would corrupt that. Users
who who want a safe output should just pass "-s". Default output is
unchanged. Maybe we could change the default in a future version if
there is demand.

No backport needed though it can be useful if it's easily done.

Reported-by: Claude (ANT-2026-ER52H2CR)
2 months agoBUG/MINOR: quic: delete the wrong CIDs when post-handshake frames fail
Frederic Lecaille [Wed, 2 Sep 2026 13:06:55 +0000 (15:06 +0200)]
BUG/MINOR: quic: delete the wrong CIDs when post-handshake frames fail

quic_build_post_handshake_frames() registers up to three connection IDs and, on
error, deletes those in [1, max[ where <max> is the loop countdown. This has no
relation to what the call really registered: depending on which iteration
fails, the CIDs are left behind, or an unrelated one is deleted.
->next_cid_seq_num is not restored either, so a later successful retry
issues sequence numbers which are not consecutive with the ones the peer
already knows, which RFC 9000 5.1.1 forbids.

Save the sequence number before the loop, delete exactly the range this call
registered, and give the numbers back.

This should be backported as far as 2.6.

2 months agoBUG/MINOR: qpack: accept a Required Insert Count which cannot be reached
Frederic Lecaille [Wed, 2 Sep 2026 07:41:33 +0000 (09:41 +0200)]
BUG/MINOR: qpack: accept a Required Insert Count which cannot be reached

qpack_decode_fs() parses the field section prefix but never looks at the
result. haproxy advertises a null dynamic table capacity, so a conformant
encoder cannot produce a non null Encoded Required Insert Count, and RFC 9204
4.5.1.1 asks for QPACK_DECOMPRESSION_FAILED in that case. It is now rejected.

Nothing could be done with the accepted prefix, as every field line
representation referencing the dynamic table is already rejected, so this is a
compliance fix rather than a security one.

This should be backported as far as 2.6.

2 months agoBUG/MINOR: qpack: encoder and decoder stream data is never consumed
Frederic Lecaille [Tue, 1 Sep 2026 14:57:42 +0000 (16:57 +0200)]
BUG/MINOR: qpack: encoder and decoder stream data is never consumed

qpack_decode_enc() and qpack_decode_dec() always return 0, which
h3_parse_uni_stream_no_h3() propagates as a consumed byte count. qcs_consume()
is thus never called: the data stays in the Rx buffer and the flow control
credit is never released, so both QPACK unidirectional streams stall for good on
the first byte the peer sends. On the encoder stream that byte is 0x20, which a
conformant peer sends to announce a null dynamic table capacity.

Report the number of consumed bytes instead, and skip the stream ID of Section
Acknowledgment and Stream Cancellation, which haproxy has no use for. The Insert
Count Increment test only caught 0x00 to 0x0f and now covers its whole range.
The encoder stream insert instructions are rejected, as haproxy advertises a
null dynamic table capacity.

This bug was introduced by commit b4672fb6f ("MINOR: qpack: Add QPACK
compression").

This should be backported as far as 2.6.

2 months agoMINOR: quic: protect the ChaCha20 header protection context from being freed
Frederic Lecaille [Tue, 1 Sep 2026 07:52:01 +0000 (09:52 +0200)]
MINOR: quic: protect the ChaCha20 header protection context from being freed

quic_tls_secrets_keys_free() calls EVP_CIPHER_CTX_free() on <hp_ctx> without
checking it against EVP_CIPHER_CTX_CHACHA20. With AWS-LC this member may hold
this value, which points to a static object and not to an allocated context.
quic_tls_ctx_secs_free() already guards against it.

This cannot happen today, as the only caller runs before the contexts are
created. Add the same guard, and reset <hp_ctx> and <ctx> so that calling this
function twice is harmless.

Should be backported as far as 2.6 to ease any future bug fix backport.

2 months agoREGTEST: resolvers: cover UDP to TCP fallback
Manu Nicolas [Wed, 2 Sep 2026 10:09:24 +0000 (10:09 +0000)]
REGTEST: resolvers: cover UDP to TCP fallback

Add a Python DNS fixture listening on UDP and TCP on the same port.
It returns an empty truncated response over UDP and a complete response
larger than 1232 bytes over TCP.

The fixture only uses the Python standard library. Tag the test as
devel because it depends on an external interpreter. The feature check
skips it when python3 is unavailable.

Cover default and family-prefixed fallback, late A responses after an
A/AAAA switch, complete UDP replies, invalid questions, explicit
UDP-only and TCP-only nameservers, TCP connection failures, and SRV
updates. A TCP-only truncated SRV response contains usable records and
exercises partial SRV processing without another fallback.

Check the resolver CLI and statistics counters as well as the
Prometheus metric.

2 months agoMEDIUM: resolvers: retry truncated UDP responses over TCP
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.

2 months agoMEDIUM: resolvers: accept full-size DNS messages over streams
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.

2 months agoMINOR: dns: select the nameserver server type explicitly
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.

2 months agoCLEANUP: dns: use a single maximum DNS message size
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.

2 months agoMINOR: tools: report explicit address type selection
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.

2 months agoCLEANUP: resolvers: stop clearing the DNS response buffer
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.

Remove the unnecessary buffer clear.

2 months agoBUG/MINOR: resolvers: classify empty truncated responses
Manu Nicolas [Tue, 1 Sep 2026 10:44:42 +0000 (10:44 +0000)]
BUG/MINOR: resolvers: classify empty truncated responses

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.

This should be backported as far as 2.4.

2 months agoBUG/MEDIUM: resolvers: Use the right server to init DNS streams
Christopher Faulet [Tue, 1 Sep 2026 15:51:07 +0000 (17:51 +0200)]
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.

It is a 3.5-specific issue. No backported needed.

2 months agoBUN/MINOR: hlua: reset hlua_body variable after per-thread files loading
Christopher Faulet [Tue, 1 Sep 2026 14:51:54 +0000 (16:51 +0200)]
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.

2 months agoMINOR: haload: add FastCGI support
Frederic Lecaille [Mon, 31 Aug 2026 09:25:16 +0000 (11:25 +0200)]
MINOR: haload: add FastCGI support

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.

2 months agoMINOR: mux-fcgi: also skip sc_opposite() for haload streams
Frederic Lecaille [Mon, 31 Aug 2026 08:35:16 +0000 (10:35 +0200)]
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.

2 months agoMINOR: haload: rename <hld_http_ver> to <hld_proto>
Frederic Lecaille [Mon, 31 Aug 2026 08:20:18 +0000 (10:20 +0200)]
MINOR: haload: rename <hld_http_ver> to <hld_proto>

Not only an HTTP version, it's an index for the stats.
Renamed before adding a non-HTTP protocol (FCGI) to it.

2 months agoREGTESTS: ssl: add verifyhost SAN vs CN matching test
William Lallemand [Tue, 1 Sep 2026 09:13:17 +0000 (09:13 +0000)]
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.

2 months agoMEDIUM: ssl: do not fall back to Subject CN when a SAN dNSName is present
William Lallemand [Tue, 1 Sep 2026 08:24:25 +0000 (10:24 +0200)]
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.

This issue was reported in GitHub issue #3486.

2 months agoIMPORT: slz: get rid of the bogus USE_64BIT_QUEUE and check siezof(long) instead
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).

This must be backported to 1.3.

This is libslz upstream commit 54cf477a1736b3e747af14ce1ec38d7c28b34cfc

2 months agoIMPORT: uslz: do not replay the header-detection bytes on the raw deflate path
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:

state->in_ptr = (const unsigned char *)compressed_data;
state->in_top = state->in_ptr + compressed_size;

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.

This is libslz upstream commit f052863a91aeb44021ed1bd52f680df45667496a

2 months agoBUG/MEDIUM: filters: check the filter name before comparing it
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".

2 months agoCLEANUP: quic: remove the unused packet number of the key phase structure
Frederic Lecaille [Mon, 31 Aug 2026 15:11:14 +0000 (17:11 +0200)]
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.

2 months agoBUG/MINOR: quic: late packets of the first key phase are dropped
Frederic Lecaille [Mon, 31 Aug 2026 14:53:48 +0000 (16:53 +0200)]
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").

This must be backported as far as 2.6.

2 months agoBUG/MEDIUM: quic: crash on key update phase change after a failed one
Frederic Lecaille [Mon, 31 Aug 2026 12:26:25 +0000 (14:26 +0200)]
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").

Thanks to Bruno Henc for reporting this issue.

This must be backported as far as 2.6.

2 months agoBUG/MINOR: payload: bound ClientHello extension lists by the extension length
William Lallemand [Sat, 29 Aug 2026 10:05:33 +0000 (10:05 +0000)]
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:

    groups=ff010020aaaaaaaaaaaa...   <- next extension's header and body
    sigalgs=ff010020aaaaaaaaaaaa...
    keyshare=ff01
    sni=HIJACKED!!

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.

2 months agoBUG/MINOR: payload: fix the cipher_len bound check in smp_client_hello_parse()
William Lallemand [Sat, 29 Aug 2026 09:49:09 +0000 (09:49 +0000)]
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").

Reported-by: kongr <kongr@krsecu.com>
2 months agoBUG/MINOR: quic: fix zero value of tune.quic.sec.stateless-reset-rate
Frederic Lecaille [Thu, 27 Aug 2026 13:59:41 +0000 (15:59 +0200)]
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.

2 months agoMINOR: haload: enable per-thread-group FD tables when available
Frederic Lecaille [Thu, 27 Aug 2026 12:59:48 +0000 (14:59 +0200)]
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.

2 months agoBUILD: flt-decomp: Remove unused stream_get_decomp() function
Christopher Faulet [Fri, 28 Aug 2026 15:33:26 +0000 (17:33 +0200)]
BUILD: flt-decomp: Remove unused stream_get_decomp() function

This unused function was left by error after refactoring. Let's remove it
now.

2 months agoREGTESTS: decomp: Add more scripts to test HTTP/TCP decompression
Christopher Faulet [Fri, 28 Aug 2026 09:10:49 +0000 (11:10 +0200)]
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.

2 months agoREG-TESTS: decompression: check the response header rewriting
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.

2 months agoMEDIUM: filters: implicitly enable "decomp-req" and "decomp-res" when decompression...
Aurelien DARRAGON [Thu, 20 Aug 2026 17:49:58 +0000 (19:49 +0200)]
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.

2 months agoMINOR: flt-decomp: warn if built without USE_SLZ
Aurelien DARRAGON [Thu, 20 Aug 2026 12:47:28 +0000 (14:47 +0200)]
MINOR: flt-decomp: warn if built without USE_SLZ

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.

2 months agoREG-TESTS: decompression: add basic decompression test
Aurelien DARRAGON [Tue, 11 Aug 2026 14:41:26 +0000 (16:41 +0200)]
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.

2 months agoDOC: config: add documentation for decompression filter
Aurelien DARRAGON [Wed, 12 Aug 2026 18:25:17 +0000 (20:25 +0200)]
DOC: config: add documentation for decompression filter

Document the "decomp-req" and "decomp-res" filters (which both support
TCP and HTTP streams) and associated options.

2 months agoMINOR: flt-decomp: add support for HTTP decompression
Aurelien DARRAGON [Wed, 12 Aug 2026 17:33:37 +0000 (19:33 +0200)]
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).

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.