Commit 55e4febcde for asterisk.org

commit 55e4febcde26475c52645ffffb168e61fb74fc1a
Author: Yury Kirsanov <y.kirsanov@gmail.com>
Date:   Thu Oct 22 13:11:52 2026 +1100

    res_pjsip_refer: Fix race that can leave the bridge:all topic locked

    refer_blind_callback() subscribes the REFER progress object to the
    bridge:all topic and then applies message type filters to the new
    subscription. The subscription is live as soon as stasis_subscribe_pool()
    returns, and in the blind transfer path ast_bridge_transfer_blind() has
    already set the transfer's completed flag, so another thread can
    unsubscribe it while the filters are still being applied:

    * refer_progress_framehook_destroy(), on the transferee's channel
      thread, when the progress frame hook is detached or the channel is
      destroyed.
    * refer_progress_bridge(), on a stasis pool thread, when the transferee
      enters a bridge.

    stasis_subscription_accept_message_type() locks subscription->topic,
    updates the accepted message types and unlocks subscription->topic, and
    holds no reference on the subscription while doing so. If the final
    unref lands between the lock and the unlock, subscription_dtor() sets
    sub->topic to NULL, the unlock is called on NULL ("FRACK!, Failed
    assertion user_data is NULL" from stasis_subscription_accept_message_type)
    and bridge:all is never unlocked. Every later publish to bridge:all then
    blocks: channels can no longer join or leave bridges, media stops,
    stasis taskprocessors back up past their high water mark, res_pjsip
    stops processing requests because of the taskprocessor overload alert,
    and "core stop now" hangs on the same lock.

    The progress object's lock is now held across the subscribe and the
    filter setup, and around both unsubscribe calls, so the two sides can no
    longer overlap. Nothing else locks the progress object. The frame hook
    destroy callback runs with the channel locked, so the lock order is
    channel, progress, topic. refer_blind_callback() holds no channel lock
    while it holds the progress lock, and bridge:all has no synchronous
    subscribers, so subscribing under the progress lock cannot call back
    into code that locks a channel.

    The failure branch now tests a flag set under the lock instead of
    re-reading progress->bridge_sub, which another thread may have cleared.

    Resolves: #930

diff --git a/res/res_pjsip_refer.c b/res/res_pjsip_refer.c
index 841cc83ac0..1a4585b74b 100644
--- a/res/res_pjsip_refer.c
+++ b/res/res_pjsip_refer.c
@@ -305,7 +305,9 @@ static void refer_progress_bridge(void *data, struct stasis_subscription *sub,
 		if (ast_sip_push_task(progress->serializer, refer_progress_notify, notification)) {
 			ao2_cleanup(notification);
 		}
+		ao2_lock(progress);
 		progress->bridge_sub = stasis_unsubscribe(progress->bridge_sub);
+		ao2_unlock(progress);
 	}

 	chan = ast_channel_get_by_name(progress->transferee);
@@ -470,9 +472,11 @@ static void refer_progress_framehook_destroy(void *data)
 		}
 	}

+	ao2_lock(progress);
 	if (progress->bridge_sub) {
 		progress->bridge_sub = stasis_unsubscribe(progress->bridge_sub);
 	}
+	ao2_unlock(progress);

 	ao2_cleanup(progress);
 	SCOPE_EXIT_RTN("%p\n", progress);
@@ -834,6 +838,7 @@ static void refer_blind_callback(struct ast_channel *chan, struct transfer_chann
 			.data = refer->progress,
 			.disable_inheritance = 1,
 		};
+		int subscribed;

 		refer->progress->transferee = ast_strdup(ast_channel_uniqueid(chan));
 		if (!refer->progress->transferee) {
@@ -881,9 +886,22 @@ static void refer_blind_callback(struct ast_channel *chan, struct transfer_chann
 		ao2_ref(refer->progress, +1);
 		/* We also will need to detect if the transferee enters a bridge. This is currently the only reliable way to
 		 * detect if the transfer target has answered the call
+		 *
+		 * The subscription is live as soon as it is created and another thread may unsubscribe
+		 * it while the filters below are applied, so hold the progress lock across both. The
+		 * unsubscribe sites take the same lock.
 		 */
+		ao2_lock(refer->progress);
 		refer->progress->bridge_sub = stasis_subscribe_pool(ast_bridge_topic_all(), refer_progress_bridge, refer->progress);
-		if (!refer->progress->bridge_sub) {
+		subscribed = refer->progress->bridge_sub != NULL;
+		if (subscribed) {
+			stasis_subscription_accept_message_type(refer->progress->bridge_sub, ast_channel_entered_bridge_type());
+			stasis_subscription_accept_message_type(refer->progress->bridge_sub, stasis_subscription_change_type());
+			stasis_subscription_set_filter(refer->progress->bridge_sub, STASIS_SUBSCRIPTION_FILTER_SELECTIVE);
+		}
+		ao2_unlock(refer->progress);
+
+		if (!subscribed) {
 			struct refer_progress_notification *notification = refer_progress_notification_alloc(refer->progress, 200,
 				PJSIP_EVSUB_STATE_TERMINATED);

@@ -901,10 +919,6 @@ static void refer_blind_callback(struct ast_channel *chan, struct transfer_chann
 			ast_channel_unlock(chan);

 			ao2_cleanup(refer->progress);
-		} else {
-			stasis_subscription_accept_message_type(refer->progress->bridge_sub, ast_channel_entered_bridge_type());
-			stasis_subscription_accept_message_type(refer->progress->bridge_sub, stasis_subscription_change_type());
-			stasis_subscription_set_filter(refer->progress->bridge_sub, STASIS_SUBSCRIPTION_FILTER_SELECTIVE);
 		}
 	}