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