Commit 47949eff1b for asterisk.org

commit 47949eff1b314a924834b25faba25a6aa53c691c
Author: Cetin Cem <cetincem@gaga.dev>
Date:   Sat Oct 3 23:49:14 2026 +0300

    res_rtp_asterisk.c: Fix cleanup after RTP allocation failure.

    Initialize DTLS timer IDs before RTP allocation can fail, preventing
    cleanup from cancelling uninitialized timers. Add a regression test
    for port exhaustion and reuse without a scheduler.

    AI assistance: Codex generated the code, test, validation scripts and text.

    Fixes: #1757

diff --git a/res/res_rtp_asterisk.c b/res/res_rtp_asterisk.c
index f5b4619e3a..894ce243ad 100644
--- a/res/res_rtp_asterisk.c
+++ b/res/res_rtp_asterisk.c
@@ -4147,11 +4147,17 @@ static int ice_create(struct ast_rtp_instance *instance, struct ast_sockaddr *ad
 }
 #endif

+/* Allocate an RTP transport with timer state safe for cleanup on failure. */
 static int rtp_allocate_transport(struct ast_rtp_instance *instance, struct ast_rtp *rtp)
 {
 	int x, startplace, i, maxloops;
 	unsigned int port_start, port_end;

+#if defined(HAVE_OPENSSL) && (OPENSSL_VERSION_NUMBER >= 0x10001000L) && !defined(OPENSSL_NO_SRTP)
+	rtp->rekeyid = -1;
+	rtp->dtls.timeout_timer = -1;
+#endif
+
 	rtp->strict_rtp_state = (strictrtp ? STRICT_RTP_CLOSED : STRICT_RTP_OPEN);

 	/* Determine the port range to use: per-instance override or global */
@@ -4224,11 +4230,6 @@ static int rtp_allocate_transport(struct ast_rtp_instance *instance, struct ast_
 	}
 #endif

-#if defined(HAVE_OPENSSL) && (OPENSSL_VERSION_NUMBER >= 0x10001000L) && !defined(OPENSSL_NO_SRTP)
-	rtp->rekeyid = -1;
-	rtp->dtls.timeout_timer = -1;
-#endif
-
 	return 0;
 }

diff --git a/tests/test_res_rtp.c b/tests/test_res_rtp.c
index 46dad0932e..211bc610d9 100644
--- a/tests/test_res_rtp.c
+++ b/tests/test_res_rtp.c
@@ -259,6 +259,63 @@ static void test_write_and_read_interleaved_frames(struct ast_rtp_instance *inst
 	}
 }

+/* Verify failed RTP allocation without a scheduler is safe and releases its resources. */
+AST_TEST_DEFINE(rtp_port_exhaustion)
+{
+	RAII_VAR(struct ast_rtp_instance *, occupied, NULL, ast_rtp_instance_destroy);
+	RAII_VAR(struct ast_rtp_instance *, instance, NULL, ast_rtp_instance_destroy);
+	struct ast_sockaddr address;
+	struct ast_rtp_instance_options options;
+
+	switch (cmd) {
+	case TEST_INIT:
+		info->name = "rtp_port_exhaustion";
+		info->category = "/res/res_rtp/";
+		info->summary = "RTP port exhaustion without a scheduler";
+		info->description =
+			"Exhaust a per-instance RTP port range with no scheduler, as used by "
+			"UnicastRTP. Verify allocation fails safely and succeeds after the "
+			"occupied port is released.";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	ast_sockaddr_parse(&address, "127.0.0.1", 0);
+	occupied = ast_rtp_instance_new("asterisk", NULL, &address, NULL);
+	if (!occupied) {
+		ast_test_status_update(test, "Unable to allocate the initial RTP instance\n");
+		return AST_TEST_FAIL;
+	}
+
+	ast_rtp_instance_get_local_address(occupied, &address);
+	/* The occupied port is the only even port in this range. */
+	options.port_start = ast_sockaddr_port(&address);
+	options.port_end = options.port_start + 1;
+	instance = ast_rtp_instance_new_with_options("asterisk", NULL, &address,
+		NULL, &options);
+	if (instance) {
+		ast_test_status_update(test, "Allocation succeeded with an exhausted port range\n");
+		return AST_TEST_FAIL;
+	}
+
+	ast_rtp_instance_destroy(occupied);
+	occupied = NULL;
+	instance = ast_rtp_instance_new_with_options("asterisk", NULL, &address,
+		NULL, &options);
+	if (!instance) {
+		ast_test_status_update(test, "Unable to reuse the released RTP port\n");
+		return AST_TEST_FAIL;
+	}
+	ast_rtp_instance_get_local_address(instance, &address);
+	if (ast_sockaddr_port(&address) != options.port_start) {
+		ast_test_status_update(test, "RTP instance did not reuse the released port\n");
+		return AST_TEST_FAIL;
+	}
+
+	return AST_TEST_PASS;
+}
+
 AST_TEST_DEFINE(nack_no_packet_loss)
 {
 	RAII_VAR(struct ast_rtp_instance *, instance1, NULL, ast_rtp_instance_destroy);
@@ -839,8 +896,10 @@ cleanup:
 	return result;
 }

+/* Unregister RTP regression tests when the module unloads. */
 static int unload_module(void)
 {
+	AST_TEST_UNREGISTER(rtp_port_exhaustion);
 	AST_TEST_UNREGISTER(payload_merge_preserves_preferences);
 	AST_TEST_UNREGISTER(payload_merge_abandoned_offer);
 	AST_TEST_UNREGISTER(mes);
@@ -854,8 +913,10 @@ static int unload_module(void)
 	return 0;
 }

+/* Register RTP regression tests with the unit test framework. */
 static int load_module(void)
 {
+	AST_TEST_REGISTER(rtp_port_exhaustion);
 	AST_TEST_REGISTER(nack_no_packet_loss);
 	AST_TEST_REGISTER(nack_nominal);
 	AST_TEST_REGISTER(nack_overflow);