Skip to content

Commit 42a067c

Browse files
committed
BUG#38425487 mysql_command_factory->connect() failure when running in explicit mode leaks session due to premature cleanup
Issue ===== When mysql_command_services runs in explicit mode (thread initialized via cmd_thread_srv->init()), a failed connect() (e.g., bad username) leaks the server session owned by the command-services backend. The error log shows: [ERROR] [MY-010470] [Server] Closed forcefully 1 session left opened by plugin server_service Root cause ========== Two problems overlapped: 1) On failure, connect_helper() calls end_server() + mysql_close_free(), which continues into mysql_extension_free(). However, in early failure paths the session service pointer (mcs_extn->session_svc) could be set too late, so teardown had no valid session to close. 2) The consumer service release and session handling lives in the component's close(), which is not guaranteed to run on early connect() failures. The failure path already invoked mysql_close_free() at that point. Fix === - Centralize server-backed cleanup in mysql_extension_free() to ensure that it always closes the backend session and resets vars and frees ext->mcs_extn accordingly. Also, to release the consumer services acquired earlier. - Assign mcs_extn->session_svc earlier in cssm_begin_connect() so that all early failures still have a valid session handle for cleanup. - Make the component close() a thin wrapper that releases consumer services and calls mysql_close() only. Change-Id: I36bbf60280a065cc2481a3ae829ef299b08e1592
1 parent 968441b commit 42a067c

File tree

6 files changed

+217
-99
lines changed

6 files changed

+217
-99
lines changed

components/test/test_mysql_command_services.cc

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
3030
#include <mysql/service_srv_session_info.h>
3131
#include <cstdio>
3232
#include <cstring>
33+
#include <thread>
3334

3435
REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_thd_security_context, thd_security_ctx);
3536
REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_account_database_security_context_lookup,
@@ -38,6 +39,7 @@ REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_security_context_options,
3839
security_ctx_options);
3940
REQUIRES_SERVICE_PLACEHOLDER_AS(udf_registration, udf_srv);
4041
REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_command_factory, cmd_factory_srv);
42+
REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_command_thread, cmd_thread_srv);
4143
REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_command_options, cmd_options_srv);
4244
REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_command_query, cmd_query_srv);
4345
REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_command_query_result,
@@ -57,6 +59,7 @@ REQUIRES_SERVICE_AS(udf_registration, udf_srv),
5759
account_db_security_ctx_lookup),
5860
REQUIRES_SERVICE_AS(mysql_security_context_options, security_ctx_options),
5961
REQUIRES_SERVICE_AS(mysql_command_factory, cmd_factory_srv),
62+
REQUIRES_SERVICE_AS(mysql_command_thread, cmd_thread_srv),
6063
REQUIRES_SERVICE_AS(mysql_command_options, cmd_options_srv),
6164
REQUIRES_SERVICE_AS(mysql_command_query, cmd_query_srv),
6265
REQUIRES_SERVICE_AS(mysql_command_query_result, cmd_query_result_srv),
@@ -389,6 +392,57 @@ static long long test_mysql_command_services_error_code_udf(
389392
return static_cast<long long>(err_no);
390393
}
391394

