From a097e124213c164845dcaaa8feed2e70bc1e7191 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Sat, 8 Aug 2026 19:48:05 +0500 Subject: [PATCH] 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. --- .../mod_event_socket/mod_event_socket.c | 11 + tests/unit/Makefile.am | 1 + .../conf_event_socket/event_socket.conf.xml | 11 + tests/unit/conf_event_socket/freeswitch.xml | 35 ++ tests/unit/test_mod_event_socket.c | 356 ++++++++++++++++++ 5 files changed, 414 insertions(+) create mode 100644 tests/unit/conf_event_socket/event_socket.conf.xml create mode 100644 tests/unit/conf_event_socket/freeswitch.xml create mode 100644 tests/unit/test_mod_event_socket.c 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 + #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 @@ + + + + + + + + + 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 @@ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
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 + * + * 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 + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dmitry Verenitsin + * + * + * test_mod_event_socket.c -- Tests for mod_event_socket + * + */ + +#include +#include + +#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()