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);
}
}