Commit a097e12421 for freeswitch.com
commit a097e124213c164845dcaaa8feed2e70bc1e7191
Author: Dmitry Verenitsin <morbit85@gmail.com>
Date: Sat Aug 8 19:48:05 2026 +0500
Merge commit from fork
`read_packet()` passed a peer-supplied `Content-Length` straight to
`switch_zmalloc(body, clen + 1)`. Huge values drove `calloc` failure
and `switch_zmalloc` `abort()`-ed the daemon.
- Cap `Content-Length` at 16 MiB; reject negatives.
- Destroy the partially-built `*event` at the new rejection site and
at the existing body-recv failure path so callers don't leak it.
- Add `test_mod_event_socket` covering `INT_MAX`, above-cap,
negative, `atoi`-overflow, zero, and valid-non-zero-body cases.
diff --git a/src/mod/event_handlers/mod_event_socket/mod_event_socket.c b/src/mod/event_handlers/mod_event_socket/mod_event_socket.c
index 31bac3305b..f72d33c861 100644
--- a/src/mod/event_handlers/mod_event_socket/mod_event_socket.c
+++ b/src/mod/event_handlers/mod_event_socket/mod_event_socket.c
@@ -31,9 +31,12 @@
*
*/
#include <switch.h>
+
#define CMD_BUFLEN 1024 * 1000
#define MAX_QUEUE_LEN 100000
#define MAX_MISSED 500
+#define MAX_CONTENT_LENGTH (16 * 1024 * 1024)
+
SWITCH_MODULE_LOAD_FUNCTION(mod_event_socket_load);
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_event_socket_shutdown);
SWITCH_MODULE_RUNTIME_FUNCTION(mod_event_socket_runtime);
@@ -1311,6 +1314,13 @@ static switch_status_t read_packet(listener_t *listener, switch_event_t **event,
if (!strcasecmp(var, "content-length")) {
clen = atoi(val);
+ if (clen < 0 || clen > MAX_CONTENT_LENGTH) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
+ "Rejecting Content-Length '%s' (out of range)\n", val);
+ switch_event_destroy(event);
+ switch_goto_status(SWITCH_STATUS_FALSE, end);
+ }
+
if (clen > 0) {
char *body;
char *p;
@@ -1325,6 +1335,7 @@ static switch_status_t read_packet(listener_t *listener, switch_event_t **event,
if (prefs.done || (!SWITCH_STATUS_IS_BREAK(status) && status != SWITCH_STATUS_SUCCESS)) {
free(body);
+ switch_event_destroy(event);
switch_goto_status(SWITCH_STATUS_FALSE, end);
}
diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am
index 47542e34ea..fed97bb535 100644
--- a/tests/unit/Makefile.am
+++ b/tests/unit/Makefile.am
@@ -7,6 +7,7 @@ noinst_PROGRAMS += switch_stun
noinst_PROGRAMS += test_tts_format
noinst_PROGRAMS+= switch_hold switch_sip
noinst_PROGRAMS += test_mod_verto
+noinst_PROGRAMS += test_mod_event_socket
if HAVE_PCAP
noinst_PROGRAMS += switch_rtp_pcap
diff --git a/tests/unit/conf_event_socket/event_socket.conf.xml b/tests/unit/conf_event_socket/event_socket.conf.xml
new file mode 100644
index 0000000000..d7c5258926
--- /dev/null
+++ b/tests/unit/conf_event_socket/event_socket.conf.xml
@@ -0,0 +1,11 @@
+<configuration name="event_socket.conf" description="Socket Client (test)">
+ <settings>
+ <param name="nat-map" value="false"/>
+ <param name="listen-ip" value="127.0.0.1"/>
+ <!--
+ Port 38021 chosen to avoid clashing with a default vanilla install on 8021.
+ -->
+ <param name="listen-port" value="38021"/>
+ <param name="password" value="ClueCon"/>
+ </settings>
+</configuration>
diff --git a/tests/unit/conf_event_socket/freeswitch.xml b/tests/unit/conf_event_socket/freeswitch.xml
new file mode 100644
index 0000000000..2051ebdfbe
--- /dev/null
+++ b/tests/unit/conf_event_socket/freeswitch.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<document type="freeswitch/xml">
+ <X-PRE-PROCESS cmd="set" data="local_ip_v4=127.0.0.1"/>
+ <X-PRE-PROCESS cmd="set" data="domain=127.0.0.1"/>
+
+ <section name="configuration" description="Configuration">
+
+ <configuration name="modules.conf" description="Modules">
+ <modules>
+ <load module="mod_console"/>
+ <load module="mod_event_socket"/>
+ </modules>
+ </configuration>
+
+ <configuration name="switch.conf" description="Core Configuration">
+ <settings>
+ <param name="colorize-console" value="false"/>
+ <param name="loglevel" value="debug"/>
+ </settings>
+ </configuration>
+
+ <configuration name="console.conf" description="Console Logger">
+ <mappings>
+ <map name="all" value="console,debug,info,notice,warning,err,crit,alert"/>
+ </mappings>
+ <settings>
+ <param name="colorize" value="false"/>
+ <param name="loglevel" value="debug"/>
+ </settings>
+ </configuration>
+
+ <X-PRE-PROCESS cmd="include" data="event_socket.conf.xml"/>
+
+ </section>
+</document>
diff --git a/tests/unit/test_mod_event_socket.c b/tests/unit/test_mod_event_socket.c
new file mode 100644
index 0000000000..3454ecbdfc
--- /dev/null
+++ b/tests/unit/test_mod_event_socket.c
@@ -0,0 +1,356 @@
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2026, Anthony Minessale II <anthm@freeswitch.org>
+ *
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ *
+ * The Initial Developer of the Original Code is
+ * Anthony Minessale II <anthm@freeswitch.org>
+ * Portions created by the Initial Developer are Copyright (C)
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Dmitry Verenitsin <dmitry.verenitsin@signalwire.com>
+ *
+ *
+ * test_mod_event_socket.c -- Tests for mod_event_socket
+ *
+ */
+
+#include <switch.h>
+#include <test/switch_test.h>
+
+#define ESL_TEST_HOST "127.0.0.1"
+#define ESL_TEST_PORT 38021
+
+/* Must match MAX_CONTENT_LENGTH in src/mod/event_handlers/mod_event_socket/mod_event_socket.c */
+#define ESL_MAX_CONTENT_LENGTH (16 * 1024 * 1024)
+
+static switch_status_t esl_connect(switch_socket_t **sock_out, switch_memory_pool_t *pool)
+{
+ switch_sockaddr_t *addr = NULL;
+ switch_socket_t *sock = NULL;
+ int attempts;
+
+ if (switch_sockaddr_info_get(&addr, ESL_TEST_HOST, SWITCH_UNSPEC,
+ ESL_TEST_PORT, 0, pool) != SWITCH_STATUS_SUCCESS) {
+ return SWITCH_STATUS_FALSE;
+ }
+
+ for (attempts = 0; attempts < 50; attempts++) {
+ if (switch_socket_create(&sock, switch_sockaddr_get_family(addr),
+ SOCK_STREAM, SWITCH_PROTO_TCP, pool) != SWITCH_STATUS_SUCCESS) {
+ return SWITCH_STATUS_FALSE;
+ }
+ switch_socket_opt_set(sock, SWITCH_SO_TCP_NODELAY, 1);
+
+ if (switch_socket_connect(sock, addr) == SWITCH_STATUS_SUCCESS) {
+ *sock_out = sock;
+ return SWITCH_STATUS_SUCCESS;
+ }
+
+ switch_socket_close(sock);
+ sock = NULL;
+ switch_yield(100000);
+ }
+
+ return SWITCH_STATUS_FALSE;
+}
+
+static switch_status_t send_all(switch_socket_t *sock, const char *buf, switch_size_t len)
+{
+ switch_size_t remaining = len;
+ const char *p = buf;
+
+ while (remaining > 0) {
+ switch_size_t n = remaining;
+ if (switch_socket_send(sock, p, &n) != SWITCH_STATUS_SUCCESS) {
+ return SWITCH_STATUS_FALSE;
+ }
+ if (n == 0) {
+ return SWITCH_STATUS_FALSE;
+ }
+ p += n;
+ remaining -= n;
+ }
+ return SWITCH_STATUS_SUCCESS;
+}
+
+/* Read a single ESL frame, terminating at "\n\n" or buffer-full. */
+static switch_size_t recv_frame(switch_socket_t *sock, char *out, switch_size_t cap)
+{
+ switch_size_t got = 0;
+
+ while (got < cap - 1) {
+ switch_size_t want = 1;
+ if (switch_socket_recv(sock, out + got, &want) != SWITCH_STATUS_SUCCESS || want == 0) {
+ break;
+ }
+ got++;
+ if (got >= 2 && out[got - 1] == '\n' && out[got - 2] == '\n') {
+ break;
+ }
+ }
+ out[got] = '\0';
+ return got;
+}
+
+/* Drain whatever the server has to say until the socket closes. */
+static void recv_until_close(switch_socket_t *sock)
+{
+ char tmp[256];
+
+ for (;;) {
+ switch_size_t want = sizeof(tmp);
+ if (switch_socket_recv(sock, tmp, &want) != SWITCH_STATUS_SUCCESS || want == 0) {
+ break;
+ }
+ }
+}
+
+/* Connect, read auth/request greeting. Returns connected socket or NULL. */
+static switch_socket_t *esl_connect_and_greet(switch_memory_pool_t *pool)
+{
+ switch_socket_t *sock = NULL;
+ char buf[256] = { 0 };
+ switch_size_t got;
+
+ if (esl_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) {
+ return NULL;
+ }
+
+ got = recv_frame(sock, buf, sizeof(buf));
+ if (got == 0 || !strstr(buf, "auth/request")) {
+ switch_socket_close(sock);
+ return NULL;
+ }
+
+ return sock;
+}
+
+/* Send an auth packet carrying the supplied Content-Length value, wait for
+ * the server to drop the connection, then reconnect. Returns
+ * SWITCH_STATUS_SUCCESS only when the reconnect succeeds and the listener
+ * answers with the auth/request greeting — i.e. the daemon is still alive
+ * and serving connections. */
+static switch_status_t probe_bad_content_length(switch_memory_pool_t *pool, const char *clen_value)
+{
+ switch_socket_t *sock = NULL;
+ switch_socket_t *probe = NULL;
+ char packet[256];
+ switch_size_t pkt_len;
+ switch_status_t status = SWITCH_STATUS_FALSE;
+
+ sock = esl_connect_and_greet(pool);
+ if (!sock) {
+ goto probe_bad_clen_done;
+ }
+
+ pkt_len = switch_snprintf(packet, sizeof(packet),
+ "auth ClueCon\n"
+ "Content-Length: %s\n"
+ "\n",
+ clen_value);
+
+ if (send_all(sock, packet, pkt_len) != SWITCH_STATUS_SUCCESS) {
+ goto probe_bad_clen_done;
+ }
+
+ /* Server is expected to drop the connection on a malformed
+ * Content-Length. Drain until close so the next probe starts clean. */
+ recv_until_close(sock);
+
+ /* The reconnect is the liveness check: if the listener thread or
+ * the whole daemon went down, this retries 50 times then fails. */
+ probe = esl_connect_and_greet(pool);
+ if (probe) {
+ status = SWITCH_STATUS_SUCCESS;
+ }
+
+probe_bad_clen_done:
+ if (sock) switch_socket_close(sock);
+ if (probe) switch_socket_close(probe);
+ return status;
+}
+
+FST_CORE_DB_BEGIN("./conf_event_socket")
+{
+ FST_SUITE_BEGIN(test_mod_event_socket)
+ {
+ FST_SETUP_BEGIN()
+ {
+ fst_requires_module("mod_event_socket");
+ switch_yield(500000);
+ }
+ FST_SETUP_END()
+
+ FST_TEARDOWN_BEGIN()
+ {
+ }
+ FST_TEARDOWN_END()
+
+ FST_TEST_BEGIN(content_length_validation)
+ {
+ switch_socket_t *sock = NULL;
+ char val[32];
+ char reply[256] = { 0 };
+
+ /* INT_MAX, above-cap, negative, and atoi-overflow values must
+ * all be rejected without taking the daemon down. */
+ fst_xcheck(probe_bad_content_length(fst_pool, "2147483647") == SWITCH_STATUS_SUCCESS,
+ "daemon must still be alive after Content-Length: INT_MAX");
+
+ switch_snprintf(val, sizeof(val), "%d", ESL_MAX_CONTENT_LENGTH + 1);
+ fst_xcheck(probe_bad_content_length(fst_pool, val) == SWITCH_STATUS_SUCCESS,
+ "daemon must still be alive after Content-Length above cap");
+
+ fst_xcheck(probe_bad_content_length(fst_pool, "-1") == SWITCH_STATUS_SUCCESS,
+ "daemon must still be alive after Content-Length: -1");
+
+ fst_xcheck(probe_bad_content_length(fst_pool, "99999999999999999") == SWITCH_STATUS_SUCCESS,
+ "daemon must still be alive after Content-Length atoi-overflow value");
+
+ /* Content-Length: 0 is a valid no-body packet and must be
+ * accepted — auth completes normally. */
+ do {
+ sock = esl_connect_and_greet(fst_pool);
+ if (!sock) {
+ fst_fail("could not connect to event_socket listener");
+ break;
+ }
+
+ if (send_all(sock, "auth ClueCon\nContent-Length: 0\n\n",
+ strlen("auth ClueCon\nContent-Length: 0\n\n")) != SWITCH_STATUS_SUCCESS) {
+ fst_fail("could not send auth packet");
+ break;
+ }
+
+ recv_frame(sock, reply, sizeof(reply));
+ fst_check_string_has(reply, "+OK accepted");
+ } while (0);
+
+ if (sock) switch_socket_close(sock);
+ }
+ FST_TEST_END()
+
+ /* A packet with a valid non-zero Content-Length must be parsed
+ * end-to-end: command, headers, and body fully consumed before
+ * the server replies. */
+ FST_TEST_BEGIN(content_length_with_body)
+ {
+ switch_socket_t *sock = NULL;
+ char reply[256] = { 0 };
+ const char *packet = "auth ClueCon\nContent-Length: 5\n\n12345";
+
+ do {
+ sock = esl_connect_and_greet(fst_pool);
+ if (!sock) {
+ fst_fail("could not connect to event_socket listener");
+ break;
+ }
+
+ if (send_all(sock, packet, strlen(packet)) != SWITCH_STATUS_SUCCESS) {
+ fst_fail("could not send auth packet with body");
+ break;
+ }
+
+ recv_frame(sock, reply, sizeof(reply));
+ fst_check_string_has(reply, "+OK accepted");
+ } while (0);
+
+ if (sock) switch_socket_close(sock);
+ }
+ FST_TEST_END()
+
+ /* A `sendevent` with a body must dispatch an event whose body
+ * bytes match what was sent. Exercises the authenticated
+ * command-loop and end-to-end body delivery via the listener's
+ * event subscription. */
+ FST_TEST_BEGIN(sendevent_body_roundtrip)
+ {
+ switch_socket_t *sock = NULL;
+ char buf[8192] = { 0 };
+ switch_size_t got;
+ const char *send_pkt =
+ "sendevent CUSTOM\n"
+ "Event-Subclass: test_body_roundtrip\n"
+ "Content-Length: 11\n"
+ "\n"
+ "hello world";
+
+ do {
+ sock = esl_connect_and_greet(fst_pool);
+ if (!sock) {
+ fst_fail("could not connect to event_socket listener");
+ break;
+ }
+
+ if (send_all(sock, "auth ClueCon\n\n",
+ strlen("auth ClueCon\n\n")) != SWITCH_STATUS_SUCCESS) {
+ fst_fail("could not send auth");
+ break;
+ }
+ recv_frame(sock, buf, sizeof(buf));
+ if (!strstr(buf, "+OK accepted")) {
+ fst_fail("auth not accepted");
+ break;
+ }
+
+ /* Subscribe to our specific CUSTOM subclass so that
+ * unrelated custom events on the wire don't leak into
+ * the assertions below. */
+ if (send_all(sock, "event plain CUSTOM test_body_roundtrip\n\n",
+ strlen("event plain CUSTOM test_body_roundtrip\n\n")) != SWITCH_STATUS_SUCCESS) {
+ fst_fail("could not send event subscription");
+ break;
+ }
+ memset(buf, 0, sizeof(buf));
+ recv_frame(sock, buf, sizeof(buf));
+ if (!strstr(buf, "+OK event listener enabled")) {
+ fst_fail("event subscription not acked");
+ break;
+ }
+
+ if (send_all(sock, send_pkt, strlen(send_pkt)) != SWITCH_STATUS_SUCCESS) {
+ fst_fail("could not send sendevent packet");
+ break;
+ }
+
+ /* Drain reply + dispatched event into a single buffer.
+ * The recv timeout bounds the wait when the listener
+ * has nothing more to send. */
+ memset(buf, 0, sizeof(buf));
+ switch_socket_timeout_set(sock, 500 * 1000);
+ got = 0;
+ while (got < sizeof(buf) - 1) {
+ switch_size_t want = sizeof(buf) - 1 - got;
+ if (switch_socket_recv(sock, buf + got, &want) != SWITCH_STATUS_SUCCESS) break;
+ if (want == 0) break;
+ got += want;
+ }
+ buf[got] = '\0';
+
+ fst_check_string_has(buf, "Event-Name: CUSTOM");
+ fst_check_string_has(buf, "Event-Subclass: test_body_roundtrip");
+ fst_check_string_has(buf, "hello world");
+ } while (0);
+
+ if (sock) switch_socket_close(sock);
+ }
+ FST_TEST_END()
+ }
+ FST_SUITE_END()
+}
+FST_CORE_END()