Skip to content

Commit a09595c

Browse files
committed
PDO - Added PHP::disconnect and PHP::isConnected and refactored free object.
Added disconnect for __destruct alternative to disconnect from db, related to bug #40681 and requests #62065 and #67473. Added PHP::isConnected to yield current connection status. Fixed bug #63343 in rework of PDO pdo_dbh_free_storage logic to avoid transaction rollback against persistent db connection. Added pdo_is_persisted and pdo_list_entry_from_key helpers to determine when persistent db handler is actively registered as resource, avoiding opaque use of _pdo_dbh_t::refcount to this end. Updated php_pdo_internal_construct_driver to consume added list entry helper and to check _pdo_dbh_t::is_closed in addition to liveness when validating a discovered persistent db connection for re-use. Removed extraneous call to zend_list_close given that only a persistent destructor is defined for the _pdo_dbh_t resource that zend_list_close does not invoke. Fixed potential for bad memory access in persistent connections from former PDO objects, once the connection has been replaced due to liveness check failure. The resource was freed without invalidating other PDO object references, but now the resource will not be freed until the last referring PDO object is freed. Made _pdo_dbh_t::refcount accurate and consistent, regardless of persistent connection, to represent count of PDO object's referencing the handle. In the case of persistent connections, the count was +1, and in the case of non-persistent connections, never decremented.
1 parent 6cc4ae1 commit a09595c

File tree

7 files changed

+276
-53
lines changed

7 files changed

+276
-53
lines changed

ext/pdo/pdo_dbh.c

Lines changed: 120 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,21 @@ static char *dsn_from_uri(char *uri, char *buf, size_t buflen) /* {{{ */
221221
}
222222
/* }}} */
223223

