[9.11] [CVE-2026-3039] sec: usr: Fix GSS-API resource leak

Fixed a memory leak where each GSS-API TKEY negotiation leaked a security context inside the GSS library. An unauthenticated attacker could exhaust server memory by sending repeated TKEY queries to a server with tkey-gssapi-keytab configured. The leaked memory was allocated by the GSS library, bypassing BIND's memory accounting.

Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now rejected, as BIND never supported it correctly and Kerberos/SPNEGO completes in a single round.

Also implemented missing RFC 3645 requirement: the client now verifies that mutual authentication and integrity flags are granted by the GSS-API mechanism (Section 3.1.1).

Resolves-Vulnerability: CVE-2026-3039
Resolves: RHEL-177673
This commit is contained in:
Petr Menšík 2026-05-27 14:26:23 +02:00
parent f523ee34fd
commit 6e9e27a88b
2 changed files with 382 additions and 1 deletions

View File

@ -0,0 +1,375 @@
From 7c2afb490391a308733da1c37730328477bd80d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= <ondrej@isc.org>
Date: Wed, 18 Mar 2026 00:10:35 +0100
Subject: [PATCH] Fix GSS-API context leak in TKEY negotiation
Reject multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) in
dst_gssapi_acceptctx(). Each call to gss_accept_sec_context()
allocates a context inside the GSS library; without this fix, the
context handle was passed back to process_gsstkey() which did not
store it persistently, leaking it on every incomplete negotiation.
An unauthenticated attacker could exhaust server memory by sending
repeated TKEY queries with GSSAPI tokens, each leaking one GSS
context. The leaked memory is allocated by the GSS library via
malloc(), bypassing BIND's memory accounting.
In practice, Kerberos/SPNEGO (the only mechanism used with BIND)
completes in a single round, so rejecting continuation does not
affect real-world deployments. See RFC 3645 Section 4.1.3.
(cherry picked from commit 3883058bf284de2889e4e3676767e58ad91a0ae3)
(cherry picked from commit e3c74f19c6b3b29ec649062f95bf1df5688a0799)
Fix output token and GSS context leaks in TKEY/GSS-API error paths
In dst_gssapi_acceptctx(), rename outtoken to outtokenp (matching BIND
convention for output pointer parameters) and free the allocated output
token buffer on error in the cleanup path.
In process_gsstkey(), route the empty-principal error path through
cleanup via CLEANUP() instead of returning early, so that the output
token, GSS context, and TSIG key are all freed consistently by the
existing cleanup block.
(cherry picked from commit f2240d2d06a1a68b622bd6b00a52c6fe4274426d)
(cherry picked from commit f2f1c0cb244a6d9e2fd8550a38bd2465856abce3)
---
lib/dns/gssapictx.c | 127 ++++++++++++++++++++---------------
lib/dns/include/dst/gssapi.h | 17 ++---
lib/dns/tkey.c | 50 +++++++-------
3 files changed, 104 insertions(+), 90 deletions(-)
diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c
index 482c25e1cc..a58c3071d4 100644
--- a/lib/dns/gssapictx.c
+++ b/lib/dns/gssapictx.c
@@ -594,7 +594,14 @@ dst_gssapi_initctx(dns_name_t *name, isc_buffer_t *intoken,
0, NULL, gintokenp,
NULL, &gouttoken, &ret_flags, NULL);
- if (gret != GSS_S_COMPLETE && gret != GSS_S_CONTINUE_NEEDED) {
+ switch (gret) {
+ case GSS_S_COMPLETE:
+ result = ISC_R_SUCCESS;
+ break;
+ case GSS_S_CONTINUE_NEEDED:
+ result = DNS_R_CONTINUE;
+ break;
+ default:
gss_err_message(mctx, gret, minor, err_message);
if (err_message != NULL && *err_message != NULL)
gss_log(3, "Failure initiating security context: %s",
@@ -619,14 +626,10 @@ dst_gssapi_initctx(dns_name_t *name, isc_buffer_t *intoken,
RETERR(isc_buffer_copyregion(outtoken, &r));
}
- if (gret == GSS_S_COMPLETE)
- result = ISC_R_SUCCESS;
- else
- result = DNS_R_CONTINUE;
-
- out:
- if (gouttoken.length != 0U)
+out:
+ if (gouttoken.length != 0U) {
(void)gss_release_buffer(&minor, &gouttoken);
+ }
(void)gss_release_name(&minor, &gname);
return (result);
}
@@ -634,7 +637,7 @@ dst_gssapi_initctx(dns_name_t *name, isc_buffer_t *intoken,
isc_result_t
dst_gssapi_acceptctx(gss_cred_id_t cred,
const char *gssapi_keytab,
- isc_region_t *intoken, isc_buffer_t **outtoken,
+ isc_region_t *intoken, isc_buffer_t **outtokenp,
gss_ctx_id_t *ctxout, dns_name_t *principal,
isc_mem_t *mctx) {
isc_region_t r;
@@ -647,15 +650,11 @@ dst_gssapi_acceptctx(gss_cred_id_t cred,
isc_result_t result;
char buf[1024];
- REQUIRE(outtoken != NULL && *outtoken == NULL);
+ REQUIRE(outtokenp != NULL && *outtokenp == NULL);
+ REQUIRE(*ctxout == NULL);
REGION_TO_GBUFFER(*intoken, gintoken);
- if (*ctxout == NULL)
- context = GSS_C_NO_CONTEXT;
- else
- context = *ctxout;
-
if (gssapi_keytab != NULL) {
#if defined(ISC_PLATFORM_GSSAPI_KRB5_HEADER) || defined(WIN32)
gret = gsskrb5_register_acceptor_identity(gssapi_keytab);
@@ -697,8 +696,15 @@ dst_gssapi_acceptctx(gss_cred_id_t cred,
switch (gret) {
case GSS_S_COMPLETE:
- case GSS_S_CONTINUE_NEEDED:
break;
+ /*
+ * RFC 3645 4.1.3: we don't handle GSS_S_CONTINUE_NEEDED
+ * Multi-round GSS-API negotiation is not supported.
+ */
+ case GSS_S_CONTINUE_NEEDED:
+ gss_log(3, "multi-round GSS-API negotiation not supported");
+ (void)gss_delete_sec_context(&minor, &context, NULL);
+ /* FALLTHROUGH */
case GSS_S_DEFECTIVE_TOKEN:
case GSS_S_DEFECTIVE_CREDENTIAL:
case GSS_S_BAD_SIG:
@@ -711,7 +717,7 @@ dst_gssapi_acceptctx(gss_cred_id_t cred,
case GSS_S_BAD_MECH:
case GSS_S_FAILURE:
result = DNS_R_INVALIDTKEY;
- /* fall through */
+ /* FALLTHROUGH */
default:
gss_log(3, "failed gss_accept_sec_context: %s",
gss_error_tostring(gret, minor, buf, sizeof(buf)));
@@ -722,55 +728,70 @@ dst_gssapi_acceptctx(gss_cred_id_t cred,
}
if (gouttoken.length > 0U) {
- RETERR(isc_buffer_allocate(mctx, outtoken,
+ RETERR(isc_buffer_allocate(mctx, outtokenp,
(unsigned int)gouttoken.length));
GBUFFER_TO_REGION(gouttoken, r);
- RETERR(isc_buffer_copyregion(*outtoken, &r));
+ result = isc_buffer_copyregion(*outtokenp, &r);
+ if (result != ISC_R_SUCCESS) {
+ goto out;
+ }
(void)gss_release_buffer(&minor, &gouttoken);
}
- if (gret == GSS_S_COMPLETE) {
- gret = gss_display_name(&minor, gname, &gnamebuf, NULL);
- if (gret != GSS_S_COMPLETE) {
- gss_log(3, "failed gss_display_name: %s",
- gss_error_tostring(gret, minor,
- buf, sizeof(buf)));
- RETERR(ISC_R_FAILURE);
- }
+ INSIST(gret == GSS_S_COMPLETE);
- /*
- * Compensate for a bug in Solaris8's implementation
- * of gss_display_name(). Should be harmless in any
- * case, since principal names really should not
- * contain null characters.
- */
- if (gnamebuf.length > 0U &&
- ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0')
- gnamebuf.length--;
+ gret = gss_display_name(&minor, gname, &gnamebuf, NULL);
+ if (gret != GSS_S_COMPLETE) {
+ gss_log(3, "failed gss_display_name: %s",
+ gss_error_tostring(gret, minor, buf, sizeof(buf)));
+ result = ISC_R_FAILURE;
+ goto out;
+ }
- gss_log(3, "gss-api source name (accept) is %.*s",
- (int)gnamebuf.length, (char *)gnamebuf.value);
+ /*
+ * Compensate for a bug in Solaris8's implementation
+ * of gss_display_name(). Should be harmless in any
+ * case, since principal names really should not
+ * contain null characters.
+ */
+ if (gnamebuf.length > 0U &&
+ ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0')
+ {
+ gnamebuf.length--;
+ }
- GBUFFER_TO_REGION(gnamebuf, r);
- isc_buffer_init(&namebuf, r.base, r.length);
- isc_buffer_add(&namebuf, r.length);
+ gss_log(3, "gss-api source name (accept) is %.*s", (int)gnamebuf.length,
+ (char *)gnamebuf.value);
- RETERR(dns_name_fromtext(principal, &namebuf, dns_rootname,
- 0, NULL));
+ GBUFFER_TO_REGION(gnamebuf, r);
+ isc_buffer_init(&namebuf, r.base, r.length);
+ isc_buffer_add(&namebuf, r.length);
- if (gnamebuf.length != 0U) {
- gret = gss_release_buffer(&minor, &gnamebuf);
- if (gret != GSS_S_COMPLETE)
- gss_log(3, "failed gss_release_buffer: %s",
- gss_error_tostring(gret, minor, buf,
- sizeof(buf)));
- }
- } else
- result = DNS_R_CONTINUE;
+ result = dns_name_fromtext(principal, &namebuf, dns_rootname, 0, NULL);
+ if (result != ISC_R_SUCCESS) {
+ goto out;
+ }
*ctxout = context;
- out:
+out:
+ if (result != ISC_R_SUCCESS && *outtokenp != NULL) {
+ isc_buffer_free(outtokenp);
+ }
+
+ if (result != ISC_R_SUCCESS && context != GSS_C_NO_CONTEXT) {
+ (void)gss_delete_sec_context(&minor, &context, NULL);
+ }
+
+ if (gnamebuf.length != 0U) {
+ gret = gss_release_buffer(&minor, &gnamebuf);
+ if (gret != GSS_S_COMPLETE) {
+ gss_log(3, "failed gss_release_buffer: %s",
+ gss_error_tostring(gret, minor, buf,
+ sizeof(buf)));
+ }
+ }
+
if (gname != NULL) {
gret = gss_release_name(&minor, &gname);
if (gret != GSS_S_COMPLETE)
diff --git a/lib/dns/include/dst/gssapi.h b/lib/dns/include/dst/gssapi.h
index 8e31587871..bf1c4c959a 100644
--- a/lib/dns/include/dst/gssapi.h
+++ b/lib/dns/include/dst/gssapi.h
@@ -126,20 +126,17 @@ dst_gssapi_acceptctx(gss_cred_id_t cred,
* generated by gss_accept_sec_context() to be sent to the
* initiator
* 'context' is a valid pointer to receive the generated context handle.
- * On the initial call, it should be a pointer to NULL, which
- * will be allocated as a gss_ctx_id_t. Subsequent calls
- * should pass in the handle generated on the first call.
- * Call dst_gssapi_releasecred to delete the context and free
- * the memory.
*
* Requires:
- * 'outtoken' to != NULL && *outtoken == NULL.
+ * 'outtoken' != NULL && *outtoken == NULL.
+ * 'context' != NULL && *context == NULL.
*
* Returns:
- * ISC_R_SUCCESS msg was successfully updated to include the
- * query to be sent
- * DNS_R_CONTINUE transaction still in progress
- * other an error occurred while building the message
+ * ISC_R_SUCCESS msg was successfully updated to include
+ * the query to be sent
+ * DNS_R_INVALIDTKEY an error occurred while accepting the
+ * context
+ * ISC_R_FAILURE other error occurred
*/
isc_result_t
diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c
index ae7a76cd3d..03bf521ee6 100644
--- a/lib/dns/tkey.c
+++ b/lib/dns/tkey.c
@@ -503,18 +503,9 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin,
return (ISC_R_SUCCESS);
}
- /*
- * XXXDCL need to check for key expiry per 4.1.1
- * XXXDCL need a way to check fully established, perhaps w/key_flags
- */
-
intoken.base = tkeyin->key;
intoken.length = tkeyin->keylen;
- result = dns_tsigkey_find(&tsigkey, name, &tkeyin->algorithm, ring);
- if (result == ISC_R_SUCCESS)
- gss_ctx = dst_key_getgssctx(tsigkey->key);
-
principal = dns_fixedname_initname(&fixed);
/*
@@ -523,25 +514,27 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin,
result = dst_gssapi_acceptctx(tctx->gsscred, tctx->gssapi_keytab,
&intoken, &outtoken, &gss_ctx,
principal, tctx->mctx);
- if (result == DNS_R_INVALIDTKEY) {
- if (tsigkey != NULL)
- dns_tsigkey_detach(&tsigkey);
+ if (result != ISC_R_SUCCESS) {
tkeyout->error = dns_tsigerror_badkey;
- tkey_log("process_gsstkey(): dns_tsigerror_badkey"); /* XXXSRA */
- return (ISC_R_SUCCESS);
- }
- if (result != DNS_R_CONTINUE && result != ISC_R_SUCCESS)
+ tkey_log("process_gsstkey(): dns_tsigerror_badkey");
+ result = ISC_R_SUCCESS;
goto failure;
+ }
+
/*
- * XXXDCL Section 4.1.3: Limit GSS_S_CONTINUE_NEEDED to 10 times.
+ * Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is
+ * rejected in dst_gssapi_acceptctx(), so if we reach here the
+ * negotiation is complete and the principal must be set.
*/
isc_stdtime_get(&now);
if (dns_name_countlabels(principal) == 0U) {
- if (tsigkey != NULL) {
- dns_tsigkey_detach(&tsigkey);
- }
+ tkeyout->error = dns_tsigerror_badkey;
+ tkey_log("process_gsstkey(): "
+ "completed context with empty principal");
+ result = ISC_R_SUCCESS;
+ goto failure;
} else if (tsigkey == NULL) {
#ifdef GSSAPI
OM_uint32 gret, minor, lifetime;
@@ -612,6 +605,10 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin,
return (ISC_R_SUCCESS);
failure:
+ if (dstkey == NULL && gss_ctx != NULL) {
+ dst_gssapi_deletectx(tctx->mctx, &gss_ctx);
+ }
+
if (tsigkey != NULL)
dns_tsigkey_detach(&tsigkey);
@@ -621,10 +618,10 @@ failure:
if (outtoken != NULL)
isc_buffer_free(&outtoken);
- tkey_log("process_gsstkey(): %s",
- isc_result_totext(result)); /* XXXSRA */
-
- return (result);
+ if (result != ISC_R_SUCCESS) {
+ tkey_log("process_gsstkey(): %s", isc_result_totext(result));
+ }
+ return result;
}
static isc_result_t
@@ -1521,9 +1518,8 @@ dns_tkey_gssnegotiate(dns_message_t *qmsg, dns_message_t *rmsg,
&dstkey, NULL));
/*
- * XXXSRA This seems confused. If we got CONTINUE from initctx,
- * the GSS negotiation hasn't completed yet, so we can't sign
- * anything yet.
+ * GSS negotiation is complete (CONTINUE returned earlier).
+ * Create the TSIG key from the established context.
*/
RETERR(dns_tsigkey_createfromkey(tkeyname,
--
2.54.0

View File

@ -68,7 +68,7 @@ Summary: The Berkeley Internet Name Domain (BIND) DNS (Domain Name System) serv
Name: bind
License: MPLv2.0
Version: 9.11.36
Release: 16%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist}.7
Release: 16%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist}.8
Epoch: 32
Url: https://www.isc.org/downloads/bind/
#
@ -207,6 +207,8 @@ Patch213: bind-9.11-d-max-records-checkconf.patch
Patch214: bind-9.11-CVE-2025-40778.patch
# https://gitlab.isc.org/isc-projects/bind9/commit/e5357c1623da3842227d2c76468b76bc983584d6
Patch215: bind-9.11-CVE-2026-1519.patch
# https://gitlab.isc.org/isc-projects/bind9/commit/94a96c69193587b2c6df30055fbb0c1d00622a7d
Patch216: bind-9.11-CVE-2026-3039.patch
# SDB patches
Patch11: bind-9.3.2b2-sdbsrc.patch
@ -637,6 +639,7 @@ are used for building ISC DHCP.
%patch -P 213 -p1 -b .records-checkconf
%patch -P 214 -p1 -b .CVE-2025-40778
%patch -P 215 -p1 -b .CVE-2026-1519
%patch -P 216 -p1 -b .CVE-2026-3039
mkdir lib/dns/tests/testdata/dstrandom
cp -a %{SOURCE50} lib/dns/tests/testdata/dstrandom/random.data
@ -1689,6 +1692,9 @@ rm -rf ${RPM_BUILD_ROOT}
%endif
%changelog
* Wed May 27 2026 Petr Menšík <pemensik@redhat.com> - 32:9.11.36-16.8
- Fix GSS-API resource leak (CVE-2026-3039)
* Fri Mar 27 2026 Petr Menšík <pemensik@redhat.com> - 32:9.11.36-16.7
- Denial of Service via maliciously crafted DNSSEC-validated zone
(CVE-2026-1519)