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

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

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

This should be backported up to 2.8.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two separate gaps caused this:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This is libslz upstream commit 4adc28e64716f3db5df9a1edc6eeb9234a37f6ad

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

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

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

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

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

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

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

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

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

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

This should be backported to 3.3.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This may be backported as far as 2.4.

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

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

This should be backported wherever the converter above is backported.

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

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

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

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

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

This must be backported to 3.2.

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

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

Must be backported to 3.4.

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

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

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

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

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

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

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

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

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

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

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

This is what commit c5f41118c01db4babcf6919f5eae4ce308adcfef should have
been.

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

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

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

Must be backported to every stable branch.

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

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

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

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

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

Must be backported in every stable branches.

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

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

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

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

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

This must be backported in every stable release.

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

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

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

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

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

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

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

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

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

Must be backported in every stable branches.

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

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

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

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

Must be backported in every stable branches.

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

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

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

This patch should be backported up to 3.0.

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

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

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

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

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

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

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

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

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

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

This patch can be backported to all stable branches.

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

Add a TARGET=generic job which compiles with openssl.

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

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

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

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

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

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

This can be backported wherever it applies.

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

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

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

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

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

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

This fix should be backported to versions 2.6 and upper.

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

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

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

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

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

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

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

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

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

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

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

Thank you to @OneZ3r0 for having reported this issue.

Should be backported to all versions.

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

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

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

This should be backported to all stable branches.

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

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

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

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

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

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

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

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

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

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

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

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

This must be backported to all supported versions.

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

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

Thanks to this, backtraces are clean again:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The code size reduced by 6.5 kB.

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

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

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

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

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

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

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

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

2 months agoMINOR: debug: make the complain() function start to complete the message
Willy Tarreau [Mon, 10 Aug 2026 16:28:38 +0000 (18:28 +0200)]
MINOR: debug: make the complain() function start to complete the message

The function will be extended to automatically prepend/append some info.
For now all it does is to always prepend/append the "\n" that each message
has at the beggining and at the end.

We're doing this into a locally allocated iovec and emit it at once using
writev(). This will be easy to extend to pass extra info. The \n alone
removed 1.5 kB of code.

2 months agoCLEANUP: debug: remove the now unused counter argument to complain()
Willy Tarreau [Mon, 10 Aug 2026 15:32:57 +0000 (17:32 +0200)]
CLEANUP: debug: remove the now unused counter argument to complain()

Let's drop it before someone has the idea to use it again.

We take this opportunity for swapping complain()'s arguments which were
really poorly ordered since we're really passing a context first, so the
calls read better this way, particularly since we plan to add more.

This drops another 4 kB of code due to the number of calls.

2 months agoMINOR: debug: only emit and count warnings when explicitly requested
Willy Tarreau [Mon, 10 Aug 2026 15:01:13 +0000 (17:01 +0200)]
MINOR: debug: only emit and count warnings when explicitly requested

Previously complain() would set TAINTED_WARN was set even for a
check_if() because there was no way to distinguish a warn from a check
and a warn was systematically accounted for when the type was not a
bug. Likewise, BUG_ON() would dump a trace whenever a non-fatal event
happened, which is not the goal either as it prevents from counting
events without logging them (e.g. COUNT_IF). Now that we have explicit
flags for each type, let's consider them when choosing what to emit.

Note that for now complain() will still log the "msg" line if used
without FATL nor WARN though.

2 months agoMINOR: debug: use bit fields for the debug type and fatality
Willy Tarreau [Mon, 10 Aug 2026 14:16:34 +0000 (16:16 +0200)]
MINOR: debug: use bit fields for the debug type and fatality

Till now the BUG_ON() series of macros has been relying on magic values
0,1,2, and 3 passed down the chain to decide whether to log a bug or a
warning, and whether to crash or just warn. But this doesn't capture the
whole usage and is causing difficulties because we also have type "check",
and these values are passed to variables historically named "crash" or
"taint" suggesting booleans while bit-exact checks have to be done.

