Blame SOURCES/Fix-bugs-with-concurrent-use-of-MEMORY-ccaches.patch

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