public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Jan Macku <jamacku@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/curl] f44: Resolves: CVE-2026-11586 - WS Auto-PONG memory exhaustion
Date: Mon, 24 Aug 2026 14:03:14 GMT	[thread overview]
Message-ID: <178758019422.1.3662016725428605620.rpms-curl-85f22282048d@fedoraproject.org> (raw)

A new commit has been pushed.

Repo   : rpms/curl
Branch : f44
Commit : 85f22282048dd65b30f6b89fa5ff38113374163c
Author : Jan Macku <jamacku@redhat.com>
Date   : 2026-08-24T12:52:50+02:00
Stats  : +208/-0 in 2 file(s)
URL    : https://src.fedoraproject.org/rpms/curl/c/85f22282048dd65b30f6b89fa5ff38113374163c?branch=f44

Log:
Resolves: CVE-2026-11586 - WS Auto-PONG memory exhaustion

---
diff --git a/0014-curl-8.18.0-CVE-2026-11586.patch b/0014-curl-8.18.0-CVE-2026-11586.patch
new file mode 100644
index 0000000..ce175ae
--- /dev/null
+++ b/0014-curl-8.18.0-CVE-2026-11586.patch
@@ -0,0 +1,204 @@
+From 2c1a702de4d44681bab1db6691520442aec08804 Mon Sep 17 00:00:00 2001
+From: Stefan Eissing <stefan@eissing.org>
+Date: Mon, 8 Jun 2026 16:57:01 +0200
+Subject: [PATCH] ws: make pong sending lazy
+
+Do not send PONG frames unless there is sufficient space left in the
+websocket send buffer. A server might be lazy in reading our data and
+intermediary PONG frames can be skipped by a client (RFC 6455, ch.
+5.5.3).
+
+Add test case measuring no real RSS increase on a server blasting with
+PING frames.
+
+Closes #21911
+
+(cherry picked from commit 849317ff5c5a5e13f50ec3d001e46ddffa77d8a4)
+---
+ lib/ws.c                         | 33 +++++++++-----
+ tests/http/test_20_websockets.py | 78 ++++++++++++++++++++++++++++++++
+ 2 files changed, 99 insertions(+), 12 deletions(-)
+
+diff --git a/lib/ws.c b/lib/ws.c
+index 37cacae966..fcacef2079 100644
+--- a/lib/ws.c
++++ b/lib/ws.c
+@@ -624,6 +624,7 @@ static CURLcode ws_enc_add_cntrl(struct Curl_easy *data,
+                                  size_t plen,
+                                  unsigned int frame_type)
+ {
++  (void)data;
+   DEBUGASSERT(plen <= WS_MAX_CNTRL_LEN);
+   if(plen > WS_MAX_CNTRL_LEN)
+     return CURLE_BAD_FUNCTION_ARGUMENT;
+@@ -633,13 +634,6 @@ static CURLcode ws_enc_add_cntrl(struct Curl_easy *data,
+   ws->pending.type = frame_type;
+   ws->pending.payload_len = plen;
+   memcpy(ws->pending.payload, payload, plen);
+-
+-  if(!ws->enc.payload_remain) { /* not in the middle of another frame */
+-    CURLcode result = ws_enc_add_pending(data, ws);
+-    if(!result)
+-      (void)ws_flush(data, ws, Curl_is_in_callback(data));
+-    return result;
+-  }
+   return CURLE_OK;
+ }
+ 
+@@ -708,7 +702,7 @@ static CURLcode ws_cw_write(struct Curl_easy *data,
+ {
+   struct ws_cw_ctx *ctx = writer->ctx;
+   struct websocket *ws;
+-  CURLcode result;
++  CURLcode result = CURLE_OK;
+ 
+   CURL_TRC_WRITE(data, "ws_cw_write(len=%zu, type=%d)", nbytes, type);
+   if(!(type & CLIENTWRITE_BODY) || data->set.ws_raw_mode)
+@@ -741,7 +735,8 @@ static CURLcode ws_cw_write(struct Curl_easy *data,
+     if(result == CURLE_AGAIN) {
+       /* insufficient amount of data, keep it for later.
+        * we pretend to have written all since we have a copy */
+-      return CURLE_OK;
++      result = CURLE_OK;
++      goto out;
+     }
+     else if(result) {
+       failf(data, "[WS] decode payload error %d", (int)result);
+@@ -752,10 +747,16 @@ static CURLcode ws_cw_write(struct Curl_easy *data,
+   if((type & CLIENTWRITE_EOS) && !Curl_bufq_is_empty(&ctx->buf)) {
+     failf(data, "[WS] decode ending with %zd frame bytes remaining",
+           Curl_bufq_len(&ctx->buf));
+-    return CURLE_RECV_ERROR;
++    result = CURLE_RECV_ERROR;
+   }
+ 
+-  return CURLE_OK;
++out:
++  if(!result) {
++    result = ws_flush(data, ws, Curl_is_in_callback(data));
++    if(result == CURLE_AGAIN)
++      result = CURLE_OK;
++  }
++  return result;
+ }
+ 
+ /* WebSocket payload decoding client writer. */
+@@ -1616,8 +1617,16 @@ CURLcode curl_ws_recv(CURL *d, void *buffer,
+ static CURLcode ws_flush(struct Curl_easy *data, struct websocket *ws,
+                          bool blocking)
+ {
++  CURLcode result;
++
++  /* If there is space, add any pending control frame */
++  if(Curl_bufq_len(&ws->sendbuf) < ws->sendbuf.chunk_size) {
++    result = ws_enc_add_pending(data, ws);
++    if(result && (result != CURLE_AGAIN))
++      return result;
++  }
++
+   if(!Curl_bufq_is_empty(&ws->sendbuf)) {
+-    CURLcode result;
+     const uint8_t *out;
+     size_t outlen, n;
+ #ifdef DEBUGBUILD
+diff --git a/tests/http/test_20_websockets.py b/tests/http/test_20_websockets.py
+index 2612b0afe1..b935ed9d67 100644
+--- a/tests/http/test_20_websockets.py
++++ b/tests/http/test_20_websockets.py
+@@ -24,11 +24,15 @@
+ #
+ ###########################################################################
+ #
++import base64
++import hashlib
+ import logging
+ import os
++import re
+ import shutil
+ import socket
+ import subprocess
++import threading
+ import time
+ from datetime import datetime, timedelta
+ from typing import Dict
+@@ -207,3 +211,77 @@ class TestWebsockets:
+         large = 0
+         r = client.run(args=[f'-{model}', '-c', str(count), '-m', str(large), url])
+         r.check_exit_code(0)
++
++    def test_20_11_crazy_pings(self, env: Env):
++        st = {}
++        send_rounds = 1
++
++        def srv():
++            try:
++                with socket.socket() as s:
++                    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
++                    s.bind(("127.0.0.1", 0))
++                    s.listen(1)
++                    st["p"] = s.getsockname()[1]
++
++                    c, _ = s.accept()
++                    c.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
++                    c.settimeout(Env.SERVER_TIMEOUT)
++                    req = b""
++                    while b"\r\n\r\n" not in req:
++                        req += c.recv(4096)
++
++                    k = re.search(rb"(?im)^Sec-WebSocket-Key:\s*(\S+)", req).group(1)
++                    a = base64.b64encode(
++                        hashlib.sha1(k + b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest()
++                    ).decode()
++                    c.sendall(
++                        (
++                            "HTTP/1.1 101 Switching Protocols\r\n"
++                            "Upgrade: websocket\r\n"
++                            "Connection: Upgrade\r\n"
++                            f"Sec-WebSocket-Accept: {a}\r\n\r\n"
++                        ).encode()
++                    )
++
++                    f = b"\x89\x00" * 65536  # PING frames, many
++                    try:
++                        for _ in range(send_rounds):
++                            c.sendall(f)
++                        f = b"\x88\x00"  # CLOSE frame
++                        c.sendall(f)
++                    except OSError:
++                        pass
++                    time.sleep(1)
++                    c.close()
++            except OSError as e:
++                st["err"] = e
++
++        curl = CurlClient(env=env)
++        send_rounds = 2
++        threading.Thread(target=srv, daemon=True).start()
++        while "p" not in st and "err" not in st:
++            time.sleep(0.01)
++        assert "err" not in st, f'ws-ping server failed to start: {st["err"]}'
++
++        url = f'ws://127.0.0.1:{st["p"]}/'
++        r = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
++                               with_profile=True)
++        assert r.exit_code in [55, 56], f'{r.dump_logs()}'  # SEND/RECV_ERROR
++        assert r.profile, f'{r}'
++        rss1 = r.profile.stats['rss'] / (1024 * 1024)
++
++        st.clear()
++        send_rounds = 10
++        threading.Thread(target=srv, daemon=True).start()
++        while "p" not in st and "err" not in st:
++            time.sleep(0.01)
++        assert "err" not in st, f'ws-ping server failed to start: {st["err"]}'
++
++        url = f'ws://127.0.0.1:{st["p"]}/'
++        r = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
++                               with_profile=True)
++        assert r.exit_code in [55, 56], f'{r.dump_logs()}'  # SEND/RECV_ERROR
++        assert r.profile, f'{r}'
++        rss2 = r.profile.stats['rss'] / (1024 * 1024)
++        assert (rss1 * 1.1) >= rss2, 'bad memory increase'
+-- 
+2.55.0
+

diff --git a/curl.spec b/curl.spec
index e75ab49..677f71d 100644
--- a/curl.spec
+++ b/curl.spec
@@ -63,6 +63,9 @@ Patch012: 0012-curl-8.18.0-CVE-2026-7009.patch
 # Fix QUIC zero-length UDP datagrams busy-loop (CVE-2026-11352)
 Patch013: 0013-curl-8.18.0-CVE-2026-11352.patch
 
+# Fix WS Auto-PONG memory exhaustion (CVE-2026-11586)
+Patch014: 0014-curl-8.18.0-CVE-2026-11586.patch
+
 # patch making libcurl multilib ready
 Patch101: 0101-curl-7.32.0-multilib.patch
 
@@ -488,6 +491,7 @@ rm -f ${RPM_BUILD_ROOT}%{_mandir}/man1/wcurl.1*
 %changelog
 * Mon Aug 24 2026 Jan Macku <jamacku@redhat.com> - 8.18.0-9
 - Fix QUIC zero-length UDP datagrams busy-loop (CVE-2026-11352)
+- Fix WS Auto-PONG memory exhaustion (CVE-2026-11586)
 
 * Wed Jul 29 2026 Jan Macku <jamacku@redhat.com> - 8.18.0-8
 - Fix trailing dot domain super cookie (CVE-2026-8924)

                 reply	other threads:[~2026-08-24 14:03 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=178758019422.1.3662016725428605620.rpms-curl-85f22282048d@fedoraproject.org \
    --to=jamacku@redhat.com \
    --cc=git-commits@fedoraproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox