d86ed3a
From f1db05d8839b39fd48471dcb29881c12ed27a434 Mon Sep 17 00:00:00 2001
d86ed3a
From: =?UTF-8?q?Pavel=20B=C5=99ezina?= <pbrezina@redhat.com>
d86ed3a
Date: Thu, 29 Oct 2020 14:57:53 +0100
d86ed3a
Subject: [PATCH 14/19] sss_ptr_hash: fix double free for circular dependencies
d86ed3a
d86ed3a
If the hash table delete callback deletes the stored item,
d86ed3a
we can end up in double free in case when we try to override
d86ed3a
an existing item (hash_enter(key) where key already exists).
d86ed3a
d86ed3a
```c
d86ed3a
static void delete_cb(hash_entry_t *item,
d86ed3a
                      hash_destroy_enum deltype,
d86ed3a
                      void *pvt)
d86ed3a
{
d86ed3a
    talloc_free(item->value.ptr);
d86ed3a
}
d86ed3a
d86ed3a
hash_enter(key);
d86ed3a
hash_enter(key);
d86ed3a
```
d86ed3a
d86ed3a
The doble free it self is fine, since it is done via talloc destructor
d86ed3a
and talloc can cope with that. However, the hash table fails to store
d86ed3a
the new entry because hash_delete is called twice.
d86ed3a
d86ed3a
```
d86ed3a
_sss_ptr_hash_add -> hash_enter -> hash_delete(old) -> delete_cb -> sss_ptr_hash_value_destructor -> hash_delete
d86ed3a
```
d86ed3a
---
d86ed3a
 src/tests/cmocka/test_sss_ptr_hash.c | 39 ++++++++++++++++++++++++++++
d86ed3a
 src/tests/cmocka/test_utils.c        |  3 +++
d86ed3a
 src/tests/cmocka/test_utils.h        |  1 +
d86ed3a
 src/util/sss_ptr_hash.c              | 20 ++++++++++++++
d86ed3a
 4 files changed, 63 insertions(+)
d86ed3a
d86ed3a
diff --git a/src/tests/cmocka/test_sss_ptr_hash.c b/src/tests/cmocka/test_sss_ptr_hash.c
d86ed3a
index 1458238f537970d0ecde80bd36830b28970ca364..31cf8b705367498822094f8811b393c1b35e12bc 100644
d86ed3a
--- a/src/tests/cmocka/test_sss_ptr_hash.c
d86ed3a
+++ b/src/tests/cmocka/test_sss_ptr_hash.c
d86ed3a
@@ -91,6 +91,45 @@ void test_sss_ptr_hash_with_free_cb(void **state)
d86ed3a
     assert_int_equal(free_counter, MAX_ENTRIES_AMOUNT*2);
d86ed3a
 }
d86ed3a
 
d86ed3a
+void test_sss_ptr_hash_overwrite_with_free_cb(void **state)
d86ed3a
+{
d86ed3a
+    hash_table_t *table;
d86ed3a
+    int free_counter = 0;
d86ed3a
+    unsigned long count;
d86ed3a
+    char *payload;
d86ed3a
+    char *value;
d86ed3a
+    errno_t ret;
d86ed3a
+
d86ed3a
+    table = sss_ptr_hash_create(global_talloc_context,
d86ed3a
+                                free_payload_cb,
d86ed3a
+                                &free_counter);
d86ed3a
+    assert_non_null(table);
d86ed3a
+
d86ed3a
+    payload = talloc_strdup(table, "test_value1");
d86ed3a
+    assert_non_null(payload);
d86ed3a
+    talloc_set_name_const(payload, "char");
d86ed3a
+    ret = sss_ptr_hash_add_or_override(table, "test", payload, char);
d86ed3a
+    assert_int_equal(ret, 0);
d86ed3a
+    count = hash_count(table);
d86ed3a
+    assert_int_equal(count, 1);
d86ed3a
+    value = sss_ptr_hash_lookup(table, "test", char);
d86ed3a
+    assert_ptr_equal(value, payload);
d86ed3a
+
d86ed3a
+
d86ed3a
+    payload = talloc_strdup(table, "test_value2");
d86ed3a
+    assert_non_null(payload);
d86ed3a
+    talloc_set_name_const(payload, "char");
d86ed3a
+    ret = sss_ptr_hash_add_or_override(table, "test", payload, char);
d86ed3a
+    assert_int_equal(ret, 0);
d86ed3a
+    count = hash_count(table);
d86ed3a
+    assert_int_equal(count, 1);
d86ed3a
+    value = sss_ptr_hash_lookup(table, "test", char);
d86ed3a
+    assert_ptr_equal(value, payload);
d86ed3a
+
d86ed3a
+    talloc_free(table);
d86ed3a
+    assert_int_equal(free_counter, 2);
d86ed3a
+}
d86ed3a
+
d86ed3a
 struct table_wrapper
