Commit 176ecdad3c for asterisk.org

commit 176ecdad3cc97437d552d180936faf54c8f2b432
Author: Peter Krall <pekkaar@gmail.com>
Date:   Fri Apr 17 14:35:31 2026 +0200

    res_stasis/resource_bridges: Split bridge playback control and wrapper cleanup

    Modified the bridge playback teardown so the worker thread removes only the
    playback control, while the after-bridge callback removes the playback
    wrapper once the announcer has actually left the bridge.

    This avoids a stale window where a new playback request could create a
    replacement announcer before the old announcer had fully exited the holding
    bridge.

    Also replaced the flexible trailing bridge_id storage in the shared worker
    thread data with an optional bridge_id pointer, since recording paths use the
    same structure without a bridge id.

    Fixes: #1861

diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h
index 8a41f555d0..44f811cf7f 100644
--- a/include/asterisk/stasis_app.h
+++ b/include/asterisk/stasis_app.h
@@ -795,6 +795,15 @@ int stasis_app_bridge_playback_channel_add(struct ast_bridge *bridge,
 	struct ast_channel *chan,
 	struct stasis_app_control *control);

+/*!
+ * \brief Remove a bridge playback channel's control from the app controls list.
+ *
+ * \param bridge_id The unique ID of the bridge the playback channel is in.
+ * \param control The app control structure for the playback channel
+ */
+void stasis_app_bridge_playback_channel_control_remove(const char *bridge_id,
+	struct stasis_app_control *control);
+
 /*!
  * \brief remove channel from list of ARI playback channels for bridges.
  *
diff --git a/res/ari/resource_bridges.c b/res/ari/resource_bridges.c
index 0baaa44b1f..712f6a5ac1 100644
--- a/res/ari/resource_bridges.c
+++ b/res/ari/resource_bridges.c
@@ -282,7 +282,7 @@ struct bridge_channel_control_thread_data {
 	struct ast_channel *bridge_channel;
 	struct stasis_app_control *control;
 	struct stasis_forward *forward;
-	char bridge_id[0];
+	char *bridge_id;
 };

 static void *bridge_channel_control_thread(void *data)
@@ -292,7 +292,7 @@ static void *bridge_channel_control_thread(void *data)
 	struct stasis_app_control *control = thread_data->control;
 	struct stasis_forward *forward = thread_data->forward;
 	ast_callid callid = ast_channel_callid(bridge_channel);
-	char *bridge_id = ast_strdupa(thread_data->bridge_id);
+	char *bridge_id = thread_data->bridge_id;

 	if (callid) {
 		ast_callid_threadassoc_add(callid);
@@ -304,7 +304,10 @@ static void *bridge_channel_control_thread(void *data)
 	stasis_app_control_execute_until_exhausted(bridge_channel, control);
 	stasis_app_control_flush_queue(control);

-	stasis_app_bridge_playback_channel_remove(bridge_id, control);
+	if (bridge_id) {
+		stasis_app_bridge_playback_channel_control_remove(bridge_id, control);
+		ast_free(bridge_id);
+	}
 	stasis_forward_cancel(forward);
 	ao2_cleanup(control);
 	ast_hangup(bridge_channel);
@@ -532,7 +535,7 @@ static void ari_bridges_play_new(const char **args_media,
 	ast_bridge_queue_everyone_else(bridge, NULL, &prog);

 	/* Give play_channel and control reference to the thread data */
-	thread_data = ast_malloc(sizeof(*thread_data) + strlen(bridge->uniqueid) + 1);
+	thread_data = ast_calloc(1, sizeof(*thread_data));
 	if (!thread_data) {
 		stasis_app_bridge_playback_channel_remove((char *)bridge->uniqueid, control);
 		ast_ari_response_alloc_failed(response);
@@ -542,11 +545,17 @@ static void ari_bridges_play_new(const char **args_media,
 	thread_data->bridge_channel = play_channel;
 	thread_data->control = control;
 	thread_data->forward = channel_forward;
-	/* Safe */
-	strcpy(thread_data->bridge_id, bridge->uniqueid);
+	thread_data->bridge_id = ast_strdup(bridge->uniqueid);
+	if (!thread_data->bridge_id) {
+		stasis_app_bridge_playback_channel_remove((char *)bridge->uniqueid, control);
+		ast_ari_response_alloc_failed(response);
+		ast_free(thread_data);
+		return;
+	}

 	if (ast_pthread_create_detached(&threadid, NULL, bridge_channel_control_thread, thread_data)) {
 		stasis_app_bridge_playback_channel_remove((char *)bridge->uniqueid, control);
+		ast_free(thread_data->bridge_id);
 		ast_ari_response_alloc_failed(response);
 		ast_free(thread_data);
 		return;
@@ -653,7 +662,7 @@ static void ari_bridges_handle_play(
 		 * in which case we'll revert to ari_bridges_play_new.
 		 */
 		if (ari_bridges_play_found(args_media, args_media_count, args_lang,
-				args_offset_ms, args_skipms, args_playback_id, response,bridge,
+				args_offset_ms, args_skipms, args_playback_id, response, bridge,
 				play_channel) == PLAY_FOUND_CHANNEL_UNAVAILABLE) {
 			continue;
 		}
diff --git a/res/res_stasis.c b/res/res_stasis.c
index cc17a21b4f..6912f3a9c5 100644
--- a/res/res_stasis.c
+++ b/res/res_stasis.c
@@ -714,6 +714,13 @@ static void remove_bridge_playback(char *bridge_id)
 	ast_free(bridge_id);
 }

+void stasis_app_bridge_playback_channel_control_remove(const char *bridge_id,
+	struct stasis_app_control *control)
+{
+	ast_assert(!ast_strlen_zero(bridge_id));
+	ao2_unlink(app_controls, control);
+}
+
 static void playback_after_bridge_cb_failed(enum ast_bridge_after_cb_reason reason, void *data)
 {
 	char *bridge_id = data;
@@ -777,7 +784,7 @@ void stasis_app_bridge_playback_channel_remove(char *bridge_id,
 		 * called or is in progress. No need to unlink the control here since that has
 		 * been done or is about to be done in the after bridge callback
 		 */
-		ao2_unlink(app_controls, control);
+		stasis_app_bridge_playback_channel_control_remove(bridge_id, control);
 		ao2_ref(wrapper, -1);
 	}
 }