Let's replace them with a combination that separately indicates:
  - the type (check, warning, bug, abort), one bit per type, expected
    to be exclusive
  - type fatality (warning or fatal), one bit per level, must be
    exclusive as well

This allows us to easily replace the checks and map the numbers to their
equivalent definitions. The macros are called DBG_DET_* (debug details),
and the "crash" and "taint" fields have been renamed "details" as well.

For now this is the exact equivalent of what we previously had.

2 months agoMINOR: debug: move printing of the hint to ha_backtrace_to_stderr()
Willy Tarreau [Wed, 5 Aug 2026 14:22:40 +0000 (16:22 +0200)]
MINOR: debug: move printing of the hint to ha_backtrace_to_stderr()

The hint suggesting the user to contact developers with a core is only
emitted along with a backtrace sent to stderr, and it's currently
present as a macro in many call places. Let's move it to the function
and pass an argument to ask for it to be emitted (i.e. only upon crash
since a warning doesn't produce a core).

This reduces the binary size by 38 kB.

2 months agoDEBUG: deduplicate __ABORT_NOW() between DEBUG_USE_ABORT and ha_crash_now()
Willy Tarreau [Mon, 10 Aug 2026 14:31:14 +0000 (16:31 +0200)]
DEBUG: deduplicate __ABORT_NOW() between DEBUG_USE_ABORT and ha_crash_now()

The __ABORT_NOW() macro is defined for the two modes with a single,
insignificant difference, which is how to crash. Given that there's
already an #ifdef for the abort case, better make that macro point
to ha_crash_now() for the default case and eliminate one of the
definitions. This code is sufficiently difficult to read to avoid this.

2 months agoDEBUG: cli: add a "debug dev abort" debugging command
Willy Tarreau [Mon, 10 Aug 2026 14:29:44 +0000 (16:29 +0200)]
DEBUG: cli: add a "debug dev abort" debugging command

We had "debug dev bug/check/warn" to test the various BUG_ON() macros
but we didn't have a way to test ABORT_NOW(). So let's introduce
"debug dev abort" for this.

2 months agoMINOR: debug: check the match count in __BUG_ON_ONCE() and not in complain()
Willy Tarreau [Mon, 10 Aug 2026 14:50:39 +0000 (16:50 +0200)]
MINOR: debug: check the match count in __BUG_ON_ONCE() and not in complain()

The fix in commit 7a78d6c600 ("BUG/MINOR: debug: only dump the trace once
in __BUG_ON_ONCE()") warned that it's not strictly atomic, but we could
do better and take this opportunity for cleaning the code: use a fetch-add
in __BUG_ON_ONCE() and pass NULL to complain(), which thus no longer has
any call place requiring it to check a counter. This only very slightly
inflates the code (~138B) since each BUG_ON_ONCE() now has to load 1,
xadd(), check the return value instead of leaving it to complain(), but
this is totally marginal compared to the benefits.

This can even be backported where the fix above is backported if needed.

2 months agoCLEANUP: debug: reorder definitions in bug.h
Willy Tarreau [Wed, 12 Aug 2026 14:04:43 +0000 (16:04 +0200)]
CLEANUP: debug: reorder definitions in bug.h

This file has become totally unreadable because it's littered with lots
of #if, and mixes unconditional and conditional declarations that have
been accumulating over time. Let's first reorganize it so that:

  - externs are defined first
  - isolated macros are defined second
  - enums/structs are defined third, optionally with their macros and
    inline functions if unconditional.
  - then the conditional stuff.

A few types were made permanent as there's no point hiding them behind
ifdefs, except complicating a developer's life in another file.

Now most common stuff is quickly found and the more involved stuff remains
at the end.

2 months agoCLEANUP: debug: move ha_backtrace_to_stderr() declaration to bug.h
Willy Tarreau [Tue, 11 Aug 2026 06:05:35 +0000 (08:05 +0200)]
CLEANUP: debug: move ha_backtrace_to_stderr() declaration to bug.h

The DUMP_TRACE() macro was made just to manually reference the external
ha_backtrace_to_stderr() function that is defined in debug.h, but this
makes the code even harder to follow. In fact, the current rule that
bug.h contains all mandatory declarations and that debug.h contains the
optional ones is not respected here, so let's just move the declaration
there and get rid of the macro.

2 months agoDEBUG: cli: unstatify cli_process_cmdline()
Willy Tarreau [Tue, 11 Aug 2026 13:26:52 +0000 (15:26 +0200)]
DEBUG: cli: unstatify cli_process_cmdline()

There's no point keeping this function static, and because of this it's
never resolved in backtraces despite appearing quite often since it's
located between the io_handler and various parse functions that may
possiblly trigger issues. So let's remove the static modifier on it.

2 months agoREGTESTS: checks: skip tcp-check-client-hello.vtc under FIPS mode
William Lallemand [Thu, 13 Aug 2026 14:27:35 +0000 (14:27 +0000)]
REGTESTS: checks: skip tcp-check-client-hello.vtc under FIPS mode

This test hangs and gets killed after a 10s timeout when run against
an SSL library running in FIPS mode (e.g. AWS-LC-FIPS): backend be1's
check server line uses "curves X25519" to exercise X25519 key-share
routing (group 0x001d), which is rejected at config-parse time by
HAProxy's own FIPS compliance checks (src/fips.c) since X25519 is not
a NIST P-curve. The "h1" haproxy process then fails to start, and
vtest waits on it until its startup timeout kills it with SIGKILL
instead of reporting a clean startup failure.

Skip the test using the fips_mode() config condition predicate.

2 months agoREGTESTS: ssl: skip tests broken by FIPS mode
William Lallemand [Thu, 13 Aug 2026 14:07:14 +0000 (14:07 +0000)]
REGTESTS: ssl: skip tests broken by FIPS mode

Both reg-tests fail when run against an SSL library running in FIPS
mode (e.g. AWS-LC-FIPS), for two distinct reasons:

- tls12_ssl_crt-list_filters.vtc configures the "kRSA" (non-ECDHE)
  TLSv1.2 cipher suite, which is rejected at config-parse time by
  HAProxy's own FIPS compliance checks (src/fips.c).

- ssl_generate_certificate.vtc's P-384 check observes the default
  curve/group (e.g. P-256) instead of the configured secp384r1: the
  "ecdhe" bind keyword does not appear to restrict the negotiated
  curve/group in this mode, reproduced independently of the TLS
  version forced by the client.

Skip both using the newly introduced fips_mode() config condition
predicate rather than disabling them outright, so they keep running
everywhere else.

2 months agoMINOR: ssl: report FIPS mode in -vv for OpenSSL >= 3.0 too
William Lallemand [Thu, 13 Aug 2026 13:51:11 +0000 (13:51 +0000)]
MINOR: ssl: report FIPS mode in -vv for OpenSSL >= 3.0 too

The "SSL library FIPS mode" line in "haproxy -vv" was only computed
for SSL libraries implementing the legacy FIPS_mode() API (OpenSSL
1.0.x/1.1.x and compatible libraries such as AWS-LC), and silently
omitted for OpenSSL 3.0 and above.

Use the openssl_fips_mode() helper introduced for the fips_mode()
config condition predicate instead of calling FIPS_mode() directly:
it also covers OpenSSL >= 3.0 via
EVP_default_properties_is_fips_enabled(), and reports "no" rather
than omitting the line entirely for any SSL library supporting
neither API. The line is now unconditionally printed.

2 months agoMEDIUM: ssl: add fips_mode() config condition predicate
William Lallemand [Thu, 13 Aug 2026 09:28:05 +0000 (09:28 +0000)]
MEDIUM: ssl: add fips_mode() config condition predicate

Add a new "fips_mode()" predicate usable in .if/.elif configuration
conditional blocks and with the "-cc" command line option. It
evaluates to true when the loaded SSL library is currently running in
FIPS mode.

The check relies on a new openssl_fips_mode() helper in tools.c,
following the same pattern as openssl_compare_current_version() and
awslc_compare_current_api(). Two APIs are used depending on the SSL
library:

  - FIPS_mode(), implemented by OpenSSL 1.0.x/1.1.x (including
    FIPS-validated builds) and by compatible libraries such as AWS-LC.

  - EVP_default_properties_is_fips_enabled(), for OpenSSL 3.0 and
    above, where FIPS_mode() was removed in favor of a provider-based
    FIPS model. It reports whether the default library context
    currently resolves algorithm fetches to the FIPS provider, which
    is the closest 3.x equivalent.

The predicate is a no-op (always false) with any other SSL library, or
when built without SSL support.

This lets configurations, and reg-tests in particular, detect and
adapt to (or skip) TLS constructs that are rejected by HAProxy's own
FIPS compliance checks (src/fips.c) when FIPS mode is active, e.g.
non-ECDHE TLS 1.2 cipher suites.

2 months agoBUG/MEDIUM: bwlim: fix a stick-table entry leak in shared mode
Olivier Houchard [Thu, 13 Aug 2026 13:57:56 +0000 (15:57 +0200)]
BUG/MEDIUM: bwlim: fix a stick-table entry leak in shared mode

In shared mode, bwlim_set_limit() stores the stick-table entry returned
by stktable_get_entry(), which takes a reference, into st->ts without
releasing any entry already stored there. When set-bandwidth-limit is
executed more than once for the same stream and shared filter, the
intermediate reference is leaked: bwlim_detach() only releases the last
one. The entry then stays pinned (ref_cnt > 0) and can never be purged,
and with distinct keys the table fills up until the features depending
on it are denied service.

Release the previously held entry before overwriting st->ts.

This was introduced in 2.7 by commit 2b6777021 ("MEDIUM: bwlim: Add
support of bandwith limitation at the stream level") and must be
backported to all stable branches.

Many thanks to Red Hat and AISLE Research for reporting the issue and
providing a fix.

2 months agoMINOR: sample: add new converter has_ctl() to detect control characters
Willy Tarreau [Thu, 13 Aug 2026 13:03:26 +0000 (15:03 +0200)]
MINOR: sample: add new converter has_ctl() to detect control characters

Control characters, as defined by RFC5234, are 0x00 to 0x1F and 0x7F.
They're not easy to insert in a config by definition and difficult to
match. Let's add a sample converter which takes a binary input and
returns a boolean indicating if any such character is found within a
possibly configurable class.

By default with no argument, it checks the range above except TAB (0x9)
which is common. With argument "any", it checks them all. With argument
"http", it only checks the strictly forbidden HTTP ones (CR, LF, NUL)
in headers. Otherwise it takes a mask made of the bits corresponding
to each character, with bit 32 corresponding to character 0x7F.

It can be used to detect anomalies, e.g. by logging or dropping when
any such character is found.

2 months agoDOC: security: clarify that only up-to-date versions may get securty reports
Willy Tarreau [Thu, 13 Aug 2026 09:49:26 +0000 (11:49 +0200)]
DOC: security: clarify that only up-to-date versions may get securty reports

With the increasing rate of low-effort, AI-driven security reports,
we're seeing an increasing level of noise. Let's first clarify what's
usually obvious to developers but not necessarily to bug reporters,
which is that vulnerability reports must exclusively apply to latest
version of a branch. The goal here is to reduce the amount of time
wasted analyzing an issue to finally respond "already fixed 3 months
ago".

Let's also add the link to the bugs page to ease extra checks on the
reporter's side when they cannot re-run the scan (which usually is
the case when time slots are granted on software scanners and they're
run against an outdated version).

This must be backported where the security doc is already present.

2 months agoBUG/MEDIUM: http: fix authority parsing for absolute-form URI with empty path
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>
3 months agoBUG/MINOR: server: fix off-by-one error when parsing and copying source port range
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.

3 months 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.

3 months 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.

3 months 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.

3 months 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)
3 months 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.

3 months 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>