d86ed3a
 {
d86ed3a
     hash_table_t **table;
d86ed3a
diff --git a/src/tests/cmocka/test_utils.c b/src/tests/cmocka/test_utils.c
d86ed3a
index d77a972c1bc93638085c3d49131247fefb333d56..d258622fb50e849a3efabb123960db410eb399e1 100644
d86ed3a
--- a/src/tests/cmocka/test_utils.c
d86ed3a
+++ b/src/tests/cmocka/test_utils.c
d86ed3a
@@ -2144,6 +2144,9 @@ int main(int argc, const char *argv[])
d86ed3a
         cmocka_unit_test_setup_teardown(test_sss_ptr_hash_with_free_cb,
d86ed3a
                                         setup_leak_tests,
d86ed3a
                                         teardown_leak_tests),
d86ed3a
+        cmocka_unit_test_setup_teardown(test_sss_ptr_hash_overwrite_with_free_cb,
d86ed3a
+                                        setup_leak_tests,
d86ed3a
+                                        teardown_leak_tests),
d86ed3a
         cmocka_unit_test_setup_teardown(test_sss_ptr_hash_with_lookup_cb,
d86ed3a
                                         setup_leak_tests,
d86ed3a
                                         teardown_leak_tests),
d86ed3a
diff --git a/src/tests/cmocka/test_utils.h b/src/tests/cmocka/test_utils.h
d86ed3a
index 44b9479f965ee830ea0937c0fd89b87e35796598..458bcb750569c1f5f346917f29aa8b5500891988 100644
d86ed3a
--- a/src/tests/cmocka/test_utils.h
d86ed3a
+++ b/src/tests/cmocka/test_utils.h
d86ed3a
@@ -35,6 +35,7 @@ void test_concatenate_string_array(void **state);
d86ed3a
 
d86ed3a
 /* from src/tests/cmocka/test_sss_ptr_hash.c */
d86ed3a
 void test_sss_ptr_hash_with_free_cb(void **state);
d86ed3a
+void test_sss_ptr_hash_overwrite_with_free_cb(void **state);
d86ed3a
 void test_sss_ptr_hash_with_lookup_cb(void **state);
d86ed3a
 void test_sss_ptr_hash_without_cb(void **state);
d86ed3a
 
d86ed3a
diff --git a/src/util/sss_ptr_hash.c b/src/util/sss_ptr_hash.c
d86ed3a
index 6409236c782bac729ec51502019c04c83bce7cab..e3805dac4052b587d395b7163f5c45e1ba0aa6dc 100644
d86ed3a
--- a/src/util/sss_ptr_hash.c
d86ed3a
+++ b/src/util/sss_ptr_hash.c
d86ed3a
@@ -54,6 +54,7 @@ struct sss_ptr_hash_value {
d86ed3a
     hash_table_t *table;
d86ed3a
     const char *key;
d86ed3a
     void *payload;
d86ed3a
+    bool delete_in_progress;
d86ed3a
 };
d86ed3a
 
d86ed3a
 static int
d86ed3a
@@ -61,12 +62,22 @@ sss_ptr_hash_value_destructor(struct sss_ptr_hash_value *value)
d86ed3a
 {
d86ed3a
     hash_key_t table_key;
d86ed3a
 
d86ed3a
+    /* Do not call hash_delete() if we got here from hash delete callback when
d86ed3a
+     * the callback calls talloc_free(payload) which frees the value. This
d86ed3a
+     * should not happen since talloc will avoid circular free but let's be
d86ed3a
+     * over protective here. */
d86ed3a
+    if (value->delete_in_progress) {
d86ed3a
+        return 0;
d86ed3a
+    }
d86ed3a
+
d86ed3a
+    value->delete_in_progress = true;
d86ed3a
     if (value->table && value->key) {
d86ed3a
         table_key.type = HASH_KEY_STRING;
d86ed3a
         table_key.str = discard_const_p(char, value->key);
d86ed3a
         if (hash_delete(value->table, &table_key) != HASH_SUCCESS) {
d86ed3a
             DEBUG(SSSDBG_CRIT_FAILURE,
d86ed3a
                   "failed to delete entry with key '%s'\n", value->key);
d86ed3a
+            value->delete_in_progress = false;
d86ed3a
         }
d86ed3a
     }
d86ed3a
 
d86ed3a
@@ -127,6 +138,15 @@ sss_ptr_hash_delete_cb(hash_entry_t *item,
d86ed3a
     callback_entry.key = item->key;
d86ed3a
     callback_entry.value.type = HASH_VALUE_PTR;
d86ed3a
     callback_entry.value.ptr = value->payload;
d86ed3a
+
d86ed3a
+    /* Delete the value in case this callback has been called directly
d86ed3a
+     * from dhash (overwriting existing entry) instead of hash_delete()
d86ed3a
+     * in value's destructor. */
d86ed3a
+    if (!value->delete_in_progress) {
d86ed3a
+        talloc_set_destructor(value, NULL);
d86ed3a
+        talloc_free(value);
d86ed3a
+    }
d86ed3a
+
d86ed3a
     /* Even if execution is already in the context of
d86ed3a
      * talloc_free(payload) -> talloc_free(value) -> ...
d86ed3a
      * there still might be legitimate reasons to execute callback.
d86ed3a
-- 
d86ed3a
2.25.4
d86ed3a