Control: tags 1102673 + patch Control: tags 1102673 + pending Dear maintainer,
I've prepared an NMU for haproxy (versioned as 3.0.10-0.1) and uploaded it to DELAYED/7. Please feel free to tell me if I should cancel it. Upgrading to 3.0.10 looked more reasonable to me than backporting just the (one-line) CVE fix, but either is fine for me. A maintainer upload of either would be my preferred option. cu Adrian
diffstat for haproxy-3.0.9 haproxy-3.0.10 CHANGELOG | 49 ++++++++++++ SUBVERS | 2 VERDATE | 4 - VERSION | 2 debian/changelog | 9 ++ doc/configuration.txt | 33 +++++++- include/haproxy/compiler.h | 1 include/haproxy/fd-t.h | 6 + include/haproxy/fd.h | 4 + include/haproxy/mux_fcgi-t.h | 3 include/haproxy/quic_conn.h | 10 +- include/haproxy/task-t.h | 9 +- include/haproxy/task.h | 62 ++++++++++++---- include/import/plock.h | 20 ++++- src/backend.c | 36 ++++++--- src/cli.c | 4 - src/debug.c | 17 +++- src/ev_epoll.c | 95 ++++++++++++++++++++++++ src/fd.c | 7 + src/h3.c | 84 ++++++++++++++++++++-- src/hlua.c | 21 ++++- src/hlua_fcn.c | 58 ++------------- src/http_ana.c | 46 ++++++++---- src/log.c | 41 ++++++---- src/mux_fcgi.c | 164 ++++++++++++++++++++++++++++++------------- src/mux_h2.c | 12 +++ src/mux_quic.c | 41 ++++++---- src/peers.c | 5 + src/proto_rhttp.c | 2 src/sample.c | 2 src/sink.c | 2 src/stick_table.c | 13 +++ src/stream.c | 29 +++++++ src/tools.c | 10 +- tests/exp/filltab25.c | 2 35 files changed, 704 insertions(+), 201 deletions(-) diff -Nru haproxy-3.0.9/CHANGELOG haproxy-3.0.10/CHANGELOG --- haproxy-3.0.9/CHANGELOG 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/CHANGELOG 2025-04-22 14:53:02.000000000 +0300 @@ -1,6 +1,55 @@ ChangeLog : =========== +2025/04/22 : 3.0.10 + - MINOR: log: support "raw" logformat node typecast + - BUG/MINOR: peers: fix expire learned from a peer not converted from ms to ticks + - BUG/MEDIUM: peers: prevent learning expiration too far in futur from unsync node + - BUG/MEDIUM: mux-quic: fix crash on RS/SS emission if already close local + - BUG/MINOR: mux-quic: remove extra BUG_ON() in _qcc_send_stream() + - BUG/MINOR: log: fix gcc warn about truncating NUL terminator while init char arrays + - DOC: config: fix two missing "content" in "tcp-request" examples + - BUILD: compiler: undefine the CONCAT() macro if already defined + - BUG/MINOR: rhttp: fix incorrect dst/dst_port values + - BUG/MINOR: backend: do not overwrite srv dst address on reuse + - BUG/MEDIUM: backend: fix reuse with set-dst/set-dst-port + - BUG/MEDIUM: stream: Fix a possible freeze during a forced shut on a stream + - TESTS: Fix build for filltab25.c + - MINOR: task: add thread safe notification_new and notification_wake variants + - BUG/MINOR: hlua_fcn: fix potential UAF with Queue:pop_wait() + - CLEANUP: log: adjust _lf_cbor_encode_byte() comment + - BUG/MINOR: log: fix CBOR encoding with LOG_VARTEXT_START() + lf_encode_chunk() + - BUG/MEDIUM: sample: fix risk of overflow when replacing multiple regex back-refs + - BUG/MINOR: backend: do not use the source port when hashing clientip + - BUG/MINOR: hlua: fix invalid errmsg use in hlua_init() + - DOC: config: add the missing "profiling.memory" to the global kw index + - BUG/MINOR: http-ana: Properly detect client abort when forwarding the response + - BUG/MEDIUM: http-ana: Report 502 from req analyzer only during rsp forwarding + - BUG/MINOR: sink: add tempo between 2 connection attempts for sft servers (2) + - BUG/MEDIUM: h3: trim whitespaces when parsing headers value + - BUG/MEDIUM: h3: trim whitespaces in header value prior to QPACK encoding + - BUG/MINOR: h3: filter upgrade connection header + - BUG/MINOR: h3: reject invalid :path in request + - BUG/MINOR: h3: reject request URI with invalid characters + - BUG/MEDIUM: hlua: fix hlua_applet_{http,tcp}_fct() yield regression (lost data) + - BUG/MINOR: quic: do not crash on CRYPTO ncbuf alloc failure + - DEBUG: stream: Add debug counters to track some client/server aborts + - BUG/MINOR: stktable: invalid use of stkctr_set_entry() with mixed table types + - BUG/MEDIUM: mux-fcgi: Properly handle read0 on partial records + - DEBUG: fd: add a counter of takeovers of an FD since it was last opened + - MINOR: fd: add a generation number to file descriptors + - MINOR: epoll: permit to mask certain specific events + - DEBUG: epoll: store and compare the FD's generation count with reported event + - MEDIUM: epoll: skip reports of stale file descriptors + - IMPORT: plock: give higher precedence to W than S + - IMPORT: plock: lower the slope of the exponential back-off + - IMPORT: plock: use cpu_relax() for a shorter time in EBO + - BUG/MINOR debug: fix !USE_THREAD_DUMP in ha_thread_dump_fill() + - BUG/MINOR: mux-h2: prevent past scheduling with idle connections + - BUG/MINOR: rhttp: fix reconnect if timeout connect unset + - BUG/MINOR: rhttp: ensure GOAWAY can be emitted after reversal + - MINOR: tools: also protect the library name resolution against concurrent accesses + 2025/03/20 : 3.0.9 - BUG/MEDIUM: ssl: chosing correct certificate using RSA-PSS with TLSv1.3 - BUG/MEDIUM: mux-quic: do not attach on already closed stream diff -Nru haproxy-3.0.9/debian/changelog haproxy-3.0.10/debian/changelog --- haproxy-3.0.9/debian/changelog 2025-03-22 18:21:15.000000000 +0200 +++ haproxy-3.0.10/debian/changelog 2025-04-23 21:47:45.000000000 +0300 @@ -1,3 +1,12 @@ +haproxy (3.0.10-0.1) unstable; urgency=medium + + * Non-maintainer upload. + * New upstream release. + - CVE-2025-32464: heap buffer overflow in sample_conv_regsub() + (Closes: #1102673) + + -- Adrian Bunk <b...@debian.org> Wed, 23 Apr 2025 21:47:45 +0300 + haproxy (3.0.9-1) unstable; urgency=medium * New upstream release. diff -Nru haproxy-3.0.9/doc/configuration.txt haproxy-3.0.10/doc/configuration.txt --- haproxy-3.0.9/doc/configuration.txt 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/doc/configuration.txt 2025-04-22 14:53:02.000000000 +0300 @@ -3,7 +3,7 @@ Configuration Manual ---------------------- version 3.0 - 2025/03/20 + 2025/04/22 This document covers the configuration language as implemented in the version @@ -1409,6 +1409,7 @@ - nopoll - noreuseport - nosplice + - profiling.memory - profiling.tasks - server-state-base - server-state-file @@ -1422,6 +1423,7 @@ - tune.comp.maxlevel - tune.disable-fast-forward - tune.disable-zero-copy-forwarding + - tune.epoll.mask-events - tune.events.max-events-at-once - tune.fail-alloc - tune.fd.edge-triggered @@ -3328,6 +3330,31 @@ tune.h1.zero-copy-fwd-recv, tune.h1.zero-copy-fwd-send, tune.h2.zero-copy-fwd-send, tune.quic.zero-copy-fwd-send +tune.epoll.mask-events <event[,...]> + Along HAProxy's history, a few complex issues were met that were caused by + bugs in the epoll mechanism in the Linux kernel. These ones usually are very + rare and unreproducible outside the reporter's environment, and may only be + worked around by disabling epoll and switching to poll instead, which is not + very satisfying for high performance environments. Each time, issues affect + only very specific (and rare) event types, and offering the ability to mask + them can constitute a more acceptable work-around. This options offers this + possibility by permitting to silently ignore events a few uncommon events + and replace them with an input (which reports an unspecified incoming event). + The effect is to avoid the fast error processing paths in certain places and + only use the common paths. This should never be used unless being invited to + do so by an expert in order to diagnose or work around a kernel bug. + + The option takes a single argument which is a comma-delimited list of words + each designating an event to be masked. The currently supported list of + events is: + - "err": mask the EPOLLERR event + - "hup": mask the EPOLLHUP events + - "rdhup": mask the EPOLLRDHUP events + + Example: + # mask all non-traffic epoll events: + tune.epoll.mask-events err,hup,rdhup + tune.events.max-events-at-once <number> Sets the number of events that may be processed at once by an asynchronous task handler (from event_hdl API). <number> should be included between 1 @@ -13396,8 +13423,8 @@ # and reject everything else. (works for HTTP/1 and HTTP/2 connections) acl is_host_com hdr(Host) -i example.com tcp-request inspect-delay 5s - tcp-request switch-mode http if HTTP - tcp-request reject # non-HTTP traffic is implicit here + tcp-request content switch-mode http if HTTP + tcp-request content reject # non-HTTP traffic is implicit here ... http-request reject unless is_host_com diff -Nru haproxy-3.0.9/include/haproxy/compiler.h haproxy-3.0.10/include/haproxy/compiler.h --- haproxy-3.0.9/include/haproxy/compiler.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/haproxy/compiler.h 2025-04-22 14:53:02.000000000 +0300 @@ -248,6 +248,7 @@ #define TOSTR(x) _TOSTR(x) /* concatenates the two strings after resolving possible macros */ +#undef CONCAT // Turns out NetBSD defines it to the same in exec_elf.h #define _CONCAT(a,b) a ## b #define CONCAT(a,b) _CONCAT(a,b) diff -Nru haproxy-3.0.9/include/haproxy/fd.h haproxy-3.0.10/include/haproxy/fd.h --- haproxy-3.0.9/include/haproxy/fd.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/haproxy/fd.h 2025-04-22 14:53:02.000000000 +0300 @@ -489,6 +489,10 @@ fdtab[fd].iocb = iocb; fdtab[fd].state = newstate; fdtab[fd].thread_mask = thread_mask; + + /* just for debugging: how many times taken over since last fd_insert() */ + fdtab[fd].nb_takeover = 0; + fd_drop_tgid(fd); #ifdef DEBUG_FD diff -Nru haproxy-3.0.9/include/haproxy/fd-t.h haproxy-3.0.10/include/haproxy/fd-t.h --- haproxy-3.0.9/include/haproxy/fd-t.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/haproxy/fd-t.h 2025-04-22 14:53:02.000000000 +0300 @@ -193,6 +193,12 @@ void *owner; /* the connection or listener associated with this fd, NULL if closed */ unsigned int state; /* FD state for read and write directions (FD_EV_*) + FD_POLL_* */ unsigned int refc_tgid; /* refcounted tgid, updated atomically */ + /* the info below are mainly used for epoll debugging/strengthening. + * they're filling the rest of the cache line but may easily be dropped + * if the room is needed for more important stuff. + */ + unsigned int nb_takeover; /* number of times this FD was taken over since inserted (used for debugging) */ + unsigned int generation; /* number of times this FD was closed before (used for epoll strengthening) */ #ifdef DEBUG_FD unsigned int event_count; /* number of events reported */ #endif diff -Nru haproxy-3.0.9/include/haproxy/mux_fcgi-t.h haproxy-3.0.10/include/haproxy/mux_fcgi-t.h --- haproxy-3.0.9/include/haproxy/mux_fcgi-t.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/haproxy/mux_fcgi-t.h 2025-04-22 14:53:02.000000000 +0300 @@ -58,6 +58,9 @@ #define FCGI_CF_ERR_PENDING 0x00008000 /* A write error was detected (block sends but not reads) */ #define FCGI_CF_ERROR 0x00010000 /* A read error was detected (handled has an abort) */ +#define FCGI_CF_DEM_SHORT_READ 0x00020000 /* demux blocked on incomplete frame */ +#define FCGI_CF_END_REACHED 0x00040000 /* pending data too short with RCVD_SHUT present */ + /* This function is used to report flags in debugging tools. Please reflect * below any single-bit flag addition above in the same order via the diff -Nru haproxy-3.0.9/include/haproxy/quic_conn.h haproxy-3.0.10/include/haproxy/quic_conn.h --- haproxy-3.0.9/include/haproxy/quic_conn.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/haproxy/quic_conn.h 2025-04-22 14:53:02.000000000 +0300 @@ -130,7 +130,11 @@ } -/* Allocate the underlying required memory for <ncbuf> non-contiguous buffer */ +/* Allocate the underlying required memory for <ncbuf> non-contiguous buffer. + * Does nothing if buffer is already allocated. + * + * Returns the buffer instance or NULL on allocation failure. + */ static inline struct ncbuf *quic_get_ncbuf(struct ncbuf *ncbuf) { struct buffer buf = BUF_NULL; @@ -138,8 +142,8 @@ if (!ncb_is_null(ncbuf)) return ncbuf; - b_alloc(&buf, DB_MUX_RX); - BUG_ON(b_is_null(&buf)); + if (!b_alloc(&buf, DB_MUX_RX)) + return NULL; *ncbuf = ncb_make(buf.area, buf.size, 0); ncb_init(ncbuf, 0); diff -Nru haproxy-3.0.9/include/haproxy/task.h haproxy-3.0.10/include/haproxy/task.h --- haproxy-3.0.9/include/haproxy/task.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/haproxy/task.h 2025-04-22 14:53:02.000000000 +0300 @@ -751,6 +751,17 @@ } } +static inline struct notification *_notification_new(struct list *purge, struct task *wakeup) +{ + struct notification *com = pool_alloc(pool_head_notification); + if (!com) + return NULL; + LIST_APPEND(purge, &com->purge_me); + HA_SPIN_INIT(&com->lock); + com->task = wakeup; + return com; +} + /* This function register a new signal. "lua" is the current lua * execution context. It contains a pointer to the associated task. * "link" is a list head attached to an other task that must be wake @@ -760,13 +771,20 @@ */ static inline struct notification *notification_new(struct list *purge, struct list *event, struct task *wakeup) { - struct notification *com = pool_alloc(pool_head_notification); + struct notification *com = _notification_new(purge, wakeup); if (!com) return NULL; - LIST_APPEND(purge, &com->purge_me); LIST_APPEND(event, &com->wake_me); - HA_SPIN_INIT(&com->lock); - com->task = wakeup; + return com; +} + +/* thread safe variant */ +static inline struct notification *notification_new_mt(struct list *purge, struct mt_list *event, struct task *wakeup) +{ + struct notification *com = _notification_new(purge, wakeup); + if (!com) + return NULL; + MT_LIST_APPEND(event, &com->wake_me_mt); return com; } @@ -815,6 +833,19 @@ } } +static inline void _notification_wake(struct notification *com) +{ + HA_SPIN_LOCK(NOTIF_LOCK, &com->lock); + if (!com->task) { + HA_SPIN_UNLOCK(NOTIF_LOCK, &com->lock); + pool_free(pool_head_notification, com); + return; + } + task_wakeup(com->task, TASK_WOKEN_MSG); + com->task = NULL; + HA_SPIN_UNLOCK(NOTIF_LOCK, &com->lock); + +} /* This function sends signals. It wakes all the tasks attached * to a list head, and remove the signal, and free the used * memory. The wake list is not locked because it is owned by @@ -827,16 +858,21 @@ /* Wake task and delete all pending communication signals. */ list_for_each_entry_safe(com, back, wake, wake_me) { - HA_SPIN_LOCK(NOTIF_LOCK, &com->lock); LIST_DELETE(&com->wake_me); - if (!com->task) { - HA_SPIN_UNLOCK(NOTIF_LOCK, &com->lock); - pool_free(pool_head_notification, com); - continue; - } - task_wakeup(com->task, TASK_WOKEN_MSG); - com->task = NULL; - HA_SPIN_UNLOCK(NOTIF_LOCK, &com->lock); + _notification_wake(com); + } +} + +/* thread safe variant */ +static inline void notification_wake_mt(struct mt_list *wake) +{ + struct notification *com; + struct mt_list *elt1, elt2; + + /* Wake task and delete all pending communication signals. */ + mt_list_for_each_entry_safe(com, wake, wake_me_mt, elt1, elt2) { + MT_LIST_DELETE_SAFE(elt1); + _notification_wake(com); } } diff -Nru haproxy-3.0.9/include/haproxy/task-t.h haproxy-3.0.10/include/haproxy/task-t.h --- haproxy-3.0.9/include/haproxy/task-t.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/haproxy/task-t.h 2025-04-22 14:53:02.000000000 +0300 @@ -105,8 +105,13 @@ struct notification { struct list purge_me; /* Part of the list of signals to be purged in the case of the LUA execution stack crash. */ - struct list wake_me; /* Part of list of signals to be targeted if an - event occurs. */ + union { + struct list wake_me; /* Part of list of signals to be targeted if an + event occurs. */ + struct mt_list wake_me_mt; /* thread safe signal list */ + } wake; +# define wake_me wake.wake_me +# define wake_me_mt wake.wake_me_mt struct task *task; /* The task to be wake if an event occurs. */ __decl_thread(HA_SPINLOCK_T lock); }; diff -Nru haproxy-3.0.9/include/import/plock.h haproxy-3.0.10/include/import/plock.h --- haproxy-3.0.9/include/import/plock.h 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/include/import/plock.h 2025-04-22 14:53:02.000000000 +0300 @@ -93,7 +93,7 @@ loops -= 32768; } #endif - for (; loops >= 60; loops --) + for (; loops >= 90; loops --) pl_cpu_relax(); for (; loops >= 1; loops--) @@ -107,7 +107,7 @@ * values and still growing. This allows competing threads to * wait different times once the threshold is reached. */ - m = ((m + (m >> 1)) + 2) & 0x3ffff; + m = ((m + (m >> 2)) + 1) & 0x1ffff; } while (1); return ret; @@ -176,7 +176,7 @@ * values and still growing. This allows competing threads to * wait different times once the threshold is reached. */ - m = ((m + (m >> 1)) + 2) & 0x3ffff; + m = ((m + (m >> 2)) + 1) & 0x1ffff; } while (1); return ret; @@ -549,6 +549,13 @@ __pl_r = pl_ldadd_acq(__lk_r, __set_r); \ if (!__builtin_expect(__pl_r & __msk_r, 0)) \ break; \ + if (!__builtin_expect(__pl_r & PLOCK64_WL_ANY, 0)) { \ + /* S only: let it finish but impose ourselves */ \ + pl_sub_noret_lax(__lk_r, PLOCK64_RL_1); \ + __pl_r = pl_wait_unlock_long(__lk_r, PLOCK64_RL_ANY); \ + __pl_r = pl_ldadd_acq(__lk_r, PLOCK64_RL_1); \ + break; \ + } \ pl_sub_noret_lax(__lk_r, __set_r); \ __pl_r = pl_wait_unlock_long(__lk_r, __msk_r); \ } \ @@ -566,6 +573,13 @@ __pl_r = pl_ldadd_acq(__lk_r, __set_r); \ if (!__builtin_expect(__pl_r & __msk_r, 0)) \ break; \ + if (!__builtin_expect(__pl_r & PLOCK32_WL_ANY, 0)) { \ + /* S only: let it finish but impose ourselves */ \ + pl_sub_noret_lax(__lk_r, PLOCK32_RL_1); \ + __pl_r = pl_wait_unlock_int(__lk_r, PLOCK32_RL_ANY); \ + __pl_r = pl_ldadd_acq(__lk_r, PLOCK32_RL_1); \ + break; \ + } \ pl_sub_noret_lax(__lk_r, __set_r); \ __pl_r = pl_wait_unlock_int(__lk_r, __msk_r); \ } \ diff -Nru haproxy-3.0.9/src/backend.c haproxy-3.0.10/src/backend.c --- haproxy-3.0.9/src/backend.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/backend.c 2025-04-22 14:53:02.000000000 +0300 @@ -858,13 +858,14 @@ return err; } -/* Allocate an address for the destination endpoint - * The address is taken from the currently assigned server, or from the - * dispatch or transparent address. +/* Allocate <*ss> address unless already set. Address is then set to the + * destination endpoint of <srv> server, or via <s> from a dispatch or + * transparent address. * - * Returns SRV_STATUS_OK on success. Does nothing if the address was - * already set. - * On error, no address is allocated and SRV_STATUS_INTERNAL is returned. + * Note that no address is allocated if server relies on reverse HTTP. + * + * Returns SRV_STATUS_OK on success, or if already already set. Else an error + * code is returned and <*ss> is not allocated. */ static int alloc_dst_address(struct sockaddr_storage **ss, struct server *srv, struct stream *s) @@ -874,6 +875,11 @@ if (*ss) return SRV_STATUS_OK; + if (srv && (srv->flags & SRV_F_RHTTP)) { + /* For reverse HTTP, destination address is unknown. */ + return SRV_STATUS_OK; + } + if ((s->flags & SF_DIRECT) || (s->be->lbprm.algo & BE_LB_KIND)) { /* A server is necessarily known for this stream */ if (!(s->flags & SF_ASSIGNED)) @@ -1151,6 +1157,15 @@ return SRV_STATUS_INTERNAL; **ss = *addr; + if ((src->opts & CO_SRC_TPROXY_MASK) == CO_SRC_TPROXY_CIP) { + /* always set port to zero when using "clientip", or + * the idle connection hash will include the port part. + */ + if (addr->ss_family == AF_INET) + ((struct sockaddr_in *)*ss)->sin_port = 0; + else if (addr->ss_family == AF_INET6) + ((struct sockaddr_in6 *)*ss)->sin6_port = 0; + } break; case CO_SRC_TPROXY_DYN: @@ -1433,8 +1448,7 @@ } /* 3. destination address */ - if (srv && srv_is_transparent(srv)) - hash_params.dst_addr = s->scb->dst; + hash_params.dst_addr = s->scb->dst; /* 4. source address */ hash_params.src_addr = bind_addr; @@ -1665,6 +1679,9 @@ srv_conn->src = bind_addr; bind_addr = NULL; + /* copy the target address into the connection */ + *srv_conn->dst = *s->scb->dst; + /* mark? */ if (s->flags & SF_BC_MARK) { srv_conn->mark = s->bc_mark; @@ -1688,9 +1705,6 @@ if (!srv_conn) return SF_ERR_RESOURCE; - /* copy the target address into the connection */ - *srv_conn->dst = *s->scb->dst; - /* Copy network namespace from client connection */ srv_conn->proxy_netns = cli_conn ? cli_conn->proxy_netns : NULL; diff -Nru haproxy-3.0.9/src/cli.c haproxy-3.0.10/src/cli.c --- haproxy-3.0.9/src/cli.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/cli.c 2025-04-22 14:53:02.000000000 +0300 @@ -1411,7 +1411,7 @@ suspicious = 1; chunk_printf(&trash, - " %5d : st=0x%06x(%c%c %c%c%c%c%c W:%c%c%c R:%c%c%c) ref=%#x gid=%d tmask=0x%lx umask=0x%lx prmsk=0x%lx pwmsk=0x%lx owner=%p iocb=%p(", + " %5d : st=0x%06x(%c%c %c%c%c%c%c W:%c%c%c R:%c%c%c) ref=%#x gid=%d tmask=0x%lx umask=0x%lx prmsk=0x%lx pwmsk=0x%lx owner=%p gen=%u tkov=%u iocb=%p(", fd, fdt.state, (fdt.state & FD_CLONED) ? 'C' : 'c', @@ -1433,6 +1433,8 @@ polled_mask[fd].poll_recv, polled_mask[fd].poll_send, fdt.owner, + fdt.generation, + fdt.nb_takeover, fdt.iocb); resolve_sym_name(&trash, NULL, fdt.iocb); diff -Nru haproxy-3.0.9/src/debug.c haproxy-3.0.10/src/debug.c --- haproxy-3.0.9/src/debug.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/debug.c 2025-04-22 14:53:02.000000000 +0300 @@ -348,6 +348,7 @@ { struct buffer *old = NULL; +#ifdef USE_THREAD_DUMP /* A thread that's currently dumping other threads cannot be dumped, or * it will very likely cause a deadlock. */ @@ -366,14 +367,12 @@ old = NULL; } while (!HA_ATOMIC_CAS(&ha_thread_ctx[thr].thread_dump_buffer, &old, buf)); -#ifdef USE_THREAD_DUMP /* asking the remote thread to dump itself allows to get more details * including a backtrace. */ if (thr != tid) ha_tkill(thr, DEBUGSIG); else -#endif ha_thread_dump_one(thr, thr != tid); /* now wait for the dump to be done (or cancelled) */ @@ -388,6 +387,20 @@ } ha_thread_relax(); } +#else /* !USE_THREAD_DUMP below, we're on the target thread */ + /* when thread-dump is not supported, we can only dump our own thread */ + if (thr != tid) + return NULL; + + /* the buffer might not be valid in case of a panic, since we + * have to allocate it ourselves in this case. + */ + if ((ulong)buf == 0x2UL) + buf = get_trash_chunk(); + HA_ATOMIC_STORE(&th_ctx->thread_dump_buffer, buf); + old = buf; + ha_thread_dump_one(tid, 0); +#endif return (struct buffer *)((ulong)old & ~0x1UL); } diff -Nru haproxy-3.0.9/src/ev_epoll.c haproxy-3.0.10/src/ev_epoll.c --- haproxy-3.0.9/src/ev_epoll.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/ev_epoll.c 2025-04-22 14:53:02.000000000 +0300 @@ -16,6 +16,7 @@ #include <haproxy/activity.h> #include <haproxy/api.h> +#include <haproxy/cfgparse.h> #include <haproxy/clock.h> #include <haproxy/fd.h> #include <haproxy/global.h> @@ -28,6 +29,7 @@ /* private data */ static THREAD_LOCAL struct epoll_event *epoll_events = NULL; static int epoll_fd[MAX_THREADS] __read_mostly; // per-thread epoll_fd +static uint epoll_mask = 0; // events to be masked and turned to EPOLLIN #ifndef EPOLLRDHUP /* EPOLLRDHUP was defined late in libc, and it appeared in kernel 2.6.17 */ @@ -150,7 +152,8 @@ ev.events |= EPOLLOUT; done: - ev.data.fd = fd; + ev.events &= ~epoll_mask; + ev.data.u64 = ((u64)fdtab[fd].generation << 32) + fd; epoll_ctl(epoll_fd[tid], opcode, fd, &ev); } @@ -249,9 +252,55 @@ for (count = 0; count < status; count++) { unsigned int n, e; + uint64_t epoll_data; + uint ev_gen, fd_gen; e = epoll_events[count].events; - fd = epoll_events[count].data.fd; + epoll_data = epoll_events[count].data.u64; + + /* epoll_data contains the fd's generation in the 32 upper bits + * and the fd in the 32 lower ones. + */ + fd = (uint32_t)epoll_data; + ev_gen = epoll_data >> 32; + fd_gen = _HA_ATOMIC_LOAD(&fdtab[fd].generation); + + if (unlikely(ev_gen != fd_gen)) { + /* this is a stale report for an older instance of this FD, + * we must ignore it. + */ + + if (_HA_ATOMIC_LOAD(&fdtab[fd].owner)) { + ulong tmask = _HA_ATOMIC_LOAD(&fdtab[fd].thread_mask); + if (!(tmask & ti->ltid_bit)) { + /* thread has change. quite common, that's already handled + * by fd_update_events(), let's just report sensitivive + * events for statistics purposes. + */ + if (e & (EPOLLRDHUP|EPOLLHUP|EPOLLERR)) + COUNT_IF(1, "epoll report of HUP/ERR on a stale fd reopened on another thread (harmless)"); + } else { + /* same thread but different generation, this smells bad, + * maybe that could be caused by crossed takeovers with a + * close() in between or something like this, but this is + * something fd_update_events() cannot detect. It still + * remains relatively safe for HUP because we consider it + * once we've read all pending data. + */ + if (e & EPOLLERR) + COUNT_IF(1, "epoll report of ERR on a stale fd reopened on the same thread (suspicious)"); + else if (e & (EPOLLRDHUP|EPOLLHUP)) + COUNT_IF(1, "epoll report of HUP on a stale fd reopened on the same thread (suspicious)"); + else + COUNT_IF(1, "epoll report of a harmless event on a stale fd reopened on the same thread (suspicious)"); + } + } else if (ev_gen + 1 != fd_gen) { + COUNT_IF(1, "epoll report of event on a closed recycled fd (rare)"); + } else { + COUNT_IF(1, "epoll report of event on a just closed fd (harmless)"); + } + continue; + } if ((e & EPOLLRDHUP) && !(cur_poller.flags & HAP_POLL_F_RDHUP)) _HA_ATOMIC_OR(&cur_poller.flags, HAP_POLL_F_RDHUP); @@ -259,6 +308,11 @@ #ifdef DEBUG_FD _HA_ATOMIC_INC(&fdtab[fd].event_count); #endif + if (e & epoll_mask) { + e |= EPOLLIN; + e &= ~epoll_mask; + } + n = ((e & EPOLLIN) ? FD_EV_READY_R : 0) | ((e & EPOLLOUT) ? FD_EV_READY_W : 0) | ((e & EPOLLRDHUP) ? FD_EV_SHUT_R : 0) | @@ -404,6 +458,43 @@ p->fork = _do_fork; } +/* config parser for global "tune.epoll.mask-events", accepts "err", "hup", "rdhup" */ +static int cfg_parse_tune_epoll_mask_events(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + char *comma, *kw; + + if (too_many_args(1, args, err, NULL)) + return -1; + + epoll_mask = 0; + for (kw = args[1]; kw && *kw; kw = comma) { + comma = strchr(kw, ','); + if (comma) + *(comma++) = 0; + + if (strcmp(kw, "err") == 0) + epoll_mask |= EPOLLERR; + else if (strcmp(kw, "hup") == 0) + epoll_mask |= EPOLLHUP; + else if (strcmp(kw, "rdhup") == 0) + epoll_mask |= EPOLLRDHUP; + else { + memprintf(err, "'%s' expects a comma-delimited list of 'err', 'hup' and 'rdhup' but got '%s'.", args[0], kw); + return -1; + } + } + return 0; +} + +/* config keyword parsers */ +static struct cfg_kw_list cfg_kws = {ILH, { + { CFG_GLOBAL, "tune.epoll.mask-events", cfg_parse_tune_epoll_mask_events }, + { 0, NULL, NULL } +}}; + +INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws); INITCALL0(STG_REGISTER, _do_register); diff -Nru haproxy-3.0.9/src/fd.c haproxy-3.0.10/src/fd.c --- haproxy-3.0.9/src/fd.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/fd.c 2025-04-22 14:53:02.000000000 +0300 @@ -354,8 +354,10 @@ /* perform the close() call last as it's what unlocks the instant reuse * of this FD by any other thread. */ - if (!fd_disown) + if (!fd_disown) { + fdtab[fd].generation++; close(fd); + } _HA_ATOMIC_DEC(&ha_used_fds); } @@ -541,6 +543,9 @@ */ fd_stop_recv(fd); + /* essentially for debugging */ + fdtab[fd].nb_takeover++; + /* we're done with it */ HA_ATOMIC_AND(&fdtab[fd].running_mask, ~ti->ltid_bit); diff -Nru haproxy-3.0.9/src/h3.c haproxy-3.0.10/src/h3.c --- haproxy-3.0.9/src/h3.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/h3.c 2025-04-22 14:53:02.000000000 +0300 @@ -529,6 +529,7 @@ unsigned int flags = HTX_SL_F_NONE; struct ist meth = IST_NULL, path = IST_NULL; struct ist scheme = IST_NULL, authority = IST_NULL; + struct ist v; int hdr_idx, ret; int cookie = -1, last_cookie = -1, i; const char *ctl; @@ -732,6 +733,37 @@ flags |= HTX_SL_F_VER_11; flags |= HTX_SL_F_XFER_LEN; + /* RFC 9114 4.3.1. Request Pseudo-Header Fields + * + * This pseudo-header field MUST NOT be empty for "http" or "https" + * URIs; "http" or "https" URIs that do not contain a path component + * MUST include a value of / (ASCII 0x2f). An OPTIONS request that + * does not include a path component includes the value * (ASCII + * 0x2a) for the :path pseudo-header field; see Section 7.1 of + * [HTTP]. + */ + if ((isteqi(scheme, ist("http")) || isteqi(scheme, ist("https"))) && + (!istlen(path) || + (istptr(path)[0] != '/' && !isteq(path, ist("*"))))) { + TRACE_ERROR("invalid ':path' pseudo-header", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs); + h3s->err = H3_ERR_MESSAGE_ERROR; + qcc_report_glitch(h3c->qcc, 1); + len = -1; + goto out; + } + + /* Ensure that final URI does not contains LWS nor CTL characters. */ + for (i = 0; i < path.len; i++) { + unsigned char c = istptr(path)[i]; + if (HTTP_IS_LWS(c) || HTTP_IS_CTL(c)) { + TRACE_ERROR("invalid character in path", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs); + h3s->err = H3_ERR_MESSAGE_ERROR; + qcc_report_glitch(h3c->qcc, 1); + len = -1; + goto out; + } + } + sl = htx_add_stline(htx, HTX_BLK_REQ_SL, flags, meth, path, ist("HTTP/3.0")); if (!sl) { len = -1; @@ -838,6 +870,7 @@ else if (isteq(list[hdr_idx].n, ist("connection")) || isteq(list[hdr_idx].n, ist("proxy-connection")) || isteq(list[hdr_idx].n, ist("keep-alive")) || + isteq(list[hdr_idx].n, ist("upgrade")) || isteq(list[hdr_idx].n, ist("transfer-encoding"))) { /* RFC 9114 4.2. HTTP Fields * @@ -869,7 +902,15 @@ goto out; } - if (!htx_add_header(htx, list[hdr_idx].n, list[hdr_idx].v)) { + /* trim leading/trailing LWS */ + for (v = list[hdr_idx].v; v.len; v.len--) { + if (unlikely(HTTP_IS_LWS(*v.ptr))) + v.ptr++; + else if (!unlikely(HTTP_IS_LWS(v.ptr[v.len - 1]))) + break; + } + + if (!htx_add_header(htx, list[hdr_idx].n, v)) { len = -1; goto out; } @@ -972,6 +1013,7 @@ int hdr_idx, ret; const char *ctl; int qpack_err; + struct ist v; int i; TRACE_ENTER(H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs); @@ -1048,6 +1090,7 @@ isteq(list[hdr_idx].n, ist("connection")) || isteq(list[hdr_idx].n, ist("proxy-connection")) || isteq(list[hdr_idx].n, ist("keep-alive")) || + isteq(list[hdr_idx].n, ist("upgrade")) || isteq(list[hdr_idx].n, ist("te")) || isteq(list[hdr_idx].n, ist("transfer-encoding"))) { TRACE_ERROR("forbidden HTTP/3 headers", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs); @@ -1077,7 +1120,15 @@ goto out; } - if (!htx_add_trailer(htx, list[hdr_idx].n, list[hdr_idx].v)) { + /* trim leading/trailing LWS */ + for (v = list[hdr_idx].v; v.len; v.len--) { + if (unlikely(HTTP_IS_LWS(*v.ptr))) + v.ptr++; + else if (!unlikely(HTTP_IS_LWS(v.ptr[v.len - 1]))) + break; + } + + if (!htx_add_trailer(htx, list[hdr_idx].n, v)) { TRACE_ERROR("cannot add trailer", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs); len = -1; goto out; @@ -1565,6 +1616,29 @@ return -1; } +/* Encode header field name <n> value <v> into <buf> buffer using QPACK. Strip + * any leading/trailing WS in value prior to encoding. + * + * Returns 0 on success else non zero. + */ +static int h3_encode_header(struct buffer *buf, + const struct ist n, const struct ist v) +{ + struct ist v_strip; + char *ptr; + + /* trim leading/trailing LWS */ + for (v_strip = v; istlen(v_strip); --v_strip.len) { + ptr = istptr(v_strip); + if (unlikely(HTTP_IS_LWS(*ptr))) + ++v_strip.ptr; + else if (!unlikely(HTTP_IS_LWS(ptr[istlen(v_strip) - 1]))) + break; + } + + return qpack_encode_header(buf, n, v_strip); +} + static int h3_resp_headers_send(struct qcs *qcs, struct htx *htx) { int err; @@ -1662,6 +1736,7 @@ if (isteq(list[hdr].n, ist("connection")) || isteq(list[hdr].n, ist("proxy-connection")) || isteq(list[hdr].n, ist("keep-alive")) || + isteq(list[hdr].n, ist("upgrade")) || isteq(list[hdr].n, ist("transfer-encoding"))) { continue; } @@ -1675,7 +1750,7 @@ list[hdr].v = ist("trailers"); } - if (qpack_encode_header(&headers_buf, list[hdr].n, list[hdr].v)) + if (h3_encode_header(&headers_buf, list[hdr].n, list[hdr].v)) goto err; } @@ -1824,12 +1899,13 @@ isteq(list[hdr].n, ist("connection")) || isteq(list[hdr].n, ist("proxy-connection")) || isteq(list[hdr].n, ist("keep-alive")) || + isteq(list[hdr].n, ist("upgrade")) || isteq(list[hdr].n, ist("te")) || isteq(list[hdr].n, ist("transfer-encoding"))) { continue; } - if (qpack_encode_header(&headers_buf, list[hdr].n, list[hdr].v)) { + if (h3_encode_header(&headers_buf, list[hdr].n, list[hdr].v)) { TRACE_STATE("not enough room for all trailers", H3_EV_TX_FRAME|H3_EV_TX_HDR, qcs->qcc->conn, qcs); if (qcc_release_stream_txbuf(qcs)) goto end; diff -Nru haproxy-3.0.9/src/hlua.c haproxy-3.0.10/src/hlua.c --- haproxy-3.0.9/src/hlua.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/hlua.c 2025-04-22 14:53:02.000000000 +0300 @@ -10897,6 +10897,7 @@ struct act_rule *rule = ctx->rule; struct proxy *px = strm->be; struct hlua *hlua = tcp_ctx->hlua; + int yield = 0; if (unlikely(se_fl_test(ctx->sedesc, (SE_FL_EOS|SE_FL_ERROR|SE_FL_SHR|SE_FL_SHW)))) goto out; @@ -10915,6 +10916,7 @@ /* yield. */ case HLUA_E_AGAIN: + yield = 1; if (hlua->wake_time != TICK_ETERNITY) task_schedule(tcp_ctx->task, hlua->wake_time); break; @@ -10960,7 +10962,12 @@ } out: - /* eat the whole request */ + /* eat the whole request unless yield was requested which means + * we are not done yet + */ + if (yield) + return; + if (ctx->flags & APPCTX_FL_INOUT_BUFS) b_reset(&ctx->inbuf); else @@ -11102,6 +11109,7 @@ struct proxy *px = strm->be; struct hlua *hlua = http_ctx->hlua; struct htx *req_htx, *res_htx; + int yield = 0; res_htx = htx_from_buf(&res->buf); @@ -11136,6 +11144,7 @@ /* yield. */ case HLUA_E_AGAIN: + yield = 1; if (hlua->wake_time != TICK_ETERNITY) task_schedule(http_ctx->task, hlua->wake_time); goto out; @@ -11208,7 +11217,13 @@ out: htx_to_buf(res_htx, &res->buf); - /* eat the whole request */ + + /* eat the whole request unless yield was requested which means + * we are not done yet + */ + if (yield) + return; + if (co_data(req)) { req_htx = htx_from_buf(&req->buf); co_htx_skip(req, req_htx, co_data(req)); @@ -14290,7 +14305,7 @@ void hlua_init(void) { int i; - char *errmsg; + char *errmsg = NULL; #ifdef USE_OPENSSL struct srv_kw *kw; int tmp_error; diff -Nru haproxy-3.0.9/src/hlua_fcn.c haproxy-3.0.10/src/hlua_fcn.c --- haproxy-3.0.9/src/hlua_fcn.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/hlua_fcn.c 2025-04-22 14:53:02.000000000 +0300 @@ -513,22 +513,10 @@ struct mt_list list; }; -/* used to store wait entries in queue->wait_tasks */ -struct hlua_queue_wait -{ - struct task *task; - struct mt_list entry; -}; - /* This is the memory pool containing struct hlua_queue_item (queue items) */ DECLARE_STATIC_POOL(pool_head_hlua_queue, "hlua_queue", sizeof(struct hlua_queue_item)); -/* This is the memory pool containing struct hlua_queue_wait - * (queue waiting tasks) - */ -DECLARE_STATIC_POOL(pool_head_hlua_queuew, "hlua_queuew", sizeof(struct hlua_queue_wait)); - static struct hlua_queue *hlua_check_queue(lua_State *L, int ud) { return hlua_checkudata(L, ud, class_queue_ref); @@ -555,8 +543,6 @@ { struct hlua_queue *queue = hlua_check_queue(L, 1); struct hlua_queue_item *item; - struct mt_list *elt1, elt2; - struct hlua_queue_wait *waiter; if (lua_gettop(L) != 2 || lua_isnoneornil(L, 2)) { luaL_error(L, "unexpected argument"); @@ -581,9 +567,7 @@ MT_LIST_APPEND(&queue->list, &item->list); /* notify tasks waiting on queue:pop_wait() (if any) */ - mt_list_for_each_entry_safe(waiter, &queue->wait_tasks, entry, elt1, elt2) { - task_wakeup(waiter->task, TASK_WOKEN_MSG); - } + notification_wake_mt(&queue->wait_tasks); lua_pushboolean(L, 1); return 1; @@ -639,24 +623,25 @@ static int _hlua_queue_pop_wait(lua_State *L, int status, lua_KContext ctx) { struct hlua_queue *queue = hlua_check_queue(L, 1); - struct hlua_queue_wait *wait = lua_touserdata(L, 2); /* new pop attempt */ if (!_hlua_queue_pop(L, queue)) { + struct hlua *hlua; + + hlua = hlua_gethlua(L); + if (!notification_new_mt(&hlua->com, &queue->wait_tasks, hlua->task)) { + lua_pushnil(L); + return 1; /* memory error, return nil */ + } hlua_yieldk(L, 0, 0, _hlua_queue_pop_wait, TICK_ETERNITY, 0); // wait retry return 0; // never reached, yieldk won't return } - /* remove task from waiting list */ - MT_LIST_DELETE(&wait->entry); - pool_free(pool_head_hlua_queuew, wait); - return 1; // success } static int hlua_queue_pop_wait(lua_State *L) { struct hlua_queue *queue = hlua_check_queue(L, 1); - struct hlua_queue_wait *wait; struct hlua *hlua; BUG_ON(!queue); @@ -676,21 +661,6 @@ /* no pending items, waiting required */ - wait = pool_alloc(pool_head_hlua_queuew); - if (!wait) { - lua_pushnil(L); - return 1; /* memory error, return nil */ - } - - wait->task = hlua->task; - MT_LIST_INIT(&wait->entry); - - /* add task to queue's wait list */ - MT_LIST_TRY_APPEND(&queue->wait_tasks, &wait->entry); - - /* push wait entry at index 2 on the stack (queue is already there) */ - lua_pushlightuserdata(L, wait); - /* Go to waiting loop which immediately performs a new attempt to make * sure we didn't miss a push during the wait entry initialization. * @@ -736,20 +706,8 @@ static int hlua_queue_gc(struct lua_State *L) { struct hlua_queue *queue = hlua_check_queue(L, 1); - struct hlua_queue_wait *wait; struct hlua_queue_item *item; - /* Purge waiting tasks (if any) - * - * It is normally not expected to have waiting tasks, except if such - * task has been aborted while in the middle of a queue:pop_wait() - * function call. - */ - while ((wait = MT_LIST_POP(&queue->wait_tasks, typeof(wait), entry))) { - /* free the wait entry */ - pool_free(pool_head_hlua_queuew, wait); - } - /* purge remaining (unconsumed) items in the queue */ while ((item = MT_LIST_POP(&queue->list, typeof(item), list))) { /* free the queue item */ diff -Nru haproxy-3.0.9/src/http_ana.c haproxy-3.0.10/src/http_ana.c --- haproxy-3.0.9/src/http_ana.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/http_ana.c 2025-04-22 14:53:02.000000000 +0300 @@ -501,8 +501,9 @@ if (!ret) continue; } - if (!http_apply_redirect_rule(rule, s, txn)) + if (!http_apply_redirect_rule(rule, s, txn)) { goto return_int_err; + } goto done; } @@ -989,12 +990,15 @@ if ((s->scb->flags & SC_FL_SHUT_DONE) && co_data(req)) { /* request errors are most likely due to the server aborting the - * transfer.Bit handle server aborts only if there is no - * response. Otherwise, let a change to forward the response - * first. + * transfer. But handle server aborts only if the response was + * not received yet. Otherwise, let the response analyzer the + * responsability to handle the error. It is especially + * important to properly handle L7-retries but also K/A silent close. */ - if (txn->rsp.msg_state >= HTTP_MSG_BODY && htx_is_empty(htxbuf(&s->res.buf))) + if (txn->rsp.msg_state >= HTTP_MSG_BODY && htx_is_empty(htxbuf(&s->res.buf))) { + COUNT_IF(1, "Server abort during request forwarding"); goto return_srv_abort; + } } http_end_request(s); @@ -1026,17 +1030,23 @@ missing_data_or_waiting: /* stop waiting for data if the input is closed before the end */ - if (msg->msg_state < HTTP_MSG_ENDING && (s->scf->flags & (SC_FL_ABRT_DONE|SC_FL_EOS))) + if (msg->msg_state < HTTP_MSG_ENDING && (s->scf->flags & (SC_FL_ABRT_DONE|SC_FL_EOS))) { + COUNT_IF(1, "Client abort during request forwarding"); goto return_cli_abort; + } waiting: /* waiting for the last bits to leave the buffer */ if (s->scb->flags & SC_FL_SHUT_DONE) { - /* Handle server aborts only if there is no response. Otherwise, - * let a change to forward the response first. + /* Handle server aborts only if the response was not received + * yet. Otherwise, let the response analyzer the responsability + * to handle the error. It is especially important to properly + * handle L7-retries but also K/A silent close. */ - if (htx_is_empty(htxbuf(&s->res.buf))) + if (txn->rsp.msg_state >= HTTP_MSG_BODY && htx_is_empty(htxbuf(&s->res.buf))) { + COUNT_IF(1, "Server abort during request forwarding"); goto return_srv_abort; + } } /* When TE: chunked is used, we need to get there again to parse remaining @@ -1099,6 +1109,7 @@ if (objt_server(s->target)) _HA_ATOMIC_INC(&__objt_server(s->target)->counters.internal_errors); status = 500; + COUNT_IF(1, "Internal error during request forwarding"); goto return_prx_cond; return_bad_req: @@ -1106,6 +1117,7 @@ if (sess->listener && sess->listener->counters) _HA_ATOMIC_INC(&sess->listener->counters->failed_req); status = 400; + COUNT_IF(1, "Request parsing error during request forwarding"); /* fall through */ return_prx_cond: @@ -2137,6 +2149,7 @@ if ((s->scf->flags & SC_FL_SHUT_DONE) && co_data(res)) { /* response errors are most likely due to the client aborting * the transfer. */ + COUNT_IF(1, "Client abort during response forwarding"); goto return_cli_abort; } @@ -2150,8 +2163,10 @@ return 0; missing_data_or_waiting: - if (s->scf->flags & SC_FL_SHUT_DONE) + if (s->scf->flags & SC_FL_SHUT_DONE) { + COUNT_IF(1, "Client abort during response forwarding"); goto return_cli_abort; + } /* stop waiting for data if the input is closed before the end. If the * client side was already closed, it means that the client has aborted, @@ -2159,12 +2174,15 @@ * server abort. */ if (msg->msg_state < HTTP_MSG_ENDING && (s->scb->flags & (SC_FL_EOS|SC_FL_ABRT_DONE))) { - if ((s->scf->flags & SC_FL_ABRT_DONE) && - (s->scb->flags & SC_FL_SHUT_DONE)) + if ((s->scb->flags & (SC_FL_ABRT_DONE|SC_FL_SHUT_DONE)) == (SC_FL_ABRT_DONE|SC_FL_SHUT_DONE)) { + COUNT_IF(1, "Client abort during response forwarding"); goto return_cli_abort; + } /* If we have some pending data, we continue the processing */ - if (htx_is_empty(htx)) + if (htx_is_empty(htx)) { + COUNT_IF(1, "Server abort during response forwarding"); goto return_srv_abort; + } } /* When TE: chunked is used, we need to get there again to parse @@ -2224,6 +2242,7 @@ _HA_ATOMIC_INC(&__objt_server(s->target)->counters.internal_errors); if (!(s->flags & SF_ERR_MASK)) s->flags |= SF_ERR_INTERNAL; + COUNT_IF(1, "Internal error during response forwarding"); goto return_error; return_bad_res: @@ -2233,6 +2252,7 @@ health_adjust(__objt_server(s->target), HANA_STATUS_HTTP_RSP); } stream_inc_http_fail_ctr(s); + COUNT_IF(1, "Response parsing error during response forwarding"); /* fall through */ return_error: diff -Nru haproxy-3.0.9/src/log.c haproxy-3.0.10/src/log.c --- haproxy-3.0.9/src/log.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/log.c 2025-04-22 14:53:02.000000000 +0300 @@ -117,8 +117,8 @@ "warning", "notice", "info", "debug" }; -const char sess_term_cond[16] = "-LcCsSPRIDKUIIII"; /* normal, Local, CliTo, CliErr, SrvTo, SrvErr, PxErr, Resource, Internal, Down, Killed, Up, -- */ -const char sess_fin_state[8] = "-RCHDLQT"; /* cliRequest, srvConnect, srvHeader, Data, Last, Queue, Tarpit */ +const char sess_term_cond[] = "-LcCsSPRIDKUIIII"; /* normal, Local, CliTo, CliErr, SrvTo, SrvErr, PxErr, Resource, Internal, Down, Killed, Up, -- */ +const char sess_fin_state[] = "-RCHDLQT"; /* cliRequest, srvConnect, srvHeader, Data, Last, Queue, Tarpit */ const struct buffer empty = { }; @@ -682,11 +682,17 @@ else { /* custom type */ *str = 0; // so that typecast_str is 0 terminated - typecast = type_to_smp(typecast_str); - if (typecast != SMP_T_STR && typecast != SMP_T_SINT && - typecast != SMP_T_BOOL) { - memprintf(err, "unexpected output type '%.*s' at position %d line : '%s'. Supported types are: str, sint, bool", (int)(str - typecast_str), typecast_str, (int)(typecast_str - backfmt), fmt); - goto fail; + if (strcmp(typecast_str, "raw") == 0) { + /* special case */ + typecast = SMP_TYPES; + } + else { + typecast = type_to_smp(typecast_str); + if (typecast != SMP_T_STR && typecast != SMP_T_SINT && + typecast != SMP_T_BOOL) { + memprintf(err, "unexpected output type '%.*s' at position %d line : '%s'. Supported types are: str, sint, bool, raw", (int)(str - typecast_str), typecast_str, (int)(typecast_str - backfmt), fmt); + goto fail; + } } } cformat = LF_EDONAME; @@ -1873,8 +1879,6 @@ /* lf cbor function ptr used to encode a single byte according to RFC8949 * - * for now only hex form is supported. - * * The function may only be called under CBOR context (that is when * LOG_OPT_ENCODE_CBOR option is set). * @@ -2129,7 +2133,7 @@ encode_byte = _lf_map_escape_byte; if (ctx->options & LOG_OPT_ENCODE_CBOR) { - if (!bytes_stop) { + if (!bytes_stop || ctx->in_text) { /* printable chars: use cbor text */ cbor_string_prefix = 0x60; } @@ -3202,8 +3206,8 @@ return process_send_log(loggers, level, -1, metadata, message, size); } -const char sess_cookie[8] = "NIDVEOU7"; /* No cookie, Invalid cookie, cookie for a Down server, Valid cookie, Expired cookie, Old cookie, Unused, unknown */ -const char sess_set_cookie[8] = "NPDIRU67"; /* No set-cookie, Set-cookie found and left unchanged (passive), +const char sess_cookie[] = "NIDVEOU7"; /* No cookie, Invalid cookie, cookie for a Down server, Valid cookie, Expired cookie, Old cookie, Unused, unknown */ +const char sess_set_cookie[] = "NPDIRU67"; /* No set-cookie, Set-cookie found and left unchanged (passive), Set-cookie Deleted, Set-Cookie Inserted, Set-cookie Rewritten, Set-cookie Updated, unknown, unknown */ @@ -3822,9 +3826,16 @@ * know how to deal with binary data. */ if (ctx->options & LOG_OPT_ENCODE) { - if (ctx->typecast == SMP_T_STR || - ctx->typecast == SMP_T_SINT || - ctx->typecast == SMP_T_BOOL) { + if (ctx->typecast == SMP_TYPES) { + /* raw data, ignore LOG opts and enforce binary + * to dump smp as-is + */ + ctx->options = LOG_OPT_NONE; + type = SMP_T_BIN; + } + else if (ctx->typecast == SMP_T_STR || + ctx->typecast == SMP_T_SINT || + ctx->typecast == SMP_T_BOOL) { /* enforce type */ type = ctx->typecast; } diff -Nru haproxy-3.0.9/src/mux_fcgi.c haproxy-3.0.10/src/mux_fcgi.c --- haproxy-3.0.9/src/mux_fcgi.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/mux_fcgi.c 2025-04-22 14:53:02.000000000 +0300 @@ -438,41 +438,57 @@ /* Indicates whether or not the we may call the fcgi_recv() function to attempt * to receive data into the buffer and/or demux pending data. The condition is * a bit complex due to some API limits for now. The rules are the following : - * - if an error or a shutdown was detected on the connection and the buffer - * is empty, we must not attempt to receive + * - if an error or a shutdown was detected on the connection, + we must not attempt to receive + * - if we're subscribed for receving, no need to try again * - if the demux buf failed to be allocated, we must not try to receive and - * we know there is nothing pending - * - if no flag indicates a blocking condition, we may attempt to receive, - * regardless of whether the demux buffer is full or not, so that only - * de demux part decides whether or not to block. This is needed because - * the connection API indeed prevents us from re-enabling receipt that is - * already enabled in a polled state, so we must always immediately stop - * as soon as the demux can't proceed so as never to hit an end of read - * with data pending in the buffers. - * - otherwise must may not attempt + * we know there is nothing pending (we'll be woken up once allocated) + * - if the demux buf is full, we will not be able to receive. + * - otherwise we may attempt to receive */ static inline int fcgi_recv_allowed(const struct fcgi_conn *fconn) { - if (fconn->flags & (FCGI_CF_EOS|FCGI_CF_ERROR)) + if (fconn->flags & (FCGI_CF_EOS|FCGI_CF_ERROR) || fconn->state == FCGI_CS_CLOSED) return 0; - if (b_data(&fconn->dbuf) == 0 && fconn->state == FCGI_CS_CLOSED) + if ((fconn->wait_event.events & SUB_RETRY_RECV)) return 0; - if (!(fconn->flags & FCGI_CF_DEM_DALLOC) && - !(fconn->flags & FCGI_CF_DEM_BLOCK_ANY)) - return 1; + if (!(fconn->flags & (FCGI_CF_DEM_DALLOC | FCGI_CF_DEM_DFULL))) + return 1; return 0; } -/* Restarts reading on the connection if it was not enabled */ +/* Indicates whether it's worth waking up the I/O handler to restart demuxing. + * Its conditions are the following: + * - if the buffer is empty and the connection is closed, there's nothing + * to demux + * - if a short read was reported, no need to try demuxing again + * - if some blocking conditions remain, no need to try again + * - otherwise it's safe to try demuxing again + */ +static inline int fcgi_may_demux(const struct fcgi_conn *fconn) +{ + if (fconn->state == FCGI_CS_CLOSED && !b_data(&fconn->dbuf)) + return 0; + + if (fconn->flags & FCGI_CF_DEM_SHORT_READ) + return 0; + + if (fconn->flags & FCGI_CF_DEM_BLOCK_ANY) + return 0; + + return 1; +} + + +/* restarts reading/processing on the connection if we can receive or demux + * (both are called from the same tasklet). + */ static inline void fcgi_conn_restart_reading(const struct fcgi_conn *fconn, int consider_buffer) { - if (!fcgi_recv_allowed(fconn)) - return; - if ((!consider_buffer || !b_data(&fconn->dbuf)) && - (fconn->wait_event.events & SUB_RETRY_RECV)) + if (!fcgi_recv_allowed(fconn) && !fcgi_may_demux(fconn)) return; tasklet_wakeup(fconn->wait_event.tasklet); } @@ -783,15 +799,15 @@ } } -/* Detect a pending read0 for a FCGI connection. It happens if a read0 is - * pending on the connection AND if there is no more data in the demux - * buffer. The function returns 1 to report a read0 or 0 otherwise. +/* Detect a pending read0 for a FCGI connection. It happens if a read0 was + * already reported on a previous xprt->rcvbuf() AND a record parser failed + * to parse pending data, confirming no more progress is possible because + * we're facing a truncated frame. The function returns 1 to report a read0 + * or 0 otherwise. */ static int fcgi_conn_read0_pending(struct fcgi_conn *fconn) { - if ((fconn->flags & FCGI_CF_EOS) && !b_data(&fconn->dbuf)) - return 1; - return 0; + return !!(fconn->flags & FCGI_CF_END_REACHED); } @@ -1532,6 +1548,7 @@ /* process full record only */ if (b_data(dbuf) < (fconn->drl + fconn->drp)) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; TRACE_DEVEL("leaving on missing data", FCGI_EV_RX_RECORD|FCGI_EV_RX_GETVAL, fconn->conn); return 0; } @@ -2257,10 +2274,10 @@ if (fconn->state == FCGI_CS_RECORD_P) goto end_transfer; - if (b_data(dbuf) < (fconn->drl + fconn->drp) && - b_size(dbuf) > (fconn->drl + fconn->drp) && - buf_room_for_htx_data(dbuf)) + if (b_data(dbuf) < (fconn->drl + fconn->drp) && !b_full(dbuf)) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; goto fail; // incomplete record + } if (!fcgi_get_buf(fconn, &fstrm->rxbuf)) { fconn->flags |= FCGI_CF_DEM_SALLOC; @@ -2320,23 +2337,31 @@ TRACE_ENTER(FCGI_EV_RX_RECORD|FCGI_EV_RX_STDOUT, fconn->conn, fstrm); + if (b_data(&fconn->dbuf) < (fconn->drl + fconn->drp) && !b_full(&fconn->dbuf)) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; + goto fail; // incomplete record + } + fconn->state = FCGI_CS_RECORD_P; TRACE_STATE("switching to RECORD_P", FCGI_EV_RX_RECORD|FCGI_EV_RX_STDOUT, fconn->conn, fstrm); + fconn->drl += fconn->drp; fconn->drp = 0; ret = MIN(b_data(&fconn->dbuf), fconn->drl); b_del(&fconn->dbuf, ret); fconn->drl -= ret; - if (fconn->drl) { - TRACE_DEVEL("leaving on missing data or error", FCGI_EV_RX_RECORD|FCGI_EV_RX_STDOUT, fconn->conn, fstrm); - return 0; - } + if (fconn->drl) + goto fail; fconn->state = FCGI_CS_RECORD_H; fstrm->flags |= FCGI_SF_ES_RCVD; TRACE_PROTO("FCGI STDOUT record rcvd", FCGI_EV_RX_RECORD|FCGI_EV_RX_STDOUT, fconn->conn, fstrm, 0, (size_t[]){0}); TRACE_STATE("stdout data fully send, switching to RECORD_H", FCGI_EV_RX_RECORD|FCGI_EV_RX_FHDR|FCGI_EV_RX_EOI, fconn->conn, fstrm); TRACE_LEAVE(FCGI_EV_RX_RECORD|FCGI_EV_RX_STDOUT, fconn->conn, fstrm); return 1; + + fail: + TRACE_DEVEL("leaving on missing data or error", FCGI_EV_RX_RECORD|FCGI_EV_RX_STDOUT, fconn->conn, fstrm); + return 0; } /* Processes a STDERR record. Returns > 0 on success, 0 if it couldn't do @@ -2355,13 +2380,13 @@ if (fconn->state == FCGI_CS_RECORD_P || !fconn->drl) goto end_transfer; - if (b_data(dbuf) < (fconn->drl + fconn->drp) && - b_size(dbuf) > (fconn->drl + fconn->drp) && - buf_room_for_htx_data(dbuf)) + if (b_data(dbuf) < (fconn->drl + fconn->drp) && !b_full(dbuf)) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; goto fail; // incomplete record + } chunk_reset(&trash); - ret = b_force_xfer(&trash, dbuf, MIN(b_room(&trash), fconn->drl)); + ret = b_force_xfer(&trash, dbuf, MIN(b_room(&trash) - 2, fconn->drl)); if (!ret) goto fail; fconn->drl -= ret; @@ -2416,6 +2441,7 @@ /* process full record only */ if (b_data(dbuf) < (fconn->drl + fconn->drp)) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; TRACE_DEVEL("leaving on missing data", FCGI_EV_RX_RECORD|FCGI_EV_RX_ENDREQ, fconn->conn); return 0; } @@ -2477,6 +2503,7 @@ TRACE_STATE("receiving FCGI record header", FCGI_EV_RX_RECORD|FCGI_EV_RX_FHDR, fconn->conn); ret = fcgi_decode_record_hdr(&fconn->dbuf, 0, &hdr); if (!ret) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; TRACE_ERROR("header record decoding failure", FCGI_EV_RX_RECORD|FCGI_EV_RX_ENDREQ|FCGI_EV_FSTRM_ERR, fconn->conn, fstrm); goto fail; } @@ -2494,8 +2521,13 @@ /* process as many incoming records as possible below */ while (1) { + /* Make sure to clear DFULL if contents were deleted */ + if (!b_full(&fconn->dbuf)) + fconn->flags &= ~FCGI_CF_DEM_DFULL; + if (!b_data(&fconn->dbuf)) { TRACE_DEVEL("no more Rx data", FCGI_EV_RX_RECORD, fconn->conn); + fconn->flags |= FCGI_CF_DEM_SHORT_READ; break; } @@ -2507,8 +2539,10 @@ if (fconn->state == FCGI_CS_RECORD_H) { TRACE_PROTO("receiving FCGI record header", FCGI_EV_RX_RECORD|FCGI_EV_RX_FHDR, fconn->conn); ret = fcgi_decode_record_hdr(&fconn->dbuf, 0, &hdr); - if (!ret) + if (!ret) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; break; + } b_del(&fconn->dbuf, ret); new_record: @@ -2520,6 +2554,10 @@ TRACE_STATE("FCGI record header rcvd, switching to RECORD_D", FCGI_EV_RX_RECORD|FCGI_EV_RX_FHDR, fconn->conn); } + /* Make sure to clear DFULL if contents were deleted */ + if (!b_full(&fconn->dbuf)) + fconn->flags &= ~FCGI_CF_DEM_DFULL; + /* Only FCGI_CS_RECORD_D or FCGI_CS_RECORD_P */ tmp_fstrm = fcgi_conn_st_by_id(fconn, fconn->dsi); @@ -2579,6 +2617,12 @@ * larger than the buffer so we drain all of * their contents until we reach the end. */ + if (b_data(&fconn->dbuf) < (fconn->drl + fconn->drp) && !b_full(&fconn->dbuf)) { + fconn->flags |= FCGI_CF_DEM_SHORT_READ; + ret = 0; + break; + } + fconn->state = FCGI_CS_RECORD_P; fconn->drl += fconn->drp; fconn->drp = 0; @@ -2603,6 +2647,15 @@ } fail: + if (fconn->state == FCGI_CS_CLOSED || (fconn->flags & FCGI_CF_DEM_SHORT_READ)) { + if (fconn->flags & FCGI_CF_EOS) + fconn->flags |= FCGI_CF_END_REACHED; + } + + /* Make sure to clear DFULL if contents were deleted */ + if (!b_full(&fconn->dbuf)) + fconn->flags &= ~FCGI_CF_DEM_DFULL; + /* we can go here on missing data, blocked response or error */ if (fstrm && fcgi_strm_sc(fstrm) && (b_data(&fstrm->rxbuf) || @@ -2746,14 +2799,17 @@ TRACE_DATA("failed to receive data, subscribing", FCGI_EV_FCONN_RECV, conn); conn->xprt->subscribe(conn, conn->xprt_ctx, SUB_RETRY_RECV, &fconn->wait_event); } - else + else if (ret) { + fconn->flags &= ~FCGI_CF_DEM_SHORT_READ; TRACE_DATA("recv data", FCGI_EV_FCONN_RECV, conn, 0, 0, (size_t[]){ret}); + } if (conn_xprt_read0_pending(conn)) { TRACE_DATA("received read0", FCGI_EV_FCONN_RECV, conn); fconn->flags |= FCGI_CF_EOS; } - if (conn->flags & CO_FL_ERROR) { + if (conn->flags & CO_FL_ERROR && + (!b_data(&fconn->dbuf) || (fconn->flags & FCGI_CF_DEM_SHORT_READ))) { TRACE_DATA("connection error", FCGI_EV_FCONN_RECV, conn); fconn->flags |= FCGI_CF_ERROR; } @@ -3000,14 +3056,30 @@ TRACE_POINT(FCGI_EV_FCONN_WAKE, conn); - if (b_data(&fconn->dbuf) && !(fconn->flags & FCGI_CF_DEM_BLOCK_ANY)) { - fcgi_process_demux(fconn); + if (!(fconn->flags & FCGI_CF_DEM_BLOCK_ANY) && + (b_data(&fconn->dbuf) || (fconn->flags & FCGI_CF_EOS))) { + do { + fcgi_process_demux(fconn); + + /* hint: if we ended up aligned on a record, we've very + * likely reached the end, no point trying again. + */ + if (fconn->state == FCGI_CS_RECORD_H) + break; + + if (!fcgi_recv_allowed(fconn)) + break; + + /* OK, it's worth trying to grab a few more records */ + fcgi_recv(fconn); + + } while ((b_data(&fconn->dbuf) && fcgi_may_demux(fconn)) || (fconn->flags & FCGI_CF_EOS)); + + /* now's time to wake the task up */ + fcgi_conn_restart_reading(fconn, 0); if (fconn->state == FCGI_CS_CLOSED || (fconn->flags & FCGI_CF_ERROR)) b_reset(&fconn->dbuf); - - if (buf_room_for_htx_data(&fconn->dbuf)) - fconn->flags &= ~FCGI_CF_DEM_DFULL; } fcgi_send(fconn); diff -Nru haproxy-3.0.9/src/mux_h2.c haproxy-3.0.10/src/mux_h2.c --- haproxy-3.0.9/src/mux_h2.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/mux_h2.c 2025-04-22 14:53:02.000000000 +0300 @@ -732,6 +732,16 @@ } else { h2c->task->expire = TICK_ETERNITY; } + + /* Timer may already be expired, in this case it must not be requeued. + * Currently it may happen with idle conn calculation based on shut or + * http-req/ka timeouts. + */ + if (unlikely(tick_is_expired(h2c->task->expire, now_ms))) { + task_wakeup(h2c->task, TASK_WOKEN_TIMER); + goto leave; + } + task_queue(h2c->task); leave: TRACE_LEAVE(H2_EV_H2C_WAKE); @@ -3513,6 +3523,8 @@ struct proxy *prx = l->bind_conf->frontend; h2c->flags &= ~H2_CF_IS_BACK; + /* Must manually init max_id so that GOAWAY can be emitted. */ + h2c->max_id = 0; h2c->shut_timeout = h2c->timeout = prx->timeout.client; if (tick_isset(prx->timeout.clientfin)) diff -Nru haproxy-3.0.9/src/mux_quic.c haproxy-3.0.10/src/mux_quic.c --- haproxy-3.0.9/src/mux_quic.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/mux_quic.c 2025-04-22 14:53:02.000000000 +0300 @@ -1148,6 +1148,25 @@ } } +/* Register <qcs> stream for emission of STREAM, STOP_SENDING or RESET_STREAM. + * Set <urg> to true if stream should be emitted in priority. This is useful + * when sending STOP_SENDING or RESET_STREAM, or for emission on an application + * control stream. + */ +static void _qcc_send_stream(struct qcs *qcs, int urg) +{ + struct qcc *qcc = qcs->qcc; + + if (urg) { + LIST_DEL_INIT(&qcs->el_send); + LIST_INSERT(&qcc->send_list, &qcs->el_send); + } + else { + if (!LIST_INLIST(&qcs->el_send)) + LIST_APPEND(&qcs->qcc->send_list, &qcs->el_send); + } +} + /* Prepare for the emission of RESET_STREAM on <qcs> with error code <err>. */ void qcc_reset_stream(struct qcs *qcs, int err) { @@ -1186,14 +1205,13 @@ qcs_alert(qcs); } - qcc_send_stream(qcs, 1, 0); + _qcc_send_stream(qcs, 1); tasklet_wakeup(qcc->wait_event.tasklet); } -/* Register <qcs> stream for emission of STREAM, STOP_SENDING or RESET_STREAM. - * Set <urg> to 1 if stream content should be treated in priority compared to - * other streams. For STREAM emission, <count> must contains the size of the - * frame payload. This is used for flow control accounting. +/* Register <qcs> stream for STREAM emission. Set <urg> to 1 if stream content + * should be treated in priority compared to other streams. <count> must + * contains the size of the frame payload, used for flow control accounting. */ void qcc_send_stream(struct qcs *qcs, int urg, int count) { @@ -1201,17 +1219,10 @@ TRACE_ENTER(QMUX_EV_QCS_SEND, qcc->conn, qcs); - /* Cannot send if already closed. */ + /* Cannot send STREAM if already closed. */ BUG_ON(qcs_is_close_local(qcs)); - if (urg) { - LIST_DEL_INIT(&qcs->el_send); - LIST_INSERT(&qcc->send_list, &qcs->el_send); - } - else { - if (!LIST_INLIST(&qcs->el_send)) - LIST_APPEND(&qcs->qcc->send_list, &qcs->el_send); - } + _qcc_send_stream(qcs, urg); if (count) { qfctl_sinc(&qcc->tx.fc, count); @@ -1234,7 +1245,7 @@ TRACE_STATE("abort stream read", QMUX_EV_QCS_END, qcc->conn, qcs); qcs->flags |= (QC_SF_TO_STOP_SENDING|QC_SF_READ_ABORTED); - qcc_send_stream(qcs, 1, 0); + _qcc_send_stream(qcs, 1); tasklet_wakeup(qcc->wait_event.tasklet); end: diff -Nru haproxy-3.0.9/src/peers.c haproxy-3.0.10/src/peers.c --- haproxy-3.0.9/src/peers.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/peers.c 2025-04-22 14:53:02.000000000 +0300 @@ -1740,6 +1740,11 @@ memcpy(&expire, *msg_cur, expire_sz); *msg_cur += expire_sz; expire = ntohl(expire); + /* Protocol contains expire in MS, check if value is less than table config */ + if (expire > table->expire) + expire = table->expire; + /* the rest of the code considers expire as ticks and not MS */ + expire = MS_TO_TICKS(expire); } newts = stksess_new(table, NULL); diff -Nru haproxy-3.0.9/src/proto_rhttp.c haproxy-3.0.10/src/proto_rhttp.c --- haproxy-3.0.9/src/proto_rhttp.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/proto_rhttp.c 2025-04-22 14:53:02.000000000 +0300 @@ -287,7 +287,7 @@ l->rx.rhttp.pend_conn = conn; /* On success task will be woken up by H2 mux after reversal. */ - l->rx.rhttp.task->expire = conn ? + l->rx.rhttp.task->expire = conn && tick_isset(srv->proxy->timeout.connect) ? tick_add_ifset(now_ms, srv->proxy->timeout.connect) : MS_TO_TICKS(now_ms + 1000); } diff -Nru haproxy-3.0.9/src/sample.c haproxy-3.0.10/src/sample.c --- haproxy-3.0.9/src/sample.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/sample.c 2025-04-22 14:53:02.000000000 +0300 @@ -3163,7 +3163,7 @@ output->data = exp_replace(output->area, output->size, start, arg_p[1].data.str.area, pmatch); /* replace the matching part */ - max = output->size - output->data; + max = trash->size - trash->data; if (max) { if (max > output->data) max = output->data; diff -Nru haproxy-3.0.9/src/sink.c haproxy-3.0.10/src/sink.c --- haproxy-3.0.9/src/sink.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/sink.c 2025-04-22 14:53:02.000000000 +0300 @@ -573,7 +573,7 @@ if (appctx_init(appctx) == -1) goto out_free_appctx; - sft->last_conn = TICK_ETERNITY; + sft->last_conn = now_ms; return appctx; /* Error unrolling */ diff -Nru haproxy-3.0.9/src/stick_table.c haproxy-3.0.10/src/stick_table.c --- haproxy-3.0.9/src/stick_table.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/stick_table.c 2025-04-22 14:53:02.000000000 +0300 @@ -503,6 +503,9 @@ /* * Looks in table <t> for a sticky session with same key as <ts>. * Returns pointer on requested sticky session or NULL if none was found. + * + * <ts> must originate from a table with same key type and length than <t>, + * else it is undefined behavior. */ struct stksess *__stktable_lookup(struct stktable *t, struct stksess *ts, uint shard) { @@ -524,6 +527,9 @@ * Returns pointer on requested sticky session or NULL if none was found. * The refcount of the found entry is increased and this function * is protected using the table lock + * + * <ts> must originate from a table with same key type and length than <t>, + * else it is undefined behavior. */ struct stksess *stktable_lookup(struct stktable *t, struct stksess *ts) { @@ -3646,7 +3652,12 @@ return NULL; if (unlikely(args[arg].type == ARGT_TAB)) { - /* an alternate table was specified, let's look up the same key there */ + /* an alternate table was specified, let's look up the same key there + * unless the table key type or length differs from the tracked one + */ + if (args[arg].data.t->type != stkptr->table->type || + args[arg].data.t->key_size != stkptr->table->key_size) + return NULL; stkctr->table = args[arg].data.t; stkctr_set_entry(stkctr, stktable_lookup(stkctr->table, stksess)); return stkctr; diff -Nru haproxy-3.0.9/src/stream.c haproxy-3.0.10/src/stream.c --- haproxy-3.0.9/src/stream.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/stream.c 2025-04-22 14:53:02.000000000 +0300 @@ -1839,6 +1839,7 @@ sc_shutdown(scf); //sc_report_error(scf); TODO: Be sure it is useless if (!(req->analysers) && !(res->analysers)) { + COUNT_IF(1, "Report a client abort (no analysers)"); _HA_ATOMIC_INC(&s->be->be_counters.cli_aborts); _HA_ATOMIC_INC(&sess->fe->fe_counters.cli_aborts); if (sess->listener && sess->listener->counters) @@ -1862,6 +1863,7 @@ if (srv) _HA_ATOMIC_INC(&srv->counters.failed_resp); if (!(req->analysers) && !(res->analysers)) { + COUNT_IF(1, "Report a client abort (no analysers)"); _HA_ATOMIC_INC(&s->be->be_counters.srv_aborts); _HA_ATOMIC_INC(&sess->fe->fe_counters.srv_aborts); if (sess->listener && sess->listener->counters) @@ -2167,6 +2169,7 @@ if (srv) _HA_ATOMIC_INC(&srv->counters.cli_aborts); s->flags |= SF_ERR_CLICL; + COUNT_IF(1, "Report unhandled client error"); } else if (req->flags & CF_READ_TIMEOUT) { _HA_ATOMIC_INC(&s->be->be_counters.cli_aborts); @@ -2176,6 +2179,7 @@ if (srv) _HA_ATOMIC_INC(&srv->counters.cli_aborts); s->flags |= SF_ERR_CLITO; + COUNT_IF(1, "Report unhandled client timeout (RD)"); } else { _HA_ATOMIC_INC(&s->be->be_counters.srv_aborts); @@ -2185,6 +2189,7 @@ if (srv) _HA_ATOMIC_INC(&srv->counters.srv_aborts); s->flags |= SF_ERR_SRVTO; + COUNT_IF(1, "Report unhandled server timeout (WR)"); } sess_set_term_flags(s); @@ -2213,6 +2218,7 @@ if (srv) _HA_ATOMIC_INC(&srv->counters.srv_aborts); s->flags |= SF_ERR_SRVCL; + COUNT_IF(1, "Report unhandled server error"); } else if (res->flags & CF_READ_TIMEOUT) { _HA_ATOMIC_INC(&s->be->be_counters.srv_aborts); @@ -2222,6 +2228,7 @@ if (srv) _HA_ATOMIC_INC(&srv->counters.srv_aborts); s->flags |= SF_ERR_SRVTO; + COUNT_IF(1, "Report unhandled server timeout (RD)"); } else { _HA_ATOMIC_INC(&s->be->be_counters.cli_aborts); @@ -2231,6 +2238,7 @@ if (srv) _HA_ATOMIC_INC(&srv->counters.cli_aborts); s->flags |= SF_ERR_CLITO; + COUNT_IF(1, "Report unhandled client timeout (WR)"); } sess_set_term_flags(s); } @@ -2788,11 +2796,28 @@ if (stream->scb->flags & (SC_FL_SHUT_DONE|SC_FL_SHUT_WANTED)) return; - sc_schedule_shutdown(stream->scb); - sc_schedule_abort(stream->scb); stream->task->nice = 1024; if (!(stream->flags & SF_ERR_MASK)) stream->flags |= why; + + if (objt_server(stream->target)) { + if (stream->flags & SF_CURR_SESS) { + stream->flags &= ~SF_CURR_SESS; + _HA_ATOMIC_DEC(&__objt_server(stream->target)->cur_sess); + } + + sess_change_server(stream, NULL); + if (may_dequeue_tasks(objt_server(stream->target), stream->be)) + process_srv_queue(objt_server(stream->target)); + } + + /* shutw is enough to stop a connecting socket */ + stream->scb->flags |= SC_FL_ERROR | SC_FL_NOLINGER; + sc_shutdown(stream->scb); + + stream->scb->state = SC_ST_CLO; + if (stream->srv_error) + stream->srv_error(stream, stream->scb); } /* dumps an error message for type <type> at ptr <ptr> related to stream <s>, diff -Nru haproxy-3.0.9/src/tools.c haproxy-3.0.10/src/tools.c --- haproxy-3.0.9/src/tools.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/src/tools.c 2025-04-22 14:53:02.000000000 +0300 @@ -5503,11 +5503,12 @@ goto use_array; i = dladdr_and_size(addr, &dli, &size); - if (!isolated) - HA_SPIN_UNLOCK(OTHER_LOCK, &dladdr_lock); - if (!i) + if (!i) { + if (!isolated) + HA_SPIN_UNLOCK(OTHER_LOCK, &dladdr_lock); goto use_array; + } /* 1. prefix the library name if it's not the same object as the one * that contains the main function. The name is picked between last '/' @@ -5529,6 +5530,9 @@ ha_thread_relax(); } + if (!isolated) + HA_SPIN_UNLOCK(OTHER_LOCK, &dladdr_lock); + if (dli_main.dli_fbase != dli.dli_fbase) { fname = dli.dli_fname; p = strrchr(fname, '/'); diff -Nru haproxy-3.0.9/SUBVERS haproxy-3.0.10/SUBVERS --- haproxy-3.0.9/SUBVERS 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/SUBVERS 2025-04-22 14:53:02.000000000 +0300 @@ -1,2 +1,2 @@ --7f0031e +-346eb4f diff -Nru haproxy-3.0.9/tests/exp/filltab25.c haproxy-3.0.10/tests/exp/filltab25.c --- haproxy-3.0.9/tests/exp/filltab25.c 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/tests/exp/filltab25.c 2025-04-22 14:53:02.000000000 +0300 @@ -182,7 +182,7 @@ p = sw; } -main(int argc, char **argv) { +int main(int argc, char **argv) { int conns; int i; diff -Nru haproxy-3.0.9/VERDATE haproxy-3.0.10/VERDATE --- haproxy-3.0.9/VERDATE 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/VERDATE 2025-04-22 14:53:02.000000000 +0300 @@ -1,2 +1,2 @@ -2025-03-20 14:27:37 +0100 -2025/03/20 +2025-04-22 13:53:02 +0200 +2025/04/22 diff -Nru haproxy-3.0.9/VERSION haproxy-3.0.10/VERSION --- haproxy-3.0.9/VERSION 2025-03-20 15:27:37.000000000 +0200 +++ haproxy-3.0.10/VERSION 2025-04-22 14:53:02.000000000 +0300 @@ -1 +1 @@ -3.0.9 +3.0.10