]> git.ipfire.org Git - thirdparty/haproxy.git/log
thirdparty/haproxy.git
2 hours agoBUG/MEDIUM: http: fix authority parsing for absolute-form URI with empty path master
Mani Goyal [Wed, 12 Aug 2026 06:56:22 +0000 (12:26 +0530)] 
BUG/MEDIUM: http: fix authority parsing for absolute-form URI with empty path

http_parse_authority() only stopped scanning at '/', not '?' or '#'.
For an absolute-form request-target with no path but a query string
(e.g. "http://host?token=..."), the authority scan ran to the end of
the URI and swallowed the query string into the authority. This
caused http_scheme_based_normalize() to see an empty path and append
'/' after the query string instead of between the host and the
query, corrupting the request on the wire. The same corruption
happens with a literal '#' in the request-target when HTTP
violations are tolerated (option accept-unsafe-violations-in-http-
request), since it is not rejected by the request-line parser in
that mode either.

Per RFC 3986 section 3.2, authority terminates at '/', '?', or '#',
or at the end of the URI, so also stop at these two delimiters.

Add cases to h1_host_normalization.vtc covering an empty path with a
query string, with and without a port needing normalization. Add a
new h1_authority_fragment_char.vtc covering the '#' terminator
specifically, since it requires accept-unsafe-violations-in-http-
request to reach the parser at all and doesn't belong in the shared
frontend used by the other host-normalization cases.

This should be backported to all stable versions.

Should fix issue #3460.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
18 hours agoBUG/MINOR: server: fix off-by-one error when parsing and copying source port range flx04/master
Frederic Lecaille [Wed, 12 Aug 2026 17:06:07 +0000 (19:06 +0200)] 
BUG/MINOR: server: fix off-by-one error when parsing and copying source port range

When allocating a source port range, port_range_alloc_range() allocates
a ring structure of size n + 1 to accommodate a sentinel slot for the
lock-free ring buffer.

However, both srv_parse_source() and srv_conn_src_sport_range_cpy()
were incorrectly using range->size directly, filling and copying the
sentinel slot as if it were a valid port. This ->size port_range struct
field should never be used.

This off-by-one error caused an extra port to be populated. When copying a
configuration (e.g. via default-server or server-template), this extra
slot became allocatable, allowing connections to bind beyond the configured
port range (e.g. binding port 5002 when 5000-5001 was set). Furthermore, if
the range reached port 65535, the extra port wrapped to 0, producing
CO_ER_PORT_RANGE connection failures.

Fix this by introducing port_range_count() to cleanly return the number of
usable ports (range->size - 1), and use it in both srv_parse_source()
and srv_conn_src_sport_range_cpy().

Many thanks to Red Hat and AISLE Research for reporting this.

Must be backported as far as 2.6.

21 hours agoBUG/MINOR: hq_interop: fix potential NULL dereference in _hq_trace_http()
Frederic Lecaille [Wed, 12 Aug 2026 13:44:08 +0000 (15:44 +0200)] 
BUG/MINOR: hq_interop: fix potential NULL dereference in _hq_trace_http()

This bug can be triggered only if the hq_interop traces are enabled. It
has been reported by GH #3468.

In _hq_trace_http(), qcs->qcc->conn was directly passed to TRACE_PRINTF_LOC.
However, qcs can be NULL, leading to a crash.

Fix this by using the <qcc> parameter passed to the function instead,
with a NULL check.

No need to backport.

21 hours agoBUG/MINOR: h3: fix potential NULL pointer dereference in _h3_trace_header()
Frederic Lecaille [Wed, 12 Aug 2026 13:27:46 +0000 (15:27 +0200)] 
BUG/MINOR: h3: fix potential NULL pointer dereference in _h3_trace_header()

This bug can be triggered only when the h3 traces are enabled.

It has been reported by coverity in GH #3469 where qcc was checked for NULL
earlier in _h3_trace_header(), then dereferenced without a check via qcc->conn
during the next TRACE_PRINTF_LOC() call.

This bug was introduced by this commit:

    BUG/MINOR: h3: adjust HTTP headers traces

and should be backported with it, if needed.

21 hours agoBUG/MINOR: server: fix memory leak on "default-server" parsing failures
Frederic Lecaille [Wed, 12 Aug 2026 13:04:58 +0000 (15:04 +0200)] 
BUG/MINOR: server: fix memory leak on "default-server" parsing failures

This is a fix for very minor bug which may be triggerd only at parsing time
upon parsing failures (allocation failure, or default server name mismatch).

When parsing a named "default-server" instance in _srv_parse_init(), <name> is
allocated via strdup(). If a name mismatch occurs or if srv_alloc() fails,
the function jumps to the <out> label without freeing this string.

Fix this by initializing <name> to NULL at the function start and calling
free(name) in the <out> cleanup block.

This issue was reported by GH #3462.

No need to backport.

21 hours agoBUG/MINOR: quic: avoid a division by zero in the BBR pacing interval
Willy Tarreau [Thu, 6 Aug 2026 07:45:26 +0000 (09:45 +0200)] 
BUG/MINOR: quic: avoid a division by zero in the BBR pacing interval

bbr_pacing_inter() divides by the connection's pacing rate without
checking it. That rate is recomputed as a truncated integer product of the
estimated bandwidth by the pacing gain, and once full bandwidth has been
reached the new value is stored unconditionally, even when it truncated
down to zero. So a QUIC peer that acknowledges slowly and sparsely enough
to drive the bandwidth estimate down to a single byte per second, then
lets the connection leave Startup, makes the next pacing computation
divide by zero.

The resulting SIGFPE is not caught, so it takes down the whole worker
along with every connection it was serving, which any remote QUIC client
controls the conditions of.

Let's floor the divisor at 1, as the default pacing helper effectively
does.

Must be backported as far as 3.1.

Reported-by: Claude (ANT-2026-6B82W4A7)
22 hours agoBUG/MINOR: ssl: reject an embedded NUL in the full-DN ssl_*_dn() fetches
William Lallemand [Wed, 12 Aug 2026 09:52:46 +0000 (09:52 +0000)] 
BUG/MINOR: ssl: reject an embedded NUL in the full-DN ssl_*_dn() fetches

ssl_sock_get_dn_oneline() has the same flaw fixed in ssl_sock_get_dn_entry()
by the previous commit: it copies each DN component's ASN.1-declared bytes
verbatim via memcpy(), keeping an embedded NUL and whatever follows it. It
is used to build the full one-line DN returned by ssl_c_s_dn/ssl_c_i_dn/
ssl_r_dn when no field argument is given, so the same tree-vs-list ACL
match inconsistency applies to it.

Apply the same fix: reject the whole DN (return 0) if any entry has a NUL
followed by a non-NUL byte, and otherwise keep a single trailing NUL and
skip any extra ones.

This should be backported to every stable branch, alongside the previous
commit.

22 hours agoBUG/MINOR: ssl: reject an embedded NUL in the ssl_*_dn(entry) fetches
William Lallemand [Wed, 12 Aug 2026 09:19:54 +0000 (09:19 +0000)] 
BUG/MINOR: ssl: reject an embedded NUL in the ssl_*_dn(entry) fetches

ssl_sock_get_dn_entry() copied the ASN.1-declared bytes of a DN entry
verbatim, keeping the exact length. pat_match_str()'s tree branch
(used by the default, exact-match "-m str" ACLs) then looks the value
up with ebst_lookup(), which treats it as a NUL-terminated C string.

A certificate with e.g. CN = "admin\0.attacker-owned.example" is
therefore matched and authorized as "admin": the producer keeps the
full value, but the ebtree consumer silently truncates at the first
NUL. ACLs using "-i" are unaffected since they take the length-correct
list branch instead of the tree.

Reject such a value in the fetch itself, before it reaches the
pattern-matching layer: a NUL followed by any further non-NUL byte
now makes ssl_sock_get_dn_entry() fail as if the entry did not exist,
so ssl_c_s_dn(CN)/ssl_c_i_dn(CN)/ssl_r_dn(CN) and any map/ACL relying
on it can't be bypassed this way. Trailing NUL byte(s) with nothing
after them aren't a truncation risk, so a single one is kept in the
returned length instead of the whole ASN.1 length, and any extra ones
are skipped.

The impact is low since it would need to be signed by the CA anyway.

This should be backported to every stable branch.

Reported-by: Vivek Parikh <vivek.parikh@breachx.ai>
26 hours agoIMPORT: slz: prevent build compilation error due to unused __slz_make_crc_table()
Aurelien DARRAGON [Wed, 12 Aug 2026 09:09:29 +0000 (11:09 +0200)] 
IMPORT: slz: prevent build compilation error due to unused __slz_make_crc_table()

In __slz_common_initialize(), __slz_make_crc_table() was not
systematically called (ie: when __ARM_FEATURE_CRC32 is defined the
crc32_fast[] table is not used). However since the function is defined
as static, AND its content is already guarded by the same
__ARM_FEATURE_CRC32, we actually need to call always call the function,
else we could face a compilation warning on some systems such as:

src/slz_common.c:258:20: error: unused function '__slz_make_crc_table' [-Werror,-Wunused-function]
  258 | static inline void __slz_make_crc_table(void)

This is libslz upstream commit 7d8744a64ca78ff4ac66bff4f6839d416537f312

26 hours agoBUG/MINOR: wurfl: fix memory leak of information list and patch strings at deinit
scientiamobile [Tue, 11 Aug 2026 10:15:39 +0000 (12:15 +0200)] 
BUG/MINOR: wurfl: fix memory leak of information list and patch strings at deinit

The strings duplicated with strdup() for each "wurfl-information-list" token
(wi->data.name) and each "wurfl-patch-file" path (wp->patch_file_path) were
never freed: ha_wurfl_deinit() only freed the list nodes, not the strings they
own. A simple config check on a configuration using these keywords is enough to
leak them, as reported by ASAN.

Free wi->data.name and wp->patch_file_path before freeing their list nodes.

This bug has been present since the module was introduced in commit d0027ed5b
("MEDIUM: wurfl: add Scientiamobile WURFL device detection module"). It should
be backported to all stable versions.

This fixes issue #2084.
Reported-by: Ilya Shipitsin <chipitsine@gmail.com>
28 hours agoBUG/MEDIUM: cache: do not release an entry under the cache read lock
Rémi Tricot-Le Breton [Mon, 10 Aug 2026 15:23:30 +0000 (17:23 +0200)] 
BUG/MEDIUM: cache: do not release an entry under the cache read lock

In http_action_req_cache_use() the cache lock is taken in read mode rather
than write mode in order to keep decent performance. But the secondary key
lookup releases the primary entry from there with needs_locking = 0, which
means "the caller already holds the write lock", because dropping the last
reference removes the entry from the tree. A removal could therefore run
while other threads were walking the same tree under their own read lock.

Release the entry after the read-locked section instead, where taking the
write lock for the removal is allowed.

The row is handed back only once the reference has been dropped, and no
longer before: a row that returns to the avail list while a reference is
still held on its entry can be recycled by another thread, and the release
would then be applied to blocks that hold a response body. This is also why
the reattach cannot simply be left where it was.

This is present at least as far back as 3.0 and should be backported to all
stable branches.

Reported-by: Claude (ANT-2026-Y40G33XK)
28 hours agoBUG/MINOR: cache: do not retain imcomplete or stripped secondary entry
Rémi Tricot-Le Breton [Mon, 10 Aug 2026 15:22:43 +0000 (17:22 +0200)] 
BUG/MINOR: cache: do not retain imcomplete or stripped secondary entry

Skip retaining secondary entries altogether if its row was recycled
while we were looking it up, which CACHE_EF_COMPLETE having been cleared
tells us. Such an entry was already treated as a miss, only further down
and after having been detached.

This patch does not need to be backported.

28 hours agoBUG/MEDIUM: cache: retain the primary or secondary entry only when detaching its row
Rémi Tricot-Le Breton [Mon, 10 Aug 2026 15:22:13 +0000 (17:22 +0200)] 
BUG/MEDIUM: cache: retain the primary or secondary entry only when detaching its row

http_action_req_cache_use() retains the entry returned by the lookup, then
takes the shctx lock to detach its row. Until that detach the row is still
in the avail list, and shctx_row_reserve_hot() recycles a row under the
shctx lock alone, without ever taking the cache lock, so the cache read
lock does not protect a retained entry. In that window another thread can
recycle the row: the entry is queued on the cleanup list and its blocks are
refilled with a response body, after which our detach works from a
block_count and a last_reserved that no longer describe that row, splices
the wrong blocks out of the avail list and takes a reference on blocks
owned by another row. A block then sits both in the avail list and in a
live row, and the next thread reserving it reads a body as a cache_entry.

Retain the entry under the same shctx lock that detaches its row, and
only once it is known to be usable. A row recycled in the meantime is no
longer considered complete, which cache_free_blocks() clears under that
lock, so the existing test already rejects it. The entry is still
readable at that point because recycled blocks are only written once
shctx_row_reserve_hot() has returned, which cannot happen before
cache_reserve_finish() has taken the write lock on the tree our read
lock holds.

The other path then holds no reference, so it has nothing to release.

This should be backported to all stable branches.

28 hours agoIMPORT: slz/uslz: defer the output bookkeeping to a checkpoint
Aurelien DARRAGON [Tue, 11 Aug 2026 16:45:08 +0000 (18:45 +0200)] 
IMPORT: slz/uslz: defer the output bookkeeping to a checkpoint

Every emitted byte went through _PUT_UPDT(), which incremented five separate
counters by the same length (dec_bsize, dec_total, index, crc_flush and
distance_avail) and tested three limits, plus a fourth test on dec_bsize
before each symbol and a rollover test per loop iteration. For a literal
that is about ten operations and four branches around a single store, and
crc_flush lives in the state structure, so it was a read-modify-write
through memory which could never stay in a register.

Those counters are all the same position expressed in different units,
so we now only maintain the output pointer <out> and compare it against
a precomputed limit <out_lim>, the closest of the three points where
something actually has to be done:

  - the end of the ring, where <out> wraps back to <out_base> ;
  - the end of the current checksum batch ;
  - the point where the decoded block fills the caller's buffer.

