]> git.kaiwu.me - haproxy.git/log
haproxy.git
8 days agoCLEANUP: quic: update the outdated comment of quic_tls_derive_keys()
Frederic Lecaille [Thu, 29 Oct 2026 14:38:16 +0000 (15:38 +0100)]
CLEANUP: quic: update the outdated comment of quic_tls_derive_keys()

The comment still refers to a <ctx> parameter which no longer exists and
claims that the derived key, IV and header protection key have the same
size, which the code below contradicts. Describe the actual derivation
from RFC 9001 5.1 instead.

8 days agoCLEANUP: quic: remove the unneeded quic_rx.h include from quic_tls.c
Frederic Lecaille [Thu, 29 Oct 2026 14:33:23 +0000 (15:33 +0100)]
CLEANUP: quic: remove the unneeded quic_rx.h include from quic_tls.c

None of the declarations of haproxy/quic_rx.h is used by quic_tls.c.
Let's remove the include.

8 days agoCLEANUP: quic: remove duplicated quic_cstream_new() declaration
Frederic Lecaille [Thu, 29 Oct 2026 14:26:55 +0000 (15:26 +0100)]
CLEANUP: quic: remove duplicated quic_cstream_new() declaration

The function prototype is declared twice in include/haproxy/quic_conn.h.
Let's remove the second one.

8 days agoCLEANUP: quic: remove the stale initial_salt declaration
Frederic Lecaille [Thu, 29 Oct 2026 14:23:12 +0000 (15:23 +0100)]
CLEANUP: quic: remove the stale initial_salt declaration

No symbol named initial_salt exists anymore: the Initial salts are the
per-version initial_salt_draft_29, initial_salt_v1 and initial_salt_v2,
reached through the initial_salt member of struct quic_version. Let's
remove the leftover declaration.

8 days agoCLEANUP: quic: remove the unused count and flags from struct quic_tls_kp
Frederic Lecaille [Thu, 29 Oct 2026 14:20:21 +0000 (15:20 +0100)]
CLEANUP: quic: remove the unused count and flags from struct quic_tls_kp

The count and flags members of struct quic_tls_kp are only initialized by
quic_tls_kp_init() and read nowhere: the key phase state is kept in the pn
member of the TLS secrets and in the flags member of the TLS context.
Let's remove them.

8 days agoCLEANUP: quic: remove the unused key parameter from quic_tls_decrypt()
Frederic Lecaille [Thu, 29 Oct 2026 14:08:28 +0000 (15:08 +0100)]
CLEANUP: quic: remove the unused key parameter from quic_tls_decrypt()

The key parameter of quic_tls_decrypt() and quic_tls_decrypt2() is never
read: the key is installed in the cipher context by
quic_tls_rx_ctx_init(), only the IV differs per packet. Dead since the
cipher contexts became reusable (commit f4605748f).

8 days agoBUG/MINOR: quic: fix the header protection key length dump with AWS-LC
Frederic Lecaille [Thu, 29 Oct 2026 13:49:16 +0000 (14:49 +0100)]
BUG/MINOR: quic: fix the header protection key length dump with AWS-LC

Under QUIC_AEAD_API, tls_hp() returns EVP_CIPHER_CHACHA20, which is an
EVP_AEAD cast to EVP_CIPHER. Applied to it, EVP_CIPHER_key_length() reads
an unrelated field and returns 1 instead of 32, so
quic_tls_keys_hexdump() only dumps one byte of the header protection key.
quic_tls_derive_keys() has the same computation but is guarded by
 #ifdef QUIC_AEAD_API. Move that special case to a helper in quic_tls.h
and use it for both.

This only affects the trace output. Measured on AWS-LC 5.9.0: 1 instead
of 32 for ChaCha20, AES-128 and AES-256 unaffected.

Should be backported to all stable versions even if only the QUIC traces
are impacted.

8 days agoCLEANUP: quic: remove unused tx crypto size and offset counters
Frederic Lecaille [Thu, 29 Oct 2026 13:28:28 +0000 (14:28 +0100)]
CLEANUP: quic: remove unused tx crypto size and offset counters

The sz and offset members of the tx crypto substructure of
quic_enc_level are written but never read: the buffered CRYPTO data are
located with the bufs array and the sz member of each quic_crypto_buf.
Let's remove them.

8 days agoCLEANUP: quic: remove the unused tx part of struct quic_cstream
Frederic Lecaille [Thu, 29 Oct 2026 13:14:16 +0000 (14:14 +0100)]
CLEANUP: quic: remove the unused tx part of struct quic_cstream

struct quic_cstream carries a tx substructure with offset, sent_offset
and buf members, inherited from the qcs object it was derived from to
handle CRYPTO streams. Nothing ever reads them: the TX side of the
CRYPTO stream is handled by the tx crypto buffers of its encryption
level for the data, and by the stream descriptor attached to the
cstream. Only quic_cstream_new() sets the three members and no reader
exists anywhere in src/ or include/.

They were already unread when they were introduced (commit 7e3f7c47e
"MINOR: quic: New quic_cstream object implementation"), so let's remove
them along with their initialization.

8 days agoCLEANUP: quic: remove duplicated rx offset initialization in quic_cstream_new()
Frederic Lecaille [Thu, 29 Oct 2026 10:51:33 +0000 (11:51 +0100)]
CLEANUP: quic: remove duplicated rx offset initialization in quic_cstream_new()

<cs->rx.offset> is set to 0 twice in a row. Let's drop the second useless
assignment.

8 days agoMINOR: quic: use the right trace event in quic_tls_key_update()
Frederic Lecaille [Thu, 29 Oct 2026 10:47:16 +0000 (11:47 +0100)]
MINOR: quic: use the right trace event in quic_tls_key_update()

The next RX/TX secret lengths are reported with QUIC_EV_CONN_SPPKTS,
whose handler expects a <struct quic_tx_packet *>, while the rest of the
function uses QUIC_EV_CONN_KP event. TRACE_PRINTF() bypasses the source
callback through <&trace_no_cb>, so nothing crashed: the message was
just filtered under the wrong event.

8 days agoCLEANUP: quic: remove duplicated key phase pointer assignments
Frederic Lecaille [Thu, 29 Oct 2026 10:44:51 +0000 (11:44 +0100)]
CLEANUP: quic: remove duplicated key phase pointer assignments

<nxt_rx> and <nxt_tx> are already initialized from <qc->ku> at
declaration time in <quic_tls_key_update()>, and <qc->ku> is not
modified in between, so the two assignments placed after TRACE_ENTER()
are dead duplicates.

8 days agoBUG/MEDIUM: leastconn: reset the smallest key when backups take over
Aleksandar Lazic [Mon, 2 Nov 2026 17:00:00 +0000 (18:00 +0100)]
BUG/MEDIUM: leastconn: reset the smallest key when backups take over

