]> git.kaiwu.me - haproxy.git/log
haproxy.git
2 months agoBUG/MINOR: pattern: do not match a string with an embedded \0 on its prefix
Frederic Lecaille [Fri, 11 Sep 2026 09:50:50 +0000 (11:50 +0200)]
BUG/MINOR: pattern: do not match a string with an embedded \0 on its prefix

pat_match_str() looks the sample up in the pattern tree with
ebst_lookup(), which reads it as a C string and stops at the first \0.
A sample "/public\0/admin" with <data> = 14 was thus matched as
"/public" by the default case-sensitive "-m str" exact match, while the
case-insensitive match, which uses the list, compares the length and
rejected it.

Such samples are produced by converters that decode client data, e.g.
url_dec ("%00") or json_query ("\u0000"), so an allow-list written as
"http-request deny unless { path,url_dec -m str /public }" could be
bypassed with "/public%00/admin". Maps (map, map_str) use the same
matcher and were affected too.

Patterns are loaded as C strings and never contain a \0, so such a
sample cannot be an exact match. Check it once at the top of
pat_match_str() and return no match.

Note that a deny-list such as "-m str /admin" used to reject
"/admin%00zz" by accident and now lets it through, since it is not an
exact match. The has_ctl() converter can be used to reject any decoded
\0.