When <out> reaches <out_lim> we reach a single "checkpoint" which accounts
for the bytes produced since the previous one, checksums them while they
are still hot in the cache, wraps the ring, reports a full buffer if
needed, recomputes the limit and jumps back to whichever of the three
emitting sites called it. Emitting a literal is now a store and a single
pointer increment, and the counters move at most once every CRC_BLOCK
bytes. The emitting loops have to compare against something to know when
to stop anyway, so the test on <out_lim> is not an added cost.

This comes with multiple benefits:

  - distance_avail is no longer needed. It was min(dec_total, out_max)
    and was only ever compared against a distance, which never exceeds
    32kB while the ring is at least that large, so "have we produced at
    least <distance> bytes" is exactly "has the ring wrapped, or is the
    distance within the current position". A <wrapped> flag gives the
    first half and the match copy already computes the second, so the
    check became free.

  - crc_flush was removed from the state. Rather than carrying pending
    bytes across calls, the return paths now flush the checksum, which
    continues to guarantee that on entry the ring position is exactly
    dec_total % out_max with nothing pending. That is what makes the
    whole thing work, and it also removes by construction the overflow
    that this field had to take care of.

  - the match copy is now driven by <out_lim>: since the destination
    can no longer wrap in the middle of a chunk, the four interleaved
    copy phases collapse into two cases, the match starting in the
    upper part of the ring or below the current position. A match no
    longer has to fit entirely in the remaining space either, it is
    emitted in as many chunks as needed, so USLZ_DECODE_E_OUT_BUFFER
    is now unreachable.

  - the stored block copy becomes a plain loop bounded by the input,
    the remaining block length and the limit.

  - the rollover check on dec_total moves to the checkpoint, the only
    place where the value changes now.

Measured with tests/codepad.sh, decompressing silesia in slz or in
gzip forms now shows:
                   before    after
  silesia.tslz     0.8762 s  0.7837 s   -10.6 %
  silesia.tgz      1.5175 s  1.3875 s    -8.6 %

And as a bonus, the code is now smaller (measured on x86_64):

  src/uslz.o        14078 -> 12574 (-1504)

tests/uslztest.sh remains at 1764/1764.

This is libslz upstream commit 716793740ca6347a95e3e90c86d2a9c7dc46ec2c

28 hours agoIMPORT: slz/uslz: inline the bit reader and the huffman decoders
Aurelien DARRAGON [Tue, 11 Aug 2026 16:41:42 +0000 (18:41 +0200)] 
IMPORT: slz/uslz: inline the bit reader and the huffman decoders

gethuff(), gethuff_fixed() and bit_accumulate() were all purposely marked
noinline due to tests showing a significant performance decrease and code
increase by inlining them. That's annoying because most of the arguments
they need can easily be optimized once the compiler has a full view of
them, and it's quite visible with perf top when decompressing an slz stream
that 30% of the CPU is spent in gethuff_fixed() and bit_accumulate(), and
that when decompressing a gzip stream, it's 62% in gethuff() alone!

It turns out that it's only combinations of 1 or 2 of them inlined that
ruins the performance, but inlining the 3 at once instead shrinks the
code and boosts the performance by letting the compiler keep all these
local variables in registers. As a proof, the code is now ~352 bytes
smaller, and 13 to 17% faster (respectively for slz and gzip streams).
That's one example of situations where individual changes bring nothing
good.

This is libslz upstream commit d1e73bed32b47ee43e91255126ab7fcf82a1e21b

28 hours agoIMPORT: slz/uslz: add a method to impose an envelope format (gzip or zlib)
Aurelien DARRAGON [Tue, 11 Aug 2026 16:40:05 +0000 (18:40 +0200)] 
IMPORT: slz/uslz: add a method to impose an envelope format (gzip or zlib)

This is actually a way to refuse non-matching formats and not letting the
decoder auto-detect a format. This is done using uslz_init_fmt() with a
4th argument instead of uslz_init():

  - SLZ_FMT_DEFLATE skips detection entirely, since the first bits of the
    stream are already the first block header (deflate has no envelpoe).

  - SLZ_FMT_GZIP and SLZ_FMT_ZLIB parse their envelope as before, but the
    stream is now rejected with E_CORRUPT if it does not carry the
    announced one, instead of falling back to raw deflate and returning
    garbage. That might matter when the format comes from the outside, an
    HTTP Content- Encoding for instance, where accepting a different
    envelope is wrong (though would likely have to be accepted anyway).

The main purpose is in fact to use this in tests to validate specific
envelopes, something that is currently not possible (i.e. if zenc lies,
zdec silently adapts).

This is libslz upstream commit d2829de469741f0c664fcc3e29d6152ab6993ef4

28 hours agoIMPORT: slz/uslz: decode all the members of a multi-member gzip stream
Aurelien DARRAGON [Tue, 11 Aug 2026 16:35:15 +0000 (18:35 +0200)] 
IMPORT: slz/uslz: decode all the members of a multi-member gzip stream

A gzip file is a series of members (rfc1952), which is what "gzip -c a b",
"cat a.gz b.gz" and most log rotators produce. uslz stopped after the first
one and returned USLZ_DECODE_SUCCESS, so the caller silently got truncated
content with no way to notice:

    cat a.gz b.gz | gzip -dc | wc -c   ->  151636
    cat a.gz b.gz | ./zdec   | wc -c   ->   76799    and rc=0

This is not commonly used with our targetted use cases but can sometimes
be seen in incremental backups for example where extra inputs will be
ignored.

The required change is not that big but is not obvious:

- First, the gzip trailer is 8 bytes, crc32 followed by isize, and only
  the crc32 was consumed. The four isize bytes were left in the stream,
  so nothing could have recognised the next member's magic behind them.
  Both halves are now accumulated before anything is compared, which
  also makes the trailer read resumable without having to remember how
  far into it we got; isize is consumed but not verified.

- Second, a new function, uslz_next_member() detects a following member
  and resets the per-member state (checksum, flags, state machine, bit
  accumulator) while preserving the output ring, the total decoded size
  and the drain offset, so that the members' contents are simply
  concatenated. It is called both right after a member completes, so
  that a single call decodes as many members as its input holds, and at
  the start of a call, so that a member boundary falling between two
  calls works too.

  The two magic bytes may be split across calls, and the header buffer
  cannot hold them in the meantime because it shares storage with the
  distance table which the member just decoded has overwritten. The bit
  accumulator isn't used at that point and survives across calls, so it
  is reused here to store the previous bytes and the confirmed magic is
  then passed to the format detection, which already knows how to
  accumulate the rest of a header across calls.

- Third, when a member ends exactly at the end of the input, there is no
  way to tell whether another one follows without more data. Success is
  reported, which is what a caller with nothing left to send needs, and
  the check is retried on the next call for a caller which has more.
  This is now stated in uslz_decode()'s documentation: success means
  complete as far as the data provided goes, and a caller with input
  left must call again anyway. Anything after the last member which is
  not a gzip magic is ignored as trailing garbage, as gzip(1) does.

With all this done, concatenating two silesia archives and passing them
to zdec properly now reports twice the uncompressed size.

This was the last failure of tests/uslztest.sh which now shows 1514/1514.

This is libslz upstream commit 76b983eccb8220fdd3083bd6d826a5ea07f45af3

28 hours agoIMPORT: slz/uslz: verify the trailer checksum before reporting completion
Aurelien DARRAGON [Tue, 11 Aug 2026 16:31:23 +0000 (18:31 +0200)] 
IMPORT: slz/uslz: verify the trailer checksum before reporting completion

uslz_decode_block() sets USLZ_FL_COMPLETE at the top of its completion
path, before reading and comparing the trailer checksum. If the trailer
bytes were not available yet, we jump to out_of_data, and uslz_decode()
then sees a complete stream in its drain path and jumps to end,
returning USLZ_DECODE_SUCCESS without ever calling uslz_decode_block()
again. Thus the checksum is never verified at all in this case, meaning
that under some scheduling circumstances, an invalid stream could appear
as valid.

On top of that, nothing ever assigns USLZ_ST_CHECK_CKSUM, so even if the
decoder had been called again, the state machine could not have resumed
in the middle of the trailer: it would have gone back to decoding symbols
and interpreted the trailer as deflate data.

The result was that a stream with a corrupted checksum was accepted,
silently, depending only on where the caller happened to cut its input.
On a 319-byte zlib stream the corruption went unnoticed at chunk sizes
1 to 7, 9, 15, 21, 35, 45 and 53, and on a 331-byte gzip stream with a
corrupted crc32 at 12, 13, 17, 18, 19, 25, 27, 36 and 54. (reminder,
deflate has no checksum).

This patch addresses this problem this way:

  - USLZ_ST_CHECK_CKSUM is set before reading the trailer, so that an
    out of data return really can resume there.

  - crc_flush is cleared after the final uslz_update_crc(), so as not
    to checksum the same bytes a second time in case of resume.

  - USLZ_FL_COMPLETE is set only *after* the comparison succeeded.

Now, trying to decode a corrupted trailer for either a zlib or gzip
stream properly reports a corruption (tested with 80 positions for each).

This is libslz upstream commit 1dc3cd773637477e5de33609d63527a0fd9bc4e7

28 hours agoIMPORT: slz/uslz: widen crc_flush, it overflowed on large stored blocks
Aurelien DARRAGON [Tue, 11 Aug 2026 16:27:49 +0000 (18:27 +0200)] 
IMPORT: slz/uslz: widen crc_flush, it overflowed on large stored blocks

crc_flush counts the bytes decoded but not checksummed yet. It was
declared as a (signed) short, while a single stored (uncompressed) block
copy can add up to a whole output buffer in one call, making it overflow:

    crc_flush=-32768 after +=32768 (index=32768)

That happens with any incompressible payload, which the encoders emit as
stored blocks, as soon as the input is fed in chunks large enough for the
copy to advance by more than 32767 bytes at once. The subsequent call to
uslz_update_crc() then gets a negative length. The checksum functions
happen not to touch memory in that case (their loops simply do not run),
so the visible symptom is a wrong checksum and a valid stream rejected
with USLZ_DECODE_E_BAD_CRC -- but the invariant crc_flush <= index is
broken from then on, and (out_base + index - crc_flush) can point before
the output buffer, which would read out of bounds.

Here we perform two changes:

  - crc_flush becomes an int. It has to be larger than the output
    buffer, not smaller.

  - the CRC_BLOCK batch flush becomes a while loop instead of an if.
    It only ever flushed one CRC_BLOCK per emitted chunk, so a large
    copy left the rest pending and defeated the point of batching the
    checksum in blocks small enough to stay in L1. With the loop,
    crc_flush is back below CRC_BLOCK after every emission, which also
    bounds it tightly.

It's easy to reproduce with a 40kB incompressible file (e.g. urandom):

    gzip -9 -c < rand40k > r.gz9
    ./uslztest 32768 1000000 < r.gz9     # was: error (5), now: crc=195cf5ee

tests/uslztest.sh goes from 1488/1514 to 1510/1514. The four remaining
failures are for future patches.

This is libslz upstream commit 96b80bb978e676db75a631bf63aaf5d32c22f015

28 hours agoIMPORT: slz/uslz: make the gzip FEXTRA field resumable
Aurelien DARRAGON [Tue, 11 Aug 2026 16:24:33 +0000 (18:24 +0200)] 
IMPORT: slz/uslz: make the gzip FEXTRA field resumable

The FEXTRA optional header field is a 2-byte little endian length followed
by that many bytes to skip, and either part can be split across calls. The
code had a fast path and a slow path for each of the two parts, and used
buf_len=0 to mean "nothing pending" but that is also true right after the
fast path has consumed the length and found the payload incomplete.

Resuming from that point took the fast path again and read the length a
second time, from what were in fact the first two payload bytes. With the
payload being "ABCDEF", XLEN became 0x4241 = 16961 instead of 6, so the
decoder skipped 16963 bytes of deflate data and the stream was lost.

This could be reproduced with a gzip stream carrying FEXTRA, FNAME,
FCOMMENT and FHCRC: it failed for input chunk size from 2 to 8 and 12
to 17, i.e. whenever a call boundary happened to fall inside the FEXTRA
field.

Let's replace the four paths by one: always stash XLEN in the header
buffer and use buf_len as the count of bytes of the whole field
consumed so far, so that below 2 we are reading the length and above
we are skipping the payload. That is resumable at any byte and needs
no special case, at the cost of skipping the payload one byte at a
time, which is fine for a field that is rare and usually a few bytes
long.

tests/uslztest.sh goes from 1438/1514 to 1488/1514.

This is libslz upstream commit f26fc5cd52abe47431832c807524262740d13450

28 hours agoIMPORT: slz/uslz: don't lose the bytes consumed by the header fast paths
Aurelien DARRAGON [Tue, 11 Aug 2026 16:21:28 +0000 (18:21 +0200)] 
IMPORT: slz/uslz: don't lose the bytes consumed by the header fast paths

The format autodetection in uslz_decode() has a fast path for the common
case where the first call carries enough data to parse the header directly
from the caller's buffer, and a slow path that accumulates the header into
hdr_detect.buf across calls. The fast path consumes bytes from the caller's
buffer *before* it knows whether it will be able to complete, and it had no
way to give them back, so three things went wrong:

1) Raw deflate (rfc1951) streams were losing their first two bytes. The
   fast path consumes two bytes to sniff the magic, and the fallback that
   feeds them back to the block decoder is guarded by "else if (buf_len)",
   which is false on exactly that path. Every raw stream fed more than 2
   bytes at a time therefore failed with E_CORRUPT (or E_INVALID_BLOCK_CODE,
   depending on what the two lost bytes happened to decode as), i.e. raw
   deflate was simply unusable:

       ./zdec < raw.deflate     ->  error (14)

   This is fixed by rewinding in_ptr to the start of the header, which
   also lets the fast path skip the local copy the accumulating path
   needs.

2) A gzip stream whose first call carried 3 to 9 bytes was mis-parsed.
   That is more than 2, so the fast path consumed the magic, but less
   than 10, so the 10-byte header could not be parsed, and it fell back
   to need_more_header, which starts filling hdr_detect.buf at buf_len=0,
   dropping the two bytes already consumed. The next call then parsed the
   header two bytes too far. This is fixed by rewinding before the
   fallback.
