Commit 97c9e9e674 for asterisk.org
commit 97c9e9e67460918b3932735260457a305d728c9c
Author: Mike Bradeen <mbradeen@sangoma.com>
Date: Tue Sep 29 15:07:33 2026 -0600
chan_pjsip: Prevent CHANNEL() deadlock during call pickup
CHANNEL() reads could previously wait on a PJSIP serializer while
holding the channel lock, resulting in a deadlock with concurrent
masquerade and update processing.
To avoid this, we now read immutable values directly and cache mutable
values via snapshot in a channel-owned datastore protected by the
channel lock. The cache is Refreshed from the session serializer so
readers no longer wait on it.
This also requires protecting T.38 state updates and termination-time
media-state swaps with the channel lock.
Fixes: #2036
diff --git a/channels/chan_pjsip.c b/channels/chan_pjsip.c
index 403637b482..517afaec93 100644
--- a/channels/chan_pjsip.c
+++ b/channels/chan_pjsip.c
@@ -69,6 +69,7 @@
#include "asterisk/stream.h"
#include "pjsip/include/chan_pjsip.h"
+#include "pjsip/include/channel_info.h"
#include "pjsip/include/dialplan_functions.h"
#include "pjsip/include/cli_functions.h"
@@ -516,6 +517,7 @@ static int send_direct_media_request(void *data)
cdata->chan, cdata->vrtp, session->active_media_state->default_session[AST_MEDIA_TYPE_VIDEO], session);
}
ast_channel_unlock(cdata->chan);
+ pjsip_channel_info_update(session);
if (direct_media_mitigate_glare(cdata->session)) {
ast_debug(4, "Disregarding setting RTP on %s: mitigating re-INVITE glare\n", ast_channel_name(cdata->chan));
@@ -680,6 +682,14 @@ static struct ast_channel *chan_pjsip_new(struct ast_sip_session *session, int s
}
ast_channel_tech_pvt_set(chan, channel);
+ if (pjsip_channel_info_create(chan, session)) {
+ /* Keep technology hangup from accessing the not yet attached session. */
+ ast_channel_tech_pvt_set(chan, NULL);
+ ao2_cleanup(channel);
+ ast_channel_unlock(chan);
+ ast_hangup(chan);
+ SCOPE_EXIT_RTN_VALUE(NULL, "Couldn't create PJSIP channel state\n");
+ }
if (!ast_stream_topology_get_count(session->pending_media_state->topology) ||
!compatible_formats_exist(session->pending_media_state->topology, session->endpoint->media.codecs)) {
@@ -2622,14 +2632,24 @@ static struct hangup_data *hangup_data_alloc(int cause, struct ast_channel *chan
return h_data;
}
-/*! \brief Clear a channel from a session along with its PVT */
-static void clear_session_and_channel(struct ast_sip_session *session, struct ast_channel *ast)
+/*! \brief Clear a channel from a session along with its PVT (channel lock held) */
+static void clear_session_and_channel_locked(struct ast_sip_session *session,
+ struct ast_channel *ast)
{
session->channel = NULL;
set_channel_on_rtp_instance(session, "");
ast_channel_tech_pvt_set(ast, NULL);
}
+/*! \brief Clear a channel from a session along with its PVT */
+static void clear_session_and_channel(struct ast_sip_session *session, struct ast_channel *ast)
+{
+ /* Serialize detachment with channel-locked readers of the session. */
+ ast_channel_lock(ast);
+ clear_session_and_channel_locked(session, ast);
+ ast_channel_unlock(ast);
+}
+
static int hangup(void *data)
{
struct hangup_data *h_data = data;
@@ -2709,7 +2729,7 @@ failure:
/* Go ahead and do our cleanup of the session and channel even if we're not going
* to be able to send our SIP request/response
*/
- clear_session_and_channel(channel->session, ast);
+ clear_session_and_channel_locked(channel->session, ast);
ao2_cleanup(channel);
ao2_cleanup(h_data);
@@ -3537,6 +3557,7 @@ static int load_module(void)
ast_format_cap_append_by_type(chan_pjsip_tech.capabilities, AST_MEDIA_TYPE_AUDIO);
ast_rtp_glue_register(&chan_pjsip_rtp_glue);
+ pjsip_channel_info_register();
if (ast_channel_register(&chan_pjsip_tech)) {
ast_log(LOG_ERROR, "Unable to register channel class %s\n", channel_type);
@@ -3622,6 +3643,7 @@ static int load_module(void)
return 0;
end:
+ pjsip_channel_info_unregister();
ao2_cleanup(pjsip_uids_onhold);
pjsip_uids_onhold = NULL;
ast_sip_session_unregister_supplement(&chan_pjsip_ack_supplement);
@@ -3651,6 +3673,7 @@ end:
/*! \brief Unload the PJSIP channel from Asterisk */
static int unload_module(void)
{
+ pjsip_channel_info_unregister();
ao2_cleanup(pjsip_uids_onhold);
pjsip_uids_onhold = NULL;
diff --git a/channels/pjsip/channel_info.c b/channels/pjsip/channel_info.c
new file mode 100644
index 0000000000..67dd7c78ec
--- /dev/null
+++ b/channels/pjsip/channel_info.c
@@ -0,0 +1,297 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2026, Sangoma Technologies Corporation
+ *
+ * Michael Bradeen <mbradeen@sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*!
+ * \file
+ * \brief Channel-locked PJSIP information for CHANNEL() reads
+ *
+ * The channel datastore owns cached dialog URIs, the remote tag, signaling
+ * security, RTP instance references, and media security and hold flags.
+ * Readers and writers access the cache under the channel lock. A datastore
+ * lookup returns a borrowed pointer valid only while that lock is held.
+ *
+ * Session serializer tasks refresh the cache through session->channel.
+ * SIP supplements refresh it during incoming message processing and queue
+ * refreshes for subsequent changes. Masquerades suspend the serializer while
+ * changing the channel association and move the datastore with the channel.
+ *
+ * CHANNEL() reads use the cache for values that SIP and SDP processing can
+ * change without the channel lock. Stable identity and original request
+ * information are read directly from the session. Direct-media addresses are
+ * read under the channel lock. RTP APIs supply RTP addresses and statistics.
+ *
+ * These reads do not wait for the session serializer or acquire the dialog
+ * lock while holding the channel lock to avoid a deadlock with masquerade
+ * processing.
+ *
+ */
+
+#include "asterisk.h"
+
+#include <pjsip.h>
+
+#include "asterisk/astobj2.h"
+#include "asterisk/channel.h"
+#include "asterisk/datastore.h"
+#include "asterisk/res_pjsip.h"
+#include "asterisk/res_pjsip_session.h"
+#include "asterisk/rtp_engine.h"
+
+#include "include/channel_info.h"
+
+static void channel_info_destroy(void *data)
+{
+ struct pjsip_channel_info *info = data;
+
+ ast_free(info->remote_tag);
+ ao2_cleanup(info->audio_rtp);
+ ao2_cleanup(info->video_rtp);
+ ast_free(info);
+}
+
+static void channel_info_breakdown(void *data, struct ast_channel *old_chan,
+ struct ast_channel *new_chan)
+{
+ struct pjsip_channel_info *info = data;
+ struct ast_datastore *datastore = info->datastore;
+
+ /*
+ * Core calls breakdown on the destination's old datastores before moving
+ * the source's datastores over. Both channels are locked. The moved
+ * datastore needs no fixup because it contains no channel pointer.
+ */
+ if (!ast_channel_datastore_remove(new_chan, datastore)) {
+ ast_datastore_free(datastore);
+ }
+}
+
+static const struct ast_datastore_info channel_info_datastore = {
+ .type = "chan_pjsip_channel_info",
+ .destroy = channel_info_destroy,
+ .chan_breakdown = channel_info_breakdown,
+};
+
+struct pjsip_channel_info *pjsip_channel_info_get(struct ast_channel *chan)
+{
+ struct ast_datastore *datastore;
+
+ datastore = ast_channel_datastore_find(chan, &channel_info_datastore, NULL);
+ return datastore ? datastore->data : NULL;
+}
+
+/* Return whether the value changed, preserving print failures for the reader. */
+static int copy_uri(struct pjsip_channel_uri *dest, pjsip_uri_context_e context,
+ const void *uri, const char *type)
+{
+ char value[PJSIP_MAX_URL_SIZE] = "";
+ int length = 0;
+ int changed;
+
+ if (uri) {
+ length = pjsip_uri_print(context, uri, value, sizeof(value));
+ if (length < 0 || length >= sizeof(value)) {
+ ast_log(LOG_ERROR, "Unable to capture PJSIP %s in %zu bytes\n",
+ type, sizeof(value));
+ value[0] = '\0';
+ length = -1;
+ } else {
+ value[length] = '\0';
+ }
+ }
+ changed = dest->length != length || strcmp(dest->value, value);
+ ast_copy_string(dest->value, value, sizeof(dest->value));
+ dest->length = length;
+ return changed;
+}
+
+static void channel_info_update_locked(struct pjsip_channel_info *info,
+ struct ast_sip_session *session)
+{
+ pjsip_dialog *dlg = session->inv_session ? session->inv_session->dlg : NULL;
+ struct ast_sip_session_media *media;
+
+ if (dlg) {
+ int target_changed;
+ const pj_str_t *tag = &dlg->remote.info->tag;
+
+ target_changed = copy_uri(&info->target_uri, PJSIP_URI_IN_REQ_URI,
+ dlg->target, "target_uri");
+ copy_uri(&info->local_uri, PJSIP_URI_IN_FROMTO_HDR,
+ dlg->local.info->uri, "local_uri");
+ copy_uri(&info->remote_uri, PJSIP_URI_IN_FROMTO_HDR,
+ dlg->remote.info->uri, "remote_uri");
+ /* PJSIP_MAX_TAG_LEN describes generated tags, not a received-tag limit. */
+ if (!info->remote_tag || strlen(info->remote_tag) != tag->slen
+ || (tag->slen && memcmp(info->remote_tag, tag->ptr, tag->slen))) {
+ char *copy = ast_strndup(tag->slen ? tag->ptr : "", tag->slen);
+
+ ast_free(info->remote_tag);
+ info->remote_tag = copy;
+ }
+#ifdef HAVE_PJSIP_GET_DEST_INFO
+ if (target_changed || info->target_uri.length < 0 || info->secure < 0) {
+ pjsip_host_info dest;
+ pj_pool_t *pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(),
+ "channel-info", 128, 128);
+
+ info->secure = -1;
+ if (pool) {
+ if (pjsip_get_dest_info(dlg->target, NULL, pool, &dest) == PJ_SUCCESS) {
+ info->secure = !!(dest.flag & PJSIP_TRANSPORT_SECURE);
+ }
+ pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool);
+ }
+ }
+#else
+ ast_log(LOG_WARNING, "Asterisk has been built against a version of pjproject which does not have the required functionality to support the 'secure' argument. Please upgrade to version 2.3 or later.\n");
+ (void) target_changed;
+#endif
+ } else {
+ copy_uri(&info->target_uri, PJSIP_URI_IN_REQ_URI, NULL, "target_uri");
+ copy_uri(&info->local_uri, PJSIP_URI_IN_FROMTO_HDR, NULL, "local_uri");
+ copy_uri(&info->remote_uri, PJSIP_URI_IN_FROMTO_HDR, NULL, "remote_uri");
+ ast_free(info->remote_tag);
+ info->remote_tag = NULL;
+ info->secure = -1;
+ }
+
+ media = session->active_media_state
+ ? session->active_media_state->default_session[AST_MEDIA_TYPE_AUDIO] : NULL;
+ ao2_replace(info->audio_rtp, media ? media->rtp : NULL);
+ info->audio_secure = media && media->srtp
+ && ast_test_flag(media->srtp, AST_SRTP_CRYPTO_OFFER_OK);
+ info->audio_held = media && media->remotely_held;
+ media = session->active_media_state
+ ? session->active_media_state->default_session[AST_MEDIA_TYPE_VIDEO] : NULL;
+ ao2_replace(info->video_rtp, media ? media->rtp : NULL);
+ info->video_secure = media && media->srtp
+ && ast_test_flag(media->srtp, AST_SRTP_CRYPTO_OFFER_OK);
+ info->video_held = media && media->remotely_held;
+}
+
+int pjsip_channel_info_create(struct ast_channel *chan,
+ struct ast_sip_session *session)
+{
+ struct ast_datastore *datastore;
+ struct pjsip_channel_info *info;
+
+ datastore = ast_datastore_alloc(&channel_info_datastore, NULL);
+ if (!datastore) {
+ return -1;
+ }
+ info = ast_calloc(1, sizeof(*info));
+ if (!info) {
+ ast_datastore_free(datastore);
+ return -1;
+ }
+ info->datastore = datastore;
+ info->secure = -1;
+ datastore->data = info;
+
+ /* The caller holds the channel lock and is running in the serializer. */
+ channel_info_update_locked(info, session);
+ ast_channel_datastore_add(chan, datastore);
+ return 0;
+}
+
+void pjsip_channel_info_update(struct ast_sip_session *session)
+{
+ struct ast_channel *chan = session->channel;
+ struct pjsip_channel_info *info;
+
+ /* Serializer execution stabilizes session->channel across a masquerade. */
+ if (!chan) {
+ return;
+ }
+ ast_channel_lock(chan);
+ info = pjsip_channel_info_get(chan);
+ if (info) {
+ channel_info_update_locked(info, session);
+ }
+ ast_channel_unlock(chan);
+}
+
+static int channel_info_update_task(void *data)
+{
+ struct ast_sip_session *session = data;
+
+ pjsip_channel_info_update(session);
+ ao2_ref(session, -1);
+ return 0;
+}
+
+void pjsip_channel_info_schedule_update(struct ast_sip_session *session)
+{
+ /*
+ * Capture changes made after supplements or from an outgoing transport
+ * callback outside the serializer. Never wait for the queued refresh.
+ */
+ if (ast_sip_push_task(session->serializer, channel_info_update_task,
+ ao2_bump(session))) {
+ ast_log(LOG_WARNING, "Unable to queue PJSIP channel information refresh for %s\n",
+ ast_sip_session_get_name(session));
+ ao2_ref(session, -1);
+ }
+}
+
+static int channel_info_incoming_request(struct ast_sip_session *session,
+ struct pjsip_rx_data *rdata)
+{
+ pjsip_channel_info_update(session);
+ pjsip_channel_info_schedule_update(session);
+ return 0;
+}
+
+static void channel_info_incoming_response(struct ast_sip_session *session,
+ struct pjsip_rx_data *rdata)
+{
+ pjsip_channel_info_update(session);
+ pjsip_channel_info_schedule_update(session);
+}
+
+static void channel_info_outgoing_request(struct ast_sip_session *session,
+ struct pjsip_tx_data *tdata)
+{
+ pjsip_channel_info_schedule_update(session);
+}
+
+static void channel_info_outgoing_response(struct ast_sip_session *session,
+ struct pjsip_tx_data *tdata)
+{
+ pjsip_channel_info_schedule_update(session);
+}
+
+static struct ast_sip_session_supplement channel_info_supplement = {
+ /* Refresh before channel supplements publish snapshots or queue control frames. */
+ .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL - 1,
+ .incoming_request = channel_info_incoming_request,
+ .incoming_response = channel_info_incoming_response,
+ .outgoing_request = channel_info_outgoing_request,
+ .outgoing_response = channel_info_outgoing_response,
+ .response_priority = AST_SIP_SESSION_AFTER_MEDIA,
+};
+
+void pjsip_channel_info_register(void)
+{
+ ast_sip_session_register_supplement(&channel_info_supplement);
+}
+
+void pjsip_channel_info_unregister(void)
+{
+ ast_sip_session_unregister_supplement(&channel_info_supplement);
+}
diff --git a/channels/pjsip/dialplan_functions.c b/channels/pjsip/dialplan_functions.c
index 594a56601b..003c8ca3b8 100644
--- a/channels/pjsip/dialplan_functions.c
+++ b/channels/pjsip/dialplan_functions.c
@@ -48,6 +48,7 @@
#include "asterisk/res_pjsip.h"
#include "asterisk/res_pjsip_session.h"
#include "include/chan_pjsip.h"
+#include "include/channel_info.h"
#include "include/dialplan_functions.h"
/*!
@@ -62,64 +63,58 @@ static const char *t38state_to_string[T38_MAX_ENUM] = {
};
/*!
- * \internal \brief Handle reading RTP information
+ * \internal \brief Escape a captured URI into the dialplan result buffer
*/
-static int channel_read_rtp(struct ast_channel *chan, const char *type, const char *field, char *buf, size_t buflen)
+static int copy_escaped_uri(struct ast_channel *chan, const char *type,
+ const struct pjsip_channel_uri *uri, char *buf, size_t size)
{
- struct ast_sip_channel_pvt *channel = ast_channel_tech_pvt(chan);
- struct ast_sip_session *session;
- struct ast_sip_session_media *media;
- struct ast_sockaddr addr;
-
- if (!channel) {
- ast_log(AST_LOG_WARNING, "Channel %s has no pvt!\n", ast_channel_name(chan));
+ if (uri->length < 0 || uri->length >= size) {
+ ast_log(LOG_ERROR, "Channel %s: Unescaped %s too long for %zu byte buffer\n",
+ ast_channel_name(chan), type, size);
+ buf[0] = '\0';
return -1;
}
+ ast_escape_quoted(uri->value, buf, size);
+ return 0;
+}
- session = channel->session;
- if (!session) {
- ast_log(AST_LOG_WARNING, "Channel %s has no session!\n", ast_channel_name(chan));
- return -1;
- }
+/*!
+ * \internal \brief Handle reading RTP information
+ */
+static int channel_read_rtp(struct ast_channel *chan, struct ast_rtp_instance *rtp,
+ const struct ast_sockaddr *direct_media_addr, unsigned int secure,
+ unsigned int held, const char *type, const char *field, char *buf, size_t buflen)
+{
+ struct ast_sockaddr addr;
if (ast_strlen_zero(type)) {
ast_log(AST_LOG_WARNING, "You must supply a type field for 'rtp' information\n");
return -1;
}
- if (ast_strlen_zero(field) || !strcmp(field, "audio")) {
- media = session->active_media_state->default_session[AST_MEDIA_TYPE_AUDIO];
- } else if (!strcmp(field, "video")) {
- media = session->active_media_state->default_session[AST_MEDIA_TYPE_VIDEO];
- } else {
+ if (!ast_strlen_zero(field) && strcmp(field, "audio") && strcmp(field, "video")) {
ast_log(AST_LOG_WARNING, "Unknown media type field '%s' for 'rtp' information\n", field);
return -1;
}
- if (!media || !media->rtp) {
+ if (!rtp) {
ast_log(AST_LOG_WARNING, "Channel %s has no %s media/RTP session\n",
ast_channel_name(chan), S_OR(field, "audio"));
return -1;
}
if (!strcmp(type, "src")) {
- ast_rtp_instance_get_local_address(media->rtp, &addr);
+ ast_rtp_instance_get_local_address(rtp, &addr);
ast_copy_string(buf, ast_sockaddr_stringify(&addr), buflen);
} else if (!strcmp(type, "dest")) {
- ast_rtp_instance_get_remote_address(media->rtp, &addr);
+ ast_rtp_instance_get_remote_address(rtp, &addr);
ast_copy_string(buf, ast_sockaddr_stringify(&addr), buflen);
} else if (!strcmp(type, "direct")) {
- ast_copy_string(buf, ast_sockaddr_stringify(&media->direct_media_addr), buflen);
+ ast_copy_string(buf, ast_sockaddr_stringify(direct_media_addr), buflen);
} else if (!strcmp(type, "secure")) {
- if (media->srtp) {
- struct ast_sdp_srtp *srtp = media->srtp;
- int flag = ast_test_flag(srtp, AST_SRTP_CRYPTO_OFFER_OK);
- snprintf(buf, buflen, "%d", flag ? 1 : 0);
- } else {
- snprintf(buf, buflen, "%d", 0);
- }
+ snprintf(buf, buflen, "%u", secure);
} else if (!strcmp(type, "hold")) {
- snprintf(buf, buflen, "%d", media->remotely_held ? 1 : 0);
+ snprintf(buf, buflen, "%u", held);
} else {
ast_log(AST_LOG_WARNING, "Unknown type field '%s' specified for 'rtp' information\n", type);
return -1;
@@ -131,38 +126,20 @@ static int channel_read_rtp(struct ast_channel *chan, const char *type, const ch
/*!
* \internal \brief Handle reading RTCP information
*/
-static int channel_read_rtcp(struct ast_channel *chan, const char *type, const char *field, char *buf, size_t buflen)
+static int channel_read_rtcp(struct ast_channel *chan, struct ast_rtp_instance *rtp,
+ const char *type, const char *field, char *buf, size_t buflen)
{
- struct ast_sip_channel_pvt *channel = ast_channel_tech_pvt(chan);
- struct ast_sip_session *session;
- struct ast_sip_session_media *media;
-
- if (!channel) {
- ast_log(AST_LOG_WARNING, "Channel %s has no pvt!\n", ast_channel_name(chan));
- return -1;
- }
-
- session = channel->session;
- if (!session) {
- ast_log(AST_LOG_WARNING, "Channel %s has no session!\n", ast_channel_name(chan));
- return -1;
- }
-
if (ast_strlen_zero(type)) {
ast_log(AST_LOG_WARNING, "You must supply a type field for 'rtcp' information\n");
return -1;
}
- if (ast_strlen_zero(field) || !strcmp(field, "audio")) {
- media = session->active_media_state->default_session[AST_MEDIA_TYPE_AUDIO];
- } else if (!strcmp(field, "video")) {
- media = session->active_media_state->default_session[AST_MEDIA_TYPE_VIDEO];
- } else {
+ if (!ast_strlen_zero(field) && strcmp(field, "audio") && strcmp(field, "video")) {
ast_log(AST_LOG_WARNING, "Unknown media type field '%s' for 'rtcp' information\n", field);
return -1;
}
- if (!media || !media->rtp) {
+ if (!rtp) {
ast_log(AST_LOG_WARNING, "Channel %s has no %s media/RTP session\n",
ast_channel_name(chan), S_OR(field, "audio"));
return -1;
@@ -181,7 +158,7 @@ static int channel_read_rtcp(struct ast_channel *chan, const char *type, const c
stat_field = AST_RTP_INSTANCE_STAT_FIELD_QUALITY_MES;
}
- if (!ast_rtp_instance_get_quality(media->rtp, stat_field, buf, buflen)) {
+ if (!ast_rtp_instance_get_quality(rtp, stat_field, buf, buflen)) {
ast_log(AST_LOG_WARNING, "Unable to retrieve 'rtcp' statistics for %s\n", ast_channel_name(chan));
return -1;
}
@@ -238,7 +215,7 @@ static int channel_read_rtcp(struct ast_channel *chan, const char *type, const c
{ NULL, },
};
- if (ast_rtp_instance_get_stats(media->rtp, &stats, AST_RTP_INSTANCE_STAT_ALL)) {
+ if (ast_rtp_instance_get_stats(rtp, &stats, AST_RTP_INSTANCE_STAT_ALL)) {
ast_log(AST_LOG_WARNING, "Unable to retrieve 'rtcp' statistics for %s\n", ast_channel_name(chan));
return -1;
}
@@ -260,191 +237,127 @@ static int channel_read_rtcp(struct ast_channel *chan, const char *type, const c
return 0;
}
-static int print_escaped_uri(struct ast_channel *chan, const char *type,
- pjsip_uri_context_e context, const void *uri, char *buf, size_t size)
+/* The original Request-URI is cloned before channel creation and never changed. */
+static int read_request_uri(struct ast_channel *chan, struct ast_sip_session *session,
+ char *buf, size_t size)
{
- int res;
- char *buf_copy;
+ int length;
+ char *copy;
- res = pjsip_uri_print(context, uri, buf, size);
- if (res < 0) {
- ast_log(LOG_ERROR, "Channel %s: Unescaped %s too long for %d byte buffer\n",
- ast_channel_name(chan), type, (int) size);
-
- /* Empty buffer that likely is not terminated. */
+ if (!session->request_uri) {
+ return 0;
+ }
+ length = pjsip_uri_print(PJSIP_URI_IN_REQ_URI, session->request_uri, buf, size);
+ if (length < 0 || length >= size) {
+ ast_log(LOG_ERROR, "Channel %s: Request URI too long for %zu byte buffer\n",
+ ast_channel_name(chan), size);
buf[0] = '\0';
return -1;
}
-
- buf_copy = ast_strdupa(buf);
- ast_escape_quoted(buf_copy, buf, size);
+ buf[length] = '\0';
+ copy = ast_strdupa(buf);
+ ast_escape_quoted(copy, buf, size);
return 0;
}
-/*!
- * \internal \brief Handle reading signalling information
- */
-static int channel_read_pjsip(struct ast_channel *chan, const char *type, const char *field, char *buf, size_t buflen)
+/* The caller holds the channel lock. No dialog lock or serializer wait is allowed. */
+static int channel_read_pjsip(struct ast_channel *chan, struct ast_sip_session *session,
+ const char *type, char *buf, size_t len)
{
- struct ast_sip_channel_pvt *channel = ast_channel_tech_pvt(chan);
- char *buf_copy;
- pjsip_dialog *dlg;
- int res = 0;
-
- if (!channel) {
- ast_log(AST_LOG_WARNING, "Channel %s has no pvt!\n", ast_channel_name(chan));
- return -1;
- }
-
- dlg = channel->session->inv_session->dlg;
+ const struct pjsip_channel_info *info;
if (ast_strlen_zero(type)) {
ast_log(LOG_WARNING, "You must supply a type field for 'pjsip' information\n");
return -1;
- } else if (!strcmp(type, "call-id")) {
- snprintf(buf, buflen, "%.*s", (int) pj_strlen(&dlg->call_id->id), pj_strbuf(&dlg->call_id->id));
- } else if (!strcmp(type, "secure")) {
-#ifdef HAVE_PJSIP_GET_DEST_INFO
- pjsip_host_info dest;
- pj_pool_t *pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(), "secure-check", 128, 128);
- pjsip_get_dest_info(dlg->target, NULL, pool, &dest);
- snprintf(buf, buflen, "%d", dest.flag & PJSIP_TRANSPORT_SECURE ? 1 : 0);
- pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool);
-#else
- ast_log(LOG_WARNING, "Asterisk has been built against a version of pjproject which does not have the required functionality to support the 'secure' argument. Please upgrade to version 2.3 or later.\n");
- return -1;
-#endif
- } else if (!strcmp(type, "target_uri")) {
- res = print_escaped_uri(chan, type, PJSIP_URI_IN_REQ_URI, dlg->target, buf,
- buflen);
- } else if (!strcmp(type, "local_uri")) {
- res = print_escaped_uri(chan, type, PJSIP_URI_IN_FROMTO_HDR, dlg->local.info->uri,
- buf, buflen);
- } else if (!strcmp(type, "local_tag")) {
- ast_copy_pj_str(buf, &dlg->local.info->tag, buflen);
- buf_copy = ast_strdupa(buf);
- ast_escape_quoted(buf_copy, buf, buflen);
- } else if (!strcmp(type, "remote_uri")) {
- res = print_escaped_uri(chan, type, PJSIP_URI_IN_FROMTO_HDR,
- dlg->remote.info->uri, buf, buflen);
- } else if (!strcmp(type, "remote_tag")) {
- ast_copy_pj_str(buf, &dlg->remote.info->tag, buflen);
- buf_copy = ast_strdupa(buf);
- ast_escape_quoted(buf_copy, buf, buflen);
- } else if (!strcmp(type, "request_uri")) {
- if (channel->session->request_uri) {
- res = print_escaped_uri(chan, type, PJSIP_URI_IN_REQ_URI,
- channel->session->request_uri, buf, buflen);
- }
- } else if (!strcmp(type, "t38state")) {
- ast_copy_string(buf, t38state_to_string[channel->session->t38state], buflen);
- } else if (!strcmp(type, "local_addr")) {
- RAII_VAR(struct ast_datastore *, datastore, NULL, ao2_cleanup);
- struct transport_info_data *transport_data;
+ }
- datastore = ast_sip_session_get_datastore(channel->session, "transport_info");
- if (!datastore) {
- ast_log(AST_LOG_WARNING, "No transport information for channel %s\n", ast_channel_name(chan));
+ if (!strcmp(type, "call-id") || !strcmp(type, "local_tag")) {
+ pjsip_dialog *dlg = session->inv_session ? session->inv_session->dlg : NULL;
+
+ if (!dlg) {
return -1;
}
- transport_data = datastore->data;
+ /* These identities are fixed for the lifetime of the retained dialog. */
+ if (!strcmp(type, "call-id")) {
+ ast_copy_pj_str(buf, &dlg->call_id->id, len);
+ } else {
+ char *copy;
- if (pj_sockaddr_has_addr(&transport_data->local_addr)) {
- pj_sockaddr_print(&transport_data->local_addr, buf, buflen, 3);
+ ast_copy_pj_str(buf, &dlg->local.info->tag, len);
+ copy = ast_strdupa(buf);
+ ast_escape_quoted(copy, buf, len);
+ }
+ return 0;
+ }
+ if (!strcmp(type, "request_uri")) {
+ return read_request_uri(chan, session, buf, len);
+ }
+ if (!strcmp(type, "t38state")) {
+ if (session->t38state < 0 || session->t38state >= T38_MAX_ENUM) {
+ return -1;
}
- } else if (!strcmp(type, "remote_addr")) {
- RAII_VAR(struct ast_datastore *, datastore, NULL, ao2_cleanup);
- struct transport_info_data *transport_data;
+ ast_copy_string(buf, t38state_to_string[session->t38state], len);
+ return 0;
+ }
+ if (!strcmp(type, "local_addr") || !strcmp(type, "remote_addr")) {
+ RAII_VAR(struct ast_datastore *, datastore,
+ ast_sip_session_get_datastore(session, "transport_info"), ao2_cleanup);
+ struct transport_info_data *transport;
+ const pj_sockaddr *addr;
- datastore = ast_sip_session_get_datastore(channel->session, "transport_info");
if (!datastore) {
- ast_log(AST_LOG_WARNING, "No transport information for channel %s\n", ast_channel_name(chan));
+ ast_log(LOG_WARNING, "No transport information for channel %s\n",
+ ast_channel_name(chan));
return -1;
}
- transport_data = datastore->data;
-
- if (pj_sockaddr_has_addr(&transport_data->remote_addr)) {
- pj_sockaddr_print(&transport_data->remote_addr, buf, buflen, 3);
+ /* The transport payload is fully initialized before publication and immutable. */
+ transport = datastore->data;
+ addr = !strcmp(type, "local_addr") ? &transport->local_addr : &transport->remote_addr;
+ if (pj_sockaddr_has_addr(addr)) {
+ pj_sockaddr_print(addr, buf, len, 3);
}
- } else {
- ast_log(AST_LOG_WARNING, "Unrecognized argument '%s' for 'pjsip' information\n", type);
- return -1;
+ return 0;
}
- return res;
-}
-
-/*! \brief Struct used to push function arguments to task processor */
-struct pjsip_func_args {
- struct ast_sip_session *session;
- const char *param;
- const char *type;
- const char *field;
- char *buf;
- size_t len;
- int ret;
-};
-
-/*! \internal \brief Taskprocessor callback that handles the read on a PJSIP thread */
-static int read_pjsip(void *data)
-{
- struct pjsip_func_args *func_args = data;
-
- if (!strcmp(func_args->param, "rtp")) {
- if (!func_args->session->channel) {
- func_args->ret = -1;
- return 0;
- }
- func_args->ret = channel_read_rtp(func_args->session->channel, func_args->type,
- func_args->field, func_args->buf,
- func_args->len);
- } else if (!strcmp(func_args->param, "rtcp")) {
- if (!func_args->session->channel) {
- func_args->ret = -1;
- return 0;
- }
- func_args->ret = channel_read_rtcp(func_args->session->channel, func_args->type,
- func_args->field, func_args->buf,
- func_args->len);
- } else if (!strcmp(func_args->param, "endpoint")) {
- if (!func_args->session->endpoint) {
- ast_log(AST_LOG_WARNING, "Channel %s has no endpoint!\n", func_args->session->channel ?
- ast_channel_name(func_args->session->channel) : "<unknown>");
- func_args->ret = -1;
- return 0;
- }
- snprintf(func_args->buf, func_args->len, "%s", ast_sorcery_object_get_id(func_args->session->endpoint));
- } else if (!strcmp(func_args->param, "contact")) {
- if (!func_args->session->contact) {
- return 0;
- }
- snprintf(func_args->buf, func_args->len, "%s", ast_sorcery_object_get_id(func_args->session->contact));
- } else if (!strcmp(func_args->param, "aor")) {
- if (!func_args->session->aor) {
- return 0;
+ info = pjsip_channel_info_get(chan);
+ if (!info) {
+ ast_log(LOG_WARNING, "Channel %s has no PJSIP channel information\n",
+ ast_channel_name(chan));
+ return -1;
+ }
+ if (!strcmp(type, "secure")) {
+#ifdef HAVE_PJSIP_GET_DEST_INFO
+ if (info->secure < 0) {
+ return -1;
}
- snprintf(func_args->buf, func_args->len, "%s", ast_sorcery_object_get_id(func_args->session->aor));
- } else if (!strcmp(func_args->param, "pjsip")) {
- if (!func_args->session->channel) {
- func_args->ret = -1;
- return 0;
+ snprintf(buf, len, "%d", info->secure);
+#else
+ ast_log(LOG_WARNING, "Asterisk was built without support for CHANNEL(pjsip,secure)\n");
+ return -1;
+#endif
+ } else if (!strcmp(type, "target_uri")) {
+ return copy_escaped_uri(chan, type, &info->target_uri, buf, len);
+ } else if (!strcmp(type, "local_uri")) {
+ return copy_escaped_uri(chan, type, &info->local_uri, buf, len);
+ } else if (!strcmp(type, "remote_uri")) {
+ return copy_escaped_uri(chan, type, &info->remote_uri, buf, len);
+ } else if (!strcmp(type, "remote_tag")) {
+ if (!info->remote_tag) {
+ return -1;
}
- func_args->ret = channel_read_pjsip(func_args->session->channel, func_args->type,
- func_args->field, func_args->buf,
- func_args->len);
+ ast_escape_quoted(info->remote_tag, buf, len);
} else {
- func_args->ret = -1;
+ ast_log(LOG_WARNING, "Unrecognized argument '%s' for 'pjsip' information\n", type);
+ return -1;
}
-
return 0;
}
-
int pjsip_acf_channel_read(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len)
{
- struct pjsip_func_args func_args = { 0, };
struct ast_sip_channel_pvt *channel;
char *parse = ast_strdupa(data);
+ int res = 0;
AST_DECLARE_APP_ARGS(args,
AST_APP_ARG(param);
@@ -463,6 +376,11 @@ int pjsip_acf_channel_read(struct ast_channel *chan, const char *cmd, char *data
return -1;
}
+ if (!len) {
+ ast_log(LOG_ERROR, "Cannot call %s without an output buffer\n", cmd);
+ return -1;
+ }
+
AST_STANDARD_APP_ARGS(args, parse);
ast_channel_lock(chan);
@@ -487,24 +405,57 @@ int pjsip_acf_channel_read(struct ast_channel *chan, const char *cmd, char *data
return -1;
}
- func_args.session = ao2_bump(channel->session);
- ast_channel_unlock(chan);
-
memset(buf, 0, len);
- func_args.param = args.param;
- func_args.type = args.type;
- func_args.field = args.field;
- func_args.buf = buf;
- func_args.len = len;
- if (ast_sip_push_task_wait_serializer(func_args.session->serializer, read_pjsip, &func_args)) {
- ast_log(LOG_WARNING, "Unable to read properties of channel %s: failed to push task\n", ast_channel_name(chan));
- ao2_ref(func_args.session, -1);
- return -1;
+ if (!strcmp(args.param, "endpoint")) {
+ if (!channel->session->endpoint) {
+ res = -1;
+ } else {
+ ast_copy_string(buf, ast_sorcery_object_get_id(channel->session->endpoint), len);
+ }
+ } else if (!strcmp(args.param, "contact")) {
+ if (channel->session->contact) {
+ ast_copy_string(buf, ast_sorcery_object_get_id(channel->session->contact), len);
+ }
+ } else if (!strcmp(args.param, "aor")) {
+ if (channel->session->aor) {
+ ast_copy_string(buf, ast_sorcery_object_get_id(channel->session->aor), len);
+ }
+ } else if (!strcmp(args.param, "pjsip")) {
+ res = channel_read_pjsip(chan, channel->session, args.type, buf, len);
+ } else if (!strcmp(args.param, "rtp") || !strcmp(args.param, "rtcp")) {
+ const struct pjsip_channel_info *info = pjsip_channel_info_get(chan);
+ struct ast_rtp_instance *rtp;
+ struct ast_sip_session_media *media;
+ struct ast_sockaddr no_address = { { 0, } };
+ const struct ast_sockaddr *direct_media_addr;
+ unsigned int secure;
+ unsigned int held;
+ int video = !strcmp(S_OR(args.field, "audio"), "video");
+
+ if (!info) {
+ res = -1;
+ } else {
+ /* The channel lock keeps these retained instances alive throughout the read. */
+ rtp = video ? info->video_rtp : info->audio_rtp;
+ media = channel->session->active_media_state
+ ? channel->session->active_media_state->default_session[video
+ ? AST_MEDIA_TYPE_VIDEO : AST_MEDIA_TYPE_AUDIO] : NULL;
+ direct_media_addr = media ? &media->direct_media_addr : &no_address;
+ secure = video ? info->video_secure : info->audio_secure;
+ held = video ? info->video_held : info->audio_held;
+ if (!strcmp(args.param, "rtp")) {
+ res = channel_read_rtp(chan, rtp, direct_media_addr, secure, held,
+ args.type, args.field, buf, len);
+ } else {
+ res = channel_read_rtcp(chan, rtp, args.type, args.field, buf, len);
+ }
+ }
+ } else {
+ res = -1;
}
- ao2_ref(func_args.session, -1);
-
- return func_args.ret;
+ ast_channel_unlock(chan);
+ return res;
}
int pjsip_acf_dial_contacts_read(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len)
diff --git a/channels/pjsip/include/channel_info.h b/channels/pjsip/include/channel_info.h
new file mode 100644
index 0000000000..65973faac5
--- /dev/null
+++ b/channels/pjsip/include/channel_info.h
@@ -0,0 +1,92 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2026, Sangoma Technologies Corporation
+ *
+ * Michael Bradeen <mbradeen@sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*!
+ * \file
+ * \brief Channel-locked PJSIP information interface
+ */
+
+#ifndef _CHAN_PJSIP_CHANNEL_INFO_H
+#define _CHAN_PJSIP_CHANNEL_INFO_H
+
+#include <pjsip.h>
+
+#include "asterisk/channel.h"
+#include "asterisk/res_pjsip_session.h"
+
+struct pjsip_channel_uri {
+ char value[PJSIP_MAX_URL_SIZE];
+ /*! Printed length, or -1 if the URI did not fit. */
+ int length;
+};
+
+/*! All members are protected by the owning channel's lock. */
+struct pjsip_channel_info {
+ struct ast_datastore *datastore;
+ struct pjsip_channel_uri target_uri;
+ struct pjsip_channel_uri local_uri;
+ struct pjsip_channel_uri remote_uri;
+ /*! Unescaped received tag; NULL if unavailable or allocation failed. */
+ char *remote_tag;
+ /*! Signaling security, or -1 if unavailable. */
+ int secure;
+ /* Retained because active/pending media objects may be shared and changed
+ * outside the channel lock. RTP APIs provide live addresses/statistics.
+ */
+ struct ast_rtp_instance *audio_rtp;
+ struct ast_rtp_instance *video_rtp;
+ unsigned int audio_secure;
+ unsigned int video_secure;
+ unsigned int audio_held;
+ unsigned int video_held;
+};
+
+/*!
+ * \brief Create channel-owned PJSIP information before channel publication
+ * \pre The caller holds the channel lock and executes in the session serializer.
+ * \retval 0 on success
+ * \retval -1 on failure
+ */
+int pjsip_channel_info_create(struct ast_channel *chan, struct ast_sip_session *session);
+
+/*!
+ * \brief Refresh channel information under the channel lock
+ * \pre The caller executes in the session serializer.
+ */
+void pjsip_channel_info_update(struct ast_sip_session *session);
+
+/*!
+ * \brief Schedule a refresh from any thread without waiting for the serializer
+ */
+void pjsip_channel_info_schedule_update(struct ast_sip_session *session);
+
+/*!
+ * \brief Find the channel's information, or NULL if it has none
+ * \pre The caller holds the channel lock.
+ * \note The result is borrowed and may only be used while the channel is locked.
+ * Do not release it with ao2_cleanup(). The channel datastore owns its lifetime.
+ */
+struct pjsip_channel_info *pjsip_channel_info_get(struct ast_channel *chan);
+
+/*! \brief Register the channel information session supplement. */
+void pjsip_channel_info_register(void);
+
+/*! \brief Unregister the channel information session supplement. */
+void pjsip_channel_info_unregister(void);
+
+#endif /* _CHAN_PJSIP_CHANNEL_INFO_H */
diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c
index 473b498c36..e599e3bb7e 100644
--- a/res/res_pjsip_session.c
+++ b/res/res_pjsip_session.c
@@ -3491,7 +3491,17 @@ void ast_sip_session_terminate(struct ast_sip_session *session, int response)
* media sessions here.
*/
ast_sip_session_media_stats_save(session, session->active_media_state);
+ /* Publish the replacement under the channel lock used by media readers.
+ * Reset the detached state after unlocking: stream teardown may wait for
+ * scheduler callbacks that need the channel lock.
+ */
+ if (session->channel) {
+ ast_channel_lock(session->channel);
+ }
SWAP(session->active_media_state, session->pending_media_state);
+ if (session->channel) {
+ ast_channel_unlock(session->channel);
+ }
ast_sip_session_media_state_reset(session->pending_media_state);
switch (session->inv_session->state) {
diff --git a/res/res_pjsip_t38.c b/res/res_pjsip_t38.c
index 86aa8c1921..fcae90407c 100644
--- a/res/res_pjsip_t38.c
+++ b/res/res_pjsip_t38.c
@@ -137,7 +137,14 @@ static void t38_change_state(struct ast_sip_session *session, struct ast_sip_ses
return;
}
+ /* CHANNEL(pjsip,t38state) reads this under the channel lock. */
+ if (session->channel) {
+ ast_channel_lock(session->channel);
+ }
session->t38state = new_state;
+ if (session->channel) {
+ ast_channel_unlock(session->channel);
+ }
ast_debug(2, "T.38 state changed to '%u' from '%u' on channel '%s'\n",
new_state, old_state,
session->channel ? ast_channel_name(session->channel) : "<gone>");