Since commit 9fe72bba3 ("MAJOR: leastconn; Revamp the way servers are
ordered."), the lookup starts at the smallest key recorded in
lbprm.lb_seq, and this key is shared by the active and the backup
trees. Under load it follows the keys of the active servers.

When the last active server goes down or into maintenance, the key is
only reset if it equals the key of that server, which is often not the
case under load. With "option allbackups" the backup tree is then
looked up from that key, and the idle backup servers, whose key is 0,
are not found. fwlc_get_next_server() returns NULL, the streams are
queued in the backend, and without maxconn nothing dequeues them: every
request ends with a 503 after "timeout queue", although a backup server
is up. It only recovers when a server is queued at a lower key again.
Without "option allbackups" the first backup server is used directly
and the problem does not happen.

Fix it by resetting the smallest key when the last active server leaves
the tree.

This fixes GitHub issue #3555. It can be reproduced with an active and
a backup server, "balance leastconn" and "option allbackups", by
setting the active server to maintenance under load.

This should be backported as far as 3.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
8 days agoMINOR: http-act: Add 'no-fail' option on 'wait-for-body' action
Christopher Faulet [Mon, 2 Nov 2026 20:20:02 +0000 (21:20 +0100)]
MINOR: http-act: Add 'no-fail' option on 'wait-for-body' action

With this option, the "wait-for-body" action does not return an error at the
end of the timeout but continues the message analysis with the body received
so far, instead of responding with a 408 "Request Timeout" error to the
client on the request side or a 504 "Gateway Timeout" error on the response
side. The remaining body bytes, if any, are streamed to the server or to the
client once received.

8 days agoDOC: configuration: Clarify wait-for-body doc for the response side
Christopher Faulet [Mon, 2 Nov 2026 20:24:31 +0000 (21:24 +0100)]
DOC: configuration: Clarify wait-for-body doc for the response side

The documentation was too request-oriented and did not mention the 504
"Gateway Timeout" error returned to the client when the timeout occurs while
waiting for a response body. It is now side-neutral and explicitly mentions
the 408 and the 504 errors returned on timeout, depending on the side.

9 days agoBUG/MINOR: http-ana: fix response compression for applets
Willy Tarreau [Mon, 2 Nov 2026 16:01:34 +0000 (17:01 +0100)]
BUG/MINOR: http-ana: fix response compression for applets

In 3.5-dev6, commit 1a99a7c0f4 ("MINOR: flt-http-comp: Check request
headers for response compression earlier") was needed to make sure
that processing of the accept-encoding request header happened in the
suitable order when both compression offload and decompression are
used at the same time. While theoretically it's no longer needed now
with the updated filter sequences merged into -dev9, it still uncovered
an interesting vestigal construct that we could carbon-14 date to 2014
in 1.5-dev24, precisely with this commit: 70730dddbd ("MEDIUM: http:
enable analysers to have keep-alive on stats").

By then, applets were not yet autonomous objects outside of the stream,
but were entirely part of the stream and were not much compatible with
analyzer processing, with a few exceptions, and the stream (by then
called a session) was reset to a pre-parsing state after each response
to process the next one, resulting in great difficulties explaining
why only select analyzers were kept and the other ones dropped.

In 3.5 all of this is old history and it's a bit surprising and even
refreshing to find living witnesses of this antique era, having managed
to both adapt to and resist natural evolution.

What happened with the 3.5-dev6 patch above is that function
select_compression_request_header() is no longer called by the
compression code during requests, and is expected to be called by the
rest of the natural request processing flow. But the antique code that
only keeps a subset of analyzers didn't care about ANA_REQ_HTTP_INNER
by then since applets didn't need that, and since it's the only place
which can still make sure that this function is called, the result
is that the accept-encoding header is no longer inspected for applets,
because either it was a frontend and the flag was already missing, or
it was a backend and that flag which was set for the backend was reset
by that code. Because of this, compression of applet responses (stats,
cache, promex, Lua services) is silently disabled since 3.5-dev6.

Upon careful inspecting, it turns out that all flags that were added
by the antique code (AN_REQ_FLT_HTTP_HDRS, AN_REQ_HTTP_XFER_BODY etc)
are all properly set by http_process_common() which is called on
AN_REQ_HTTP_INNER. Thus the fix consists in removing all that old
code to stop purging flags, and only enabling AN_REQ_HTTP_INNER when
applets are branched from frontends, so that the flag is always set,
and that the rest of the processing is properly performed in both
cases.

With this patch, sending curl --compressed to a config with only
a frontend or only a backend containing "stats uri /" properly
returns compressed contents again.

No backport is needed since it only affects 3.5-dev6 (3.4 and older
still work fine thanks to the code that the patch above dropped).

Incidentally, regtest legacy-implicit-order.vtc needed to be fixed
because it was written after the change above, when the cache's
output wasn't compressed by the existing rules. Now with the fix,
the cache's output gets compressed again, so adding a separate
proxy for this allows to distinguish the request processing.

9 days agoBUG/MEDIUM: proxy: fix stick table expired entries cleanup on stopping
Amaury Denoyelle [Mon, 2 Nov 2026 13:34:42 +0000 (14:34 +0100)]
BUG/MEDIUM: proxy: fix stick table expired entries cleanup on stopping

Since the following patch, proxy internal management task is only
created for frontends or if a stick-table is present. This notably
serves to purge expired entries from tables on soft-stop.

  a4a17eb36698c927de1c3171236fcc5a6997721a
  OPTIM/MINOR: proxy: do not init proxy management task if unused

However, the condition was incorrectly written, as it is never true for
the stick-table case. This is because table's <current> member is always
zero at parse time as the stick-table is still empty. Thus, the task is
not allocated for backends even if stick-table is declared. This results
in worker processes potentially leaving expired entries in stick-tables.

The error originates from reusing the condition from manage_proxy(),
which performs the stick-table cleanup. In this context it makes sense
to check <current> value.

Fix the condition to ensure the task is allocated when a stick-table is
declared. Checking the proxy's <table> member is sufficient for that.

This fixes github issue #3554.

This must be backported up to 3.4.

9 days agoCI: github: cache VTest binary in FreeBSD workflow
William Lallemand [Mon, 2 Nov 2026 10:42:06 +0000 (10:42 +0000)]
CI: github: cache VTest binary in FreeBSD workflow

The FreeBSD job rebuilds VTest2 from source on every run. The shared
setup-vtest action cannot be used directly because it runs on the Ubuntu
host and would produce a Linux binary rather than a FreeBSD one.

Restore and save the workspace-relative vtest directory on the host with
actions/cache. Build into that directory inside the guest and explicitly
enable copyback so the resulting FreeBSD binary is available to the save
step after the VM exits.

Use the same release and architecture variables for the VM and cache key.
Keep the FreeBSD namespace separate from other jobs, and include the
upstream VTest SHA, build-script hash and a manual version suffix to
invalidate incompatible entries. Skip both cache steps if the SHA lookup
returns no output.

Probe the cached binary with -V and rebuild if it is missing or unusable.
Print the selected version and pass its absolute path to the full
regression suite. Save only after the VM step succeeds and the restore
was not an exact hit; retain the existing build and failure diagnostics.

9 days agoBUILD: github: run FreeBSD CI on every branch
William Lallemand [Mon, 2 Nov 2026 10:59:34 +0000 (10:59 +0000)]
BUILD: github: run FreeBSD CI on every branch

The FreeBSD workflow was the only workflow in .github/workflows to
restrict itself to the master and next branches. Pull request branches,
merged topic branches and tags were therefore never built or tested on
FreeBSD, unlike macOS and Alpine, which are part of the push-triggered
VTest matrix. As a result changes touching platform specific code could
be merged without any FreeBSD validation.

Drop the branches filter so the workflow triggers on every push, as the
VTest, Windows and Contrib workflows already do. The existing job
condition still limits the run to the haproxy repository, or to a manual
workflow_dispatch, so forks are unaffected.

9 days agoCI: github: add AWS-LC FIPS testing workflow
William Lallemand [Fri, 23 Oct 2026 11:51:51 +0000 (11:51 +0000)]
CI: github: add AWS-LC FIPS testing workflow

The FIPS branches of AWS-LC were covered by the VTest matrix, but only
built in non-FIPS mode: nothing exercised the FIPS-specific code paths in
HAProxy, in particular the compliance checks in src/fips.c which reject
non-approved ciphers, curves, signature algorithms and TLS versions at
configuration time.  A regression there is invisible until someone builds
HAProxy against an AWS-LC FIPS library.

Add a nightly workflow that builds AWS-LC with FIPS=1 and runs the VTest
and unit test suites against it, in the same way the other SSL library
workflows do.  It tests three sources, resolved through
determine_aws_lc():

  - AWS-LC 3.x, the fips-2024-09-27 branch;
  - AWS-LC 4.x, the fips-2025-09-12-lts branch;
  - the latest AWS-LC release tag, whose FIPS module is the one currently
    submitted for validation, so that the newest code is covered too
    instead of only the two maintenance branches.

Branch specs resolve to the head commit of the branch, which is passed as
AWS_LC_VERSION and downloaded as an immutable archive by build-ssl.sh, so
a run is reproducible and the build cache is only renewed when the branch
advances.  The resolved release and commit are both part of the cache
key, matching the version-specific identity used by the VTest job so the
two workflows never share a half-populated cache entry.

The workflow only runs on the upstream repository, or on manual dispatch
from a fork, like the other SSL library workflows.

Note that the AWS-LC latest release is not expected to be identical to
either FIPS branch; it is built in FIPS mode for its own FIPS module
version, which is what the extra job is meant to detect early.

9 days agoREGTESTS: ssl: skip AWS-LC FIPS rejected crypto in ciphersuites/curves tests
William Lallemand [Fri, 23 Oct 2026 11:50:51 +0000 (11:50 +0000)]
REGTESTS: ssl: skip AWS-LC FIPS rejected crypto in ciphersuites/curves tests

Four reg-tests deliberately configure cryptographic material that
HAProxy's FIPS compliance checks reject when the SSL library runs in FIPS
mode, so they can never start a configuration there:

  - ssl_ciphersuites.vtc and quic/ssl_ciphersuites.vtc configure the
    TLS_CHACHA20_POLY1305_SHA256 ciphersuite, which is not FIPS-approved;
  - ssl_curves_selection.vtc, quic/ssl_curves_selection.vtc and
    ssl_curve_name.vtc, quic/ssl_curve_name.vtc configure the X25519
    group, which is likewise rejected.

Each then fails while parsing the configuration rather than on the
behaviour under test, which is why ssl_fips_check_ciphersuites(),
ssl_fips_check_curves() and their callers in src/ssl_sock.c exist in the
first place; the rejection is the expected outcome of the check, not a
defect in the test.

Skip those files with the fips_mode() config condition predicate already
used elsewhere in reg-tests/ssl (see "REGTESTS: ssl: skip tests broken by
FIPS mode"), so that they keep running against every non-FIPS SSL library
including OpenSSL and plain AWS-LC, and are only skipped when FIPS is
actually active.

9 days agoBUG/MEDIUM: ssl: skip TLS 1.3 suites in the FIPS TLS 1.2 cipher check
Chris Staite [Sun, 1 Nov 2026 19:57:37 +0000 (19:57 +0000)]
BUG/MEDIUM: ssl: skip TLS 1.3 suites in the FIPS TLS 1.2 cipher check

With AWS-LC in FIPS mode, ssl_fips_check_ciphers() walks the list returned
by SSL_CTX_get_ciphers() and rejects any entry whose key exchange is not
ECDHE (or DHE). But AWS-LC documents that this list includes the TLS 1.3
cipher suites as well, and for these SSL_CIPHER_get_kx_nid() returns
NID_kx_any since TLS 1.3 suites do not specify the key exchange. Every
TLS 1.3 suite is thus reported as a non-FIPS cipher, including the
FIPS-approved ones that are set by default:

  FIPS mode active but non-FIPS cipher(s) configured:
  'TLS_AES_128_GCM_SHA256', 'TLS_AES_256_GCM_SHA384'.

Since the internal httpclient server is checked as well, HAProxy refuses
to start in FIPS mode with any configuration, even one without a single
SSL setting. This was observed with AWS-LC FIPS 4.0.0.

TLS 1.3 suites are already validated against their own list by
ssl_fips_check_ciphersuites(), so ssl_fips_check_ciphers() must only
consider TLS 1.2 ciphers. Skip the ciphers whose minimum protocol version
is TLS 1.3 or above. SSL_CIPHER_get_min_version() was preferred over
testing for NID_kx_any because it expresses the intent directly rather
than relying on a side effect of how TLS 1.3 suites are described. Non-FIPS
TLS 1.2 ciphers (e.g. AES128-SHA or ECDHE-RSA-CHACHA20-POLY1305) and
non-FIPS TLS 1.3 suites (e.g. TLS_CHACHA20_POLY1305_SHA256) are still
rejected.

This bug was introduced in 3.5-dev3 by commit 9e2809508 ("MEDIUM: ssl: add
FIPS TLS 1.2 cipher check for AWS-LC"), no backport is needed.

12 days agoBUILD: Makefile: allow external makefiles to provide build targets
William Lallemand [Fri, 30 Oct 2026 09:57:52 +0000 (10:57 +0100)]
BUILD: Makefile: allow external makefiles to provide build targets

Let EXTRA_MAKE fragments set EXTRA_TARGET_BUILD to a nonempty value to
provide their own build and cleanup targets instead of building HAProxy.
Keep the common option handling and regression-test targets available.

Evaluate the selector after global compiler and linker options have been
initialized so a fragment can install its alternate build rules there.

12 days agoBUG/MINOR: debug: fix "debug dev fd" crash on unknown address families
Amaury Denoyelle [Fri, 30 Oct 2026 09:53:08 +0000 (10:53 +0100)]
BUG/MINOR: debug: fix "debug dev fd" crash on unknown address families

"debug dev fd" formats the local and peer addresses of each fd with
sa2str() and then replaces non-printable characters in the result. It
returns NULL for address families other than AF_INET, AF_INET6, AF_UNIX
and some AF_CUST_*. For supported address families, it can also return
NULL in case of a memory allocation failure.

Only an admin in expert mode can run the command.

Let's skip the cleanup and print "?" when sa2str() fails.

This fixes GitHub issue #3546.

This must be backported to all stable versions. In fact, strlen()
without guard was introduced in 3.1, which guarantees a crash in case of
an unsupported address. For older releases, only an unsafe
chunk_appendf() is present which may cause issues.

12 days agoMINOR: thread: add EXTRAS_LOCK so addons can name their locks
William Lallemand [Fri, 30 Oct 2026 10:54:43 +0000 (11:54 +0100)]
MINOR: thread: add EXTRAS_LOCK so addons can name their locks

Provide an unconditional EXTRAS_LOCK label so addons can name their
locks using the existing lock debugging and statistics infrastructure.
This avoids adding addon-specific lock labels or build-option guards to
the core enum. Report the label as EXTRAS in lock statistics.

12 days agoBUILD: Makefile: extend the capabilities of EXTRA_MAKE
William Lallemand [Thu, 29 Oct 2026 22:46:35 +0000 (22:46 +0000)]
BUILD: Makefile: extend the capabilities of EXTRA_MAKE

Allow external Makefile.mk loaded through EXTRA_MAKE to extend the main
build without adding addon-specific logic to the root Makefile.

Provide the following extension points:

  - EXTRA_USE_OPTS registers additional USE_* switches. Check unknown
    options after loading the fragments so ERR=1 accepts these switches.
  - EXTRA_CFLAGS and EXTRA_LDFLAGS append compiler and linker flags.
  - EXTRA_INCLUDES adds headers to the build dependencies so changes to
    addon headers trigger recompilation.
  - EXTRA_LATE_HOOK evaluates addon logic after the object lists and
    compiler options have been collected, for example to generate
    metadata from the final build options.
  - EXTRA_INSTALL and EXTRA_UNINSTALL add targets to the install and
    uninstall prerequisites.

Reuse the existing EXTRA_OBJS variable for objects supplied by addons.
Keep all as the default goal so rules in an external fragment do not
change what a plain make invocation builds.

12 days agoBUG/MINOR: sample: fix NULL dereference in the ltime/utime converters
Martin Mirchev [Wed, 28 Oct 2026 14:33:16 +0000 (22:33 +0800)]
BUG/MINOR: sample: fix NULL dereference in the ltime/utime converters

conv_time_common() returns NULL when alloc_trash_chunk() fails, and when
the format string expanded for "%N" does not fit in the trash chunk. Its
four callers, sample_conv_us_ltime(), sample_conv_ms_ltime(),
sample_conv_us_utime() and sample_conv_ms_utime(), dereference the
result without checking it.

A configuration using one of these converters with a long format, e.g.
ms_ltime("%N%N%N...") expanding to more than bufsize, is accepted by
"haproxy -c", and the first request evaluating it crashes the process
with a NULL dereference. A failed allocation at runtime has the same
effect with an ordinary format.

Let's make the converter fail instead, like on its other error paths.

This fixes GitHub issue #3545.

This must be backported to all stable versions up to 3.0, as commit
5e63e1636 introduced the first version of conv_time_common() with a
possible NULL return.

12 days agoBUG/MINOR: errors: reset diagnostic buffers after thread cleanup
William Lallemand [Tue, 13 Oct 2026 14:13:03 +0000 (14:13 +0000)]
BUG/MINOR: errors: reset diagnostic buffers after thread cleanup

A live worker frees its thread-local diagnostic buffers before EXTRA_OBJS
teardown, whose deinitializers can still emit diagnostics.

ha_free() releases the allocation and clears only the area pointer. It
leaves size, head and data unchanged, while b_is_null() tests size rather
than area. A previously allocated buffer therefore still looks allocated
with a NULL area. Later diagnostics skip lazy allocation and can crash
when accessing the buffer.

Reset both usermsgs_buf and usermsgs_ctx.str to BUF_NULL after freeing
their areas. This clears the stale size and contents metadata so later
diagnostics can allocate fresh buffers. The post-deinit cleanup releases
any buffers reallocated during EXTRA_OBJS teardown.

This could be backported in every maintained versions.

13 days agoMINOR: sym: use the symbol table to resolve a DSO name
Olivier Houchard [Wed, 28 Oct 2026 17:51:13 +0000 (18:51 +0100)]
MINOR: sym: use the symbol table to resolve a DSO name

resolve_dso_name() only used dladdr() to find the object containing an
address. Our in-process symbol table also knows the owning object, is
lock-free and async-signal-safe, and also works on static builds where
dladdr() may not. Try it first and keep dladdr() as a fallback for
addresses that are not covered by any symbol, or when the table has not
been built yet.

13 days agoCLEANUP: sym: factor out the short object name extraction
Olivier Houchard [Wed, 28 Oct 2026 17:51:06 +0000 (18:51 +0100)]
CLEANUP: sym: factor out the short object name extraction

dump_sym_lookup() and resolve_dso_name() both keep the part of an object's
path between the last '/' and the first following '.'. Introduce
append_obj_name() for it and use it in both places.

13 days agoBUG/MINOR: checks/log: force implicit UDP checks as TCP for log backend
Aurelien DARRAGON [Thu, 29 Oct 2026 12:47:11 +0000 (13:47 +0100)]
BUG/MINOR: checks/log: force implicit UDP checks as TCP for log backend

As described in GH #3528, declaring an UDP log server with "check"
keyword, without specifying check port would result in haproxy
starting a check using TCP oriented API while the underlying address
family was implicit datagram. It would either cause the check to
always fail, or even segfault on some systems where the network API
from the operating system doesn't expect DGRAM family with stream oriented
functions such as "connect()".

To fix the issue, although we do support UDP log endpoints, there is no
UDP checks, and we don't offer ICMP checks, so let's just fallback to
TCP protocol when we detect the address family is DGRAM when starting
a check from a log backend.

The documentation was updated to describe this pitfall.

Many thanks to GH user @ssavant2 for filing a bug report and providing
useful reproducer.

It may be backported to 3.0.

2 weeks agoBUILD: sym: Fix build on recent glibc
Olivier Houchard [Wed, 28 Oct 2026 16:05:19 +0000 (17:05 +0100)]
BUILD: sym: Fix build on recent glibc

With a recent glibc, as found on Ubuntu 26.04, with the default locale,
sort and comm do not always agree on the ordering of all characters,
notably punctuation characters, and comm will thus fail when reading the
output of sort, producing a warning, and prevent the minidebug section
from being created.
Just force them to use the C locale, where this is not a problem.

2 weeks ago[RELEASE] Released version 3.5-dev10 v3.5-dev10
Willy Tarreau [Wed, 28 Oct 2026 14:43:02 +0000 (15:43 +0100)]
[RELEASE] Released version 3.5-dev10

Released version 3.5-dev10 with the following main changes :
    - BUG/MEDIUM: stats-file: shm-stats-file be counters not working anymore
    - BUG/MINOR: quic: rearm packet on packet received even non ack-eliciting
    - REGTESTS: use HAPROXY_TEST_TIMEOUT for wait in purge srv/be
    - BUILD: sym: Make sure minidebug works correctly with most environments
    - BUILD: sym: Use dl_iterate_phdr() for more OSes.
    - BUILD: sym: Get it to build and work on various OSes
    - BUILD: sym: Make sure __always_inline always is inline.
    - BUILD: sym: Build properly with gold as a linker
    - BUILD: sym: Keep a temporary binary without .gnu_debugdata
    - BUILD: sym: Don't add a minidebug section to haterm and haload
    - MINOR: quic: use the listener for Retry and Version Negotiation emission
    - MINOR: quic: define a function to emit a datagram on a listener socket
    - BUG/MEDIUM: quic: use datagram destination as source for Retry
    - BUG/MINOR: quic: use datagram destination as source for stateless reset
    - BUG/MINOR: quic: use datagram destination as source for version negotiation
    - REGTESTS: quic: check Retry source address on a wildcard listener
    - BUG/MINOR: quic: handle EINTR on emission of packets without connection
    - MINOR: quic: rename confusing qc_purge_txbuf() to qc_flush_txbuf()
    - BUG/MEDIUM: mux_quic: prevent uaf on fctl frame after send error
    - BUG/MINOR: mux_quic: prevent RS/SS on unistream with hq-interop
    - BUG/MINOR: check: do not inspect errno if conn FDLESS
    - BUG/MINOR: mux_quic: do not crash on SE_SHR_DRAIN shut
    - MEDIUM: quic: implement the AEAD confidentiality limit
    - OPTIM: quic: use native HKDF APIs where available
    - TESTS: quic: add HKDF unit tests using RFC 5869 vectors
    - BUG/MEDIUM: peers: keep the earliest sync task deadline
    - DOC: config: explain per ACL line limitations
    - MINOR: quic: use the same HP encryption helpers for RX and TX
    - BUG/MINOR: quic: valid NEW_TOKEN connections are counted as half-open
    - BUG/MEDIUM: cli: Fix parsing of big payloads with dynamically allocated buffer
    - BUG/MINOR: filters: fix silent compression disable with legacy "filter compression" first
    - MINOR: filters: Support the global side in filter_place_class()
    - OPTIMIZE: stream: Stop holding a reference on the frontend during stream_free()
    - BUG/MEDIUM: mux-h1: Don't close the connection on a payload parsing error
    - BUG/MEDIUM: h1: make the Connection/Upgrade headers cleanup idempotent
    - BUG/MINOR: h1: keep accepting unsafe violations after a parser reset
    - BUG/MEDIUM: h1: don't rewrite the Upgrade header when not cleaning headers
    - BUG/MEDIUM: h1-htx: don't stall on messages ending exactly at the buffer's end
    - BUG/MEDIUM: mux-h1: don't truncate the response when closing after draining
    - BUG/MEDIUM: mux-h1: don't truncate responses on soft-stop
    - BUG/MINOR: h1: don't use uninitialized start line pointers when restarting
    - BUG/MINOR: mux-h1: only reset the session's timing info on the frontend side
    - BUG/MINOR: mux-h1: fix the open streams counter on stream creation failure
    - BUG/MINOR: mux-h1: wake the connection up when a pending error can be sent
    - BUG/MINOR: mux-h1: don't dereference a NULL opposite sedesc in h1_fastfwd()
    - BUG/MEDIUM: h1: reject CONNECT request targets other than "host:port"
    - BUG/MEDIUM: mux-h1: don't lose the end of the previous chunk in h1_make_chunk()
    - BUG/MEDIUM: mux-h1: don't stall when draining more than a buffer of data
    - BUG/MINOR: mux-h1: reset the content-length/chunking state after a 1xx response
    - BUG/MINOR: mux-h1: don't keep fast-forwarding disabled for the next streams
    - BUG/MINOR: mux-h1: don't report a 408 error on timeout of a closed connection
    - BUG/MEDIUM: mux-h1: don't swap buffers when transferring data with a reserved offset
    - BUG/MINOR: mux-h1: report payload parsing glitches to the session
    - BUG/MEDIUM: mux-h1: don't lose error responses once a content-length was parsed
    - BUG/MEDIUM: h1: build a missing Host header from the authority of the URI
    - BUG/MINOR: h1: reject CONNECT authorities with an empty host
    - BUG/MINOR: h1: forget a previous pass' results when parsing headers only
    - BUG/MEDIUM: mux-h1: don't send chunked responses to HTTP/1.0 clients
    - BUG/MINOR: mux-h1: convert the <from> argument of h1-case-adjust to lower case
    - BUG/MEDIUM: h1: reject absolute http(s) URIs with an empty host
    - BUG/MINOR: mux-h1: only rely on the client's method for bodyless responses
    - BUG/MINOR: mux-h1: search the websocket key in the right message
    - BUG/MINOR: mux-h1: don't forward request trailers named like server name header
    - BUG/MEDIUM: mux-h1: don't spread-close idle conns with idle-close-on-response
    - BUG/MINOR: mux-h1: Don't subscribe for receives once read0 or error received
    - BUILD: makefile: better support git worktrees
    - BUG/MINOR: sym: don't crash on images linked at a fixed base
    - CI: github: run several weekly/monthly jobs daily
    - DOC: remove the AWS-LC CI image
    - BUG/MEDIUM: resolvers: reject too long labels when encoding a name
    - CI: github: fix VTest cache paths in container jobs
    - BUG/MAJOR: mux-quic: do not crash on connection error after glitch threshold
    - BUG/MAJOR: mux-h2: split absolute URIs like the HTTP analysers do on output
    - BUG/MEDIUM: h3: split absolute URIs like the HTTP analysers do on output
    - BUG/MINOR: filters: free the error message of the filter-config parsers
    - MINOR: filters: initialize the filter classes before the register stage
    - BUG/MEDIUM: bwlim: never burst more than the pending data in input
    - MINOR: filters: Move cache-store after decompression in global classes list
    - BUG/MINOR: sample: Fix wrong 'base64urldec' return value check in 'jwt_member_query'
    - BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on SETTINGS parsing
    - BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on GOAWAY parsing
    - BUG/MEDIUM: h3: fix BUG_ON() crash with glitch after QPACK dec err
    - MINOR: h3: remove dead code on h3_rcv_buf() error path
    - BUG/MINOR: h3: fix risk of BUG_ON() crash with glitch on unknown frame
    - BUG/MINOR: h3: fix BUG_ON() crash with glitch on htx_set_eom() failure
    - REGTESTS: filters: Fix the global filters order as reported on the CLI
    - Revert "BUG/MAJOR: mux-quic: do not crash on connection error after glitch threshold"
    - MEDIUM: filters: automatically order the implicit core filters in legacy mode
    - DOC: filters: document the automatic ordering of the legacy core filters
    - MEDIUM: filters: eval the backend instances first on the response path
    - MEDIUM: filters: eval the legacy backend filters first on the response path
    - BUG/MINOR: flt-decomp: Report error about filter order in lagacy mode only
    - BUG/MEDIUM: h3: fix memory corruption on trailers parsing realign
    - BUG/MINOR: mux_quic: wake streams on soft-stop using listener socket
    - BUG/MINOR: h3: enforce maxrewrite on request headers
    - BUG/MINOR: h3: fix crash with maxrewrite on response headers
    - BUG/MINOR: cfgparse/peers: warn and prevent undefined behavior when peers is missing bind info
    - BUG/MEDIUM: task: do not destroy twice the tasks already killed in mworker_cleantasks()
    - BUG/MEDIUM: mux_quic: fix crash on QCS double remote close
    - BUG/MEDIUM: mux_quic: ensure FIN is reported after Rx data transfer
    - MINOR: thread: split out thread_bind_to_cpu_map()
    - OPTIM: fd: Make each thread group's FD table NUMA-local
    - OPTIM: fd: make each thread group's polled_mask NUMA-local
    - MINOR: ssl: Add new 'pha' server option
    - BUILD: thread: fix builds without thread support
    - DOC: config: add missing acme.scheduler in the global index
    - MINOR: tools: add find_word_index_in_array() to find a keyword
    - MINOR: http/config: add acceptable-http-re{quest,sponse}-violations
    - MINOR: http: add one control per existing HTTP violation
    - MEDIUM: http: add the "short-status-code" violation exception
    - BUG/MINOR: jwt: don't truncate binary HMAC secrets at the first NULL byte
    - BUG/MINOR: jws: fix ECDSA_SIG leak in jws_b64_signature()
    - BUG/MINOR: jwe: only accept exact "alg" and "enc" names in JWE tokens
    - BUG/MINOR: jwe: reject non-empty encrypted keys with "dir" and "ECDH-ES"
    - BUG/MINOR: jwe: only match the exact "ALL" word in jwt.decrypt_{alg,enc}_list
    - BUG/MINOR: jwt: enforce the ECDSA signature length in jwt_verify
    - BUG/MINOR: mux-quic: prevent send corruption on buf realign
    - BUG/MEDIUM: mux_quic: fix FIN received with an already known offset
    - BUG/MINOR: quic: fix "show quic" filtered connection
    - BUG/MINOR: mux_quic: fix MAX_STREAMS limit
    - BUG/MINOR: mux_quic: fix overflow on avail_streams
    - MINOR: cli/server: speed up proxy/server purge via the CLI
    - BUG/MINOR: promex: don't loop forever when a module skips a time series
    - BUG/MINOR: http-ana: call the http_reset filter callback on L7 retries
    - DOC: filters: channel_start_analyze is called again on L7 retries
    - BUG/MEDIUM: server: Don't use the cached ALPN for websocket
    - BUG/MAJOR: h3: always check :path for all scheme values
    - BUG/MEDIUM: h3: prevent conn failure on trailers decoding without space
    - BUG/MINOR: h3: do not retry on trailers too large
    - BUG/MINOR: h3: do not wait after Tx realloc failure
    - BUG/MEDIUM: connection: don't leak the fd of an accepted connection
    - BUG/MINOR: hlua/filters: Track the payload phase per channel
    - BUG/MEDIUM: hlua/filters: make HTTPMessage.eom/may_recv relative to the filter's data
    - MINOR: hlua/filters: Add HTTPMessage.closed() to check if input is closed
    - BUG/MINOR: hlua/filters: Restrict HTTPMessage.set_eom/unset_eom to visible data
    - BUG/MEDIUM: hlua/filters: Make Channel.may_recv relative to the filter's data
    - BUG/MEDIUM: hlua/filters: Never arm the analysis timer for the lua wake-ups
    - BUG/MEDIUM: filters: Don't propagate a close while a filter waits on a channel
    - BUG/MEDIUM: deviceatlas: isolate threads while swapping the reloaded atlas
    - BUG/MINOR: deviceatlas: refresh the user-agent evidence id after a reload
    - BUG/MINOR: deviceatlas: ignore the scheduler's update in progress marker
    - BUG/MINOR: deviceatlas: use the result's property type when printing
    - BUG/MINOR: deviceatlas: consume a scheduled update before loading it
    - BUG/MEDIUM: peers: do not keep a dict entry cached when its update was not sent
    - BUG/MEDIUM: peers: fix byte order of last_get on updates without identifier
    - BUG/MINOR: peers: forget the appctx when the peer session init fails
    - BUG/MINOR: peers: don't lose the peer's version when the hello is fragmented
    - BUG/MINOR: peers: reject over-long varints in intdecode()
    - BUG/MINOR: peers: account incoming sessions in the per-thread applet counters
    - BUG/MINOR: peers: compare the received expiration date as unsigned
    - BUG/MINOR: peers: don't use remote table definitions from a previous session
    - BUG/MINOR: peers: forget the remote table when ignoring an unknown key type
    - BUG/MEDIUM: listener: enforce rate-limit sessions across thread groups
    - BUG/MINOR: hlua: unlock Patref before add_bulk type error
    - MINOR: filters: Set the class flags on filter class registration
    - MINOR: filters: Deduce the default filter API from the implied filters
    - MINOR: filters: Warn about ignored legacy filters for a proxy
    - MINOR: filters: Declare a proxy as using the legacy API before parsing filter directive
    - MINOR: filters: Add a function to remove a filter conf from all proxy lists
    - MINOR: filters: Add a function to find a legacy filter configuration during parsing
    - MINOR: cache: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: fcgi-app: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: bwlim: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: flt-decomp: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: flt-http-comp: Stop to explicitly loop on the proxy's filter_configs list
    - MINOR: filters: Document the public helpers missing from the filters guide
    - BUG/MEDIUM: applet: check buffer sizes before HTX transfer
    - DOC: config: Fix documentation about dot character in variable names
    - DOC: configuration: document library compatibility for TLS keywords
    - DOC: configuration: document library compatibility for pha
    - MINOR: filters: Pre-initialize the instances in check_config_validity()
    - BUG/MAJOR: mux_quic: remove data not yet sent on reset stream
    - BUG/MEDIUM: mux_quic: do not reset timeout after shut
    - BUG/MINOR: mux_quic: fix possible BUG_ON() after frame alloc failure
    - BUG/MINOR: mux_quic: fix ALPN retrieval from server
    - BUG/MINOR: mux_quic: fix stream elasticity dec on qcm_init() failure
    - BUG/MINOR: h3: reset HTX message after trailers parsing error
    - BUG/MINOR: thread: don't turn reversed thread ranges into "even"
    - BUG/MINOR: tools: make strl2irc() and strl2llrc() reject a lone minus sign
    - BUG/MINOR: cfgparse-listen: reject negative capture lengths
    - BUG/MINOR: stick-table: reject negative stick counter IDs in sc-* actions
    - CLEANUP: resolvers: don't close the nameserver socket twice on deinit
    - BUG/MINOR: connection: check the NetScaler CIP header length against received data
    - BUG/MINOR: connection: check the IP header length in NetScaler CIP
    - BUG/MEDIUM: ssl-gencert: fix a NULL dereference with QUIC listeners
    - BUG/MEDIUM: ssl: fix an out-of-bounds read on SSL_CTX switch with a .sctl file
    - BUG/MEDIUM: ssl: support SCTL with AWS-LC
    - BUG/MINOR: ssl: fix a double-free of the .sctl buffer with several contexts
    - BUG/MINOR: ssl: support SCTL in TLS 1.3 Certificate messages
    - DOC: configuration: clarify SCT support by TLS library and version
    - BUG/MINOR: server: free the sample expression when it is not usable on a server
    - BUG/MEDIUM: mux-spop: Don't include DALLOC/DFULL flags in SPOP_CF_DEM_BLOCK_ANY
    - DOC: config: add missing entry for health checks
    - IMPORT: slz: update slz to version 1.4-dev3

2 weeks agoIMPORT: slz: update slz to version 1.4-dev3
Willy Tarreau [Wed, 28 Oct 2026 14:18:42 +0000 (15:18 +0100)]
IMPORT: slz: update slz to version 1.4-dev3

This refreshes the slz library to 1.4-dev3, bringing a number of
improvements:
  - x1.4 to x20 compression speed depending on input contents, due
    to better handling of non-compressible data (x20, think CSS/HTML
    with embedded images) and many other optimizations giving at
    least x1.4 at the base.

  - slightly higher default compressed ratio (46.34% vs 46.97%) and
    top compressed ratio (43.03% vs 45.33%) on silesia corpus, and
    23.36% vs 26.22% on linux kernel, which now compresses at 1 GB/s
    vs 600 MB/s before on a P-core of a Core Ultra 265k.

  - x2 to x3 decompression speed (mostly 2.5) depending on input
    stream, best gains on gzip input which was previously much slower
    since not as much in the target.

  - better support for crc32 on ARM (properly ignore older archs)

  - runtime detection of CPU support for PCLMULQDQ on x86_64

This version now passes all lzbench tests so it was the right moment
to update it. More updates to come soon (taking content-type into
account to adjust optimizations) still being worked on, but nothing
critical.

In order to ease updates, just like for xz or ceb, stubs were added
in src/ that directly load the original files from include/import.

2 weeks agoDOC: config: add missing entry for health checks
Willy Tarreau [Mon, 26 Oct 2026 18:05:03 +0000 (19:05 +0100)]
DOC: config: add missing entry for health checks

In 3.4, a new health checks section (12.9) was added by commit 6fbccae1ab
("MEDIUM: tcpcheck/server: Add healthcheck server keyword") but it didn't
get a global entry. Let's do it.

This can be backported to 3.4.

2 weeks agoBUG/MEDIUM: mux-spop: Don't include DALLOC/DFULL flags in SPOP_CF_DEM_BLOCK_ANY
Adam Szymanski [Wed, 28 Oct 2026 10:19:59 +0000 (11:19 +0100)]
BUG/MEDIUM: mux-spop: Don't include DALLOC/DFULL flags in SPOP_CF_DEM_BLOCK_ANY

SPOP_CF_DEM_BLOCK_ANY is documented as "aggregate of the demux flags above
except DALLOC/DFULL", like H2_CF_DEM_BLOCK_ANY and FCGI_CF_DEM_BLOCK_ANY, but
its value 0x7E0 also covers SPOP_CF_DEM_DALLOC (0x20) and SPOP_CF_DEM_DFULL
(0x40).

When a single recv() fills the demux buffer, spop_recv() sets
SPOP_CF_DEM_DFULL. spop_process() then skips spop_process_demux() because
the flag is part of SPOP_CF_DEM_BLOCK_ANY, and the only place clearing
SPOP_CF_DEM_DFULL is inside that skipped block. spop_recv_allowed() also
refuses to read. The connection stays stuck with a full dbuf forever: the
peer sees a zero TCP window, ACK frames are never parsed, every stream
attached to the connection times out, and new streams keep being attached
to it because it is still considered available.

This happens with pipelining when enough ACK frames are queued at once to
fill the 16kB demux buffer in one read (e.g. ACKs carrying ~1-3kB
variables), so "no option pipelining" in the spoe-agent section hides it.

The value must be 0x780 (MROOM|SALLOC|SFULL|TOOMANY).

Reproduced on HAProxy 3.4.7 with nbthread 4, pipelining enabled,
50 concurrent HTTP keep-alive clients, and a Go SPOA agent returning
ACK frames carrying approximately 3kB of variables. In three 10-second
test runs, the unpatched version left 3-5 connections stuck with
DEM_DFULL and produced 400-520 SPOE timeouts per run. With the fix,
no stuck connections or SPOE timeouts were observed.

This must be backported as far as 3.1.

Co-authored-by: ITGG <kontakt@itgg.eu>
2 weeks agoBUG/MINOR: server: free the sample expression when it is not usable on a server
Martin Mirchev [Wed, 28 Oct 2026 14:08:10 +0000 (22:08 +0800)]
BUG/MINOR: server: free the sample expression when it is not usable on a server

parse_srv_expr() parses the "sni" and "pool-conn-name" expressions of a
server. When the fetch cannot be used on a server connection (e.g. a
fetch that needs a response such as "res.hdr(host)"), it reports an error
and returns without releasing the expression it has just allocated.

At startup this is harmless since the process exits on the error, but the
same function is used by the "add server" CLI command. Each rejected
"add server ... sni <expr>" leaks the expression (about 200 bytes per
command, as reported by valgrind), so a script or an operator repeatedly
issuing such commands slowly increases the process' memory usage. Only
admin-level CLI users can trigger it.

This fixes GitHub issue #3547.

This must be backported to all stable versions starting with 3.0, where
the code was introduced by commit 91001422b4 ("MINOR: server: generalize
sni expr parsing").

2 weeks agoDOC: configuration: clarify SCT support by TLS library and version
William Lallemand [Tue, 27 Oct 2026 15:13:40 +0000 (15:13 +0000)]
DOC: configuration: clarify SCT support by TLS library and version

List the TLS versions and libraries supporting SCT delivery, and indicate
that QUIC is unsupported.

This should be backported with the SCTL fixes.

2 weeks agoBUG/MINOR: ssl: support SCTL in TLS 1.3 Certificate messages
William Lallemand [Tue, 27 Oct 2026 15:13:40 +0000 (15:13 +0000)]
BUG/MINOR: ssl: support SCTL in TLS 1.3 Certificate messages

SSL_CTX_add_server_custom_ext() only sends SCT lists in TLS 1.2 and earlier
ServerHello messages. Use SSL_CTX_add_custom_ext() on vanilla OpenSSL 1.1.1
and later to support TLS 1.3 Certificate messages as well. Retain the legacy
API for other libraries and older OpenSSL versions.

Send SCTs only on the leaf Certificate entry when requested by the client
and present for the selected certificate.

Keep SCT delivery disabled for QUIC: valgrind reports a use-after-free in
native OpenSSL 3.5.5 when SSL_set_SSL_CTX() merges transport parameters with
custom extensions. Also skip loading SCT lists into QUIC contexts,
including on AWS-LC.

Verified exact SCT payloads in TLS 1.2 and TLS 1.3 with a two-entry chain,
SNI isolation, omission without a client request, QUIC omission, and clean
config-check/deinit under valgrind.

This must be backported to all supported versions.

2 weeks agoBUG/MINOR: ssl: fix a double-free of the .sctl buffer with several contexts
William Lallemand [Tue, 27 Oct 2026 14:32:47 +0000 (14:32 +0000)]
BUG/MINOR: ssl: fix a double-free of the .sctl buffer with several contexts

ssl_sock_load_sctl() stores the ckch's .sctl buffer in SSL_CTX ex_data,
whose free callback releases it when the context is destroyed. The ckch
also frees this buffer, causing a double-free during deinit or "haproxy -c",
even with a single bind and certificate. Sharing the certificate across
contexts adds further frees of the same buffer.

Give each context its own copy, leaving the original owned by the ckch.
AWS-LC already copies the list through its native API.

This bug was introduced in 2.1 by commit 0dfae6c315 ("MINOR: ssl: load sctl
from buf OR from a file") and must be backported to all versions since then.

2 weeks agoBUG/MEDIUM: ssl: support SCTL with AWS-LC
William Lallemand [Thu, 22 Oct 2026 15:12:18 +0000 (15:12 +0000)]
BUG/MEDIUM: ssl: support SCTL with AWS-LC

AWS-LC owns extension 18 and rejects server custom extension registration.
The previous commit skips that registration, leaving .sctl files ignored.

Use SSL_CTX_set_signed_cert_timestamp_list(), available in all AWS-LC
releases, to set the serialized SCT list. AWS-LC copies the list and handles
context switches. Enable HAVE_SSL_SCTL for AWS-LC despite its missing
SN_ct_cert_scts OID.

AWS-LC also validates the list. Report a rejected list as a warning and omit
the extension, allowing the certificate to remain usable.

Tested with AWS-LC 4.2.0 and 3.0.0: TLS 1.2 sends the exact SCT payload for
the selected certificate with a sidecar and omits it for another certificate
on the same bind.

This must be backported to 3.0, the first version supporting AWS-LC.

2 weeks agoBUG/MEDIUM: ssl: fix an out-of-bounds read on SSL_CTX switch with a .sctl file
William Lallemand [Thu, 22 Oct 2026 12:26:54 +0000 (12:26 +0000)]
BUG/MEDIUM: ssl: fix an out-of-bounds read on SSL_CTX switch with a .sctl file

A certificate's .sctl sidecar registers extension 18 only on that
certificate's SSL_CTX. Connections start on initial_ctx without this
extension, then switch contexts in the ClientHello callback.

OpenSSL sizes the received-extensions array from the initial context but
parses it using the selected context's extension count. An unauthenticated
ClientHello selecting a certificate with a .sctl file can therefore trigger
an out-of-bounds read in tls_parse_all_extensions(). Depending on the heap
contents, the parser may also write past the array and read adjacent data.

Register the extension in the same order on every switchable frontend
context. The add callback omits it when the selected context has no SCT
list. Skip QUIC contexts because native OpenSSL's SSL_set_SSL_CTX() reads
freed memory when merging the transport parameters extension with custom
extensions. Skip registration on AWS-LC, which owns extension 18.

Workaround: remove "sctl" from "ssl-load-extra-files" or remove the .sctl files.

This should be backported to all supported versions.

Reported-by: Finlay Moss <fmoss2003@gmail.com>
2 weeks agoBUG/MEDIUM: ssl-gencert: fix a NULL dereference with QUIC listeners
Frederic Lecaille [Wed, 28 Oct 2026 10:26:02 +0000 (11:26 +0100)]
BUG/MEDIUM: ssl-gencert: fix a NULL dereference with QUIC listeners

ssl_sock_generate_certificate_from_conn() retrieves the connection from
the SSL object through ssl_app_data_index. A QUIC SSL object only
carries the quic_conn, stored under ssl_qc_app_data_index, so the
connection is always NULL there and conn_get_dst() dereferences a NULL
pointer.

This function is called from ssl_sock_switchctx_cbk() when the
ClientHello has no SNI and 'generate-certificates' is set on the bind
line. Any client that omits the server_name extension therefore kills
the worker (SIGSEGV), with no skill required: connecting to an IP
address is enough, since RFC 6066 forbids IP literals in SNI.

The bug is not specific to the OpenSSL compatibility layer. It
reproduces with USE_QUIC=1 whether or not USE_QUIC_OPENSSL_COMPAT is
set: the callback is registered the same way (HAVE_SSL_CLIENT_HELLO_CB)
and the ex_data assignment in qc_ssl_sess_init() is identical. It was
reproduced on a quictls build (USE_QUIC=1 alone, no 'limited-quic'
needed) and on an OpenSSL build with USE_QUIC_OPENSSL_COMPAT=1, both
crashing in ssl_sock_generate_certificate_from_conn().

Use ssl_sock_get_conn(), which handles both the connection and the
quic_conn, and fall back on the quic_conn's local address, the QUIC
counterpart of conn->dst, when there is no connection. When no address
is available, return 0 so that the caller keeps its default certificate
selection instead of crashing.

This must be backported to all stable versions.

Reported-by: Martin Mirchev <mirchevmartin2203@gmail.com>
2 weeks agoBUG/MINOR: connection: check the IP header length in NetScaler CIP
Martin Mirchev [Tue, 27 Oct 2026 06:17:35 +0000 (14:17 +0800)]
BUG/MINOR: connection: check the IP header length in NetScaler CIP

conn_recv_netscaler_cip() takes the TCP ports at ip_hl * 4 bytes after the
start of the IPv4 header without checking ip_hl nor that the announced
header length covers them:

  - with ip_hl < 5 the ports are taken from the IPv4 header itself (e.g.
    ip_hl=0 gives source port 16384 from the version/IHL byte);
  - with a header length shorter than ip_hl * 4 + 4 (e.g. ip_hl=15 and a
    40-byte header) they are taken from the payload or from bytes past the
    received data, which are leftovers of the previous use of the trash
    buffer, possibly from another connection.

The same applies to IPv6 with a header length shorter than 44 bytes. The
reads stay inside the trash buffer, so this is not a memory safety issue,
but the session addresses end up with wrong ports. Let's reject such
headers with CO_ER_CIP_BAD_PROTO.

This should be backported to all stable versions.

2 weeks agoBUG/MINOR: connection: check the NetScaler CIP header length against received data
Martin Mirchev [Tue, 27 Oct 2026 02:43:46 +0000 (10:43 +0800)]
BUG/MINOR: connection: check the NetScaler CIP header length against received data

conn_recv_netscaler_cip() only checks that the announced header length
<hdr_len> is not larger than the whole peeked data, which includes the
8 (legacy) or 12 (standard) byte CIP prefix. The header is then consumed
with a single recv() of prefix + hdr_len bytes into the trash chunk. With
hdr_len close to tune.bufsize (e.g. 16384 with the default bufsize), this
recv() writes up to 12 bytes past the end of the trash buffer.

Make sure the announced header fits in the data received after the prefix
before going further.

CIP is only accepted from trusted peers, so this is not a security issue,
but a sender misbehaving that way must not corrupt memory.

This should be backported to all stable versions.

2 weeks agoCLEANUP: resolvers: don't close the nameserver socket twice on deinit
Martin Mirchev [Tue, 27 Oct 2026 06:29:44 +0000 (14:29 +0800)]
CLEANUP: resolvers: don't close the nameserver socket twice on deinit

resolvers_destroy() calls fd_delete() on the UDP socket of each nameserver,
which already closes it, then calls close() on the same FD again. This
second close() fails with EBADF (visible with strace on soft-stop), and
could close an unrelated FD if the number were reused in between. Let's
just drop the redundant close().

This came with resolvers_destroy() in 2.7 (commit e606c84fe "MINOR:
resolvers: resolvers_destroy() deinit and free a resolver"). There is no
need to backport it, though it does not hurt.

2 weeks agoBUG/MINOR: stick-table: reject negative stick counter IDs in sc-* actions
Martin Mirchev [Tue, 27 Oct 2026 06:37:07 +0000 (14:37 +0800)]
BUG/MINOR: stick-table: reject negative stick counter IDs in sc-* actions

The parsers of sc-inc-gpc, sc-inc-gpc0/1, sc-add-gpc, sc-set-gpt and
sc-set-gpt0 read the stick counter ID with strtol() into a signed int and
only check it against tune.stick-counters. A negative ID such as in
"sc-inc-gpc(0,-2)" or "sc-set-gpt0(-1)" is thus accepted, and at runtime
the actions read &s->stkctr[-2] or &sess->stkctr[-1], before the start of
the stick counters array. ASan reports a heap-buffer-overflow READ in
stkctr_entry() called from the action, and a release build may crash or
use whatever lies there as a stick-table entry pointer to update it.

The sc_* sample fetches are not affected since they convert the ID to an
unsigned int before the check, and track-sc* parses an unsigned number.

Let's also reject negative IDs. A reg-test is added.

This should be backported to all stable branches. sc-add-gpc() appeared
in 2.8 and the others are older.

2 weeks agoBUG/MINOR: cfgparse-listen: reject negative capture lengths
Martin Mirchev [Tue, 27 Oct 2026 06:37:07 +0000 (14:37 +0800)]
BUG/MINOR: cfgparse-listen: reject negative capture lengths

"capture request header", "capture response header" and "capture cookie"
read their length with atol() and never check it, unlike "declare capture"
and "http-request capture" which reject lengths <= 0. A negative length,
or one that does not fit in an int such as 4294967295, is thus accepted,
and the first matching message crashes the process:

  - for headers, the capture pool is created with size len+1 (0 for -1),
    then http_capture_headers() calls isttrim(v, h->len), which converts
    -1 to SIZE_MAX and does not trim anything, so the whole header value
    is copied into the tiny pool object. ASan reports a heap-buffer-overflow
    WRITE from http_capture_headers(), and a release build usually dies
    soon after with a pool tag mismatch, a malloc() corruption abort or a
    SIGSEGV;

  - for the cookie, http_manage_client_side_cookies() clamps log_len to
    capture_len (-1) and calls memcpy() with (size_t)-1 (ASan:
    negative-size-param).

Let's reject negative lengths and lengths that do not fit in an int at
parsing time. A length of zero is still accepted since it is harmless
(it captures an empty string) and some configurations may rely on it. A
reg-test is added.

This should be backported to all stable branches. The parser was moved
to proxy_parse_capture() in 3.5-dev, older branches have the same atol()
calls in cfg_parse_listen().

2 weeks agoBUG/MINOR: tools: make strl2irc() and strl2llrc() reject a lone minus sign
Martin Mirchev [Tue, 27 Oct 2026 03:36:58 +0000 (11:36 +0800)]
BUG/MINOR: tools: make strl2irc() and strl2llrc() reject a lone minus sign

strl2irc() and strl2llrc() are documented to accurately detect any error
(truncated string, invalid chars, overflows). But when the string is made
of a single '-', the negative branch skips the sign, the digit loop does not
run since there is nothing left, and the function returns success with 0.

As a result, a lone "-" is silently taken as 0 wherever these functions are
used to validate a number:
  - "uid -" and "gid -" in the global section set uid/gid 0 (root)
    instead of emitting the usual "is not a number" warning. When started
    as root, this also suppresses the warning about running as root
    without "user"/"uid", so the process silently keeps root privileges;
  - "set server <b>/<s> check-port -" (and "agent-port -") on the CLI
    sets port 0 instead of failing with "provided port is not an
    integer";
  - req.hdr_ip() accepts "1.2.3.4:-" as a valid address and port;
  - an integer ACL pattern ":-" is taken as ":0", and a "-" value in the
    CLI "set table ... data.<type> -" command stores 0.

Let's return an error when the sign is not followed by any digit. A
reg-test checks that "uid -" and "gid -" now produce the warning (made
fatal with -dW).

This has been there since the functions were introduced, so the fix may be
backported to all stable branches.
[wt: better not backport as it could break configs running by accident]

2 weeks agoBUG/MINOR: thread: don't turn reversed thread ranges into "even"
Martin Mirchev [Tue, 27 Oct 2026 03:36:58 +0000 (11:36 +0800)]
BUG/MINOR: thread: don't turn reversed thread ranges into "even"

parse_thread_set() checks that the last thread number of a range is within
the permitted limits, but not that it is greater than or equal to the first
one. When storing the set, "max < min" is the internal encoding of "all"
(max=0), "odd" (max=-1) and "even" (max=-2), so any range written in
reverse order such as "thread 4-2" or "thread 1/4-2" on a "bind" line was
silently accepted and stored as "even" (memset 0xaa). With "nbthread 4",
"thread 4-2" binds the listener to threads 2 and 4 instead of threads 2 to
4, and "thread 4-1" also gives threads 2 and 4. There is no warning, so the
only visible effect is that some threads never accept connections on that
listener, which is easy to miss and hard to diagnose.

The other range parsers ("cpu-map", "process" via parse_process_number(),
and the cpuset parser) swap a reversed range, so let's do the same here.
This keeps configurations that rely on such ranges working, with the threads
the user wrote. A reg-test is added to check that "2-4", "4-2" and "1/4-2"
give the same thread mask.

This bug was introduced with the thread_set API in 2.8 (commit bef43dfa6
"MINOR: thread: add a simple thread_set API"), so this fix may be backported
to all stable branches down to 2.8.

2 weeks agoBUG/MINOR: h3: reset HTX message after trailers parsing error
Amaury Denoyelle [Tue, 27 Oct 2026 14:42:04 +0000 (15:42 +0100)]
BUG/MINOR: h3: reset HTX message after trailers parsing error

HTTP/3 trailers parsing was adapted by the following patch to rely on
the common function http_trailers_to_htx().

  25a11d345425be08cb01fec0eae5dfe8b423ee57
  MEDIUM: h3: rely on the generic trailers parser

Along this change, h3_trailers_to_htx() error path has been updated to
regroup failure with or without glitch via two labels. However, it
bypasses HTX message reset, leaving the stream buffer appearing as full
and possibly containing a partial trailers conversion.

Fix this by restoring the single unified function end. Return value and
glitch are directly adjusted in the switch statement from
http_trailers_to_htx() result.

No need to backport.

2 weeks agoBUG/MINOR: mux_quic: fix stream elasticity dec on qcm_init() failure
Amaury Denoyelle [Tue, 27 Oct 2026 15:09:28 +0000 (16:09 +0100)]
BUG/MINOR: mux_quic: fix stream elasticity dec on qcm_init() failure

On qcm_init() early failure, QCC is freed via qcc_release(). This
function always decrements reserved streams from the thread group pool
when stream elasticity is active for frontend connections.

This causes an issue as qcm_init() may fail before accounting the
committed streams: this is the case for <pacing_task> allocation
failure. Thread group counter is thus invalid after the extra decrement
and may cause an underflow and wrap.

Fix this by moving pacing task allocation later in qcm_init() so that
streams accounting is now performed before any failure path.

This must be backported up to 3.4.

2 weeks agoBUG/MINOR: mux_quic: fix ALPN retrieval from server
Amaury Denoyelle [Tue, 27 Oct 2026 14:11:34 +0000 (15:11 +0100)]
BUG/MINOR: mux_quic: fix ALPN retrieval from server

On backend side, ALPN may be retrieved from the server when 0-RTT is
used. This requires the server lock to protect concurrent modification
of the server access.

This code was badly written as server ALPN was still accessed outside of
the lock. Fix this by copying the server ALPN in a local string before
releasing the lock.

This also fixes the detection of an empty server ALPN to avoid an
unnecessary call to quic_alpn_to_app_ops().

This must be backported up to 3.4.

2 weeks agoBUG/MINOR: mux_quic: fix possible BUG_ON() after frame alloc failure
Amaury Denoyelle [Tue, 27 Oct 2026 10:01:13 +0000 (11:01 +0100)]
BUG/MINOR: mux_quic: fix possible BUG_ON() after frame alloc failure

Reset the list of frames to send at the end of qcc_io_send() if at least
one QCS instance was moved to the qcs_failed list and is reinserted into
the send list. This is mandatory to ensure frame generation will be
performed again for the failed QCS instances.

This issue is detected as it triggers a BUG_ON() crash on the second
qcc_io_send() call, as the send list will contain the previously failed
QCS after qcc_send_frames() despite not being blocked on flow control or
buffer allocation.

This bug occurs after qcs_send() failure, which is either due to the
connection currently being subscribed for send after a transient network
error or after a frame allocation failure under memory pressure.
However, the first case cannot occur as a transient error will not leave
any QCS in failed list. Thus, this fix is only relevant for memory
allocation failure, which explains its minor severity.

This must be backported up to 3.2 after a period of observation. For
older releases, a local frame list is used instead in qcc_io_send().

2 weeks agoBUG/MEDIUM: mux_quic: do not reset timeout after shut
Amaury Denoyelle [Wed, 28 Oct 2026 09:38:29 +0000 (10:38 +0100)]
BUG/MEDIUM: mux_quic: do not reset timeout after shut

When a connection is shutting down, MUX QUIC uses shut-timeout if
defined as a final timeout value. However, the timer was reset every
time qcc_refresh_timeout() was called. In effect, a connection could
stay indefinitely due to repeated activity from the peer.

Fix this by ensuring shut-timeout is only set once in
qcc_refresh_timeout(). The value is not updated if QCC task timeout is
already set.

Note that shut-timeout could be completely ignored if set to a greater
value than the default client/server timeout. However, as the docs
imply, this is not expected, as it is used to ensure a closing
connection will quickly end.

This must be backported up to 2.8.

2 weeks agoBUG/MAJOR: mux_quic: remove data not yet sent on reset stream
Amaury Denoyelle [Mon, 26 Oct 2026 10:21:32 +0000 (11:21 +0100)]
BUG/MAJOR: mux_quic: remove data not yet sent on reset stream

When a stream is reset, it is expected that no more data will be
emitted. Thus, prepared data not yet sent is subtracted from the
connection flow control. However, nothing is performed for the data
itself, which may have important side effects.

Most of the time, this issue is invisible as RESET_STREAM frame is
finally emitted, QCS instance is fully closed and removed from the send
list, ready for its deletion. However, an issue is possible if the read
channel is not yet closed and output has been blocked for whatever
reason. If output is unblocked before the read channel closure, QCS may
emit new data STREAM frames, triggering a BUG_ON() on stream flow
control in qcm_ctrl_send(). This assertion reveals a possible violation
of RFC 9000 3.3 (STREAM frame sent in "Reset Sent" state) and of the
final size already advertised by RESET_STREAM frame.

The proper fix to this issue is to also clear prepared data when stream
is reset, along with the flow control subtraction. This guarantees that
no more STREAM frames will be emitted after stream reset, as any
potential snd_buf stream layer callback will drain content.

This must be backported to all stable releases. Note that qcs_tx_buf()
is available since 3.4, needs adjustment for previous releases.

Many thanks to "Sungho Park (@gigio1023)" for its report and analysis of
the issue.

2 weeks agoMINOR: filters: Pre-initialize the instances in check_config_validity()
Christopher Faulet [Wed, 28 Oct 2026 08:01:51 +0000 (09:01 +0100)]
MINOR: filters: Pre-initialize the instances in check_config_validity()

The filter instances were pre-checked by a callback registered at the
pre-check stage, flt_precheck_instances_all(). This was annoying for
the modules adding instances after the configuration parsing, from
their own pre-check callback: the pre-check callbacks run before
check_config_validity(), so the instances they add are never
finalized.

The pre-check of all instances now runs from check_config_validity(),
before the servers are initialized: the default filter mode is
applied, then every instance is finalized by the parse callback of its
class, and the filter-enable/filter-disable and filter-sequence
directives are applied, as before. The pre-check callbacks may add
instances when the whole configuration was parsed, they are then
pre-initialized like the ones declared by the configuration.

The functions are renamed accordingly: flt_precheck_instances_all()
becomes flt_preinit_instances_all(), now exported, and
flt_precheck_instances(), flt_precheck_class_instances() and
flt_precheck_instance() respectively become flt_preinit_instances(),
flt_preinit_class_instances() and flt_preinit_instance().

2 weeks agoDOC: configuration: document library compatibility for pha
William Lallemand [Fri, 23 Oct 2026 10:56:53 +0000 (10:56 +0000)]
DOC: configuration: document library compatibility for pha

Add a Compatibility line to the pha server keyword documentation, following
the format introduced by commit 18003c561 (DOC: configuration: document
library compatibility for TLS keywords).

Document the minimum OpenSSL version and the lack of support in AWS-LC.

2 weeks agoDOC: configuration: document library compatibility for TLS keywords
William Lallemand [Tue, 13 Oct 2026 20:28:12 +0000 (20:28 +0000)]
DOC: configuration: document library compatibility for TLS keywords

Several TLS configuration keywords describe their OpenSSL requirements in
prose without a standardized Compatibility line, leaving AWS-LC support
unclear to readers.

Add Compatibility lines for the default bind and server ciphersuites,
bind and server ciphersuites, and TLS key logging. These require OpenSSL
1.1.1 or newer and AWS-LC 1.22.0 or newer (API 22). Replace the certificate
compression support sentence with a Compatibility line requiring OpenSSL
3.2.0 and explicitly stating that AWS-LC is unsupported.

Leave keywords requiring OpenSSL versions below 1.1.1 unchanged. This is
a documentation-only change; no runtime behavior is modified.

This may be backported to maintained branches containing the corresponding
keywords and AWS-LC support (as far as 3.0).

2 weeks agoDOC: config: Fix documentation about dot character in variable names
Christopher Faulet [Tue, 27 Oct 2026 13:20:17 +0000 (14:20 +0100)]
DOC: config: Fix documentation about dot character in variable names

In the section 2.8 (Variables), the doc character ('.') was not listed in
the set of characters allowed in a variable name. However it is an
error. The dot character is allowed. It cannot be used in the scope. But it
is allowed in the name.

The bug was introduced with the commit b643fbb1a ("DOC: config: Add a
dedicated section about variables"), when the section 2.8 was added.

This patch could be backported as far as 2.8.

2 weeks agoBUG/MEDIUM: applet: check buffer sizes before HTX transfer
Adam Crawford [Mon, 26 Oct 2026 04:21:40 +0000 (23:21 -0500)]
BUG/MEDIUM: applet: check buffer sizes before HTX transfer

The appctx_htx_snd_buf() fast path uses b_xfer() when the destination
HTX message is empty and the source contains a complete HTX message.
However, it does not verify that both buffers have the same capacity.

When tune.bufsize.large is configured and wait-for-body uses
use-large-buffer, the channel buffer may be larger than the applet
input buffer. The fast path can then attempt to transfer more data
than the destination can hold, triggering a fatal bounds check and
terminating the worker process.

Only use the fast path when the source and destination buffers have
the same capacity. Otherwise, use the existing HTX block-wise transfer
path.

[cf: Must be backported to 3.4]

2 weeks agoMINOR: filters: Document the public helpers missing from the filters guide
Christopher Faulet [Tue, 27 Oct 2026 12:45:40 +0000 (13:45 +0100)]
MINOR: filters: Document the public helpers missing from the filters guide

The filters internals guide did not reference several public functions
of the filter API, mainly the ones handling the configuration during
the parsing and the post-parsing stages. They are now described, at the
place where a filter developer needs them:

  * filter_find_class() looks a filter class up by name, in the class
    registry section (3.2);

  * flt_conf_add(), flt_conf_insert() and flt_conf_delete() manage the
    three configuration orders of a legacy filter configuration, in the
    legacy keyword parser section (3.2.2), with the constraint on the
    FLT_CFG_FL_EXPLICIT flag;

  * flt_use_legacy_filter() tests the filter mode of a proxy during the
    parsing, in the same section, with the constraint not to cache its
    result before the post-parsing stage;

  * flt_find_legacy_conf() searches the legacy filter configurations
    during the parsing, whatever the filter mode, in the section about
    referencing a filter configuration from other directives (3.7);

  * flt_has_explicit_config() detects an explicit filter declaration,
    in the same section.

2 weeks agoMINOR: flt-http-comp: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 10:54:35 +0000 (11:54 +0100)]
MINOR: flt-http-comp: Stop to explicitly loop on the proxy's filter_configs list

The "compression", "comp-req" and "comp-res" filter keyword parsers
scanned the filter_configs list of the proxy by hand to detect an
already declared compression filter configuration, matching on the
filter ids.

The parsers now use flt_find_conf(), the supported way to find a filter
configuration, which walks the right list depending on the filter mode of
the proxy. The "compression" duplicate check resolves the two possible
configurations with two successive lookups, one per direction.

2 weeks agoMINOR: flt-decomp: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 10:51:14 +0000 (11:51 +0100)]
MINOR: flt-decomp: Stop to explicitly loop on the proxy's filter_configs list

The "decomp-req" and "decomp-res" filter keyword parsers scanned the
filter_configs list of the proxy by hand to detect an already declared
decompression filter configuration, matching on the filter id.

The parsers now use flt_find_conf(), the supported way to find a filter
configuration, which walks the right list depending on the filter mode
of the proxy. As a side effect, the message of the "decomp-res" duplicate
error now names the right filter.

2 weeks agoMINOR: bwlim: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 10:50:33 +0000 (11:50 +0100)]
MINOR: bwlim: Stop to explicitly loop on the proxy's filter_configs list

The bandwidth limitation filter keyword parsers scanned the
filter_configs list of the proxy by hand to detect an already declared
"bwlim-in" or "bwlim-out" configuration, matching on the filter ids and
the limitation name.

The parsers now use flt_find_conf() with a match callback, the supported
way to find a filter configuration, which walks the right list depending
on the filter mode of the proxy. The duplicate check matches both filter
ids at once, with a single lookup.

2 weeks agoMINOR: fcgi-app: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 09:48:34 +0000 (10:48 +0100)]
MINOR: fcgi-app: Stop to explicitly loop on the proxy's filter_configs list

The "fcgi-app" filter keyword parser and the implicit "use-fcgi-app"
declaration resolution both scanned the filter_configs list of the proxy
by hand to find a previously declared FCGI filter configuration, matching
on the filter id and the application name.

The parsers now use flt_find_conf() with a match callback, the supported
way to find a filter configuration, which walks the right list depending
on the filter mode of the proxy. An explicit "fcgi-app" declaration now
rejects the replacement of another explicit declaration instead of any
one found, the implicit configuration it replaces is removed with
flt_conf_delete(), and the one-fcgi-app-per-backend check of
"use-fcgi-app" resolves the FCGI instance directly with
flt_find_instance() instead of a manual loop on the configurations.

2 weeks agoMINOR: cache: Stop to explicitly loop on the proxy's filter_configs list
Christopher Faulet [Tue, 27 Oct 2026 09:48:00 +0000 (10:48 +0100)]
MINOR: cache: Stop to explicitly loop on the proxy's filter_configs list

The "cache" filter keyword parser and the implicit "cache-store"
declaration resolution both scanned the filter_configs list of the proxy
by hand to find a previously declared cache filter configuration. The
lookup filters were inlined in the loops, matching on the filter id and
the cache name.

The parsers now use flt_find_conf() with a match callback, the supported
way to find a filter configuration, which walks the right list depending
on the filter mode of the proxy. The implicit cache-store instance is
added directly with its class, without a name lookup through the class
registry, and the implicit configuration replaced by an explicit
declaration is removed with flt_conf_delete().

2 weeks agoMINOR: filters: Add a function to find a legacy filter configuration during parsing
Christopher Faulet [Tue, 27 Oct 2026 13:07:10 +0000 (14:07 +0100)]
MINOR: filters: Add a function to find a legacy filter configuration during parsing

flt_find_conf() resolves a filter configuration through the filter mode
of the proxy: from the filter instances in the new mode, from the legacy
configurations otherwise. A directive parsing its own configuration
during the configuration parsing cannot use it: the mode of the proxy is
not known yet, it is deduced later from the implied filters, and in the
new mode the legacy configurations of the filters that registered no
class are ignored after the post-parsing stage.

flt_find_legacy_conf() is the parsing-time counterpart of
flt_find_conf(): it always searches the legacy filter configurations of
the proxy, in declaration order, whatever the filter mode. Like
flt_find_conf(), it filters the candidates by filter id, with an
optional match callback. It must only be used during the configuration
parsing, before the mode is deduced: the check callbacks of the rules
keep resolving their configuration with flt_find_conf(), once the
instances were finalized.

2 weeks agoMINOR: filters: Add a function to remove a filter conf from all proxy lists
Christopher Faulet [Tue, 27 Oct 2026 09:47:21 +0000 (10:47 +0100)]
MINOR: filters: Add a function to remove a filter conf from all proxy lists

A legacy filter configuration is linked in three lists of its proxy: the
global order with the 'list' member and the request and response orders
with the 'by_req' and 'by_res' members. Removing such a configuration
meant unlinking the three members by hand, in every filter supporting an
implicit declaration replaced by an explicit one.

The new flt_conf_delete() helper does it at once, as the counterpart of
flt_conf_add() and flt_conf_insert(). It will be used by the filters
parsing an explicit declaration that replaces an implicit one.

2 weeks agoMINOR: filters: Declare a proxy as using the legacy API before parsing filter directive
Christopher Faulet [Tue, 27 Oct 2026 10:14:17 +0000 (11:14 +0100)]
MINOR: filters: Declare a proxy as using the legacy API before parsing filter directive

The "filter" directive declares a legacy filter configuration on the
proxy being parsed, which selects the legacy filter mode: the
PR_FL_FILTER_LEGACY flag is set on the proxy. Until now the flag was set
at the end of the parsing, after the filter keyword was resolved and its
configuration produced.

The filter keyword parsers may call flt_find_conf() to resolve a
previously declared configuration, or may add an implicit instance with
flt_add_implicit_instance(). Both rely on the filter mode of the proxy
to walk the right configuration list: without the flag they would
inspect the filter instances instead of the legacy filter
configurations. The keyword parser of a filter is called while the
directive is being parsed, so the mode must be known before, not after.

The flag is now set as soon as the "filter" directive is identified, so
the keyword parsers always see the mode their own directive selects.

2 weeks agoMINOR: filters: Warn about ignored legacy filters for a proxy
Christopher Faulet [Tue, 27 Oct 2026 10:15:51 +0000 (11:15 +0100)]
MINOR: filters: Warn about ignored legacy filters for a proxy

A proxy without explicit filter declaration now gets its filter mode
deduced from the filters implied by its other directives. When it implies
filters of both kinds, it uses the filter instance mode and the
class-less ones are ignored: their configurations are legacy filter
configurations with no filter instance, so they are never evaluated and
the features they implement silently stop working on that proxy.

Such a proxy is now reported at startup, with a warning per ignored
configuration, naming the configuration and reminding the two ways to
have it evaluated: declare the filter explicitly with the "filter"
directive, which selects the legacy mode for the whole proxy, or set the
"filter-api downgrade-to-legacy" global option.

The relation between a legacy configuration and the instances of its
filter relies on the callbacks table of the filter: a filter supporting
both modes produces configurations with the same table from its legacy
keyword parser and from its class parse callback (see
flt_parse_instance_legacy()), so an instance with the same table means
the filter declared its class. The check runs after the instances are
finalized, when their configurations exist, and before the class
references are released by the flattening.

All the filters supported in-tree register their classes, so only the
third-party filters relying on an implicit declaration are concerned.
The documentation is updated accordingly, in the filters internals guide
and in the configuration manual.

2 weeks agoMINOR: filters: Deduce the default filter API from the implied filters
Christopher Faulet [Mon, 26 Oct 2026 10:16:59 +0000 (11:16 +0100)]
MINOR: filters: Deduce the default filter API from the implied filters

Until now, a proxy with no explicit filter declaration (neither "filter" nor
"filter-*" directives) always used the new filter API, except when the
legacy API was globally forced with "filter-api downgrade-to-legacy". But a
third-party filter relying on an implicit declaration only, and not
registering a filter class, was silently ignored in the new API: such a
filter declares a legacy filter configuration, without any class, hence
without any instance.

Now the API used by a proxy without explicit filter declaration is deduced
from what its other directives imply. A proxy with filter configurations and
no filter instance uses the legacy API: the implied filters declared no
class, so they are invisible to the new API. A proxy with filter instances
uses the new API, where the class-less filters it may also imply are
ignored. The explicit declarations and the "filter-api downgrade-to-legacy"
option keep precedence, as before.

All the filters supported in-tree register their classes and are
instantiated in the new API, so they are unaffected: a proxy implying only
in-tree filters always has instances, and keeps using the new API.  The
change only makes the class-less third-party filters work again without the
global downgrade, and it is the first step towards making the new API the
only one.

The documentation is updated accordingly, in the filters internals guide and
in the configuration manual, for the "filter-api" option and the filter mode
selection.

2 weeks agoMINOR: filters: Set the class flags on filter class registration
Christopher Faulet [Mon, 26 Oct 2026 09:41:57 +0000 (10:41 +0100)]
MINOR: filters: Set the class flags on filter class registration

The classes registered with filter_register_class() or
filter_register_class_full() could not set their flags, in particular
FLT_CLS_FL_MULTI: the flags passed to flt_init_class() were always 0, so a
class of an external module was always limited to a single instance per
proxy, even with an id.

filter_register_class() and filter_register_class_full() now take a
'flags' argument, passed to flt_init_class() as for the internal classes.
The class flags are the FLT_CLS_FL_* flags, only FLT_CLS_FL_MULTI for now,
set when several instances of the class are allowed in the same proxy.

The filters documentation is updated accordingly, in the class
registration section and in the filter instance mode conversion guide.
The example in the comment of filter_init_classes() is updated too.

2 weeks agoBUG/MINOR: hlua: unlock Patref before add_bulk type error
Kelly Kane [Sun, 25 Oct 2026 01:03:03 +0000 (01:03 +0000)]
BUG/MINOR: hlua: unlock Patref before add_bulk type error

Patref:add_bulk checks table entries while holding the pattern reference
write lock. luaL_checktype() raises on a non-string entry and bypasses
the unlock, blocking later updates. Check types without raising while
locked, then unlock before reporting the original Lua error.

Signed-off-by: Kelly Kane <kelly@hawknetworks.com>
It should be backported to 3.2.

2 weeks agoBUG/MEDIUM: listener: enforce rate-limit sessions across thread groups
Manu Nicolas [Tue, 20 Oct 2026 11:29:55 +0000 (11:29 +0000)]
BUG/MEDIUM: listener: enforce rate-limit sessions across thread groups

Since session rates are accounted per thread group, the "rate-limit
sessions" check sums, for each group, the remaining budget computed
against the full configured limit. A frontend therefore keeps accepting
connections as long as any group stays below the limit:

  - with evenly loaded groups, the frontend accepts up to the limit
    multiplied by the number of thread groups;
  - when some groups receive no traffic, e.g. with "bind ... thread
    1/all" and several thread groups, the limit is never enforced.

No explicit thread group setting is needed to be affected. Since 3.3,
the default "performance" cpu-policy creates one thread group per CCX
when neither "nbthread" nor "thread-groups" is set, and binds are
spread over all groups. On machines with several CCX, a frontend with
no thread-related setting may thus accept several times its configured
session rate. Setting "thread-groups 1" works around the issue.

With "rate-limit sessions 100" and three thread groups, a local test
measured about 270 sessions/s on a bind using all groups, and more than
21000 sessions/s on a bind restricted to the first group.

The limit is meant to apply to the frontend as a whole. Let's sum the
per-group session totals first and compare the result with the limit,
both for the number of sessions that may still be accepted and for the
delay before the next one. Two helpers, proxy_fe_sess_remain() and
proxy_fe_sess_delay(), now do this for all callers. The same test then
measures 100 sessions/s in both cases.

A reg-test is provided: with a bind restricted to the first of two
thread groups and a limit of one session per second, the second session
must wait for the rate to drop and see fe_sess_rate at 1, not 2.

This was introduced in 3.3 by commit 16eb0fab3 ("MAJOR: counters:
dispatch counters over thread groups"), so this must be backported to
3.3.

2 weeks agoBUG/MINOR: peers: forget the remote table when ignoring an unknown key type
Frederic Lecaille [Fri, 23 Oct 2026 16:18:36 +0000 (18:18 +0200)]
BUG/MINOR: peers: forget the remote table when ignoring an unknown key type

Commit 1696cfaa1 ("BUG/MEDIUM: peers: enforce check on incoming table key
type") makes peer_treat_definemsg() ignore a table definition with an
unknown key type. But contrary to all other paths ignoring a definition,
it leaves p->remote_table set to the local table matched by name. The
update messages which follow are thus not ignored but decoded using the
local table's key type and applied to it, while they were built for a
different key type. This is precisely the situation the commit wanted to
cover (a newer peer supporting new key types).

Let's reset p->remote_table as the other paths do.

HAProxy never emits such a value: peer_net_key_type[] only covers the five
supported key types, so a compliant peer stays below PEER_KT_TYPES. This is
therefore latent today, and becomes reachable the day a new key type is
added, when a newer peer talks to an older one in a mixed-version cluster.

This must be backported to all stable versions.

2 weeks agoBUG/MINOR: peers: don't use remote table definitions from a previous session
Frederic Lecaille [Fri, 23 Oct 2026 16:01:06 +0000 (18:01 +0200)]
BUG/MINOR: peers: don't use remote table definitions from a previous session

The remote table ID and the list of data types announced by the remote
peer in table definition messages are stored in the shared table and are
never reset when the session ends. Since remote_id defaults to 0, a table
switch message designating table ID 0 selects any table the peer never
defined, and after a reconnection, a table switch message referencing a
table ID defined during the previous session selects it with the previous
session's data types, without the checks performed on definition messages
(e.g. the key type).

In both cases the update that follows is still decoded: the entry key is
inserted into the local table even when no data type is announced, so such
a peer can create entries carrying only a key and no value. On a small
table this can evict legitimate entries.

Let's reset the remote definitions when the session ends, and never match
table ID 0 in switch messages.

Reproducibility: both issues require a received "stick-table switch"
message (type 131). HAProxy has never emitted this message: since the
peers protocol 2.0 was introduced in 2015 (b3971ab06), the function in
charge of building it, peer_prepare_switchmsg(), actually emits a table
definition message (type 130), and doc/peers.txt documents type 131 as
unused. However, the message is part of the documented protocol and its
type is still assigned, so a third-party or modified implementation may
well emit it, possibly in good faith, since its name and semantics
describe exactly a table selection by ID. It is therefore reachable by a
non-HAProxy peer, but not by any version of HAProxy itself.

Note that the statement in the original report about a conforming peer
which "always defines a table before switching to it" does not hold
either, as HAProxy never switches at all.

This must be backported as far as 3.0.

2 weeks agoBUG/MINOR: peers: compare the received expiration date as unsigned
Frederic Lecaille [Fri, 23 Oct 2026 15:39:13 +0000 (17:39 +0200)]
BUG/MINOR: peers: compare the received expiration date as unsigned

The expiration date received in timed update messages is an unsigned
32-bit value on the wire, but it is stored into an int and compared with
the table's expire as a signed value. A value of 2^31 or more thus becomes
negative, is not capped to the table's expire, and results in an entry
expiring in the past, hence purged immediately, instead of being capped to
the table's expiration delay.

Let's compare it as unsigned.

Note that only a buggy or malicious peer sending such a value can reach this: a
compliant peer always sends tick_remain() of its own entry, which cannot
exceed its table's expire and is therefore below 2^31. The effect is
limited to the entry carried by the message, which never lives: it is
purged at the next expiration pass. But since peers are meant to keep
identical tables on every node, this lets such a peer make a chosen key
disappear prematurely on its neighbours, which is a divergence rather
than a local drop.

This can be backported to all stable versions.

2 weeks agoBUG/MINOR: peers: account incoming sessions in the per-thread applet counters
Frederic Lecaille [Fri, 23 Oct 2026 09:46:36 +0000 (11:46 +0200)]
BUG/MINOR: peers: account incoming sessions in the per-thread applet counters

The per-thread applet counters of a peers section are used to pick the
least loaded thread when creating an outgoing peer session. They are
incremented in peer_session_create() for outgoing sessions only, but
decremented in __peer_session_deinit() for all sessions, including
incoming ones. Each incoming session thus makes the counter of its thread
drift towards negative values, and outgoing sessions then tend to be
created on the threads which handled incoming ones, defeating the load
balancing.

Must be backported as far as 3.0.

2 weeks agoBUG/MINOR: peers: reject over-long varints in intdecode()
Frederic Lecaille [Fri, 23 Oct 2026 08:32:21 +0000 (10:32 +0200)]
BUG/MINOR: peers: reject over-long varints in intdecode()

intdecode() shifts continuation bytes by 7 more bits for as long as they
have their stop bit set, without any upper bound on the shift count. A
64-bit value never needs more than 10 bytes, i.e. a shift of 60, but a
malformed message can carry a varint whose 10th continuation byte would
be shifted by 67. Shifting a 64-bit value by 64 or more is undefined
behavior, reported by UBSan, and yields a garbage value.

Check the shift count along with the end of buffer before consuming the
byte, so that such an encoding is rejected. Values up to 2^64-1, which is
encoded on exactly 10 bytes, are still accepted.

Only a buggy or malicious peer sending a malformed varint can reach this:
a compliant peer never emits more than 10 bytes, and peer sessions are
expected to run on trusted networks. The decoded value is later
range-checked against the message bounds, so no memory safety issue is
known; this is UB hardening.

This is long-standing and can be backported to all stable versions.

2 weeks agoBUG/MINOR: peers: don't lose the peer's version when the hello is fragmented
Frederic Lecaille [Thu, 22 Oct 2026 09:34:58 +0000 (11:34 +0200)]
BUG/MINOR: peers: don't lose the peer's version when the hello is fragmented

The peers applet parses the protocol version from the first line of the
hello message in the GETVERSION state. However, maj_ver and min_ver are
local variables which are reset on each pass through the handler's state
machine.

If the host or peer line is not available yet, the handler returns and
resumes later in GETHOST or GETPEER, with the version lost. As a result,
PEER_F_DWNGRD is not updated and retains its previous value. A peer
announcing protocol version 2.0 may then receive timed updates it does not
support, while a 2.1 peer may receive updates without expiration.

Preserve the minor version in appctx->st1 until the peer is identified,
then use it to set or clear PEER_F_DWNGRD and reset st1. This field is
otherwise only used for error status codes, which error paths overwrite
before entering EXIT.

Since GETPEER can only be reached after a valid version has been parsed,
there is no need for an unknown-version sentinel. Remove the now unused
initialization of maj_ver and min_ver.

This has been there since 1.7 with commit 523cc9e85 ("MEDIUM: peers: Fix a
peer stick-tables synchronization issue."). This fix should be backported
to all stable versions.

2 weeks agoBUG/MINOR: peers: forget the appctx when the peer session init fails
Frederic Lecaille [Wed, 21 Oct 2026 16:26:09 +0000 (18:26 +0200)]
BUG/MINOR: peers: forget the appctx when the peer session init fails

peer_session_create() assigns the new appctx to peer->appctx and accounts
it in the per-thread applet counters, then the session is initialized by
peer_session_init() when the appctx first runs. If this initialization
fails (e.g. memory allocation failure), the appctx is freed by
appctx_free_on_early_error() without the release callback being called, so
peer->appctx keeps pointing to freed memory: the sync task then never
tries to reconnect this peer (it believes a session exists), a later
incoming connection from this peer or a soft-stop would call
peer_session_forceshutdown() on the freed appctx, and the applet counter
remains incremented.

Let's make peer_session_init() detach the appctx from the peer on error,
release the applet counter, and wake the sync task up so that a new
connection is attempted once the reconnect timer expires.

This can be backported to all stable versions where peer_session_init()
exists (2.6 and above).

2 weeks agoBUG/MEDIUM: peers: fix byte order of last_get on updates without identifier
Frederic Lecaille [Tue, 20 Oct 2026 14:12:36 +0000 (16:12 +0200)]
BUG/MEDIUM: peers: fix byte order of last_get on updates without identifier

Since commit f12252c7a ("BUG/MEDIUM: peers: Fix update message parsing
during a full resync"), peer_treat_updatemsg() computes the update ID
in a local variable whose byte order depends on the message type.
Explicit identifiers are read in network byte order, while implicit
identifiers are computed in host byte order as st->last_get + 1.

Outside full resynchronization, htonl() is then applied to this variable
in both cases before storing it in st->last_get. This correctly converts
explicit identifiers, since htonl() and ntohl() perform the same
transformation, but incorrectly byte-swaps implicit identifiers on
little-endian machines.

The incorrect identifier is sent in ACK messages and stored by the
sender as st->update. This value initializes the transmission cursors
after a reconnection, so its corruption may cause updates to be skipped
or unnecessarily resent. A subsequent update carrying an explicit
identifier restores the correct last_get value, which can hide the
issue when inspecting the final state after a burst of updates.

This can be reproduced by completing the initial resynchronization,
then sending an UPDATE with identifier 199 followed by an INCUPDATE.
On a little-endian receiver, last_get and the acknowledged identifier
become 3355443200 (0xc8000000) instead of 200. A regression test exercising
this sequence fails without the fix and passes with it.

Convert explicit identifiers to host byte order immediately after
reading them, then store the resulting identifier without conversion.
Leave the full-resynchronization guard unchanged.

This must be backported as far as 3.3.

2 weeks agoBUG/MEDIUM: peers: do not keep a dict entry cached when its update was not sent
Frederic Lecaille [Tue, 20 Oct 2026 12:40:53 +0000 (14:40 +0200)]
BUG/MEDIUM: peers: do not keep a dict entry cached when its update was not sent

When an update message carries a server_key which is not yet in the peer's
dictionary TX cache, peer_prepare_updatemsg() inserts it into the cache and
encodes the full value. But if the message does not fit in the applet's
output buffer, applet_putblk() returns -1 and the message is simply dropped,
to be built again on the next call since last_pushed was not advanced. At
this point the entry is found in the TX cache, so only its ID is emitted,
and the remote peer, which never received the value, resolves this ID to a
NULL (or stale) entry in its RX cache. The server_key of this entry is then
lost on the remote side, as well as for all subsequent updates referencing
the same server for the whole session's lifetime, or it may even point to
another server whose value previously occupied the same slot.

This is easily observed during a resync of a table storing server_key with a
few servers having long names: the first update carrying a new server name
that does not fit in the buffer is retried without its value. With a debug
print on the receiver, ~100 entries out of 400 were received with an unknown
dict ID in such a test.

Let's record the node newly inserted in the TX cache in the update params,
and remove it from the cache when the message could not be emitted, so that
the next attempt sends the full value again.

The issue exists since the dict cache was introduced in 2.0 with commit
8d78fa7de ("MINOR: peers: Make peers protocol support new "server_name" data
type."), so this should be backported to all supported versions.

2 weeks agoBUG/MINOR: deviceatlas: consume a scheduled update before loading it
David Carlier [Tue, 20 Oct 2026 14:59:06 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: consume a scheduled update before loading it

The update flag in the shared map was only cleared after a successful
reload. When the published data file could not be opened, compiled or
loaded, every following request retried it: an unreadable file produced
one alert per request, flooding the logs and lowering the throughput,
and a file failing to compile was compiled again on each request. This
is the case for example since the automatic chroot became the default,
as the data file path is no longer reachable at run time.

The flag is now cleared as soon as the path has been read, so a failing
file is reported once and retried only when the scheduler publishes a
new one. This also avoids losing an update published while the previous
one was being compiled.

This should be backported to lower branches.

2 weeks agoBUG/MINOR: deviceatlas: use the result's property type when printing
David Carlier [Tue, 20 Oct 2026 14:59:05 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: use the result's property type when printing

da_haproxy() took the property type from the atlas catalogue, but a result
may carry a different type for a property, e.g. a client-side property from
the cookie. The typed getter then failed and the value was printed empty.
The type is now read from the result with da_getproptype().

This should be backported to lower branches.

2 weeks agoBUG/MINOR: deviceatlas: ignore the scheduler's update in progress marker
David Carlier [Tue, 20 Oct 2026 14:59:04 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: ignore the scheduler's update in progress marker

The scheduler sets the first byte of the shared map to '0' while it
rewrites the data file path. Any non-zero byte was taken as a pending
update, so a request could read a partial path and emit a spurious
"invalid path" alert. '0' is now treated as no update, as the nginx
module does.

This should be backported to lower branches.

2 weeks agoBUG/MINOR: deviceatlas: refresh the user-agent evidence id after a reload
David Carlier [Tue, 20 Oct 2026 14:59:03 +0000 (15:59 +0100)]
BUG/MINOR: deviceatlas: refresh the user-agent evidence id after a reload

The user-agent evidence id used by da-csv-conv was only computed at init.
Ids belong to the data file, so after a hot-reload of a file with a
different header list, da-csv-conv could pass a stale id and return wrong
or empty properties until restart.

This should be backported to lower branches.

2 weeks agoBUG/MEDIUM: deviceatlas: isolate threads while swapping the reloaded atlas
David Carlier [Tue, 20 Oct 2026 14:59:02 +0000 (15:59 +0100)]
BUG/MEDIUM: deviceatlas: isolate threads while swapping the reloaded atlas

When da_haproxy_checkinst() hot-reloads the atlas, the old instance was
closed and its image freed under dadwsch_lock only, while lookups on other
threads take no lock. With several threads, a request could search a freed
image or a partially copied atlas and crash. In addition, the other threads
spun on the lock during the whole compilation of the new data file, long
enough for the watchdog to kill the process with large files.

The lock is now only tried, so other threads keep serving with the current
atlas during the compilation. Once the new atlas is ready the lock is
released, the swap is done between thread_isolate() and thread_release(),
and the old image is freed afterwards.

This should be backported to lower branches.

v2: release dadwsch_lock before thread_isolate(), which must not be
called with a lock held.

2 weeks agoBUG/MEDIUM: filters: Don't propagate a close while a filter waits on a channel
Christopher Faulet [Fri, 23 Oct 2026 16:33:29 +0000 (18:33 +0200)]
BUG/MEDIUM: filters: Don't propagate a close while a filter waits on a channel

When a filter asks to wait during the analysis of a channel, with
filter.WAIT, all the data it holds are kept in the buffer, waiting to be
processed when it is called back. But a close could be propagated to the
other side in the meantime: when a shutdown or an abort was received on
the channel, the close was forwarded to the peer side, for instance a
backend EOF was immediately forwarded to the client. The stream could
then terminate before the filter resumed, and the held data were lost,
typically a complete response held by lua filters waiting for their
wake-up, delivered with no framing and delimited by the backend close.

So now, when a filter asks to wait, the close is no longer propagated:
like the connection on the request side, the channel keeps the
CF_AUTO_CLOSE flag cleared while a filter waits on it. The flag is
restored by the analyzers on each pass, so the close is simply deferred
until the filter resumes, and nothing changes for the streams without
filters waiting.

The "lua-eof" reg-test, introduced to record this race and marked broken
until it was fixed, now passes and is un-broken. It exercises both the
legacy filter chains and the filter instance mode, with a keep-alive
connection reuse.

This patch should be backported to all supported version after a perido of
observation.

2 weeks agoBUG/MEDIUM: hlua/filters: Never arm the analysis timer for the lua wake-ups
Christopher Faulet [Fri, 23 Oct 2026 14:52:40 +0000 (16:52 +0200)]
BUG/MEDIUM: hlua/filters: Never arm the analysis timer for the lua wake-ups

A lua filter may set a wake-up date from a callback, with
filter.wake_time(), to be called back later to continue its processing.
This date was armed on the analysis timer of the filtered channel. But this
timer is a real timer, shared by all the filters and the analyzers attached
to the channel, and it is single-owner: it must only be manipulated by one
entity at a time, which consumes its own expiry.  Setting a wake-up date on
it was thus wrong. In practice, the date was systematically preserved when
it was not expired, another filter or an analyzer may rely on it, especially
during the message analysis, when the analyzers use it as a blocking timer
(the "pause" action for instance). But the value could be left armed after
the filter was done with the channel: when it expired later, without any
owner, nothing could reset or rearm it. The stream task was then woken up on
the expired timer and looped forever rearming it, a busy loop until the
watchdog killed the process. This may happen long after the filter finished
its processing, for instance a wake-up date armed during the payload
analysis may be left behind when the payload analysis ends, while the
analysis of the other channel is still in progress.

The wake-up date is not a real timer, only an upper bound on the wait
duration: the filter must be woken up at most at this date, earlier is fine,
for instance on a data event. So now it is armed on the stream task
expiration date instead, which is not a real timer either, it is only used
to wake up the task at the lowest armed date. The stream task expiration
date is always recomputed from all its sources, so nothing can be left
behind on it. The lua filters remember nothing.

But a wake-up on the stream task alone does not report any event on the
channels. So, when the task is woken up because a timer expired, the
check_timeouts callback of the lua filters is called: it reports a read
event on the channels filtered by the lua filter when the wake-up date
expired, so its analyzers and its filters are called back. Otherwise, the
wake-up date is re-armed on the stream task expiration date, in case it was
dropped from it.

This way, the analysis timers are only used by their owners, and the lua
filters get their wake-ups without interfering with them. The
http-request/response actions, which also support act.wake_time(), continue
to arm the analysis timer: they are called back by the analyzers of the
ruleset they belong to, which consume their own expiry on this timer.

This patch should be backported to all supported versions.

2 weeks agoBUG/MEDIUM: hlua/filters: Make Channel.may_recv relative to the filter's data
Christopher Faulet [Fri, 23 Oct 2026 07:23:28 +0000 (09:23 +0200)]
BUG/MEDIUM: hlua/filters: Make Channel.may_recv relative to the filter's data

Channel.may_recv() tested the whole channel to know if more data may be
received, ignoring the data visible by the calling filter. But when several
Lua filters are chained on a channel and an earlier one holds back some data
(for instance to rewrite the body), the channel input may be closed while
the current filter still waits for data held upstream. In that case, the
function must return true, because the held data can still be exposed to
the filter, even if nothing will be received on the connection anymore.

Now, like HTTPMessage.eom(), HTTPMessage.may_recv() and
HTTPMessage.closed(), the result is relative to the data visible by the
current filter: the function always returns true if an earlier filter
holds back data.

This patch should be backported to all supported versions.

2 weeks agoBUG/MINOR: hlua/filters: Restrict HTTPMessage.set_eom/unset_eom to visible data
Christopher Faulet [Fri, 23 Oct 2026 07:23:28 +0000 (09:23 +0200)]
BUG/MINOR: hlua/filters: Restrict HTTPMessage.set_eom/unset_eom to visible data

HTTPMessage.set_eom() and HTTPMessage.unset_eom() manipulate the EOM flag
of the whole HTX message, without checking the data visible by the calling
filter. But when several Lua filters are chained on a channel and an
earlier one holds back some data (for instance to rewrite the body), the
end of the message may be reached by this filter while the current one
still waits for data held upstream. In that case, setting the EOM would
duplicate it, and unsetting it could not remove the flag from the data
held by the earlier filter.

Now both functions can only be called when all data of the message are
visible by the current filter, like HTTPMessage.eom(),
HTTPMessage.may_recv() and HTTPMessage.closed(). Otherwise an error is
raised. The error message of HTTPMessage.unset_eom() was also fixed: it
referred to the 'set_eom' function.

This patch should be backported to all supported versions.

2 weeks agoMINOR: hlua/filters: Add HTTPMessage.closed() to check if input is closed
Christopher Faulet [Fri, 23 Oct 2026 07:23:28 +0000 (09:23 +0200)]
MINOR: hlua/filters: Add HTTPMessage.closed() to check if input is closed

There is no way for a Lua filter to know if the input of the HTTP message
it is filtering is closed, i.e. if no more data will be received on the
corresponding channel because the end of the stream was reached or an
error was detected.

HTTPMessage.eom() is not suitable to detect that. It relies on the message
framing and is only true when the end of the message was properly
reported. For instance, when a truncated request is received, no end of
message is reported but the input is nonetheless closed.

HTTPMessage.may_recv() is not suitable either because, as its name
implies, it only tells if more data may still be received at this
point. An unfinished message whose input is closed returns false.

Conversely, HTTPMessage.closed() returns true as soon as the input is
closed, whatever the message framing state. However, as for
HTTPMessage.eom(), the result is relative to the data visible by the
current filter. It is only true when all data were already exposed to
it. So if an earlier filter holds back some data, the function returns
false until these data are visible.

2 weeks agoBUG/MEDIUM: hlua/filters: make HTTPMessage.eom/may_recv relative to the filter's...
Christopher Faulet [Tue, 20 Oct 2026 12:46:51 +0000 (12:46 +0000)]
BUG/MEDIUM: hlua/filters: make HTTPMessage.eom/may_recv relative to the filter's data

HTTPMessage.eom() and HTTPMessage.may_recv() rely on the whole HTX message
to know if more data are expected or not, by testing if the end of message
was reached. However when several Lua filters are chained on a channel and
an earlier one holds back some data (for instance to rewrite the body), the
next ones may perform a test on data it should not see. This could lead to
unexpected behavior, mainly by disclosing too early the end of the message
to next layer.

Now an help function is used to know if all data are visible or
not. HTTPMessage.eom() is always false is all data are not visible by the
current filter and HTTPMessage.may_recv() always true in that case.

This should be backported to all supported versions but it will have to be
adapted because the HTX API was changed in 3.5.

2 weeks agoBUG/MINOR: hlua/filters: Track the payload phase per channel
Christopher Faulet [Thu, 22 Oct 2026 13:32:03 +0000 (15:32 +0200)]
BUG/MINOR: hlua/filters: Track the payload phase per channel

HLUA_FLT_CTX_FL_PAYLOAD is used to know if a filter is in its payload
forwarding stage, when its http_payload or tcp_payload callback is
called, because the data of a channel may only be manipulated in this
context. But this was a per-context flag, shared by both channels,
while the payload phase is specific to a channel. Thus, when a filter
processed the request payload, a forbidden operation could be performed
on the response data while it was analyzed, and reciprocally.

Now the payload phase is tracked per channel, with a dedicated flag for
the request and the response. All the data manipulation functions now
check the flag of the channel on which they operate. The flags are set
or cleared by the payload and analysis callbacks of the corresponding
channel. In particular, start_analyze only clears the flag of the
channel being analyzed, so starting the analysis of a channel no longer
resets the payload phase of the other one.

2 weeks agoBUG/MEDIUM: connection: don't leak the fd of an accepted connection
Christopher Faulet [Thu, 22 Oct 2026 08:12:11 +0000 (10:12 +0200)]
BUG/MEDIUM: connection: don't leak the fd of an accepted connection

When session_accept_fd() fails in conn_prepare(), e.g. because the SSL
connection limit (maxsslconn) is reached, conn_ctrl_init() hasn't run yet
and CO_FL_CTRL_READY isn't set. conn_ctrl_close() then cannot close the fd,
leaving it open. Additional incoming connections on the same listener keep
accumulating leaked fds, even after the SSL connections are released, and
may ultimately exhaust maxsock, also preventing non-SSL frontends from
accepting.

Fix this by closing the fd in conn_ctrl_close() when it hasn't been
registered yet, unless the connection is fdless.

Many thanks to Adam Crawford for the report.

This should be backported to all supported versions.

2 weeks agoBUG/MINOR: h3: do not wait after Tx realloc failure
Amaury Denoyelle [Fri, 23 Oct 2026 09:22:48 +0000 (11:22 +0200)]
BUG/MINOR: h3: do not wait after Tx realloc failure

The following commit implements for QUIC transcoders a realloc operation
when a smallbuf is not sufficient to encode headers. This is available
via qcc_realloc_stream_txbuf() function.

  0d6112b40b20f9eea0cf38dc304bc1155b673588
  MINOR: mux-quic: retry after small buf alloc failure

However, an alloc failure of this function was considered as a transient
error, with 0 returned in this case. Fix this so that it is now a fatal
error which will trigger the connection closure, which is the correct
handling for memory allocation issues in QUIC MUX layer.

This must be backported up to 3.2. Note that the chunk relative to
h3_req_headers_send() is specific for QUIC backend usage and must only
be picked on 3.3 and above.

2 weeks agoBUG/MINOR: h3: do not retry on trailers too large
Amaury Denoyelle [Fri, 23 Oct 2026 08:44:41 +0000 (10:44 +0200)]
BUG/MINOR: h3: do not retry on trailers too large

The following patch adds the ability for the QUIC transcoder to release
a buffer if there is not enough space in it and allocate a new one
instead. This is mostly used during DATA and trailers encoding.

  cd22200d239773a851c6b78e722864fa5103af6a
  MEDIUM: mux-quic: release Tx buf on too small room

For trailers encoding it may cause an infinite loop if the content is
larger than a whole buffer. In this case, transcoder will release and
allocate a new buffer without interruption before the watchdog is
triggered.

Fix this by extending h3_resp_trailers_send(). The buffer is now
released only when it contains residual not yet sent or acknowledged
data. If the buffer is empty, trailers are too large and an error is
returned, which will close the connection with INTERNAL_ERROR code. This
method was in fact already implemented in h3_resp_headers_send().

This must be backported up to 3.0.

2 weeks agoBUG/MEDIUM: h3: prevent conn failure on trailers decoding without space
Amaury Denoyelle [Thu, 22 Oct 2026 13:36:02 +0000 (15:36 +0200)]
BUG/MEDIUM: h3: prevent conn failure on trailers decoding without space

HTTP/3 trailers are decoded in function h3_trailers_to_htx(). If this
fails due to a lack of space, a negative value is returned, resulting in
a connection closed with INTERNAL_ERROR. This is incorrect as it could
have occurred because of data not yet consumed in HTX output buffer.

One solution would be to temporarily block demux when such situation is
encountered. However, it is not easily implementable now since the
introduction of http_trailers_to_htx() which is shared with HTTP/2
parser. A value HTTP_PRS_TOO_MANY can be caused either due to a
transient lack of space. However, it is also used to report a number of
decoded trailers exceeding tune.http.maxhdr configuration which is a
fatal error.

HTTP/2 parser does not have the same problem as it only starts decoding
of trailers when output HTX buffer is empty. This is largely due to the
fact that HPACK decoding cannot be rolled back after a transient error
as it can have side effects related to dynamic table decoding. HTTP/3 is
not concerned as QPACK dynamic table capability is not implemented.

Thus, apply the same approach in HTTP/3: trailers decoding will be
started only after HTX output buffer is emptied. Flag QC_SF_DEM_FULL is
set to temporarily block demux if needed and 0 is returned from
h3_trailers_to_htx(). Decoding will be retried after a successful read
from the stream layer.

This solution is in fact already used for HEADERS when dealing with
intermediaries HTTP responses. Its impact is minimal given that trailers
usage in H3 is rare. Also, it will simplify a future QPACK dynamic table
implementation.

This must be backported up to 2.8.