From: Olivier Houchard Date: Wed, 4 Nov 2026 12:42:28 +0000 (+0100) Subject: BUG/MEDIUM: ssl: disable kTLS on OpenSSL versions affected by a known bug X-Git-Tag: v3.5-dev11~43 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=e7c2ba5e1fcf8a6bb5a2a8f2848c9bfa1224b1e4;p=haproxy.git BUG/MEDIUM: ssl: disable kTLS on OpenSSL versions affected by a known bug With vanilla OpenSSL, a blocked kTLS record keeps referencing the caller's buffer and the library retries from it, even though SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is set; the H2 fast-forward reuses that buffer, so streams get corrupted. Upstream fixed this in 3.4.6 and 3.5.7, so disable kTLS on older versions and report it, with "tune.ssl.ktls-unsafe" to opt in anyway. To be backported to 3.3 and 3.4. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 2698b1462..b35ae413f 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -5962,6 +5962,13 @@ tune.ssl.keyupdate-rate-limit KeyUpdate is cpu-expensive, and there is little reason to receive a lot of them. Using a value of "0" disables the rate limiting. The default value is 100. +tune.ssl.ktls-unsafe + Use kTLS even on OpenSSL versions affected by a bug that corrupts streams + when a pending record's buffer is reused (fixed in 3.4.6 and 3.5.7). On + those versions kTLS is refused, and a notice reports it when "ktls on" is + parsed, unless this option is set. It has no effect on fixed versions nor + on other SSL libraries. Disabled by default. + tune.ssl.lifetime Sets how long a cached SSL session may remain valid. This time is expressed in seconds and defaults to 300 (5 min). It is important to understand that it diff --git a/include/haproxy/openssl-compat.h b/include/haproxy/openssl-compat.h index 5064a8393..065600734 100644 --- a/include/haproxy/openssl-compat.h +++ b/include/haproxy/openssl-compat.h @@ -624,6 +624,17 @@ static inline unsigned long ERR_peek_error_func(const char **func) #define HA_USE_KTLS #endif /* OPENSSL_IS_BORINGSSL || OPENSSL_IS_AWSLC */ +/* OpenSSL 3.4.6 and 3.5.7 fixed a kTLS bug where a blocked send kept + * referencing the caller's buffer; earlier versions corrupt streams. + */ +#if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) +#define HAVE_KTLS_TX_RETRY_FIX +#elif defined(HAVE_VANILLA_OPENSSL) && \ + (OPENSSL_VERSION_NUMBER >= 0x30500070L || \ + (OPENSSL_VERSION_NUMBER >= 0x30400060L && OPENSSL_VERSION_NUMBER < 0x30500000L)) +#define HAVE_KTLS_TX_RETRY_FIX +#endif + #endif /* USE_KTLS */ #endif /* USE_OPENSSL */ diff --git a/include/haproxy/ssl_sock-t.h b/include/haproxy/ssl_sock-t.h index 512f8831d..4e29cebb5 100644 --- a/include/haproxy/ssl_sock-t.h +++ b/include/haproxy/ssl_sock-t.h @@ -292,6 +292,7 @@ struct global_ssl { int skip_self_issued_ca; int async; /* whether we use ssl async mode */ + int ktls_unsafe; /* allow kTLS despite the library kTLS bug */ char *listen_default_ciphers; char *connect_default_ciphers; diff --git a/include/haproxy/ssl_sock.h b/include/haproxy/ssl_sock.h index b8dbc9025..a2dc8ab37 100644 --- a/include/haproxy/ssl_sock.h +++ b/include/haproxy/ssl_sock.h @@ -40,6 +40,8 @@ extern struct eb_root crtlists_tree; extern struct eb_root cafile_tree; extern int sctl_ex_index; extern struct global_ssl global_ssl; + +int ssl_sock_ktls_allowed(void); extern struct ssl_crtlist_kw ssl_crtlist_kws[]; extern struct methodVersions methodVersions[]; __decl_thread(extern HA_SPINLOCK_T ckch_lock); diff --git a/src/cfgparse-ssl.c b/src/cfgparse-ssl.c index d35e4ac52..1b9862e1b 100644 --- a/src/cfgparse-ssl.c +++ b/src/cfgparse-ssl.c @@ -547,6 +547,17 @@ static int ssl_parse_global_keyupdate_ratelimit(char **args, int section_type, s return 0; } +static int ssl_parse_global_ktls_unsafe(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + if (too_many_args(0, args, err, NULL)) + return -1; + + global_ssl.ktls_unsafe = 1; + return 0; +} + /* Allow to explicitly disable certificate compression when set to "off" */ #ifdef SSL_OP_NO_RX_CERTIFICATE_COMPRESSION static int ssl_parse_certificate_compression(char **args, int section_type, struct proxy *curpx, @@ -1030,7 +1041,7 @@ static int ssl_bind_parse_ktls(char **args, int cur_arg, struct proxy *px, struc return ERR_ALERT | ERR_FATAL; } if (strcasecmp(args[cur_arg + 1], "on") == 0) { - conf->ktls = 1; + conf->ktls = ssl_sock_ktls_allowed(); } else if (strcasecmp(args[cur_arg + 1], "off") == 0) { conf->ktls = 0; } else { @@ -2116,7 +2127,8 @@ static int srv_parse_ktls(char **args, int *cur_arg, struct proxy *px, struct se } if (strcasecmp(args[*cur_arg + 1], "on") == 0) { - newsrv->ssl_ctx.options |= SRV_SSL_O_KTLS; + if (ssl_sock_ktls_allowed()) + newsrv->ssl_ctx.options |= SRV_SSL_O_KTLS; } else if (strcasecmp(args[*cur_arg + 1], "off") == 0) { newsrv->ssl_ctx.options &= ~SRV_SSL_O_KTLS; } else { @@ -2894,6 +2906,7 @@ static struct cfg_kw_list cfg_kws = {ILH, { { CFG_GLOBAL, "tune.ssl.ssl-ctx-cache-size", ssl_parse_global_int }, { CFG_GLOBAL, "tune.ssl.capture-cipherlist-size", ssl_parse_global_capture_buffer }, { CFG_GLOBAL, "tune.ssl.capture-buffer-size", ssl_parse_global_capture_buffer }, + { CFG_GLOBAL, "tune.ssl.ktls-unsafe", ssl_parse_global_ktls_unsafe }, { CFG_GLOBAL, "tune.ssl.keylog", ssl_parse_global_keylog }, { CFG_GLOBAL, "tune.ssl.keyupdate-rate-limit", ssl_parse_global_keyupdate_ratelimit }, { CFG_GLOBAL, "ssl-default-bind-ciphers", ssl_parse_global_ciphers }, diff --git a/src/ssl_sock.c b/src/ssl_sock.c index ff546670b..b9fcf36c8 100644 --- a/src/ssl_sock.c +++ b/src/ssl_sock.c @@ -6348,6 +6348,29 @@ void ssl_sock_srv_try_reuse_sess(struct ssl_sock_ctx *ctx, struct server *srv) * handshake flag on the connection. It is safe to call it multiple times. * It returns 0 on success and -1 in error case. */ +/* kTLS silently corrupts streams on libraries that keep referencing the caller's + * buffer after a blocked send (fixed in 3.4.6 and 3.5.7), so it is refused by + * default and the user has to opt in. + */ +int ssl_sock_ktls_allowed(void) +{ +#ifdef HAVE_KTLS_TX_RETRY_FIX + return 1; +#else + static char warned; + + if (global_ssl.ktls_unsafe) + return 1; + if (!warned) { + warned = 1; + ha_notice("kTLS disabled: this OpenSSL version is affected by a known " + "kTLS bug; upgrade to at least 3.4.6 or 3.5.7, or set " + "'tune.ssl.ktls-unsafe' to use it anyway.\n"); + } + return 0; +#endif +} + static int ssl_sock_init(struct connection *conn, void **xprt_ctx) { struct ssl_sock_ctx *ctx; @@ -6427,7 +6450,8 @@ static int ssl_sock_init(struct connection *conn, void **xprt_ctx) ssl_sock_srv_try_reuse_sess(ctx, srv); #ifdef HA_USE_KTLS - if ((srv->ssl_ctx.options & SRV_SSL_O_KTLS) && !(global.tune.options & GTUNE_NO_KTLS)) { + if ((srv->ssl_ctx.options & SRV_SSL_O_KTLS) && + !(global.tune.options & GTUNE_NO_KTLS) && ssl_sock_ktls_allowed()) { #ifdef HAVE_VANILLA_OPENSSL SSL_set_options(ctx->ssl, SSL_OP_ENABLE_KTLS); #endif @@ -6474,7 +6498,8 @@ static int ssl_sock_init(struct connection *conn, void **xprt_ctx) #endif #ifdef HA_USE_KTLS - if (bc->ssl_conf.ktls && !(global.tune.options & GTUNE_NO_KTLS)) { + if (bc->ssl_conf.ktls && + !(global.tune.options & GTUNE_NO_KTLS) && ssl_sock_ktls_allowed()) { #ifdef HAVE_VANILLA_OPENSSL SSL_set_options(ctx->ssl, SSL_OP_ENABLE_KTLS); #endif