Skip to content

Commit 2bc6b41

Browse files
committed
Fix disconnect_all() method to work also during global destruction phase
During global destruction phase ChildHandles DBI attribute may be already released or children can be undefs due to weakrefs. So instead of relaying on ChildHandles, maintain own double linked list of all imp_dbh structures with active MYSQL connection. This does not use any Perl data structures so they are available at any time, including global destruction phase. Fixes: jhthorsen/mojo-mysql#47 (comment) Fixes: 17d5aa0
1 parent f5eb73a commit 2bc6b41

File tree

4 files changed

+105
-55
lines changed

4 files changed

+105
-55
lines changed

MANIFEST

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ t/10connect.t
1919
t/11data_sources.t
2020
t/12embedded.t
2121
t/13disconnect.t
22+
t/14destruct.t
2223
t/15reconnect.t
2324
t/16dbi-get_info.t
2425
t/20createdrop.t

dbdimp.c

Lines changed: 47 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2291,6 +2291,8 @@ static bool mariadb_dr_connect(
22912291

22922292
imp_dbh->async_query_in_flight = NULL;
22932293

2294+
mariadb_list_add(imp_drh->active_imp_dbhs, imp_dbh->list_entry, imp_dbh);
2295+
22942296
return TRUE;
22952297
}
22962298

@@ -2370,10 +2372,16 @@ static bool mariadb_db_my_login(pTHX_ SV* dbh, imp_dbh_t *imp_dbh)
23702372
}
23712373
if (!found_taken_pmysql)
23722374
{
2375+
/* This imp_dbh data belongs to different connection, so destructor should not touch it */
2376+
imp_dbh->list_entry = NULL;
23732377
imp_dbh->pmysql = NULL;
23742378
mariadb_dr_do_error(dbh, CR_CONNECTION_ERROR, "Connection error: dbi_imp_data is not valid", "HY000");
23752379
return FALSE;
23762380
}
2381+
2382+
/* Add imp_dbh entry into active_imp_dbhs list */
2383+
mariadb_list_add(imp_drh->active_imp_dbhs, imp_dbh->list_entry, imp_dbh);
2384+
23772385
return TRUE;
23782386
}
23792387
if (DBIc_TRACE_LEVEL(imp_xxh) >= 2)
@@ -2452,6 +2460,9 @@ SV *mariadb_db_take_imp_data(SV *dbh, imp_xxh_t *imp_xxh, void *foo)
24522460
imp_drh->taken_pmysqls = newAV();
24532461
av_push(imp_drh->taken_pmysqls, newSViv(PTR2IV(imp_dbh->pmysql)));
24542462

2463+
/* MYSQL* was taken from imp_dbh so remove it also from active_imp_dbhs list */
2464+
mariadb_list_remove(imp_drh->active_imp_dbhs, imp_dbh->list_entry);
2465+
24552466
return &PL_sv_no;
24562467
}
24572468

@@ -2979,39 +2990,23 @@ static void mariadb_dr_close_mysql(pTHX_ imp_drh_t *imp_drh, MYSQL *pmysql)
29792990
}
29802991
}
29812992

