[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1118547: trixie-pu: package dnsdist/1.9.10-1+deb13u1



Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: dnsdist@packages.debian.org, security@debian.org
Control: affects -1 + src:dnsdist
User: release.debian.org@packages.debian.org
Usertags: pu

[ Reason ]
Fix CVE-2025-8671, CVE-2025-30187. They are not high-sev issues.

[ Impact ]
Security issues remain open.

[ Tests ]
I've done a manual test but I cannot test the resource exhaustion 
issues.

[ Risks ]
Patch is from upstream.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

[ Changes ]
Apply upstream patch fixing both CVEs.

diff -Nru dnsdist-1.9.10/debian/changelog dnsdist-1.9.10/debian/changelog
--- dnsdist-1.9.10/debian/changelog	2025-05-21 10:30:17.000000000 +0200
+++ dnsdist-1.9.10/debian/changelog	2025-09-12 10:39:35.000000000 +0200
@@ -1,3 +1,11 @@
+dnsdist (1.9.10-1+deb13u1) trixie; urgency=medium
+
+  * d/{gbp.conf,.gitlab-ci.yml}: setup for trixie
+  * Apply upstream fix for CVE-2025-8671, CVE-2025-30187
+    (Closes: #1115643)
+
+ -- Chris Hofstaedtler <zeha@debian.org>  Fri, 12 Sep 2025 10:39:35 +0200
+
 dnsdist (1.9.10-1) unstable; urgency=medium
 
   * New upstream version 1.9.10 including fix for CVE-2025-30193
diff -Nru dnsdist-1.9.10/debian/gbp.conf dnsdist-1.9.10/debian/gbp.conf
--- dnsdist-1.9.10/debian/gbp.conf	2025-05-21 10:30:03.000000000 +0200
+++ dnsdist-1.9.10/debian/gbp.conf	2025-09-12 10:39:35.000000000 +0200
@@ -2,3 +2,4 @@
 pristine-tar = True
 multimaint-merge = True
 patch-numbers = False
+debian-branch = debian/trixie
diff -Nru dnsdist-1.9.10/debian/.gitlab-ci.yml dnsdist-1.9.10/debian/.gitlab-ci.yml
--- dnsdist-1.9.10/debian/.gitlab-ci.yml	2025-05-21 10:30:03.000000000 +0200
+++ dnsdist-1.9.10/debian/.gitlab-ci.yml	2025-09-12 10:39:35.000000000 +0200
@@ -3,7 +3,7 @@
  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
 
 variables:
-  RELEASE: 'unstable'
+  RELEASE: 'trixie'
   SALSA_CI_DISABLE_APTLY: 1
   SALSA_CI_DISABLE_PIUPARTS: 1
   SALSA_CI_DISABLE_REPROTEST: 1
diff -Nru dnsdist-1.9.10/debian/patches/series dnsdist-1.9.10/debian/patches/series
--- dnsdist-1.9.10/debian/patches/series	1970-01-01 01:00:00.000000000 +0100
+++ dnsdist-1.9.10/debian/patches/series	2025-09-12 10:39:35.000000000 +0200
@@ -0,0 +1 @@
+upstream/CVE-2025-8671-CVE-2025-30187-1.9.10.patch
diff -Nru dnsdist-1.9.10/debian/patches/upstream/CVE-2025-8671-CVE-2025-30187-1.9.10.patch dnsdist-1.9.10/debian/patches/upstream/CVE-2025-8671-CVE-2025-30187-1.9.10.patch
--- dnsdist-1.9.10/debian/patches/upstream/CVE-2025-8671-CVE-2025-30187-1.9.10.patch	1970-01-01 01:00:00.000000000 +0100
+++ dnsdist-1.9.10/debian/patches/upstream/CVE-2025-8671-CVE-2025-30187-1.9.10.patch	2025-09-12 10:39:35.000000000 +0200
@@ -0,0 +1,374 @@
+From: Remi Gacogne <remi.gacogne@powerdns.com>
+Date: Thu, 11 Sep 2025 13:38:49 +0200
+Subject: PowerDNS Security Advisory 2025-05 for DNSdist: Denial of service via crafted DoH exchange
+
+While working on adding mitigations against the MadeYouReset (CVE-2025-8671)
+attack, we noticed a potential denial of service in our DNS over HTTPS
+implementation when using the nghttp2 provider: an attacker might be able to
+cause a denial of service by crafting a DoH exchange that triggers an unbounded
+I/O read loop, causing an unexpected consumption of CPU resources. We assigned
+CVE-2025-30187 to this issue.
+
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1115643
+
+
+diff -ruw dnsdist-1.9.10.orig/dnsdist-doh-common.hh dnsdist-1.9.10/dnsdist-doh-common.hh
+--- dnsdist-1.9.10.orig/dnsdist-doh-common.hh	2025-05-20 11:13:25.000000000 +0200
++++ dnsdist-1.9.10/dnsdist-doh-common.hh	2025-09-11 11:09:57.007006314 +0200
+@@ -35,6 +35,8 @@
+ 
+ namespace dnsdist::doh
+ {
++static constexpr uint32_t MAX_INCOMING_CONCURRENT_STREAMS{100U};
++
+ std::optional<PacketBuffer> getPayloadFromPath(const std::string_view& path);
+ }
+ 
+diff -ruw dnsdist-1.9.10.orig/dnsdist-nghttp2-in.cc dnsdist-1.9.10/dnsdist-nghttp2-in.cc
+--- dnsdist-1.9.10.orig/dnsdist-nghttp2-in.cc	2025-05-20 11:13:25.000000000 +0200
++++ dnsdist-1.9.10/dnsdist-nghttp2-in.cc	2025-09-11 11:10:21.628205731 +0200
+@@ -288,7 +288,7 @@
+ 
+ void IncomingHTTP2Connection::handleConnectionReady()
+ {
+-  constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100U}}};
++  constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS}}};
+   auto ret = nghttp2_submit_settings(d_session.get(), NGHTTP2_FLAG_NONE, settings.data(), settings.size());
+   if (ret != 0) {
+     throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
+@@ -440,6 +440,24 @@
+       if (nghttp2_session_want_read(d_session.get()) != 0) {
+         updateIO(IOState::NeedRead, handleReadableIOCallback);
+       }
++      else {
++        if (getConcurrentStreamsCount() == 0) {
++          d_connectionDied = true;
++          stopIO();
++        }
++        else {
++          updateIO(IOState::Done, handleReadableIOCallback);
++        }
++      }
++    }
++    else {
++      if (getConcurrentStreamsCount() == 0) {
++        d_connectionDied = true;
++        stopIO();
++      }
++      else {
++        updateIO(IOState::Done, handleReadableIOCallback);
++      }
+     }
+   }
+   catch (const std::exception& e) {
+@@ -547,12 +565,22 @@
+   NGHTTP2Headers::addCustomDynamicHeader(headers, name, value);
+ }
+ 
++std::unordered_map<IncomingHTTP2Connection::StreamID, IncomingHTTP2Connection::PendingQuery>::iterator IncomingHTTP2Connection::getStreamContext(StreamID streamID)
++{
++  auto streamIt = d_currentStreams.find(streamID);
++  if (streamIt == d_currentStreams.end()) {
++    /* it might have been closed by the remote end in the meantime */
++    d_killedStreams.erase(streamID);
++  }
++  return streamIt;
++}
++
+ IOState IncomingHTTP2Connection::sendResponse(const struct timeval& now, TCPResponse&& response)
+ {
+   if (response.d_idstate.d_streamID == -1) {
+     throw std::runtime_error("Invalid DoH stream ID while sending response");
+   }
+-  auto streamIt = d_currentStreams.find(response.d_idstate.d_streamID);
++  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
+   if (streamIt == d_currentStreams.end()) {
+     /* it might have been closed by the remote end in the meantime */
+     return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
+@@ -592,7 +620,7 @@
+     throw std::runtime_error("Invalid DoH stream ID while handling I/O error notification");
+   }
+ 
+-  auto streamIt = d_currentStreams.find(response.d_idstate.d_streamID);
++  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
+   if (streamIt == d_currentStreams.end()) {
+     /* it might have been closed by the remote end in the meantime */
+     return;
+@@ -735,17 +763,18 @@
+     NGHTTP2Headers::addCustomDynamicHeader(headers, key, value);
+   }
+ 
++  context.d_sendingResponse = true;
+   auto ret = nghttp2_submit_response(d_session.get(), streamID, headers.data(), headers.size(), &data_provider);
+   if (ret != 0) {
+-    d_currentStreams.erase(streamID);
+     vinfolog("Error submitting HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
++    d_currentStreams.erase(streamID);
+     return false;
+   }
+ 
+   ret = nghttp2_session_send(d_session.get());
+   if (ret != 0) {
+-    d_currentStreams.erase(streamID);
+     vinfolog("Error flushing HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
++    d_currentStreams.erase(streamID);
+     return false;
+   }
+ 
+@@ -921,7 +950,7 @@
+   /* is this the last frame for this stream? */
+   if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) {
+     auto streamID = frame->hd.stream_id;
+-    auto stream = conn->d_currentStreams.find(streamID);
++    auto stream = conn->getStreamContext(streamID);
+     if (stream != conn->d_currentStreams.end()) {
+       conn->handleIncomingQuery(std::move(stream->second), streamID);
+     }
+@@ -941,7 +970,16 @@
+ {
+   auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+ 
+-  conn->d_currentStreams.erase(stream_id);
++  auto streamIt = conn->d_currentStreams.find(stream_id);
++  if (streamIt == conn->d_currentStreams.end()) {
++    return 0;
++  }
++
++  if (!streamIt->second.d_sendingResponse) {
++    conn->d_killedStreams.emplace(stream_id);
++  }
++
++  conn->d_currentStreams.erase(streamIt);
+   return 0;
+ }
+ 
+@@ -952,20 +990,29 @@
+   }
+ 
+   auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+-  auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
+-  if (!insertPair.second) {
+-    /* there is a stream ID collision, something is very wrong! */
+-    vinfolog("Stream ID collision (%d) on connection from %d", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
+-    conn->d_connectionClosing = true;
+-    conn->d_needFlush = true;
+-    nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
+-    auto ret = nghttp2_session_send(conn->d_session.get());
++  auto close_connection = [](IncomingHTTP2Connection* connection, int32_t streamID, const ComboAddress& remote) -> int {
++    connection->d_connectionClosing = true;
++    connection->d_needFlush = true;
++    nghttp2_session_terminate_session(connection->d_session.get(), NGHTTP2_REFUSED_STREAM);
++    auto ret = nghttp2_session_send(connection->d_session.get());
+     if (ret != 0) {
+-      vinfolog("Error flushing HTTP response for stream %d from %s: %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort(), nghttp2_strerror(ret));
++      vinfolog("Error flushing HTTP response for stream %d from %s: %s", streamID, remote.toStringWithPort(), nghttp2_strerror(ret));
+       return NGHTTP2_ERR_CALLBACK_FAILURE;
+     }
+ 
+     return 0;
++  };
++
++  if (conn->getConcurrentStreamsCount() >= dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS) {
++    vinfolog("Too many concurrent streams on connection from %s", conn->d_ci.remote.toStringWithPort());
++    return close_connection(conn, frame->hd.stream_id, conn->d_ci.remote);
++  }
++
++  auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
++  if (!insertPair.second) {
++    /* there is a stream ID collision, something is very wrong! */
++    vinfolog("Stream ID collision (%d) on connection from %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
++    return close_connection(conn, frame->hd.stream_id, conn->d_ci.remote);
+   }
+ 
+   return 0;
+@@ -1002,7 +1049,7 @@
+       return nameLen == expected.size() && memcmp(name, expected.data(), expected.size()) == 0;
+     };
+ 
+-    auto stream = conn->d_currentStreams.find(frame->hd.stream_id);
++    auto stream = conn->getStreamContext(frame->hd.stream_id);
+     if (stream == conn->d_currentStreams.end()) {
+       vinfolog("Unable to match the stream ID %d to a known one!", frame->hd.stream_id);
+       return NGHTTP2_ERR_CALLBACK_FAILURE;
+@@ -1065,7 +1112,7 @@
+ int IncomingHTTP2Connection::on_data_chunk_recv_callback(nghttp2_session* session, uint8_t flags, IncomingHTTP2Connection::StreamID stream_id, const uint8_t* data, size_t len, void* user_data)
+ {
+   auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+-  auto stream = conn->d_currentStreams.find(stream_id);
++  auto stream = conn->getStreamContext(stream_id);
+   if (stream == conn->d_currentStreams.end()) {
+     vinfolog("Unable to match the stream ID %d to a known one!", stream_id);
+     return NGHTTP2_ERR_CALLBACK_FAILURE;
+@@ -1155,7 +1202,7 @@
+ 
+ uint32_t IncomingHTTP2Connection::getConcurrentStreamsCount() const
+ {
+-  return d_currentStreams.size();
++  return d_currentStreams.size() + d_killedStreams.size();
+ }
+ 
+ boost::optional<struct timeval> IncomingHTTP2Connection::getIdleClientReadTTD(struct timeval now) const
+@@ -1208,6 +1255,9 @@
+     ttd = getClientWriteTTD(now);
+     d_ioState->update(newState, callback, shared, ttd);
+   }
++  else if (newState == IOState::Done) {
++    d_ioState->reset();
++  }
+ }
+ 
+ void IncomingHTTP2Connection::handleIOError()
+@@ -1217,6 +1267,7 @@
+   d_outPos = 0;
+   nghttp2_session_terminate_session(d_session.get(), NGHTTP2_PROTOCOL_ERROR);
+   d_currentStreams.clear();
++  d_killedStreams.clear();
+   stopIO();
+ }
+ 
+diff -ruw dnsdist-1.9.10.orig/dnsdist-nghttp2-in.hh dnsdist-1.9.10/dnsdist-nghttp2-in.hh
+--- dnsdist-1.9.10.orig/dnsdist-nghttp2-in.hh	2025-05-20 11:13:25.000000000 +0200
++++ dnsdist-1.9.10/dnsdist-nghttp2-in.hh	2025-09-11 11:10:04.764742240 +0200
+@@ -55,6 +55,7 @@
+     size_t d_queryPos{0};
+     uint32_t d_statusCode{0};
+     Method d_method{Method::Unknown};
++    bool d_sendingResponse{false};
+   };
+ 
+   IncomingHTTP2Connection(ConnectionInfo&& connectionInfo, TCPClientThreadData& threadData, const struct timeval& now);
+@@ -86,6 +87,7 @@
+   std::unique_ptr<DOHUnitInterface> getDOHUnit(uint32_t streamID) override;
+ 
+   void stopIO();
++  std::unordered_map<StreamID, PendingQuery>::iterator getStreamContext(StreamID streamID);
+   uint32_t getConcurrentStreamsCount() const;
+   void updateIO(IOState newState, const FDMultiplexer::callbackfunc_t& callback);
+   void handleIOError();
+@@ -101,6 +103,7 @@
+ 
+   std::unique_ptr<nghttp2_session, decltype(&nghttp2_session_del)> d_session{nullptr, nghttp2_session_del};
+   std::unordered_map<StreamID, PendingQuery> d_currentStreams;
++  std::unordered_set<StreamID> d_killedStreams;
+   PacketBuffer d_out;
+   PacketBuffer d_in;
+   size_t d_outPos{0};
+diff -ruw dnsdist-1.9.10.orig/doh.cc dnsdist-1.9.10/doh.cc
+--- dnsdist-1.9.10.orig/doh.cc	2025-05-20 11:13:25.000000000 +0200
++++ dnsdist-1.9.10/doh.cc	2025-09-11 11:10:16.325285812 +0200
+@@ -313,6 +313,7 @@
+   struct timeval d_connectionStartTime{0, 0};
+   size_t d_nbQueries{0};
+   int d_desc{-1};
++  uint8_t d_concurrentStreams{0};
+ };
+ 
+ static thread_local std::unordered_map<int, DOHConnection> t_conns;
+@@ -386,6 +387,17 @@
+   return reasonIt->second;
+ }
+ 
++static DOHConnection* getConnectionFromQuery(const h2o_req_t* req)
++{
++  h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
++  const int descriptor = h2o_socket_get_fd(sock);
++  if (descriptor == -1) {
++    /* this should not happen, but let's not crash on it */
++    return nullptr;
++  }
++  return &t_conns.at(descriptor);
++}
++
+ /* Always called from the main DoH thread */
+ static void handleResponse(DOHFrontend& dohFrontend, st_h2o_req_t* req, uint16_t statusCode, const PacketBuffer& response, const std::unordered_map<std::string, std::string>& customResponseHeaders, const std::string& contentType, bool addContentType)
+ {
+@@ -461,6 +473,10 @@
+ 
+     ++dohFrontend.d_errorresponses;
+   }
++
++  if (auto* conn = getConnectionFromQuery(req)) {
++    --conn->d_concurrentStreams;
++  }
+ }
+ 
+ static std::unique_ptr<DOHUnit> getDUFromIDS(InternalQueryState& ids)
+@@ -918,6 +934,8 @@
+    via a pipe */
+ static void doh_dispatch_query(DOHServerConfig* dsc, h2o_handler_t* self, h2o_req_t* req, PacketBuffer&& query, const ComboAddress& local, const ComboAddress& remote, std::string&& path)
+ {
++  auto* conn = getConnectionFromQuery(req);
++
+   try {
+     /* we only parse it there as a sanity check, we will parse it again later */
+     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+@@ -949,6 +967,9 @@
+       }
+     }
+ 
++    if (conn != nullptr) {
++      ++conn->d_concurrentStreams;
++    }
+ #ifdef HAVE_H2O_SOCKET_GET_SSL_SERVER_NAME
+     h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
+     const char * sni = h2o_socket_get_ssl_server_name(sock);
+@@ -966,17 +987,26 @@
+       if (!dsc->d_querySender.send(std::move(dohUnit))) {
+         ++dnsdist::metrics::g_stats.dohQueryPipeFull;
+         vinfolog("Unable to pass a DoH query to the DoH worker thread because the pipe is full");
++        if (conn != nullptr) {
++          --conn->d_concurrentStreams;
++        }
+         h2o_send_error_500(req, "Internal Server Error", "Internal Server Error", 0);
+       }
+     }
+     catch (...) {
+       vinfolog("Unable to pass a DoH query to the DoH worker thread because we couldn't write to the pipe: %s", stringerror());
++      if (conn != nullptr) {
++        --conn->d_concurrentStreams;
++      }
+       h2o_send_error_500(req, "Internal Server Error", "Internal Server Error", 0);
+     }
+ #endif /* USE_SINGLE_ACCEPTOR_THREAD */
+   }
+   catch (const std::exception& e) {
+     vinfolog("Had error parsing DoH DNS packet from %s: %s", remote.toStringWithPort(), e.what());
++    if (conn != nullptr) {
++      --conn->d_concurrentStreams;
++    }
+     h2o_send_error_400(req, "Bad Request", "The DNS query could not be parsed", 0);
+   }
+ }
+@@ -1046,15 +1076,19 @@
+     }
+     // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): h2o API
+     auto* dsc = static_cast<DOHServerConfig*>(req->conn->ctx->storage.entries[0].data);
+-    h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
+-
+-    const int descriptor = h2o_socket_get_fd(sock);
+-    if (descriptor == -1) {
++    auto* connPtr = getConnectionFromQuery(req);
++    if (connPtr == nullptr) {
++      return 0;
++    }
++    auto& conn = *connPtr;
++    if (conn.d_concurrentStreams >= dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS) {
++      vinfolog("Too many concurrent streams on connection from %d", conn.d_remote.toStringWithPort());
+       return 0;
+     }
+ 
+-    auto& conn = t_conns.at(descriptor);
+     ++conn.d_nbQueries;
++
++    h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
+     if (conn.d_nbQueries == 1) {
+       if (h2o_socket_get_ssl_session_reused(sock) == 0) {
+         ++dsc->clientState->tlsNewSessions;
+@@ -1121,6 +1155,7 @@
+       for (const auto& entry : *responsesMap) {
+         if (entry->matches(path)) {
+           const auto& customHeaders = entry->getHeaders();
++          ++conn.d_concurrentStreams;
+           handleResponse(*dsc->dohFrontend, req, entry->getStatusCode(), entry->getContent(), customHeaders ? *customHeaders : dsc->dohFrontend->d_customResponseHeaders, std::string(), false);
+           return 0;
+         }

Reply to: