749169
From 8cb69a3657064ff6bb90a208cfad5fb91e30c307 Mon Sep 17 00:00:00 2001
d7027e
From: Greg Hudson <ghudson@mit.edu>
d7027e
Date: Sun, 1 Jul 2018 00:12:25 -0400
d7027e
Subject: [PATCH] Fix bugs with concurrent use of MEMORY ccaches
d7027e
d7027e
A memory ccache iterator stores an alias into the cache object's
d7027e
linked list of credentials.  If the cache is reinitialized while the
d7027e
iterator is active, the alias becomes invalid.  Also, multiple handles
d7027e
referencing the same memory ccache all use aliases to the same data
d7027e
object; if one of the handles is destroyed, the other contains a
d7027e
dangling pointer.
d7027e
d7027e
Fix the first issue by adding a generation counter to the cache and to
d7027e
cursors, incremented each time the cache is initialized or destroyed.
d7027e
Check the generation on each cursor step and end the iteration if the
d7027e
list was invalidated.  Fix the second issue by adding a reference
d7027e
count to the cache object, counting one reference for the table slot
d7027e
and one for each open handle.  Empty the cache object on each destroy
d7027e
operation, but only release the object when the last handle to it is
d7027e
destroyed or closed.
d7027e
d7027e
Add regression tests for the two issues to t_cc.c.
d7027e
d7027e
The first issue was reported by Sorin Manolache.
d7027e
d7027e
ticket: 8202
d7027e
tags: pullup
d7027e
target_version: 1.16-next
d7027e
target_version: 1.15-next
d7027e
d7027e
(cherry picked from commit 146dadec8fe7ccc4149eb2e3f577cc320aee6efb)
d7027e
---
d7027e
 src/lib/krb5/ccache/cc_memory.c | 164 ++++++++++++++++++++------------
d7027e
 src/lib/krb5/ccache/t_cc.c      |  51 ++++++++++
d7027e
 2 files changed, 154 insertions(+), 61 deletions(-)
d7027e
d7027e
diff --git a/src/lib/krb5/ccache/cc_memory.c b/src/lib/krb5/ccache/cc_memory.c
d7027e
index c5425eb3a..8cdaff7fb 100644
d7027e
--- a/src/lib/krb5/ccache/cc_memory.c
d7027e
+++ b/src/lib/krb5/ccache/cc_memory.c
d7027e
@@ -102,18 +102,20 @@ extern krb5_error_code krb5_change_cache (void);
d7027e
 typedef struct _krb5_mcc_link {
d7027e
     struct _krb5_mcc_link *next;
d7027e
     krb5_creds *creds;
d7027e
-} krb5_mcc_link, *krb5_mcc_cursor;
d7027e
+} krb5_mcc_link;
d7027e
 
d7027e
 /* Per-cache data header.  */
d7027e
 typedef struct _krb5_mcc_data {
d7027e
     char *name;
d7027e
     k5_cc_mutex lock;
d7027e
     krb5_principal prin;
d7027e
-    krb5_mcc_cursor link;
d7027e
+    krb5_mcc_link *link;
d7027e
     krb5_timestamp changetime;
d7027e
     /* Time offsets for clock-skewed clients.  */
d7027e
     krb5_int32 time_offset;
d7027e
     krb5_int32 usec_offset;
d7027e
+    int refcount;               /* One for the table slot, one per handle */
d7027e
+    int generation;             /* Incremented at each initialize */
d7027e
 } krb5_mcc_data;
d7027e
 
d7027e
 /* List of memory caches.  */
d7027e
@@ -122,6 +124,12 @@ typedef struct krb5_mcc_list_node {
d7027e
     krb5_mcc_data *cache;
d7027e
 } krb5_mcc_list_node;
d7027e
 
d7027e
+/* Iterator over credentials in a memory cache. */
d7027e
+struct mcc_cursor {
d7027e
+    int generation;
d7027e
+    krb5_mcc_link *next_link;
d7027e
+};
d7027e
+
d7027e
 /* Iterator over memory caches.  */
d7027e
 struct krb5_mcc_ptcursor_data {
d7027e
     struct krb5_mcc_list_node *cur;
d7027e
@@ -132,7 +140,23 @@ static krb5_mcc_list_node *mcc_head = 0;
d7027e
 
d7027e
 static void update_mcc_change_time(krb5_mcc_data *);
d7027e
 
d7027e
-static void krb5_mcc_free (krb5_context context, krb5_ccache id);
d7027e
+/* Remove creds from d, invalidate any existing cursors, and unset the client
d7027e
+ * principal.  The caller is responsible for locking. */
d7027e
+static void
d7027e
+empty_mcc_cache(krb5_context context, krb5_mcc_data *d)
d7027e
+{
d7027e
+    krb5_mcc_link *curr, *next;
d7027e
+
d7027e
+    for (curr = d->link; curr != NULL; curr = next) {
d7027e
+        next = curr->next;
d7027e
+        krb5_free_creds(context, curr->creds);
d7027e
+        free(curr);
d7027e
+    }
d7027e
+    d->link = NULL;
d7027e
+    d->generation++;
d7027e
+    krb5_free_principal(context, d->prin);
d7027e
+    d->prin = NULL;
d7027e
+}
d7027e
 
d7027e
 /*
d7027e
  * Modifies:
d7027e
@@ -150,16 +174,12 @@ krb5_mcc_initialize(krb5_context context, krb5_ccache id, krb5_principal princ)
d7027e
 {
d7027e
     krb5_os_context os_ctx = &context->os_context;
d7027e
     krb5_error_code ret;
d7027e
-    krb5_mcc_data *d;
d7027e
+    krb5_mcc_data *d = id->data;
d7027e
 
d7027e
-    d = (krb5_mcc_data *)id->data;
d7027e
     k5_cc_mutex_lock(context, &d->lock);
d7027e
+    empty_mcc_cache(context, d);
d7027e
 
d7027e
-    krb5_mcc_free(context, id);
d7027e
-
d7027e
-    d = (krb5_mcc_data *)id->data;
d7027e
-    ret = krb5_copy_principal(context, princ,
d7027e
-                              &d->prin);
d7027e
+    ret = krb5_copy_principal(context, princ, &d->prin);
d7027e
     update_mcc_change_time(d);
d7027e
 
d7027e
     if (os_ctx->os_flags & KRB5_OS_TOFFSET_VALID) {
d7027e
@@ -185,61 +205,59 @@ krb5_mcc_initialize(krb5_context context, krb5_ccache id, krb5_principal princ)
d7027e
 krb5_error_code KRB5_CALLCONV
d7027e
 krb5_mcc_close(krb5_context context, krb5_ccache id)
d7027e
 {
d7027e
+    krb5_mcc_data *d = id->data;
d7027e
+    int count;
d7027e
+
d7027e
     free(id);
d7027e
-    return KRB5_OK;
d7027e
-}
d7027e
-
d7027e
-static void
d7027e
-krb5_mcc_free(krb5_context context, krb5_ccache id)
d7027e
-{
d7027e
-    krb5_mcc_cursor curr,next;
d7027e
-    krb5_mcc_data *d;
d7027e
-
d7027e
-    d = (krb5_mcc_data *) id->data;
d7027e
-    for (curr = d->link; curr;) {
d7027e
-        krb5_free_creds(context, curr->creds);
d7027e
-        next = curr->next;
d7027e
-        free(curr);
d7027e
-        curr = next;
d7027e
+    k5_cc_mutex_lock(context, &d->lock);
d7027e
+    count = --d->refcount;
d7027e
+    k5_cc_mutex_unlock(context, &d->lock);
d7027e
+    if (count == 0) {
d7027e
+        /* This is the last active handle referencing d and d has been removed
d7027e
+         * from the table, so we can release it. */
d7027e
+        empty_mcc_cache(context, d);
d7027e
+        free(d->name);
d7027e
+        k5_cc_mutex_destroy(&d->lock);
d7027e
+        free(d);
d7027e
     }
d7027e
-    d->link = NULL;
d7027e
-    krb5_free_principal(context, d->prin);
d7027e
+    return KRB5_OK;
d7027e
 }
d7027e
 
d7027e
 /*
d7027e
  * Effects:
d7027e
  * Destroys the contents of id. id is invalid after call.
d7027e
- *
d7027e
- * Errors:
d7027e
- * system errors (locks related)
d7027e
  */
d7027e
 krb5_error_code KRB5_CALLCONV
d7027e
 krb5_mcc_destroy(krb5_context context, krb5_ccache id)
d7027e
 {
d7027e
     krb5_mcc_list_node **curr, *node;
d7027e
-    krb5_mcc_data *d;
d7027e
+    krb5_mcc_data *d = id->data;
d7027e
+    krb5_boolean removed_from_table = FALSE;
d7027e
 
d7027e
     k5_cc_mutex_lock(context, &krb5int_mcc_mutex);
d7027e
 
d7027e
-    d = (krb5_mcc_data *)id->data;
d7027e
     for (curr = &mcc_head; *curr; curr = &(*curr)->next) {
d7027e
         if ((*curr)->cache == d) {
d7027e
             node = *curr;
d7027e
             *curr = node->next;
d7027e
             free(node);
d7027e
+            removed_from_table = TRUE;
d7027e
             break;
d7027e
         }
d7027e
     }
d7027e
     k5_cc_mutex_unlock(context, &krb5int_mcc_mutex);
d7027e
 
d7027e
+    /* Empty the cache and remove the reference for the table slot.  There will
d7027e
+     * always be at least one reference left for the handle being destroyed. */
d7027e
     k5_cc_mutex_lock(context, &d->lock);
d7027e
-
d7027e
-    krb5_mcc_free(context, id);
d7027e
-    free(d->name);
d7027e
+    empty_mcc_cache(context, d);
d7027e
+    if (removed_from_table)
d7027e
+        d->refcount--;
d7027e
     k5_cc_mutex_unlock(context, &d->lock);
d7027e
-    k5_cc_mutex_destroy(&d->lock);
d7027e
-    free(d);
d7027e
-    free(id);
d7027e
+
d7027e
+    /* Invalidate the handle, possibly removing the last reference to d and
d7027e
+     * freeing it. */
d7027e
+    krb5_mcc_close(context, id);
d7027e
 
d7027e
     krb5_change_cache ();
d7027e
     return KRB5_OK;
d7027e
@@ -279,9 +297,12 @@ krb5_mcc_resolve (krb5_context context, krb5_ccache *id, const char *residual)
d7027e
     for (ptr = mcc_head; ptr; ptr=ptr->next)
d7027e
         if (!strcmp(ptr->cache->name, residual))
d7027e
             break;
d7027e
-    if (ptr)
d7027e
+    if (ptr != NULL) {
d7027e
         d = ptr->cache;
d7027e
-    else {
d7027e
+        k5_cc_mutex_lock(context, &d->lock);
d7027e
+        d->refcount++;
d7027e
+        k5_cc_mutex_unlock(context, &d->lock);
d7027e
+    } else {
d7027e
         err = new_mcc_data(residual, &d);
d7027e
         if (err) {
d7027e
             k5_cc_mutex_unlock(context, &krb5int_mcc_mutex);
d7027e
@@ -326,14 +347,18 @@ krb5_error_code KRB5_CALLCONV
d7027e
 krb5_mcc_start_seq_get(krb5_context context, krb5_ccache id,
d7027e
                        krb5_cc_cursor *cursor)
d7027e
 {
d7027e
-    krb5_mcc_cursor mcursor;
d7027e
+    struct mcc_cursor *mcursor;
d7027e
     krb5_mcc_data *d;
d7027e
 
d7027e
+    mcursor = malloc(sizeof(*mcursor));
d7027e
+    if (mcursor == NULL)
d7027e
+        return KRB5_CC_NOMEM;
d7027e
     d = id->data;
d7027e
     k5_cc_mutex_lock(context, &d->lock);
d7027e
-    mcursor = d->link;
d7027e
+    mcursor->generation = d->generation;
d7027e
+    mcursor->next_link = d->link;
d7027e
     k5_cc_mutex_unlock(context, &d->lock);
d7027e
-    *cursor = (krb5_cc_cursor) mcursor;
d7027e
+    *cursor = mcursor;
d7027e
     return KRB5_OK;
d7027e
 }
d7027e
 
d7027e
@@ -361,23 +386,34 @@ krb5_error_code KRB5_CALLCONV
d7027e
 krb5_mcc_next_cred(krb5_context context, krb5_ccache id,
d7027e
                    krb5_cc_cursor *cursor, krb5_creds *creds)
d7027e
 {
d7027e
-    krb5_mcc_cursor mcursor;
d7027e
+    struct mcc_cursor *mcursor;
d7027e
     krb5_error_code retval;
d7027e
+    krb5_mcc_data *d = id->data;
d7027e
 
d7027e
-    /* Once the node in the linked list is created, it's never
d7027e
-       modified, so we don't need to worry about locking here.  (Note
d7027e
-       that we don't support _remove_cred.)  */
d7027e
-    mcursor = (krb5_mcc_cursor) *cursor;
d7027e
-    if (mcursor == NULL)
d7027e
-        return KRB5_CC_END;
d7027e
     memset(creds, 0, sizeof(krb5_creds));
d7027e
-    if (mcursor->creds) {
d7027e
-        retval = k5_copy_creds_contents(context, mcursor->creds, creds);
d7027e
-        if (retval)
d7027e
-            return retval;
d7027e
+    mcursor = *cursor;
d7027e
+    if (mcursor->next_link == NULL)
d7027e
+        return KRB5_CC_END;
d7027e
+
d7027e
+    /*
d7027e
+     * Check the cursor generation against the cache generation in case the
d7027e
+     * cache has been reinitialized or destroyed, freeing the pointer in the
d7027e
+     * cursor.  Keep the cache locked while we copy the creds and advance the
d7027e
+     * pointer, in case another thread reinitializes the cache after we check
d7027e
+     * the generation.
d7027e
+     */
d7027e
+    k5_cc_mutex_lock(context, &d->lock);
d7027e
+    if (mcursor->generation != d->generation) {
d7027e
+        k5_cc_mutex_unlock(context, &d->lock);
d7027e
+        return KRB5_CC_END;
d7027e
     }
d7027e
-    *cursor = (krb5_cc_cursor)mcursor->next;
d7027e
-    return KRB5_OK;
d7027e
+
d7027e
+    retval = k5_copy_creds_contents(context, mcursor->next_link->creds, creds);
d7027e
+    if (retval == 0)
d7027e
+        mcursor->next_link = mcursor->next_link->next;
d7027e
+
d7027e
+    k5_cc_mutex_unlock(context, &d->lock);
d7027e
+    return retval;
d7027e
 }
d7027e
 
d7027e
 /*
d7027e
@@ -396,14 +432,18 @@ krb5_mcc_next_cred(krb5_context context, krb5_ccache id,
d7027e
 krb5_error_code KRB5_CALLCONV
d7027e
 krb5_mcc_end_seq_get(krb5_context context, krb5_ccache id, krb5_cc_cursor *cursor)
d7027e
 {
d7027e
-    *cursor = 0L;
d7027e
+    free(*cursor);
d7027e
+    *cursor = NULL;
d7027e
     return KRB5_OK;
d7027e
 }
d7027e
 
d7027e
-/* Utility routine: Creates the back-end data for a memory cache, and
d7027e
-   threads it into the global linked list.
d7027e
-
d7027e
-   Call with the global list lock held.  */
d7027e
+/*
d7027e
+ * Utility routine: Creates the back-end data for a memory cache, and threads
d7027e
+ * it into the global linked list.  Give the new object two references, one for
d7027e
+ * the table slot and one for the caller's handle.
d7027e
+ *
d7027e
+ * Call with the global list lock held.
d7027e
+ */
d7027e
 static krb5_error_code
d7027e
 new_mcc_data (const char *name, krb5_mcc_data **dataptr)
d7027e
 {
d7027e
@@ -432,6 +472,8 @@ new_mcc_data (const char *name, krb5_mcc_data **dataptr)
d7027e
     d->changetime = 0;
d7027e
     d->time_offset = 0;
d7027e
     d->usec_offset = 0;
d7027e
+    d->refcount = 2;
d7027e
+    d->generation = 0;
d7027e
     update_mcc_change_time(d);
d7027e
 
d7027e
     n = malloc(sizeof(krb5_mcc_list_node));
d7027e
diff --git a/src/lib/krb5/ccache/t_cc.c b/src/lib/krb5/ccache/t_cc.c
d7027e
index 6069cabd3..cd4569c4c 100644
d7027e
--- a/src/lib/krb5/ccache/t_cc.c
d7027e
+++ b/src/lib/krb5/ccache/t_cc.c
d7027e
@@ -386,6 +386,55 @@ test_misc(krb5_context context)
d7027e
     krb5_cc_dfl_ops = ops_save;
d7027e
 
d7027e
 }
d7027e
+
d7027e
+/*
d7027e
+ * Regression tests for #8202.  Because memory ccaches share objects between
d7027e
+ * different handles to the same cache and between iterators and caches,
d7027e
+ * historically there have been some bugs when those objects are released.
d7027e
+ */
d7027e
+static void
d7027e
+test_memory_concurrent(krb5_context context)
d7027e
+{
d7027e
+    krb5_error_code kret;
d7027e
+    krb5_ccache id1, id2;
d7027e
+    krb5_cc_cursor cursor;
d7027e
+    krb5_creds creds;
d7027e
+
d7027e
+    /* Create two handles to the same memory ccache and destroy them. */
d7027e
+    kret = krb5_cc_resolve(context, "MEMORY:x", &id1);
d7027e
+    CHECK(kret, "resolve 1");
d7027e
+    kret = krb5_cc_resolve(context, "MEMORY:x", &id2);
d7027e
+    CHECK(kret, "resolve 2");
d7027e
+    kret = krb5_cc_destroy(context, id1);
d7027e
+    CHECK(kret, "destroy 1");
d7027e
+    kret = krb5_cc_destroy(context, id2);
d7027e
+    CHECK(kret, "destroy 2");
d7027e
+
d7027e
+    kret = init_test_cred(context);
d7027e
+    CHECK(kret, "init_creds");
d7027e
+
d7027e
+    /* Reinitialize the cache after creating an iterator for it, and verify
d7027e
+     * that the iterator ends gracefully. */
d7027e
+    kret = krb5_cc_resolve(context, "MEMORY:x", &id1);
d7027e
+    CHECK(kret, "resolve");
d7027e
+    kret = krb5_cc_initialize(context, id1, test_creds.client);
d7027e
+    CHECK(kret, "initialize");
d7027e
+    kret = krb5_cc_store_cred(context, id1, &test_creds);
d7027e
+    CHECK(kret, "store");
d7027e
+    kret = krb5_cc_start_seq_get(context, id1, &cursor);
d7027e
+    CHECK(kret, "start_seq_get");
d7027e
+    kret = krb5_cc_initialize(context, id1, test_creds.client);
d7027e
+    CHECK(kret, "initialize again");
d7027e
+    kret = krb5_cc_next_cred(context, id1, &cursor, &creds);
d7027e
+    CHECK_BOOL(kret != KRB5_CC_END, "iterator should end", "next_cred");
d7027e
+    kret = krb5_cc_end_seq_get(context, id1, &cursor);
d7027e
+    CHECK(kret, "end_seq_get");
d7027e
+    kret = krb5_cc_destroy(context, id1);
d7027e
+    CHECK(kret, "destroy");
d7027e
+
d7027e
+    free_test_cred(context);
d7027e
+}
d7027e
+
d7027e
 extern const krb5_cc_ops krb5_mcc_ops;
d7027e
 extern const krb5_cc_ops krb5_fcc_ops;
d7027e
 
d7027e
@@ -434,6 +483,8 @@ main(void)
d7027e
     do_test(context, "MEMORY:");
d7027e
     do_test(context, "FILE:");
d7027e
 
d7027e
+    test_memory_concurrent(context);
d7027e
+
d7027e
     krb5_free_context(context);
d7027e
     return 0;
d7027e
 }