2982-
/*
2983-
***************************************************************************
2984-
*
2985-
* Name: mariadb_db_disconnect
2986-
*
2987-
* Purpose: Disconnect a database handle from its database
2988-
*
2989-
* Input: dbh - database handle being disconnected
2990-
* imp_dbh - drivers private database handle data
2991-
*
2992-
* Returns: 1 for success (always)
2993-
*
2994-
**************************************************************************/
2995-
2996-
int mariadb_db_disconnect(SV* dbh, imp_dbh_t* imp_dbh)
2993+
static void mariadb_db_close_mysql(pTHX_ imp_drh_t *imp_drh, imp_dbh_t *imp_dbh)
29972994
{
2998-
dTHX;
29992995
AV *av;
30002996
I32 i;
30012997
MAGIC *mg;
30022998
SV **svp;
30032999
SV *sv;
30043000
SV *sth;
30053001
imp_sth_t *imp_sth;
3006-
D_imp_xxh(dbh);
3007-
D_imp_drh_from_dbh;
30083002

3009-
/* We assume that disconnect will always work */
3010-
/* since most errors imply already disconnected. */
3003+
if (DBIc_TRACE_LEVEL(imp_dbh) >= 2)
3004+
PerlIO_printf(DBIc_LOGPIO(imp_dbh), "\tmariadb_db_close_mysql: imp_dbh=%p pmysql=%p\n", imp_dbh, imp_dbh->pmysql);
3005+
30113006
DBIc_ACTIVE_off(imp_dbh);
3012-
if (DBIc_TRACE_LEVEL(imp_xxh) >= 2)
3013-
PerlIO_printf(DBIc_LOGPIO(imp_xxh), "imp_dbh->pmysql: %p\n",
3014-
imp_dbh->pmysql);
3007+
3008+
if (imp_dbh->list_entry)
3009+
mariadb_list_remove(imp_drh->active_imp_dbhs, imp_dbh->list_entry);
30153010

30163011
if (imp_dbh->pmysql)
30173012
{
@@ -3045,14 +3040,39 @@ int mariadb_db_disconnect(SV* dbh, imp_dbh_t* imp_dbh)
30453040
* CVE 2017-3302 do not do it. So do it manually to prevent crash. */
30463041
if (imp_sth->stmt && imp_sth->stmt->mysql)
30473042
{
3048-
if (DBIc_TRACE_LEVEL(imp_xxh) >= 2)
3049-
PerlIO_printf(DBIc_LOGPIO(imp_xxh), "Applying CVE 2017-3302 workaround for sth=0x%p\n", imp_sth);
3043+
if (DBIc_TRACE_LEVEL(imp_dbh) >= 2)
3044+
PerlIO_printf(DBIc_LOGPIO(imp_dbh), "Applying CVE 2017-3302 workaround for sth=%p\n", imp_sth);
30503045
imp_sth->stmt->mysql = NULL;
30513046
}
30523047
}
30533048
}
30543049
}
30553050
}
3051+
}
3052+
3053+
/*
3054+
***************************************************************************
3055+
*
3056+
* Name: mariadb_db_disconnect
3057+
*
3058+
* Purpose: Disconnect a database handle from its database
3059+
*
3060+
* Input: dbh - database handle being disconnected
3061+
* imp_dbh - drivers private database handle data
3062+
*
3063+
* Returns: 1 for success (always)
3064+
*
3065+
**************************************************************************/
3066+
3067+
int mariadb_db_disconnect(SV* dbh, imp_dbh_t* imp_dbh)
3068+
{
3069+
dTHX;
3070+
D_imp_drh_from_dbh;
3071+
PERL_UNUSED_ARG(dbh);
3072+
3073+
/* We assume that disconnect will always work */
3074+
/* since most errors imply already disconnected. */
3075+
mariadb_db_close_mysql(aTHX_ imp_drh, imp_dbh);
30563076

30573077
/* We don't free imp_dbh since a reference still exists */
30583078
/* The DESTROY method is the only one to 'free' memory. */
@@ -3075,10 +3095,8 @@ int mariadb_db_disconnect(SV* dbh, imp_dbh_t* imp_dbh)
30753095

30763096
int mariadb_dr_discon_all (SV *drh, imp_drh_t *imp_drh) {
30773097
dTHX;
3078-
dSP;
30793098
int ret;
30803099
SV **svp;
3081-
AV *av;
30823100
I32 i;
30833101
PERL_UNUSED_ARG(drh);
30843102

@@ -3098,34 +3116,8 @@ int mariadb_dr_discon_all (SV *drh, imp_drh_t *imp_drh) {
30983116
imp_drh->taken_pmysqls = NULL;
30993117
}
31003118

3101-
svp = hv_fetchs((HV*)DBIc_MY_H(imp_drh), "ChildHandles", FALSE);
3102-
if (svp && *svp)
3103-
{
3104-
SvGETMAGIC(*svp);
3105-
if (SvROK(*svp) && SvTYPE(SvRV(*svp)) == SVt_PVAV)
3106-
{
3107-
av = (AV *)SvRV(*svp);
3108-
for (i = AvFILL(av); i >= 0; --i)
3109-
{
3110-
svp = av_fetch(av, i, FALSE);
3111-
if (!svp || !*svp || !sv_isobject(*svp))
3112-
continue;
3113-
3114-
ENTER;
3115-
SAVETMPS;
3116-
3117-
PUSHMARK(SP);
3118-
EXTEND(SP, 1);
3119-
PUSHs(sv_2mortal(newSVsv(*svp)));
3120-
PUTBACK;
3121-
3122-
call_method("disconnect", G_VOID|G_DISCARD|G_EVAL|G_KEEPERR);
3123-
3124-
FREETMPS;
3125-
LEAVE;
3126-
}
3127-
}
3128-
}
3119+
while (imp_drh->active_imp_dbhs)
3120+
mariadb_db_close_mysql(aTHX_ imp_drh, (imp_dbh_t *)imp_drh->active_imp_dbhs->data);
31293121

31303122
ret = 1;
31313123

dbdimp.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,35 @@ enum av_attribs {
417417
}; /* purposes only */
418418

419419

420+
/* Double linked list */
421+
struct mariadb_list_entry {
422+
void *data;
423+
struct mariadb_list_entry *prev;
424+
struct mariadb_list_entry *next;
425+
};
426+
#define mariadb_list_add(list, entry, ptr) \
427+
STMT_START { \
428+
Newz(0, (entry), 1, struct mariadb_list_entry); \
429+
(entry)->data = (ptr); \
430+
(entry)->prev = NULL; \
431+
(entry)->next = (list); \
432+
if ((list)) \
433+
(list)->prev = (entry); \
434+
(list) = (entry); \
435+
} STMT_END
436+
#define mariadb_list_remove(list, entry) \
437+
STMT_START { \
438+
if ((entry)->prev) \
439+
(entry)->prev->next = (entry)->next; \
440+
if ((entry)->next) \
441+
(entry)->next->prev = (entry)->prev; \
442+
if ((list) == (entry)) \
443+
(list) = (entry)->next; \
444+
Safefree((entry)); \
445+
(entry) = NULL; \
446+
} STMT_END
447+
448+
420449
/*
421450
* This is our part of the driver handle. We receive the handle as
422451
* an "SV*", say "drh", and receive a pointer to the structure below
@@ -429,6 +458,8 @@ enum av_attribs {
429458
*/
430459
struct imp_drh_st {
431460
dbih_drc_t com; /* MUST be first element in structure */
461+
462+
struct mariadb_list_entry *active_imp_dbhs; /* List of imp_dbh structures with active MYSQL* */
432463
unsigned long int instances;
433464
bool non_embedded_started;
434465
#if !defined(HAVE_EMBEDDED) && defined(HAVE_BROKEN_INIT)
@@ -454,6 +485,7 @@ struct imp_drh_st {
454485
struct imp_dbh_st {
455486
dbih_dbc_t com; /* MUST be first element in structure */
456487

488+
struct mariadb_list_entry *list_entry; /* Entry of imp_drh->active_imp_dbhs list */
457489
MYSQL *pmysql;
458490
bool connected; /* Set to true after DBI->connect finished */
459491
bool auto_reconnect;

t/14destruct.t

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use strict;
2+
use warnings;
3+
4+
use Test::More;
5+
use DBI;
6+
7+
use vars qw($test_dsn $test_user $test_password);
8+
use lib 't', '.';
9+
require 'lib.pl';
10+
11+
my $dbh = DbiTestConnect($test_dsn, $test_user, $test_password, { RaiseError => 1, PrintError => 0 });
12+
13+
plan tests => 3;
14+
15+
ok my $sth1 = $dbh->prepare("SELECT 1");
16+
ok my $sth2 = $dbh->prepare("SELECT 1", { mariadb_server_prepare => 1 });
17+
18+
# install a handler so that a warning about unfreed resources gets caught
19+
$SIG{__WARN__} = sub { die @_ };
20+
21+
END {
22+
my $sth1_copy = $sth1;
23+
my $sth2_copy = $sth2;
24+
pass;
25+
}

0 commit comments

Comments
 (0)