]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
hashtable: add option to disable RCU locks
authorNikola Pajkovsky <nikolap@openssl.org>
Thu, 25 Sep 2025 16:32:17 +0000 (18:32 +0200)
committerNeil Horman <nhorman@openssl.org>
Thu, 2 Oct 2025 12:05:02 +0000 (08:05 -0400)
a new config option _no_rcu_ is added into HT_CONFIG. When _no_rcu_ is
set then hashtable can be guarded with any other locking primitives,
and behives as ordinary hashtable. Also, all the impact of the
atomics used internally to the hash table was mitigated.

RCU performance

   # INFO:  @ test/lhash_test.c:747
   # multithread stress runs 40000 ops in 40.779656 seconds

No RCU, guarded with RWLOCK

   # INFO:  @ test/lhash_test.c:747
   # multithread stress runs 40000 ops in 36.976926 seconds

Signed-off-by: Nikola Pajkovsky <nikolap@openssl.org>
Reviewed-by: Saša Nedvědický <sashan@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Neil Horman <nhorman@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/28677)

crypto/core_namemap.c
crypto/hashtable/hashtable.c
doc/internal/man3/ossl_ht_new.pod
fuzz/hashtable.c
include/internal/hashtable.h
test/lhash_test.c

index 50491db62bbb1d53f33941015e10f85de1ef6269..507327a1cbdcc60adcfd61ee41d49bafcf41c767 100644 (file)
@@ -547,7 +547,7 @@ OSSL_NAMEMAP *ossl_namemap_stored(OSSL_LIB_CTX *libctx)
 OSSL_NAMEMAP *ossl_namemap_new(OSSL_LIB_CTX *libctx)
 {
     OSSL_NAMEMAP *namemap;
-    HT_CONFIG htconf = { NULL, NULL, NULL, NAMEMAP_HT_BUCKETS, 1, 1 };
+    HT_CONFIG htconf = { NULL, NULL, NULL, NAMEMAP_HT_BUCKETS, 1, 1, 0 };
 
     htconf.ctx = libctx;
 
index 99971ef71fd16525da9d1e385a8c5e2cacfe7e76..da81dab1308a45e8eb0fc70112d6afd14e8f1440 100644 (file)
@@ -71,7 +71,6 @@
 # include <sanitizer/tsan_interface.h>
 #endif
 
-#include "internal/numbers.h"
 /*
  * When we do a lookup/insert/delete, there is a high likelihood
  * that we will iterate over at least part of the neighborhood list
@@ -188,10 +187,14 @@ HT *ossl_ht_new(const HT_CONFIG *conf)
     if (new == NULL)
         return NULL;
 
-    new->atomic_lock = CRYPTO_THREAD_lock_new();
-    if (new->atomic_lock == NULL)
+    if (conf->lockless_reads && conf->no_rcu)
         goto err;
 
+    if (!conf->no_rcu) {
+        new->atomic_lock = CRYPTO_THREAD_lock_new();
+        if (new->atomic_lock == NULL)
+            goto err;
+    }
     memcpy(&new->config, conf, sizeof(*conf));
 
     if (new->config.init_neighborhoods != 0) {
@@ -222,18 +225,21 @@ HT *ossl_ht_new(const HT_CONFIG *conf)
         goto err;
     new->md->neighborhood_mask = new->wpd.neighborhood_len - 1;
 
-    new->lock = ossl_rcu_lock_new(1, conf->ctx);
-    if (new->lock == NULL)
-        goto err;
-
+    if (!conf->no_rcu) {
+        new->lock = ossl_rcu_lock_new(1, conf->ctx);
+        if (new->lock == NULL)
+            goto err;
+    }
     if (new->config.ht_hash_fn == NULL)
         new->config.ht_hash_fn = internal_ht_hash_fn;
 
     return new;
 
 err:
-    CRYPTO_THREAD_lock_free(new->atomic_lock);
-    ossl_rcu_lock_free(new->lock);
+    if (!conf->no_rcu) {
+        CRYPTO_THREAD_lock_free(new->atomic_lock);
+        ossl_rcu_lock_free(new->lock);
+    }
     if (new->md != NULL)
         OPENSSL_free(new->md->neighborhood_ptr_to_free);
     OPENSSL_free(new->md);
@@ -243,16 +249,25 @@ err:
 
 int ossl_ht_read_lock(HT *htable)
 {
+    if (htable->config.no_rcu)
+        return 1;
+
     return ossl_rcu_read_lock(htable->lock);
 }
 
 void ossl_ht_read_unlock(HT *htable)
 {
+    if (htable->config.no_rcu)
+        return;
+
     ossl_rcu_read_unlock(htable->lock);
 }
 
 void ossl_ht_write_lock(HT *htable)
 {
+    if (htable->config.no_rcu)
+        return;
+
     ossl_rcu_write_lock(htable->lock);
     htable->wpd.need_sync = 0;
 }
@@ -261,6 +276,9 @@ void ossl_ht_write_unlock(HT *htable)
 {
     int need_sync = htable->wpd.need_sync;
 
+    if (htable->config.no_rcu)
+        return;
+
     htable->wpd.need_sync = 0;
     ossl_rcu_write_unlock(htable->lock);
     if (need_sync)
@@ -308,15 +326,25 @@ static int ossl_ht_flush_internal(HT *h)
     newmd->neighborhood_mask = DEFAULT_NEIGH_LEN - 1;
 
     /* Swap the old and new mutable data sets */
-    oldmd = ossl_rcu_deref(&h->md);
-    ossl_rcu_assign_ptr(&h->md, &newmd);
+    if (!h->config.no_rcu) {
+        oldmd = ossl_rcu_deref(&h->md);
+        ossl_rcu_assign_ptr(&h->md, &newmd);
+    } else {
+        oldmd = h->md;
+        h->md = newmd;
+    }
 
     /* Set the number of entries to 0 */
     h->wpd.value_count = 0;
     h->wpd.neighborhood_len = DEFAULT_NEIGH_LEN;
 
-    ossl_rcu_call(h->lock, free_oldmd, oldmd);
+    if (!h->config.no_rcu) {
+        ossl_rcu_call(h->lock, free_oldmd, oldmd);
+    } else {
+        free_oldmd(oldmd);
+    }
     h->wpd.need_sync = 1;
+
     return 1;
 }
 
