Hi Karol,

On Thu, Jul 23, 2026 at 06:07:39PM +0200, Karol Kucharski wrote:
> Hi,
> 
> I tested a simple configuration where HAProxy connects to a backend 
> server
> using TLS 1.3 with ktls and splice options enabled.
> 
> haproxy.cfg
> ```
> global
>      expose-experimental-directives
>      nbthread 1
> 
> defaults
>      mode tcp
>      timeout connect 1s
>      timeout client 1s
>      timeout server 1s
>      option splice-request
>      option splice-response
> 
> frontend tcp_in
>      bind *:8080
>      default_backend tls_out
> 
> backend tls_out
>      server server 127.0.0.1:4043 ssl verify none ktls on force-tlsv13
> ```
> 
> After the initial handshake phase, the server sends two 
> new_session_ticket
> handshake messages.
> HAProxy tries to receive those messages with the splice syscall which 
> results
> in the EINVAL error.
> There is a comment in the HAProxy source (in the raw_sock_to_pipe 
> function)
> regarding such situation:
> 
> ```
> * EINVAL, however, is a bit special.
> * If we're trying to splice from a KTLS
> * socket, the kernel may return EINVAL
> * to signal that the current TLS record
> * is not application data, and that we
> * have to call recvmsg() to get it.
> * This is not really an error, and doesn't
> * mean we won't be able to splice later.
> * Choosing EINVAL there is a bit unfortunate,
> * because it can mean many things, but we
> * should not get it for any other reason.
> */
> ```
> 
> Using strace I've seen only repeated splice calls returning EINVAL, 
> without
> any recvmsg call to handle the failed splice call.
> 
> ```
> epoll_wait(4, [], 200, 0)               = 0
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=429214144}) = 
> 0
> splice(9, NULL, 11, NULL, 1073741824, SPLICE_F_MOVE|SPLICE_F_NONBLOCK) = 
> -1 EINVAL (Invalid argument)
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=429392356}) = 
> 0
> epoll_wait(4, [], 200, 0)               = 0
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=429572280}) = 
> 0
> splice(9, NULL, 11, NULL, 1073741824, SPLICE_F_MOVE|SPLICE_F_NONBLOCK) = 
> -1 EINVAL (Invalid argument)
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=429753718}) = 
> 0
> epoll_wait(4, [], 200, 0)               = 0
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=429931308}) = 
> 0
> splice(9, NULL, 11, NULL, 1073741824, SPLICE_F_MOVE|SPLICE_F_NONBLOCK) = 
> -1 EINVAL (Invalid argument)
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=430114691}) = 
> 0
> epoll_wait(4, [], 200, 0)               = 0
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=430520235}) = 
> 0
> splice(9, NULL, 11, NULL, 1073741824, SPLICE_F_MOVE|SPLICE_F_NONBLOCK) = 
> -1 EINVAL (Invalid argument)
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=430703514}) = 
> 0
> epoll_wait(4, [], 200, 0)               = 0
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=430879870}) = 
> 0
> splice(9, NULL, 11, NULL, 1073741824, SPLICE_F_MOVE|SPLICE_F_NONBLOCK) = 
> -1 EINVAL (Invalid argument)
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=431062900}) = 
> 0
> epoll_wait(4, [], 200, 0)               = 0
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=431239849}) = 
> 0
> splice(9, NULL, 11, NULL, 1073741824, SPLICE_F_MOVE|SPLICE_F_NONBLOCK) = 
> -1 EINVAL (Invalid argument)
> clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0, tv_nsec=431422559}) = 
> 0
> epoll_wait(4, [], 200, 0)               = 0
> ```
> 
> I've tested versions 3.4.2 and git master, both with OpenSSL 3.5.6 and 
> AWS-LC 5.1.0.
> 
> I've added a simple hack in the raw_sock_to_pipe function:
> ```
> diff --git a/src/raw_sock.c b/src/raw_sock.c
> index 33fb8f5c3..1552e44bc 100644
> --- a/src/raw_sock.c
> +++ b/src/raw_sock.c
> @@ -137,6 +137,21 @@ int raw_sock_to_pipe(struct connection *conn, void 
> *xprt_ctx, struct pipe *pipe,
>                                   */
>                                  if (errno != EINVAL)
>                                          retval = -1;
> +                               else
> +                               {
> +                                       char dbuff[1024];
> +                                       char cbuff[64];
> +                                       struct msghdr msg;
> +                                       struct iovec iov;
> +                                       memset(&msg, 0, sizeof(msg));
> +                                       iov.iov_base = dbuff;
> +                                       iov.iov_len = sizeof(dbuff);
> +                                       msg.msg_control = cbuff;
> +                                       msg.msg_controllen = 
> sizeof(cbuff);
> +                                       msg.msg_iov = &iov;
> +                                       msg.msg_iovlen = 1;
> +                                       (void)recvmsg(conn->handle.fd, 
> &msg, 0);
> +                               }
>                                  goto leave;
>                          }
>                          else if (errno == EINTR) {
> ```
> And it seems to work (HAProxy receives the data from the server and 
> forwards
> it to the client).
> Of course it's not a proper solution but unfortunately I'm not familiar 
> enough
> with the HAProxy architecture to make a proper fix.

Thanks a lot for reporting!
There is code in ssl_sock_to_buf() to take care of exactly that problem,
but obviously you managed, with a bit of a different timing, to get that
case when haproxy switched to using splicing already, so it will do no
good here. I'm actually working in that area, and I have a fix that
should take care of that for OpenSSL already. Now I'm gonna fix that for
AWS-LC too, and I'll let you know when I think it should now work.

Thanks again!

Olivier



Reply via email to