395+
// Run in thread + failed connect + cleanup
396+
static long long test_mysql_command_services_explicit_connect_fail_cleanup_udf(
397+
UDF_INIT *, UDF_ARGS *, unsigned char *is_null, unsigned char *error) {
398+
*is_null = 0;
399+
*error = 1;
400+
401+
constexpr const char *bad_user = "no_such_user";
402+
constexpr const char *host = "localhost";
403+
404+
long long ret_err = -1;
405+
406+
// Spawn a thread, since the cmd thread must run off the server's main session
407+
// thread
408+
std::thread worker([&] {
409+
MYSQL_H mysql_h = nullptr;
410+
411+
if (cmd_thread_srv->init() != 0) return;
412+
413+
do {
414+
if (cmd_factory_srv->init(&mysql_h) != 0 || mysql_h == nullptr) break;
415+
416+
// Set the options
417+
if (cmd_options_srv->set(mysql_h, MYSQL_NO_LOCK_REGISTRY,
418+
reinterpret_cast<void *>(1)) != 0 ||
419+
cmd_options_srv->set(mysql_h, MYSQL_COMMAND_USER_NAME, bad_user) !=
420+
0 ||
421+
cmd_options_srv->set(mysql_h, MYSQL_COMMAND_HOST_NAME, host) != 0) {
422+
cmd_factory_srv->close(mysql_h);
423+
mysql_h = nullptr;
424+
break; // Failed setting the options, exit
425+
}
426+
427+
// Expect failure: wrong user
428+
ret_err = (cmd_factory_srv->connect(mysql_h) != 0) ? 1 : 0;
429+
430+
// Always close the handle even on failure
431+
cmd_factory_srv->close(mysql_h);
432+
mysql_h = nullptr;
433+
} while (false);
434+
435+
cmd_thread_srv->end();
436+
});
437+
438+
worker.join();
439+
440+
*error = (ret_err == -1)
441+
? 1
442+
: 0; // only mark UDF-level error if we never tried connect
443+
return ret_err;
444+
}
445+
392446
static mysql_service_status_t init() {
393447
Udf_func_string udf1 = test_mysql_command_services_udf;
394448
if (udf_srv->udf_register("test_mysql_command_services_udf", STRING_RESULT,
@@ -416,6 +470,18 @@ static mysql_service_status_t init() {
416470
return 1;
417471
}
418472

473+
Udf_func_longlong udf4 =
474+
test_mysql_command_services_explicit_connect_fail_cleanup_udf;
475+
if (udf_srv->udf_register(
476+
"test_mysql_command_services_explicit_connect_fail_cleanup_udf",
477+
INT_RESULT, reinterpret_cast<Udf_func_any>(udf4), nullptr, nullptr)) {
478+
fprintf(
479+
stderr,
480+
"Can't register "
481+
"test_mysql_command_services_explicit_connect_fail_cleanup_udf UDF\n");
482+
return 1;
483+
}
484+
419485
return 0;
420486
}
421487

@@ -433,6 +499,13 @@ static mysql_service_status_t deinit() {
433499
fprintf(stderr,
434500
"Can't unregister the test_mysql_command_services_error_code_udf "
435501
"UDF\n");
502+
if (udf_srv->udf_unregister(
503+
"test_mysql_command_services_explicit_connect_fail_cleanup_udf",
504+
&was_present))
505+
fprintf(
506+
stderr,
507+
"Can't unregister "
508+
"test_mysql_command_services_explicit_connect_fail_cleanup_udf UDF\n");
436509
return 0; /* success */
437510
}
438511

mysql-test/suite/test_services/r/test_mysql_command_services_component.result

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,8 @@ SELECT test_mysql_command_services_error_code_udf("SELECT * FROM test.no_such_ta
114114
test_mysql_command_services_error_code_udf("SELECT * FROM test.no_such_table")
115115
1146
116116
SET @@GLOBAL.DEBUG = '-d, mysql_command_services_component_test_errno';
117+
# Test : failing connect() when running in an initialized thread; cleanup (cmd_factory->close() + cmd_thread->end()) must not leak the session
118+
SELECT test_mysql_command_services_explicit_connect_fail_cleanup_udf();
119+
test_mysql_command_services_explicit_connect_fail_cleanup_udf()
120+
1
117121
UNINSTALL COMPONENT "file://component_test_mysql_command_services";

mysql-test/suite/test_services/t/test_mysql_command_services_component.test

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ SET @@GLOBAL.DEBUG = '+d, mysql_command_services_component_test_errno';
8989
SELECT test_mysql_command_services_error_code_udf("SELECT * FROM test.no_such_table");
9090
SET @@GLOBAL.DEBUG = '-d, mysql_command_services_component_test_errno';
9191

92+
--echo # Test : failing connect() when running in an initialized thread; cleanup (cmd_factory->close() + cmd_thread->end()) must not leak the session
93+
SELECT test_mysql_command_services_explicit_connect_fail_cleanup_udf();
94+
95+
# Error-log must not contain: [ERROR] [MY-010470] [Server] Closed forcefully 1 session left opened by plugin server_service
96+
9297
UNINSTALL COMPONENT "file://component_test_mysql_command_services";

sql-common/client.cc

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
#include "sql/client_settings.h"
140140
#include "sql/server_component/mysql_command_services_imp.h"
141141
/* mysql_command_service_extn */
142+
#include "sql/mysqld.h" // srv_registry
142143
#else
143144
#include "libmysql/client_settings.h"
144145
#endif
@@ -3374,6 +3375,138 @@ void mysql_extension_bind_free(MYSQL_EXTENSION *ext) {
33743375
memset(&ext->bind_info, 0, sizeof(ext->bind_info));
33753376
}
33763377

3378+
#ifdef MYSQL_SERVER
3379+
/**
3380+
Release services.
3381+
3382+
@param[in] consumer_refs A valid mysql_command_consumer_refs object.
3383+
@param[in] mcs_ext A valid mysql_command_service_extn object.
3384+
@param[in] srv_registry Registry service pointer.
3385+
*/
3386+
static void release_services(mysql_command_consumer_refs *consumer_refs,
3387+
mysql_command_service_extn *mcs_ext,
3388+
mysql_service_registry_t *srv_registry) {
3389+
if (consumer_refs) {
3390+
if (consumer_refs->factory_srv) {
3391+
/* This service call is used to free the memory, the allocation
3392+
was happened through factory_srv->start() service api. */
3393+
consumer_refs->factory_srv->end(
3394+
reinterpret_cast<SRV_CTX_H>(mcs_ext->consumer_srv_data));
3395+
srv_registry->release(reinterpret_cast<my_h_service>(
3396+
const_cast<SERVICE_TYPE_NO_CONST(mysql_text_consumer_factory_v1) *>(
3397+
consumer_refs->factory_srv)));
3398+
}
3399+
if (consumer_refs->metadata_srv)
3400+
srv_registry->release(reinterpret_cast<my_h_service>(
3401+
const_cast<SERVICE_TYPE_NO_CONST(mysql_text_consumer_metadata_v1) *>(
3402+
consumer_refs->metadata_srv)));
3403+
if (consumer_refs->row_factory_srv)
3404+
srv_registry->release(reinterpret_cast<my_h_service>(
3405+
const_cast<SERVICE_TYPE_NO_CONST(
3406+
mysql_text_consumer_row_factory_v1) *>(
3407+
consumer_refs->row_factory_srv)));
3408+
if (consumer_refs->error_srv)
3409+
srv_registry->release(reinterpret_cast<my_h_service>(
3410+
const_cast<SERVICE_TYPE_NO_CONST(mysql_text_consumer_error_v1) *>(
3411+
consumer_refs->error_srv)));
3412+
if (consumer_refs->get_null_srv)
3413+
srv_registry->release(reinterpret_cast<my_h_service>(
3414+
const_cast<SERVICE_TYPE_NO_CONST(mysql_text_consumer_get_null_v1) *>(
3415+
consumer_refs->get_null_srv)));
3416+
if (consumer_refs->get_integer_srv)
3417+
srv_registry->release(reinterpret_cast<my_h_service>(
3418+
const_cast<SERVICE_TYPE_NO_CONST(
3419+
mysql_text_consumer_get_integer_v1) *>(
3420+
consumer_refs->get_integer_srv)));
3421+
if (consumer_refs->get_longlong_srv)
3422+
srv_registry->release(reinterpret_cast<my_h_service>(
3423+
const_cast<SERVICE_TYPE_NO_CONST(
3424+
mysql_text_consumer_get_longlong_v1) *>(
3425+
consumer_refs->get_longlong_srv)));
3426+
if (consumer_refs->get_decimal_srv)
3427+
srv_registry->release(reinterpret_cast<my_h_service>(
3428+
const_cast<SERVICE_TYPE_NO_CONST(
3429+
mysql_text_consumer_get_decimal_v1) *>(
3430+
consumer_refs->get_decimal_srv)));
3431+
if (consumer_refs->get_double_srv)
3432+
srv_registry->release(reinterpret_cast<my_h_service>(
3433+
const_cast<SERVICE_TYPE_NO_CONST(
3434+
mysql_text_consumer_get_double_v1) *>(
3435+
consumer_refs->get_double_srv)));
3436+
if (consumer_refs->get_date_time_srv)
3437+
srv_registry->release(reinterpret_cast<my_h_service>(
3438+
const_cast<SERVICE_TYPE_NO_CONST(
3439+
mysql_text_consumer_get_date_time_v1) *>(
3440+
consumer_refs->get_date_time_srv)));
3441+
if (consumer_refs->get_string_srv)
3442+
srv_registry->release(reinterpret_cast<my_h_service>(
3443+
const_cast<SERVICE_TYPE_NO_CONST(
3444+
mysql_text_consumer_get_string_v1) *>(
3445+
consumer_refs->get_string_srv)));
3446+
if (consumer_refs->client_capabilities_srv)
3447+
srv_registry->release(reinterpret_cast<my_h_service>(
3448+
const_cast<SERVICE_TYPE_NO_CONST(
3449+
mysql_text_consumer_client_capabilities_v1) *>(
3450+
consumer_refs->client_capabilities_srv)));
3451+
}
3452+
}
3453+
3454+
/**
3455+
Free the command service extension.
3456+
3457+
This function releases consumer services and closes/detaches the backend
3458+
session as appropriate, then frees 'ext->mcs_extn'.
3459+
Safe to call even if some fields are null/partially initialized. No-op if
3460+
'ext' or 'ext->mcs_extn' is null.
3461+
3462+
@param[in,out] ext The MYSQL_EXTENSION owning the command service state.
3463+
On return, any consumer services are released, cleans up
3464+
the backend session (detached sessions are detached/
3465+
closed, THD-associated sessions are left to their owner),
3466+
and 'ext->mcs_extn' is freed and set to nullptr. The 'ext'
3467+
object itself is not freed.
3468+
*/
3469+
static void mysql_command_service_extn_free(MYSQL_EXTENSION *ext) {
3470+
if (!ext || !ext->mcs_extn) return;
3471+
3472+
auto *mcs_ext = reinterpret_cast<mysql_command_service_extn *>(ext->mcs_extn);
3473+
3474+
// Release consumer services acquired earlier
3475+
if (mcs_ext->command_consumer_services) {
3476+
auto *consumer_refs = reinterpret_cast<mysql_command_consumer_refs *>(
3477+
mcs_ext->command_consumer_services);
3478+
bool no_lock_registry = mcs_ext->no_lock_registry;
3479+
3480+
if (consumer_refs) {
3481+
no_lock_registry
3482+
? release_services(consumer_refs, mcs_ext, srv_registry_no_lock)
3483+
: release_services(consumer_refs, mcs_ext, srv_registry);
3484+
delete consumer_refs;
3485+
consumer_refs = nullptr;
3486+
3487+
// avoid dangling ptr
3488+
mcs_ext->command_consumer_services = nullptr;
3489+
}
3490+
}
3491+
3492+
// Close session
3493+
if (mcs_ext->session_svc) {
3494+
if (!mcs_ext->is_thd_associated) {
3495+
// Detached session: detach then close
3496+
srv_session_detach(mcs_ext->session_svc);
3497+
srv_session_close(mcs_ext->session_svc);
3498+
}
3499+
// THD-associated session: do not detach/close here (owner will close it)
3500+
mcs_ext->session_svc = nullptr;
3501+
}
3502+
3503+
// Reset var, free ext storage, and set ptr to nullptr
3504+
mcs_ext->is_thd_associated = false;
3505+
my_free(mcs_ext);
3506+
ext->mcs_extn = nullptr;
3507+
}
3508+
#endif
3509+
33773510
void mysql_extension_free(MYSQL_EXTENSION *ext) {
33783511
if (!ext) return;
33793512
if (ext->trace_data) my_free(ext->trace_data);
@@ -3400,7 +3533,7 @@ void mysql_extension_free(MYSQL_EXTENSION *ext) {
34003533
ext->mysql_async_context = nullptr;
34013534
}
34023535
#ifdef MYSQL_SERVER
3403-
if (ext->mcs_extn) my_free(ext->mcs_extn);
3536+
mysql_command_service_extn_free(ext);
34043537
#endif
34053538
// free state change related resources.
34063539
free_state_change_info(ext);

sql/server_component/mysql_command_backend.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,12 @@ mysql_state_machine_status cssm_begin_connect(mysql_async_connect *ctx) {
224224
thd = mysql_session->get_thd();
225225
mcs_extn->is_thd_associated = false;
226226
Security_context_handle sc;
227+
mcs_extn->session_svc = mysql_session;
227228
if (mysql_security_context_imp::get(thd, &sc)) return STATE_MACHINE_FAILED;
228229
if (mysql_security_context_imp::lookup(sc, user, host, nullptr, db))
229230
return STATE_MACHINE_FAILED;
230231
mcs_extn->mcs_thd = thd;
231232
mysql->thd = thd;
232-
mcs_extn->session_svc = mysql_session;
233233
} else {
234234
mysql->thd = reinterpret_cast<void *>(mcs_extn->mcs_thd);
235235
}

0 commit comments

Comments
 (0)