3) The tests on the remaining input used <compressed_size>, which is only
   decremented by the fast paths and never by the accumulating ones, so it
   overstates what is left in the caller's buffer as soon as the two are
   mixed. "if (compressed_size >= 2)" and "if (compressed_size >= xlen)"
   could therefore both succeed with fewer bytes actually available, read
   the FEXTRA length or payload **past in_top** and advance in_ptr beyond
   it. Fixed by testing state->in_top - state->in_ptr, which is exact on
   every path; <compressed_size> is no longer modified.

tests/uslztest.sh goes from 1108/1514 to 1438/1514, and raw, gzip, zlib
and slz streams now decode correctly at every input chunk size from 1 to
24 and beyond, for every supported ring size. The gzip variant carrying
FEXTRA is still broken at some chunk sizes, this is for a future fix.

This is libslz upstream commit cea782efdd111bf476b625051fe4d37cf28be92d

28 hours agoIMPORT: slz/uslz: don't reset the drain offset when nothing was decoded
Aurelien DARRAGON [Tue, 11 Aug 2026 16:19:09 +0000 (18:19 +0200)] 
IMPORT: slz/uslz: don't reset the drain offset when nothing was decoded

The drain code in uslz_decode() resets dec_bofs to zero when the amount
of data it can report is zero, in order to handle the case where the
previous drain stopped exactly on the end of the ring and the pending
block restarts at its beginning.

But this test also matches when there is simply nothing pending at all,
which happens on every USLZ_DECODE_OUT_OF_DATA return that could not
decode a single byte, i.e. very often when the caller feeds small input
chunks. In that case dec_bofs was reset while the pending data was in
fact located further in the ring, and the next drain handed the caller a
pointer to the wrong place, silently returning stale bytes.

The decoded stream was correct (the checksum matched), only the pointer
reported to the caller was wrong, which made this hard to notice: zdec
uses 8kB input chunks and almost always has something to drain.

Fix this by only resetting dec_bofs when there really is pending data.

With the new tests/uslztest.sh matrix this moves the number of passing
combinations from 642/1260 to 848/1260, the remaining failures being
pre-existing issues in the raw deflate and zlib trailer handling and in
the gzip header parsing with very small input chunks.

This is libslz upstream commit 747a544fb374e57d443bc553f092120b07c00a60

28 hours agoIMPORT: slz/uslz: fix incorrect sign extension in shift when reading the adler32...
Aurelien DARRAGON [Tue, 11 Aug 2026 16:16:52 +0000 (18:16 +0200)] 
IMPORT: slz/uslz: fix incorrect sign extension in shift when reading the adler32 trailer

The zlib trailer is assembled byte by byte into the 64-bit bit accumulator:

    bit_accum |= in_ptr[0] << (24 - num_bits);

in_ptr[0] is an unsigned char, that is unfortunately promoted to signed
int when shifted left, thus introducing a sign bit in upper bits if bit 7
was set with num_bits=0, that gets sign-extended to 64-bit in the
accumulator, resulting in a wrong checksum (and the stream is rejected
with USLZ_DECODE_E_BAD_CRC). Let's just cast it to uint32_t before shifting
to fix this. Note that other similar places already had the cast, this one
was just overlooked.

This is libslz upstream commit 98cf96c18d5b4636886ac66b9b6dbec323eaf11c

28 hours agoIMPORT: slz: clarify that the size promise applies to the stream, not to a call
Aurelien DARRAGON [Tue, 11 Aug 2026 16:14:45 +0000 (18:14 +0200)] 
IMPORT: slz: clarify that the size promise applies to the stream, not to a call

The output size guarantee of slz_rfc1951_encode() reads as if it applied
to every call, but up to 31 bits are retained in the queue from one call
to the next (on 64-bit systems), so a call may emit a few bytes that
belong to the data of the previous ones, and a single call may emit up to
5 bytes more than expected. Let's just clarify this to avoid future
surprises.

This is libslz upstream commit 5fa0c8da22b7d0a6d67f287a5a2af6af8e6d2b85

28 hours agoIMPORT: slz: avoid undefined shifts when building the word byte by byte
Aurelien DARRAGON [Tue, 11 Aug 2026 16:11:44 +0000 (18:11 +0200)] 
IMPORT: slz: avoid undefined shifts when building the word byte by byte

On the architectures that do not define UNALIGNED_FASTER, the 32-bit word
compared against the reference table is assembled one byte at a time:

    word = ((unsigned char)in[pos] << 8) +
           ((unsigned char)in[pos + 1] << 16) +
           ((unsigned char)in[pos + 2] << 24);

Unexpectedly, an unsigned char is promoted to a *signed* int when
shifting, so shifting a value >= 128 by 24 places can overflow it, which
is undefined behaviour depending on build options. It happens to produce
the expected result with the usual compilers, but ubsan on an i386 build
reports it for about half of input bytes:

  src/slz.c:482:107: runtime error: left shift of 220 by 24 places cannot
                     be represented in type 'int'

Let's just properly cast the uchars to u32 before shifting (this does
not change the produced code at all).

This is libslz upstream commit fbbb46aa54a4d330c6037f72693e0f79c7853354

28 hours agoIMPORT: slz: fix the adler32 accumulators signedness on 32-bit
Aurelien DARRAGON [Tue, 11 Aug 2026 16:09:33 +0000 (18:09 +0200)] 
IMPORT: slz: fix the adler32 accumulators signedness on 32-bit

slz_adler32_block() unfortunately uses a signed long as the crc accumulator
instead of an unsigned one, meaning that for CRC values where the 32th bit
is set on 32-bit machines, the right shift will drag sign bits and corrupt
it. This only affects zlib streams on 32-bit systems (rfc1950) and has
been there for a very long time, showing that the zlib format is really
not much used in target environments.

The fix is trivial, just change the accumulators to unsigned long.

This is libslz upstream commit 912a707525fd2d6a63c9884ef02f69e7379c304c

28 hours agoIMPORT: slz: do not append a block to an already finished stream
Aurelien DARRAGON [Tue, 11 Aug 2026 16:05:02 +0000 (18:05 +0200)] 
IMPORT: slz: do not append a block to an already finished stream

slz_rfc1951_flush() terminates the pending block then emits an empty
stored block to byte-align the output. But encode() called with <more>
cleared may leave the stream in SLZ_ST_LAST, that is, inside a block whose
BFINAL bit has already been sent. Flushing there terminated that block,
which completes the deflate stream, and then emitted one more block with
BFINAL set again. Those 5 bytes sit past the end of the stream, so the
gzip or zlib trailer that finish() appends right after is no longer where
the peer expects it: it reads the empty block as the checksum.

The data itself decodes correctly, only the check fails, which makes it
particularly difficult to diagnose. Raw deflate is not affected as it has
no trailer to shift, and a stream that ends in EOB state is not affected
either since its queue is empty and flush() returns early. Fuzzing random
sequences of encode/flush/finish showed ~2% corrupt streams for gzip and
zlib.

When the terminated block was the final one there is nothing left to align
to, and nothing may be appended, so let's just flush the pending bits and
return. This also makes the flush 4 bytes shorter in that case, and the
BFINAL bit of the empty block is now always zero, which is what the state
guarantees at that point.

This is libslz upstream commit 12e726390c96a21bd910d02a2fc594bcd743c131

28 hours agoIMPORT: slz: bound the bits wasted by the 9-bit literals
Aurelien DARRAGON [Tue, 11 Aug 2026 16:01:27 +0000 (18:01 +0200)] 
IMPORT: slz: bound the bits wasted by the 9-bit literals

Commit libslz/002e838 ("bug: always make sure to limit fixed output to
less than worst case literals") made sure that switching from EOB to FIXED
to emit a reference is only done when the reference pays for the way back,
based on the fact that once in FIXED state a reference is always smaller
than the bytes it replaces.

This was unfortunately not enough: the literals interleaved with those
references can still fail. Indeed, octets 144 to 255 cost 9 bits instead
of the 8 they would cost in a stored block. The <bit9> counter measures
exactly that, but it was reset after every reference, so a stream
alternating just under 52 such literals with a cheap reference never
reached the threshold that sends them as a stored block, and kept
inflating for as long as the pattern lasted. 51 random literals >= 144
followed by 4 bytes copied from 8 bytes earlier (i.e. almost the exact
same pattern as previously tested) produce 1073161 bytes out of 1048576
(+2.34%) where the API promises at most 1048663, i.e. 24 kB more than a
caller sizing its output buffer from that promise would expect for a
single call.

Bit9 isn't sufficient to track the debt cross references, so let's add a
second <debt> counter to the stream's unused space. It accumulates the
bits actually wasted by the literals emitted in huffman mode, and each
reference records what it saved over the same bytes sent as literals,
bounded to zero. Above SLZ_MAX_DEBT (200 bits, i.e. 25 bytes) the encoder
stops trusting bit9: pending literals are stored and references have to
compensate for the full round trip, which lets the literal runs merge into
a full 65535-byte block and stop the growth.

The crafted stream now produces 1048677 bytes (+14 bytes over the
documented maximum instead of +24509). Other inputs such as text or
silesia corpus do not show any change since it's quite hard to fall
into this case.

Note that the threshold is deliberately much larger than the 52 bits of
a switch to amortize oscillations without needlessly sending literals.

This is libslz upstream commit 039fdf8aac3acfdcaa27ae30b387c942bc58ef84

28 hours agoIMPORT: slz: use the exact switch cost for the last literals of a block
Aurelien DARRAGON [Tue, 11 Aug 2026 15:57:56 +0000 (17:57 +0200)] 
IMPORT: slz: use the exact switch cost for the last literals of a block

The decision to send the pending literals as a stored block rather than in
fixed huffman mode is taken when the 9-bit literals wasted more than the
52 bits it costs to leave the fixed huffman encoding and to come back to
it. But for the last literals of a block, nothing comes after the stored
block, so there is no need to pay for the block type of a next block nor
for the EOB, while the huffman variant still has to send an EOB. The
switch is thus 10 bits cheaper, and 10 more when the stream is still in
EOB state, since then the block type is needed in both cases and no EOB
has to be terminated.

Using 52 there made the encoder prefer huffman for data that was cheaper
to store, and the output could exceed the documented maximum. The smallest
case found by fuzzing is a 47-byte input entirely made of bytes >= 144
which produced 55 bytes (3 bits of block type + 47*9 bits + 7 bits of EOB)
where the stored block only needs 52, for a documented maximum of 54.

With these correct costs, we no longer see outputs exceed the documented
maximum, wether it's with small inputs (tested with ~3 million random
small inputs as small as 47 bytes), or usual files found in tests/ and
bash, gcc, libc, and silesia. No performance change was observed either.

Note that a stream can still exceed the documented maximum by a few bytes
(17 bytes were observed on a 390000-byte crafted input) because each
reference emitted between two stored blocks forces them out and adds a
5-byte block header that the accounting attributes to the reference. This
is for a future fix.

This is libslz upstream commit 97757536178f24aeb2cb41278706a88c1242f414

28 hours agoIMPORT: slz: fix the documented worst case size of flush() and finish()
Aurelien DARRAGON [Tue, 11 Aug 2026 15:54:06 +0000 (17:54 +0200)] 
IMPORT: slz: fix the documented worst case size of flush() and finish()

The documented output buffer requirements of the flush() and finish()
functions date back to the 32-bit queue, where at most 7 bits could be
pending. Since the 64-bit queue was introduced (used on x86_64 and armv8),
up to 31 bits may be pending, and the accounting also forgot the EOB that
may have to be emitted before the empty block. As a result a caller
strictly sizing its output buffer from the documentation could be short
by one to two bytes and see the encoder write past the end of its buffer.

Let's update the document worst cases for these functions depending on
what they still have to emit: 31 pending bits + 7 for EOB + 3 for
BFINAL/BTYPE + 7 for EOB or 32 for LEN+NLEN, rounded up to the next
byte (easily forgotten):

  function           claimed   real
  rfc1951_flush()        9      10
  rfc1951_finish()       4       6
  rfc1950_flush()       11      12
  rfc1950_finish()       8      10
  rfc1952_flush()       19      20
  rfc1952_finish()      12      14

Note that all values are at least as large as the previously claimed ones
and that 32-bit systems never consume more than what was claimed, so the
new documented values are valid both for 32 and 64 bits.

Even though this patch only touches comments, it's marked as a bug so
that it is backported where it matters and users have a chance to spot
the new values.

This is libslz upstream commit 1d774851bc0fe2788ef75d9d74057ecd0c54f868

28 hours agoIMPORT: slz: do not read past the end of the input around the match loop
Aurelien DARRAGON [Tue, 11 Aug 2026 15:48:50 +0000 (17:48 +0200)] 
IMPORT: slz: do not read past the end of the input around the match loop

slz_rfc1951_encode() pre-loads the first 3 bytes of the input into <word>
before entering the main loop on architectures which do not define
UNALIGNED_FASTER (e.g. i386, or big endian ones). This was done
unconditionally, thus inputs shorter than 3 bytes (including empty ones)
caused up to 3 bytes to be read past the end of the input buffer, which
may segfault if the buffer ends on the last page of a mapping. This is
easily reproduced on i386 by placing a zero-length input right before an
unmapped page.

The pre-loaded word is only ever used if the main loop is entered, which
requires at least 4 remaining bytes, so let's simply condition the load
on this.

The exact same case exists at the end of the loop where we can go beyond
end-3 and try to read 3 or 4 bytes before getting back to the beggining
of the loop, so we're using the same condition here, which helps the
compiler perform the test only once and use unconditional branches from
there.

The code is unchanged on x86_64 and armv8 (out of ifdef) and no
measurable change is observed on other archs.

This is libslz upstream commit 4ff4b66c804629089c0bb16f141a6320f92eba10

28 hours agoIMPORT: slz: update slz to version 1.3.0
Aurelien DARRAGON [Tue, 11 Aug 2026 15:42:49 +0000 (17:42 +0200)] 
IMPORT: slz: update slz to version 1.3.0

This version provides the uslz (u stands for uncompress) API that allows
to decompress zlib/gzip compatible streams (including the ones generated
by slz) using 32K sliding buffer. It is meant for users that care about
memory footprint, but if performance is the only priority, using zlib is
perfectly fine for this.

