Amaury Denoyelle [Mon, 19 Oct 2026 11:55:19 +0000 (13:55 +0200)]
MINOR: h3: remove dead code on h3_rcv_buf() error path
Previous patches rework error handling and glitch reporting in
h3_rcv_buf() after a connection level error occured. A similar pattern
is adopted after each frame parsers : when a connection level error is
encountered, glitch is incremented and parsing is immediately
interrupted using early return via err label.
To ensure the code remains safe though with future evolution, h3c <err>
check on h3_rcv_buf() epilogue is replaced so that qcc_set_error() is
only set if not already done, which is currently unnecessary. A
WARN_ON() should help to detect such cases.
Amaury Denoyelle [Mon, 19 Oct 2026 11:48:06 +0000 (13:48 +0200)]
BUG/MEDIUM: h3: fix BUG_ON() crash with glitch after QPACK dec err
This is part of the reworking of glitch reporting in H3 module. The
current patch handles QPACK decoding error when parsing a HEADERS frame.
Function qcc_report_glitch() is now only called after qcc_set_error(),
for both request, response and headers/trailers parsing.
Amaury Denoyelle [Mon, 19 Oct 2026 11:19:49 +0000 (13:19 +0200)]
BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on GOAWAY parsing
This patch fixes glitch reporting after GOAWAY frame parsing. Similar to
the previous commit, qcc_report_glitch() is called only after
qcc_set_error() after h3_parse_goaway_frm().
Amaury Denoyelle [Mon, 19 Oct 2026 11:12:59 +0000 (13:12 +0200)]
BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on SETTINGS parsing
There is a risk of BUG_ON() crash when glitch are reported via
qcc_report_glitch() before settings a connection-level error using
qcc_set_error(). This happens when reaching the glitch level with the
former function also calling qcc_set_error(). A BUG_ON() is triggered as
it must not be called multiple times per connection to prevent error
code erasure.
This patch fixes glitch reporting during SETTINGS frame parsing. Now,
qcc_report_glitch() is only called after qcc_set_error() on
h3_parse_settings_frm() return.
MINOR: filters: Move cache-store after decompression in global classes list
It is more logical to have the decompression class first, event on the
global list, because by default, the decompression is always performed
before the caching. The better to have a global order reflecting as much as
possible the channels order.
BUG/MEDIUM: bwlim: never burst more than the pending data in input
When the bandwidth limitation filter applies a limitation on a channel, it
returns the amount of data the stream is authorized to forward to the data
filtering layer.
This amount could be larger than the amount of pending data in the channel
input when the min-size burst was used: with a shared limitation, the stream
quota was shared between all users and the burst was not clamped to the
pending input data when the producer of the channel was still open. When
this happened, more output data than available were removed from the
channel. The channel was then corrupted and the stream spinned at 100% CPU
with its tasklet woken up in a loop while the response buffer shown a
negative input count.
The fix clamps the burst to the pending input data, as already done when the
producer closed its endpoint. This way the filter never reports more data
than the channel really holds.
This patch should fix the issue #3541. It must be backported as far as 2.6.
Miroslav Zagorac [Fri, 16 Oct 2026 19:32:49 +0000 (21:32 +0200)]
MINOR: filters: initialize the filter classes before the register stage
The filter classes of HAProxy are initialized from the register stage,
where the internal classes are registered too. A module registering its
own class from that stage is refused, since whether it runs before or
after this initialization depends on the link order, and the objects of an
addon are linked first: its classes are registered too early, always.
Initializing the classes from the prepare stage leaves the whole register
stage to the modules, which then register their classes in time for
"haproxy -vv" to report them, the filter classes being listed while the
arguments are parsed, between the register and the init stages.
Nothing else moves: the classes are initialized in the same order, and the
placement of a class still decides the evaluation order of the filters.
The filter documentation was updated to match.
Aleksandar Lazic [Sat, 17 Oct 2026 19:40:58 +0000 (21:40 +0200)]
BUG/MINOR: filters: free the error message of the filter-config parsers
flt_precheck_instance() passes a message to the ->parse() callback of
the filter classes, which may allocate it with memprintf() to report an
error. The message was reported with ha_alert() but never released, so
it leaked each time a "filter-config" instance failed to be parsed, for
instance with "filter-config trace id t1 bogus-option", as reported by
ASAN.
This only happens on configuration errors, which are fatal at boot, but
the instances of the proxies created at runtime are parsed the same way.
Let's release the message once reported, and also on success in case a
parser fills it without failing.
This was brought by commit b3075cf7f ("MEDIUM: filters: Parse and
finalize filter-config instances") in 3.5-dev9, so no backport is
needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BUG/MEDIUM: h3: split absolute URIs like the HTTP analysers do on output
h3_req_headers_send() derived :authority and :path from absolute-form
URIs with the same flaw as the H2 mux, fixed by the previous commit:
the authority ended at the first '/' instead of the first '/', '?' or
'#' (RFC3986#3.2), the userinfo was not stripped, and an empty path
was sent as an empty :path. A request evaluated by haproxy for one
host/path could be delivered to an H3 server with another one,
bypassing host and path based rules.
Now the authority is extracted like http_parse_authority() does, the
userinfo is stripped, and :path always starts with a '/' (or is '*'
for OPTIONS). As H3 backends are still experimental, the impact is
limited.
This was introduced by commit 235e818fa ("MINOR: h3: complete HTTP/3
request scheme encoding") in 3.3. Must be backported as far as 3.3.
BUG/MAJOR: mux-h2: split absolute URIs like the HTTP analysers do on output
When deriving :authority and :path from an absolute-form URI, the
authority ended at the first '/' only instead of the first '/', '?' or
'#' (RFC3986#3.2), and the userinfo was stripped up to the first '@'
instead of the last one. The analysers and sample fetches rely on
http_parse_authority() which does it correctly, so a request could be
evaluated for one host/path and delivered to the H2 server with
another one, bypassing host and path based rules.
Now the authority is extracted like http_parse_authority() does, and a
'/' is prepended to :path when needed.
This exists since commit b8ce8905c ("MEDIUM: mux-h2: do not map Host to
:authority on output") in 2.1, the userinfo part since 29fa2667a
("BUG/MINOR: mux-h2: strip the userinfo when deriving :authority for a
server") in 3.5-dev5. Must be backported to all stable versions (the
userinfo part only where 29fa2667a was backported).
BUG/MAJOR: mux-quic: do not crash on connection error after glitch threshold
When a glitch reported by the HTTP/3 parser (SETTINGS/GOAWAY parsing or
QPACK decoding error) reaches tune.quic.{fe,be}.sec.glitches-threshold,
_qcc_report_glitch() sets a connection error before the caller also
calls qcc_set_error() with h3c->err. The BUG_ON() on double call then
kills the process, remotely triggerable by any client.
Make qcc_set_error() keep the first error and ignore subsequent calls.
Introduced by commit 0d35f8d91 ("MINOR: h3: report glitch on RFC
violation") in 3.0-dev12. Must be backported to all stable versions
since 3.0.
CI: github: fix VTest cache paths in container jobs
The Alpine job rebuilds VTest without publishing a cache entry. Its
cache save receives the host workspace path, which does not exist inside
the container, and reports a Path Validation Error. Changing only the
cache path to "vtest" still leaves installation using the context path,
so the cached directory remains empty.
Install VTest under the runtime GITHUB_WORKSPACE and use the same
location in all workflows that call setup-vtest. Keep restore and save
paths workspace-relative so that the installed binary is found inside
the container and cache hits restore it where the callers expect it.
Split caching into explicit restore and save steps, saving immediately
after installing VTest rather than relying on the post-job hook. Reuse
the restore step's primary key. Include the distribution and version in
the key to keep Alpine/musl binaries separate from glibc builds on
runners with the same architecture.
BUG/MEDIUM: resolvers: reject too long labels when encoding a name
A 256-byte label passed through do-resolve (e.g. from hdr(host)) wraps
its encoded length to zero. Callers using strdup() then truncate the
name but retain its full length, causing out-of-bounds reads and heap
data disclosure in DNS queries. Embedded NULs cause the same issue.
Reject labels longer than DNS_MAX_LABEL_SIZE and embedded NULs in
resolv_str_to_dn_label(), before callers duplicate the encoded name.
This affects do-resolve since its introduction in 2.0 by commit 333939c2e ("MINOR: action: new '(http-request|tcp-request content)
do-resolve' action"). Must be backported to all stable branches.
Change the schedule of the cross-zoo, illumos, quictls, fedora-rawhide
and netbsd workflows so that they run every day instead of weekly or
monthly.
The hour of the original schedule is kept for each workflow, so that
the jobs stay spread over the day, and the quictls comment is updated
to mention the daily cadence.
Olivier Houchard [Sat, 17 Oct 2026 22:19:01 +0000 (00:19 +0200)]
BUG/MINOR: sym: don't crash on images linked at a fixed base
collect_dynsym_mem() biases the dynamic entries below the load
address, but those of an image linked at a fixed base sit far above
it and are used as is: this is the case of the vDSO image that old
kernels (e.g. 3.10) build prelinked, and haproxy then crashes at
start-up, the tables being read through unmapped pointers.
Fix that by trying to figure out if the pointer was already relocated
or not.
Willy Tarreau [Fri, 16 Oct 2026 18:27:51 +0000 (20:27 +0200)]
BUILD: makefile: better support git worktrees
Git worktrees replace the .git directory with a file which contains the
location to the git directory. The version generation test is based on
the detection of .git/. as a directory, and thus fails inside a worktree.
Let's relax the test to only see if .git exists.
BUG/MINOR: mux-h1: Don't subscribe for receives once read0 or error received
In h1_rcv_buf(), we no longer subscribe for reads once a read0 or an error
was received. Nothing can be read anymore, and the subscription itself is
useless. In addition, since the socket remains readable at EOF, the
connection could theoretically be woken up again and again, waking the
stream up in turn, which would make the process spin until the server
responds. No reliable reproducer was found for this, but it is safer to
not subscribe for reads when no more data can ever be received.
This patch could be backported to all supported versions.
BUG/MEDIUM: mux-h1: don't spread-close idle conns with idle-close-on-response
With "option idle-close-on-response", idle frontend connections must not be
closed during a soft-stop: haproxy waits for the next request and closes the
connection after responding to it, so that clients (or load balancers in
front of haproxy) don't send a request on a connection being
closed. h1_process() properly checks this option before closing an idle
connection when the proxy is stopping. But when "close-spread-time" is set,
h1_refresh_timeout() also considers idle frontend connections as candidates
to be closed during the spread window, without checking the option, and
shortens their timeout to a random point within the window. The timeout
task then silently closes them.
Let's not consider idle connections for the spread close when the option is
set, as done in h1_process().
BUG/MINOR: mux-h1: don't forward request trailers named like server name header
With "http-send-name-header <name>", the H1 mux skips any request header
with the same name and adds its own with the server's name. But request
trailers were not filtered, so a client sending a chunked request with a
trailer "X-Srv: evil" (with "http-send-name-header X-Srv") had it forwarded
after the "x-srv: s1" header added by haproxy. Servers merging trailers into
the headers may then see the client's value. Let's skip such trailers, like
it is done for headers.
"option http-drop-request-trailers" may be used as a workaround.
This patch could be backported to all supported versions.
BUG/MINOR: mux-h1: search the websocket key in the right message
When the headers of a websocket handshake are parsed, the websocket key is
searched by h1_search_websocket_key() in the headers following the HTX
message's first block, which is supposed to be the start-line that was just
parsed. But on the server side, interim responses (1xx) may still be present
in the HTX message when the final response is parsed. So when a server sends
"HTTP/1.1 100 Continue\r\n\r\n" immediately followed by the "101 Switching
Protocols" response in the same segment, the key is searched in the 100
response, it is not found and the response is rejected with a 502, while it
is valid.
Let's loop backward on the HTX message instead, which is the one that was
just parsed.
A reg-test was added for this.
This patch could be backported to all supported versions.
BUG/MINOR: mux-h1: only rely on the client's method for bodyless responses
When the response is emitted to the client, the H1 stream is marked as
expecting a bodyless response if the HTX start-line has the
HTX_SL_F_BODYLESS_RESP flag, which is set when the response is parsed on the
server side for a response to a HEAD request. But this is the method sent to
the server, which may differ from the client's one. With "http-request
set-method HEAD" applied to a GET request, the response received from the
server has no payload but carries a content-length, which is forwarded to
the client. Since the response was considered as bodyless on the client
side, the connection was kept alive instead of being closed, so the client
took the next response as the payload of the first one:
A response to a client's HEAD request is already marked as bodyless when the
request is parsed. So only 204 and 304 statuses must be considered
here. Then the connection is closed as for any response announcing a payload
that is not received.
BUG/MEDIUM: h1: reject absolute http(s) URIs with an empty host
An absolute-form request target with the http or https scheme and an empty
host was accepted and forwarded verbatim to H1 servers, for instance:
GET http:///admin.internal/x HTTP/1.1\r\n\r\n
GET http://:8080/x HTTP/1.1\r\n\r\n
GET http://@/x HTTP/1.1\r\n\r\n
RFC9110#4.2.1 says that a recipient must reject such URIs as invalid. Here
the rules see an empty host (or ":8080") and the path "/admin.internal/x",
while servers relying on WHATWG-style URL parsers (e.g. NodeJS) take
"admin.internal" as the host and "/x" as the path. It's a parser
differential that may be used to bypass host or path based rules, depending
on the server.
Let's reject these requests, unless unsafe violations are accepted (then the
error is only captured). A reg-test was added for this.
Note that the "strange cases" C32 to C35 of the h1_host_normalization
reg-test explicitly verified that "GET http://:" with "host: :80" (or
"https://:" and "host: :443" / "host: :") were accepted and normalized to
"http:///" with an empty host. These cases are now rejected with a 400, so
the test was updated accordingly. Developers may want to double-check that
there was no other reason to accept them.
The validation of the request target was introduced by commit 25bcdb1d9
("BUG/MAJOR: h1: Be stricter on request target validation during message
parsing") in 3.0-dev12, which didn't cover this case. This should be
backported as far as 3.0.
BUG/MINOR: mux-h1: convert the <from> argument of h1-case-adjust to lower case
The "h1-case-adjust" global directive (and the file loaded with
"h1-case-adjust-file") takes the header name to match in lower case, as
specified in the documentation, but it was not checked. So with:
h1-case-adjust X-Up X-Up
the rule was accepted without any warning but never matched since header
names are looked up in lower case, and "x-up" was still emitted. Let's
convert <from> to lower case before inserting it in the map, which also
makes duplicate detection work regardless of the case.
BUG/MEDIUM: mux-h1: don't send chunked responses to HTTP/1.0 clients
When a response without content-length is sent to an H1 client, the chunked
transfer-coding is added if the response is in HTTP/1.1, and a chunked
response is forwarded as-is. But the version which matters here is the
request's one: the chunked transfer-coding must not be sent in a response to
an HTTP/1.0 request (RFC9112#6.1), since such a client doesn't know about
it.
Now, on the frontend side, when the request was not HTTP/1.1 and the
response has a body without content-length, it is switched to an unknown
transfer length: no transfer-encoding header is emitted, the payload is sent
close-delimited (trailers are dropped) and the close mode is forced. For
bodyless responses (HEAD, 204, 304), only the transfer-encoding header is
removed.
A reg-test was added for this.
This patch must be backported to all supported versions.
BUG/MINOR: h1: forget a previous pass' results when parsing headers only
In H1_MF_HDRS_ONLY mode, which is used for FCGI responses (no start-line),
h1_headers_to_hdr_list() always parses the headers again from the beginning
when it's called again after an incomplete parsing. But unlike the normal
restart path, the flags and lengths found during the previous pass were not
reset. So when the headers of an FCGI response are received in several
parts, the content-length found in the first part is seen as a duplicate
during the second pass and the header is dropped. The response is then
forwarded to the client without content-length, close-delimited, and the
client connection is closed. Similarly, "transfer-encoding: chunked" is seen
twice and reported as an error (ignored in this mode).
Let's reset the flags as on a regular restart, keeping the version and
method flags set by the caller in this mode, as well as the lengths.
This patch should be backported to all supported versions.
BUG/MINOR: h1: reject CONNECT authorities with an empty host
h1_validate_connect_authority() checks that the authority of a CONNECT
request contains a non-empty port, but it doesn't check the host part. So
"CONNECT :443 HTTP/1.1" was accepted and forwarded with or without a Host
header, and with "Host: :443" the Host header was even normalized to an
empty value. Such a target is invalid (RFC9112#3.2.3 requires a
uri-host). Let's reject it.
This was introduced by commit 3f5fbe940 ("BUG/MEDIUM: h1: Improve authority
validation for CONNCET request") in 2.7. It should be backported as far as
2.8.
BUG/MEDIUM: h1: build a missing Host header from the authority of the URI
An HTTP/1 request without a Host header could be accepted as-is, while
host-based rules require a host to apply to the real target. This concerns
the two request-target forms that carry an authority:
- the authority-form, used by CONNECT requests (RFC9112#3.2.3), where the
authority is the request target and must not contain userinfo, path or
query;
- the absolute-form (RFC9112#3.2.2), where the authority of the absolute
URI is what an H1 server must use as host, and what is sent as
:authority to H2/H3 servers.
The H1 parser only checked the consistency between the URI's authority and
the Host header when the latter was present. Now, when there is no Host
header, one is added with the authority's value (userinfo removed), so that
host-based rules apply to the real target and H1 servers receive a Host
header matching the request target, as RFC9112#3.2 requires for
HTTP/1.1. For the authority-form, it is only added once the authority was
validated, and for both forms it is not added when the authority contains
chars that are forbidden in the Host header, which is only possible when
unsafe violations are accepted.
A default port is removed from the synthesized value, like it is done when a
Host header with a default port is present, so that the rules see the same
value with and without the Host header: for the authority-form, both ports
80 and 443 are considered default since there is no scheme (CONNECT), and
for the absolute-form the default port depends on the URI's scheme (80 for
http, 443 for https).
The Host header is inserted before the terminating empty header, so that it
is accounted for in the header list.
The reg-tests h1_absolute_uri_no_host and h1_connect_no_host_no_body cover
this.
This is long-standing and should be backported to all supported versions.
BUG/MEDIUM: mux-h1: don't lose error responses once a content-length was parsed
When a request is rejected before its stream is created, an error should be
send by the H1 mux and the embryonic H1 stream is destroyed. But if a
non-null content-length was already parsed, the request was considered as
truncated, discarding the error sending and closing the connection. The
error was logged but not sent.
The regression was introduced by the commit commit 75dd2c18d ("BUG/MEDIUM:
mux-h1: close the connection on a short content-length").
To fix the issue, before destroying an embryonic H1 stream, the current
request and response lengths are reset.
This must be backported wherever commit 75dd2c18d was backported, so as far
as 2.8.
BUG/MINOR: mux-h1: report payload parsing glitches to the session
Glitches detected by the H1 mux on a frontend connection are reported to the
session by h1_process(), by comparing the number of glitches before and
after parsing. But h1_process() only parses the beginning of messages. Once
the stream is created, the payload is parsed from h1_rcv_buf(), and the
glitches reported there (invalid chunk sizes, invalid chunk CRLF, invalid
trailers...) only increment the connection's counter, never the session's
one.
Now h1_rcv_buf() reports the new glitches to the session, like h1_process()
does.
This was introduced with the glitches support by commit 05b457002 ("MEDIUM:
mux-h1: implement basic glitches support") in 3.4. So this patch should be
backported to 3.4.
BUG/MEDIUM: mux-h1: don't swap buffers when transferring data with a reserved offset
In h1_nego_ff(), when the output is chunked, some room may be reserved to emit
the chunk size later, in h1_done_ff(). However, when the producer's buffer is
not empty, a first copy may be performed via the b_xfer() function. At this
stage the buffers may be swapped while it is not expected. The reserved space
may be moved and the head position lost.
In practice, it is pretty hard to hit the bug.
Now the data are always copied when room is reserved, using b_force_xfer().
b_xfer() is still used when no room is reserved.
This was introduced by commit e8cc8a60b in 3.1 (backported to 3.0). It
must be backported as far as 3.0.
BUG/MINOR: mux-h1: don't report a 408 error on timeout of a closed connection
When a frontend connection times out, the mux H1 tries to send a 408 error
to the client unless the connection is in the CLOSING state. But a
connection may also be in the CLOSED state, waiting the client closes on its
side. In that case too, no 408 must be emitted. Because of this issue, the
failed requests counters are incremented, a "408 <BADREQ>" log line is
emitted while it should not.
Now the error is only sent if the connection is still alive.
BUG/MINOR: mux-h1: don't keep fast-forwarding disabled for the next streams
H1C_F_CANT_FASTFWD is set on the H1 connection when the opposite endpoint of
the current stream does not support fast-forwarding (or when it failed). It
prevents the mux from announcing it may fast-forward data. But it is never
cleared, so once a stream faced such an endpoint (e.g. a response produced
by an applet or with filters), zero-copy forwarding remains disabled for all
the next streams of a kept-alive frontend connection, and for all the
streams of all the clients reusing an idle backend connection. It only
affects performance.
This flag depends on the stream's opposite endpoint, so it is now cleared
when the H1 stream is destroyed, like H1C_F_WANT_FASTFWD.
This was introduced by commit 2d80eb5b7 ("MEDIUM: mux-h1: Add
fast-forwarding support") in 2.9. It may be backported as far as 3.0.
BUG/MINOR: mux-h1: reset the content-length/chunking state after a 1xx response
Since commit a6b32922f ("MINOR: mux-h1: Keep custom "Content-Length: 0"
header in 1xx and 204 messages"), h1_make_headers() emits a
"content-length: 0" header found in a 1xx response and marks it as emitted
with H1S_F_HAVE_CLEN. But once the 1xx response is sent, h1_make_eoh() resets
the response state for the final response and only clears H1S_F_HAVE_O_CONN.
H1S_F_HAVE_CLEN remains set, so the final response's content-length header
is either considered as a duplicate and compared to the reset body length
(0), which makes the response fail with a processing error after its status
line was sent if it is not 0, or skipped when it is 0, leaving a keep-alive
response without any length. H1S_F_HAVE_CHNK could be affected the same way.
The H1 parser drops content-length from 1xx responses, so this can only
happen with servers using another protocol (e.g. H2), which may send it
despite RFC9110 forbidding it.
Now both flags are reset with H1S_F_HAVE_O_CONN after a 1xx response.
A reg-test was added to verify this.
This must be backported wherever commit a6b32922f was backported, so as far
as 3.2.
BUG/MEDIUM: mux-h1: don't stall when draining more than a buffer of data
When the H1 connection is in DRAINING state, h1_process() parses the input
data using a stream rxbuf, which limits the amount of data consumed per call
to a buffer minus the reserve. But the input buffer may contain more data
than that. If no more data are received, nothing calls h1_process() again:
the connection only subscribes for reads. The remaining data, the end of the
request payload and possibly the next pipelined request, are then left
unprocessed until the client timeout strikes, which kills the connection and
the pipelined request is lost.
Now, when data are still pending in the input buffer after a parsing pass in
DRAINING state, the parsing is retried.
This was introduced by commit 077906da1 ("MAJOR: mux-h1: Drain requests on
client side before shut a stream down") in 3.0. It must be backported as far
as 3.0.
BUG/MEDIUM: mux-h1: don't lose the end of the previous chunk in h1_make_chunk()
h1_make_chunk() is used during zero-copy forwarding to emit the chunk size
line of the next chunk, preceded by the CRLF ending the previous chunk when
the message is in H1_MSG_CHUNK_CRLF state. Both are written into a temporary
view of the output buffer which is only committed on success. But the state
was switched to H1_MSG_CHUNK_SIZE as soon as the CRLF was written into this
view. So if the chunk size line itself did not fit, the function returned on
the "full" path without committing anything, while the state indicated that
the CRLF was already emitted. On the next attempt, the chunk size was
emitted directly after the previous chunk's data, without the CRLF,
producing an invalid chunked message.
Now the state is only changed once both the CRLF and the chunk size were
successfully written.
This was introduced by commit 6dff013fa ("MINOR: mux-h1: Add function to add
size of a chunk to an outgoind message") in 2.9. It must be backported as
far as 3.0.
BUG/MEDIUM: h1: reject CONNECT request targets other than "host:port"
For CONNECT requests, the H1 parser validates the authority extracted by
http_parse_authority() (with userinfo removed and stopping at the first '/',
'?' or '#') against the Host header and checks it for forbidden
characters. But the request target itself is kept and forwarded as-is. So
the following targets were accepted with "Host: allowed.com:443":
and forwarded verbatim to the server (or as the :authority of an H2
request). RFC9112#3.2.3 says the authority-form only contains
"uri-host:port". The next hop (typically a forward proxy) may parse such
invalid targets differently than haproxy (e.g. "internal" port 22), and
connect to a destination that was not checked by the host-based rules.
Now, a CONNECT request is rejected (or the error is captured when unsafe
violations are accepted, like for other authority errors) if the
validated authority does not span the whole request target.
A reg-test was added to verify this.
This was introduced by commit 3f5fbe940 ("BUG/MEDIUM: h1: Improve
authority validation for CONNCET request") in 2.7 which validated the
stripped authority. It should be backported as far as 2.8.
BUG/MINOR: mux-h1: don't dereference a NULL opposite sedesc in h1_fastfwd()
In h1_fastfwd(), when the opposite stream endpoint descriptor cannot be
retrieved, the function jumps to the "out" label, which unconditionally
dereferences <sdo> (iobuf flags and se_done_ff()), leading to a NULL pointer
dereference.
It is probably not reachable today, because fast-forwarding is only
attempted when the opposite endpoint announced it can consume fast-forwarded
data, which implies both sides are linked. But the error path is wrong
anyway. Now the fast-forwarding is aborted in this case, like when nothing
can be done, so that the regular receive path is used instead.
This patch may be backported as far as 3.0 if needed.
BUG/MINOR: mux-h1: wake the connection up when a pending error can be sent
When h1_send_error() fails to allocate the output buffer, it sets
H1C_F_ABRT_PENDING and the connection is queued in the buffer wait list with
H1C_F_OUT_ALLOC. But when a buffer becomes available, h1_buf_available()
only handles H1C_F_OUT_ALLOC if an H1 stream exists, and then only wakes the
stream up for sending. When the error concerns a connection without H1
stream (e.g. a 408 on an idle connection or a 405 for an H2 preface received
on a kept-alive connection), or an embryonic H1 stream (no stream connector
to wake), nothing is done: the offered buffer is consumed for nothing,
H1C_F_OUT_ALLOC stays set so that any further attempt in h1_send_error()
gives up immediately, and the error is never sent. The connection then
lingers until its timeout.
In addition, even when the H1 connection's tasklet is woken up, h1_io_cb()
only calls h1_process() if some data were sent or received, so the pending
error would not be processed either.
Now, when an error is pending, h1_buf_available() clears the allocation flag
and wakes the connection's tasklet up, and h1_io_cb() always calls
h1_process() in this case so that the error is sent.
This can only happen when buffers allocation fails. The issue exists in some
form since the error handling on allocation failures was added, at least
since commit 56a499475 ("CLEANUP: mux-h1: Rename H1C_F_ERR_PENDING into
H1C_F_ABRT_PENDING") in 2.7. It may be backported as far as 2.8 after some
observation time.
BUG/MINOR: mux-h1: fix the open streams counter on stream creation failure
The "h1_open_streams" stats counter is incremented at the end of
h1c_frt_stream_new() and h1c_bck_stream_new(), and decremented in
h1s_destroy(). But when the stream connector attachment or the sedesc
allocation fails after h1s_new() succeeded, h1s_destroy() is called on the
error path and decrements the counter which was not incremented. The gauge
then drifts and may become negative.
The counter is now incremented in h1s_new() as soon as the H1 stream is
allocated, so that it is always balanced by h1s_destroy(). The total streams
counter is left where it was so that it only counts successfully created
streams.
This patch may be backported to all supported versions.
BUG/MINOR: mux-h1: only reset the session's timing info on the frontend side
When an H1 stream is detached, h1s_finish_detach() resets the session's
accept date, handshake and idle times so that the next request on the same
connection reports the right values. But this is also performed for backend
H1 streams, whose <sess> is the client's session. For an H1 frontend it does
not matter because the frontend stream is detached afterwards and resets
these values again. But for H2 and H3 frontends, the session is shared by
all streams of the client connection and the mux sets these values itself
when creating each stream. In this case the backend detach overwrites them,
so the next stream created on the client connection reports a wrong
accept/request date and idle time (%tr, %Ti, %Tt in logs): they are counted
from the end of the previous exchange with the server instead of from the
previous stream as the H2/H3 muxes intend.
For instance, with an H2 frontend and an H1 server, a client sending two
requests on the same connection, the first one taking 1 second, logs
"Ti=0 Tt=0" for the second one, instead of "Ti=1002 Tt=1002" with an H2
server or with this patch.
The timing info are now only reset for frontend H1 streams.
This patch may be backported to all supported versions.
BUG/MINOR: h1: don't use uninitialized start line pointers when restarting
When h1_headers_to_hdr_list() is called again on an incomplete message,
it resumes from the state where it stopped, scans until the end of the
headers, then restarts the parsing from the beginning. During this first
scan, the start line elements (union h1_sl) were not set by the current
call and are uninitialized. However two places in the response start
line parsing still use them:
- in H1_MSG_RPVER, sl.st.v.ptr is used to compute the version length
and to read the version's digits to set H1_MF_VER_11. The result is
discarded on restart, but this reads from a random pointer if the
computed length is 8;
- in H1_MSG_RPCODE, the number of digits is computed from sl.st.c.ptr
since commit 8bdcc5516 ("BUG/MEDIUM: h1: limit status codes to 3
digits by default"). With a random pointer, a valid response whose
status code is split across two reads (e.g. "HTTP/1.1 2" then
"00 OK...") may be rejected as invalid in strict mode, or its error
position captured in lenient mode, depending on the stack contents.
Now the start line pointers are initialized to the beginning of the
message, and these checks are skipped during the first scan of a
restart, since they are performed again during the full parsing.
This only concerns responses, so it can only be triggered by a server.
It must be backported wherever commit 8bdcc5516 was backported. The RPVER
part may be backported further but it is harmless.
BUG/MEDIUM: mux-h1: don't truncate responses on soft-stop
During a soft-stop, h1_process() releases the idle frontend connections,
i.e. those flagged with H1C_F_WAIT_NEXT_REQ. But this flag is set by
h1s_destroy() as soon as both the request and the response reached the DONE
state, which only means that the response was entirely formatted into the
output buffer, not that it was sent. So when the client is still downloading
the end of a response, the connection is released and the remaining data are
lost, the client receiving a truncated response.
The connection is now only released if its output buffer is empty.
Otherwise it is kept until everything was sent: h1_send() then wakes
h1_process() up which performs the release.
This was introduced by commit efc6e9564 ("MEDIUM: mux_h1: release idling
frontend conns on soft-stop") in 2.4. It must be backported to all supported
versions.
BUG/MEDIUM: mux-h1: don't truncate the response when closing after draining
When the response is finished while the request is not, the H1 stream is
detached and the H1 connection switches to the DRAINING state to consume
the end of the request before processing the next one. When the end of
the request is received and the connection must not be kept alive (e.g.
"Connection: close" or HTTP/1.0 request), h1_process() shuts the
connection down for writes and immediately releases it, without checking
whether the end of the response is still pending in the output buffer.
Thus any response data still waiting to be sent are lost and the client
receives a truncated response.
This happens with responses produced while the request payload is still
being uploaded (http-request deny/return/redirect/auth, or a server
responding before reading the request payload) when the client does not
read the response while uploading. For instance, with a 120kB response
returned by "http-request return" (tune.bufsize 131072), a client sending
a 1MB POST request with "Connection: close" and a small receive buffer
gets only the first 12kB of the response once its upload is finished
(the "H1C released with pending output data" debug counter is
incremented). It works in keep-alive mode because h1s_finish_detach() is
used and takes care of the output buffer.
Now, in this case, the H1 connection is switched to the closing state
and the H1 stream is destroyed. Then, the connection is only shut down
and released if there are no more pending output data. Otherwise, the
shutdown is performed by h1_send() once all data are sent, as for the
other paths.
This was introduced by commit 077906da1 ("MAJOR: mux-h1: Drain requests
on client side before shut a stream down") in 3.0. It must be backported
as far as 3.0.
BUG/MEDIUM: h1-htx: don't stall on messages ending exactly at the buffer's end
h1_parse_msg_hdrs() realigns the input buffer when its data wrap, then
passes b_tail() as the end of the area to parse. But when the data end
exactly at the end of the buffer's area (head + data == size), they do not
wrap so no realign is performed, while b_tail() wraps to the buffer's
origin, blocking parsing of headers. This can only happen with pipelined
requests.
Because of this issue, the parsing of headers can be blocked waiting for
more data and the http-request timeout can be reached, especially if it
happens exactly on the end of headers for a bodyless request.
In #3540, the same issue led to a 414 error returned on a valid request.
Parsing of trailers don't have the issue because it compares b_peek(ofs) to
b_tail(), but it misses the case of a full buffer whose head is not at the
beginning (start == end, without wrapping).
To fix both, the area is realigned only if the data really wrap, and the
end of the area to parse is computed with b_stop(), which points to the
byte following the end of the data, even when they end exactly at the
end of the buffer's area: b_stop() only wraps after the end of the area,
never to the buffer's origin, contrary to b_tail().
This bug exists since the H1 mux was introduced.
This patch should fix the issue #3540. It must be backported to all supported
versions.
BUG/MEDIUM: h1: don't rewrite the Upgrade header when not cleaning headers
Since commit 8dd49dfab ("BUG/MEDIUM: h1: Skip all h2c values from Upgrade
headers during parsing"), h1_parse_upgrade_header() always rewrites the
Upgrade header value in place to remove the "h2c" and "h2" tokens. This is
desired for the H1 mux, which converts the message to HTX, but not for the
other users of h1_headers_to_hdr_list(). In particular, in TCP mode,
smp_prefetch_http() parses the raw request directly in the channel buffer to
evaluate HTTP sample fetches. There, the request bytes are forwarded as-is
to the server, so the client's Upgrade header was modified in flight.
The rewrite is now only performed when H1_MF_CLEAN_CONN_HDR is set, which is
the flag already used to clean the Connection header up and that is set by
the H1 and FCGI muxes. Without this flag, the value is left untouched and
only the H1_MF_UPG_WEBSOCKET/H1_MF_UPG_HDR flags are computed. The flag's
description was updated accordingly.
This must be backported wherever commit 8dd49dfab was backported, so to all
supported versions.
BUG/MINOR: h1: keep accepting unsafe violations after a parser reset
The H1 parser relies on h1m->err_pos being preset to -1 instead of -2 to
accept some violations ("option accept-unsafe-violations-in-http-request"
and "option accept-unsafe-violations-in-http-response"). The H1 mux sets it
when the H1 stream is created. However the H1 message is reset in a few
places using h1m_init_req() or h1m_init_res(), which set err_pos back to -2:
- in h1_process_demux(), when a 1xx interim response was received, to
parse the final response;
- in h1_postparse_req_hdrs() and h1_postparse_res_hdrs(), when the headers
do not fit in the output buffer and the parsing must be restarted later.
As a result, the strict mode is silently enforced on the final response
following a 1xx interim response (or after an output full condition), and a
response accepted otherwise is rejected with a 502.
To fix the issue, the lenient mode is restored after these resets: from the
proxy's options in the mux, and from the previous err_pos value in the
h1_postparse_* functions.
BUG/MEDIUM: h1: make the Connection/Upgrade headers cleanup idempotent
The H1 parser rewrites the Connection header value in place to remove the
"close" and "keep-alive" tokens (H1_MF_CLEAN_CONN_HDR, always set by the H1
mux), and the Upgrade header value to remove the "h2c" and "h2" tokens. The
kept tokens are moved to the beginning of the value and the value's length
is reduced, but the original bytes beyond the new length are left untouched
in the input buffer.
However the parser is designed to restart from the beginning of the message
once an incomplete one is completed, and it relies on in-place modifications
being idempotent. When the headers block is received in several parts and
one of these headers was already processed in a first pass, the second pass
parses the rewritten value followed by the remains of the original one.
To fix the issue, the bytes freed at the end of the rewritten value are now
replaced with spaces so that parsing the same area again gives the same
value (trailing spaces are trimmed). And since the removed "close" and
"keep-alive" tokens are not there anymore on the second pass, the
H1_MF_CONN_CLO and H1_MF_CONN_KAL flags are now preserved when the parsing
is restarted, and when the parser is reset after an output full condition.
The Connection header issue exists since commit a51ebb7f5 ("MEDIUM: h1: Add
an option to sanitize connection headers during parsing") in 2.0, the
Upgrade one since commit 8dd49dfab ("BUG/MEDIUM: h1: Skip all h2c values
from Upgrade headers during parsing") in 3.4 (and its backports). This must
be backported to all stable versions.
BUG/MEDIUM: mux-h1: Don't close the connection on a payload parsing error
When a parsing error is reported by the H1 mux on the payload of a message
(chunked encoding, content-length, ...), after the stream was created, the
SE_FL_ERROR flag was set on the stream endpoint. This flag reports a fatal
error and the stream closes the client connection before the HTTP analyzers
can build and send the 400 response from HTX_FL_PARSING_ERROR. In the end,
no error is sent to the client and it only sees a connection close.
Before the commit 4ccca7e ("BUG/MINOR: mux-h1: Only report a SE error on
demux error"), SE_FL_EOI/SE_FL_EOS were reported and the analyzers had a
chance to send the error response before the connection closure. But, at
that time, the error was not reported to the termination events. Now, the
error is reported as EOI when the stream connector is still attached, and
the error itself is reported by the mux via the termination events and
consumed by the analyzers via the htx flags. SE_FL_ERROR is only reported
when there is no stream connector attached to the h1s, because it means the
mux owns the error and will handle it itself.
No configuration is impacted because the connection is always closed after
such an error. This should be backported as far as 3.2.
OPTIMIZE: stream: Stop holding a reference on the frontend during stream_free()
Holding a reference on the frontend during the whole stream teardown has a
performance impact: all the streams of a frontend atomically increment and
decrement the same counter.
Instead, the frontend capture pools are flushed before the stream connectors
teardown and the remaining pools after it, depending on a flush_pools
flag. That way the frontend is no longer dereferenced after sc_destroy().
Must be backported with the SPOE fix b5db88d9c ("spoe: Let an agent outlive
its applets during a backend deletion"), as far as 3.3.
MINOR: filters: Support the global side in filter_place_class()
The filter classes are anchored in three independent orders: the global one,
which drives the side-independent operations (configuration checks,
initialization and destruction), and the request and response ones, which
drive the stream evaluation order. Until now the global order could only
follow the class registration order, filter_place_class() only accepted
FLT_SIDE_REQ and FLT_SIDE_RES, and the per-side anchors were two anonymous
inline structs, which made the global order impossible to refine and the
placement code side-specific.
The three orders now share a common anchor type, struct filter_class_side,
holding the before/after constraint lists, the link in the order list and a
placed flag. The class registry (filter_class.list) stays a flat list in
registration order, used by filter_find_class() and "haproxy -vv", while the
global order is a new distinct list, global_filter_classes, rooted in
filter_class.global.list. As for the channel sides, the global order is a
placement tree: a registered class is appended at the end of it, and
filter_place_class() re-parents it relative to another class.
filter_place_class() now accepts FLT_SIDE_GLOBAL. Any registered class may be
used as a reference on that side, since the global order contains all of them,
whereas a channel side still requires the reference class to be placed on that
same side. Moving a class in the global order does not change its request or
response positions, and the three placements remain independent. Two new
errors are reported: a class cannot be placed relative to itself, and a class
cannot be placed twice on the same side.
filter_register_class_full() places the class on the global side as well, so
the helper keeps placing the class symmetrically in every order. Its error
path now detaches the class from all its orders, through the new
flt_unplace_class() helper, instead of only unlinking it from the registry.
The default order of the internal classes does not change: the same classes
are appended to the global order, which is required for the global class
references to be created at proxy initialization. This only makes the global
order refinable by modules and lifts the last asymmetry between the three
orders.
The documentation is updated accordingly, in both the configuration manual
and the internals guide, together with the example in the comment of
filter_init_classes().
BUG/MINOR: filters: fix silent compression disable with legacy "filter compression" first
The legacy "filter compression" declaration stored a pointer on the proxy
compression options in the filter configuration at parsing time. When the
"compression" directives were placed after the filter declaration, the
options were not allocated yet, so the filter kept a NULL pointer and the
compression was silently disabled at runtime.
The compression options are now allocated when the filter is declared, the
"compression" directives parsed later reuse the same structure, in both
declaration orders.
This bug was introduced by the commit d759f474 ("MINOR: flt-http-comp: Use
compression options as filter config"), which started to store the pointer
on the proxy compression options in the filter configuration and to check
it at runtime, while it was previously always NULL and the options were
resolved at runtime through the frontend and backend proxies. There is no
need to backport this fix, it is a 3.5-specific issue.
BUG/MEDIUM: cli: Fix parsing of big payloads with dynamically allocated buffer
Since the 3.4, a dedicated buffer is used for big payload on the CLI. When
the command line exceeds the buffer size because of a big payload, a
dedicated buffer is dynamically allocated to copy the payload. However, on
realloc, input data were moved in the dynamic buffer without parsing
them. This could prevent the end of the payload from being correctly
detected.
To fix the issue, instead of moving input data in the newly allocated
buffer, we retry the parsing.
But a payload line may be longer than tune.bufsize. In this case, only the
beginning of the line is present in the input buffer, and the parsing
always fails to find its end. The input buffer is never emptied and the
flag reporting a full input buffer is never cleared, so the peer is never
unblocked and the remaining part of the line is never received. The CLI
keeps looping on the same partial line, burning the CPU, and the command
is never executed. This was already the case before, when the input data
were moved in the dynamic buffer without parsing them: the part of a line
already received could be moved several times without progress.
Thus, when the parsing fails because the input buffer contains an
incomplete payload line, the part already received is moved into the
payload buffer, waiting for more data, and the full-input-buffer flag is
cleared. For this purpose, the beginning of the incomplete current payload
line is saved in the CLI context, and updated when the payload buffer is
reallocated. When a full line is received, the end of the payload is
searched in the whole assembled line: the end of the payload must also be
detected when the line marking it is itself received in several chunks.
This patch should fix the issue #3524. It must be backported to 3.4.
BUG/MINOR: quic: valid NEW_TOKEN connections are counted as half-open
Commit 29fb1aee5 ("MINOR: quic-be: QUIC connection allocation adaptation
(qc_new_conn())") replaced the token presence check with token_odcid->len
when marking a listener connection's peer address as validated.
Only Retry token validation populates token_odcid. A connection created
with a valid NEW_TOKEN token is therefore counted in half_open_conn and
remains subject to the anti-amplification limit until a Handshake packet
is successfully processed, even though the token has been accepted.
This does not prevent the connection from being established, but the
unnecessary anti-amplification limit may delay the handshake. The inflated
half_open_conn counter may also reach the Retry threshold earlier, causing
more Retry packets to be sent to other clients connecting without a token.
Use initial_pkt->token_len instead, as any non-empty token has already
been validated by the caller before allocating the connection. This marks
the peer address as validated for both accepted token formats without
changing the outgoing connection handling.
Thank you to Yeonggi Kim for having reported this issue on the haproxy mailing
list.
MINOR: quic: use the same HP encryption helpers for RX and TX
Header protection is applied by generating a mask and XORing it with
the header. Removing it requires the same mask and the same XOR, which
restores the original header.
With AES-128-CTR, AES-256-CTR and ChaCha20, encryption and decryption
produce the same mask from the same key and packet sample. This is a
property of these algorithms, not specific to OpenSSL.
Use quic_tls_enc_hp_ctx_init() and quic_tls_hp_encrypt() for RX too,
with the RX key, and remove the redundant decryption helpers.
BUG/MEDIUM: peers: keep the earliest sync task deadline
When a peer reconnects, heartbeats to another idle peer may be delayed.
The remote peer may then close the connection due to inactivity.
All peers in a section share one sync task. While processing each peer,
the task's deadline is overwritten, even when an earlier peer needs to
wake up sooner.
Keep the earliest deadline across all peers, both when updates are pending
and when peers are idle.
The faulty assignments were introduced by commit 045e0d4b3 ("BUG/MINOR: peers: Really close the sessions with no heartbeat.")
during 2.0 development.
Thank you to @snh for having reported this issue in GH #3536.
This fix should be backported to all maintained stable branches.
QUIC key and token derivation currently uses EVP_PKEY for all HKDF
operations, creating and configuring a context for each derivation.
Use HKDF_extract(), HKDF_expand() and HKDF() directly with AWS-LC and
BoringSSL. With OpenSSL 3, use EVP_KDF directly to bypass the EVP_PKEY
compatibility layer and pass the parameters in a single call. Keep the
EVP_PKEY approach for the other SSL libraries.
Centralize the three HKDF modes in a common helper and make the extract
and expand wrappers private to quic_tls.c.
Local HTTP/3 tests with an unchanged HALoad client, one full handshake
per request and empty responses showed these median request rates
and paired gains:
SSL library HATerm HEAD HATerm patched Paired gain
OpenSSL 3.5.4 486 540 +11%
AWS-LC 5.9.0 903 950 +4%
without session resumption.
Validated with RFC 5869 and RFC 9001 vectors and full QUIC handshakes.
MEDIUM: quic: implement the AEAD confidentiality limit
RFC 9001 section 6.6 limits the number of packets which may be encrypted
with the same keys and requires a key update before this budget is
exhausted. HAProxy did not enforce this limit or initiate local key
updates.
Count successful packet encryptions in a new <count> member of
quic_tls_secrets. Check the 1-RTT TX count from qc_send() and attempt a
key update from seven eighths of the confidentiality limit, subject to
the handshake confirmation and acknowledgment requirements of RFC 9001
section 6.1. This leaves some budget to keep sending while these
requirements are not yet satisfied.
Also check the remaining budget from qc_may_build_pkt(): a single call
to qc_send() may encrypt several packets, so checking only on entry
cannot prevent the limit from being exceeded. Reserve the last allowed
encryption for CONNECTION_CLOSE. If no update is possible before this
reserve is reached, close with AEAD_LIMIT_REACHED while the current
keys can still protect the closing packet.
quic_tls_initiate_key_update() is the local counterpart of a peer-
initiated update. Reset the TX count and record the first packet number
of the new phase when rotating keys. Track locally initiated updates
with QUIC_FL_TLS_KP_LOCAL_IN_PROGRESS so that packets still sent by the
peer with the previous keys can be decrypted without triggering another
rotation. Clear this flag when a packet authenticates with the current
keys.
For AES-GCM, the limit is 2^23 packets per key, approximately 10 GB for
1200-byte packets. Long-lived connections can therefore reach it.
ChaCha20-Poly1305 has a confidentiality limit greater than the QUIC
packet number space and does not require this check.
Validated with haterm and haload over HTTP/3 using AES-128-GCM. A
single-connection transfer triggered a local update naturally at
7,340,032 encrypted packets, without counter injection. The TX count
was reset to zero and the transfer completed successfully. Injected
boundary scenarios also verified that CONNECTION_CLOSE consumes the
reserved packet without exceeding the limit.
This introduces locally initiated key updates for the first time.
Keep it in 3.5 for interoperability exposure before considering a
backport.
BUG/MINOR: mux_quic: do not crash on SE_SHR_DRAIN shut
qcm_strm_shut() callback was extended to ensure stream is properly
closed. Only SE_SHR_DRAIN mode was left unsupported as it was considered
not possible to encounter it. As such a BUG_ON() was added in shut
callback.
Since QUIC backend support, SE_SHR_DRAIN may be used on shut for checks.
As such, it is required to properly handle it. This patch removes the
BUG_ON() and treat SE_SHR_DRAIN similarly to SE_SHR_RESET.
BUG/MINOR: check: do not inspect errno if conn FDLESS
On check failure, conn errno is inspected via
retrieve_errno_from_socket(), which relies on the connection FD. When
using a CO_FL_FDLESS connection, this triggers a BUG_ON().
This causes crashes for QUIC backend checks, available since 3.3. To fix
this, simply skip call to retrieve_errno_from_socket() when CO_FL_FDLESS
is set.
BUG/MINOR: mux_quic: prevent RS/SS on unistream with hq-interop
When app layer has been shut, qcs_new() will immediately abort a new
stream via attach callback.
App proto hq-interop does not define shutdown callback. In this case, it
is expected that the app layer won't reject a new stream during attach.
MUX QUIC is thus responsible to shut the stream itself using
RESET_STREAM + STOP_SENDING.
This causes a protocol violation if the opened stream is unidirectional.
On RESET_STREAM emission, the peer may close the connection on fatal
error, aborting any possibly still active transfers. Fix this by
ensuring only STOP_SENDING is emitted in this case.
In practice, this bug is not encountered today as it only impacts
hq-interop protocol, and uni streams are not used in this case.
BUG/MEDIUM: mux_quic: prevent uaf on fctl frame after send error
When performing emission in QUIC MUX, flow control frames are emitted
first via qcc_emit_fctl(), with a single frame per packet. Emission is
interrupted if an error is returned by the transport layer via
qcc_send_frames().
For each successful MAX_STREAM_DATA frame emission, <msd_frm> pointer of
QCS must be reset. This is necessary as the frame ownership moved to the
transport layer.
Previously, in case of qcc_send_frames() error, input frame was
reinserted in the MUX send list and QCS ownership of the frame would
remain. This is incorrect though in case of a transient error (for
example EAGAIN), as transport layer now owns the frame as it is
responsible to retry emission later. When the frame is finally emitted
and freed by the transport layer, an UAF may occur when QCS flow control
is updated.
This bug occurs with connection owned sockets only. This is because
transient socket errors are simply ignored when using the listener
socket.
To fix this, rewrite qcc_emit_fctl() error path. Cleanup of <msd_frm> is
now performed systematically if frame input list is empty. The function
is then still interrupted if qcc_send_frames() returned an error.
This must be backported up to 3.2, after a period of observation.
MINOR: quic: rename confusing qc_purge_txbuf() to qc_flush_txbuf()
Function qc_purge_txbuf() naming is confusing as it may imply that
prepared data are simply discarded. In fact, the objective of this
function is to first send all of its content before clearing it.
This names also conflicts with qc_purge_tx_buf() (note the extra
underscore) which reset the buffer. Thus, renaming the first one is
better to prevent any confusion.
Yeonggi Kim [Thu, 15 Oct 2026 23:45:58 +0000 (08:45 +0900)]
BUG/MINOR: quic: handle EINTR on emission of packets without connection
Retry, Version Negotiation and Stateless Reset packets are emitted
without connection via quic_lstnr_sock_send(). If sendmsg() is
interrupted by a signal, the error is reported to the caller and the
packet is lost, while qc_snd_buf() retries the emission on EINTR.
Also retry the emission on EINTR in quic_lstnr_sock_send(). The impact
is limited, as the listener socket is non-blocking so sendmsg() is not
expected to be interrupted in practice, and a new packet will be
emitted in response to the next ones from the peer.
This may be backported along with the previous patches up to 2.8.
Yeonggi Kim [Thu, 15 Oct 2026 23:45:57 +0000 (08:45 +0900)]
REGTESTS: quic: check Retry source address on a wildcard listener
Add a new QUIC regtest for Retry emission through a listener bound on a
wildcard address. The Retry must be emitted with the address targeted
by the client as source, else a client relying on a connected socket
drops it and the handshake never completes.
This test relies on two haproxy instances, as QUIC client and server.
The QUIC server is reached through 127.0.0.2, while the kernel would
select 127.0.0.1 as source to reply to the client. As 127.0.0.2 is a
local address by default only on Linux, the BSD, macOS, Solaris and
generic targets are excluded. The wildcard listener port reuses the
number of the TCP port allocated by vtest for the same frontend, and the
CLI is used to wait for the QUIC listener to be ready before starting
the client.
This test is related to github issue #3503. As it requires QUIC backend
support, it may only be backported to 3.3 and above, along with the fix
"BUG/MEDIUM: quic: use datagram destination as source for Retry".
Yeonggi Kim [Thu, 15 Oct 2026 23:45:56 +0000 (08:45 +0900)]
BUG/MINOR: quic: use datagram destination as source for version negotiation
When a QUIC listener is bound on a wildcard address, the source address
of emitted datagrams must be the local address targeted by the client,
else the kernel selects it from the route to the client. Both may differ
when several addresses are assigned to the host, for example VIPs
configured on a loopback interface behind a load balancer in direct
routing mode. In this case, QUIC clients relying on a connected UDP
socket silently drop the datagram.
Version Negotiation packets are emitted without connection, via
send_version_negotiation() which used a bare sendto() on the listener
socket without source address. In the above conditions, a client which
offers an unsupported QUIC version never receives the list of supported
versions, so it cannot switch to one of them.
Fix this by adding a source address parameter to
send_version_negotiation(), before the destination one, similarly to the
previous patches for Retry and stateless reset. Its caller sets it to
the datagram destination address, and emission now relies on
quic_lstnr_sock_send(), which only uses it if the listener is bound on a
wildcard address. Apart from the source address, the emission is
unchanged.
This should fix github issue #3503.
This should be backported up to 2.8. Note that it relies on the first
two patches of the series:
- MINOR: quic: use the listener for Retry and Version Negotiation emission
- MINOR: quic: define a function to emit a datagram on a listener socket
Prior to 3.4, quic_dgram addresses are of sockaddr_storage type, so the
casts are not necessary. Prior to 2.9, send_version_negotiation() is a
static function defined in quic_conn.c.
Yeonggi Kim [Thu, 15 Oct 2026 23:45:55 +0000 (08:45 +0900)]
BUG/MINOR: quic: use datagram destination as source for stateless reset
When a QUIC listener is bound on a wildcard address, the source address
of emitted datagrams must be the local address targeted by the client,
else the kernel selects it from the route to the client. Both may differ
when several addresses are assigned to the host, for example VIPs
configured on a loopback interface behind a load balancer in direct
routing mode. In this case, QUIC clients relying on a connected UDP
socket silently drop the datagram.
Stateless reset packets are emitted without connection, via
send_stateless_reset() which used a bare sendto() on the listener socket
without source address. In the above conditions, the client never
receives the stateless reset. This typically happens after a restart of
haproxy with a configured cluster-secret, for clients which still use
connection IDs of the previous process: instead of being notified
immediately, they only give up after their idle timeout.
Fix this by adding a source address parameter to send_stateless_reset(),
before the destination one, similarly to the previous patch for Retry.
Its caller sets it to the datagram destination address, and emission
now relies on quic_lstnr_sock_send(), which only uses it if the listener
is bound on a wildcard address. Apart from the source address, the
emission is unchanged.
This should fix github issue #3503.
This should be backported up to 2.8. Note that it relies on the first
two patches of the series:
- MINOR: quic: use the listener for Retry and Version Negotiation emission
- MINOR: quic: define a function to emit a datagram on a listener socket
Prior to 3.4, quic_dgram addresses are of sockaddr_storage type, so the
casts are not necessary. Prior to 2.9, send_stateless_reset() is a
static function defined in quic_conn.c.
Yeonggi Kim [Thu, 15 Oct 2026 23:45:54 +0000 (08:45 +0900)]
BUG/MEDIUM: quic: use datagram destination as source for Retry
When a QUIC listener is bound on a wildcard address, the local address
targeted by the client is retrieved on reception via IP_PKTINFO or
equivalent. It is then used as source address for all datagrams of the
connection, either by binding the connection socket on it, or via
ancillary data when the listener socket is used. Without it, the kernel
selects the source address from the route to the client, which may
differ from the one targeted by the client. This is typically the case
when several addresses are assigned to the host, for example VIPs
configured on a loopback interface behind a load balancer in direct
routing mode. As QUIC clients usually rely on connected UDP sockets,
datagrams from another address are then silently dropped by their
kernel.
However, Retry packets are emitted before any connection exists, via
send_retry() which used a bare sendto() on the listener socket without
source address. In the above conditions, the client never receives the
Retry and keeps retransmitting its Initial packet until it gives up.
Retry is emitted in particular when quic-force-retry is set on the bind
line, when a quic-initial send-retry rule matches, or when the number of
half-open connections reaches tune.quic.fe.sec.retry-threshold, so
handshakes may also fail only intermittently under load. Apart from not
using Retry at all, which removes client address validation, the usual
workaround is to explicitly bind each address, in addition to or instead
of the wildcard one.
Fix this by adding a source address parameter to send_retry(), before
the destination one. Its callers set it to the datagram destination
address, and emission now relies on quic_lstnr_sock_send(), which only
uses it if the listener is bound on a wildcard address. Apart from the
source address, the emission is unchanged. Also fix the trace emitted
on send failure, which was a copy of the previous one.
This should fix github issue #3503.
This should be backported up to 2.8. Note that it relies on the two
previous patches:
- MINOR: quic: use the listener for Retry and Version Negotiation emission
- MINOR: quic: define a function to emit a datagram on a listener socket
Prior to 3.4, quic_dgram addresses are of sockaddr_storage type, so the
casts are not necessary. Prior to 2.9, send_retry() is a static
function defined in quic_conn.c.
Yeonggi Kim [Thu, 15 Oct 2026 23:45:53 +0000 (08:45 +0900)]
MINOR: quic: define a function to emit a datagram on a listener socket
Most QUIC datagrams are emitted via qc_snd_buf() which relies on a
quic_conn instance. When the connection uses the listener socket and the
listener is bound on a wildcard address, the local address retrieved on
reception is set as datagram source via ancillary data. This guarantees
that replies are emitted from the address targeted by the client, which
may otherwise drop them if its socket is connected.
However, some packets are emitted before any connection instance
exists: Retry, Version Negotiation and Stateless Reset. These are sent
via a bare sendto() on the listener socket, without source address.
Define a new function quic_lstnr_sock_send() for these packets. It
emits a datagram through a listener socket, with an extra parameter for
the local address targeted by the peer. This address is set as source
via cmsg_set_saddr() as in qc_snd_buf(), only if the listener is bound
on a wildcard address. This rule is now defined in a new function
quic_lstnr_may_set_src(), on which qc_may_use_saddr() also relies.
Apart from the source address, the emission is identical to the
existing sendto() calls: no flag is set and EINTR is not retried. As the
listener socket is non-blocking, a comment highlights that a datagram
is lost if the socket buffer is full, as there is no connection to retry
its emission later.
This patch does not introduce any functional change. It is a
prerequisite for the following fixes on Retry, Stateless Reset and
Version Negotiation emission. As such, it should be backported with
them up to 2.8.
Yeonggi Kim [Thu, 15 Oct 2026 23:45:52 +0000 (08:45 +0900)]
MINOR: quic: use the listener for Retry and Version Negotiation emission
send_retry() and send_version_negotiation() take the listener socket FD
as first argument, while send_stateless_reset() takes the listener
instance. Make the first two also take the listener instance. Also
rename their <addr> argument to <dst> to remove any ambiguity, as it is
the destination of the emitted datagram, and fix send_stateless_reset()
comment which still mentioned a FD.
This patch does not introduce any functional change. It is a
prerequisite for the following fixes on Retry, Stateless Reset and
Version Negotiation emission. As such, it should be backported with
them up to 2.8. Prior to 2.9, these functions are static functions
defined in quic_conn.c.
Olivier Houchard [Thu, 15 Oct 2026 15:12:18 +0000 (17:12 +0200)]
BUILD: sym: Keep a temporary binary without .gnu_debugdata
When we linked the binary, before we attempt to generate its
.gnu_debugdata section, copy the binary, as haproxy.noxz, just so that
if for any reason, adding the new section fails, we at least still have
a working binary.
Olivier Houchard [Thu, 15 Oct 2026 14:50:23 +0000 (16:50 +0200)]
BUILD: sym: Build properly with gold as a linker
Some linkers, such as gold, will not let us do our trick of having our
note in our own program header, and will merge it with other notes. If
that happens, then detect it, and just link the binary a second time,
now that we know the size of the section, as we created the xz file
already, we can add it without wasting any binary space.
Olivier Houchard [Wed, 14 Oct 2026 13:35:36 +0000 (15:35 +0200)]
BUILD: sym: Make sure __always_inline always is inline.
__always_inline is provided by most OS, but on Linux and FreeBSD, it
does come with the "inline" keyword, while it does not at least on
NetBSD.
Solve the issue by undefining it, and always providing "inline", as it
is what the xz code expects, otherwise gcc will be unhappy.
Olivier Houchard [Wed, 14 Oct 2026 13:23:14 +0000 (15:23 +0200)]
BUILD: sym: Get it to build and work on various OSes
Define NT_GNU_BUILD_ID and STT_GNU_IFUNC, as those are GNU extensions,
and not every OS defines it.
Define ElfW() if it is not provided by the OS already.
Make sure sections are actually readable, as sometimes they are not on
OpenBSD.
The NetBSD linker does not use an empty name for the executable, so
don't recognize it that way. However, every OS will provide the
executable first, so rely on that instead.
With those changes, this builds and works on NetBSD, OpenBSD and
Solaris/Illumos.
Olivier Houchard [Wed, 14 Oct 2026 11:23:54 +0000 (13:23 +0200)]
BUILD: sym: Make sure minidebug works correctly with most environments
Do not assume we're using GNU binutils when building the minidebug
section, so only use options most tools understand, and check if nm
actually exports the symbols in the format we expect.
Also do not use grep -F, as it doesn't exist on Solaris/Illumos, and we
don't actually need it.
This should help with github issue #3531.
Amaury Denoyelle [Thu, 15 Oct 2026 12:47:34 +0000 (14:47 +0200)]
REGTESTS: use HAPROXY_TEST_TIMEOUT for wait in purge srv/be
Replace hardcoded value for wait timeout in regtests for server/backend
purge mechanism. The variable HAPROXY_TEST_TIMEOUT is used with 5s as a
default value if unspecified, similar to what is already used for
haproxy timeout in most of the test suite.
This should prevent spurrious test failures due to timing issue. In
particular, this should fix github CI runners execution.
Some timers remain hardcoded to small values when wait is expected to
either fail or suceed immediately. A larger value would cause the test
to take an unnecessary delay when a real failure should be reported.
Amaury Denoyelle [Wed, 14 Oct 2026 13:40:56 +0000 (15:40 +0200)]
BUG/MINOR: quic: rearm packet on packet received even non ack-eliciting
QUIC idle timeout must be refreshed for each valid packet received.
Previously, this was only performed for ack-eliciting packets. In case
the client only emit non ack-eliciting frames, the connection would be
closed despite being still active.
Fix idle timeout mechanism by calling qc_idle_timer_rearm() on every
packets, outside of QUIC_FL_RX_PACKET_ACK_ELICITING flag. As previously,
in case of an ack-eliciting packet, both idle and ack expire are
refreshed, but the latter will be queued as it is smaller.
This issue was detected with SSE running on top of an HTTP/3 connection,
with the server pushing updates and the client emitting only ACKs. This
should fix github issue #3534.
This must be backported up to 2.6, after a period of observation.
BUG/MEDIUM: stats-file: shm-stats-file be counters not working anymore
As described in GH #3527, a weird behavior affects backend shared counters
where haproxy would restore stats inconsistently. In fact, the described
behavior happens because the stats file preloading mechanism is broken
for backend objects since 5495c88 ("MEDIUM: counters: Dynamically allocate
per-thread group counters") because we checked for the frontend shared map
(allocated once per process) instead of the backend one for backend
objects, and the frontend map is only allocated for frontends or hybrid
proxies, and not backends. This caused the preloading to abort.
We fix by checking the relevant backend oriented map instead. It should be
backported to 3.4.
Thanks to GH user @ssavant2 for his detailed report and patch feedback.
Willy Tarreau [Wed, 14 Oct 2026 14:03:07 +0000 (16:03 +0200)]
[RELEASE] Released version 3.5-dev9
Released version 3.5-dev9 with the following main changes :
- CLEANUP: sym: move symbol resolution out of tools.c
- MINOR: sym: add an in-process symbol table for backtraces
- MINOR: sym: collect the symbols known to the dynamic linker
- MINOR: sym: add helpers to collect the symbols of an ELF image
- IMPORT: xz: import the xz-embedded decoder
- MEDIUM: sym: store a compressed symbol table in a loadable section
- MINOR: sym: read local symbols and debug info from ELF files
- MINOR: sym: also read the compressed symbol tables from the ELF files
- OPTIM: sym: skip the embedded table when the file provided the symbols
- MINOR: makefile: add CROSS_TOOLCHAIN to prefix the target's tools
- BUG/MINOR: proxy: add PR_CAP_LB for dynamically added backend
- BUG/MINOR: server: fix "del server" description
- BUG/MINOR: cli: update wait help usage
- BUG/MEDIUM: server: remove immediately from GUID tree on delete server
- DOC: server: explicit refcount interaction in "del server"
- MINOR: ocsp: Remove free of main structure from ssl_sock_free_ocsp_data
- MINOR: ocsp: Send OCSP response status back to caller
- MINOR: ocsp: Raise specific ocsp error code in ocsp parsing/verifying functions
- MINOR: ocsp: Dup OCSP_CERTID in ssl_ocsp_create_request_details
- MEDIUM: ocsp: Create new do-ocsp-request action
- MINOR: ocsl: Add 'inapplicable' return code to client certificate ocsp check
- DOC: ocsp: Add doc for the new 'do-ocsp-request' action
- REGTESTS: ocsp: Add 'do-ocsp-request' tests
- BUILD: ssl: fix build without USE_OPENSSL + clear_ocsp_clt_check_ctx
- DEV: sse-latency: add a latency measuring tool for SSE response streams
- MINOR: http-ana: automatically enable http-no-delay for streaming events (SSE)
- REGTESTS: cli: don't check the HAProxy string
- BUILD: Hopefully fix build on FreeBSD
- BUILD: sym: Define HA_HAVE_DL_ITERATE_PHDR on FreeBSD
- BUG/MINOR: log: fix erroneous error message in log-steps parser
- MINOR: log: add "loggers" section
- MINOR: log: support 'to' parameter for "log" directive
- MINOR: log: support explicit <loggers> list for do_log_ctx()
- MINOR: log: support 'to' parameter fo do-log action
- DOC: config: document new "loggers" section and relevant uses
- MINOR: ssl: support curve fetches with AWS-LC API 30
- BUILD: ssl: avoid mixed declarations in the AWS-LC curve fetch
- DEBUG: support reading set-dumpable from HAPROXY_FORCE_DUMPABLE
- BUG/MINOR: hq-interop: prevent double EOM on request with standalone FIN
- BUG/MINOR: ssl: fix trace crash on FE conn release in io cb
- BUG/MINOR: ssl: fix trace crash on BE conn release in io cb
- BUG/MEDIUM: ssl: crt-list ciphers ignored with AWS-LC
- BUG/MEDIUM: ssl: crt-list curves ignored with AWS-LC
- CI: github: test AWS-LC 3.x and 4.x LTS branches
- CI: github: test the latest AWS-LC release on development branches
- CI: github: cache QuicTLS builds
- CI: github: move wolfSSL 5.7.0 testing to a nightly workflow
- BUG/MEDIUM: listener: Consider maxconn before waking listeners
- BUG/MINOR: ssl: don't uppercase the native group name
- DOC: ssl: clarify the NID-based curve fetch branch
- BUG/MINOR: ssl: use standard names for X25519 and X448 groups
- MINOR: ssl: split curve names into ANSI, IANA and NIST columns
- MINOR: ssl: skip unavailable NIDs in curves2nid lookup
- MINOR: ssl: complete registered group names and library NID mappings
- MINOR: ssl: add curve2name converter
- MINOR: ssl: accept integer group IDs in curve2name
- BUG/MAJOR: mux-quic: prevent crash on QCS already set for purging
- BUG/MINOR: mux_quic: prevent purgeable QCS lingering
- BUG/MINOR: mux_quic: extend FINAL_SIZE_ERROR detection on stream
- BUG/MINOR: mux_quic: report conn error on RESET_STREAM fctl violation
- BUG/MEDIUM: mux_quic: account discarded data in connection flow control
- BUG/MINOR: mux_quic: account data in conn fctl on early detach
- BUG/MINOR: h3: reject frames with truncated header
- BUG/MINOR: h3: close connection on GOAWAY frame too long
- BUG/MINOR: h3: reject RS/SS for QPACK uni streams
- BUG/MEDIUM: mux_quic: notify stream on RESET_STREAM reception
- BUG/MINOR: ssl: Fix potential NULL-deref in "dump ssl cert" with AWS-LC
- BUG/MEDIUM: h3: fix end-of-input report on DATA wrapping
- MEDIUM: h3: do not close conn on unaligned non DATA frame
- MINOR: doc: elaborate on diagnostic mode behavior
- BUG/MEDIUM: h3: fix incorrect DATA size block on DEM FULL
- BUG/MAJOR: cfgparse: properly check userlist section when scanning keywords
- BUG/MINOR: cfgparse-global: add missing gotos to error label in max-spread-checks
- BUG/MINOR: cfgparse-global: add missing calloc() check for the global description
- BUG/MINOR: cfgparse-listen: release the previous description of the proxy
- BUG/MINOR: cfgparse-listen: fix the argument checked by "email-alert"
- BUG/MINOR: cfgparse-listen: fix quoting of the condition conflict warning
- BUG/MINOR: cfgparse-global: reject extra arguments to numa-cpu-mapping and anonkey
- BUG/MINOR: cfgparse-listen: always reject extra arguments to "persist"
- BUG/MINOR: cfgparse-listen: don't lose the errors reported while creating a proxy
- BUG/MINOR: cfgparse-listen: properly reject extra args in a backend section
- BUG/MINOR: proxy: also look for the closest option among cfg_opts3
- BUG/MINOR: cfgparse-listen: complete the list of options for the suggestions
- CLEANUP: proxy: adapt the error message for removed "option transparent"
- MINOR: cfgparse: expose the keyword modifier to the keyword parsers
- CLEANUP: cfgparse-global: move the argument-less boolean options to a parser
- CLEANUP: cfgparse-global: move "set-dumpable" to its own parser
- CLEANUP: cfgparse-global: move "uid", "gid", "user" and "group" to its own parser
- CLEANUP: cfgparse-global: move "external-check" to its own parser
- CLEANUP: cfgparse-global: move "cluster-secret" to its own parser
- CLEANUP: cfgparse-global: move the process-wide limits to a single parser
- CLEANUP: cfgparse-global: move the compression limits to a single parser
- CLEANUP: cfgparse-global: move "ssl-server-verify" to its own parser
- CLEANUP: cfgparse-global: move "node" and "description" to their own parser
- CLEANUP: cfgparse-global: move "unix-bind" to its own parser
- CLEANUP: cfgparse-global: move the log keywords to a single parser
- CLEANUP: cfgparse-global: move the state file keywords to their own parser
- CLEANUP: cfgparse-global: move the spread-checks keywords to their own parser
- CLEANUP: cfgparse-global: move "cpu-map" to its own parser
- CLEANUP: cfgparse-global: move "anonkey" to its own parser
- CLEANUP: cfgparse-global: ask not to add new keywords in common_kw_list[]
- CLEANUP: cfgparse-global: simplify cfg_parse_global()
- MINOR: cfgparse-listen: introduce proxy_parse_removed_kw() and use it
- MEDIUM: cfgparse-listen: move removed keywords' help messages into an array
- CLEANUP: cfgparse-listen: move "disabled" and "enabled" to their own parser
- CLEANUP: cfgparse-listen: move the connection limits to thier own parser
- CLEANUP: cfgparse-listen: move the backend one-arg keywords to their own parser
- CLEANUP: cfgparse-listen: move "mode" to its own parser
- CLEANUP: cfgparse-listen: move "id" and "description" to their own parser
- CLEANUP: cfgparse-listen: move "acl" to its own parser
- CLEANUP: cfgparse-listen: move the monitor keywords to their own parser
- CLEANUP: cfgparse-listen: move the persistence keywords to their own parser
- CLEANUP: cfgparse-listen: move "capture" to its own parser
- CLEANUP: cfgparse-listen: move the log format keywords to their own parser
- CLEANUP: cfgparse-listen: add parser for "log", "log-tag" and "unique-id-header"
- CLEANUP: cfgparse-listen: move the load-balancing keywords to their own parser
- CLEANUP: cfgparse-listen: move "source" "usesrc" and "interface" to their own parser
- CLEANUP: cfgparse-listen: add parser for server/backend sekection keywords
- CLEANUP: cfgparse-listen: move the HTTP rule sets to their own parser
- CLEANUP: cfgparse-listen: move "redirect" to its own parser
- CLEANUP: cfgparse-listen: move "stick-table" to its own parser
- CLEANUP: cfgparse-listen: move "stick" to its own parser
- CLEANUP: cfgparse-listen: move "cookie" to its own parser
- CLENAUP: cfgparse-listen: move "email-alert" to its own parser
- CLEANUP: cfgparse-listen: move "stats" to its own parser
- CLENAUP: cfgparse-listen: move the "server" keywords to their own parser
- CLEANUP: cfgparse-listen: move "bind" to its own parser
- MINOR: log: add proxy_logformat_origin() to detect a proxy's log-format type
- CLEANUP: cfgparse-listen: move "option" to its own parser
- CLEANUP: cfgparse-listen: simplify the second half of cfg_parse_listen()
- MINOR: cfgparse: always suggest the section names on an unknown keyword
- CLEANUP: cfgparse: remove the unused common_kw_list[] definitions
- MEDIUM: spoe: add the "on-switching-rules" event
- BUG/MEDIUM: cli: fix the last command detection on a full command line
- REGTESTS: cli: test a command line of exactly tune.bufsize bytes
- BUG/MEDIUM: log: do not ignore the peer's shutdown in the syslog applet
- REGTESTS: log: check that a log-forward TCP session ends on client close
- BUG/MINOR: spoe: Fix the message list splice with an empty list
- BUG/MINOR: flt-decomp: Fix the decompression filter keyword list name
- MEDIUM: flt-http-comp: Enable compression directions from algorithms
- MINOR: flt-http-comp: Use compression options as filter config
- BUG/MINOR: flt-http-comp: Enable implicit filters independently per direction
- MINOR: flt-http-comp: Don't uselessly init a compression filter
- MINOR: flt-decomp: Don't instantiate a filter without applicable direction
- MINOR: bwlim: Use a different filter ID for in/out filters
- MINOR: fcgi: Move application validation to the post-check stages
- MINOR: filters: Introduce filter classes and per-side class ordering
- MINOR: filters: Add per-proxy filter instance storage and lookup
- MINOR: filters: Inherit filter instances from defaults sections
- MINOR: filters: Add instance parsers for the built-in classes
- MEDIUM: filters: Parse and finalize filter-config instances
- MINOR: filters: Add filter-enable and filter-disable directives
- MINOR: filters: Add per-side filter-sequence constraints
- MINOR: filters: Mirror implicit filters as filter instances
- MINOR: filters: Expose classes and instances through the CLI
- MINOR: filters: Reject filter-* directives in anonymous defaults
- MINOR: filters: Restrict defaults inheritance with explicit filter directives
- MINOR: filters: Reject mixing legacy and filter-* directives
- MINOR: filters: Maintain a global instance list per proxy
- MINOR: filters: Use global instance order for startup operations
- MINOR: filters: Support global instance sequencing
- MINOR: filters: Expose global filter ordering through the CLI
- MINOR: filters: Use a proxy flag to detect use of legacy 'filter' directive
- MINOR: filters: Don't finalize filter instances in legacy mode
- MINOR: filters: Add helpers to iterate and find filter configurations
- MINOR: filters: Add flt_has_filter() and use it in proxy_finalize()
- MINOR: filters: Select legacy or instance model in lifecycle callbacks
- MEDIUM: filters: Attach filter instances to streams
- MEDIUM: cache: Resolve cache rules during the check stage
- MINOR: cache: Use the filter helpers for cache checks and post-check
- MINOR: fcgi-app: Use the filter configuration helpers for lookups and checks
- MINOR: bwlim: Look up the bandwidth filter with the filter helpers
- MINOR: flt-decomp: Use the filter helpers for implicit decompression checks
- MINOR: flt-http-comp: Use the filter helpers for implicit compression checks
- MINOR: spoe: Use the filter configuration helpers for lookups and checks
- REGTESTS: filters: Add a test for per-proxy stream filter ordering
- REGTESTS: filters: Exercise Lua inheritance and state selection
- REGTESTS: filters: Exercise stream isolation across backend modes
- REGTESTS: filters: Exercise Lua callback resumption
- REGTESTS: filters: Exercise Lua filters across HTTP retries
- REGTESTS: filters: Exercise Lua probes around mixed built-in filters
- REGTESTS: filters: Exercise TCP Lua payload chains and sequencing
- REGTESTS: filters: Exercise per-thread Lua instance state
- REGTESTS: filters: Record Lua callback waits across backend EOF
- REGTESTS: filters: Check non-HTX Lua filters in HTTP chains
- MINOR: filters: Add a global option to enforce the legacy filter api
- DOC: filters: Document the filter instance mode and the legacy mode
- DOC: lua: Document the filter instance declaration of Lua filters
- CLEANUP: filters: Use the stream filter instance wording in comments
- MINOR: filters: List the filter classes in haproxy -vv
- DOC: config: Review "Supported filters" section
- MINOR: lua: Protect the common Lua state during a runtime filter deinit
- MEDIUM: stream: Hold a reference on the frontend during stream_free()
- MINOR: applet: Add a destroy callback to the applets
- BUG/MEDIUM: spoe: Let an agent outlive its applets during a backend deletion
- MEDIUM: filters: Activate the instances inherited by dynamic backends
- DOC: filters: Document how to port an existing filter
- BUG/MINOR: hlua: assume proxy_find_by_id() may return NULL on previously existing server
- MINOR: event_hdl: add PROXY family events
- MINOR: proxy: add e_sub proxy subscrition list
- MINOR: event_hdl/proxy: publish EVENT_HDL_SUB_PROXY_{ADD,DEL,PUB,UNPUB} events
- MINOR: hlua: expose PROXY event family through Lua
- BUG/MEDIUM: mux-h2: don't return a freed tasklet from h2_deferred_shut()
- BUG/MEDIUM: mux-fcgi: don't return a freed tasklet from fcgi_deferred_shut()
- BUG/MEDIUM: mux-h1: don't pass an upgraded connection to traces in h1s_destroy()
- BUG/MEDIUM: mux-h2: report a dead rhttp connection in h2_wake()
- BUG/MEDIUM: mux-h2: fix a possible crash on rxbuf allocation failure
- MINOR: cache: Remove a useless variable in http_action_req_cache_use
- BUG/MEDIUM: mux-h1: also release the embryonic stream when the error is empty
- BUG/MINOR: http-fetch: don't report pseudo-headers in TCP mode
- BUG/MEDIUM: mux-h1: only accept the H2 preface on the first request
- MINOR: stats: report the SSL library FIPS mode in "show info"
- REGTESTS: http-messaging: Fix http_fail_cnt_body to succeed with small timeouts
- REGTEST: ssl: require OpenSSL 1.1.1 for crt-list ciphersuites tests
- DOC: make: fix EXTRA_MAKE fragment filename
- BUG/MINOR: cfgparse-global: only warn when "localpeer" is set together with -L
- DOC: configuration: document AWS-LC compatibility restrictions
- OPTIM/MINOR: server: restrict <all_servers> list usage
- MINOR: server: rename <all_servers> list to <servers_to_init>
- MINOR: server: refactor cli_find_server()
- MINOR: server: extract unregistering from "delete server" CLI parser
- MINOR: proxy: define purge task and purge servers list
- MEDIUM: server: implement "purge server" CLI
- MINOR: server: set maintenance mode on "purge server"
- MINOR: proxy: yield during servers purge
- MINOR: cli: refactor CLI wait command parser
- MINOR: cli: implement wait srv-unknown
- OPTIM/MINOR: server: remove unnecessary server lookup during purge
- REGTESTS: server: test "purge server" CLI command
- MEDIUM: server: implement "purge all servers" CLI
- MINOR: server: yield during "purge all servers"
- REGTESTS: server: test "purge all servers" CLI command
- MINOR: proxy: extract proxy runtime deletion in a specific function
- MEDIUM: proxy: implement purge backend
- MINOR: cli: implement wait be-unknown
- OPTIM/MINOR: proxy: remove unnecessary lookup during purge
- REGTESTS: proxy: test "purge backend" CLI
- BUG/MAJOR: mux-h2: properly handle short reads on PING frames
- BUG/MAJOR: h1-htx: report an error on a too long chunk-size line
BUG/MAJOR: h1-htx: report an error on a too long chunk-size line
When a chunked message starts with a chunk-size line longer than the input
buffer, the parser was looping forever: the line was never complete, no data
could be consumed, and the connection remained stuck until the client
timeout because the buffer was full and could not be extended.
The full-buffer detection used for too large headers and trailers is now
also applied to the chunk-size line: if nothing was parsed and the input
buffer is full, the line cannot possibly fit and a parsing error is reported
instead of waiting for more data that will never fit.
A workaround consists to reject chunk-encoded request:
http-request reject if { req.hdr(transfer-encoding) -i chunked }
This should be backported at least to 3.0. But possibly to all stable
versions with some modifications. However, no loop can be experienced on 2.8
and 2.6.
Many thanks to Adam Crawford for the report and the fix.
Willy Tarreau [Mon, 12 Oct 2026 12:38:34 +0000 (14:38 +0200)]
BUG/MAJOR: mux-h2: properly handle short reads on PING frames
Handling of partially received H2 frames was fixed in 2.5 with commit b5f7b52968 ("BUG/MEDIUM: mux-h2: Handle remaining read0 cases on partial
frames"), however it turns out that the PING one was apparently missed.
However by then it was not a problem, but became one when commit 633c41c62
("MEDIUM: mux-h2: rework h2_restart_reading() to differentiate recv and
demux") was merged in 3.1 because now the mux's tasklet can properly wake
up as long as the job is not finished, which unfortunately doesn't play
well once combined with the fix above, because as long as the data are
not consumed, we continue to wake up, and we don't consume them until
the frame is complete, or the timeout http-request strikes.
Let's simply implement the check for the short read in h2c_handle_ping()
to avoid the spurious wakeup.
This needs to be backported to 3.2, maybe even 2.6 since the checks in
other functions didn't change since then.
As a workaround, setting "timeout http-request" to a low value such as
1-5s mitigates the CPU consumption.
OPTIM/MINOR: proxy: remove unnecessary lookup during purge
Simplifies backend background deletion in purge task. Call to
be_check_for_deletion() is removed, preventing an unnecessary lookup on
proxies global list.
Instead, only <beconn> member check is necessary. Backend configuration
which could block deletion were already tested via "purge backend".
Also, if backend has been reset to publish state, it is no longer
flagged PR_FL_TO_PURGE.
Similar to srv-unknown, a new CLI wait condition "be-unknown" is added
to check if a name can be used for a new backend instance to create. A
failure is returned if a proxy already exists with this name, but it can
wait if this is a backend instance targeted for background deletion.
Add a new CLI command "purge backend", similar to "purge server". Its
objective is to remove a proxy instance at runtime, or if not possible
due to a transient error, flag the proxy for background deletion.
To implement it, a new flag PR_FL_TO_PURGE is defined. It is tested
inside proxy purge task to periodically test backend deletion via
be_check_for_deletion() until the operation can be completed.
Also, be_check_for_deletion() has been extended to add a pr_del_mode as
parameter. When using purge, backend server list can still be non empty,
at the condition that every entries in it are flagged for background
deletion as well.
A backend deletion can be cancelled by using publish/unpublish commands.
This will simply remove PR_FL_TO_PURGE and log a message to report the
cancellation.
MINOR: proxy: extract proxy runtime deletion in a specific function
A new function be_unregister() is created, with content extracted from
the main body of "delete backend" CLI parser. This is necessary to
support different methods for backend deletion.
REGTESTS: server: test "purge all servers" CLI command
Add a reg-test for the new CLI command "purge all servers". This ensures
that this command is able to perform three different actions for the
three created servers: skip an unremovable server, immediately delete if
possible and flag for background deletion. The last case relies on
barrier usage similar to "purge server" reg-test.
Amaury Denoyelle [Tue, 13 Oct 2026 09:53:54 +0000 (11:53 +0200)]
MINOR: server: yield during "purge all servers"
Extend "purge all servers" handler to yield after 50 iterations. The
objective is to avoid to consume too much time under thread isolation.
When yielding must be performed, applet_have_more_data() is manually
called. This is necessary as the applet is not blocked on buffer output.
Without it, CLI handler won't be called again.
Implement a new CLI command "purge all servers <be>". It tries to delete
every server present in a backend or if not possible flag them for
background deletion. If the deletion is impossible due to a specific
configuration of the server, the instance is skipped and the following
entries are handled.
At the end of the iteration, a summary is displayed on the CLI with the
number of servers deleted, flagged and skipped. If at least one server
is flagged for background deletion, proxy purge task is scheduled.
The command runs via a parser and a handler. While it is not strictly
necessary currently, this will serve as a skeleton to implement
yielding.
Add a reg-test for the new CLI command "purge server".
To test the background deletion, server reponses are synchronized via a
barrier. When the barrier is unlocked, srv-unknown is used to test that
the background deletion has completed. In the second test, background
deletion is instead interrupted via "disable server" before unlocking
the barrier.
OPTIM/MINOR: server: remove unnecessary server lookup during purge
Function srv_check_for_deletion() was previously used in proxy purge
task to determine if a server is ready to be deleted. However, this
performs an unnecessary server lookup as the server instance is already
known at this stage.
Simplify this by replacing it with new helper srv_in_use(), which
reports if a server still has active/pending/idle connections. If not
the case, removal can be performed.
This code is safe as it runs in thread isolation. If a server is removed
via another path before proxy purge task, it will be removed from
backend purge list. Furthermore, it also eliminates unnecessary checks
on server configuration : only a removable server can be inserted in the
proxy purge list.
Define a new CLI wait condition named "srv-unknown".
This will report if the server name is currently not used by an instance
in a specific backend. An error is immediately reported if a matching
instance is found, unless it is flagged for background deletion.
Typically, this is useful after CLI "purge server" to detect the
deletion completion and before creating a new instance via "add server".
Reorganize cli_parse_wait() which is responsible to parse CLI wait
command invocation.
This patch splits the function in two parts. First, args[2] is compared
as string to check the comparison argument and assigned <ctx.cond>
member. The second part of the function reuses <ctx.cond> to perform the
necessary initialization.
This patch will allow to extend wait command to add similar conditions
to the already existing one, without having to either duplicate the code
or perform multiple string comparison.
Function proxy_purge_servers() loops through server purge list and tries
to remove them. This is executed under thread isolation for the whole
function duration.
To prevent blocking for too long, yield after 50 servers deleted. The
purge task is immediately requeued but this gives an opportunity for the
other threads to achieve some work.
For now, a static value is applied as the yield limit. It may be useful
to render it configurable if users want to prioritize traffic even more.
MINOR: server: set maintenance mode on "purge server"
This patch changes behavior of "purge server" to automatically set a
server in maintenance mode if needed. This is not performed if a server
cannot be removed due to a configuration limitation.
The objective is to improve the user API. While not a big change in the
context of a single "purge server", automatic maintenance mode will
become a must have for the future "purge all servers". Thus, this change
allow guaranteeing a similar behavior between these two commands.
To implement it, a new argument is added to srv_check_for_deletion(),
indicating the deletion mode. For better code clarity, this is defined
as an enum type pr_del_mode.