bind/bind-9.18-CVE-2026-3039.patch
2026-06-08 00:53:41 -04:00

387 lines
12 KiB
Diff

From 775156d85236e8cc032191f16304af36f30f30c6 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 3d8e0d068f08694282c5ecd3bd6c332de6c75485)
(cherry picked from commit c420039fee2f428032a2f72aa82a4ed2fcc92f31)
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 6c46c85d02849fb659584275313529794039f433)
(cherry picked from commit d273e3def097fbd186f23bcbb146c3ffb4f47301)
%RH:
Some cleanup changes compared to original commits.
---
lib/dns/gssapictx.c | 129 +++++++++++++++++++----------------
lib/dns/include/dst/gssapi.h | 17 ++---
lib/dns/tkey.c | 49 ++++++-------
3 files changed, 99 insertions(+), 96 deletions(-)
diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c
index 06580c2352..abed2384c5 100644
--- a/lib/dns/gssapictx.c
+++ b/lib/dns/gssapictx.c
@@ -100,6 +100,13 @@ static gss_OID_desc __gss_spnego_mechanism_oid_desc = {
goto out; \
} while (0)
+#define CHECK(x) \
+ do { \
+ result = (x); \
+ if (result != ISC_R_SUCCESS) \
+ goto cleanup; \
+ } while (0)
+
static void
name_to_gbuffer(const dns_name_t *name, isc_buffer_t *buffer,
gss_buffer_desc *gbuffer) {
@@ -612,7 +619,14 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken,
GSS_SPNEGO_MECHANISM, flags, 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",
@@ -638,12 +652,6 @@ dst_gssapi_initctx(const 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) {
(void)gss_release_buffer(&minor, &gouttoken);
@@ -654,7 +662,7 @@ out:
isc_result_t
dst_gssapi_acceptctx(dns_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,
dns_gss_ctx_id_t *ctxout, dns_name_t *principal,
isc_mem_t *mctx) {
isc_region_t r;
@@ -667,16 +675,11 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab,
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 HAVE_GSSAPI_GSSAPI_KRB5_H || HAVE_GSSAPI_KRB5_H
gret = gsskrb5_register_acceptor_identity(gssapi_keytab);
@@ -721,8 +724,15 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab,
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:
@@ -735,7 +745,7 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab,
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)));
@@ -746,59 +756,64 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab,
}
if (gouttoken.length > 0U) {
- isc_buffer_allocate(mctx, outtoken,
+ isc_buffer_allocate(mctx, outtokenp,
(unsigned int)gouttoken.length);
GBUFFER_TO_REGION(gouttoken, r);
- RETERR(isc_buffer_copyregion(*outtoken, &r));
+ CHECK(isc_buffer_copyregion(*outtokenp, &r));
(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 cleanup;
+ }
- 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;
- }
+ CHECK(dns_name_fromtext(principal, &namebuf, dns_rootname, 0, NULL));
*ctxout = context;
-out:
+cleanup:
+ 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 494b4b0762..5945bf7637 100644
--- a/lib/dns/include/dst/gssapi.h
+++ b/lib/dns/include/dst/gssapi.h
@@ -113,20 +113,17 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab,
* 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 dns_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 66f4c5de52..679280dd14 100644
--- a/lib/dns/tkey.c
+++ b/lib/dns/tkey.c
@@ -535,19 +535,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);
/*
@@ -556,28 +546,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) {
#if HAVE_GSSAPI
OM_uint32 gret, minor, lifetime;
@@ -640,6 +629,9 @@ 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);
}
@@ -652,9 +644,9 @@ failure:
isc_buffer_free(&outtoken);
}
- tkey_log("process_gsstkey(): %s", isc_result_totext(result)); /* XXXSRA
- */
-
+ if (result != ISC_R_SUCCESS) {
+ tkey_log("process_gsstkey(): %s", isc_result_totext(result));
+ }
return result;
}
@@ -1577,9 +1569,8 @@ dns_tkey_gssnegotiate(dns_message_t *qmsg, dns_message_t *rmsg,
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(
--
2.54.0