]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
stasis.c: Fix deadlock in stasis_topic_pool_get_topic during module load
authorphoneben <3232963@gmail.com>
Sat, 29 Nov 2025 18:08:13 +0000 (20:08 +0200)
committergithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Mon, 26 Jan 2026 20:33:27 +0000 (20:33 +0000)
stasis.c: Fix deadlock in stasis_topic_pool_get_topic during module load.

Deadlock occurs when res_manager_devicestate loads concurrently with
device state operations due to lock ordering violation:

Thread 1: Holds pool lock → needs topic lock (in stasis_forward_all)
Thread 2: Holds topic lock → needs pool lock (in stasis_topic_pool_get_topic)

Fix: Release pool lock before calling stasis_topic_create() and
stasis_forward_all(). Re-acquire only for insertion with race check.

Preserves borrowed reference semantics while breaking the deadlock cycle.

Fixes: #1611
main/stasis.c

index 6f5ab019f7cd7b849aee2e9867072f3e149f89db..a041fe49341fc9b582feb4a58e2b05d05ea1a67e 100644 (file)
@@ -1776,6 +1776,22 @@ static void send_subscription_unsubscribe(struct stasis_topic *topic,
 struct topic_pool_entry {
        struct stasis_forward *forward;
        struct stasis_topic *topic;
+       /*
+        * Per-entry initialization state. This lets us serialize creation of a
+        * given topic name without holding the pool container lock while doing
+        * the heavy lifting (topic creation, forwarding setup, etc).
+        *
+        * A topic_pool_entry starts life in an "in-flight" state where neither
+        * initialized nor failed are set.  The first thread to link the entry
+        * into the pool becomes the creator and is responsible for completing
+        * initialization, setting one of the flags, and broadcasting init_cond.
+        * Other threads that find the same entry simply wait for initialization
+        * to complete and then reuse the created topic.
+        */
+       ast_mutex_t init_lock;
+       ast_cond_t init_cond;
+       unsigned int initialized; /* terminal success state */
+       unsigned int failed; /* terminal failure state */
        char name[0];
 };
 
@@ -1786,6 +1802,8 @@ static void topic_pool_entry_dtor(void *obj)
        entry->forward = stasis_forward_cancel(entry->forward);
        ao2_cleanup(entry->topic);
        entry->topic = NULL;
+       ast_cond_destroy(&entry->init_cond);
+       ast_mutex_destroy(&entry->init_lock);
 }
 
 static struct topic_pool_entry *topic_pool_entry_alloc(const char *topic_name)
@@ -1797,9 +1815,9 @@ static struct topic_pool_entry *topic_pool_entry_alloc(const char *topic_name)
        if (!topic_pool_entry) {
                return NULL;
        }
-
+       ast_mutex_init(&topic_pool_entry->init_lock);
+       ast_cond_init(&topic_pool_entry->init_cond, NULL);
        strcpy(topic_pool_entry->name, topic_name); /* Safe */
-
        return topic_pool_entry;
 }
 
@@ -1947,48 +1965,164 @@ void stasis_topic_pool_delete_topic(struct stasis_topic_pool *pool, const char *
 
        ao2_find(pool->pool_container, search_topic_name, OBJ_SEARCH_KEY | OBJ_NODATA | OBJ_UNLINK);
 }