Credits to Andrew Church <achurch@achurch.org> for providing
https://achurch.org/tinflate.c aka tiny inflate library as public domain.
libslz decompression implementation was greatly inspired from tinflate
library by Andrew Church.

zdec utility was updated to leverage this new API in order to act as a
fully operationnal decompression tool instead of simply dumping basic
debug infos like it used to do.

Shortlog:

Aurelien DARRAGON (5):
        REORG: move generic internal function and helpers inside slz-prv.h file
        MAJOR: reorganize libslz to split compression API code from common API code
        MAJOR: slz: implement uncompress API (uslz)
        REORG: rename zdec utility to zdecode
        MINOR: add zdec decompression utility

This is libslz upstream commit 5a002417e1c3706c8cc835767e5f7b02c6ea5078

2 days agoREGTESTS: ssl: add ssl_c_policies test
Juan Pablo Mora [Thu, 6 Aug 2026 15:07:56 +0000 (17:07 +0200)] 
REGTESTS: ssl: add ssl_c_policies test

Add a VTC covering both modes of ssl_c_policies(): the full list of
Certificate Policies OIDs, and the "-m found" single-OID check (both
a present and an absent OID).

reg-tests/ssl/certs/client1.pem (used by the other ssl_c_* sample
tests) is an X509v1 certificate and carries no extensions at all, so
it cannot be reused here. A dedicated, disposable CA and client
certificate (ca_policies.crt / client_policies.pem) were generated
for this test only, with a Certificate Policies extension containing
0.4.0.1862.1.4 and a filler OID. No personal or production data is
involved, only generic placeholder subject fields consistent with
the other certs in this directory.

Verified locally with vtest (VTest2, "last" tag), alongside
ssl_client_samples.vtc and ssl_client_auth.vtc to confirm no
regressions. checkpatch.pl clean (only the generic, inapplicable
"does MAINTAINERS need updating?" notice, since there is no SSL
maintainer entry).

2 days agoMINOR: ssl: add ssl_c_policies sample fetch
Juan Pablo Mora [Thu, 6 Aug 2026 15:07:55 +0000 (17:07 +0200)] 
MINOR: ssl: add ssl_c_policies sample fetch

Until now there was no way in HAProxy to inspect the "Certificate
Policies" X509v3 extension of a client certificate presented during
mTLS client auth. This is needed to take routing/access decisions
based on the policy under which the certificate was issued, e.g. to
tell apart eIDAS qualified certificates whose private key is held in
a QSCD (policy OID 0.4.0.1862.1.4, id-etsi-qcp-legal-qscd) from other
client certificates.

This adds ssl_c_policies([<oid>]), following the same extraction
pattern already used by ssl_c_san (X509_get_ext_d2i() +
comma-separated list built in a trash chunk):

  - with no argument, it returns the full comma-separated list of
    policy OIDs (numeric dotted form) found in the certificate ;
  - with an <oid> argument, it only returns a sample when this
    specific OID is present among the certificate's policies, which
    allows using the "found" match method to take a decision, eg:

      acl qualified_qscd ssl_c_policies(0.4.0.1862.1.4) -m found
      http-request deny unless qualified_qscd

doc/configuration.txt is updated accordingly.

This is a pure addition, it does not touch any existing code path.
Built with -Wall -Wextra -Werror (no warnings) and validated against
doc/coding-style.txt's checkpatch.pl invocation: clean except for one
expected hit on the missing space in "ARG1(0,STR)", which matches the
pre-existing convention used by the other 22 entries of the same
sample_fetch_keywords table in this file.

3 days agoDOC: config: clarify req.ssl_sni
William Lallemand [Mon, 10 Aug 2026 11:20:56 +0000 (11:20 +0000)] 
DOC: config: clarify req.ssl_sni

The req.ssl_sni fetch (as well as all other req.ssl_* fetches, which
share the same underlying parsing) only analyzes the first ClientHello
message found in the request buffer.

This is not obvious to users, and can lead to incorrect assumptions
when the value is used for routing or access control decisions:

  - if the client sends a second ClientHello within the same TCP
    stream, for instance following a TLS 1.3 HelloRetryRequest, or
    during a TLS renegotiation, that second ClientHello (and its SNI)
    is silently ignored.

  - when Encrypted Client Hello (ECH) is used, only the "Outer"
    ClientHello is visible on the wire, so the SNI returned is a decoy
    SNI and not the actual host the client intends to reach. The
    "Inner" ClientHello, which carries the real SNI, is encrypted and
    cannot be analyzed by this fetch.

Document these limitations on req.ssl_sni, and add a short pointer to
that documentation on the other req.ssl_* fetches (req.ssl_alpn,
req.ssl_cipherlist, req.ssl_ec_ext, req.ssl_hello_type,
req.ssl_keyshare_groups, req.ssl_sigalgs, req.ssl_st_ext,
req.ssl_supported_groups, req.ssl_ver).

This was reported by Daniel Birtwhistle.

This should be backported to all stable versions.

3 days agoBUG/MEDIUM: hlua_fcn: ensure systematic bref cleanup for patref list iterator
Aurelien DARRAGON [Fri, 7 Aug 2026 15:07:36 +0000 (17:07 +0200)] 
BUG/MEDIUM: hlua_fcn: ensure systematic bref cleanup for patref list iterator

