OPTIM: fd: Make each thread group's FD table NUMA-local
With per-thread-group FD tables, the tables are allocated and zeroed,
hence faulted in, by the main thread, and the kernel's first-touch
placement then places every group's table on the main thread's NUMA
node, making the other groups' fdtab accesses, including the atomics
done for every FD event, run on remote memory.
Allocate the tables with mmap() instead, so that their pages are only
faulted in when they are first written, and make these first writes
come from the right CPUs: the boot thread is bound to group 0's CPUs
before any FD is inserted, and to each other group's CPUs around that
group's copy of the boot table, so that every table stays local to its
own node. Each group's slice is padded to the kernel's page size so
that no page is shared between two groups. Only matters with
per-group FD tables.
set_thread_cpu_affinity() binds the current thread to the CPUs that
the cpu_map assigns to it. The next commit will need to bind to
another thread's CPUs, around the first touch of an allocation, so
split a thread_bind_to_cpu_map(tgrp, ltid) helper out of it, binding
the current thread to the CPUs assigned to local thread <ltid> of
thread group <tgrp> (0-based). set_thread_cpu_affinity() keeps its
master-process check and delegates to the new function.
Amaury Denoyelle [Tue, 20 Oct 2026 13:48:15 +0000 (15:48 +0200)]
BUG/MEDIUM: mux_quic: ensure FIN is reported after Rx data transfer
Multiple Rx buffers may be allocated per QCS instance. Each buffer is
parsed separately. If necessary, data may be transferred from the next
buffer when the parser is blocked in the middle of a frame.
This operation causes an issue if only two buffers remains and all data
with FIN are already received. In this case, QCS is not yet remotely
closed during qcc_recv(). If all content from the next buffer is
transferred, close is still not set. This prevents HTX EOM from being
set, which result in a stream closed after timeout.
Fix this by calling qcs_close_remote() in qcs_transfer_rx_data() if the
above scenario described is encountered. This is sufficient to produce
HTX EOM signal as parsing will be restarted via a new call to app_ops
rcv_buf with <fin> parameter now set.
If the next buffer has been emptied but is not truncated, it is still
reinserted in the rxbuf tree. This could result in a second
qcs_close_remote() call for the same QCS which is forbidden. Thus, this
patch depends on the below fix to prevent this situation.
Amaury Denoyelle [Wed, 21 Oct 2026 07:34:57 +0000 (09:34 +0200)]
BUG/MEDIUM: mux_quic: fix crash on QCS double remote close
A QCS instance must only be closed remotely once. A BUG_ON() enforces
it, so that FIN signalling to the upper layer is correct.
There exists a possible case of a double remote closure after data
transfer between two adjacent buffers on parser blocking. This only
happens if the next buffer is emptied and a standalone FIN is then
received. First qcs_close_remote() will be called during the empty
STREAM FIN reception. This is called again on qcc_decode_qcs() after
consuming data when dealing with the empty remaining buffer.
This patch fixes this by preventing the second call to
qcs_close_remote() in case an empty buffer is detected.
This bug is not easily reproducible as it is highly dependent on the
precise order of these different operations.
This must be backported up to 3.2, as this is the first release which
support multiple QCS Rx buffers.
BUG/MEDIUM: task: do not destroy twice the tasks already killed in mworker_cleantasks()
Since 9228fabe34 ("BUG/MEDIUM: task: Do not destroy a task that is not ours
in task_destroy"), task_destroy() calls task_kill() for a task that is not
owned by the current thread. task_kill() is not idempotent: it starts with
BUG_ON(state & TASK_KILLED), so a task which was already flagged as killed
while still being linked in a run or wait queue cannot be destroyed again.
On the 3.0 and 2.8 branches this is reachable by simply starting haproxy in
master-worker mode with a localpeer, a peers section and a program section:
the forked program child calls mworker_cleanlisteners(), which destroys the
peers sync task, then mworker_cleantasks() walks the run queue and destroys
it a second time, aborting the child with SIGILL before it reaches execvp().
The program section no longer exists on this branch, but the underlying
invariant is still violated: a task flagged TASK_KILLED by an earlier
task_kill() is deliberately left in its queue (that is how the scheduler
knows it must free it on dequeue), so mworker_cleantasks() can always stumble
upon one. Make the walk robust instead of relying on the callers never
handing it a doomed task.
Fix this by ignoring the tasks that are already flagged TASK_KILLED when
walking the queues from mworker_cleantasks(). Those tasks don't need to be
destroyed again as they're already doomed: the scheduler frees them as
soon as it dequeues them (see run_tasks_from_lists()). The check is applied
to every queue walked there (the shared run queue, and the per-thread run
and timer queues).
BUG/MINOR: cfgparse/peers: warn and prevent undefined behavior when peers is missing bind info
As described in GH #3516, declaring a peers section containing the local
peer server (with server syntax) without the matching 'bind' diretive
causes haproxy to crash since 879c78c. However, as Amaury demonstrated,
the config is invalid/incomplete: it makes no sense to declare the local
peer server without the matching bind parameters. Before this patch
haproxy would let the config pass and the result was that the peers
proxy was created without any listeners. While it somehow didn't crash
in the past (perhaps the low level code was robust enough), from a
peers logic point of view it results in the peers proxy doing nothing.
Instead of silently failing (and now risking crashes upon reload), let's
proactively report a fatal error to the user that such config is
incomplete.
Many thanks to @alexandret-ik for the detailed report.
It should be backported to 3.4, and may be relevant to olders versions
but in this case replace the ha_alert() which ha_notice() so existing
configs that used to start (despite not valid from peers point of view)
don't break.
Amaury Denoyelle [Tue, 20 Oct 2026 12:18:32 +0000 (14:18 +0200)]
BUG/MINOR: h3: fix crash with maxrewrite on response headers
This patch is similar to the previous one. This time it enforces HTTP
headers maxrewrite setting on responses parsing from the server to the
client. Thus, this change is related to the backend side handling.
As a side effect, it also prevents a BUG_ON() crash on
qcs_http_rcv_buf(). Indeed, maxrewrite is also used by the stream layer
to reduce the destination buffer. Thus, htx_xfer() may be interrupted
due to a lack of space after setting HTX_FL_PARSING_ERROR flag. This
triggers the BUG_ON() in qcs_http_rcv_buf(). This does not happen on
frontend side with requests as HTX buffer is passed to the newly created
stream instance without rcv_buf invocation.
Amaury Denoyelle [Tue, 20 Oct 2026 09:45:19 +0000 (11:45 +0200)]
BUG/MINOR: h3: enforce maxrewrite on request headers
Muxes are responsible to ensure that at least maxrewrite space remains
in the stream buffer after headers decoding. This was missing in HTTP/3
decoder.
Add an extra check on maxrewrite size after H3 HEADERS request decoding.
The connection is closed with INTERNAL_ERROR if on excess. A future
patch may implement stream-level error instead.
Amaury Denoyelle [Tue, 20 Oct 2026 09:12:00 +0000 (11:12 +0200)]
BUG/MINOR: mux_quic: wake streams on soft-stop using listener socket
Since the following patch, streams wakeup mechanism has changed so that
every still present stream are notified regardless of the connection
status. This is useful when qcm_wake() callback is used. Along with this
change, qcc_wake_streams() via qcm_wake() is no longer directly called
if the connection is on error, as previous qcc_io_process() already
perform it.
This causes issue when qcm_wake() is used to notify about proxy
soft-stop. If listener socket is used, soft-stop is not really
supported, so connection is flagged with CO_FL_ERROR and
qcc_io_process() is interrupted. This skips stream notification, so they
will remain until their timeout fire, which may block worker termination
for some delay.
Fix this by ensuring qcc_wake_streams() is always called if connection
on error in qcc_io_process() epilogue.
Amaury Denoyelle [Mon, 19 Oct 2026 15:01:46 +0000 (17:01 +0200)]
BUG/MEDIUM: h3: fix memory corruption on trailers parsing realign
Since 3.0, intermediary QCS Tx buffer has been removed to reduce data
copy. Now, QCS buffer is shared with the transport layer. This brought
some constraints as buffer realign must not be performed if there is
data already sent not yet ACKed. Data encoding function has been adapted
to remove unsafe realign. However, this was not the case for trailers
encoding, which still perform a blind b_slow_realign().
If there is data still present in the buffer when parsing a trailers
frame, this operation will result in memory corruption when
acknowledgement will be received and incorrect buffer data deleted.
This patch fixes h3_resp_trailers_send() to ensure the correct realign
API is used, similar to what is already implemented for data. This is
based on qcc_realign_stream_txbuf() which is the safe method for content
realign.
BUG/MINOR: flt-decomp: Report error about filter order in lagacy mode only
Response decompression filter cannot be placed after the cache-store one
only in legacy mode. With the new model, it is no longer an error because
order is more explicit and easier to configure.
MEDIUM: filters: eval the legacy backend filters first on the response path
In the legacy filter mode, the backend filter configurations are now
prepended to the response filter list when they are attached to a stream,
preserving their own response order, so a response is evaluated by the
backend filters before the frontend ones. The request path is unchanged.
This mirrors the behavior of the new filter mode on the response path: a
response travels in the opposite direction of a request and reaches the
backend filters first.
The regression tests relying on the response evaluation order of the legacy
mode were updated accordingly, including the mixed cases of the stream-order
test where a legacy frontend uses a instance-mode backend and the other way
around. The filters API documentation was also updated: the frontend filters
are called first on the request path, the backend ones on the response path.
MEDIUM: filters: eval the backend instances first on the response path
When the backend filters are attached to a stream, they are appended to the
global and request filter lists after the frontend ones, which is the
expected order: the request reaches the frontend filters first. But they
were also appended to the response filter list, while a response travels in
the opposite direction and reaches the backend filters first.
In the new filter mode, the backend instances are now prepended to the
response filter list, preserving their own response order, so a response is
evaluated by the backend instances before the frontend ones. The request
path is unchanged.
The regression tests relying on the response evaluation order of the
instance mode were updated accordingly.
DOC: filters: document the automatic ordering of the legacy core filters
The default evaluation orders of the filters are now documented in the
configuration manual, next to the "filter-config" documentation, for both
filter modes: the default placement of the built-in classes in the new
mode, and the automatic placement of the implicitly declared core filters
in the legacy mode, as introduced by the automatic ordering. For each
mode, the three orders are given: the global one, used for the
side-independent operations, and the request and response ones, used for
the evaluation order of the filters on the corresponding channel.
The "decompression" directives no longer recommend declaring the
decompression filters explicitly to prevent misconfigurations: when
implicitly enabled in the legacy filter mode, the decompression filter is
automatically placed at the beginning of the filter list, before the
cache and compression filters.
Both modes' orders are explicitly informational only: they describe the
current placement and they may be changed by the build options or by a
future version. Only the placements explicitly requested by a filter, and
the position of an implicitly declared legacy filter relative to the
explicitly declared ones, should be relied upon.
The filter API internals documentation explains the structural part of
the automatic ordering in legacy mode (the filter config links and the
proxy lists holding the three orders, the segment boundaries of the
explicitly declared filters) and refers to the configuration manual for
the resulting orders, as it already does for the class orders of the new
mode.
MEDIUM: filters: automatically order the implicit core filters in legacy mode
In the legacy filter mode, the implicitly declared core filters (the
decompression, cache, compression and fcgi filters) are now automatically
ordered instead of relying on the user to declare them in a valid position
or reporting a configuration error.
The forced global order, used for the side-independent operations
(configuration checks, initialization and destruction), is:
decomp-req > decomp-res > cache-store > any other filters > http-comp-req > http-comp-res > fcgi
Two additional orders are maintained for the request and the response
channels, matching the three independent orders of the new instance mode,
and used to fill the stream's request and response filter lists and by
flt_foreach_conf() for the FLT_SIDE_REQ and FLT_SIDE_RES sides:
* request: decomp-req > any other filter > http-comp-res > decomp-res > http-comp-req > fcgi
* response: fcgi > decomp-res > cache-store > any other filter > decomp-req > http-comp-res
A new FLT_CFG_FL_EXPLICIT flag marks the filters declared with the legacy
"filter" directive. Explicitly declared core filters are never moved and act
as segment boundaries: implicitly declared filters are sorted inside the
segment they appear in, so they never cross an explicitly declared one. The
request and response orders are stored in two new proxy lists,
filters_req_configs and filters_res_configs, filled with the 'by_req' and
'by_res' members of the filter configurations.
The filter configurations are now linked in the three lists at declaration
time, before flt_check() reorders them, so the lookups performed by the
rules during the configuration checks (the cache rules looking for the
cache-store filter on the response side) behave the same as before the
reordering.
Because the position of implicit filters no longer depends on the
declaration order, the errors reported when mixing an explicit filter with
implicitly declared decompression, compression, cache or fcgi filters are
removed. Explicitly declaring core filters in a conflicting order still
reports an error, the manual placement remains the user's responsibility
for explicit declarations.
The ordering was validated functionally: with an explicit filter mixed
with implicitly declared core filters, the response body is decompressed
and stored uncompressed in the cache before the trace and compression
filters see it, and a gzipped request is decompressed before reaching the
explicit filters.
The same bug that this patch fixed was also corrected via the following
serie. For now, keep the BUG_ON() in qcc_set_error() to detect incorrect
usages.
f739a0c37 BUG/MEDIUM: h3: fix BUG_ON() crash with glitch after QPACK dec err fc9a503b1 BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on GOAWAY parsing 15d9a2f32 BUG/MEDIUM: h3: fix BUG_ON() crash with glitch on SETTINGS parsing 8eb164e14 MINOR: h3: remove dead code on h3_rcv_buf() error path 745b12086 BUG/MINOR: h3: fix risk of BUG_ON() crash with glitch on unknown frame c2d82a20b BUG/MINOR: h3: fix BUG_ON() crash with glitch on htx_set_eom() failure
REGTESTS: filters: Fix the global filters order as reported on the CLI
The cache-store filter was moved after the decompression filters in the
global list. However, global-order.vtc script was not updated
accordingly. Let's do so now.
Amaury Denoyelle [Mon, 19 Oct 2026 12:04:07 +0000 (14:04 +0200)]
BUG/MINOR: h3: fix BUG_ON() crash with glitch on htx_set_eom() failure
In case of htx_set_eom() failure, qcc_set_error() is used both for DATA
and trailers parsing, setting INTERNAL_ERROR as value. Both parsers
return a negative value to interrupt h3_rcv_buf() parsing loop. However,
this causes a crash as qcc_set_error() is called a second time on the
function epilogue.
Fix this by removing qcc_set_error() usage in both h3_data_to_htx() /
h3_trailers_to_htx(). They still return a negative value, which is now
properly handled in h3_rcv_buf() epilogue : INTERNAL_ERROR is set as
both h3c <err> and h3s <err> are unset.
The first objective is to report a stream error if the stream is closed
without any request seen. However, this case does not prevent to report
an HTX EOM which is unnecessary. Worst, it could cause a crash if EOM
fails due to an internal error, as qcc_set_error() could be called a
second times after glitch reporting.
Fix both issues by interrupting parsing as soon as the stream level
error is set.
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.