DOC: configuration: document library compatibility for TLS keywords
Several TLS configuration keywords describe their OpenSSL requirements in
prose without a standardized Compatibility line, leaving AWS-LC support
unclear to readers.
Add Compatibility lines for the default bind and server ciphersuites,
bind and server ciphersuites, and TLS key logging. These require OpenSSL
1.1.1 or newer and AWS-LC 1.22.0 or newer (API 22). Replace the certificate
compression support sentence with a Compatibility line requiring OpenSSL
3.2.0 and explicitly stating that AWS-LC is unsupported.
Leave keywords requiring OpenSSL versions below 1.1.1 unchanged. This is
a documentation-only change; no runtime behavior is modified.
This may be backported to maintained branches containing the corresponding
keywords and AWS-LC support (as far as 3.0).
Amaury Denoyelle [Fri, 23 Oct 2026 09:22:48 +0000 (11:22 +0200)]
BUG/MINOR: h3: do not wait after Tx realloc failure
The following commit implements for QUIC transcoders a realloc operation
when a smallbuf is not sufficient to encode headers. This is available
via qcc_realloc_stream_txbuf() function.
However, an alloc failure of this function was considered as a transient
error, with 0 returned in this case. Fix this so that it is now a fatal
error which will trigger the connection closure, which is the correct
handling for memory allocation issues in QUIC MUX layer.
This must be backported up to 3.2. Note that the chunk relative to
h3_req_headers_send() is specific for QUIC backend usage and must only
be picked on 3.3 and above.
Amaury Denoyelle [Fri, 23 Oct 2026 08:44:41 +0000 (10:44 +0200)]
BUG/MINOR: h3: do not retry on trailers too large
The following patch adds the ability for the QUIC transcoder to release
a buffer if there is not enough space in it and allocate a new one
instead. This is mostly used during DATA and trailers encoding.
For trailers encoding it may cause an infinite loop if the content is
larger than a whole buffer. In this case, transcoder will release and
allocate a new buffer without interruption before the watchdog is
triggered.
Fix this by extending h3_resp_trailers_send(). The buffer is now
released only when it contains residual not yet sent or acknowledged
data. If the buffer is empty, trailers are too large and an error is
returned, which will close the connection with INTERNAL_ERROR code. This
method was in fact already implemented in h3_resp_headers_send().
Amaury Denoyelle [Thu, 22 Oct 2026 13:36:02 +0000 (15:36 +0200)]
BUG/MEDIUM: h3: prevent conn failure on trailers decoding without space
HTTP/3 trailers are decoded in function h3_trailers_to_htx(). If this
fails due to a lack of space, a negative value is returned, resulting in
a connection closed with INTERNAL_ERROR. This is incorrect as it could
have occurred because of data not yet consumed in HTX output buffer.
One solution would be to temporarily block demux when such situation is
encountered. However, it is not easily implementable now since the
introduction of http_trailers_to_htx() which is shared with HTTP/2
parser. A value HTTP_PRS_TOO_MANY can be caused either due to a
transient lack of space. However, it is also used to report a number of
decoded trailers exceeding tune.http.maxhdr configuration which is a
fatal error.
HTTP/2 parser does not have the same problem as it only starts decoding
of trailers when output HTX buffer is empty. This is largely due to the
fact that HPACK decoding cannot be rolled back after a transient error
as it can have side effects related to dynamic table decoding. HTTP/3 is
not concerned as QPACK dynamic table capability is not implemented.
Thus, apply the same approach in HTTP/3: trailers decoding will be
started only after HTX output buffer is emptied. Flag QC_SF_DEM_FULL is
set to temporarily block demux if needed and 0 is returned from
h3_trailers_to_htx(). Decoding will be retried after a successful read
from the stream layer.
This solution is in fact already used for HEADERS when dealing with
intermediaries HTTP responses. Its impact is minimal given that trailers
usage in H3 is rare. Also, it will simplify a future QPACK dynamic table
implementation.
Amaury Denoyelle [Thu, 22 Oct 2026 15:28:38 +0000 (17:28 +0200)]
BUG/MAJOR: h3: always check :path for all scheme values
Update HTTP/3 parser so that :path pseudo-header is always checked to
reject any invalid values.
Previously, this was only performed if scheme is HTTPS or HTTP.
HAProxy's HTTP/3 is only associated with an HTTP-compatible protocol on
the other side, so removes the unnecessary condition before verifying
path conformity.
Olivier Houchard [Fri, 16 Oct 2026 15:05:11 +0000 (17:05 +0200)]
BUG/MEDIUM: server: Don't use the cached ALPN for websocket
When using websockets, we may end up using a different ALPN than the one
we usually use with the server, so do not reuse the cached ALPN, if any,
and do not cache the one we used for that connection. Otherwise, we may
end up with websocket connections that will attempt to use the wrong
mux, and end in error.
Not making any attempt at caching the ALPN means we can't easily use
0RTT with websockets, but I do not believe this is a big problem.
Aleksandar Lazic [Wed, 21 Oct 2026 20:25:34 +0000 (22:25 +0200)]
DOC: filters: channel_start_analyze is called again on L7 retries
The filters documentation says that the flt_start_analyze and
flt_end_analyze analyzers are called only once per stream. On a L7
retry, the response analyzers are set again when the connection is
established, so channel_start_analyze is called a second time on the
response channel, without a channel_end_analyze call in between. It is
preceded by an http_reset call on the response.
Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
Aleksandar Lazic [Wed, 21 Oct 2026 20:25:18 +0000 (22:25 +0200)]
BUG/MINOR: http-ana: call the http_reset filter callback on L7 retries
The filters documentation says that the http_reset callback is called
when the request is sent again to the server for a L7 retry, but it was
only called for the 1xx informational responses. It was documented with
the L7 retries in commit a254a37ad ("MEDIUM: streams: Add the ability to
retry a request on L7 failure."), but never called on this path.
do_l7_retry() now calls it for the response, once sc_reset_endp()
succeeded so that the retry is certain, and before the L7 buffer swap,
while the failed response is still in the buffer and txn->status is
set, like for the 1xx responses. The request is sent again as is from
the L7 buffer and its callbacks are not called again, so it is not
reset. The documentation now tells it.
This patch could be backported to all stable versions.
Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
Aleksandar Lazic [Tue, 20 Oct 2026 18:42:11 +0000 (20:42 +0200)]
BUG/MINOR: promex: don't loop forever when a module skips a time series
The fill_ts() callback of a promex module may return 0 to skip the
current time series. In this case, promex_dump_module_metrics() jumps
back to the beginning of the loop on the time series with "continue",
without calling next_ts(). So fill_ts() is called again for the same
time series, forever, until the watchdog kills the process. The "next"
label, which retrieves the next time series, was obviously meant to be
used there, but it is never reached by a jump.
The modules of HAProxy itself never skip a time series with a valid
configuration (the stick-table module only skips the tables with a
null size), but an external module doing so triggers the issue at the
first scrape of its metrics.
The issue was introduced by commit 5f875414e ("MEDIUM: promex: Dump
metrics of registered modules with a way to filter them"). This patch
must be backported as far as 3.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
At the moment, when it is detected that it is not possible to complete
the purge, a background task is scheduled to retry every second. However
tests show that most of the time the operation completes almost instantly
(typically less than 20ms in local tests), so it's a bit sad to wait for
a full second in this case. In addition, it requires to apply a longer
client timeout to the VTC than any previous test.
Ideally it would be nice to use a capped exponential growth for retries,
but even without going this far, a nice first approach that brings
immediate benefit is to wait a very short time before the first retry,
while keeping the 1-second delay for subsequent ones. It's easy to do
because the code that arms the initial retry is performed in the command
pasing function while the next retries are in the task itself. That's
what this commit does. Now most of these operations will be performed
in either 0.05s or 1.05s instead of 1s. And regtests are happy again.
Amaury Denoyelle [Thu, 22 Oct 2026 12:53:59 +0000 (14:53 +0200)]
BUG/MINOR: mux_quic: fix overflow on avail_streams
Function qcc_fctl_avail_streams() return the number of streams which can
still be opened regarding the limit set by the peer.
The return value is a 32 bits integer, used to store the substraction
result. This may overflow as flow control limit is encoded on 64 bits,
which could result either in blocked transfers, or in spurrious error as
mandatory unistreams could not be opened.
Fix this by using a temporary 64 bits value, and cap it to INT_MAX if
needed. This is sufficient as values larger than INT_MAX is out of a
real case usage.
Amaury Denoyelle [Thu, 22 Oct 2026 12:22:32 +0000 (14:22 +0200)]
BUG/MINOR: mux_quic: fix MAX_STREAMS limit
A peer may emit a MAX_STREAMS frame to increase the window of opened
streams. Such frame is parsed via qcc_recv_max_streams() since 3.3 as it
is mandatory for backend support.
This patch fixes the maximum value authorized in MAX_STREAMS. As
specified by RFC 9000, it cannot exceed 2^60, the maximum number of
streams of a given type, as stream IDs are encoded on 62 bits with the 2
least significant bits reserved for the type.
CLI "show quic" output can be filtered to only a single connection entry
by specifying its address as argument. However, loop iteration and
filtering was incorrectly implemented, and in effect only the first
connection of each thread could be dumped this way.
Fix this by adjusting iteration on "show quic" handler. The basic idea
is similar. If <ctx.ptr> is set, it does not dump anything for
non-matching entries. As soon as the filtered entry is dumped, the
iteration is interrupted.
Amaury Denoyelle [Thu, 22 Oct 2026 08:04:23 +0000 (10:04 +0200)]
BUG/MEDIUM: mux_quic: fix FIN received with an already known offset
The following patch improves STREAM FIN handling on received frames by
reducing the invokation of the parser. It requires to detect the case
where FIN is received standalone without any data,
This introduces an issue when STREAM FIN is received with an already
known offset. In this case, FIN is not considered standalone. However,
as the offset was already handled, parser is skipped and this prevents
closure of the stream Rx channel until its timeout.
Fix this by marking FIN as standalone after skipping already received
offset. Thus, if <len> is reduced to 0 in qcc_recv(), <fin_standalone>
will be set even if it was received with a payload.
This must be backported up to 3.2, which introduced the above patch.
Amaury Denoyelle [Wed, 21 Oct 2026 13:47:59 +0000 (15:47 +0200)]
BUG/MINOR: mux-quic: prevent send corruption on buf realign
For QUIC MUX emission, frames prepared for sending are not rebuilt each
time qcc_io_send() is called. This was first implemented for pacing and
the principle was generalized by the following patch, so the frame list
is now preserved if emission is interrupted on pacing, transient
transport error or flow control limitation.
It is necessary to reset the frames list if some action occured during
an emission in several steps, for example when new data from upper layer
is available for sending or flow control limit is extended.
Prepared frames directly point to each QCS Tx buffer. Thus if buf
realign is performed, related frame will point to an incorrect location
in the buffer, resulting in an invalid data transfer. Note that this
issue is most of the time adverted by the fact that frames list is
resetted via qcc_send_stream() which most of the time directly follow a
realign operation.
Fix this by refreshing the frames list also during Tx buf realign. This
is simply implemented via a call to qcc_clear_frms(). It is currently
not possible to only clear the single QCS frame, as next emission would
only use already generated frames from other streams.
BUG/MINOR: jwt: enforce the ECDSA signature length in jwt_verify
RFC 7518 section 3.4 states that an ES256 signature MUST be a 64-octet
sequence made of R and S each on 32 bytes, and that validation fails
otherwise (96 bytes for ES384 and 132 for ES512). jwt_verify() and
jwt_verify_cert() accept any even length and split it in two halves, so
that R and S may be prefixed with any number of zero bytes.
This can be reproduced by taking a valid ES256 token, decoding its
signature, inserting a zero byte before each 32-byte half, and encoding it
again: the converter still returns 1.
Let's check that the signature has exactly the length mandated by the
algorithm in convert_ecdsa_sig(), and return JWT_VRFY_INVALID_TOKEN (-3)
otherwise, as was already done for odd lengths. A reg-test is provided.
This fix should be backported to all supported versions.
BUG/MINOR: jwe: only match the exact "ALL" word in jwt.decrypt_{alg,enc}_list
The parsing of the special 'ALL' case in "jwt.decrypt_alg_list" and
"jwt.decrypt_enc_list" did not check the actual string length so a too
short string could be wrongly treated as 'ALL'.
This was introduced in 3.4 by commit f82a242c8 ("MINOR: jwe: Add option to
enable/disable algorithms or encryption algorithms for jwt_decrypt"), so
the fix must be backported to 3.4.
BUG/MINOR: jwe: reject non-empty encrypted keys with "dir" and "ECDH-ES"
RFC 7516 section 5.2 step 10 requires that when direct encryption ("dir")
or direct key agreement ("ECDH-ES") are used, the JWE Encrypted Key part
of the token is empty. The jwt_decrypt_jwk() converter with "dir", and
both jwt_decrypt_jwk() and jwt_decrypt_cert() with "ECDH-ES", silently
ignore this part.
This can be reproduced by inserting any base64url string (e.g. "AAAA")
between the first two dots of a "dir" or "ECDH-ES" token: the converters
still return the decrypted contents.
Let's reject the token when the encrypted key is not empty for these
algorithms.
BUG/MINOR: jwe: only accept exact "alg" and "enc" names in JWE tokens
The 'alg' and 'enc' parser does not check the value length so a too
short value would still be seen as the full one ("A1" instead of
"A128CBC-HS256" for instance).
This was introduced in 3.4 by commit 416b87d5d ("MINOR: jwe: Add new
jwt_decrypt_secret converter"), so the fix must be backported to 3.4.
BUG/MINOR: jwt: don't truncate binary HMAC secrets at the first NULL byte
When jwt_verify() is used with an HMAC algorithm and its secret is passed
via a variable, the secret is truncated at its first NUL byte. RFC 7518
recommends random HMAC keys at least as long as the hash, which are
commonly distributed base64-encoded and fed to haproxy through something
such as "str(<b64>),b64dec" into a variable. About one random 32-byte key
out of eight contains a NUL byte. With such a key, all legitimately signed
tokens are rejected (traffic impact for the site), and tokens signed with
only the bytes located before the first NUL are accepted instead, which
for a NUL in the first bytes of the key means that a client can forge
valid tokens after a trivial brute force. Configurations passing the
secret as a string constant are not affected since the config parser
cannot produce a NUL byte there.
Willy Tarreau [Wed, 21 Oct 2026 06:51:18 +0000 (08:51 +0200)]
MEDIUM: http: add the "short-status-code" violation exception
Now by default we reject status codes shorter than 3 digits, though
this exception permits to continue to accept them. There is no valid
reason for accepting these and no known client currently supports
them, but it has been supported due to historical reasons that are
long outdated.
It is expected that noone will notice, and may be candidate for
backporting only to the latest LTS version after some observation
time.
Willy Tarreau [Tue, 20 Oct 2026 13:21:51 +0000 (15:21 +0200)]
MINOR: http: add one control per existing HTTP violation
Following the previous patch, this one extends every place where a
check for relaxing protocol violations was permitted with a specific
tunable that can individually be enabled or disabled, so that it is
now possible to selectively enable a single (or a few) exceptions
without granting other ones.
The following ones are covered here, per direction:
Most of the time the check consisted in appending this after err_pos < -1:
|| !(global.acc_hreq_vio & (1U << HTTP_VIO_...))
For the long status code, the code was slightly reordered because the
check for the lenient case was performed first and now we must do the
strict check first (but the logic is unchanged, both were exclusive
already). The logic remains: if strict mode (default) or relaxed without
acceptance for long headers, we reject beyond 3 digits. And in any case
we continue to reject values that don't fit in 16 bits.
The doc was updated with descriptions of what each keyword corresponds
to, associated risks, and examples.
This is not much intrusive and might possibly be backported to latest
LTS if users want it.
Proxies support HTTP violations relaxation in requests via option
accept-unsafe-violations-in-http-request and in responses via option
accept-unsafe-violations-in-http-response. The problem is that over
time the list of exceptions has grown quite a bit, and that a simple
application that requires to relax one tiny non-dangerous feature
such as enabling raw utf-8 in URLs, we end up enabling many other
things that can be much more dangerous.
This patch introduces a new pair of global settings that permit to
declare which of the HTTP violations are acceptable when a proxy
wants to use them. It's expected that such violations are rare
enough so that a single global setting to adjust them all process-
wide is sufficient.
What the code does is that it introduces a bit field for the request
and the response in the global section, with each bit indicating that
one violation is acceptable, and it permits via the global keyword to
replace the whole list, add an entry or remove an entry. By default,
for backwards compatibility, all exceptions are allowed, but it's
expected that most of the time users will just set one, or will keep
the defaults except some of the most dangerous ones.
For now only the declaration and parsing are handled, and only "none"
and "all" are supported. The keyword parser supports fuzzy matching
and will propose approximating names when one is misspelled.
Subsequent patches will add the options themselves.
Willy Tarreau [Mon, 19 Oct 2026 16:22:49 +0000 (18:22 +0200)]
MINOR: tools: add find_word_index_in_array() to find a keyword
This will be used to find a word among a set in an array of
arbitrary size and layout provided that the words are always
accessible via a pointer at a fixed location in each item. The
goal here is to simplify call places which want to match a word
among a list, find the associated entry, and propose a list of
possible matches.
This will return the following values depending on the looked up
word:
0 = not found
1 = found a different one which looks like a candidate
2 = found exact match
When non-zero is returned, an index pointer will designate the
associated entry in the array so that the caller can find the
context of the sought word, or at least report the mostly similar
one in the error message, like this:
size_t idx;
match = find_word_index_in_array(kw_array, sizeof(kw_array[0]), -1,
offsetof(struct keyword, name), word, &idx);
if (match != 2) {
memprintf(err, "Invalid keyword '%s'.", word);
if (match == 1)
memprintf(err, "%s Maybe you meant '%s' ?", *err, kw_array[idx].name);
goto fail;
}
Willy Tarreau [Wed, 21 Oct 2026 09:53:15 +0000 (11:53 +0200)]
DOC: config: add missing acme.scheduler in the global index
The "acme.scheduler" global directive was added in 3.2 by commit e7574cd5f0 ("MINOR: acme: add the global option 'acme.scheduler'")
but forgot to update the keyword index, so let's do it now. This
patch can be backported to 3.2.
Olivier Houchard [Wed, 21 Oct 2026 14:55:49 +0000 (16:55 +0200)]
BUILD: thread: fix builds without thread support
Commit 4312f13a2 ("OPTIM: fd: Make each thread group's FD table NUMA-local")
added an unconditional call to thread_bind_to_cpu_map() from init_pollers(),
but the helper is only defined with USE_THREAD. Building with TARGET=generic
thus fails with an implicit declaration and an undefined reference.
Add a no-op stub alongside the other non-threaded helpers.
This backend option allows post handshake authentication for the given
backend. This simply consists in adding a "post_handshake_auth"
extension in the ClientHello that tells the server that PHA is
supported. We only enable it for backends that have an explicit
certificate provided so that this certificate gets always used in
subsequent CertificateRequest messages sent by the server.
This does not work with AWS-LC (PHA unsupported) or WolfSSL which would
support it but only when WolfSSL is built with
WOLFSSL_POST_HANDSHAKE_AUTH defined (disabled by default).
Olivier Houchard [Tue, 20 Oct 2026 13:25:19 +0000 (15:25 +0200)]
OPTIM: fd: make each thread group's polled_mask NUMA-local
The polled_mask array was left aside the fdtab's NUMA placement:
its slices are not padded to the page size, so a page can be shared
between two groups, and calloc() zeroes it eagerly on the boot
thread when the allocation is served from the heap. Allocate it
with mmap() and pad the slices as the fdtab: nothing is copied
nor zeroed, the pages are born zeroed and each group's masks are
written by its own threads, bound before they first touch them, so
the lazy faulting places them on the right node by itself.
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().