224+
/* Fetch the registered persistent PDO object for the given key */
225+
static pdo_dbh_t *pdo_list_entry_from_key(const char *hashkey, size_t len)
226+
{
227+
pdo_dbh_t *pdbh = NULL;
228+
zend_resource *le;
229+
230+
if ((le = zend_hash_str_find_ptr(&EG(persistent_list), hashkey, len)) != NULL) {
231+
if (le->type == php_pdo_list_entry()) {
232+
pdbh = (pdo_dbh_t*)le->ptr;
233+
}
234+
}
235+
236+
return pdbh;
237+
}
238+
224239
/* {{{ */
225240
PHP_METHOD(PDO, __construct)
226241
{
@@ -297,7 +312,6 @@ PHP_METHOD(PDO, __construct)
297312
if (options) {
298313
int plen = 0;
299314
char *hashkey = NULL;
300-
zend_resource *le;
301315
pdo_dbh_t *pdbh = NULL;
302316
zval *v;
303317

@@ -320,36 +334,22 @@ PHP_METHOD(PDO, __construct)
320334

321335
if (is_persistent) {
322336
/* let's see if we have one cached.... */
323-
if ((le = zend_hash_str_find_ptr(&EG(persistent_list), hashkey, plen)) != NULL) {
324-
if (le->type == php_pdo_list_entry()) {
325-
pdbh = (pdo_dbh_t*)le->ptr;
326-
327-
/* is the connection still alive ? */
328-
if (pdbh->methods->check_liveness && FAILURE == (pdbh->methods->check_liveness)(pdbh)) {
329-
/* nope... need to kill it */
330-
pdbh->refcount--;
331-
zend_list_close(le);
332-
pdbh = NULL;
333-
}
334-
}
335-
}
336-
337-
if (pdbh) {
338-
call_factory = 0;
339-
} else {
337+
pdbh = pdo_list_entry_from_key(hashkey, plen);
338+
/* is the connection still alive ? */
339+
if (!pdbh || pdbh->is_closed ||
340+
(pdbh->methods->check_liveness && FAILURE == (pdbh->methods->check_liveness)(pdbh))) {
340341
/* need a brand new pdbh */
341342
pdbh = pecalloc(1, sizeof(*pdbh), 1);
342343

343-
pdbh->refcount = 1;
344344
pdbh->is_persistent = 1;
345345
pdbh->persistent_id = pemalloc(plen + 1, 1);
346346
memcpy((char *)pdbh->persistent_id, hashkey, plen+1);
347347
pdbh->persistent_id_len = plen;
348348
pdbh->def_stmt_ce = dbh->def_stmt_ce;
349+
} else {
350+
/* found viable dbh persisted */
351+
call_factory = 0;
349352
}
350-
}
351-
352-
if (pdbh) {
353353
efree(dbh);
354354
/* switch over to the persistent one */
355355
Z_PDO_OBJECT_P(object)->inner = pdbh;
@@ -393,6 +393,8 @@ PHP_METHOD(PDO, __construct)
393393
/* register in the persistent list etc. */
394394
/* we should also need to replace the object store entry,
395395
since it was created with emalloc */
396+
/* if a resource is already registered, then it failed a liveness check
397+
* and will be replaced, prompting destruct. */
396398
if ((zend_register_persistent_resource(
397399
(char*)dbh->persistent_id, dbh->persistent_id_len, dbh, php_pdo_list_entry())) == NULL) {
398400
php_error_docref(NULL, E_ERROR, "Failed to register persistent entry");
@@ -422,8 +424,9 @@ PHP_METHOD(PDO, __construct)
422424
}
423425

424426
/* the connection failed; things will tidy up in free_storage */
425-
if (is_persistent) {
426-
dbh->refcount--;
427+
if (dbh->methods) {
428+
/* free any resources allocated during failed factory */
429+
dbh->methods->closer(dbh);
427430
}
428431

429432
/* XXX raise exception */
@@ -587,6 +590,9 @@ PHP_METHOD(PDO, prepare)
587590

588591

589592
static bool pdo_is_in_transaction(pdo_dbh_t *dbh) {
593+
if (dbh->is_closed) {
594+
return false;
595+
}
590596
if (dbh->methods->in_transaction) {
591597
return dbh->methods->in_transaction(dbh);
592598
}
@@ -684,6 +690,17 @@ PHP_METHOD(PDO, inTransaction)
684690
}
685691
/* }}} */
686692

693+
/* {{{ Determine if connected */
694+
PHP_METHOD(PDO, isConnected)
695+
{
696+
pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
697+
698+
ZEND_PARSE_PARAMETERS_NONE();
699+
700+
RETURN_BOOL(!dbh->is_closed);
701+
}
702+
/* }}} */
703+
687704
PDO_API bool pdo_get_long_param(zend_long *lval, zval *value)
688705
{
689706
switch (Z_TYPE_P(value)) {
@@ -1027,8 +1044,6 @@ PHP_METHOD(PDO, errorCode)
10271044

10281045
ZEND_PARSE_PARAMETERS_NONE();
10291046

1030-
PDO_CONSTRUCT_CHECK;
1031-
10321047
if (dbh->query_stmt) {
10331048
RETURN_STRING(dbh->query_stmt->error_code);
10341049
}
@@ -1056,8 +1071,6 @@ PHP_METHOD(PDO, errorInfo)
10561071

10571072
ZEND_PARSE_PARAMETERS_NONE();
10581073

1059-
PDO_CONSTRUCT_CHECK;
1060-
10611074
array_init(return_value);
10621075

10631076
if (dbh->query_stmt) {
@@ -1068,7 +1081,8 @@ PHP_METHOD(PDO, errorInfo)
10681081
if(!strncmp(dbh->error_code, PDO_ERR_NONE, sizeof(PDO_ERR_NONE))) goto fill_array;
10691082
}
10701083

1071-
if (dbh->methods->fetch_err) {
1084+
/* Driver-implemented error is not available once database is shutdown. */
1085+
if (dbh->driver && dbh->methods->fetch_err) {
10721086
dbh->methods->fetch_err(dbh, dbh->query_stmt, return_value);
10731087
}
10741088

@@ -1366,26 +1380,55 @@ void pdo_dbh_init(int module_number)
13661380
pdo_dbh_object_handlers.get_gc = dbh_get_gc;
13671381
}
13681382

1369-
static void dbh_free(pdo_dbh_t *dbh, bool free_persistent)
1383+
/* Disconnect from the database and free associated driver. */
1384+
static void dbh_shutdown(pdo_dbh_t *dbh)
13701385
{
1371-
int i;
1386+
if (dbh->driver_data && dbh->methods && dbh->methods->rollback && pdo_is_in_transaction(dbh)) {
1387+
dbh->methods->rollback(dbh);
1388+
dbh->in_txn = false;
1389+
}
13721390

1373-
if (dbh->query_stmt) {
1374-
zval_ptr_dtor(&dbh->query_stmt_zval);
1375-
dbh->query_stmt = NULL;
1391+
if (dbh->methods) {
1392+
dbh->methods->closer(dbh);
13761393
}
13771394

1378-
if (dbh->is_persistent) {
1395+
/* Do not permit reference to driver to remain past closer(), which
1396+
* is responsible for both disconnecting the db and free-ing allocations. */
1397+
dbh->driver = NULL;
1398+
dbh->is_closed = true;
1399+
}
1400+
1401+
/* {{{ Disconnect from the database. */
1402+
PHP_METHOD(PDO, disconnect)
1403+
{
1404+
pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
1405+
1406+
ZEND_PARSE_PARAMETERS_NONE();
1407+
1408+
PDO_DBH_CLEAR_ERR();
1409+
PDO_CONSTRUCT_CHECK;
1410+
1411+
dbh_shutdown(dbh);
1412+
1413+
PDO_HANDLE_DBH_ERR();
1414+
1415+
RETURN_TRUE;
1416+
}
1417+
/* }}} */
1418+
1419+
/* Free the database when the last pdo object referencing it is freed
1420+
* or when it has been registered as a php resource, i.e. is persistent,
1421+
* and the resource is destructed, whichever comes last. */
1422+
static void dbh_free(pdo_dbh_t *dbh)
1423+
{
1424+
int i;
1425+
13791426
#if ZEND_DEBUG
1380-
ZEND_ASSERT(!free_persistent || (dbh->refcount == 1));
1427+
ZEND_ASSERT(dbh->refcount == 0);
13811428
#endif
1382-
if (!free_persistent && (--dbh->refcount)) {
1383-
return;
1384-
}
1385-
}
13861429

1387-
if (dbh->methods) {
1388-
dbh->methods->closer(dbh);
1430+
if (dbh->driver) {
1431+
dbh_shutdown(dbh);
13891432
}
13901433

13911434
if (dbh->data_source) {
@@ -1416,25 +1459,45 @@ static void dbh_free(pdo_dbh_t *dbh, bool free_persistent)
14161459
pefree(dbh, dbh->is_persistent);
14171460
}
14181461

1462+
/* Whether the given database handler is presently registered as a resource. */
1463+
static bool pdo_is_persisted(pdo_dbh_t *dbh)
1464+
{
1465+
pdo_dbh_t *pdbh = NULL;
1466+
1467+
if (dbh->persistent_id != NULL) {
1468+
pdbh = pdo_list_entry_from_key(dbh->persistent_id, dbh->persistent_id_len);
1469+
return dbh == pdbh;
1470+
}
1471+
1472+
return false;
1473+
}
1474+
14191475
static void pdo_dbh_free_storage(zend_object *std)
14201476
{
14211477
pdo_dbh_t *dbh = php_pdo_dbh_fetch_inner(std);
14221478

14231479
/* dbh might be null if we OOMed during object initialization. */
1424-
if (!dbh) {
1425-
return;
1426-
}
1480+
if (dbh) {
1481+
if (dbh->is_persistent && dbh->methods && dbh->methods->persistent_shutdown) {
1482+
dbh->methods->persistent_shutdown(dbh);
1483+
}
14271484

1428-
if (dbh->driver_data && dbh->methods && dbh->methods->rollback && pdo_is_in_transaction(dbh)) {
1429-
dbh->methods->rollback(dbh);
1430-
dbh->in_txn = false;
1431-
}
1485+
/* stmt is not persistent, even if dbh is, so it must be freed with pdo.
1486+
* Consider copying stmt error code to dbh at this point, seemingly the reason
1487+
* that the stmt is even being held, or even better, to do that at the time of
1488+
* error and remove the reference altogether. */
1489+
if (dbh->query_stmt) {
1490+
zval_ptr_dtor(&dbh->query_stmt_zval);
1491+
dbh->query_stmt = NULL;
1492+
}
14321493

1433-
if (dbh->is_persistent && dbh->methods && dbh->methods->persistent_shutdown) {
1434-
dbh->methods->persistent_shutdown(dbh);
1494+
/* a persisted dbh will be freed when the resource is destructed. */
1495+
if (!(--dbh->refcount) && !pdo_is_persisted(dbh)) {
1496+
dbh_free(dbh);
1497+
}
14351498
}
1499+
14361500
zend_object_std_dtor(std);
1437-
dbh_free(dbh, 0);
14381501
}
14391502

14401503
zend_object *pdo_dbh_new(zend_class_entry *ce)
@@ -1447,6 +1510,7 @@ zend_object *pdo_dbh_new(zend_class_entry *ce)
14471510
rebuild_object_properties(&dbh->std);
14481511
dbh->inner = ecalloc(1, sizeof(pdo_dbh_t));
14491512
dbh->inner->def_stmt_ce = pdo_dbstmt_ce;
1513+
dbh->inner->refcount++;
14501514

14511515
return &dbh->std;
14521516
}
@@ -1457,7 +1521,10 @@ ZEND_RSRC_DTOR_FUNC(php_pdo_pdbh_dtor) /* {{{ */
14571521
{
14581522
if (res->ptr) {
14591523
pdo_dbh_t *dbh = (pdo_dbh_t*)res->ptr;
1460-
dbh_free(dbh, 1);
1524+
if (!dbh->refcount) {
1525+
/* do not free if still referenced by pdo */
1526+
dbh_free(dbh);
1527+
}
14611528
res->ptr = NULL;
14621529
}
14631530
}

ext/pdo/pdo_dbh.stub.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ public function beginTransaction(): bool {}
394394
/** @tentative-return-type */
395395
public function commit(): bool {}
396396

397+
/** @tentative-return-type */
398+
public function disconnect(): bool {}
399+
397400
/** @tentative-return-type */
398401
public function errorCode(): ?string {}
399402

@@ -412,6 +415,9 @@ public static function getAvailableDrivers(): array {}
412415
/** @tentative-return-type */
413416
public function inTransaction(): bool {}
414417

418+
/** @tentative-return-type */
419+
public function isConnected(): bool {}
420+
415421
/** @tentative-return-type */
416422
public function lastInsertId(?string $name = null): string|false {}
417423

ext/pdo/pdo_dbh_arginfo.h

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/pdo/php_pdo.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ PHP_MINFO_FUNCTION(pdo);
5656
#define LONG_CONST(c) (zend_long) c
5757

5858
#define PDO_CONSTRUCT_CHECK \
59+
if (dbh->is_closed) { \
60+
zend_throw_exception_ex(php_pdo_get_exception(), 0, "connection is closed"); \
61+
RETURN_THROWS(); \
62+
} \
5963
if (!dbh->driver) { \
6064
zend_throw_error(NULL, "PDO object is not initialized, constructor was not called"); \
6165
RETURN_THROWS(); \

ext/pdo/php_pdo_driver.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ struct _pdo_dbh_t {
478478
/* persistent hash key associated with this handle */
479479
const char *persistent_id;
480480
size_t persistent_id_len;
481+
482+
/* counter of _pdo_dbh_object_t referencing this handle */
481483
unsigned int refcount;
482484

483485
/* driver specific "class" methods for the dbh and stmt */

0 commit comments

Comments
 (0)