Package: apt-cacher-ng
Version: 3.7.5-1
Tags: patch, memory-leak
X-Debbugs-CC: [email protected], [email protected]

We've recently upgraded our apt-cacher-ng from Debian Bullseye (acng 3.6.4-1) to Trixie and noticed unbounded memory leakage which resulted in OOM (32 GB RAM) in ~36 hours (20260413_14h47m21s_apt-cacher-ng.png). We've mitigated the issue by restarting the service in regular intervals, but I wasn't happy with this workaround.

cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
VERSION_CODENAME=trixie
DEBIAN_VERSION_FULL=13.4
ID=debian
HOME_URL="https://www.debian.org/";
SUPPORT_URL="https://www.debian.org/support";
BUG_REPORT_URL="https://bugs.debian.org/";

uname -a
Linux filehost01 6.12.74+deb13+1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux

Since I'm not a C++ expert, I've decided to burn some tokens and tasked Github Copilot CLI (via Claude Opus) to analyze the commits between 3.6.4 .. 3.7.5 + the repository code in general for potential leaks. I will attach the four patches for potential issues that Copilot / Claude Opus came up with. The tcpconnect.cc SSL leak seems to be the main culprit (we use some apt sources on our hosts via http://acnghost:3124/HTTPS///, which also executed apt-get update very often). Also in 2019 (commit: 527c1dece5aa56c53f019872ace6e6ebbb74238a) the comparison of the DNS cache (dns_exp_q.front()->second->m_expTime <= now) seems to have been inverted accidentally.

After rebuilding from source with the attached patches applied, memory usage is much reduced (20260414_10h02m37s_apt-cacher-ng-patched.png). There might still be some very minor leakage (RSS went up from 20288 to 55540 over night), but it seems quite stable now at this value. It could be worthwhile to dig deeper using valgrind and such, but I don't have the setup for that right now.

*Disclosure:* The provided patches have been authored in their entirety by machine learning and casually reviewed by me (the last time I did anything with C++ is probably 10 years ago). I hope there's no double free() now; the SSL BIO stuff goes over my head a bit. At the very least, it hasn't produced a crash yet. Since the AI/LLM sphere is a hot button topic in the community, feel free to disregard the proposed patches and you could recognize this mail as a mere a bug report.

Best regards,

Sebastian Döring

Senior System Administrator
Infrastructure Server and Service Operations

1&1 Telecommunication SE | Hinterm Hauptbahnhof 3 | 76137 Karlsruhe | Deutschland E-Mail: [email protected] | Web: www.1und1.de < https://www.1und1.de >

Die gesetzlichen Pflichtangaben finden Sie unter https://unternehmen.1und1.de/unternehmen/impressum/.

Member of United Internet

Diese E-Mail kann vertrauliche und/oder gesetzlich geschützte Informationen enthalten. Wenn Sie nicht der bestimmungsgemäße Adressat sind oder diese E-Mail irrtümlich erhalten haben, unterrichten Sie bitte den Absender und vernichten Sie diese E-Mail. Anderen als dem bestimmungsgemäßen Adressaten ist untersagt, diese E-Mail zu speichern, weiterzuleiten oder ihren Inhalt auf welche Weise auch immer zu verwenden.

This e-mail may contain confidential and/or privileged information. If you are not the intended recipient of this e-mail, you are hereby notified that saving, distribution or use of the content of this e-mail in any way is prohibited. If you have received this e-mail in error, please notify the sender and delete the e-mail.

From 46bf935dc1bb936759974148ac5c664e816af3be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20D=C3=B6ring?= <[email protected]>
Date: Mon, 13 Apr 2026 13:11:16 +0200
Subject: [PATCH 4/7] Bug 4: Fix tDnsResContext leak on bad DNS configuration
 (HIGH SEVERITY)

When DNS resolver is not properly configured (resolver is nullptr),
a tDnsResContext was allocated with 'new' but never freed on the
error path before returning.

Fix: Add 'delete ctx' in the error handling block before return.

Co-authored-by: Copilot <[email protected]>
---
 src/caddrinfo.cc | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/caddrinfo.cc b/src/caddrinfo.cc
index db500b9..0d0361e 100644
--- a/src/caddrinfo.cc
+++ b/src/caddrinfo.cc
@@ -443,9 +443,13 @@ void CAddrInfo::Resolve(cmstring & sHostname, uint16_t nPort, tDnsResultReporter
 			rep = decltype (rep)();
 			if (AC_UNLIKELY(!ctx || !ctx->resolver))
 			{
-				ctx->cbs.back()(make_shared<CAddrInfo>("503 Bad DNS configuration"));
-				// and defuse the fallback caller
-				ctx->cbs.clear();
+				if (ctx)
+				{
+					ctx->cbs.back()(make_shared<CAddrInfo>("503 Bad DNS configuration"));
+					// and defuse the fallback caller
+					ctx->cbs.clear();
+					delete ctx;
+				}
 				return;
 			}
 			ctx->refMe = tDnsResContext::g_active_resolver_index.emplace(key, ctx).first;
-- 
2.47.3

From 4c55ab45ec04bf3975906263fd47fa3ce29cdec7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20D=C3=B6ring?= <[email protected]>
Date: Mon, 13 Apr 2026 12:21:28 +0200
Subject: [PATCH 3/7] Bug 3: DNS Cache Cleanup Logic Inverted (MEDIUM SEVERITY)

File: src/caddrinfo.cc line 179

 // WRONG: removes NON-expired entries, keeps expired ones!
 while(... && dns_exp_q.front()->second->m_expTime >= now)
 // Should be: <= now
---
 src/caddrinfo.cc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/caddrinfo.cc b/src/caddrinfo.cc
index 0ead6dd..db500b9 100644
--- a/src/caddrinfo.cc
+++ b/src/caddrinfo.cc
@@ -176,7 +176,7 @@ void CAddrInfo::clean_dns_cache()
 	auto now=GetTime();
 	ASSERT(dns_cache.size() == dns_exp_q.size());
 	while(dns_cache.size() >= DNS_CACHE_MAX-1
-			|| (!dns_exp_q.empty() && dns_exp_q.front()->second->m_expTime >= now))
+			|| (!dns_exp_q.empty() && dns_exp_q.front()->second->m_expTime <= now))
 	{
 		dns_cache.erase(dns_exp_q.front());
 		dns_exp_q.pop_front();
-- 
2.47.3

From 163cbedd52ccb55019c82984d3ca1441e84feaa4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20D=C3=B6ring?= <[email protected]>
Date: Mon, 13 Apr 2026 12:21:10 +0200
Subject: [PATCH 2/7] Bug 2: lint_ptr Move Assignment Leak (HIGH SEVERITY)

File: src/acsmartptr.h

The move assignment operator fails to decrement the old pointer's reference count:

 // BUGGY - old pointer never freed!
 lint_ptr<T>& operator=(lint_ptr<T> &&other) {
     if(m_ptr == other.m_ptr) return *this;
     m_ptr = other.m_ptr;  // LEAK: old m_ptr never released
     other.m_ptr = nullptr;
     return *this;
 }

Root cause: The lint_ptr class is entirely new in 3.7.5 (introduced with c-ares DNS migration). Same bug exists in lint_user_ptr.
---
 src/acsmartptr.h | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/acsmartptr.h b/src/acsmartptr.h
index 3cdc578..8b23fab 100644
--- a/src/acsmartptr.h
+++ b/src/acsmartptr.h
@@ -103,7 +103,9 @@ public:
 	{
 		if(m_ptr == other.m_ptr)
 			return *this;
-
+		// Must release old reference before taking new one
+		if(m_ptr)
+			m_ptr->__dec_ref();
 		m_ptr = other.m_ptr;
 		other.m_ptr = nullptr;
 		return *this;
@@ -231,6 +233,12 @@ public:
 	{
 		if(m_ptr == other.m_ptr)
 			return *this;
+		// Must release old reference before taking new one
+		if(m_ptr)
+		{
+			m_ptr->__dec_strong_ref();
+			m_ptr->__dec_ref();
+		}
 		m_ptr = other.m_ptr;
 		other.m_ptr = nullptr;
 		return *this;
-- 
2.47.3

From 0dc13aff4a1b4134a737908d75ef994eda7ba17f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20D=C3=B6ring?= <[email protected]>
Date: Mon, 13 Apr 2026 13:11:09 +0200
Subject: [PATCH 1/7] Bug 1: Fix SSL connection memory leak (CRITICAL SEVERITY)

SSL objects created with SSL_new() were never freed because:
- BIO_set_ssl() used BIO_NOCLOSE flag, preventing BIO cleanup from freeing SSL
- No SSL_free() call existed anywhere in the codebase

Fix:
- Change BIO_NOCLOSE to BIO_CLOSE so BIO_free_all() frees the SSL object
- Add SSL_free(ssl) in error handler lambda for early-return error paths
- Store SSL pointer in m_ssl member for later use after ownership transfer

Each leaked SSL object is ~30-50KB. With modern mirrors using HTTPS,
this is likely the primary cause of the observed ~800KB/s memory growth.

Co-authored-by: Copilot <[email protected]>
---
 src/tcpconnect.cc | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/tcpconnect.cc b/src/tcpconnect.cc
index 64483c2..a58e1a9 100644
--- a/src/tcpconnect.cc
+++ b/src/tcpconnect.cc
@@ -83,6 +83,7 @@ void tcpconnect::Disconnect()
 #ifdef HAVE_SSL
 	if(m_bio)
 		BIO_free_all(m_bio), m_bio=nullptr;
+	m_ssl = nullptr;
 #endif
 
 	m_lastFile.reset();
@@ -310,10 +311,11 @@ bool tcpconnect::SSLinit(mstring &sErr)
 	SSL * ssl(nullptr);
 	mstring ebuf;
 
-	auto withSslHeadPfx = [&sErr](const char *perr)
+	auto withSslHeadPfx = [&sErr, &ssl](const char *perr)
 					{
         sErr="SSL error: ";
 		sErr+=(perr?perr:"Generic SSL failure");
+		if (ssl) SSL_free(ssl);
 		return false;
 					};
 	auto withLastSslError = [&withSslHeadPfx]()
@@ -341,7 +343,7 @@ bool tcpconnect::SSLinit(mstring &sErr)
 	}
 
 	ssl = SSL_new(m_ctx);
-	if (!m_ctx) return withLastSslError();
+	if (!ssl) return withLastSslError();
 
 	bool disableNameValidation = cfg::nsafriendly == 1;// || (bGuessedTls * cfg::nsafriendly == 2);
 	bool disableAllValidation = cfg::nsafriendly == 1; // || (bGuessedTls * (cfg::nsafriendly == 2 || cfg::nsafriendly == 3));
@@ -415,7 +417,11 @@ bool tcpconnect::SSLinit(mstring &sErr)
  	// not sure we need it but maybe the handshake can access this data
 	BIO_set_conn_hostname(m_bio, m_sHostName.c_str());
 	BIO_set_conn_port(m_bio, tPortFmter().fmt(m_nPort));
- 	BIO_set_ssl(m_bio, ssl, BIO_NOCLOSE);
+ 	BIO_set_ssl(m_bio, ssl, BIO_CLOSE);
+	// ssl ownership transferred to m_bio, store reference for validation below
+	// but don't free it in error handler anymore (BIO_free_all handles it now)
+	m_ssl = ssl;
+	ssl = nullptr;
 
  	BIO_set_nbio(m_bio, 1);
 	set_nb(m_conFd);
@@ -423,10 +429,10 @@ bool tcpconnect::SSLinit(mstring &sErr)
 	if(!disableAllValidation)
 	{
 		X509* server_cert = nullptr;
-		hret=SSL_get_verify_result(ssl);
+		hret=SSL_get_verify_result(m_ssl);
 		if( hret != X509_V_OK)
 			return withSslHeadPfx(X509_verify_cert_error_string(hret));
-		server_cert = SSL_get_peer_certificate(ssl);
+		server_cert = SSL_get_peer_certificate(m_ssl);
 		if(server_cert)
 		{
 			// XXX: maybe extract the real name to a buffer and report it additionally?
-- 
2.47.3

Reply via email to