Commit d32f273961 for asterisk.org
commit d32f273961397a99c3a99c53917744c18971799b
Author: Dragos Oancea <dragos.oancea.3.141592@gmail.com>
Date: Tue Oct 27 18:36:33 2026 +0200
chan_websocket: Enforce QUEUE_LENGTH_MAX per frame, not per message.
process_binary_message() checked the frame queue length against
QUEUE_LENGTH_MAX only once, before splitting the incoming binary
message into frames. A single WebSocket message can carry many
frames' worth of audio, so an application sending large chunks
(common with Voice AI services that generate audio faster than
real time) could push the queue far beyond its configured limit.
queue_frame_from_buffer() now checks the limit under the queue lock
for every frame and returns QUEUE_FRAME_DROPPED_FULL when the queue
is full. process_binary_message() stops at that point and discards
the remainder of the message, including any partial frame, so
non-contiguous audio is never spliced together through
leftover_data. The up-front check is kept as a fast path.
STOP_MEDIA_BUFFERING no longer aborts (and skips sending
MEDIA_BUFFERING_COMPLETED) if its final partial frame is dropped
because the queue is full; only real failures abort.
DeveloperNote: chan_websocket now enforces its 1000-frame media queue
limit on every frame instead of once per binary WebSocket message.
Applications that send more audio than fits in the remaining queue
space, in one message or without pausing on MEDIA_XOFF, will now have
the excess silently discarded, including any trailing partial frame.
Applications should honor MEDIA_XOFF/MEDIA_XON flow control and keep
individual messages reasonably small.
diff --git a/channels/chan_websocket.c b/channels/chan_websocket.c
index 2667a81c10..d80ab07819 100644
--- a/channels/chan_websocket.c
+++ b/channels/chan_websocket.c
@@ -668,6 +668,27 @@ static struct ast_frame *webchan_read(struct ast_channel *ast)
return native_frame;
}
+/*!
+ * \brief Result of queue_frame_from_buffer() when the frame was dropped
+ * because the queue is already at QUEUE_LENGTH_MAX.
+ *
+ * This is not an error; the caller should stop queueing frames from the
+ * current message but otherwise carry on.
+ */
+#define QUEUE_FRAME_DROPPED_FULL 1
+
+/*!
+ * \brief Create a frame from \a buffer and append it to the frame queue.
+ *
+ * The QUEUE_LENGTH_MAX limit is enforced here, per frame, rather than
+ * once per WebSocket message. A single binary message can carry many
+ * frames' worth of audio, so a per-message check alone lets the queue
+ * grow well past its limit.
+ *
+ * \retval 0 Frame queued.
+ * \retval QUEUE_FRAME_DROPPED_FULL Queue is full; frame dropped.
+ * \retval -1 Failure creating the frame.
+ */
static int queue_frame_from_buffer(struct websocket_pvt *instance,
char *buffer, size_t len)
{
@@ -681,6 +702,12 @@ static int queue_frame_from_buffer(struct websocket_pvt *instance,
{
SCOPED_LOCK(frame_queue_lock, &instance->frame_queue, AST_LIST_LOCK,
AST_LIST_UNLOCK);
+ if (instance->frame_queue_length >= QUEUE_LENGTH_MAX) {
+ ast_frfree(duped_frame);
+ ast_debug(4, "%s: WebSocket queue is full (%d frames). Dropping %d byte frame.\n",
+ ast_channel_name(instance->channel), instance->frame_queue_length, (int)len);
+ return QUEUE_FRAME_DROPPED_FULL;
+ }
AST_LIST_INSERT_TAIL(&instance->frame_queue, duped_frame, frame_list);
instance->frame_queue_length++;
if (!instance->queue_full && instance->frame_queue_length >= QUEUE_LENGTH_XOFF_LEVEL) {
@@ -835,7 +862,7 @@ static int handle_command(struct websocket_pvt *instance, char *buffer)
instance->unbuffered = instance->last_unbuffered;
if (instance->leftover_len > 0) {
res = queue_frame_from_buffer(instance, instance->leftover_data, instance->leftover_len);
- if (res != 0) {
+ if (res < 0) {
return res;
}
}
@@ -1022,6 +1049,12 @@ static int process_binary_message(struct websocket_pvt *instance,
int res = 0;
size_t bytes_left = 0;
+ /*
+ * Fast path: if the queue is already full, don't bother splitting
+ * the message into frames. This check alone isn't sufficient since
+ * one message can hold many frames; queue_frame_from_buffer()
+ * enforces QUEUE_LENGTH_MAX on every frame it queues.
+ */
{
SCOPED_LOCK(frame_queue_lock, &instance->frame_queue, AST_LIST_LOCK,
AST_LIST_UNLOCK);
@@ -1037,7 +1070,7 @@ static int process_binary_message(struct websocket_pvt *instance,
if (instance->unbuffered) {
res = queue_frame_from_buffer(instance, payload, payload_len);
- return res;
+ return res < 0 ? res : 0;
}
if (instance->bulk_media_in_progress && instance->leftover_len > 0) {
@@ -1075,6 +1108,11 @@ static int process_binary_message(struct websocket_pvt *instance,
if (res < 0) {
return -1;
}
+ if (res == QUEUE_FRAME_DROPPED_FULL) {
+ /* Queue filled up. Drop the rest of this message. */
+ instance->leftover_len = 0;
+ return 0;
+ }
/*
* We stole data from the current payload so decrement payload_len
@@ -1112,6 +1150,17 @@ static int process_binary_message(struct websocket_pvt *instance,
if (res < 0) {
break;
}
+ if (res == QUEUE_FRAME_DROPPED_FULL) {
+ /*
+ * Queue filled up part way through this message. Drop the
+ * remainder, including any partial frame, so we don't later
+ * splice non-contiguous audio together via leftover_data.
+ */
+ ast_debug(4, "%s: WebSocket queue filled mid-message. Dropping remaining %d bytes.\n",
+ ast_channel_name(instance->channel), (int)bytes_left);
+ instance->leftover_len = 0;
+ return 0;
+ }
bytes_read += instance->optimal_frame_size;
next_frame_ptr += instance->optimal_frame_size;
bytes_left -= instance->optimal_frame_size;