diff --git a/lib/krb5/Makefile.am b/lib/krb5/Makefile.am index 4e45556d4..6c4718a75 100644 --- a/lib/krb5/Makefile.am +++ b/lib/krb5/Makefile.am @@ -50,6 +50,7 @@ TESTS = \ test_pknistkdf \ test_time \ test_expand_toks \ + test_mcache \ test_x500 check_DATA = test_config_strings.out @@ -68,6 +69,8 @@ else test_cc_LDADD = $(LDADD) endif +test_mcache_LDADD = $(LDADD) $(PTHREAD_LIBADD) + aes_test_LDADD = $(LDADD) $(LIB_openssl_crypto) if PKINIT diff --git a/lib/krb5/mcache.c b/lib/krb5/mcache.c index 80b510757..c4e176ef6 100644 --- a/lib/krb5/mcache.c +++ b/lib/krb5/mcache.c @@ -40,6 +40,7 @@ typedef struct krb5_mcache { unsigned int refcnt; unsigned int anonymous:1; unsigned int dead:1; + unsigned int gen; krb5_principal primary_principal; struct link { krb5_creds cred; @@ -51,6 +52,11 @@ typedef struct krb5_mcache { HEIMDAL_MUTEX mutex; } krb5_mcache; +struct mcc_cursor { + struct link *next; + unsigned int gen; +}; + static HEIMDAL_MUTEX mcc_mutex = HEIMDAL_MUTEX_INITIALIZER; static struct krb5_mcache *mcc_head; @@ -232,6 +238,7 @@ mcc_initialize(krb5_context context, * MEMORY: backend in MIT. */ mcc_destroy_internal(context, m); + m->gen++; m->dead = 0; m->kdc_offset = context->kdc_sec_offset; m->mtime = time(NULL); @@ -333,8 +340,10 @@ mcc_store_cred(krb5_context context, } l = malloc (sizeof(*l)); - if (l == NULL) + if (l == NULL) { + HEIMDAL_MUTEX_unlock(&(m->mutex)); return krb5_enomem(context); + } l->next = m->creds; m->creds = l; memset (&l->cred, 0, sizeof(l->cred)); @@ -345,8 +354,9 @@ mcc_store_cred(krb5_context context, HEIMDAL_MUTEX_unlock(&(m->mutex)); return ret; } + m->gen++; m->mtime = time(NULL); - HEIMDAL_MUTEX_unlock(&(m->mutex)); + HEIMDAL_MUTEX_unlock(&(m->mutex)); return 0; } @@ -376,13 +386,21 @@ mcc_get_first (krb5_context context, krb5_cc_cursor *cursor) { krb5_mcache *m = MCACHE(id); + struct mcc_cursor *c; + + c = calloc(1, sizeof(*c)); + if (c == NULL) + return krb5_enomem(context); HEIMDAL_MUTEX_lock(&(m->mutex)); if (MISDEAD(m)) { HEIMDAL_MUTEX_unlock(&(m->mutex)); + free(c); return ENOENT; } - *cursor = m->creds; + c->gen = m->gen; + c->next = m->creds; + *cursor = c; HEIMDAL_MUTEX_unlock(&(m->mutex)); return 0; @@ -395,23 +413,23 @@ mcc_get_next (krb5_context context, krb5_creds *creds) { krb5_mcache *m = MCACHE(id); - struct link *l; + struct mcc_cursor *c = *cursor; + krb5_error_code ret; HEIMDAL_MUTEX_lock(&(m->mutex)); - if (MISDEAD(m)) { + if (MISDEAD(m) || c->gen != m->gen) { HEIMDAL_MUTEX_unlock(&(m->mutex)); return ENOENT; } - HEIMDAL_MUTEX_unlock(&(m->mutex)); - l = *cursor; - if (l != NULL) { - *cursor = l->next; - return krb5_copy_creds_contents (context, - &l->cred, - creds); - } else - return KRB5_CC_END; + if (c->next != NULL) { + ret = krb5_copy_creds_contents(context, &c->next->cred, creds); + c->next = c->next->next; + HEIMDAL_MUTEX_unlock(&(m->mutex)); + return ret; + } + HEIMDAL_MUTEX_unlock(&(m->mutex)); + return KRB5_CC_END; } static krb5_error_code KRB5_CALLCONV @@ -419,6 +437,8 @@ mcc_end_get (krb5_context context, krb5_ccache id, krb5_cc_cursor *cursor) { + free(*cursor); + *cursor = NULL; return 0; } @@ -438,6 +458,7 @@ mcc_remove_cred(krb5_context context, *q = p->next; krb5_free_cred_contents(context, &p->cred); free(p); + m->gen++; m->mtime = time(NULL); } else q = &p->next; diff --git a/lib/krb5/test_mcache.c b/lib/krb5/test_mcache.c new file mode 100644 index 000000000..fb3544661 --- /dev/null +++ b/lib/krb5/test_mcache.c @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2024 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * Threaded torture test for MEMORY ccache type. + * + * Creates NPROC * 1.5 threads that perform racy operations on MEMORY caches, + * testing both named (MEMORY:test) and anonymous (MEMORY:anonymous) caches. + */ + +#include "krb5_locl.h" +#include +#include +#include + +#define ITERATIONS 1000 + +static int verbose_flag = 0; +static int version_flag = 0; +static int help_flag = 0; + +struct thread_ctx { + int thread_id; + int use_anonymous; + int iterations; + volatile int *stop; +}; + +static void * +torture_thread(void *arg) +{ + struct thread_ctx *ctx = arg; + krb5_context context; + krb5_ccache cc = NULL; + krb5_error_code ret; + krb5_principal client; + const char *ccname; + int i; + + ret = krb5_init_context(&context); + if (ret) + errx(1, "thread %d: krb5_init_context failed: %d", ctx->thread_id, ret); + + ret = krb5_parse_name(context, "test@EXAMPLE.COM", &client); + if (ret) + krb5_err(context, 1, ret, "thread %d: krb5_parse_name", ctx->thread_id); + + ccname = ctx->use_anonymous ? "MEMORY:anonymous" : "MEMORY:test"; + + for (i = 0; i < ctx->iterations && !*ctx->stop; i++) { + char config_name[64]; + krb5_data config_data; + krb5_data retrieved_data; + int op; + + ret = krb5_cc_resolve(context, ccname, &cc); + if (ret) { + if (verbose_flag) + krb5_warn(context, ret, "thread %d iter %d: krb5_cc_resolve", + ctx->thread_id, i); + continue; + } + + ret = krb5_cc_initialize(context, cc, client); + if (ret) { + if (verbose_flag) + krb5_warn(context, ret, "thread %d iter %d: krb5_cc_initialize", + ctx->thread_id, i); + krb5_cc_close(context, cc); + cc = NULL; + continue; + } + + op = i % 4; + + switch (op) { + case 0: + case 1: { + /* Store config data */ + static const char test_value[] = "test-value"; + snprintf(config_name, sizeof(config_name), "test-config-%d", + ctx->thread_id); + config_data.data = rk_UNCONST(test_value); + config_data.length = sizeof(test_value) - 1; + + ret = krb5_cc_set_config(context, cc, NULL, config_name, + &config_data); + if (ret && verbose_flag) + krb5_warn(context, ret, + "thread %d iter %d: krb5_cc_set_config", + ctx->thread_id, i); + break; + } + + case 2: + /* Retrieve config data */ + snprintf(config_name, sizeof(config_name), "test-config-%d", + ctx->thread_id); + krb5_data_zero(&retrieved_data); + + ret = krb5_cc_get_config(context, cc, NULL, config_name, + &retrieved_data); + if (ret == 0) + krb5_data_free(&retrieved_data); + else if (ret != KRB5_CC_NOTFOUND && ret != KRB5_CC_END && + verbose_flag) + krb5_warn(context, ret, + "thread %d iter %d: krb5_cc_get_config", + ctx->thread_id, i); + break; + + case 3: + /* Remove config (by setting NULL data) */ + snprintf(config_name, sizeof(config_name), "test-config-%d", + ctx->thread_id); + + ret = krb5_cc_set_config(context, cc, NULL, config_name, NULL); + if (ret && ret != KRB5_CC_NOTFOUND && verbose_flag) + krb5_warn(context, ret, + "thread %d iter %d: krb5_cc_set_config (remove)", + ctx->thread_id, i); + break; + } + + krb5_cc_close(context, cc); + cc = NULL; + } + + krb5_free_principal(context, client); + krb5_free_context(context); + + if (verbose_flag) + printf("thread %d (%s): completed %d iterations\n", + ctx->thread_id, + ctx->use_anonymous ? "anonymous" : "named", + i); + + return NULL; +} + +static struct getargs args[] = { + { "verbose", 'v', arg_flag, &verbose_flag, "verbose", NULL }, + { "version", 0, arg_flag, &version_flag, "print version", NULL }, + { "help", 0, arg_flag, &help_flag, NULL, NULL } +}; + +static void +usage(int exitval) +{ + arg_printusage(args, sizeof(args) / sizeof(args[0]), NULL, ""); + exit(exitval); +} + +int +main(int argc, char **argv) +{ + int nthreads; + int nproc; + pthread_t *threads; + struct thread_ctx *contexts; + volatile int stop = 0; + int i; + int optidx = 0; + + setprogname(argv[0]); + + if (getarg(args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx)) + usage(1); + + if (help_flag) + usage(0); + + if (version_flag) { + printf("test_mcache (Heimdal)\n"); + return 0; + } + +#ifdef _SC_NPROCESSORS_ONLN + nproc = sysconf(_SC_NPROCESSORS_ONLN); + if (nproc < 1) + nproc = 4; +#else + nproc = 4; +#endif + + nthreads = nproc + nproc / 2; + if (nthreads < 4) + nthreads = 4; + + printf("Running with %d threads (%d processors)\n", nthreads, nproc); + + threads = calloc(nthreads, sizeof(*threads)); + contexts = calloc(nthreads, sizeof(*contexts)); + if (threads == NULL || contexts == NULL) + errx(1, "out of memory"); + + for (i = 0; i < nthreads; i++) { + contexts[i].thread_id = i; + contexts[i].use_anonymous = (i % 2 == 1); + contexts[i].iterations = ITERATIONS; + contexts[i].stop = &stop; + } + + for (i = 0; i < nthreads; i++) { + int ret = pthread_create(&threads[i], NULL, torture_thread, + &contexts[i]); + if (ret) + errx(1, "pthread_create failed: %d", ret); + } + + for (i = 0; i < nthreads; i++) { + void *res; + int ret = pthread_join(threads[i], &res); + if (ret) + errx(1, "pthread_join failed: %d", ret); + } + + printf("All threads completed successfully\n"); + + free(threads); + free(contexts); + + return 0; +}