Version in base suite: 1.9.10-1 Base version: dnsdist_1.9.10-1 Target version: dnsdist_1.9.10-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/d/dnsdist/dnsdist_1.9.10-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/d/dnsdist/dnsdist_1.9.10-1+deb13u1.dsc .gitlab-ci.yml | 2 changelog | 8 gbp.conf | 1 patches/series | 1 patches/upstream/CVE-2025-8671-CVE-2025-30187-1.9.10.patch | 374 +++++++++++++ 5 files changed, 385 insertions(+), 1 deletion(-) 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 08:30:03.000000000 +0000 +++ dnsdist-1.9.10/debian/.gitlab-ci.yml 2025-09-12 08:39:35.000000000 +0000 @@ -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/changelog dnsdist-1.9.10/debian/changelog --- dnsdist-1.9.10/debian/changelog 2025-05-21 08:30:17.000000000 +0000 +++ dnsdist-1.9.10/debian/changelog 2025-09-12 08:39:35.000000000 +0000 @@ -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 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 08:30:03.000000000 +0000 +++ dnsdist-1.9.10/debian/gbp.conf 2025-09-12 08:39:35.000000000 +0000 @@ -2,3 +2,4 @@ pristine-tar = True multimaint-merge = True patch-numbers = False +debian-branch = debian/trixie 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 00:00:00.000000000 +0000 +++ dnsdist-1.9.10/debian/patches/series 2025-09-12 08:39:35.000000000 +0000 @@ -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 00:00:00.000000000 +0000 +++ dnsdist-1.9.10/debian/patches/upstream/CVE-2025-8671-CVE-2025-30187-1.9.10.patch 2025-09-12 08:39:35.000000000 +0000 @@ -0,0 +1,374 @@ +From: Remi Gacogne +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 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 settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100U}}}; ++ constexpr std::array 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::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(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(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(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 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 getDOHUnit(uint32_t streamID) override; + + void stopIO(); ++ std::unordered_map::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 d_session{nullptr, nghttp2_session_del}; + std::unordered_map d_currentStreams; ++ std::unordered_set 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 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& 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 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(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; + }