@@ -334,8 +362,10 @@ void ossl_ht_free(HT *h)
     ossl_ht_flush_internal(h);
     ossl_ht_write_unlock(h);
     /* Freeing the lock does a final sync for us */
-    CRYPTO_THREAD_lock_free(h->atomic_lock);
-    ossl_rcu_lock_free(h->lock);
+    if (!h->config.no_rcu) {
+        CRYPTO_THREAD_lock_free(h->atomic_lock);
+        ossl_rcu_lock_free(h->lock);
+    }
     OPENSSL_free(h->md->neighborhood_ptr_to_free);
     OPENSSL_free(h->md);
     OPENSSL_free(h);
@@ -492,9 +522,14 @@ static int grow_hashtable(HT *h, size_t oldsize)
     /*
      * Now we replace the old mutable data with the new
      */
-    ossl_rcu_assign_ptr(&h->md, &newmd);
-    ossl_rcu_call(h->lock, free_old_neigh_table, oldmd);
-    h->wpd.need_sync = 1;
+    if (!h->config.no_rcu) {
+        ossl_rcu_assign_ptr(&h->md, &newmd);
+        ossl_rcu_call(h->lock, free_old_neigh_table, oldmd);
+        h->wpd.need_sync = 1;
+    } else {
+        h->md = newmd;
+        free_old_neigh_table(oldmd);
+    }
     /*
      * And we're done
      */
