bind9/lib/dns/transport.c
Artem Boldariev b109fa9192 Fix TLS certs store deletion on concurrent access
During initialisation or reconfiguration, it is possible that multiple
threads are trying to create a TLS context and associated data (like
TLS certs store) concurrently. In some cases, a thread might be too
late to add newly created data to the TLS contexts cache, in which
case it needs to be discarded. In the code that handles that case, it
was not taken into account that, in some cases, the TLS certs store
could not have been created or should not be deleted, as it is being
managed by the TLS contexts cache already. Deleting the store in such
cases might lead to crashes.

This commit fixes the issue.
2023-12-06 16:01:20 +02:00

741 lines
20 KiB
C

/*
* Copyright (C) Internet Systems Consortium, Inc. ("ISC")
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* See the COPYRIGHT file distributed with this work for additional
* information regarding copyright ownership.
*/
#include <inttypes.h>
#include <isc/hashmap.h>
#include <isc/list.h>
#include <isc/mem.h>
#include <isc/netaddr.h>
#include <isc/refcount.h>
#include <isc/result.h>
#include <isc/rwlock.h>
#include <isc/sockaddr.h>
#include <isc/util.h>
#include <dns/fixedname.h>
#include <dns/name.h>
#include <dns/transport.h>
#define TRANSPORT_MAGIC ISC_MAGIC('T', 'r', 'n', 's')
#define VALID_TRANSPORT(ptr) ISC_MAGIC_VALID(ptr, TRANSPORT_MAGIC)
#define TRANSPORT_LIST_MAGIC ISC_MAGIC('T', 'r', 'L', 's')
#define VALID_TRANSPORT_LIST(ptr) ISC_MAGIC_VALID(ptr, TRANSPORT_LIST_MAGIC)
struct dns_transport_list {
unsigned int magic;
isc_refcount_t references;
isc_mem_t *mctx;
isc_rwlock_t lock;
isc_hashmap_t *transports[DNS_TRANSPORT_COUNT];
};
typedef enum ternary { ter_none = 0, ter_true = 1, ter_false = 2 } ternary_t;
struct dns_transport {
unsigned int magic;
isc_refcount_t references;
isc_mem_t *mctx;
dns_transport_type_t type;
dns_fixedname_t fn;
dns_name_t *name;
struct {
char *tlsname;
char *certfile;
char *keyfile;
char *cafile;
char *remote_hostname;
char *ciphers;
uint32_t protocol_versions;
ternary_t prefer_server_ciphers;
bool always_verify_remote;
} tls;
struct {
char *endpoint;
dns_http_mode_t mode;
} doh;
};
static bool
transport_match(void *node, const void *key) {
dns_transport_t *transport = node;
return (dns_name_equal(transport->name, key));
}
static isc_result_t
list_add(dns_transport_list_t *list, const dns_name_t *name,
const dns_transport_type_t type, dns_transport_t *transport) {
isc_result_t result;
isc_hashmap_t *hm = NULL;
RWLOCK(&list->lock, isc_rwlocktype_write);
hm = list->transports[type];
INSIST(hm != NULL);
transport->name = dns_fixedname_initname(&transport->fn);
dns_name_copy(name, transport->name);
result = isc_hashmap_add(hm, dns_name_hash(name), transport_match, name,
transport, NULL);
RWUNLOCK(&list->lock, isc_rwlocktype_write);
return (result);
}
dns_transport_type_t
dns_transport_get_type(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->type);
}
char *
dns_transport_get_certfile(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->tls.certfile);
}
char *
dns_transport_get_keyfile(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->tls.keyfile);
}
char *
dns_transport_get_cafile(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->tls.cafile);
}
char *
dns_transport_get_remote_hostname(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->tls.remote_hostname);
}
char *
dns_transport_get_endpoint(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->doh.endpoint);
}
dns_http_mode_t
dns_transport_get_mode(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->doh.mode);
}
dns_transport_t *
dns_transport_new(const dns_name_t *name, dns_transport_type_t type,
dns_transport_list_t *list) {
dns_transport_t *transport = isc_mem_get(list->mctx,
sizeof(*transport));
*transport = (dns_transport_t){ .type = type };
isc_refcount_init(&transport->references, 1);
isc_mem_attach(list->mctx, &transport->mctx);
transport->magic = TRANSPORT_MAGIC;
list_add(list, name, type, transport);
return (transport);
}
void
dns_transport_set_certfile(dns_transport_t *transport, const char *certfile) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
if (transport->tls.certfile != NULL) {
isc_mem_free(transport->mctx, transport->tls.certfile);
}
if (certfile != NULL) {
transport->tls.certfile = isc_mem_strdup(transport->mctx,
certfile);
}
}
void
dns_transport_set_keyfile(dns_transport_t *transport, const char *keyfile) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
if (transport->tls.keyfile != NULL) {
isc_mem_free(transport->mctx, transport->tls.keyfile);
}
if (keyfile != NULL) {
transport->tls.keyfile = isc_mem_strdup(transport->mctx,
keyfile);
}
}
void
dns_transport_set_cafile(dns_transport_t *transport, const char *cafile) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
if (transport->tls.cafile != NULL) {
isc_mem_free(transport->mctx, transport->tls.cafile);
}
if (cafile != NULL) {
transport->tls.cafile = isc_mem_strdup(transport->mctx, cafile);
}
}
void
dns_transport_set_remote_hostname(dns_transport_t *transport,
const char *hostname) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
if (transport->tls.remote_hostname != NULL) {
isc_mem_free(transport->mctx, transport->tls.remote_hostname);
}
if (hostname != NULL) {
transport->tls.remote_hostname = isc_mem_strdup(transport->mctx,
hostname);
}
}
void
dns_transport_set_endpoint(dns_transport_t *transport, const char *endpoint) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_HTTP);
if (transport->doh.endpoint != NULL) {
isc_mem_free(transport->mctx, transport->doh.endpoint);
}
if (endpoint != NULL) {
transport->doh.endpoint = isc_mem_strdup(transport->mctx,
endpoint);
}
}
void
dns_transport_set_mode(dns_transport_t *transport, dns_http_mode_t mode) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_HTTP);
transport->doh.mode = mode;
}
void
dns_transport_set_tls_versions(dns_transport_t *transport,
const uint32_t tls_versions) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_HTTP ||
transport->type == DNS_TRANSPORT_TLS);
transport->tls.protocol_versions = tls_versions;
}
uint32_t
dns_transport_get_tls_versions(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->tls.protocol_versions);
}
void
dns_transport_set_ciphers(dns_transport_t *transport, const char *ciphers) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
if (transport->tls.ciphers != NULL) {
isc_mem_free(transport->mctx, transport->tls.ciphers);
}
if (ciphers != NULL) {
transport->tls.ciphers = isc_mem_strdup(transport->mctx,
ciphers);
}
}
void
dns_transport_set_tlsname(dns_transport_t *transport, const char *tlsname) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
if (transport->tls.tlsname != NULL) {
isc_mem_free(transport->mctx, transport->tls.tlsname);
}
if (tlsname != NULL) {
transport->tls.tlsname = isc_mem_strdup(transport->mctx,
tlsname);
}
}
char *
dns_transport_get_ciphers(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->tls.ciphers);
}
char *
dns_transport_get_tlsname(const dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
return (transport->tls.tlsname);
}
void
dns_transport_set_prefer_server_ciphers(dns_transport_t *transport,
const bool prefer) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
transport->tls.prefer_server_ciphers = prefer ? ter_true : ter_false;
}
bool
dns_transport_get_prefer_server_ciphers(const dns_transport_t *transport,
bool *preferp) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(preferp != NULL);
if (transport->tls.prefer_server_ciphers == ter_none) {
return (false);
} else if (transport->tls.prefer_server_ciphers == ter_true) {
*preferp = true;
return (true);
} else if (transport->tls.prefer_server_ciphers == ter_false) {
*preferp = false;
return (true);
}
UNREACHABLE();
return false;
}
void
dns_transport_set_always_verify_remote(dns_transport_t *transport,
const bool always_verify_remote) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
transport->tls.always_verify_remote = always_verify_remote;
}
bool
dns_transport_get_always_verify_remote(dns_transport_t *transport) {
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS ||
transport->type == DNS_TRANSPORT_HTTP);
return transport->tls.always_verify_remote;
}
isc_result_t
dns_transport_get_tlsctx(dns_transport_t *transport, const isc_sockaddr_t *peer,
isc_tlsctx_cache_t *tlsctx_cache, isc_mem_t *mctx,
isc_tlsctx_t **pctx,
isc_tlsctx_client_session_cache_t **psess_cache) {
isc_result_t result = ISC_R_FAILURE;
isc_tlsctx_t *tlsctx = NULL, *found = NULL;
isc_tls_cert_store_t *store = NULL, *found_store = NULL;
isc_tlsctx_client_session_cache_t *sess_cache = NULL;
isc_tlsctx_client_session_cache_t *found_sess_cache = NULL;
uint32_t tls_versions;
const char *ciphers = NULL;
bool prefer_server_ciphers;
uint16_t family;
const char *tlsname = NULL;
REQUIRE(VALID_TRANSPORT(transport));
REQUIRE(transport->type == DNS_TRANSPORT_TLS);
REQUIRE(peer != NULL);
REQUIRE(tlsctx_cache != NULL);
REQUIRE(mctx != NULL);
REQUIRE(pctx != NULL && *pctx == NULL);
REQUIRE(psess_cache != NULL && *psess_cache == NULL);
family = (isc_sockaddr_pf(peer) == PF_INET6) ? AF_INET6 : AF_INET;
tlsname = dns_transport_get_tlsname(transport);
INSIST(tlsname != NULL && *tlsname != '\0');
/*
* Let's try to re-use the already created context. This way
* we have a chance to resume the TLS session, bypassing the
* full TLS handshake procedure, making establishing
* subsequent TLS connections faster.
*/
result = isc_tlsctx_cache_find(tlsctx_cache, tlsname,
isc_tlsctx_cache_tls, family, &found,
&found_store, &found_sess_cache);
if (result != ISC_R_SUCCESS) {
const char *hostname =
dns_transport_get_remote_hostname(transport);
const char *ca_file = dns_transport_get_cafile(transport);
const char *cert_file = dns_transport_get_certfile(transport);
const char *key_file = dns_transport_get_keyfile(transport);
const bool always_verify_remote =
dns_transport_get_always_verify_remote(transport);
char peer_addr_str[INET6_ADDRSTRLEN] = { 0 };
isc_netaddr_t peer_netaddr = { 0 };
bool hostname_ignore_subject;
/*
* So, no context exists. Let's create one using the
* parameters from the configuration file and try to
* store it for further reuse.
*/
result = isc_tlsctx_createclient(&tlsctx);
if (result != ISC_R_SUCCESS) {
goto failure;
}
tls_versions = dns_transport_get_tls_versions(transport);
if (tls_versions != 0) {
isc_tlsctx_set_protocols(tlsctx, tls_versions);
}
ciphers = dns_transport_get_ciphers(transport);
if (ciphers != NULL) {
isc_tlsctx_set_cipherlist(tlsctx, ciphers);
}
if (dns_transport_get_prefer_server_ciphers(
transport, &prefer_server_ciphers))
{
isc_tlsctx_prefer_server_ciphers(tlsctx,
prefer_server_ciphers);
}
if (always_verify_remote || hostname != NULL || ca_file != NULL)
{
/*
* The situation when 'found_store != NULL' while
* 'found == NULL' may occur as there is a one-to-many
* relation between cert stores and per-transport TLS
* contexts. That is, there could be one store
* shared between multiple contexts.
*/
if (found_store == NULL) {
/*
* 'ca_file' can equal 'NULL' here, in
* which case the store with system-wide
* CA certificates will be created.
*/
result = isc_tls_cert_store_create(ca_file,
&store);
if (result != ISC_R_SUCCESS) {
goto failure;
}
} else {
store = found_store;
}
INSIST(store != NULL);
if (hostname == NULL) {
/*
* If hostname is not specified, then use the
* peer IP address for validation.
*/
isc_netaddr_fromsockaddr(&peer_netaddr, peer);
isc_netaddr_format(&peer_netaddr, peer_addr_str,
sizeof(peer_addr_str));
hostname = peer_addr_str;
}
/*
* According to RFC 8310, Subject field MUST NOT
* be inspected when verifying hostname for DoT.
* Only SubjectAltName must be checked.
*/
hostname_ignore_subject = true;
result = isc_tlsctx_enable_peer_verification(
tlsctx, false, store, hostname,
hostname_ignore_subject);
if (result != ISC_R_SUCCESS) {
goto failure;
}
/*
* Let's load client certificate and enable
* Mutual TLS. We do that only in the case when
* Strict TLS is enabled, because Mutual TLS is
* an extension of it.
*/
if (cert_file != NULL) {
INSIST(key_file != NULL);
result = isc_tlsctx_load_certificate(
tlsctx, key_file, cert_file);
if (result != ISC_R_SUCCESS) {
goto failure;
}
}
}
isc_tlsctx_enable_dot_client_alpn(tlsctx);
isc_tlsctx_client_session_cache_create(
mctx, tlsctx,
ISC_TLSCTX_CLIENT_SESSION_CACHE_DEFAULT_SIZE,
&sess_cache);
found_store = NULL;
result = isc_tlsctx_cache_add(tlsctx_cache, tlsname,
isc_tlsctx_cache_tls, family,
tlsctx, store, sess_cache, &found,
&found_store, &found_sess_cache);
if (result == ISC_R_EXISTS) {
/*
* It seems the entry has just been created from
* within another thread while we were initialising
* ours. Although this is unlikely, it could happen
* after startup/re-initialisation. In such a case,
* discard the new context and associated data and use
* the already established one from now on.
*
* Such situation will not occur after the
* initial 'warm-up', so it is not critical
* performance-wise.
*/
INSIST(found != NULL);
isc_tlsctx_free(&tlsctx);
/*
* The 'store' variable can be 'NULL' when remote server
* verification is not enabled (that is, when Strict or
* Mutual TLS are not used).
*
* The 'found_store' might be equal to 'store' as there
* is one-to-many relation between a store and
* per-transport TLS contexts. In that case, the call to
* 'isc_tlsctx_cache_find()' above could have returned a
* store via the 'found_store' variable, whose value we
* can assign to 'store' later. In that case,
* 'isc_tlsctx_cache_add()' will return the same value.
* When that happens, we should not free the store
* object, as it is managed by the TLS context cache.
*/
if (store != NULL && store != found_store) {
isc_tls_cert_store_free(&store);
}
isc_tlsctx_client_session_cache_detach(&sess_cache);
/* Let's return the data from the cache. */
*psess_cache = found_sess_cache;
*pctx = found;
} else {
/*
* Adding the fresh values into the cache has been
* successful, let's return them
*/
INSIST(result == ISC_R_SUCCESS);
*psess_cache = sess_cache;
*pctx = tlsctx;
}
} else {
/*
* The cache lookup has been successful, let's return the
* results.
*/
INSIST(result == ISC_R_SUCCESS);
*psess_cache = found_sess_cache;
*pctx = found;
}
return (ISC_R_SUCCESS);
failure:
if (tlsctx != NULL) {
isc_tlsctx_free(&tlsctx);
}
/*
* The 'found_store' is being managed by the TLS context
* cache. Thus, we should keep it as it is, as it will get
* destroyed alongside the cache. As there is one store per
* multiple TLS contexts, we need to handle store deletion in a
* special way.
*/
if (store != NULL && store != found_store) {
isc_tls_cert_store_free(&store);
}
return (result);
}
static void
transport_destroy(dns_transport_t *transport) {
isc_refcount_destroy(&transport->references);
transport->magic = 0;
if (transport->doh.endpoint != NULL) {
isc_mem_free(transport->mctx, transport->doh.endpoint);
}
if (transport->tls.remote_hostname != NULL) {
isc_mem_free(transport->mctx, transport->tls.remote_hostname);
}
if (transport->tls.cafile != NULL) {
isc_mem_free(transport->mctx, transport->tls.cafile);
}
if (transport->tls.keyfile != NULL) {
isc_mem_free(transport->mctx, transport->tls.keyfile);
}
if (transport->tls.certfile != NULL) {
isc_mem_free(transport->mctx, transport->tls.certfile);
}
if (transport->tls.ciphers != NULL) {
isc_mem_free(transport->mctx, transport->tls.ciphers);
}
if (transport->tls.tlsname != NULL) {
isc_mem_free(transport->mctx, transport->tls.tlsname);
}
isc_mem_putanddetach(&transport->mctx, transport, sizeof(*transport));
}
void
dns_transport_attach(dns_transport_t *source, dns_transport_t **targetp) {
REQUIRE(source != NULL);
REQUIRE(targetp != NULL && *targetp == NULL);
isc_refcount_increment(&source->references);
*targetp = source;
}
void
dns_transport_detach(dns_transport_t **transportp) {
dns_transport_t *transport = NULL;
REQUIRE(transportp != NULL);
REQUIRE(VALID_TRANSPORT(*transportp));
transport = *transportp;
*transportp = NULL;
if (isc_refcount_decrement(&transport->references) == 1) {
transport_destroy(transport);
}
}
dns_transport_t *
dns_transport_find(const dns_transport_type_t type, const dns_name_t *name,
dns_transport_list_t *list) {
isc_result_t result;
dns_transport_t *transport = NULL;
isc_hashmap_t *hm = NULL;
REQUIRE(VALID_TRANSPORT_LIST(list));
REQUIRE(list->transports[type] != NULL);
hm = list->transports[type];
RWLOCK(&list->lock, isc_rwlocktype_read);
result = isc_hashmap_find(hm, dns_name_hash(name), transport_match,
name, (void **)&transport);
if (result == ISC_R_SUCCESS) {
isc_refcount_increment(&transport->references);
}
RWUNLOCK(&list->lock, isc_rwlocktype_read);
return (transport);
}
dns_transport_list_t *
dns_transport_list_new(isc_mem_t *mctx) {
dns_transport_list_t *list = isc_mem_get(mctx, sizeof(*list));
*list = (dns_transport_list_t){ 0 };
isc_rwlock_init(&list->lock);
isc_mem_attach(mctx, &list->mctx);
isc_refcount_init(&list->references, 1);
list->magic = TRANSPORT_LIST_MAGIC;
for (size_t type = 0; type < DNS_TRANSPORT_COUNT; type++) {
isc_hashmap_create(list->mctx, 10, &list->transports[type]);
}
return (list);
}
void
dns_transport_list_attach(dns_transport_list_t *source,
dns_transport_list_t **targetp) {
REQUIRE(VALID_TRANSPORT_LIST(source));
REQUIRE(targetp != NULL && *targetp == NULL);
isc_refcount_increment(&source->references);
*targetp = source;
}
static void
transport_list_destroy(dns_transport_list_t *list) {
isc_refcount_destroy(&list->references);
list->magic = 0;
for (size_t type = 0; type < DNS_TRANSPORT_COUNT; type++) {
isc_result_t result;
isc_hashmap_iter_t *it = NULL;
if (list->transports[type] == NULL) {
continue;
}
isc_hashmap_iter_create(list->transports[type], &it);
for (result = isc_hashmap_iter_first(it);
result == ISC_R_SUCCESS;
result = isc_hashmap_iter_delcurrent_next(it))
{
dns_transport_t *transport = NULL;
isc_hashmap_iter_current(it, (void **)&transport);
dns_transport_detach(&transport);
}
isc_hashmap_iter_destroy(&it);
isc_hashmap_destroy(&list->transports[type]);
}
isc_rwlock_destroy(&list->lock);
isc_mem_putanddetach(&list->mctx, list, sizeof(*list));
}
void
dns_transport_list_detach(dns_transport_list_t **listp) {
dns_transport_list_t *list = NULL;
REQUIRE(listp != NULL);
REQUIRE(VALID_TRANSPORT_LIST(*listp));
list = *listp;
*listp = NULL;
if (isc_refcount_decrement(&list->references) == 1) {
transport_list_destroy(list);
}
}