Similar bug as aeff2a3b2a ("BUG/MEDIUM: hlua_fcn: ensure systematic watcher
cleanup for server list iterator") but this time it affects patref list
iterator.

If the patref list iteration is interrupted (ie: break away from the loop
or lua error), we still need to unlink the bref we set earlier because
the hlua_patref_iterator_context is a temporary object so we cannot
let a reference once the object is dead. Obviously this can corrupt
the pattern reference element bref "users" list and lead to invalid
reads as well.

Reported-by: Claude (ANT-2026-DARC9AY8)
It should be backported up to 3.2 where hlua patref API was implemented.

3 days agoBUG/MEDIUM: lua: resume Channel:send() from the unsent part of the string
Aurelien DARRAGON [Thu, 6 Aug 2026 07:39:06 +0000 (09:39 +0200)] 
BUG/MEDIUM: lua: resume Channel:send() from the unsent part of the string

hlua_channel_send_yield() keeps in <l> the number of bytes already pushed
into the channel across yields, and correctly limits the amount it tries
to push next to the remaining "sz - l" bytes. But it always passed the
beginning of the string to _hlua_channel_insert(), so after a partial
write the same prefix was sent again instead of the remainder.

For a Lua script calling Channel:send() with more data than the channel
can currently hold, the peer therefore receives the beginning of the
buffer twice and never sees its tail. Any protocol framing carried over
that stream is silently desynchronized, and the script has no way to
notice since the returned count is correct.

Let's advance the source pointer by the number of bytes already sent, as
the applet send paths do.

This was introduced in 2.5 by commit a1ac5fb28 ("MEDIUM: filters/lua: Be
prepared to filter TCP payloads"). It must be backported to all stable
versions.

Reported-by: Claude (ANT-2026-GQE208VX)
5 days agoBUG/MINOR: quic: drop multiple Retry on same connection
Amaury Denoyelle [Fri, 7 Aug 2026 14:56:18 +0000 (16:56 +0200)] 
BUG/MINOR: quic: drop multiple Retry on same connection

Ensures that only a single Retry packet is handled by a QUIC haproxy
client per connection. This is mandated by RFC 9000. The first received
token should still be sufficient to validate the connection.

This change is applied directly in quic_rx_pkt_parse(). In case of
multiple Retry, packets are silently ignored, whether token is identical
or not.

This fix is particularly important to prevent a memory leak on several
elements, first <retry_token> member of quic_conn. This also concerns
elements from the TLS stack as initial encryption level would be
reinitialized needlessly.

Reported-by: Claude (ANT-2026-CJ4Z875H)
This must be backported up to 3.3.

5 days agoMINOR: quic: stress CRYPTO Rx buffer wrapping
Amaury Denoyelle [Fri, 7 Aug 2026 14:29:19 +0000 (16:29 +0200)] 
MINOR: quic: stress CRYPTO Rx buffer wrapping

Force CRYPTO Rx buffer to be unaligned when running under stress. Most
of the time, this will cause a connection error with
CRYPTO_BUFFER_EXCEEDED as CRYPTO wrapping is not yet implemented.

5 days agoBUG/MEDIUM: quic: prevent out-of-bound read on wrapping CRYPTO content
Amaury Denoyelle [Fri, 7 Aug 2026 13:52:28 +0000 (15:52 +0200)] 
BUG/MEDIUM: quic: prevent out-of-bound read on wrapping CRYPTO content

Received CRYPTO frames are buffered in a ncbmbuf to handle out-of-order
reception. When new content is available at the current offset, TLS
stack is notified so that it can read it. This is performed either via
ha_quic_ossl_crypto_recv_rcd() (for OpenSSL 3.5+) or
qc_ssl_provide_all_quic_data().

Depending on the receiving order, CRYPTO content may wrap over time.
This is currently not supported by haproxy as stated in a comment in
qc_ssl_provide_all_quic_data(), however there is no explicit code
protection to avoid it. Thus, a TLS stack may read past the CRYPTO
content as ncbmb_data() will report the size of data with wrapping
included.

The objective of this patch is to prevent any out-of-bound read attempt
by closing the connection on error. A check is added after buffering a
new CRYPTO frame in qc_handle_crypto_frm() : if content is wrapping, an
error CRYPTO_BUFFER_EXCEEDED is reported, the buffer is released and the
connection is closed. This happens before ha_quic_ossl_crypto_recv_rcd()
or qc_ssl_provide_all_quic_data() so this is a sufficient fix.

Note that the first idea was to directly patch
ha_quic_ossl_crypto_recv_rcd() / qc_ssl_provide_all_quic_data(). However
it is not easy as return value of these functions is ignored by their
callers.

Currently, this bug is unlikely as it's not possible to obtain a
condition for CRYPTO content to wrap with the available combination of
QUIC clients and their SSL library. It was reproduced only after manual
modification on ncbmb_init() to setup head buffer pointer near its end.
However, it's not guaranteed to not occur even without code patching so
the current fix is still necessary.

In the future, it may be necessary to complete this patch so that CRYPTO
wrapping can be realigned and decoded. A COUNT_IF() has been added to
help detect when this is the case.

Reported-by: Claude (ANT-2026-Y2QP9HED)
This should be backported up to 2.6.

5 days agoBUG/MINOR: mux-h2: strip the userinfo when deriving :authority for a server
Willy Tarreau [Thu, 6 Aug 2026 08:03:28 +0000 (10:03 +0200)] 
BUG/MINOR: mux-h2: strip the userinfo when deriving :authority for a server

The H2 mux properly drops userinfo from authority on input but doesn't
drop it on output if present on input (e.g. coming from H1), which will
cause a bad request when reaching a compliant H2 server such as itself.
Let's make sure it is properly dropped there as well, as required by
RFC9113. This should be backported to all stable versions.

Reported-by: Claude (ANT-2026-R03JNY63)
5 days agoBUG/MINOR: debug: only dump the trace once in __BUG_ON_ONCE()
Willy Tarreau [Fri, 7 Aug 2026 13:06:53 +0000 (15:06 +0200)] 
BUG/MINOR: debug: only dump the trace once in __BUG_ON_ONCE()

Amaury found that CHECK_IF() was dumping the stack trace on each call,
which was not the intent. The reason is that complain() performs the
check on the counter, but the decision to dump the stack later is based
on the choice between aborting and dumping. Let's break out of this when
the count is > 1. Under high concurrency it can cause a trace never to
be emitted but this is a detail. The whole thing needs to be redone
more cleanly anyway.

This should be backported to 2.6.

5 days agoBUG/MINOR: lb-chash: bound the walk when the saved cursor changed tree
Willy Tarreau [Thu, 6 Aug 2026 07:31:15 +0000 (09:31 +0200)] 
BUG/MINOR: lb-chash: bound the walk when the saved cursor changed tree

chash.last is not reset when the tree in use switches from the backup
servers back to the active ones. The next call then starts walking the
active tree while keeping a backup node as the stop condition, which it can
never reach, so it cycles over the active tree forever as soon as no server
can be picked: all of them saturated (maxconn reached with a queue, or
served >= the dynamic maxconn), or the only one left being the server to
avoid on a redispatch. In this case, if chash.last (assigned to <stop>)
isn't in the current tree, the loop will run forever.

Let's just count the number of times we wrap and stop at the second,
which indicates that the stop server is not in the tree.

Note that this is more theoretical than practical: this needs
"hash-type consistent" with requests not carrying the hash key so that
this fallback is used at all, backup servers with "option allbackups"
(without it lbprm.fbck is returned directly and the backup tree is never
walked), and all active servers going down then one coming back while
the remaining ones are saturated. In practice it has been there since
consistent hash was introduced in 1.4 by commit 6b2e11be1 ("[MEDIUM]
backend: implement consistent hashing variation") and was never
reported. It may be backported to all stable versions.

Reported-by: Claude (ANT-2026-98TCHHRD)
5 days agoBUG/MEDIUM: session: don't release a reversed connection twice on error
Willy Tarreau [Thu, 6 Aug 2026 07:27:02 +0000 (09:27 +0200)] 
BUG/MEDIUM: session: don't release a reversed connection twice on error

A reversed rhttp connection already has its mux installed when it
reaches session_accept_fd(). If an error occurs after setup (e.g. a
"tcp-request connection reject" rule, or conn_xprt_start() failing),
the error path calls session_free(), which destroys the mux and thus
releases the connection. Control then reaches out_free_conn, which
calls conn_release() on it a second time, causing a crash.

Stop after session_free() when the connection has a mux, since it's
already handled. The listener still needs explicit release.

This dates back to reverse-http's introduction in 3.0 (12c40c25a,
"MEDIUM: rhttp: create session for active preconnect"). Must be
backported to 3.0.

Reported-by: Claude (ANT-2026-WNWQ4RGC)
5 days agoBUG/MINOR: connection: reserve the whole CRC32C TLV before saving its pointer
Willy Tarreau [Thu, 6 Aug 2026 07:27:02 +0000 (09:27 +0200)] 
BUG/MINOR: connection: reserve the whole CRC32C TLV before saving its pointer

make_proxy_line_v2() saves a pointer to the CRC32C TLV value so the
checksum can be appended once the whole header is known. It only checked
for 3 bytes (the TLV header) instead of the 7 needed for the full TLV, so
with 3 to 6 bytes left, make_tlv() emitted nothing and returned 0, yet
tlv_crc32c_p was still set, and the final write_u32() wrote 4 bytes up to
3 bytes past the end of the header buffer.

Not only this theoretically only affects servers using "send-proxy-v2"
with "proxy-v2-options crc32c" combined with "set-proxy-v2-tlv-fmt" TLVs,
but in addition in practice the proxy protocol is designed to be way
shorter than a regular buffer, and the only practical way to reach that
is to purposely write a config to demonstrate this, so it cannot happen.

This has been there since crc32c support was added in 1.9 by commit
4399c75f6 ("MINOR: proxy-v2-options: add crc32c"). It should be backported
to all stable versions.

Reported-by: Claude (ANT-2026-H10QWEV6)
5 days agoBUG/MEDIUM: fd: release the port range entry in host byte order
Dragan Dosen [Fri, 7 Aug 2026 11:08:45 +0000 (11:08 +0000)] 
BUG/MEDIUM: fd: release the port range entry in host byte order

The port recovered with getsockname() in _fd_delete_orphan() was given
back to the port range without being converted to host byte order. The
range then fills up with byte-swapped values which are handed out on the
next pass. Both TCP and QUIC are affected.

It is now released using get_host_port(), which takes care of the
conversion for both address families.

The issue was introduced with commit 02b7685013 ("MEDIUM: fd: Remove
fdinfo").

No backport needed.

6 days agoMEDIUM: pipes: Never allocate more than maxpipes
Olivier Houchard [Fri, 7 Aug 2026 10:20:05 +0000 (12:20 +0200)] 
MEDIUM: pipes: Never allocate more than maxpipes

Commit c81d794822cf8fa3c2e163cbf4cba80ad0dd9a47 made it so we could have
one pipe pool per thread group, however it did not limit how many pipes
we'd allocate per thread group, so one thread group could end up having
all the pipes. Prevent that by allowing a maximum of maxpipes / nbtgroup
per thread group.

6 days agoDOC: Document limitations of tune.fd.tables
Olivier Houchard [Thu, 6 Aug 2026 12:39:39 +0000 (14:39 +0200)] 
DOC: Document limitations of tune.fd.tables

Document that when multi-queue is used with tune.fd.tables set to
per-thread-group, then connections will only be load-balanced across
threads from the same thread group.

6 days agoMINOR: backend: Deprecate tune.takeover-other-tg-connections.
Olivier Houchard [Thu, 6 Aug 2026 10:55:39 +0000 (12:55 +0200)] 
MINOR: backend: Deprecate tune.takeover-other-tg-connections.

Deprecate tune.takeover-other-tg-connections, as we're now supposed to
use tune.idle-pool.shared.

6 days agoMINOR: backend: Do not always allow takeover across thread groups
Olivier Houchard [Thu, 6 Aug 2026 10:49:54 +0000 (12:49 +0200)] 
MINOR: backend: Do not always allow takeover across thread groups

Instead of silently ignoring tune.idle-pool.shared. full
if tune.fd.tables per-thread-group is set, have an error at startup if
both are set, as they are incompatible.

6 days agoBUG/MINOR: acme: restrict the permissions of the generated account key
William Lallemand [Thu, 6 Aug 2026 07:46:19 +0000 (09:46 +0200)] 
BUG/MINOR: acme: restrict the permissions of the generated account key

When no ACME account key exists yet, haproxy generates one and writes it
through a plain BIO_new_file(), so the file is created with the process'
umask applied. With the common 022 umask the unencrypted private key ends
up on disk as 0644, readable by every local user.

Whoever reads that key can authenticate to the CA as this haproxy ACME
account, and from there deactivate it or manipulate the orders and
revocations for the domains it has validated.

Let's restrict the file to 0600 right after creating it and before writing
anything into it.

This was introduced in 3.2 by commit 856b6042d ("MEDIUM: acme: generate
the account file when not found"). It must be backported to 3.2.

Reported-by: Claude (ANT-2026-2TZ0NDHX)
6 days agoBUG/MINOR: ssl: reject server certificate names containing a NUL byte
William Lallemand [Thu, 6 Aug 2026 07:44:59 +0000 (09:44 +0200)] 
BUG/MINOR: ssl: reject server certificate names containing a NUL byte

ssl_sock_srv_verifycbk() decodes each SAN dNSName and each CN of the
server certificate with ASN1_STRING_to_UTF8(), which returns the decoded
length, then passes the result to the hostname matcher as a plain
NUL-terminated C string and throws the length away. A name encoded as
"victim.com\0.attacker.com" is therefore compared as "victim.com" and
matches.

This defeats the point of "verify required" together with "verifyhost" or
SNI on a server line: an attacker holding a certificate with such a name,
and able to intercept the connection to the backend, passes the name check
and can read and alter all the proxied traffic. This is the CVE-2009-2408
class of bug. It requires a CA to issue such a certificate, which modern
CAs refuse to do, so the practical risk is low, but the check is cheap.

Let's compare the decoded length with strlen() and ignore any name that
does not match.

This has been there since "verifyhost" was introduced in 1.5 by commit
be55431f9 ("MINOR: ssl: Add statement 'verifyhost' to "server"
statements"). It may be backported to all stable versions.

Reported-by: Claude (ANT-2026-SNXPSVKX)
6 days agoBUG/MEDIUM: acme: don't delete a NULL token from the map
William Lallemand [Thu, 6 Aug 2026 07:29:07 +0000 (09:29 +0200)] 
BUG/MEDIUM: acme: don't delete a NULL token from the map

When an ACME task ends, acme_del_acme_ctx_map() walks the list of
authorizations and removes each challenge token from the configured map.
But an authorization is created with only its URL set, and its token is
only filled in once the CA's answer for that authorization has been
successfully parsed. The "dns-persist-01" challenge type never sets one at
all. Every termination path of the task goes through that cleanup, so a
NULL token was passed to pat_ref_delete(), which compares it against the
keys of the reference and dereferences it.

For users this means that an ACME configuration using a "map" crashes the
worker whenever the certificate renewal fails early, for instance when the
CA is unreachable and the retries are exhausted, or when it answers
something unexpected. The very component that is supposed to keep the
service running then takes it down.

Let's simply ignore authorizations without a token, they have nothing
registered in the map anyway.

This was introduced in 3.2 by commit 5555926fd ("MEDIUM: acme: use a map
to store tokens and thumbprints"). It must be backported to 3.2.

Reported-by: Claude (ANT-2026-YXC5HJZS)
6 days agoBUG/MEDIUM: ssl: require a full-length AEAD tag when decrypting with AES-GCM
Remi Tricot-Le Breton [Thu, 6 Aug 2026 07:34:24 +0000 (09:34 +0200)] 
BUG/MEDIUM: ssl: require a full-length AEAD tag when decrypting with AES-GCM

aes_process() passes the caller-provided tag length straight to
EVP_CTRL_AEAD_SET_TAG, and OpenSSL accepts and verifies GCM tags as short
as one byte. In the "aes_gcm_dec" converter the tag argument is documented
as coming from a variable, which in practice is populated from request
data, and in the JWE AES-GCM key-wrap path it comes directly from the
token. In both cases the party submitting the ciphertext also chooses how
many bytes of the authentication tag get checked.

An attacker submitting a one-byte tag therefore only needs about 256
attempts, instead of 2^128, to have arbitrary ciphertext accepted as
authentic. For a configuration relying on these converters to validate a
signed or encrypted token, this is a full authentication bypass.

The encrypt side always emits a 16-byte tag, so let's simply require
exactly that on the decrypt side.

This has been there since these converters were introduced, the shared
helper coming from commit f0e64de75 ("MINOR: ssl: Factorize AES GCM data
processing") in 3.4. It must be backported to all stable versions
providing "aes_gcm_dec".

Reported-by: Claude (ANT-2026-15HD08AS)
6 days agoBUG/MINOR: jwt: don't take an extra reference on the certificate public key
Remi Tricot-Le Breton [Thu, 6 Aug 2026 07:35:42 +0000 (09:35 +0200)] 
BUG/MINOR: jwt: don't take an extra reference on the certificate public key

X509_get_pubkey() already returns an owned reference, so the extra
EVP_PKEY_up_ref() on the cert-store path leaked one reference per
verification, since only one EVP_PKEY_free() is done afterwards. Over
time this means key objects accumulate and are never released, even
across certificate reloads.

Drop the extra reference. The "jwt_cert_tree" path below still needs
its up_ref, since it only holds a borrowed pointer.

Introduced in 3.3 by 522bca98e ("MAJOR: jwt: Allow certificate instead
of public key in jwt_verify converter"). Must be backported to 3.3.

Reported-by: Claude (ANT-2026-PKPCQ3ZN)
6 days agoBUG/MEDIUM: jwe: validate the secret length against the algorithm of the token
Remi Tricot-Le Breton [Thu, 6 Aug 2026 07:33:59 +0000 (09:33 +0200)] 
BUG/MEDIUM: jwe: validate the secret length against the algorithm of the token

The "alg" field of the JOSE header of a JWE token selects the key-wrapping
algorithm, hence the AES key size, while the key itself is the secret
configured by the operator for the "jwt_decrypt_secret" converter. That
secret is stored in an exact-size heap allocation, and neither
decrypt_cek_aeskw() nor aes_process() (used by the AES-GCM key wrap
variant) checked that it was large enough for the selected cipher before
handing its address to OpenSSL.

So a client sending a token that declares A256KW or A256GCMKW while the
configured secret is only 16 bytes long makes OpenSSL read 16 bytes past
the end of that allocation and use them as key material. This is a
remotely triggered heap over-read which may crash the worker, and whose
bytes influence the decryption result.

Let's check the secret length against the cipher's key length in both
paths before initializing the cipher.

Both were introduced in 3.4, by commits f0e64de75 ("MINOR: ssl: Factorize
AES GCM data processing") and 416b87d5d ("MINOR: jwe: Add new
jwt_decrypt_secret converter"). This must be backported to 3.4.

Reported-by: Claude (ANT-2026-TS9WFQ5T)
Reported-by: Claude (ANT-2026-9T34RNDD)
6 days agoBUG/MEDIUM: log: always reserve room for trailing 0 when using CBOR encoding helpers
Aurelien DARRAGON [Thu, 6 Aug 2026 19:15:36 +0000 (21:15 +0200)] 
BUG/MEDIUM: log: always reserve room for trailing 0 when using CBOR encoding helpers

Logging helpers leveraged by sess_build_logline_orig() can be split in
two different groups. Although they all look similar in their construction
as they take pretty much the same parameters and return the address where
following bytes can be appended, some will always try to append the
terminating NULL byte and return the address of the terminating NULL
byte, while others (which are not specifically text oriented) will simply
use all available space (they don't reserve space for the terminating NULL
byte) and return the address of the byte following the last byte written.
But since they don't try to write the \0 themselves, they will in practise
output one extra byte compared to other helpers. If no precaution is taken
and they are used as drop-in replacement to text oriented ones, this can
cause invalid writes later in the code because sess_build_logline_orig()
will always append the terminating NULL byte (even if it is already set),
thus it is mandatory that the output pointer never reaches the stopmark.

Fortunately, most pitfalls were already avoided in log generation path,
but recent commit c614fd3b ("MINOR: log: add +cbor encoding option")
made use of several encoding helpers which were not text oriented as
text oriented ones. Let's fix that by always securing 1 byte for the
terminating NULL byte when calling them (even if it not used by the
endpoint X format, sess_build_logline_orig() will append it not matter
what, so we have to live with that).

Reported-by: Claude (ANT-2026-QQ17FDX1)
It should be backported up to 3.0.

6 days agoMINOR: log/tools: fix ambiguous comments for some log encoding helpers
Aurelien DARRAGON [Thu, 6 Aug 2026 17:20:04 +0000 (19:20 +0200)] 
MINOR: log/tools: fix ambiguous comments for some log encoding helpers

Some log encoding helpers are not text oriented, and they will use all
available space since they will not try to write a terminating NULL
byte at the end of the produced output themselves. But since they
work similarly to text oriented ones, they return the address of
the byte immediately following the payload (where we expect
following bytes to be written). For text oriented ones this corresponds
in fact to the terminating NULL byte which was accounted in the
available space, while for non text-oriented ones, which doesn't reserve
space for the terminating NULL byte this corresponds to 1 byte past the
payload. If available space is strictly the size of the produced output,
then it means the returned address will be 1 byte PAST the stop limit so
no extra bytes could be written anymore. When using this helpers, callers
have to be very careful to reserve bytes (ie: terminating NULL byte) if
they need to.

It may be backported up to 3.0. Before that such ambiguities didn't exist
as logging features were strictly text-oriented and log encoders were not
available.

6 days agoBUG/MINOR: mux-fcgi: sanitize the STDERR records before logging them
Olivier Houchard [Thu, 6 Aug 2026 11:54:52 +0000 (13:54 +0200)] 
BUG/MINOR: mux-fcgi: sanitize the STDERR records before logging them

fcgi_strm_handle_stderr() emits one log line per STDERR record, appending
its own newline, but passes the record payload to app_log() with a bare
"%s", and neither app_log() nor __send_log() escape anything. FastCGI
applications routinely echo parts of the request in their warnings, so a
client whose input is reflected there can insert CR/LF and turn one record
into several log lines, or insert ESC sequences which the operator's
terminal interprets when reading the log. Verified with an application
writing "bad input 'x\r\nFAKE-INJECTED-LINE: ...\033[31m...'": the syslog
datagram carries all of it verbatim.

Escaping data emitted to logs is normally a configuration matter, but this
path bypasses the log-format machinery entirely, and the intent here is
clearly one record per line. Let's replace the control characters with a
dot before logging, which is a single pass over a record that is only ever
produced when the application writes to its stderr. Only controls are
replaced, so that the UTF-8 messages commonly found in such warnings are
left intact.

This has been there since the FCGI mux was introduced in 2.1 by commit
99eff65f4 ("MEDIUM: mux-fcgi: Add the FCGI multiplexer"). It may be
backported to all stable versions.

Reported-by: Claude (ANT-2026-9JD79F3M)
6 days agoBUG/MEDIUM: http-ana: check the cookie rewrite result before moving the offsets
Olivier Houchard [Thu, 6 Aug 2026 07:47:15 +0000 (09:47 +0200)] 
BUG/MEDIUM: http-ana: check the cookie rewrite result before moving the offsets

In the "rewrite" and "prefix" cookie modes,
http_manage_server_side_cookies() calls http_replace_header_value() and
ignores its return value. When the expansion doesn't fit it returns 0 and
leaves ctx.value untouched, yet the code still computes <delta> from
srv->cklen, advances next and hdr_end by it, and in "prefix" mode writes
"val_beg[srv->cklen] = COOKIE_DELIM" over data that was never moved. The
delimiter thus lands in whatever follows the cookie and the response is
forwarded with that byte corrupted, while the cookie is left unprefixed,
silently losing persistence.

The maxrewrite reserve normally covers the expansion, so this needs the
inserted string to be larger than it. Reproduced both with a 1100-byte
server cookie name and the default tune.maxrewrite, and with a 61-byte one
and "tune.maxrewrite 16": the Set-Cookie comes back untouched and a '~'
appears in the middle of the next header's value.

Let's check the return value like every other rewrite site does: on
failure nothing is touched, the failed_rewrites counters are incremented
and the parsing of that response's cookies stops there.

This has been there since the HTX cookie handling was added in 1.9 by
commit fcda7c685 ("MINOR: proto_htx: Add functions to manage cookies on
HTX messages"). It may be backported to all stable versions.

Reported-by: Claude (ANT-2026-FVC9MZEJ)
6 days agoBUG/MEDIUM: sock: bound the recvmsg() length when receiving old sockets
Olivier Houchard [Thu, 6 Aug 2026 07:32:10 +0000 (09:32 +0200)] 
BUG/MEDIUM: sock: bound the recvmsg() length when receiving old sockets

sock_get_old_sockets() sizes tmpbuf from the number of FDs announced by the
old process, but passes a fixed iov_len of MAX_SEND_FD entries to every
recvmsg() and loops as long as fewer FDs than announced were received,
without ever comparing curoff to the size of the allocation. A peer
announcing a single FD (4118 bytes allocated) and then streaming plain data
with no SCM_RIGHTS makes the kernel write up to 252*4118 bytes per recvmsg()
past the end of the buffer, and the loop never ends. Reproduced with a fake
old process: glibc aborts on "free(): invalid next size" after ~320 kB. Only
the peer of the -x transfer socket can do this, so it is not reachable from
the network, but it happens before privileges are dropped.

Let's clamp each recvmsg() to the room really left in the allocation and
abort the transfer when the peer sends more. Legitimate transfers are
unaffected, they use at most 1+255+1+255+4 bytes per FD.

This has been there since commit f73629d23 ("MINOR: global: Add an option to
get the old listening sockets.") in 1.8, which already sized tmpbuf on fd_nb
and the iovec on MAX_SEND_FD. It may be backported to all stable versions.

Reported-by: Claude (ANT-2026-Q363CKEH)
6 days agoBUG/MEDIUM: stick-tables: use the same bucket for string keys with a NUL
Olivier Houchard [Thu, 6 Aug 2026 07:30:13 +0000 (09:30 +0200)] 
BUG/MEDIUM: stick-tables: use the same bucket for string keys with a NUL

String stick-table keys are stored NUL-terminated and looked up in a string
ebtree, so an entry is identified by the bytes preceding the first NUL.
Accordingly stksess_kill(), __stksess_kill_if_expired(), stktable_lookup(),
stktable_requeue_exp() and stktable_set_entry() all derive the bucket from
strlen() of the stored key, but stktable_lookup_key() and
stktable_get_entry() derive it from the raw sample length. A key carrying an
embedded NUL is thus inserted in one bucket and later killed or requeued
while holding the lock of another one, so that bucket's tree ends up
modified without its lock while other threads look it up: corrupted tree,
hence a crash, a lost or duplicated entry, or a use-after-free on a stksess.

Embedded NULs are not exotic: url_decode() turns "%00" into one and keeps
going, while smp_to_stkey() passes the sample length as-is. Tracking
"url_param(q),url_dec" into a string table and sending "GET /?q=AB%00CD"
yields key_len 5 but strlen 2, hence two different buckets. The peers
protocol also transports raw key bytes.

Let's make the two remaining places stop at the first NUL as well, so that
a single canonical length is used everywhere.

The bucket split was introduced in 3.0 by commit 1a088da7c ("MAJOR:
stktable: split the keys across multiple shards to reduce contention").
This must be backported to 3.0.

Reported-by: Claude (ANT-2026-TNFHK5ZG)
6 days agoBUG/MEDIUM: http-ana: don't crash on "keep-query" in a response redirect
Olivier Houchard [Thu, 6 Aug 2026 07:28:26 +0000 (09:28 +0200)] 
BUG/MEDIUM: http-ana: don't crash on "keep-query" in a response redirect

http_apply_redirect_rule() always takes its HTX from the request channel
(htxbuf(&s->req.buf)), and the "keep-query" option rebuilds the query string
from the request start line. This works for a request redirect, but the
option is also accepted on "http-response redirect", and by then the request
has usually been forwarded and its buffer is empty, so http_get_stline()
returns NULL and htx_sl_req_uri() dereferences it. Thus a rule such as
"http-response redirect location /moved keep-query" crashes on the first
request that matches it (reproduced with a plain "GET /foo?a=b").

There is no query-string to preserve once the request is gone, so let's skip
that part when the start line is no longer available and emit the location
as-is. Scheme- and prefix-based redirects are rejected on the response path
by the parser, so they are left untouched.

This was introduced in 3.1 by commit b2877db47 ("MINOR: http-ana: Add
option to keep query-string on a localtion-based redirect"). It must be
backported to 3.1.

Reported-by: Claude (ANT-2026-7AZMS41X)
6 days agoBUG/MINOR: mux-fcgi: don't call fcgi_strm_destroy() on a NULL stream
Olivier Houchard [Thu, 6 Aug 2026 07:26:05 +0000 (09:26 +0200)] 
BUG/MINOR: mux-fcgi: don't call fcgi_strm_destroy() on a NULL stream

fcgi_stconn_new() has three "goto out" taken before the stream is allocated
(streams limit reached, no stream left) or when the allocation failed, and
the out label unconditionally calls fcgi_strm_destroy(), which dereferences
<fstrm> right away. So a failure to allocate the stream or its tasklet
crashes instead of returning a clean error. The first two paths are
normally prevented by the reuse layer which checks avail_streams first.

Let's just skip the destruction when the stream is NULL.

This came with commit 070b91bc1 ("MEDIUM: conn-stream: Be prepared to fail
to attach a cs to a mux") in 2.6, which added this destroy call for the new
sc_attach_mux() failure path without protecting the pre-existing ones (the
other muxes did it right). It must be backported to all stable versions.

Reported-by: Claude (ANT-2026-VN29N97G)
6 days agoBUG/MEDIUM: spoe: clear the applet pointer when the applet fails to start
Olivier Houchard [Thu, 6 Aug 2026 07:26:05 +0000 (09:26 +0200)] 
BUG/MEDIUM: spoe: clear the applet pointer when the applet fails to start

spoe_create_appctx() assigns the freshly allocated spoe_appctx to
ctx->spoe_appctx before creating and initializing the applet, both of which
may fail. On these error paths the spoe_appctx is released but the pointer
is left in the SPOE context, and the caller reports the failure through
spoe_stop_processing(), which reads it back, writes into it, then performs
appctx_strm(sa->owner)->parent = NULL and appctx_wakeup(sa->owner). As
<owner> sits at offset 0, right where pool_free() stores its cache linkage,
it is not even NULL but points into the pool cache, so these two writes go
through a bogus appctx. An allocation failure is needed to reach this,
either the appctx itself or the session/stream set up by spoe_init_appctx().

Let's simply clear ctx->spoe_appctx before releasing the applet context.

This was introduced in 3.1 by commit 07cf7769c ("MEDIUM: spoe: Directly
xfer NOTIFY frame when SPOE applet is created"). It must be backported to
3.1.

Reported-by: Claude (ANT-2026-KBZN81X2)
6 days agoBUG/MEDIUM: mux-fcgi: check the room left before appending the index
Olivier Houchard [Thu, 6 Aug 2026 07:21:38 +0000 (09:21 +0200)] 
BUG/MEDIUM: mux-fcgi: check the room left before appending the index

In fcgi_set_default_param(), when the decoded path ends with a '/' and the
fcgi-app declares an "index", params->scriptname is set to span the path
plus the index before both are appended to the trash chunk, and the return
value of these appends is ignored. If the chunk is full, nothing is written
but scriptname still points past its tail, so SCRIPT_NAME and
SCRIPT_FILENAME are encoded from memory located past the end of the chunk
and sent to the FastCGI application. The copy of the path a few lines above
was not checked either. Since the URI is copied into the chunk first and
the path copied again, a path larger than about a third of a buffer is
enough to reach this.

Let's check both appends and only publish the script name once its content
is really there.

Reported-by: Claude (ANT-2026-WPM4GZPQ)
6 days agoDEV: haring: bound the ring geometry to the size of the file
Willy Tarreau [Thu, 6 Aug 2026 16:33:19 +0000 (18:33 +0200)] 
DEV: haring: bound the ring geometry to the size of the file

Truncated ring files are not exceptional and the tool wasn't very robust
against them and could easily crash when trying to read past the end of
the mapping. Let's bound the sizes and offsets announced in the header
to the real file size. This also allows to relax some checks that would
reject some malformed files that are now handled as best effort ring v1.

It can be helpful to backport this to stable releases, though no issue
was really reported outside of the dev team.

Reported-by: Claude (ANT-2026-TCDNPXKV)
6 days agoBUG/MINOR: hlua: use a local buffer to format the socket addresses
Willy Tarreau [Thu, 6 Aug 2026 07:38:41 +0000 (09:38 +0200)] 
BUG/MINOR: hlua: use a local buffer to format the socket addresses

hlua_socket_info() formats the peer or local address of a Lua socket into
a function-static buffer shared by all threads. But there's no reason for
this buffer to be static, and it can cause inter-thread corruption. Let's
just drop the static modifier so that the address lies in the stack.

It can be backported to all versions since it's been there since 1.6
when sockets were introduced to Lua.

Reported-by: Claude (ANT-2026-W66XVDTK)
6 days agoBUG/MINOR: stats-file: reject tgid 0 when preloading shm objects
Willy Tarreau [Thu, 6 Aug 2026 07:25:13 +0000 (09:25 +0200)] 
BUG/MINOR: stats-file: reject tgid 0 when preloading shm objects

When preloading the shared memory stats file at startup,
shm_stats_file_preload() only checked that the tgid read from each
object was not greater than the number of configured thread groups,
but it forgot to also check for zero, which can correspond to an
entry in the process of being reused, and that would cause the
process to crash on startup.

Let's just also test 0. This should be backported to 3.3.

Reported-by: Claude (ANT-2026-71A0JC6Q)
6 days agoBUG/MEDIUM: hpack: encode long methods and schemes using the long form
Willy Tarreau [Thu, 6 Aug 2026 07:19:58 +0000 (09:19 +0200)] 
BUG/MEDIUM: hpack: encode long methods and schemes using the long form

hpack_encode_scheme() and hpack_encode_method() document that they're
limited to 127 chars since they only rely on hpack_encode_short_idx()
for literals, but the H2 mux doesn't check this. This results in only
the bytes modulo 256 being advertised on a backend connection. As such
the remaining bytes will be confused with other HPACK opcodes. Note
that in practice, the ability to exploit this to inject headers from a
front H1 connection is very limited due to the strict alphabet enabled
in schemes and methods which limits usable codes to literal headers with
indexing for absent pseudo-headers (i.e. no :scheme, :method, :path,
possibly one :authority when none is provided, but it must then match
the host), and whose value will be of 43 chars minimum.

The real impact in practice is to provoke protocol errors and cause
shared H2 backend connections to be abruptly closed in environments
using http-reuse always.

Let's simply make both encoders fall back to hpack_encode_long_idx()
for value larger than 127 bytes, like hpack_encode_path() does. The
bug has been present since 1.9 with commit 39c80ebff ("MINOR: hpack:
provide a function to encode an HTTP method").

Reported-by: Claude (ANT-2026-9SVV6W3Q)
This fix must be backported to all stable versions.

6 days agoREGTESTS: proxy: complete "del backend" test
Amaury Denoyelle [Thu, 6 Aug 2026 15:18:08 +0000 (17:18 +0200)] 
REGTESTS: proxy: complete "del backend" test

A recent fix was introduced for "del backend" on 3.4 when deleting the
first proxy declared in the configuration. This issue does not affect
the current development tree.

To ensure this case is safe, complete backend deletion reg-test with a
deletion on the first declared proxy. This could prevent any future
regression for this particular case.

This should be backported up to 3.4.

6 days agoBUG/MINOR: proxy: release watcher in "show default-server" on CLI abort
Amaury Denoyelle [Thu, 6 Aug 2026 14:19:32 +0000 (16:19 +0200)] 
BUG/MINOR: proxy: release watcher in "show default-server" on CLI abort

This patch is similar to the previous one. This time, it fixes the
command "show default-server", implemented in the current release
branch.

No need to backport.

6 days agoBUG/MINOR: proxy: release watcher in various commands on CLI abort
Amaury Denoyelle [Thu, 6 Aug 2026 14:11:53 +0000 (16:11 +0200)] 
BUG/MINOR: proxy: release watcher in various commands on CLI abort

Watcher has been added to protect several commands in src/proxy.c which
iterate over either the proxies or servers list against a runtime
deletion.

However, no io_release has been defined for such commands. This may
cause issue in case the command has yielded and is aborted before its
full completion, for example on client early disconnect. Related applet
context is freed while still subscribed into a proxy or server instance.
When this instance is deleted, a crash occurs on freed applet context
access.

To prevent this, define a dedicated io_release for the following
commands : "show servers state|conn", "show backend" and "show errors".
Any watcher is detached there.

Reported-by: Claude (ANT-2026-6Q55R82E)
This must be backported up to 3.4.

7 days agoMINOR: config: support "all" and "none" on "tune.defaults.purge"
Amaury Denoyelle [Thu, 6 Aug 2026 07:20:29 +0000 (09:20 +0200)] 
MINOR: config: support "all" and "none" on "tune.defaults.purge"

Extends "tune.defaults.purge" to support new arguments value. The values
"all" is equivalent to "proxies,servers".

The value "none" instructs to preserve all instances, which is similar
to the default behavior when the keyword is absent. It is added mainly
to simplify configuration by external APIs.

Both these values are exclusive : they cannot be mixed into the
comma-delimited list of tokens.

7 days ago[RELEASE] Released version 3.5-dev4 quic-interop flx04/quic-interop v3.5-dev4
Willy Tarreau [Thu, 6 Aug 2026 07:00:35 +0000 (09:00 +0200)] 
[RELEASE] Released version 3.5-dev4

Released version 3.5-dev4 with the following main changes :
    - CLEANUP: mux_quic: remove unused prototype
    - BUG/MEDIUM: proxy: protect "show errors" against backend deletion
    - MINOR: proxy: stress "show errors" handler
    - BUG/MINOR: haload: fix use-after-free upon updating task expiration
    - BUG/MINOR: haload: set default thread count to 1
    - BUG/MINOR: haload: fix display glitches by flushing stdout in summary
    - MINOR: haload: add rate limiting support using -R option
    - MINOR: log: use curproxy during config parsing
    - MINOR: config: define wrapper for proxies loop during check config
    - MINOR: log: convert list to standard doubly linked one
    - MINOR: sink: convert list to standard doubly linked one
    - MINOR: proxy: centralize proxies_list insert during config parsing
    - MINOR: proxy: define proxies_list iteration functions
    - MAJOR: proxy: convert proxies_list to a doubly linked struct list
    - OPTIM/MEDIUM: proxy: avoid main proxies list reordering on startup
    - CLEANUP: proxy/config: clean up after proxies list conversion
    - MINOR: proxy: rename proxies list to all_proxies
    - BUG/MEDIUM: ssl: Spell HAVE_VANILLA_OPENSSL correctly
    - BUG/MEDIUM: ssl: Handle non-application data record while splicing
    - MEDIUM: ssl: Add a way to rate-limit TLSv1.3 KeyUpdate
    - DOC: ssl: Document tune.ssl.keyupdate-rate-limit
    - BUG/MEDIUM: ssl: Put CO_ER_SSL_KEYUPDATE at the right place
    - BUILD: ssl: Do not use SSL3_MT_KEY_UPDATE, hardcode 24 instead
    - MINOR: server: rename global servers_list to all_servers
    - MINOR: server: do not return next server on srv_drop()
    - MINOR: proxy: define server list iteration functions
    - MAJOR: proxy: convert server list to a doubly linked struct list
    - OPTIM/MEDIUM: proxy/server: avoid server list reordering on startup
    - OPTIM: tools: keep a cache of recent localtime() and gmtime()
    - DEBUG: fd: catch access attempts to closed FDs
    - MINOR: halog: Add reusable function to extract the value of header captures
    - CLEANUP: halog: Clean up naming for variables related to `-hdr` processing
    - MINOR: halog: Add support filtering on header capture values using -hdr-match
    - REORG: h1-htx: Move h1 headers map in h1-htx
    - BUG/MEDIUM: mux-h1: Always adjust case for all outgoing headers as expected
    - MINOR: mux-h1: Lower the case for Sec-Websocket-* headers when manually added
    - MINOR: mux-h1: Use htx version to send default low-level errors
    - BUG/MINOR: haload: fix CPU topology detection by omitting forced "nbthread"
    - CLEANUP: haload: use <arg_thrd> instead of <global.nbthread> where applicable
    - BUG/MINOR: http-htx: fix the length moved when removing a header value
    - BUG/MEDIUM: http-fetch: don't parse a non-HTTP check buffer as an HTX message
    - BUG/MEDIUM: http-fetch: reject a negative capture id in capture.{req,res}.hdr
    - BUG/MINOR: http-fetch: fix a NULL channel dereference in smp_fetch_body()
    - BUG/MINOR: http: fix an out-of-bounds read in http_get_host_port() on empty host
    - BUG/MINOR: http-htx: check the trash allocation in http_scheme_based_normalize()
    - BUG/MINOR: h1: report the right error position on authority/host mismatch
    - BUG/MINOR: h2: don't use a block pointer to roll back a partial HTX conversion
    - BUG/MINOR: h3: don't use a block pointer to roll back a partial HTX conversion
    - BUG/MINOR: http-ana: fix a one-byte over-read in the client-side cookie parser
    - CLEANUP: htx: remove the unreachable "append_data" label in htx_reserve_max_data()
    - CLEANUP: flt-comp: remove a no-op http_remove_header() call
    - BUG/MINOR: http-act: fix a double free of the regex on a rule parsing error
    - BUG/MINOR: http-act: fix a double free of the map reference on a parsing error
    - BUG/MINOR: http-act: restore the response buffer state in the early-hint action
    - BUG/MINOR: http-act: work on a copy of the sample in del-headers-bin
    - BUG/MINOR: http-act: reject a negative capture id in the capture actions
    - CLEANUP: http-conv: index the captures array with hdr->index in the converters
    - BUG/MINOR: http-htx: check the strdup() of the "lf-string" http reply argument
    - BUG/MEDIUM: tools: make string encoding possible to fail instead of truncating
    - CLEANUP: http-conv: Remove useless enc_type init to ENC_QUERY
    - CLEANUP: http-conf: rename local trash variable
    - BUG/MINOR: htx: Perform raw copy for messages of same size in htx_copy_msg()
    - BUG/MINOR: htx: Transfer HTX_FL_EOM flag on success in htx_append_msg()
    - BUG/MINOR: http-rules: fix release of a failed "set-cookie-fmt" redirect rule
    - BUG/MINOR: slz: do not read past the end of the input around the match loop
    - CLEANUP: slz: fix the documented worst case size of flush() and finish()
    - BUG/MINOR: slz: use the exact switch cost for the last literals of a block
    - BUG/MEDIUM: slz: bound the bits wasted by the 9-bit literals
    - BUG/MINOR: slz: do not append a block to an already finished stream
    - BUG/MINOR: slz: fix the adler32 accumulators signedness on 32-bit
    - BUG/MINOR: slz: avoid undefined shifts when building the word byte by byte
    - CLEANUP: slz: clarify that the size promise applies to the stream, not to a call
    - BUG/MEDIUM: peers: check the available room before encoding dict values
    - BUG/MEDIUM: sample: reject the deprecated protobuf group wire types
    - BUG/MAJOR: ssl/ocsp: lock the OCSP response around reads in the stapling callback
    - MINOR: server: improve parsing error for server-template
    - BUG/MINOR: server: fix QUIC on server-template
    - BUG/MINOR: server: duplicate server alt_proto in srv_settings_cpy()
    - MINOR: server: ensure check-reuse-pool is init in srv_settings_init()
    - BUG/MINOR: server: fix check reuse-pool in srv_settings_cpy()
    - IMPORT: cebtree: private: fix the duplicate detection in the lookup shortcut
    - BUG/MINOR: cli: use the current argument to parse the FD spec in "show fd"
    - BUG/MINOR: cli: do not reject the "/<fd>" form of "show fd"
    - CLEANUP: haload: embed rate_task into hld_thr_info structure
    - CLEANUP: haload: factor out user scheduling into hld_usr_schedule()
    - BUG/MINOR: haload: fix rate limit bypass during stream errors
    - MEDIUM: fd: Remove fdinfo
    - MEDIUM: fd: Make it possible to have one fdtab per thread-group
    - MEDIUM: pollers: Allow one polled_mask per thread group
    - MEDIUM: pollers: Create the poller pipes before we create the thread
    - MEDIUM: listeners: Don't always balance connections across thread groups
    - MEDIUM: backend: Do not always allow takeover across thread groups
    - MEDIUM: listener: Properly handle unshared fd tables between tgroups
    - MEDIUM: cli: Transfer sockets with unshared file descriptor tables
    - MEDIUM: pollers: Only allow epoll when each tgroup has its fd table
    - MEDIUM: pipes: Have one pool of free pipes per thread group
    - MINOR: cli: Make "show fd" aware of per-thread-group FD tables
    - MINOR: debug: Report the current tgid in "debug dev fd"
    - MINOR: cli: Report the tgid along the FD in "show sess"
    - MEDIUM: dns: Stick the TCP nameserver tasks to the resolvers' thread
    - MEDIUM: server: Do not close other thread groups' connections at deinit
    - MEDIUM: resolvers: Do not close another thread group's socket at deinit
    - MEDIUM: quic: Do not use another thread group's listener FD
    - MINOR: connection: Do not retrieve src/dst on another thread group's FD
    - MEDIUM: fd: Add the tune.fd.tables option
    - BUILD: listener: Fix the build on platforms without MSG_CMSG_CLOEXEC
    - DOC: config: Document the tune.fd.tables option
    - BUG/MINOR: ech: propagate error from load_echkeys()
    - BUG/MINOR: ech: reject an ECH store with no usable private key
    - BUG/MEDIUM: counters: preserve shared.tg pointer on 'clear counters all'
    - MINOR: counters: add max-only reset helpers and use them for clear counters
    - MINOR: server: add 'clear counters server <backend>/<server>' CLI command
    - REGTESTS: stats: add test for 'clear counters server'
    - DOC: management: document 'clear counters server'
    - BUG/MEDIUM: filter: Disable auto-close on channel during TCP payload filtering
    - CLEANUP: haload: drop unused flags field from struct hld_url
    - BUG/MINOR: haload: fix stale global variables affecting URL allocations
    - MINOR: haload: support HTTP status code by version
    - DEV: patchbot: add an "O" filter to hide original lines without new notes
    - DEV: patchbot: retrieve the shared state on page load, with a timeout
    - DEV: patchbot: support passing the page settings in the URL fragment
    - OPTIM: pattern: try literal IPv6 parsing before DNS resolution in pat_parse_ip
    - OPTIM: tools/str2net: only duplicate the string when a slash is present
    - BUILD: tools: fix C23 incompatible strrchr usage
    - CLEANUP: server: remove wrong comments about server-template ID
    - MINOR: errors: further improve parsing error for server-template
    - BUG/MINOR: server: check strdup return value on server ID
    - MINOR: server: do not ignore errors during server-template init
    - BUG/MINOR: server: check strdup return on server-template ID generation
    - MINOR: server: detect name conflict earlier during parsing
    - MINOR: server: treat proxy server tree as without duplicate
    - BUG/MINOR: ech: fix label at end of compound statement
    - MINOR: ech: introduce an ech_store type and helpers in load_echkeys()
    - MEDIUM: ech: implement a lighter ECH feature for AWS-LC
    - CI: github: add USE_ECH=1 in OpenSSL and AWS-LC jobs
    - DOC: stop supporting OpenSSL version < 1.1.1
    - CI: github: remove OpenSSL 1.0.2 job
    - BUG/MINOR: proxy: fix default-server leak on post-parsing cleanup
    - MINOR: proxy: implement unpublished backend keyword
    - MINOR: server: define _srv_parse_from() for server "from" keyword
    - MINOR: server: implement "from none"
    - MINOR: proxy: keep default-server unless empty setting
    - MINOR: proxy: extend global tune.defaults.purge for default-server
    - MEDIUM: server: implement from be:
    - MINOR: server: set default-server id to NULL
    - MEDIUM: proxy: implement named default-server
    - MINOR: server: prevent name collision with a default-server
    - MEDIUM: server: implement "from srv:"
    - CLEANUP: xprt_quic: remove dead callbacks prepare_srv/destroy_srv
    - MINOR: sample: make the param converter support control characters
    - DOC: explain better that named defaults are preserved
    - DOC: better explain that default-server are now preserved
    - BUG/MINOR: proxy: fix "show backend"
    - MINOR: list: define watcher_is_attached()
    - MINOR: proxy: implement "show default-server"
    - MINOR: proxy: complete "add backend" reg-test
    - MINOR: proxy: implement "show defaults"

8 days agoMINOR: proxy: implement "show defaults"
Amaury Denoyelle [Tue, 4 Aug 2026 15:33:24 +0000 (17:33 +0200)] 
MINOR: proxy: implement "show defaults"

Implement a new command "show defaults" whose purpose is to list the
existing named defaults section. This may be needed when adding a new
backend via the CLI.

For now, only the names of the defaults instances are listed. In the
future, it could be useful to have at least some details about their
configuration.

Command does not take any argument and is implemented via a single
io_handler. No need for a watcher as iteration is performed over
defaults instances which cannot be removed at runtime.

8 days agoMINOR: proxy: complete "add backend" reg-test
Amaury Denoyelle [Wed, 5 Aug 2026 08:37:59 +0000 (10:37 +0200)] 
MINOR: proxy: complete "add backend" reg-test

Add new tests for "add backend" command, most notably a check on "mode"
argument in case defaults section does not define an explicit mode.

8 days agoMINOR: proxy: implement "show default-server"
Amaury Denoyelle [Thu, 30 Jul 2026 12:42:43 +0000 (14:42 +0200)] 
MINOR: proxy: implement "show default-server"

Define a new CLI command "show default-server". This lists all the
default-server instances, both unnamed and named. By default, all
backends are displayed. A single instance only can be requested.

Command is protected against runtime backend deletion via a watcher. It
is only attached for a full iteration. If only a single instance is
requested, watcher_is_attached() will detects this and interrupt the for
loop.

8 days agoMINOR: list: define watcher_is_attached()
Amaury Denoyelle [Tue, 4 Aug 2026 14:54:19 +0000 (16:54 +0200)] 
MINOR: list: define watcher_is_attached()

Define an utility function to report if a watcher is currently attached
on a target. This will notably be used to implement a new CLI command
"show default-server".

8 days agoBUG/MINOR: proxy: fix "show backend"
Amaury Denoyelle [Tue, 4 Aug 2026 14:17:39 +0000 (16:17 +0200)] 
BUG/MINOR: proxy: fix "show backend"

During the conversion for the main proxies list, "show backend" was
broken as no entry would be displayed. This patch restores the iteration
over the backends list.

The exact patch which introduces the regression is the following one.

  164d05706132107a405d23663dbca29916910f55
  MINOR: proxy: define proxies_list iteration functions

No need to backport unless the above patch is.

8 days agoDOC: better explain that default-server are now preserved
Amaury Denoyelle [Wed, 5 Aug 2026 09:24:58 +0000 (11:24 +0200)] 
DOC: better explain that default-server are now preserved

Complete default-server documentation by mentionning that default-server
unnamed and named are now preserved after configuration parsing.

This is already mentionned in the related keyword "tune.defaults.purge".
However, as this is a major change, it is necessary to remind it
directly in default-server section.

No need to backport.

8 days agoDOC: explain better that named defaults are preserved
Amaury Denoyelle [Wed, 5 Aug 2026 09:19:33 +0000 (11:19 +0200)] 
DOC: explain better that named defaults are preserved

Named defaults section are now kept after configuration by default. This
behavior change has already been explained in "tune.defaults.purge".

Mention again this change directly in "defaults" documentation. This is
the main entry point documentation for "defaults" sections, so the
change is now more visible to users.

This should be backported up to 3.4.

9 days agoMINOR: sample: make the param converter support control characters
Willy Tarreau [Tue, 4 Aug 2026 09:04:06 +0000 (11:04 +0200)] 
MINOR: sample: make the param converter support control characters

Sometimes it can be convenient to support delimiting of the param()
converter using control characters that can be found in some request
bodies. Since the converter only supports a single character, we can
easily make an exception for "0xHH". That's what this patch does. The
reg-test was updated to include one 0x26.

9 days agoCLEANUP: xprt_quic: remove dead callbacks prepare_srv/destroy_srv
Amaury Denoyelle [Tue, 28 Jul 2026 17:08:39 +0000 (19:08 +0200)] 
CLEANUP: xprt_quic: remove dead callbacks prepare_srv/destroy_srv

QUIC xprt defines prepare_srv callback. The main objective of this
callback is to setup server XPRT to QUIC.

This should be called once the server is fully configured, with related
code blocks in proxy_finalize() and cli_parse_add_server(). However,
this is in fact dead code as prepare_srv is in fact called through
XPRT_SSL. This is still functional though because SSL and QUIC share the
same prepare_srv, so this issue is not visible. This is the same
situation for destroy_srv callback.

This patch removes the dead code to ensure there is no ambiguity here.
There is still a design issue which should be fixed later as it's not
expected for QUIC code to rely on XPRT_SSL layer.

9 days agoMEDIUM: server: implement "from srv:"
Amaury Denoyelle [Thu, 9 Jul 2026 09:41:50 +0000 (11:41 +0200)] 
MEDIUM: server: implement "from srv:"

Implement "srv:" notation for the "from" server keyword. This allows to
specify a server or default-server by its name. It may be optionnaly
prefixed by a backend name using a slash separator. If this is not the
case, lookup is performed under the backend where the current newly
created server instance is attached.

This is implemented by extending _srv_parse_from() to support "from
srv:" argument value. An internal function lookup_srv_be_arg() is
defined to parse the argument of the form "[<be>/]<srv>". This is
similar to already existing lookup functions such as cli_find_server(),
however there is some differences which forces to have duplicated code
for the moment.

9 days agoMINOR: server: prevent name collision with a default-server
Amaury Denoyelle [Fri, 31 Jul 2026 14:23:08 +0000 (16:23 +0200)] 
MINOR: server: prevent name collision with a default-server

The previous patch has introduced the support for named default-server.
It is not possible though to declare a default-server with a name if it
collides with an already existing server instance.

This patch implements a similar check but on the other side : it ensures
that a newly created server instance does not collide with an already
existing default-server.

To implement this, a new function server_find_by_name2() is defined. It
is similar to server_find_by_name() except it also lookup in the named
default-server tree. Checks are performed on several places :
* _srv_parse_init() for a server parsing
* _srv_parse_tmpl_init() for a server-template parsing
* srv_update_server_name() for the CLI command "set server name"
* cli_parse_add_server() for the CLI command "add server"

9 days agoMEDIUM: proxy: implement named default-server
Amaury Denoyelle [Thu, 9 Jul 2026 08:41:59 +0000 (10:41 +0200)] 
MEDIUM: proxy: implement named default-server

Implement the support for named default-server. In a single backend, it
is now possible to define different default-server instances identified
by a name. Along to them, it's still possible to use the anonymous
default-server.

To define a named default-server, a new "name" argument can be used just
after the "default-server" token. It is positional to ensure extra
server settings are always set after it. This is necessary to continue
to allow to define a default-server on multiple lines, concatenating the
settings with the previous line.

Named default-server are stored in a new dedicated compact tree
<defsrv_by_name> in proxy struct.

Name must not conflict with server instances already defined in the same
backend. This is necessary for the future implementation of name
addressing on both servers and default-servers without having to specify
the lookup list.

9 days agoMINOR: server: set default-server id to NULL
Amaury Denoyelle [Thu, 9 Jul 2026 10:00:07 +0000 (12:00 +0200)] 
MINOR: server: set default-server id to NULL

Previously, default-server <id> was set to "default-server" static
string. With this patch, it is now set to NULL.

This has no noticeable impact, including on the configuration error
messages, as default-server ID is not used there. This is because
register_parsing_obj() is only used for standard server and
server-template instances, but not for default-server.

A nice side-effect of this change is that it's possible to free server
<id> member in srv_free_params(), which is less error prone (even more
with named default server future implementation).

This is a prealable for the future named default-server feature, as it
could conflict with a default-server explicitely named "default-server".
Also, for named default-server, it will be necessary to free <id>
member. Thus it's better to remove a static string reference to prevent
any issue with free() usage.

9 days agoMEDIUM: server: implement from be:
Amaury Denoyelle [Thu, 30 Jul 2026 13:25:52 +0000 (15:25 +0200)] 
MEDIUM: server: implement from be:

Implement "from be:" syntax. This instructs that the server should reuse
parameters from the default-server already defined in the same proxy
instance.

This behavior is already the current one in the configuration parser,
thus there is no visible change here. The main usage is on "add server"
command, which previously always ignored a default-server instance.

This is implemented by extending _srv_parse_from() to parse "be:"
argument value. If found, the designated server is returned via <from>
output parameter.

9 days agoMINOR: proxy: extend global tune.defaults.purge for default-server
Amaury Denoyelle [Wed, 8 Jul 2026 08:30:51 +0000 (10:30 +0200)] 
MINOR: proxy: extend global tune.defaults.purge for default-server

Previous patch changes behavior for default-server. They are now
preserved after configuration during the whole process lifetime as
dynamic servers may need them. A default-server is still purge though if
it does not contain any particular setting.

This relation between dynamic and default-server is similar to the one
between dynamic backends and named default proxies sections, which are
also kept by default at runtime.

This patch extends tune.defaults.purge keyword which was previously used
to force cleanup of named defaults section on post parsing. This now
support an extra argument to instruct the type of elements to remove :
supported values are "proxies" for named defaults sections and "servers"
for default-server.

Without any argument, the option only forces clean up of named defaults
sections. This is the simplest method to preserve backward compatibility
with previous releases.

This new setting requires a dedicated code block for default-server
purgeing in check_config_validity(). This cannot be performed during
post-section parsing as this is a global setting which can be defined
later.

9 days agoMINOR: proxy: keep default-server unless empty setting
Amaury Denoyelle [Fri, 17 Jul 2026 08:19:36 +0000 (10:19 +0200)] 
MINOR: proxy: keep default-server unless empty setting

Previously, default-server were removed as soon as a proxy section
parsing was over. This patch changes this behavior to now keep
default-server during haproxy runtime. This will allow to reuse them for
dynamic servers, which will be later implemented via "from" keyword as a
distinct feature.

Default-server may consume a noticeable amount of memory, so it can
still be desirable to clean up these elements on post-parsing. Thus, if
a default-server is reset, it will still be purged on section post
parsing. This can be achived by using a "default-server from none" final
line.

To detect if a default-server should be preserved, a new flag
SRV_F_UMODIFIED has been defined. Such flag is set as soon as a server
keyword has been parsed. It is reset when srv_settings_init() is used.
Thus, if this flag is present, default-server purge on post parsing is
skipped.

9 days agoMINOR: server: implement "from none"
Amaury Denoyelle [Wed, 29 Jul 2026 13:42:16 +0000 (15:42 +0200)] 
MINOR: server: implement "from none"

Implement "from none" on a server line. This instructs to not preset
server settings via another instance, instead relying on documented
default values.

This is useful to change the default behavior for static servers to
prevent them from reusing a default-server instance. A secondary usage
of this value is on a default-server. This reset the default-server to
the documented default settings.

This is implemented by extending _srv_parse_from() to parse "none"
value. In this case, <from> output return is set to NULL, which will
cause the server to be initialized via srv_settings_init().

9 days agoMINOR: server: define _srv_parse_from() for server "from" keyword
Amaury Denoyelle [Wed, 8 Jul 2026 08:05:21 +0000 (10:05 +0200)] 
MINOR: server: define _srv_parse_from() for server "from" keyword

Prepare the support for a new server keyword "from".

This keyword has special constraints : it is a positional one as it can
only be specified once, after the server address and before the other
parameters.

The purpose of this keyword will be to define server settings
inheritance outside of the default-server of the current backend. It
will also be useful for dynamic servers which currently do not inherit
from a default-server.

9 days agoMINOR: proxy: implement unpublished backend keyword
Amaury Denoyelle [Mon, 6 Jul 2026 13:51:41 +0000 (15:51 +0200)] 
MINOR: proxy: implement unpublished backend keyword

Add a new "unpublished" proxy keyword. This allows to start a backend
instance in unpublished state. This can be reverted via "publish
backend" on the CLI.

This keyword is restricted to backend and listen sections.

This patch is a simple feature implementation, however it can be
considered as a must-have when using dynamic backends. As such, it
should be backported up to 3.4.

9 days agoBUG/MINOR: proxy: fix default-server leak on post-parsing cleanup
Amaury Denoyelle [Fri, 17 Jul 2026 14:12:42 +0000 (16:12 +0200)] 
BUG/MINOR: proxy: fix default-server leak on post-parsing cleanup

A proxy with BE capabilities may define a default-server instance. This
instance is freed when config parser switch to another section. Beside
the server object, <conf.file> is also freed.

This causes a memleak if the default-server configuration is expanded
with keywords triggering dynamic allocations. For example, this is the
case if a cookie name is defined.

To fix this, use srv_free_params() on default-server deletion. This
function is designed to work both for server and default-server. It
ensures that every dynamic elements in it are freed. Only <id> member is
not freed by srv_free_params() : this is expected as it is a static
value for default-server instances.

Note that srv_free_params() is already used when default-server is freed
in deinit_proxy() since the following patch. However, post-parsing
cleanup has been added after it without reusing srv_free_params().

  899b547840c340cd129c3831d3858d8d2e5b452a
  BUG/MINOR: proxy/server: free default-server on deinit

This should be backported up to 2.8. Prior to it, srv_free_params() does
not exists, so it cannot be picked as is.

9 days agoCI: github: remove OpenSSL 1.0.2 job
William Lallemand [Mon, 3 Aug 2026 12:40:16 +0000 (14:40 +0200)] 
CI: github: remove OpenSSL 1.0.2 job

We don't support anymore versions this old, minimum version supported is
1.1.1.

9 days agoDOC: stop supporting OpenSSL version < 1.1.1
William Lallemand [Mon, 3 Aug 2026 12:38:34 +0000 (14:38 +0200)] 
DOC: stop supporting OpenSSL version < 1.1.1

Remove versions of OpenSSL before 1.1.1 from the documentation, 1.1.1 is
the minimal requirement.

9 days agoCI: github: add USE_ECH=1 in OpenSSL and AWS-LC jobs
William Lallemand [Mon, 3 Aug 2026 10:01:22 +0000 (12:01 +0200)] 
CI: github: add USE_ECH=1 in OpenSSL and AWS-LC jobs

Add USE_ECH=1 in standard jobs when supported.

AWS-LC and OpenSSL > 4.0 supports ECH.

Remove the specific openssl-ech job.

9 days agoMEDIUM: ech: implement a lighter ECH feature for AWS-LC
William Lallemand [Fri, 31 Jul 2026 15:59:46 +0000 (15:59 +0000)] 
MEDIUM: ech: implement a lighter ECH feature for AWS-LC

This patch implements ECH with AWS-LC. AWS-LC supports a different ECH
API than OpenSSL 4.0.

AWS-LC does not implement an API to load a PEM ECH file, the ECHCONFIG
section is parsed manually using PEM_read_bio() to feed the SSL_ECH_KEYS
object.

Runtime ECH store management ('show/add/set/del ssl ech') and ECH
status/outer-SNI reporting  are disabled under AWS-LC for now.

Should fix issue #3333.

9 days agoMINOR: ech: introduce an ech_store type and helpers in load_echkeys()
William Lallemand [Fri, 31 Jul 2026 15:26:21 +0000 (15:26 +0000)] 
MINOR: ech: introduce an ech_store type and helpers in load_echkeys()

Alias OSSL_ECHSTORE as ech_store, and wrap OSSL_ECHSTORE_new(),
OSSL_ECHSTORE_free(), SSL_CTX_set1_echstore() and the per-file PEM
loading loop behind ech_store_new(), ech_store_free(),
ech_store_set_ctx() and ech_store_load_file(). Use them in
load_echkeys() instead of the OpenSSL calls directly.

ech_store_load_file() no longer reads strerror(errno) to explain a
BIO_new_file() failure: that function records the fopen() failure
reason in the crypto library's error queue, not in errno.
load_echkeys() already drains that queue into *err on any failure, so
just keeping the message filename-only here is enough to get the real
reason appended.

This is pure preparation with no functional change otherwise: a
following commit will give ech_store and these four helpers an
AWS-LC-specific body, so that load_echkeys() can be shared between the
two SSL libraries.