This is the same issue as the one fixed for the ssl_*_dn() fetches in
commit 449133cba ("BUG/MINOR: ssl: reject an embedded \0 in the
ssl_*_dn(entry) fetches"), fixed here in the matcher so that all
producers are covered.

This must be backported to all stable branches.

Reported-by: Mike Read (@Michael-JRead)
2 months agoBUG/MINOR: cli: Reject payload with no command
Christopher Faulet [Fri, 11 Sep 2026 08:09:40 +0000 (10:09 +0200)]
BUG/MINOR: cli: Reject payload with no command

No error is reported during command line parsing when a payload pattern is
found with no command. It is not really a big deal because when the command
is processed, because there is no command, nothing is executed. It is
harmless.

But, accordingly to the doc, the payload is the last argument of a
command. So a command is required. Thus, an error is now reported when a
payload is received with no command.

This patch could be backported to 3.4. Older versions could benefit of this
patch. If we decide to backport it farther, the patch will have to be
adapted.

2 months agoBUG/MEDIUM: cli: Don't read a command or payload line into a full buffer
Christopher Faulet [Fri, 11 Sep 2026 09:13:56 +0000 (11:13 +0200)]
BUG/MEDIUM: cli: Don't read a command or payload line into a full buffer

Commit e3c175871 ("BUG/MEDIUM: cli: Don't parse next command if input buffer
is empty") fixed a crash triggered when a command line exactly filling the
buffer was finished by a payload pattern, by no longer trying to parse a
next command when the input buffer was empty.

However this only addressed the case where no more data was available. When
a command line exactly fills the buffer and ends with a payload pattern, the
payload buffer is created with a size of 0 (no room left). If payload data
is then received on a subsequent read, the loop was not interrupted and
b_getline() was called with a length of 'b_room(buf) - 1'. Because b_room()
returns a size_t, an empty buffer made this length wrap around to SIZE_MAX,
and the line was copied past the end of the trash chunk, leading to a buffer
overflow.

To fix the issue, no attempt to read a command or a payload line is
performed when there is no room left in the destination buffer. In that
case, we fall back to the existing path that reallocates a bigger (dynamic)
payload buffer, which is guaranteed to be given a non-zero size, or reports
an error for the command line. This way b_getdelim() and b_getline() are
never called on a full buffer and the size_t underflow can no longer happen.

On older versions, 1-byte OOB write is also possible, but with a lower
severity.

This patch must be backported to 3.4, along with commit e3c175871. And it
should be adapted and backported to all other stable versions.

2 months agoBUG/MEDIUM: cli: Don't parse next command if input buffer is empty
Christopher Faulet [Fri, 11 Sep 2026 07:51:11 +0000 (09:51 +0200)]
BUG/MEDIUM: cli: Don't parse next command if input buffer is empty

In the loop of the command line parser, we always tried to get the next
command, even when the input buffer was empty. However it was an issue if
the size of command line was exactly the buffer size and it was finished by
a payload pattern. In that case, an attempt to allocate a dynamic buffer for
the payload was performed with a size of 0, which led to a crash.

To fix the issue, the loop is now interrupted with there is no more data to
parse in the input buffer. This way, when an dynamic buffer must be
(re)allocated, we are sure its size will never be 0. To be sure it is true,
a BUG_ON() was added on the new dynamic buffer size.

To mitigate the issue, "tune.cli.max-payload-size" parameter can be set to
0.

This patch must be backported to 3.4.

2 months agoREGTESTS: uri_normalizer: test a normalizer used with a condition
Youngkwang Lee [Wed, 9 Sep 2026 08:31:04 +0000 (17:31 +0900)]
REGTESTS: uri_normalizer: test a normalizer used with a condition

The existing tests always use the normalizers unconditionally, so the
selection of the normalizer when a condition directly follows a
normalizer whose sub-option is omitted was not covered.

The request uses both a percent sequence and duplicate slashes so that
the expected result differs depending on which normalizer is picked:
percent-decode-unreserved leaves the duplicate slashes alone, while
path-merge-slashes, which was wrongly selected before the previous
commit, would merge them and leave the percent sequence encoded.

2 months agoBUG/MEDIUM: http_act: fix normalize-uri normalizer selection with a condition
Youngkwang Lee [Wed, 9 Sep 2026 08:31:03 +0000 (17:31 +0900)]
BUG/MEDIUM: http_act: fix normalize-uri normalizer selection with a condition

The normalizers that accept an optional sub-option ("strict" for
percent-to-uppercase and percent-decode-unreserved, "full" for
path-strip-dotdot) only set rule->action in the branch handling the
sub-option and in the branch handling the end of the arguments. When
the sub-option is omitted and an "if" or "unless" keyword directly
follows the normalizer name, none of these branches is taken and
rule->action is left untouched.

As a result the rule ends up with action 0, which is
ACT_NORMALIZE_URI_PATH_MERGE_SLASHES, and a configuration such as:

    http-request normalize-uri percent-decode-unreserved if { ... }

silently merges duplicate slashes instead of decoding percent
sequences. This is easily missed because the rule still does
something, and for a URI without duplicate slashes the request is
left unchanged, which looks like the rule was simply skipped.

Set the default action right after the normalizer name is matched and
let the sub-option overwrite it. The check rejecting unknown arguments
is kept, guarded by a test on the argument being non-empty.

This should fix GitHub issue #3493.

This may be backported to all stable versions where the affected
normalizers exist. The three of them were introduced in 2.4-dev by
commits 560e1a635 ("MINOR: uri_normalizer: Add support for supressing
leading `../` for dotdot normalizer"), a40719337 ("MINOR:
uri_normalizer: Add a `percent-upper` normalizer") and 2e4a18e04
("MINOR: uri_normalizer: Add a `percent-decode-unreserved`
normalizer").

Signed-off-by: Youngkwang Lee <ykglory0216@gmail.com>
2 months agoREGTESTS: update trailers tests to cover H1's refusal of forbidden names
Willy Tarreau [Wed, 9 Sep 2026 09:46:03 +0000 (11:46 +0200)]
REGTESTS: update trailers tests to cover H1's refusal of forbidden names

Let's extend the trailers test to make sure that forbidden trailer names
are now blocked in H1.

2 months agoMEDIUM: h1: switch to the generic trailers parser
Willy Tarreau [Wed, 9 Sep 2026 11:40:52 +0000 (13:40 +0200)]
MEDIUM: h1: switch to the generic trailers parser

Let's now use http_trailers_to_htx() instead of htx_add_all_trailers()
to parse HTTP/1 trailers. Upper case is permitted and thats the only
difference with H2/H3 for now.

The parser is stricter than the previous function, so the following
names are now rejected as it is for other versions:

  "host", "content-length", "connection", "proxy-connection"
  "keep-alive", "upgrade", "te", "transfer-encoding"

2 months agoMINOR: http-htx: condition uppercase acceptance in trailer names to HTTP_PF_UPCASE_OK
Willy Tarreau [Thu, 10 Sep 2026 14:07:54 +0000 (16:07 +0200)]
MINOR: http-htx: condition uppercase acceptance in trailer names to HTTP_PF_UPCASE_OK

The trailers parser currently rejects uppercase chars in the name.
Let's add a new flag to indicate that they are OK. This will be used
by HTTP/1. However this is not sufficient, we now need to use isteqi()
instead of isteq() to compare header names.

Neither H2 nor H3 need to pass that new flag, hence why the change is
minimal.

2 months agoMINOR: http-htx: pass extra flags to the HTTP trailers parser
Willy Tarreau [Thu, 10 Sep 2026 14:05:24 +0000 (16:05 +0200)]
MINOR: http-htx: pass extra flags to the HTTP trailers parser

Let's introduce HTTP parser flags (HTTP_PF_*) and rely on them for the
trailers parser. This one will need them to decide to enable upper case
characters or not in header names in order to reuse it for H1 parsing.

For now we only pass the direction. The current parser doesn't use it
but the goal is to generalize the mechanism to other parsers in the
future.

2 months agoREGTESTS: http-messaging: add tests for the H1 trailers
Willy Tarreau [Wed, 9 Sep 2026 07:11:01 +0000 (09:11 +0200)]
REGTESTS: http-messaging: add tests for the H1 trailers

The Transfer-Encoding test is extended to cover the HTTP/1 trailers,
both on the request and on the response path.

Valid trailers are checked to be forwarded in both directions. vtest is
not able to parse HTTP/1 trailers (it stops on the CRLF following the
last chunk), so they are read using "recv" and only the number of bytes
emitted by haproxy is verified. This is sufficient to prove that the
spaces surrounding the values were trimmed, since it changes the length,
and a second trailer-less message is exchanged over the same connection
to make sure nothing was emitted after the trailers.

The trailers rejected by the H1 parser are checked to result in a 400 on
the request path and in a 502 on the response path: invalid token in the
field name, missing colon, pseudo-header, empty field name and a CR not
followed by an LF in the value. Note that each message must be emitted
at once, otherwise haproxy has already forwarded the beginning of the
message when the invalid trailers are parsed and can only abort the
stream instead of responding with an error.

2 months agoBUG/MINOR: stream: resolve foreign groups' addresses for "show sess"
Olivier Houchard [Wed, 9 Sep 2026 16:38:30 +0000 (18:38 +0200)]
BUG/MINOR: stream: resolve foreign groups' addresses for "show sess"

A stream in another group can't have its addresses resolved from the CLI
thread when using per-thread group file descriptor tables, as
getsockname/getpeername need the owner group's FD table. Ask the owning thread
to resolve a batch and cache them in the connections, picked up when the dump
resumes.

2 months agoBUG/MEDIUM cli: dump other thread groups' FDs from their own threads
Olivier Houchard [Thu, 10 Sep 2026 16:43:00 +0000 (18:43 +0200)]
BUG/MEDIUM cli: dump other thread groups' FDs from their own threads

"show fd" data (getsockname/getpeername) is only valid in the FD's owner
group. With split FD tables, dump each foreign group's FDs from one of
its own threads via twork, one buffer at a time, forwarded to the client.

2 months agoMINOR: fd: add fd_tables_are_split() to test for split FD tables
Olivier Houchard [Thu, 10 Sep 2026 16:42:59 +0000 (18:42 +0200)]
MINOR: fd: add fd_tables_are_split() to test for split FD tables

Non-zero when FD tables are split per thread group, i.e. an FD is only
usable from within its owner group.

2 months agoMEDIUM: listener: make the rx_agent use the generic twork API
Olivier Houchard [Thu, 20 Aug 2026 16:34:45 +0000 (18:34 +0200)]
MEDIUM: listener: make the rx_agent use the generic twork API

Rework the rx_agent's per-group tasklet to use the new twork facility,
instead of hand-rolling our own.

2 months agoMEDIUM: twork: Introduce a new cross-thread/thread-group deferred work API
Olivier Houchard [Thu, 20 Aug 2026 16:34:45 +0000 (18:34 +0200)]
MEDIUM: twork: Introduce a new cross-thread/thread-group deferred work API

Introduce twork, a way to execute code on another thread, or another
thread group. The main motivation for that was to be able to obtain
information about file descriptor belonging to other thread groups, when
running with per-thread-group file descriptor tables.
The functions are :
int twork_call_on(struct twork *tw, int tid,
                  union twork_arg (*fn)(union twork_arg),
                  union twork_arg arg,
                  void (*done)(union twork_arg arg, union twork_arg ret));
Runs function fn on thread tid, providing arg as an argument, and
calling done from the caller thread when the work is complete.
int twork_call_tgroup(struct twork *tw, uint tgid,
                      union twork_arg (*fn)(union twork_arg),
                      union twork_arg arg,
                      void (*done)(union twork_arg arg, union twork_arg ret));
Same thing, but running on thread group tgid instead.

"done" is optional, if there is no need to be notified that the work is
complete.
"tw" is optional, if none is provided, one will be allocated
dynamically. Note that you can only cancel a twork if you're using one
that you provided by yourself.

int twork_cancel(struct twork *tw);
Cancels the tw, assuming it is not already running. It will return 1 on
success, and 0 on failure, in which case the work will be completed, and
the "done" function will be called.

2 months agoBUG/MEDIUM: listener: Don't drop suspend/resume deferred requests
Olivier Houchard [Tue, 1 Sep 2026 16:45:53 +0000 (18:45 +0200)]
BUG/MEDIUM: listener: Don't drop suspend/resume deferred requests

When using per-thread group file descriptors, we have to send messages
to other thread groups to tell them to suspend or resume listeners. Add
a current status for each thread group, instead of looking for the
global listener state, to know the current state for each thread group,
otherwise a rapid succession of suspend/resume could lead to an
inconsistant state.

2 months agoBUG/MINOR: cli: Make sure two instances of _getsocks don't run
Olivier Houchard [Thu, 3 Sep 2026 12:57:14 +0000 (14:57 +0200)]
BUG/MINOR: cli: Make sure two instances of _getsocks don't run

It should never happen, but two instances of _getsocks could run at the
same time, and cause a deadlock, so make sure that never happens by only
allowing one at the same time.

2 months agoREGTESTS: map: check map_regm inside an ACL over an iterating fetch
Frederic Lecaille [Thu, 10 Sep 2026 13:37:17 +0000 (15:37 +0200)]
REGTESTS: map: check map_regm inside an ACL over an iterating fetch

This test covers the bug fixed by this commit:

    7646c657a BUG/MEDIUM: map: do not overwrite the fetch context in pat_match_regm()

It uses map_regm in an ACL over req.hdr() with no occurrence. The first
value does not match, so the ACL runs a second iteration of the fetch,
which crashed the process before the fix. A request with two header
values checks that the fetch cursor survives the first iteration.

Thank you to @Michael-JRead for having produced the first version of this reg
test.

This must be backported with the fix above.

2 months agoBUG/MEDIUM: map: do not overwrite the fetch context in pat_match_regm()
Frederic Lecaille [Thu, 10 Sep 2026 12:57:30 +0000 (14:57 +0200)]
BUG/MEDIUM: map: do not overwrite the fetch context in pat_match_regm()

pat_match_regm() stored the address of the thread-local <pmatch> array
into <smp->ctx.a[0]> so that sample_conv_map() could read the capture
offsets back for exp_replace(). But this slot belongs to the sample
fetch, which uses it to keep an iteration cursor.

When map_regm is used inside an ACL over an iterating fetch (for example
req.hdr() with no occurrence), acl_exec_cond() re-runs the fetch to get
the next occurrence without clearing <smp->ctx>. On the second pass the
fetch reads back the <pmatch> pointer as its own cursor and dereferences
regex offsets as a pointer, which crashes the process. A single client
request is enough to trigger it.

Do not alias the slot: sample_conv_map() reads the thread-local <pmatch>
directly, since nothing runs another regex between pattern_exec_match()
filling it and this read. This is how http_replace_hdrs() and the
replace-path action already use <pmatch>.

Thank you to @Michael-JRead for having reported this issue.

This bug was introduced in 1.7 by commit 8feaa661b ("MINOR: map: Add
regex matching replacement").

This must be backported to all stable versions.

2 months agoBUG/MINOR: ssl: don't pass the address of a size_t as an int* to OpenSSL
Remi Tricot-Le Breton [Thu, 6 Aug 2026 07:35:00 +0000 (09:35 +0200)]
BUG/MINOR: ssl: don't pass the address of a size_t as an int* to OpenSSL

aes_process() passes &out->data, which is a size_t, to the EVP update and
final routines through an (int*) cast. OpenSSL writes an int there, so on
a 64-bit big-endian platform (s390x, ppc64) the resulting length lands in
the high half of the size_t, and the buffer's data count becomes the real
length shifted left by 32 bits. The subsequent write at out->area +
out->data then targets a wild address.

Concretely, on such builds the "aes_gcm_dec" / "aes_cbc_dec" converters
and the JWE decryption paths corrupt memory as soon as they process any
input, so any client able to submit a token to them can crash the worker.
Little-endian builds, which are the vast majority, happen to work by
chance since the count starts at zero and only the low half is written.

Let's use a proper local int for the OpenSSL output length and assign it
back to the buffer's data count.

The shared helper comes from commit f0e64de75 ("MINOR: ssl: Factorize AES
GCM data processing") in 3.4 but the same cast existed in the AES
converters before that. It must be backported to all stable versions.

Reported-by: Claude (ANT-2026-74N8W6SJ)
2 months agoMAJOR: startup: use chroot auto by default when available
Willy Tarreau [Wed, 9 Sep 2026 07:28:34 +0000 (09:28 +0200)]
MAJOR: startup: use chroot auto by default when available

3.4 implemented "chroot auto", "chroot /", unprivileged chroot and a
warning for super-user when chroot is not used. We were not doing it
for unprivileged users because this would have caused confusion with
configs triggering warnings only on certain OSes or environments.
Now's time to switch to automatic chroot attempt on startup whenever
deemed possible.

For this, we reuse the previous rules to decide to emit a warning when
a chroot is possible but not done, and instead of complaining that no
chroot is done, we just attempt it before emitting the warning only in
case of failure.

This is marked major because it may break some old rare configs that
were emitting the warning in 3.4 due to the lack of chroot, when the
user expected to access for example a UNIX socket at run time, or
using an external check. The fix remains easy (and is explained in
the warning), just use "chroot /".

The change aims at increasing the default security level for users by
default (i.e. with the least config settings), but it's not critical,
so feedback will determine if we keep it as-is, if we adjust it, or if
we revert it.

2 months agoREGTESTS: specify explicit chroot in confs using unix-stream servers
Willy Tarreau [Wed, 9 Sep 2026 16:29:58 +0000 (18:29 +0200)]
REGTESTS: specify explicit chroot in confs using unix-stream servers

For convenience reasons, a small tens of tests make use of unix sockets
on server lines, and as such cannot be chrooted. Given that non-chrooted
startups are frowned upon and even emit a warning for privileged users,
let's explicitly use the recommended "chroot /" for them.

2 months agoSCRIPTS: run-regtests: Increase internal buffer size used for logs
Christopher Faulet [Tue, 8 Sep 2026 14:16:34 +0000 (16:16 +0200)]
SCRIPTS: run-regtests: Increase internal buffer size used for logs

It was set to 2MB. But the script testing the Prometheus exporter is quite
verbose and may exceed this limit, leading to the following error:

  *    diag  0.0 Assert error in vtc_log_emit(), src/vtc_log.c line 179:
  *    diag  0.0   Condition(vtclog_left > l) not true. (errno=0 Success)

To avoid any issue for a while, the limit is now set to 4MB.

This patch should fix the issue #3494.

2 months agoBUG/MEDIUM: http-ana: apply tunnel timeout with data filters
Manu Nicolas [Thu, 3 Sep 2026 14:10:54 +0000 (14:10 +0000)]
BUG/MEDIUM: http-ana: apply tunnel timeout with data filters

The tunnel timeout is installed by process_stream() only once all
analysers are gone. HTTP data filters keep their transfer and end
analysers attached after a protocol upgrade. A stream using, for
example, set-bandwidth-limit therefore remains on its client and server
timeouts even though both HTTP messages are in tunnel mode. A short
client timeout can close a WebSocket before its heartbeat.

Apply the per-stream tunnel timeout when the second HTTP message enters
tunnel mode. Data-filter analysers remain active and continue to process
tunneled data.

It should be backported as far as 2.6.

2 months agoBUILD: fd: Make sure to include sched.h
Olivier Houchard [Tue, 8 Sep 2026 14:00:18 +0000 (16:00 +0200)]
BUILD: fd: Make sure to include sched.h

Commit 5e9662ddd63a730235deec27489c7e8897fea21d made it so that instead
of checking for the existence of CLONE_FILES, we'd define
HA_HAVE_UNSHARE if file unsharing is available. To determine that, it
checks for the existence of CLONE_FILES, however without including
sched.h, it will never be defined, and file unsharing will never be
used.
Fix that by properly including sched.h.

2 months agoBUG/MINOR: h3: only increment frame type counter on new header
Amaury Denoyelle [Thu, 3 Sep 2026 15:55:47 +0000 (17:55 +0200)]
BUG/MINOR: h3: only increment frame type counter on new header

For every new HTTP/3 received frame, a counter related to the frame type
is incremented. This operation is performed directly in h3_rcv_buf()
parsing loop.

Prior to this patch, increment would be called too many times when
dealing with large frames. Fix this by ensuring that the increment is
only reported once per frame. This is now performed directly after
HTTP/3 frame header parsing.

This must be backported up to 2.6.

2 months agoBUG/MINOR: h3: handle unknown frame type on request stream as specified
Amaury Denoyelle [Fri, 4 Sep 2026 07:55:36 +0000 (09:55 +0200)]
BUG/MINOR: h3: handle unknown frame type on request stream as specified

HTTP/3 specification allows the ability to define extra frame types. An
implementation must silently ignored unknown frame types.

Prior to this patch, haproxy stack completely discards an unknown frame.
However, on a request stream, if this acts as the last stream frame, it
would lead to a missing HTX EOM before the stream is closed on timeout.

Now, an HTX EOM is properly reported to the upper layer, which ensures
the request can successfully be achieved. A reset is emitted though if
no headers were previously received as in this case the HTTP request is
incomplete.

This must be backported up to 2.6.

2 months agoBUG/MINOR: h3: handle empty HEADERS frame as specified
Amaury Denoyelle [Thu, 3 Sep 2026 15:20:30 +0000 (17:20 +0200)]
BUG/MINOR: h3: handle empty HEADERS frame as specified

This patch ensures empty HEADERS frame are treated as expected by
rfc9114.

In some conditions, such frame is invalid and the stream is closed with
FRAME_UNEXPECTED h3 error. This is the case if this is the first frame
interpreted as header as it lacks mandatory pseudo-headers. This is also
the case if trailers were already received, but this case is already
handled by a previous patch.

The only condition in which an empty HEADERS frame may be valid is when
it is interpreted as trailers. QUIC FIN may be signalled in the same
occasion, thus h3_trailers_to_htx() has been extended to add a call to
htx_set_eom() to report the stream closure to the upper layer.

This is considered minor as it's doubtful that any H3 client will use an
empty HEADERS frame.

This must be backported up to 2.6.

2 months agoBUG/MEDIUM: h3: do not block FIN on empty DATA frame
Amaury Denoyelle [Thu, 3 Sep 2026 15:20:15 +0000 (17:20 +0200)]
BUG/MEDIUM: h3: do not block FIN on empty DATA frame

A HTTP/3 DATA frame may be empty as its payload is entirely optional.
However, prior to this patch, H3 stream parsing was interrupted in this
case, unless other frames follow. The main problem is that it would lead
to a missing EOM if QUIC FIN is reported with an empty DATA frame,
causing the stream to freeze until timeout.

The first step of this patch is to first remove the early return for
empty HTTP/3 frame payload. This has the effect to allow calling the
different frame handlers with an empty payload. The current patch is
dedicated to HTTP/3 DATA support; other patches will follow to ensure
the safety of the other frame types handlers.

The second part of this patch is to adjust h3_data_to_htx() which is the
HTTP/3 data frame handler. It is now safe to call it with an empty
frame, with or without FIN. It will default to a noop operation, unless
FIN is set, in which case htx_set_eom() is used to report the closure of
the stream read channel.

This must be backported up to 2.6.

2 months agoBUG/MINOR: h3: reject truncated frames with mandatory payload
Amaury Denoyelle [Thu, 3 Sep 2026 15:19:47 +0000 (17:19 +0200)]
BUG/MINOR: h3: reject truncated frames with mandatory payload

Most of HTTP/3 control frames contain a fix set of fields. If a frame is
truncated, an implementation must close the connection with
H3_FRAME_ERROR code.

This patch adds some basic checks on CANCEL_PUSH, PUSH_PROMISE and
MAX_PUSH_ID : if the length is null, H3_FRAME_ERROR code is used.

This bug can be considered minor as push is not supported by haproxy.
They were completely ignored before backend support as only a server is
expected to emit them.

This must be backported up to 3.3.

2 months agoMEDIUM: h3: rely on the generic trailers parser
Willy Tarreau [Tue, 8 Sep 2026 12:23:54 +0000 (14:23 +0200)]
MEDIUM: h3: rely on the generic trailers parser

h3_trailers_to_htx() performed exactly the same controls as the generic
http_trailers_to_htx() parser, but was relying on traces and glitches
for each failure case that was codified on the latter parser.

Let's just drop the parsing loop for a direct call to the function and
a check on the return codes to decide what trace/glitch to emit.

It's worth noting that H3_ERR_MESSAGE_ERROR is not set when failing to
add a header, not sure whether this is a design decision or an overlook.
In any case this behavior was preserved.

Note that pseudo-headers are detected on their syntax here, while h2
used to detect them on their assigned number, so this remains a tiny
difference between the two parsers at this point.

Regtests continue to pass.

2 months agoCLEANUP: h2: get rid of the now unused h2_make_htx_trailers()
Willy Tarreau [Tue, 8 Sep 2026 12:09:18 +0000 (14:09 +0200)]
CLEANUP: h2: get rid of the now unused h2_make_htx_trailers()

It's no longer used, let's get rid of this function.

2 months agoMINOR: mux-h2: report more accurate traces and glitches on trailers errors
Willy Tarreau [Tue, 8 Sep 2026 08:03:20 +0000 (10:03 +0200)]
MINOR: mux-h2: report more accurate traces and glitches on trailers errors

Now that we're http_trailers_to_htx(), let's benefit form the detailed
return code to emit a more accurate set of traces and glitches.

2 months agoMEDIUM: mux-h2: rely on the generic trailers parser
Willy Tarreau [Tue, 8 Sep 2026 08:03:20 +0000 (10:03 +0200)]
MEDIUM: mux-h2: rely on the generic trailers parser

http_trailers_to_htx() was made from h2_make_htx_trailers(), so it's
expected that we can get rid of the latter in favor of the former to
aim towards a generic parser.

For now we don't exploit the extended return codes, we only check for
HTTP_PRS_SUCCESS to detect parsing failures.

2 months agoREGTESTS: http-messaging: add tests for the H2 and H3 trailers
Willy Tarreau [Tue, 8 Sep 2026 08:03:10 +0000 (10:03 +0200)]
REGTESTS: http-messaging: add tests for the H2 and H3 trailers

Two new tests cover the trailers parsers of the H2 and the H3 muxes, so
that any change to these parsers can be checked against the current
behaviour.

h2_trailers.vtc checks that valid trailers are forwarded with their
leading and trailing spaces removed, on the request as well as on the
response path, and that all the trailers rejected by the parser cause
the stream to be reset: literal and indexed pseudo-headers, upper case
letters and non-token characters in the names, NUL/CR/LF in the values,
and the fields affecting the message framing or the routing.

Since vtest doesn't speak H3, h3_trailers.vtc stacks two HTTP layers in
the same haproxy: the H2 requests emitted by vtest are converted to H3
and sent over a local qmux connection to a second frontend which
converts them back to H2 for the vtest servers. Only valid trailers are
checked there because the invalid ones cannot reach the H3 parser: HTX
lower cases the trailer names and the H3 mux drops the trailers which
are forbidden on the wire.

A few notes about the tricks used in these tests:
  - a trailers frame is emitted using "txreq -noadd", which sends a
    HEADERS frame with ES+EH set and no pseudo-header at all ;
  - trailers vtest cannot encode (a NUL in a value) or decode (an empty
    value as the last field) are crafted by hand with sendhex ;
  - haproxy doesn't ack the settings of an H2 server before it has
    something else to send, so the servers must not wait for that ack ;
  - the window update emitted after the request headers is consumed
    before sending invalid trailers, so that the reset is necessarily
    the next frame to be received ;
  - "option http-buffer-request" makes sure the invalid trailers are
    detected while the stream is still open and that no server is
    involved.

2 months agoMINOR: http-htx: add a new generic trailers parser
Willy Tarreau [Tue, 8 Sep 2026 06:48:53 +0000 (08:48 +0200)]
MINOR: http-htx: add a new generic trailers parser

http_trailers_to_htx() merges h2_make_htx_trailers() and
h3_trailers_to_htx() into a unified function that takes a fields list,
performs controls and adds them to HTX. It returns http_parser_status
codes so that the caller knows what happens. For now the function is
not ready to handle HTTP/1 because it unconditionally rejects upper
case letters in field names.

The parser uses two modes to detect pseudo-headers:
  - syntax-based (it starts with a colon)
  - number-based (an index number replaces the string)

This is because the former is used by h3, the latter by h2.

2 months agoMINOR: http: define new HTTP status codes for parsers
Willy Tarreau [Tue, 8 Sep 2026 06:47:28 +0000 (08:47 +0200)]
MINOR: http: define new HTTP status codes for parsers

The new http_parser_status enum contains codes for HTTP header field
parsers (headers and trailers). This will be used by generic parsers
to return accurate error information on what exactly was invalid.

2 months agoMINOR: ring/cli: also support "show events -h" to display the help
Willy Tarreau [Mon, 7 Sep 2026 14:25:18 +0000 (16:25 +0200)]
MINOR: ring/cli: also support "show events -h" to display the help

Till now it was annoyingly showing "unknown option" which was not much
helpful.

2 months agoMINOR: ring/cli: improve the "show events" parser regarding options
Willy Tarreau [Mon, 7 Sep 2026 14:20:56 +0000 (16:20 +0200)]
MINOR: ring/cli: improve the "show events" parser regarding options

The "show events" parser remains counter-intuitive in that the options
are historically placed after the ring name, making it behave differently
from commonly used tools and other CLI commands.

Let's relax the parser to take any -XXX as a set of option and the only
word not starting with - as the sink name. That way it becomes possible
to parse options before or after the sink name. The doc was updated to
reflect this and present available options as an enumeration rather than
an unreadable wall of text.

2 months agoMINOR: ring: permit to combine options on "show events"
Willy Tarreau [Mon, 7 Sep 2026 13:56:35 +0000 (15:56 +0200)]
MINOR: ring: permit to combine options on "show events"

The "show events" command supports a handful of options starting with
a "-", but only a few combinations were manually checked for (nw and
wn), other ones may not be combined.

This patch changes the parser to permit any combination, continuing to
process them in declaration order, so that -nsw or -rnw work. The doc
was updated.

This also updates the help message which would previously not list all
options by lack of room.

2 months agoMINOR: ring: add "-r" to "show events" to dump in raw mode (undoes "-s")
Willy Tarreau [Mon, 7 Sep 2026 13:47:08 +0000 (15:47 +0200)]
MINOR: ring: add "-r" to "show events" to dump in raw mode (undoes "-s")

In anticipation for a possible future change of the default output which
might eventually become sanitized by default, let's add "-r" which does
the opposite of "-s", and turns the output back to raw. This can be used
by tools trying to be future-proof that do not want to depend on the
default setting.

2 months agoBUILD: fd: unshare is not available on NetBSD
William Lallemand [Mon, 7 Sep 2026 13:39:53 +0000 (15:39 +0200)]
BUILD: fd: unshare is not available on NetBSD

Define HA_HAVE_UNSHARE which tests CLONE_FILES with an exception on
NetBSD which has the CLONE_FILES define for clone() but doesn't provide
unshare().

  src/thread.c: In function â€˜start_extra_tgroup_threads’:
  src/thread.c:244:21: error: implicit declaration of function â€˜unshare’ [-Werror=implicit-function-declaration]
    244 |                 if (unshare(CLONE_FILES) != 0) {
        |                     ^~~~~~~

No backport needed.

2 months agoMINOR: stconn: prefer to replace endpoints rather than detaching them
Willy Tarreau [Fri, 4 Sep 2026 10:07:55 +0000 (12:07 +0200)]
MINOR: stconn: prefer to replace endpoints rather than detaching them

sc_detach_endp() is used to detach an endpoint, after which we either
assign a new one or completely release the object. Thus, it is guaranteed
that an stconn may not be found with no endpoint. However this is true in
the regular code flow, not at the signal level. And running "show threads"
during a retry, or triggering watchdog warnings on it can witness such a
stream executing instructions at the end of sc_detach_endp() before the
new endp is assigned, causing segfaults during dumps as described in
github issue #3490.

The new sedesc is allocated before releasing the old one, so we have
everything to perform a replacement. For this reason, sc_detach_endp()
was now replaced with sc_replace_endp(). It takes an optional new
endpoint to assign. When detaching, we just continue to pass NULL.
But when switching, we pass the newly allocated one and an external
observer may never see a NULL in the variable.

Note that we don't use atomics here (not really needed, and not enabled
without threads anyway). No failure was observed with millions of thread
dumps and millions of dumped streams. But if it were to still happen on
some specific archs, it would be fine to switch to an atomic store (it
was tested and works as well).

This may have to be backported to a few stable versions if it helps
improve the overall reliability during dumps, but let's wait a bit
first.

2 months agoBUG/MINOR: stream/debug: harden the stream dump function regarding signals
Willy Tarreau [Fri, 4 Sep 2026 12:43:15 +0000 (14:43 +0200)]
BUG/MINOR: stream/debug: harden the stream dump function regarding signals

As seen in GH issue #3490, it is sometimes possible to see a stream in
an intermediate state during a dump (due to the dump being called by a
signal handler). The code was already designed in a defensive way for
this, but not sufficiently.

Indeed, while a stream my never be seen in a stable state with NULL in
any of its sedesc, there exist a short period during sc_reset_endp()
where the backend sedesc is NULL for a few nanoseconds, between the
NULL and the new value, and if a dump signal happens in the middle,
the __strm_dump_to_buffer() function will crash.

This patch modifies __strm_dump_to_buffer() so that the sc, sd and conns
are taken for each side when entering the function and used as tests every
time they're about to be dereferenced. This way certain fields will be
omitted whenever such situations happen.

This new version now resists heavy connection retries combined with
1ms warnings and two parallel "show threads" loops..

This should be backported to stable releases. It is likely that it will
not easily apply to all versions, so the changes might more easily be
re-done; the principle consists in conditioning the dereference of
sc->sedesc for each side (which also happens when reading flags).

2 months agoMINOR: mux-h1: loop on HTX blocks using _blk() functions to look for WS key
Christopher Faulet [Thu, 23 Jul 2026 08:58:58 +0000 (10:58 +0200)]
MINOR: mux-h1: loop on HTX blocks using _blk() functions to look for WS key

In h1_search_websocket_key() function, instead of looping on the block
position, we now use functions returning a blocks. This commit is mandatory
for next changes.

2 months agoREORG: htx: Move functions to deal with first block after ones about next block
Christopher Faulet [Thu, 23 Jul 2026 08:35:06 +0000 (10:35 +0200)]
REORG: htx: Move functions to deal with first block after ones about next block

functions htx_get_first_* were moved after htx_get_next_*() ones. There is no
change here but it is mandatory for the next commit. htx_get_first_blk() will
rely on htx_get_next().

2 months agoDOC: htx: Fix comment of htx_expect_more() function
Christopher Faulet [Wed, 8 Jul 2026 08:30:04 +0000 (10:30 +0200)]
DOC: htx: Fix comment of htx_expect_more() function

The comment of htx_expect_more() was inverted from what the function really
does. So let's fix it.

2 months agoCLEANUP: htx: Remove ref to EOM block on HTX_FL_EOM flag comment
Christopher Faulet [Wed, 8 Jul 2026 08:27:27 +0000 (10:27 +0200)]
CLEANUP: htx: Remove ref to EOM block on HTX_FL_EOM flag comment

EOM block was removed since a while, so let's adapt the HTX_FL_EOM flag
comment.

2 months agoMINOR: htx: Remove htx_xfer_blks function
Christopher Faulet [Wed, 8 Jul 2026 08:23:11 +0000 (10:23 +0200)]
MINOR: htx: Remove htx_xfer_blks function

This function was deprecated during the 3.4 dev cycle and is no longer
used. So let's remove it.

2 months agoMINOR: httpclient: Don't manually set HTX_FL_EOM flag after calling htx_xfer()
Christopher Faulet [Wed, 8 Jul 2026 14:22:58 +0000 (16:22 +0200)]
MINOR: httpclient: Don't manually set HTX_FL_EOM flag after calling htx_xfer()

The htx_xfer() function already takes care to copy HTX_FL_EOM flag when it
is required. So there is no reason to still do it in the HTTP client I/O
handler function.

2 months agoBUG/MINOR: flt-http-comp: Rely on htx_set_eom() to restore EOM flag
Christopher Faulet [Wed, 2 Sep 2026 12:45:13 +0000 (14:45 +0200)]
BUG/MINOR: flt-http-comp: Rely on htx_set_eom() to restore EOM flag

A theoritical bug was introduced by the commit 30890fd90 ("BUG/MEDIUM:
flt-http-comp: Block HTX_FL_EOM flag till the end of compression"). On the
normal case, the HTX_FL_EOM flag was restored without checking if the HTX
message was empty or not. It could be an issue. Instead, htx_set_eom()
function must be used.

This patch must be backported with the commit above.

2 months agoBUG/MEDIUM: htx: Don't reset flags when source must be preserved during transfer
Christopher Faulet [Wed, 22 Jul 2026 08:59:57 +0000 (10:59 +0200)]
BUG/MEDIUM: htx: Don't reset flags when source must be preserved during transfer

When htx_xfer() is called with HTX_XFER_KEEP_SRC_BLKS flag, to preserve
blocks from the source HTX message, the source flags must not be reset at
the end of the transfer.

This bug can be triggered with L7-retries are configured with small
buffers. In that case, the HTX_FL_EOM flag is removed from the request
fowarded to the backend multiplexer.

This patch must be backported to 3.4.

2 months agoBUG/MINOR: hlua: Always return nil if there is no data to dup for HTTP messages
Christopher Faulet [Tue, 21 Jul 2026 12:06:26 +0000 (14:06 +0200)]
BUG/MINOR: hlua: Always return nil if there is no data to dup for HTTP messages

In _hlua_http_msg_dup(), we must always return nil if no data was
duplicated. The fact the end of the message was reached or not should not
interfere. So let's remove the extra condition on HTX_FL_EOM flag and always
return nil if no data was duplicated.

This patch could be backported to all supported versions.

2 months agoBUG/MINOR: mux-h1: Return an error on trailers if last chunk is unfinished
Christopher Faulet [Thu, 3 Sep 2026 19:52:38 +0000 (21:52 +0200)]
BUG/MINOR: mux-h1: Return an error on trailers if last chunk is unfinished

On send patch, there were already some tests to detect truncated chunks, but
always when handling an HTX DATA bloc. There was nothing similar on
trailers. So it was possibly to emit end of chunks and start to send
trailers while the last chunk was unfinished.

So it is a bug, but only it could only be triggered if another part is able
to emit invalid HTX message. Unfortunatly it was the case. This patch is
related to the commit 86a4ebc76 ("BUG/MAJOR: h3: reject H3 truncated
frames").

This patch should be backported as far as 3.0. On older versions, the code
is too different.

2 months agoBUG/MEDIUM: hlua: Never release a cosocket applet from the GC
Christopher Faulet [Fri, 4 Sep 2026 07:45:02 +0000 (09:45 +0200)]
BUG/MEDIUM: hlua: Never release a cosocket applet from the GC

The commit 38534d344b ("BUG/MEDIUM: hlua: Be able to garbage collect
uninitialized lua sockets") introduced a regression, leading to UAF
accesses.

Indeed, with a shared lua state, the garbage collector can be run on any
thread. So for a script using cosocket loaded via "lua-load" directive, the
lua socket garbage collector can run on any thread. So here there were two
problems:

  * appctx_free() must only be called by the thread owning the applet. It
    destroys the applet's task and releases the memory to the thread-local
    pool cache. Here it was possible to call appctx_free() from any other
    thread.

  * All other accesses to the applet's context were performed after the xref
    was disconnected. However, once disconnected, the applet is free to be
    released at any instant by the thread owning it, because
    hlua_socket_release() no longer finds any peer. Because the cosocket
    context is part of the applet's context, this was a use-after-free.

So now, the GC only signals the applet it must die and wakes it up, exactly
like hlua_socket_close_helper() already does, and it does so before
disconnecting the xref. This way:

  * The applet is always released by the thread owning it.

  * Because the work is performed before the xref is disconnected, a
    concurrent hlua_socket_release() is stalled on the xref lock and cannot
    release the applet in our back.

Note this also fixes the same issue in hlua_socket_close_helper(), which
still wakes up a possibly uninitialized applet. Closing a lua socket that
was never connected ("core.tcp():close()") should be enough to trigger it.

Thank you to Red Hat and AISLE Research for reporting this.

This patch must be backported to all stable versions containing the commit
38534d344b, so as far as 2.6.

2 months agoDOC: config: clarify tune tune.bufsize doc regarding various limits
Willy Tarreau [Fri, 4 Sep 2026 06:03:36 +0000 (08:03 +0200)]
DOC: config: clarify tune tune.bufsize doc regarding various limits

This mentions the 256 MB hard limit, and also clarifies that it's HTTP
request and response *headers* which are concerned by the bufsize limit.
Also added some cross-references with tune.bufsize.{large,small}.

2 months agoMINOR: quic: implement the AEAD integrity limit
Frederic Lecaille [Thu, 3 Sep 2026 13:10:47 +0000 (15:10 +0200)]
MINOR: quic: implement the AEAD integrity limit

RFC 9001 6.6 requires an endpoint to count the received packets which fail
authentication, and to close the connection with AEAD_LIMIT_REACHED once
their number exceeds the limit of the AEAD in use. This was not implemented.

Add quic_aead_integrity_limit() which returns that limit for each algorithm
tls_aead() may return, and count the failures in qc_pkt_decrypt(). This is the
only place where a packet is decrypted, whatever its encryption level, so the
count covers all the keys of the connection as the RFC asks. An unknown
algorithm gets a null limit, which closes the connection on the first failure:
QUIC must not use one.

With AES-GCM the limit is 2^52 failed packets, which at one million forged
packets per second on a single connection would take more than a century. With
ChaCha20-Poly1305 it is 2^36, about 19 hours at the same rate.

This is not a bug fix, nothing is affected in practice, so there is no hurry.
This may be backported to all the stable versions after some exposure in 3.5.

2 months agoBUG/MINOR: quic: leak of the NEW_TOKEN frame on token generation failure
Frederic Lecaille [Thu, 3 Sep 2026 08:16:59 +0000 (10:16 +0200)]
BUG/MINOR: quic: leak of the NEW_TOKEN frame on token generation failure

In quic_build_post_handshake_frames(), the NEW_TOKEN frame is appended to the
local frame list only after its token is generated. The error path taken when
quic_generate_token() fails frees that list, and therefore never sees the frame.
This only happens on a memory allocation failure, when the AEAD context used to
cipher the token cannot be created.

Append the frame as soon as it is allocated, as done just above for the
HANDSHAKE_DONE frame.

This bug was introduced by commit f627b9272 ("BUG/MEDIUM: quic: always validate
sender address on 0-RTT").

This should be backported as far as 3.0.

2 months ago[RELEASE] Released version 3.5-dev6 v3.5-dev6
Willy Tarreau [Thu, 3 Sep 2026 14:31:26 +0000 (16:31 +0200)]
[RELEASE] Released version 3.5-dev6

Released version 3.5-dev6 with the following main changes :
    - OPTIM: log/tools: switch from function pointer design to dual mode HEX/BIN design for CBOR encoding
    - IMPORT: slz/uslz: support SLZ_FMT_NONE format and force its value to 0
    - MINOR: haload: add -get/-head/-post options to select the HTTP method
    - BUG/MINOR: haload: count timed-out requests as errors, not successes
    - BUG/MINOR: haload: make -e react to request timeouts too
    - MINOR: haload: add -ee to probe and ramp back up after an error
    - MINOR: quic: add tune.quic.sec.stateless-reset-rate
    - BUG/MINOR: haload: split the rate exactly across threads
    - BUG/MINOR: haload: -ee could burst past its floor when many users fail at once
    - CLEANUP: haload: factor out the per-thread rate share computation
    - CLEANUP: haload: factor out two more duplicated -ee blocks
    - MINOR: haload: keep old-scheduled users under -ee's floor rate
    - BUILD: ssl: avoid a potential null-dereference warning on OpenSSL 1.0.2
    - BUG/MEDIUM: mux-h1: close the connection on a short content-length
    - MEDIUM: mux-h2: don't report EOM on a short content-length message
    - BUG/MEDIUM: connection: fix an infinite loop in the fc_pp_tlv() fetch
    - BUG/MEDIUM: sink: do not hold the sft lock around ring_dispatch_messages()
    - MEDIUM: threads: always abort when calling thread_isolated() under a lock
    - MEDIUM: ring: attach readers under the tail lock instead of isolating
    - BUG/MEDIUM: cache: ignore cache on redundant origin/referer
    - BUG/MINOR: fcgi-app: allow explicit filter declaration with non-cache/non-compression filters
    - BUG/MINOR: flt-http-comp: Don't read next block to detect end of data
    - BUG/MEDIUM: flt-http-comp: Block HTX_FL_EOM flag till the end of compression
    - BUG/MEDIUM: sink: initialize the settings of the implicit log server
    - BUG/MEDIUM: sink: do not create one implicit ring per logger copy
    - CLEANUP: sink: initialize and finalize the implicit forward proxy
    - MINOR: haload: add -T to add a think time after each response
    - MINOR: haload: always track and resend cookies per user
    - BUG/MINOR: haload: fix hld_new_usr()'s error path
    - MINOR: haload: add -P to report ttfb/ttlb percentiles
    - BUG/MINOR: cache: Manage collisions on primary key
    - BUG/MINOR: cache: Seed cache primary hash
    - OPTIM: server: remember the lowest known unused server ID
    - BUG/MEDIUM: mux-spop: Properly handle parsing of split HELLO/DISCONNECT frames
    - MEDIUM: servers: Do not allow reverse HTTP with per-tg fd tables
    - BUG/MEDIUM: server: Fix server-template with default-server
    - MINOR: buf: make b_rep_blk() wrapping-proof
    - MINOR: (de)compression: add decompression infrastructure
    - MEDIUM: flt-decomp: implement decomp-req and decomp-res filters
    - MINOR: flt-http-comp: Check request headers for response compression earlier
    - MINOR: flt-decomp: add support for HTTP decompression
    - DOC: config: add documentation for decompression filter
    - REG-TESTS: decompression: add basic decompression test
    - MINOR: flt-decomp: warn if built without USE_SLZ
    - MEDIUM: filters: implicitly enable "decomp-req" and "decomp-res" when decompression options are used
    - REG-TESTS: decompression: check the response header rewriting
    - REGTESTS: decomp: Add more scripts to test HTTP/TCP decompression
    - BUILD: flt-decomp: Remove unused stream_get_decomp() function
    - MINOR: haload: enable per-thread-group FD tables when available
    - BUG/MINOR: quic: fix zero value of tune.quic.sec.stateless-reset-rate
    - BUG/MINOR: payload: fix the cipher_len bound check in smp_client_hello_parse()
    - BUG/MINOR: payload: bound ClientHello extension lists by the extension length
    - BUG/MEDIUM: quic: crash on key update phase change after a failed one
    - BUG/MINOR: quic: late packets of the first key phase are dropped
    - CLEANUP: quic: remove the unused packet number of the key phase structure
    - BUG/MEDIUM: filters: check the filter name before comparing it
    - IMPORT: uslz: do not replay the header-detection bytes on the raw deflate path
    - IMPORT: slz: get rid of the bogus USE_64BIT_QUEUE and check siezof(long) instead
    - MEDIUM: ssl: do not fall back to Subject CN when a SAN dNSName is present
    - REGTESTS: ssl: add verifyhost SAN vs CN matching test
    - MINOR: haload: rename <hld_http_ver> to <hld_proto>
    - MINOR: mux-fcgi: also skip sc_opposite() for haload streams
    - MINOR: haload: add FastCGI support
    - BUN/MINOR: hlua: reset hlua_body variable after per-thread files loading
    - BUG/MEDIUM: resolvers: Use the right server to init DNS streams
    - BUG/MINOR: resolvers: classify empty truncated responses
    - CLEANUP: resolvers: stop clearing the DNS response buffer
    - MINOR: tools: report explicit address type selection
    - CLEANUP: dns: use a single maximum DNS message size
    - MINOR: dns: select the nameserver server type explicitly
    - MEDIUM: resolvers: accept full-size DNS messages over streams
    - MEDIUM: resolvers: retry truncated UDP responses over TCP
    - REGTEST: resolvers: cover UDP to TCP fallback
    - MINOR: quic: protect the ChaCha20 header protection context from being freed
    - BUG/MINOR: qpack: encoder and decoder stream data is never consumed
    - BUG/MINOR: qpack: accept a Required Insert Count which cannot be reached
    - BUG/MINOR: quic: delete the wrong CIDs when post-handshake frames fail
    - MINOR: ring: add "-s" flag to "show events" to sanitize the output
    - REGTESTS: log: add a test for "show events -s"
    - REGTESTS: log: check which log-format tags escape control characters
    - BUG/MINOR: tools: support the documented NULL in cbor_encode_bytes_prefix_bin()
    - BUG/MEDIUM: log: encode the whole non-printable range in the +json output
    - REGTESTS: log: check the escaping performed by the +json option
    - DOC: config: mention that section 8.6 does not apply to the cbor encoding
    - REGTESTS: log: check that the +cbor output passes the logged bytes through
    - BUG/MINOR: debug: restore complain tail jumps with clang
    - MEDIUM: mux-h1: Harden test for short data at end of stream
    - BUG/MEDIUM: mux-h2: Make sure we remove H2_SF_NOTIFIED on subscribe
    - BUG/MAJOR: h3: reject H3 truncated frames
    - CLEANUP: h2: remove dead code around FND_AUTH in h2_prepare_htx_reqline()

2 months agoCLEANUP: h2: remove dead code around FND_AUTH in h2_prepare_htx_reqline()
Willy Tarreau [Thu, 3 Sep 2026 12:23:01 +0000 (14:23 +0200)]
CLEANUP: h2: remove dead code around FND_AUTH in h2_prepare_htx_reqline()

In h2_prepare_htx_reqline(), we test (fields & H2_PHDR_FND_AUTH) in the
"else" branch of the same condition. Let's just drop it since it will
never match.

2 months agoBUG/MAJOR: h3: reject H3 truncated frames
Amaury Denoyelle [Thu, 3 Sep 2026 12:50:05 +0000 (14:50 +0200)]
BUG/MAJOR: h3: reject H3 truncated frames

HTTP/3 frames contain a length header. If a frame is truncated, the
connection must be immediately closed with H3_FRAME_ERROR.

This patch adds missing check for truncated frames when FIN is received.
This must be tested in two locations. First, a check is added in the
standalone FIN dedicated block. Second, another check is implemented in
the default parsing while loop, tested on every iteration as large DATA
frames can be read as streaming content.

These checks are crucial to prevent potential request content smuggling.

Thanks to Rajat Raghav for its report which reveal this issue.

This must be backported to all stable releases.

2 months agoBUG/MEDIUM: mux-h2: Make sure we remove H2_SF_NOTIFIED on subscribe
Olivier Houchard [Fri, 28 Aug 2026 10:34:53 +0000 (12:34 +0200)]
BUG/MEDIUM: mux-h2: Make sure we remove H2_SF_NOTIFIED on subscribe

The mux h2 remembers that he woke a stream for writing, and as long as
the H2_SF_NOTIFIED flag is set, won't attempt to do so again. However,
if for any reason the stream does not try to send data, and just
subscribes, then it will never be woken up. So remove the flag on
subscribe.
This can happen for example, with tcpcheck using the proxy protocol, if
for any reason the proxy protocol handshake is not done yet, as tcpcheck
will just subscribe if it sees the CO_FL_WAIT_XPRT flag.

This should be backported up to 2.8.

2 months agoMEDIUM: mux-h1: Harden test for short data at end of stream
Olivier Houchard [Thu, 3 Sep 2026 11:39:04 +0000 (13:39 +0200)]
MEDIUM: mux-h1: Harden test for short data at end of stream

Commit 75dd2c18dda29f9893574ef982ddf66de16f0408 made it so if we had a
content-length, and we sent less data than expected, we'd make sure to
close the connection and to not reuse it, otherwise the server would
still expect more data. But it can also happen with no content-length,
with chunked data. There is absolutely no reason we reach that code
while still expecting more data, so just always consider that an error,
no matter if we have content-length or not.

This should be backported up to 2.8.

2 months agoBUG/MINOR: debug: restore complain tail jumps with clang
Yongqiang Tian [Wed, 19 Aug 2026 23:27:13 +0000 (09:27 +1000)]
BUG/MINOR: debug: restore complain tail jumps with clang

Commit 9752f802b3869c96446f7e84e43d89360695c0f8 introduced
eliminate_tail_calls to turn the final call of a function into a jump.
Commit 431b6b1cd92a54f4a53b66a5ff27a64381ff9a23 then applied it to
complain() and complain_with_dbg() so that these wrappers would not appear
in diagnostic backtraces.

The GCC expansion enables sibling-call optimization. The Clang expansion
currently uses disable_tail_calls, which has the opposite meaning: Clang
documents it as preventing tail-call optimization inside the attributed
function.

This is visible through HAProxy's own "debug dev check" command. GCC 11
omits complain_with_dbg() from the resulting backtrace, while Clang 14 and
Clang 18 retain one complain_with_dbg() frame. The builds otherwise succeed,
so build success alone does not expose the difference.

Making eliminate_tail_calls empty for Clang removes the inverse attribute,
but is not sufficient by itself: both tested Clang versions then inline
ha_backtrace_to_stderr() into the wrapper and still retain its frame. Also
mark the callee noinline so that Clang emits the intended final jump.

The resulting source was tested on x86-64 at master revision
3081fdb5846b1b7ab8f93d1f28b31af13502f5db. Normal GCC 11, Clang 14, and
Clang 18 builds passed, and "debug dev check" omitted the wrapper frame in
all three repaired builds. Object-code inspection confirmed a final jump.
CI-aligned no-features builds with GCC 13 and Clang 18 also passed, including
143 default, bug, and devel regression tests for each compiler. A broader
Clang 14 run passed 167 regression tests. The enabled unit tests passed for
the repaired GCC and Clang builds. Clang 13 never optimizes the call in any
case.

The user-visible effect is limited to developer backtrace quality. This is
not a security or performance fix. This is submitted as an RFC because a
maintainer may prefer a Clang-specific spelling or a reusable noinline
abstraction. This is the approach I had in mind, and any suggestions would
be appreciated.

No backport is needed because both introducing commits are only in the 3.5
development branch.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

No backport needed.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This should be backported as far as 2.6.

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

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

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

This should be backported as far as 2.6.

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

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

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

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

This should be backported as far as 2.6.

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

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

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

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

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

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

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

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

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

2 months agoMEDIUM: resolvers: retry truncated UDP responses over TCP
Manu Nicolas [Wed, 2 Sep 2026 10:09:14 +0000 (10:09 +0000)]
MEDIUM: resolvers: retry truncated UDP responses over TCP

RFC 1123 and RFC 2181 recommend retrying a truncated DNS response
over a transport capable of carrying a larger message. HAProxy instead
treats TC as a resolver error unless the nameserver is configured to
use TCP for every query.

Give unprefixed IPv4, IPv6 and hostname nameservers both a datagram and
a stream server. Queries use UDP first and the existing stream code
opens TCP connections lazily. Explicit datagram prefixes remain
UDP-only, explicit stream prefixes remain TCP-only, and nameservers
loaded from resolv.conf gain the default fallback behavior.

When a valid UDP response is truncated, rebuild its query over TCP with
the same ID, question type and nameserver. Taking the type from the
response preserves late A or AAAA responses received after a query-type
switch. Queueing the fallback refreshes the retry deadline without
consuming a retry or initiating another A/AAAA switch.

A truncated SRV response can still be partially processed when no TCP
fallback can be initiated for that response. This preserves existing
behavior for explicit UDP and TCP nameservers and processes a partial
SRV response received during TCP fallback. Other truncated TCP
responses follow the normal error path, and no TCP response triggers
another fallback.

Failed queue operations use the existing send-error path, while
connection failures are handled by the resolver timeout and retry path.

Add a tcp_fallback counter to the CLI, resolver statistics and
Prometheus output. It counts fallback queries successfully queued to the
stream server.

2 months agoMEDIUM: resolvers: accept full-size DNS messages over streams
Manu Nicolas [Tue, 1 Sep 2026 10:44:50 +0000 (10:44 +0000)]
MEDIUM: resolvers: accept full-size DNS messages over streams

accepted_payload_size is both the EDNS UDP advertisement and the
response size limit. A complete stream response larger than this value
is therefore discarded even though DNS stream framing can carry it. It
also makes it impossible to advertise a conservative UDP size while
accepting the complete response after a transport switch.

The stream code already rejects messages above the DNS protocol limit
and buffers the message before the resolver checks its size. Accepting
up to DNS_MAX_MSG_SIZE in the resolver therefore does not increase the
stream allocation.

Keep accepted_payload_size as the UDP advertisement and receive limit,
and use the protocol maximum for stream responses. Configurations that
used it to reject large stream responses will now accept those valid
messages.

2 months agoMINOR: dns: select the nameserver server type explicitly
Manu Nicolas [Tue, 1 Sep 2026 10:44:48 +0000 (10:44 +0000)]
MINOR: dns: select the nameserver server type explicitly

DNS send, receive and response functions currently infer which server
to use from the non-null member of a nameserver. This is unambiguous
while a nameserver owns only one server, but prevents one logical
nameserver from exposing both datagram and stream servers.

Pass a dns_server_type through these interfaces and add predicates for
readable type checks. Existing queries still select the same server, so
this patch does not change runtime behavior.

2 months agoCLEANUP: dns: use a single maximum DNS message size
Manu Nicolas [Wed, 2 Sep 2026 10:08:45 +0000 (10:08 +0000)]
CLEANUP: dns: use a single maximum DNS message size

The DNS code defines separate maximum message sizes for datagram and
stream processing, but both are the DNS protocol limit of 65535 bytes.
The duplicate names make common response buffers look
transport-specific and could allow the values to diverge.

The request ring size also has a TCP-specific name even though both
datagram and stream servers use it.

Replace the message limits with DNS_MAX_MSG_SIZE and rename the ring
limit to DNS_MSG_RING_MAX_SIZE. This does not change any limit or
runtime behavior.

2 months agoMINOR: tools: report explicit address type selection
Manu Nicolas [Tue, 1 Sep 2026 10:44:47 +0000 (10:44 +0000)]
MINOR: tools: report explicit address type selection

str2sa_range() reports the resulting socket and transport types through
net_addr_type. It does not tell callers whether these types came from
defaults or from an explicit prefix. Callers that need to distinguish
the two must duplicate the parser prefix list, which can fall out of
sync.

Add explicit_type and set it for prefixes that select a socket or
transport type. Family-only prefixes leave it unset. This has no effect
on existing callers.

2 months agoCLEANUP: resolvers: stop clearing the DNS response buffer
Manu Nicolas [Tue, 1 Sep 2026 10:44:45 +0000 (10:44 +0000)]
CLEANUP: resolvers: stop clearing the DNS response buffer

resolv_process_responses() clears the whole response buffer before
each receive. The receive operation supplies the message length and
all parser accesses are bounded by the corresponding end pointer, so
no byte beyond the received message is observed.

Remove the unnecessary buffer clear.

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

The answer count is checked before the response question is parsed.
Most DNS servers return a truncated response without answers, so such
a response is classified as ANCOUNT_ZERO before the TC flag and
question type are examined. It increments the wrong counter and hides
the actual response status.

Move the empty-answer check after question parsing and TC handling.
Truncated SRV responses keep their existing partial-answer behavior.

This should be backported as far as 2.4.

2 months agoBUG/MEDIUM: resolvers: Use the right server to init DNS streams
Christopher Faulet [Tue, 1 Sep 2026 15:51:07 +0000 (17:51 +0200)]
BUG/MEDIUM: resolvers: Use the right server to init DNS streams

During TCP nameservers parsing, the wrong server was selected to initialize
the DNS stream. After the server addition, the first server of the proxy was
selected instead of the last one. So when several TCP nameservers was
defined, only the first one was properly initialized. All others was using
the server created for the first one.

The regression was introduced when the server insertion in the proxy's list
was inverted. The change was not made on the resolvers part. The
proxy_last_server() function must be used instead.

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

2 months agoBUN/MINOR: hlua: reset hlua_body variable after per-thread files loading
Christopher Faulet [Tue, 1 Sep 2026 14:51:54 +0000 (16:51 +0200)]
BUN/MINOR: hlua: reset hlua_body variable after per-thread files loading

hlua_body variable was introduced to know if the files loading was passed or
not and emit warnings if some operations are performed during runtime. In
particular, it was used to detect calls to core.use_native_mailers_config()
during runtime.

However, the hlua_body variable was reset too early in the post-init
function, before the per-thread files loading. Because of this issue,
loading mailers.lua script using 'lua-load-per-thread' directive led to the
following error during haproxy startup:

  ... use_native_mailers_config: not available outside of body context

To fix the issue, this variable is now reset at the end of the function.

This patch must be backported as far as 3.3. It should fix the issue #3487.

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

Add -fcgi (protocol selector) and --fcgi-app <str> (raw passthrough
into a generated "fcgi-app haload" section, mainly for "docroot").

The FCGI filter is attached from hld_cfg_finalize() (post-check),
not hld_pre_check(): servers don't exist yet at pre-check time, and
HAProxy's own postparser then wrongly reports no FCGI server found.

"set-param"/"pass-header" have no effect (no struct stream to run
them); use -H ":fcgi-<param>:<value>" instead.

2 months agoMINOR: mux-fcgi: also skip sc_opposite() for haload streams
Frederic Lecaille [Mon, 31 Aug 2026 08:35:16 +0000 (10:35 +0200)]
MINOR: mux-fcgi: also skip sc_opposite() for haload streams

fcgi_set_default_param() already falls back to conn_src()/conn_dst()
for check streams (struct check, sc_check()). haload streams (struct
hldstream, sc_hldstream()) need the same fallback.

This is a preparation for the FCGI support into haload.

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

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

2 months agoREGTESTS: ssl: add verifyhost SAN vs CN matching test
William Lallemand [Tue, 1 Sep 2026 09:13:17 +0000 (09:13 +0000)]
REGTESTS: ssl: add verifyhost SAN vs CN matching test

Add a VTC test to check that backend certificate verification in
ssl_sock_srv_verifycbk() adheres to RFC 6125:

1. A certificate with a matching SAN dNSName is accepted.
2. A legacy certificate without any SAN extension falls back to the
   Subject CN and is accepted.
3. A certificate with a non-matching SAN dNSName is rejected, even if
   its Subject CN matches the expected hostname.

The test uses dedicated static certificates in reg-tests/ssl/certs/
(verifyhost_ca.crt and associated leaf certificates) so that
certificates do not need to be generated at runtime.

2 months agoMEDIUM: ssl: do not fall back to Subject CN when a SAN dNSName is present
William Lallemand [Tue, 1 Sep 2026 08:24:25 +0000 (10:24 +0200)]
MEDIUM: ssl: do not fall back to Subject CN when a SAN dNSName is present

Historically, ssl_sock_srv_verifycbk() checked the certificate's
subjectAltName (SAN) extension first, but fell back to checking the
Subject Common Name (CN) whenever no SAN matched the expected hostname
(from SNI or "verifyhost").

However, per RFC 6125 section 6.4.4:
https://datatracker.ietf.org/doc/html/rfc6125#section-6.4.4

  "As noted, a client MUST NOT seek a match for a reference identifier
   of CN-ID if the presented identifiers include a DNS-ID, SRV-ID,
   URI-ID, or any application-specific identifier types supported by the
   client."

This patch changes the behavior by tracking whether any dNSName entry was
encountered in the SAN extension and skipping the Subject CN check entirely
if one was present, even if none of the SAN entries matched. Fallback to CN
is only preserved for legacy certificates lacking a dNSName SAN.

This issue was reported in GitHub issue #3486.

2 months agoIMPORT: slz: get rid of the bogus USE_64BIT_QUEUE and check siezof(long) instead
Willy Tarreau [Mon, 31 Aug 2026 12:14:16 +0000 (14:14 +0200)]
IMPORT: slz: get rid of the bogus USE_64BIT_QUEUE and check siezof(long) instead

The USE_64BIT_QUEUE was set when it was suggested to rely on 64-bit
operations on the queue and dump 32 bits at once (thus never keeping
more than 32 bits in the queue). It was also used to decide whether
the queue was stored as u32 or u64. However, this check in the public
part (slz.h) depends on a definition in the private part (slz-prv.h)
that cannot be seen by application code. Luckily, slz.c includes its
headers in the wrong order so it gets the public part before the private
one, and uses the same layout, which is why it was not detected earlier.

Let's just get rid of this USE_64BIT_QUEUE definition. It's not accurate
and cannot be matched between public and private code. Let's instead rely
on __SIZEOF_LONG__ which conveys the info we need: is the CPU efficient
when working on 64-bit integers.

The comments were adjusted to explain the difference and why it's not
dramatic to fall back to the 32-bit queue (in effect it's around a 1%
perf loss).

This must be backported to 1.3.

This is libslz upstream commit 54cf477a1736b3e747af14ce1ec38d7c28b34cfc

2 months agoIMPORT: uslz: do not replay the header-detection bytes on the raw deflate path
Willy Tarreau [Sun, 23 Aug 2026 10:42:34 +0000 (12:42 +0200)]
IMPORT: uslz: do not replay the header-detection bytes on the raw deflate path

When a stream turns out to be raw deflate and the first two bytes had to
be accumulated into hdr_detect.buf across calls, those bytes are replayed
to uslz_decode_block() from a local array, after which the code did:

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

That rewinds to the start of the caller's buffer, but the loop that
filled hdr_detect.buf consumed from that same buffer, so the bytes just
replayed were handed to the decoder a second time. Saving the real
in_ptr across the replay fixes it; in_top never moved, and is saved with
it only for symmetry.

The problem happens whenever a raw deflate stream is fed in chunks of 1
or 2 bytes, since 3 or more takes the fast path at "in_top - in_ptr > 2"
and never enters this branch. Feeding 5000 bytes of source through
uslz_decode() one byte at a time failed with USLZ_DECODE_E_GEN_HUFF and
no output at all: the duplicated bytes shift the bit stream, so the first
dynamic block's code lengths no longer form a valid tree. gzip and zlib
streams are unaffected, they leave this branch by a different location.

The issue was spotted by Qwen-3.8-27B. This fix must be backported to
1.3.

This is libslz upstream commit f052863a91aeb44021ed1bd52f680df45667496a

2 months agoBUG/MEDIUM: filters: check the filter name before comparing it
Dragan Dosen [Mon, 31 Aug 2026 22:43:56 +0000 (22:43 +0000)]
BUG/MEDIUM: filters: check the filter name before comparing it

compile_filter_sequence_elt() looks a filter up by name with strcmp(),
without checking whether the filter configuration has a name. Only the
filters declared with the "filter" keyword get one, assigned by
parse_filter(). The filters which the configuration inserts on its own
never go through that function and leave the name NULL, so haproxy can
crash while parsing a configuration which combines a "filter-sequence"
line with any of those, for example:

    frontend f
        bind :8080
        compression algo gzip
        filter-sequence request non-existent

Let's skip the filters which have no name. They cannot be referenced in
a "filter-sequence" line anyway, so the lookup now fails and reports
"invalid filter name".

This should be backported to all branches which have "filter-sequence".

2 months agoCLEANUP: quic: remove the unused packet number of the key phase structure
Frederic Lecaille [Mon, 31 Aug 2026 15:11:14 +0000 (17:11 +0200)]
CLEANUP: quic: remove the unused packet number of the key phase structure

struct quic_tls_kp has a <pn> member, set by quic_tls_rotate_keys() and
initialized by quic_tls_kp_init(), which is not read anywhere: the previous key
phase secrets are now detected with <ctx>. This patch removes it.

2 months agoBUG/MINOR: quic: late packets of the first key phase are dropped
Frederic Lecaille [Mon, 31 Aug 2026 14:53:48 +0000 (16:53 +0200)]
BUG/MINOR: quic: late packets of the first key phase are dropped

qc_pkt_decrypt() tests <qc->ku.prv_rx.pn> to know whether the previous key
phase secrets are usable. This number comes from <tls_ctx->rx.pn>, which is
only set on a key phase change and is thus still 0 during the first key phase.
After the first key update the test always fails and every late packet of the
initial key phase is dropped instead of being decrypted with the previous keys.

Test <qc->ku.prv_rx.ctx> instead: it is NULL before the first key update, and
set after it.

This bug was introduced by commit a7d2c0946 ("MINOR: quic: Enable the Key
Update process").

This must be backported as far as 2.6.

2 months agoBUG/MEDIUM: quic: crash on key update phase change after a failed one
Frederic Lecaille [Mon, 31 Aug 2026 12:26:25 +0000 (14:26 +0200)]
BUG/MEDIUM: quic: crash on key update phase change after a failed one

qc_pkt_decrypt() decrypts the first packet of a new key phase with
<qc->ku.nxt_rx.ctx> without checking it. quic_tls_key_update() leaves it NULL
when the derivation fails on a memory allocation error, and its caller only
dropped the packet, so the next key phase change crashed the process.

Closing the connection would be excessive: <tls_ctx->rx.ctx> is still valid and
the current key phase works. The next keys are now checked where they are used
and derived again if missing. <qc->ku.nxt_tx.ctx> is checked too, as
quic_tls_rotate_keys() moves it to <tls_ctx->tx.ctx>. If the derivation fails
again the packet is dropped and the peer retransmits it. A packet which
decrypted correctly is no longer dropped either.

This bug was introduced by commit a7d2c0946 ("MINOR: quic: Enable the Key
Update process").

Thanks to Bruno Henc for reporting this issue.

This must be backported as far as 2.6.

2 months agoBUG/MINOR: payload: bound ClientHello extension lists by the extension length
William Lallemand [Sat, 29 Aug 2026 10:05:33 +0000 (10:05 +0000)]
BUG/MINOR: payload: bound ClientHello extension lists by the extension length

smp_fetch_ssl_supported_groups(), smp_fetch_ssl_sigalgs(),
smp_fetch_ssl_keyshare_groups() and smp_fetch_ssl_hello_sni() all check
the length of the list carried by their extension against

    <list_len> > hs_len - 6

where <hs_len> is what remains of the *whole* extension block, not of the
extension being parsed. The list lives in the extension body, so the
bound has to be <ext_len> - 2. All four sites already reject ext_len < 2
beforehand, so the new bound cannot go negative, and since ext_len is
itself checked against hs_len - 4 a few lines above, it is strictly
tighter than the current one.

With the loose check, an extension whose body holds nothing but the
2-byte list length is accepted while announcing a list that runs into the
extensions that follow, and the fetch returns bytes belonging to another
extension:

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

For req.ssl_sni the effect is the most visible, since name_type, name_len
and the hostname are then all read from the neighbouring extension, and a
hostname is reported that appears in no server_name entry.

Those reads all stay within the ClientHello, whose bytes are supplied by
the client anyway, so this is neither an out-of-bounds access nor an
information disclosure: the returned sample ends at worst on the last
byte of the extension block. It does however let a client decouple what
use-server/use_backend rules and logs observe from what it really
announced, on a ClientHello that a conformant TLS stack would reject.

A reg-test covering, for each of the four fetches, a well-formed
extension and an over-announced list is added.

The req.ssl_supported_groups, req.ssl_sigalgs and req.ssl_keyshare_groups
parts should be backported as far as 3.2, where those fetches appeared
with commit fa063a9e7 ("MINOR: sample: add 4 new sample fetches for
clienthello parsing"). The req.ssl_sni part is much older, it dates back
to commit b6672b547 ("MINOR: acl: add support for TLS server name
matching using SNI") in 1.5-dev8, so backports to branches older than 3.2
need to keep that hunk only.

2 months agoBUG/MINOR: payload: fix the cipher_len bound check in smp_client_hello_parse()
William Lallemand [Sat, 29 Aug 2026 09:49:09 +0000 (09:49 +0000)]
BUG/MINOR: payload: fix the cipher_len bound check in smp_client_hello_parse()

smp_client_hello_parse() validates the CipherSuite length of a TLS
ClientHello with:

    ext_len > hs_len

where <hs_len> is the number of ClientHello bytes remaining, counted from
<data>, which at that point still points at the 2-byte cipher_len field
itself. The CLIENTHELLO_CIPHERSUITE exit path returns that very <data>
together with *len = cipher_len, and smp_fetch_ssl_cipherlist() then
publishes <data + 2> over <cipher_len> bytes. The cipher suites therefore
have to fit *after* the length field, so the check must be:

    ext_len > hs_len - 2

Without this, a ClientHello announcing a cipher_len one or two bytes
larger than what its handshake message can hold is accepted, and
req.ssl_cipherlist returns up to 2 bytes read past the end of the
ClientHello. When the ClientHello is the last thing in the buffer, those
2 bytes are leftovers of a previous connection, which then leak to
whatever logs or routes on req.ssl_cipherlist:

    # 45-byte ClientHello announcing cipher_len = 10 for 8 cipher bytes
    cipherlist=130213031301009fcafe   <- "cafe" sent after the TLS record
    cipherlist=130213031301009f4142   <- nothing sent after the ClientHello

The CLIENTHELLO_EXTENSIONS path was not affected; it only survived by
accident, because it re-checks "hs_len < 2" after subtracting
2 + ext_len.

A reg-test exercising both a well-formed cipher_len and an over-announced
one is added.

This must be backported as far as 3.2, where req.ssl_cipherlist was
introduced by commit fa063a9e7 ("MINOR: sample: add 4 new sample fetches
for clienthello parsing").

Reported-by: kongr <kongr@krsecu.com>