+/*!
+ * \brief Get a topic from the pool for the given name.
+ *
+ * This returns a **borrowed** reference: the pool container owns the topic
+ * and callers MUST NOT ao2_cleanup() the returned pointer.
+ *
+ * To avoid both deadlocks and wasted work we use a per-name "in-flight"
+ * topic_pool_entry while a topic is being created:
+ *
+ *  - The pool container lock is held only while looking up or inserting
+ *    the topic_pool_entry for a name.
+ *  - Exactly one thread becomes the creator for a given name and is
+ *    responsible for allocating the topic and wiring up forwarding.
+ *  - Other threads that race for the same name find the in-flight entry
+ *    and wait on its condition variable until initialization completes.
+ */
 
 struct stasis_topic *stasis_topic_pool_get_topic(struct stasis_topic_pool *pool, const char *topic_name)
 {
-       RAII_VAR(struct topic_pool_entry *, topic_pool_entry, NULL, ao2_cleanup);
-       SCOPED_AO2LOCK(topic_container_lock, pool->pool_container);
-       char *new_topic_name;
+       /*
+       * Lock ordering:
+       *
+       *   pool->pool_container (AO2 lock)
+       *       → entry->init_lock
+       *           → topic locks (inside stasis_topic_create() /
+       *                            stasis_forward_all())
+       *
+       * We intentionally do NOT hold the pool container lock while calling
+       * stasis_topic_create() or stasis_forward_all() to avoid deadlocks with
+       * other code that may take topic locks first and then need the pool lock.
+       */
+       RAII_VAR(struct topic_pool_entry *, entry, NULL, ao2_cleanup);
+       char *fq = NULL;
+       int creator = 0;
        int ret;
 
-       topic_pool_entry = ao2_find(pool->pool_container, topic_name, OBJ_SEARCH_KEY | OBJ_NOLOCK);
-       if (topic_pool_entry) {
-               return topic_pool_entry->topic;
-       }
-
-       topic_pool_entry = topic_pool_entry_alloc(topic_name);
-       if (!topic_pool_entry) {
+       if (!pool || ast_strlen_zero(topic_name)) {
                return NULL;
        }
 
-       /* To provide further detail and to ensure that the topic is unique within the scope of the
-        * system we prefix it with the pooling topic name, which should itself already be unique.
+       /* Creator / waiter split:
+        *
+        * - The first thread to create/link an entry for topic_name becomes the
+        *   "creator" and is responsible for creating the underlying stasis
+        *   topic and wiring up forwarding.
+        *
+        * - Other threads that find the entry become "waiters"; they block on
+        *   entry->init_cond until either initialization succeeds or fails.
         */
-       ret = ast_asprintf(&new_topic_name, "%s/%s", stasis_topic_name(pool->pool_topic), topic_name);
-       if (ret < 0) {
-               return NULL;
+
+       /* --- Creator selection under pool container lock --- */
+       ao2_lock(pool->pool_container);
+
+       entry = ao2_find(pool->pool_container, topic_name, OBJ_SEARCH_KEY | OBJ_NOLOCK);
+       if (!entry) {
+               entry = topic_pool_entry_alloc(topic_name);
+               if (!entry) {
+                       ao2_unlock(pool->pool_container);
+                       return NULL;
+               }
+
+               if (!ao2_link_flags(pool->pool_container, entry, OBJ_NOLOCK)) {
+                       struct topic_pool_entry *other;
+
+                       other = ao2_find(pool->pool_container, topic_name, OBJ_SEARCH_KEY | OBJ_NOLOCK);
+                       if (other) {
+                               struct topic_pool_entry *tmp = entry;
+
+                               entry = other;
+                               creator = 0;
+                               ao2_unlock(pool->pool_container);
+                               ao2_ref(tmp, -1);
+                               goto waiter_path;
+                       }
+                       
+                       ao2_unlock(pool->pool_container);
+                       return NULL;
+               }
+
+               creator = 1;
        }
 
-       topic_pool_entry->topic = stasis_topic_create(new_topic_name);
-       ast_free(new_topic_name);
-       if (!topic_pool_entry->topic) {
+       ao2_unlock(pool->pool_container);
+
+/* --- Waiter path: wait for creator to finish --- */
+waiter_path:
+       if (!creator) {
+               ast_mutex_lock(&entry->init_lock);
+               while (!entry->initialized && !entry->failed) {
+                       ast_cond_wait(&entry->init_cond, &entry->init_lock);
+               }
+
+               if (entry->initialized && !entry->failed) {
+                       struct stasis_topic *topic = entry->topic;
+                       
+                       if (!topic) {
+                               ast_debug(1, "Pooled topic '%s' marked initialized but topic is NULL\n", entry->name);
+                               ast_mutex_unlock(&entry->init_lock);
+                               return NULL;
+                       }
+                       ast_mutex_unlock(&entry->init_lock);
+                       /* Borrowed reference: container owns the topic */
+                       return topic;
+               }
+
+               ast_mutex_unlock(&entry->init_lock);
                return NULL;
        }
 
-       topic_pool_entry->forward = stasis_forward_all(topic_pool_entry->topic, pool->pool_topic);
-       if (!topic_pool_entry->forward) {
-               return NULL;
+       /* --- Creator path: perform topic creation without pool lock --- */
+       ast_mutex_lock(&entry->init_lock);
+       /* Defensive: entry may have been initialized/failed before we acquired init_lock. */
+       if (entry->initialized || entry->failed) {
+               struct stasis_topic *topic = entry->initialized ? entry->topic : NULL;
+               ast_mutex_unlock(&entry->init_lock);
+               return topic;
        }
 
-       if (!ao2_link_flags(pool->pool_container, topic_pool_entry, OBJ_NOLOCK)) {
-               return NULL;
+       ret = ast_asprintf(&fq, "%s/%s", stasis_topic_name(pool->pool_topic), topic_name);
+       if (ret < 0) {
+               entry->failed = 1;
+               goto creator_fail;
+       }
+
+       entry->topic = stasis_topic_create(fq);
+       ast_free(fq);
+       fq = NULL;
+
+       if (!entry->topic) {
+               entry->failed = 1;
+               goto creator_fail;
+       }
+
+       entry->forward = stasis_forward_all(entry->topic, pool->pool_topic);
+       if (!entry->forward) {
+               ao2_cleanup(entry->topic);
+               entry->topic = NULL;
+               entry->failed = 1;
+               goto creator_fail;
        }
 
-       return topic_pool_entry->topic;
+       entry->initialized = 1;
+       ast_cond_broadcast(&entry->init_cond);
+       ast_mutex_unlock(&entry->init_lock);
+
+       return entry->topic; /* borrowed ref */
+
+creator_fail:
+       ast_debug(1, "Failed to create pooled stasis topic '%s/%s'\n", stasis_topic_name(pool->pool_topic), entry->name);
+       ast_cond_broadcast(&entry->init_cond);
+       ast_mutex_unlock(&entry->init_lock);
+
+       /* Remove failed entry so future callers can retry */
+       ao2_lock(pool->pool_container);
+       ao2_unlink(pool->pool_container, entry);
+       ao2_unlock(pool->pool_container);
+
+       return NULL;
 }
 
 int stasis_topic_pool_topic_exists(const struct stasis_topic_pool *pool, const char *topic_name)