@@ -552,7 +587,10 @@ static int ossl_ht_insert_locked(HT *h, uint64_t hash,
         PREFETCH_NEIGHBORHOOD(md->neighborhoods[neigh_idx]);
 
         for (j = 0; j < NEIGHBORHOOD_LEN; j++) {
-            ival = ossl_rcu_deref(&md->neighborhoods[neigh_idx].entries[j].value);
+            if (!h->config.no_rcu)
+                ival = ossl_rcu_deref(&md->neighborhoods[neigh_idx].entries[j].value);
+            else
+                ival = (HT_VALUE *)md->neighborhoods[neigh_idx].entries[j].value;
             if (ival == NULL) {
                 empty_idx = j;
                 /* lockless_reads implies no deletion, we can break out */
@@ -560,9 +598,13 @@ static int ossl_ht_insert_locked(HT *h, uint64_t hash,
                     goto not_found;
                 continue;
             }
-            if (!CRYPTO_atomic_load(&md->neighborhoods[neigh_idx].entries[j].hash,
-                                    &ihash, h->atomic_lock))
-                return 0;
+            if (!h->config.no_rcu) {
+                if (!CRYPTO_atomic_load(&md->neighborhoods[neigh_idx].entries[j].hash,
+                                        &ihash, h->atomic_lock))
+                    return 0;
+            } else {
+                ihash = md->neighborhoods[neigh_idx].entries[j].hash;
+            }
             if (compare_hash(hash, ihash) && match_key(&newval->value.key,
                                                        &ival->key)) {
                 if (olddata == NULL) {
@@ -570,13 +612,19 @@ static int ossl_ht_insert_locked(HT *h, uint64_t hash,
                     return 0;
                 }
                 /* Do a replacement */
-                if (!CRYPTO_atomic_store(&md->neighborhoods[neigh_idx].entries[j].hash,
-                                         hash, h->atomic_lock))
-                    return 0;
-                *olddata = (HT_VALUE *)md->neighborhoods[neigh_idx].entries[j].value;
-                ossl_rcu_assign_ptr(&md->neighborhoods[neigh_idx].entries[j].value,
-                                    &newval);
-                ossl_rcu_call(h->lock, free_old_ht_value, *olddata);
+                if (!h->config.no_rcu) {
+                    if (!CRYPTO_atomic_store(&md->neighborhoods[neigh_idx].entries[j].hash,
+                                             hash, h->atomic_lock))
+                        return 0;
+                    *olddata = (HT_VALUE *)md->neighborhoods[neigh_idx].entries[j].value;
+                    ossl_rcu_assign_ptr(&md->neighborhoods[neigh_idx].entries[j].value,
+                                        &newval);
+                    ossl_rcu_call(h->lock, free_old_ht_value, *olddata);
+                } else {
+                    md->neighborhoods[neigh_idx].entries[j].hash = hash;
+                    *olddata = (HT_VALUE *)md->neighborhoods[neigh_idx].entries[j].value;
+                    md->neighborhoods[neigh_idx].entries[j].value = newval;
+                }
                 h->wpd.need_sync = 1;
                 return 1;
             }
@@ -591,12 +639,17 @@ static int ossl_ht_insert_locked(HT *h, uint64_t hash,
     /* If we get to here, its just an insert */
     if (empty_idx == SIZE_MAX)
         return -1; /* out of space */
-    if (!CRYPTO_atomic_store(&md->neighborhoods[neigh_idx].entries[empty_idx].hash,
-                             hash, h->atomic_lock))
-        return 0;
+    if (!h->config.no_rcu) {
+        if (!CRYPTO_atomic_store(&md->neighborhoods[neigh_idx].entries[empty_idx].hash,
+                                 hash, h->atomic_lock))
+            return 0;
+        ossl_rcu_assign_ptr(&md->neighborhoods[neigh_idx].entries[empty_idx].value,
+                            &newval);
+    } else {
+        md->neighborhoods[neigh_idx].entries[empty_idx].hash = hash;
+        md->neighborhoods[neigh_idx].entries[empty_idx].value = newval;
+    }
     h->wpd.value_count++;
-    ossl_rcu_assign_ptr(&md->neighborhoods[neigh_idx].entries[empty_idx].value,
-                        &newval);
     return 1;
 }
 
@@ -684,21 +737,31 @@ HT_VALUE *ossl_ht_get(HT *h, HT_KEY *key)
 
     hash = h->config.ht_hash_fn(key);
 
-    md = ossl_rcu_deref(&h->md);
+    if (!h->config.no_rcu)
+        md = ossl_rcu_deref(&h->md);
+    else
+        md = h->md;
     neigh_idx = neigh_idx_start = hash & md->neighborhood_mask;
     do {
         PREFETCH_NEIGHBORHOOD(md->neighborhoods[neigh_idx]);
         for (j = 0; j < NEIGHBORHOOD_LEN; j++) {
-            ival = ossl_rcu_deref(&md->neighborhoods[neigh_idx].entries[j].value);
+            if (!h->config.no_rcu)
+                ival = ossl_rcu_deref(&md->neighborhoods[neigh_idx].entries[j].value);
+            else
+                ival = md->neighborhoods[neigh_idx].entries[j].value;
             if (ival == NULL) {
                 if (lockless_reads)
                     /* lockless_reads implies no deletion, we can break out */
                     return NULL;
                 continue;
             }
-            if (!CRYPTO_atomic_load(&md->neighborhoods[neigh_idx].entries[j].hash,
-                                    &ehash, h->atomic_lock))
-                return NULL;
+            if (!h->config.no_rcu) {
+                if (!CRYPTO_atomic_load(&md->neighborhoods[neigh_idx].entries[j].hash,
+                                        &ehash, h->atomic_lock))
+                    return NULL;
+            } else {
+                ehash = md->neighborhoods[neigh_idx].entries[j].hash;
+            }
             if (compare_hash(hash, ehash) && match_key(&ival->value.key, key))
                 return (HT_VALUE *)ival;
         }
@@ -741,19 +804,53 @@ int ossl_ht_delete(HT *h, HT_KEY *key)
             continue;
         if (compare_hash(hash, h->md->neighborhoods[neigh_idx].entries[j].hash)
             && match_key(key, &v->value.key)) {
-            if (!CRYPTO_atomic_store(&h->md->neighborhoods[neigh_idx].entries[j].hash,
-                                     0, h->atomic_lock))
-                break;
+            if (!h->config.no_rcu) {
+                if (!CRYPTO_atomic_store(&h->md->neighborhoods[neigh_idx].entries[j].hash,
+                                         0, h->atomic_lock))
+                    break;
+                ossl_rcu_assign_ptr(&h->md->neighborhoods[neigh_idx].entries[j].value, &nv);
+            } else {
+                h->md->neighborhoods[neigh_idx].entries[j].hash = 0;
+                h->md->neighborhoods[neigh_idx].entries[j].value = NULL;
+            }
             h->wpd.value_count--;
-            ossl_rcu_assign_ptr(&h->md->neighborhoods[neigh_idx].entries[j].value,
-                                &nv);
             rc = 1;
             break;
         }
     }
     if (rc == 1) {
-        ossl_rcu_call(h->lock, free_old_entry, v);
+        if (!h->config.no_rcu)
+            ossl_rcu_call(h->lock, free_old_entry, v);
+        else
+            free_old_entry(v);
         h->wpd.need_sync = 1;
     }
+
     return rc;
 }
+
+HT_VALUE *ossl_ht_deref_value(HT *h, HT_VALUE **val)
+{
+    HT_VALUE *v;
+
+    if (!h->config.no_rcu)
+        v = ossl_rcu_deref(val);
+    else
+        v = *val;
+
+    return v;
+}
+
+void *ossl_ht_inner_value(HT *h, HT_VALUE *v)
+{
+    void *inner;
+
+    if (!h->config.no_rcu) {
+        inner = v->value;
+    } else {
+        inner = v->value;
+        OPENSSL_free(v);
+    }
+
+    return inner;
+}
index 18aa5d1e4d343545fa3e657f30f1b636edbc4aeb..7ad33efbbe2be8d925acc9f7c1060ed19435b1b1 100644 (file)
@@ -67,6 +67,7 @@ of:
     I<ht_free_fn> The function to call to free a value, may be NULL.
     I<ht_hash_fn> The function to generate a hash value for a key, may be NULL.
     I<init_neighborhoods> The initial number of neighborhoods in the hash table.
+    I<no_rcu> Disables RCU locking.
 
 Note that init_bucket_len may be set to zero, which will use the default initial
 bucket size, which will be automatically expanded with the hash table load
@@ -84,6 +85,9 @@ Note that lockless_write operations are done at your own risk.  Lockless
     is the callers responsibility in this mode of operation to provide thread
     synchronization.
 
+Note that if RCU locks are disabled, calls to ossl_ht_[read/write]_[lock/unlock]
+have no effect.
+
 =item *
 
 ossl_ht_free() frees an allocated hash table.  Each element in the table
@@ -110,7 +114,8 @@ called to release the element data.
 =item *
 
 ossl_ht_insert() inserts an B<HT_VALUE> element into the hash table, to be
-hashed using the corresponding B<HT_KEY> value.
+hashed using the corresponding B<HT_KEY> value. The returned pointer olddata must
+be freed only when RCU is disabled and insert performs a replacement.
 
 =item *
 
index 8aac7bf47b86964ac6011b4d9ab8635d8665e0a1..78536e42a3340d0d062bd722680dd7a5d63b62cf 100644 (file)
@@ -99,7 +99,7 @@ static void fuzz_free_cb(HT_VALUE *v)
 
 int FuzzerInitialize(int *argc, char ***argv)
 {
-    HT_CONFIG fuzz_conf = {NULL, fuzz_free_cb, NULL, 0, 1};
+    HT_CONFIG fuzz_conf = {NULL, fuzz_free_cb, NULL, 0, 1, 0};
 
     OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
     ERR_clear_error();
index 86d3518b77ec89ca46600eae6eecac01d64f35fe..951265714c7639b10285d04d5ab558d7c84a5456 100644 (file)
@@ -54,6 +54,7 @@ typedef struct ht_config_st {
     size_t init_neighborhoods;
     uint32_t collision_check;
     uint32_t lockless_reads;
+    uint32_t no_rcu;
 } HT_CONFIG;
 
 /*
@@ -199,7 +200,7 @@ pfx ossl_unused int ossl_ht_##name##_##vtype##_insert(HT *h, HT_KEY *key,      \
     inval.type_id = &name##_##vtype##_id;                                      \
     rc = ossl_ht_insert(h, key, &inval, olddata == NULL ? NULL : &oval);       \
     if (oval != NULL)                                                          \
-        *olddata = (vtype *)oval->value;                                       \
+        *olddata = (vtype *)ossl_ht_inner_value(h, oval);                      \
     return rc;                                                                 \
 }                                                                              \
                                                                                \
@@ -221,7 +222,7 @@ pfx ossl_unused vtype *ossl_unused ossl_ht_##name##_##vtype##_get(HT *h,       \
     vv = ossl_ht_get(h, key);                                                  \
     if (vv == NULL)                                                            \
         return NULL;                                                           \
-    *v = ossl_rcu_deref(&vv);                                                  \
+    *v = ossl_ht_deref_value(h, &vv);                                          \
     return ossl_ht_##name##_##vtype##_from_value(*v);                          \
 }                                                                              \
                                                                                \
@@ -352,4 +353,49 @@ void ossl_ht_value_list_free(HT_VALUE_LIST *list);
  */
 HT_VALUE *ossl_ht_get(HT *htable, HT_KEY *key);
 
+/**
+ * ossl_ht_deref_value - Dereference a value stored in a hash table entry
+ * @h:   The hash table handle
+ * @val: Pointer to the value pointer inside the hash table
+ *
+ * This helper returns the actual value stored in a hash table entry,
+ * with awareness of whether the table is configured for RCU (Read-Copy-Update)
+ * safe lookups.
+ *
+ * If the hash table is configured to use RCU lookups, the function
+ * calls ossl_rcu_deref() to safely read the value under RCU protection.
+ * This ensures that the caller sees a consistent pointer in concurrent environments.
+ *
+ * If RCU is not enabled (i.e. `h->config.no_rcu` is true), the function
+ * simply dereferences @val directly.
+ *
+ * Return:
+ * A pointer to the dereferenced hash table value (`HT_VALUE *`), or NULL if
+ * the underlying pointer is NULL.
+ */
+HT_VALUE *ossl_ht_deref_value(HT *p, HT_VALUE **val);
+
+/**
+ * ossl_ht_inner_value - Extract the user payload from a hash table value
+ * @h: The hash table handle
+ * @v: The hash table value wrapper (HT_VALUE)
+ *
+ * This helper returns the user-provided payload stored inside a
+ * hash table value container. The behavior differs depending on
+ * whether the hash table is configured to use RCU (Read-Copy-Update)
+ * for concurrency control.
+ *
+ * - If RCU is enabled, the function simply returns `v->value` without
+ *   modifying or freeing the container.
+ *
+ * - If RCU is disabled the container structure `v` is no longer needed once
+ *   the inner pointer has been extracted. In this case, the function frees
+ *   `v` and returns the inner `value` pointer directly.
+ *
+ * Return:
+ * A pointer to the user payload (`void *`) contained in the hash table
+ * value wrapper.
+ */
+void *ossl_ht_inner_value(HT *h, HT_VALUE *v);
+
 #endif
index c229df902c9139984bcf8c7bddf9f2805c29790c..ad818032823dfb173f41b59f0d8776fa6dacc023 100644 (file)
@@ -212,7 +212,7 @@ static uint64_t hashtable_hash(HT_KEY *key)
     return (uint64_t)(*(uint32_t *)key->keybuf);
 }
 
-static int test_int_hashtable(void)
+static int test_int_hashtable(int idx)
 {
     static struct {
         int data;
@@ -227,11 +227,8 @@ static int test_int_hashtable(void)
     };
     const size_t n_dels = OSSL_NELEM(dels);
     HT_CONFIG hash_conf = {
-        NULL,
-        NULL,
-        NULL,
-        0,
-        1,
+        .collision_check = 1,
+        .no_rcu = idx,
     };
     INTKEY key;
     int rc = 0;
@@ -405,13 +402,14 @@ static int test_hashtable_stress(int idx)
     unsigned int i;
     int testresult = 0, *p;
     HT_CONFIG hash_conf = {
-        NULL,              /* use default context */
-        hashtable_intfree, /* our free function */
-        hashtable_hash,    /* our hash function */
-        625000,            /* preset hash size */
-        1,                 /* Check collisions */
-        0                  /* Lockless reads */
+        .ht_free_fn = hashtable_intfree,
+        .ht_hash_fn = hashtable_hash,
+        .init_neighborhoods = 625000,
+        .collision_check = 1,
+        .lockless_reads = idx % 2,
+        .no_rcu = idx / 2,
     };
+
     HT *h;
     INTKEY key;
     HT_VALUE *v;
@@ -419,9 +417,11 @@ static int test_hashtable_stress(int idx)
     struct timeval start, end, delta;
 #endif
 
-    hash_conf.lockless_reads = idx;
     h = ossl_ht_new(&hash_conf);
-
+    if (h == NULL
+        && hash_conf.no_rcu
+        && hash_conf.lockless_reads)
+        return 1;
 
     if (!TEST_ptr(h))
         goto end;
@@ -456,7 +456,7 @@ static int test_hashtable_stress(int idx)
         const int j = (7 * i + 4) % n * 3 + 1;
         HT_SET_KEY_FIELD(&key, mykey, j);
 
-        switch (idx) {
+        switch (idx % 2) {
         case 0:
             if (!TEST_int_eq((ossl_ht_delete(h, TO_HT_KEY(&key))), 1)) {
                 TEST_info("hashtable didn't delete key %d\n", j);
@@ -503,9 +503,14 @@ HT_END_KEY_DEFN(MTKEY)
 
 IMPLEMENT_HT_VALUE_TYPE_FNS(TEST_MT_ENTRY, mt, static)
 
+struct ht_internal_st {
+    HT_CONFIG config;
+};
+
 static int worker_num = 0;
 static CRYPTO_RWLOCK *worker_lock;
 static CRYPTO_RWLOCK *testrand_lock;
+static CRYPTO_RWLOCK *no_rcu_lock;
 static int free_failure = 0;
 static int shutting_down = 0;
 static int global_iteration = 0;
@@ -573,21 +578,36 @@ static void do_mt_hash_work(void)
         }
         switch(behavior) {
         case DO_LOOKUP:
-            if (!ossl_ht_read_lock(m_ht))
-                break;
+            if (!m_ht->config.no_rcu) {
+                if (!ossl_ht_read_lock(m_ht))
+                    break;
+            } else {
+                if (!TEST_true(CRYPTO_THREAD_read_lock(no_rcu_lock)))
+                    break;
+            }
             m = ossl_ht_mt_TEST_MT_ENTRY_get(m_ht, TO_HT_KEY(&key), &v);
             if (m != NULL && m != expected_m) {
                 worker_exits[num] = "Read unexpected value from hashtable";
                 TEST_info("Iteration %d Read unexpected value %p when %p expected",
                           giter, (void *)m, (void *)expected_m);
             }
-            ossl_ht_read_unlock(m_ht);
+            if (!m_ht->config.no_rcu) {
+                ossl_ht_read_unlock(m_ht);
+            } else {
+                if (!TEST_true(CRYPTO_THREAD_unlock(no_rcu_lock)))
+                    break;
+            }
             if (worker_exits[num] != NULL)
                 return;
             break;
         case DO_INSERT:
         case DO_REPLACE:
-            ossl_ht_write_lock(m_ht);
+            if (!m_ht->config.no_rcu) {
+                ossl_ht_write_lock(m_ht);
+            } else {
+                if (!TEST_true(CRYPTO_THREAD_write_lock(no_rcu_lock)))
+                    break;
+            }
             if (behavior == DO_REPLACE) {
                 expected_rc = 1;
                 r = &m;
@@ -607,12 +627,22 @@ static void do_mt_hash_work(void)
             }
             if (expected_rc == 1)
                 expected_m->in_table = 1;
-            ossl_ht_write_unlock(m_ht);
+            if (!m_ht->config.no_rcu) {
+                ossl_ht_write_unlock(m_ht);
+            } else {
+                if (!TEST_true(CRYPTO_THREAD_unlock(no_rcu_lock)))
+                    break;
+            }
             if (worker_exits[num] != NULL)
                 return;
             break;
         case DO_DELETE:
-            ossl_ht_write_lock(m_ht);
+            if (!m_ht->config.no_rcu) {
+                ossl_ht_write_lock(m_ht);
+            } else {
+                if (!TEST_true(CRYPTO_THREAD_write_lock(no_rcu_lock)))
+                    break;
+            }
             expected_rc = expected_m->in_table;
             if (expected_rc == 1) {
                 /*
@@ -632,7 +662,12 @@ static void do_mt_hash_work(void)
                           expected_m->in_table ? "in table" : "not in table");
                 worker_exits[num] = "Failure on delete";
             }
-            ossl_ht_write_unlock(m_ht);
+            if (!m_ht->config.no_rcu) {
+                ossl_ht_write_unlock(m_ht);
+            } else {
+                if (!TEST_true(CRYPTO_THREAD_unlock(no_rcu_lock)))
+                    break;
+            }
             if (worker_exits[num] != NULL)
                 return;
             break;
@@ -643,14 +678,13 @@ static void do_mt_hash_work(void)
     }
 }
 
-static int test_hashtable_multithread(void)
+static int test_hashtable_multithread(int idx)
 {
     HT_CONFIG hash_conf = {
-        NULL,              /* use default context */
-        hashtable_mt_free, /* our free function */
-        NULL,              /* default hash function */
-        0,                 /* default hash size */
-        1,                 /* Check collisions */
+        .ht_free_fn = hashtable_mt_free,
+        .init_neighborhoods = 0,
+        .collision_check = 1,
+        .no_rcu = idx,
     };
     int ret = 0;
     thread_t workers[NUM_WORKERS];
@@ -672,6 +706,8 @@ static int test_hashtable_multithread(void)
         goto end_free;
     if (!TEST_ptr(testrand_lock = CRYPTO_THREAD_lock_new()))
         goto end_free;
+    if (!TEST_ptr(no_rcu_lock = CRYPTO_THREAD_lock_new()))
+        goto end_free;
 #ifdef MEASURE_HASH_PERFORMANCE
     gettimeofday(&start, NULL);
 #endif
@@ -709,11 +745,15 @@ shutdown:
     TEST_info("multithread stress runs 40000 ops in %ld.%ld seconds", delta.tv_sec, delta.tv_usec);
 #endif
 
+    worker_num = 0;
+    free_failure = 0;
+    global_iteration = 0;
 end_free:
     shutting_down = 1;
     ossl_ht_free(m_ht);
     CRYPTO_THREAD_lock_free(worker_lock);
     CRYPTO_THREAD_lock_free(testrand_lock);
+    CRYPTO_THREAD_lock_free(no_rcu_lock);
 end:
     return ret;
 }
@@ -722,8 +762,8 @@ int setup_tests(void)
 {
     ADD_TEST(test_int_lhash);
     ADD_TEST(test_stress);
-    ADD_TEST(test_int_hashtable);
-    ADD_ALL_TESTS(test_hashtable_stress, 2);
-    ADD_TEST(test_hashtable_multithread);
+    ADD_ALL_TESTS(test_int_hashtable, 2);
+    ADD_ALL_TESTS(test_hashtable_stress, 4);
+    ADD_ALL_TESTS(test_hashtable_multithread, 2);
     return 1;
 }