Skip to content

Commit 458c882

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 458c882

File tree

7 files changed

+273
-54
lines changed

7 files changed

+273
-54
lines changed

ext/pdo/pdo_dbh.c

Lines changed: 117 additions & 54 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,9 +424,6 @@ 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-
}
428427

429428
/* XXX raise exception */
430429
zend_restore_error_handling(&zeh);
@@ -587,6 +586,9 @@ PHP_METHOD(PDO, prepare)
587586

588587

589588
static bool pdo_is_in_transaction(pdo_dbh_t *dbh) {
589+
if (dbh->is_closed) {
590+
return false;
591+
}
590592
if (dbh->methods->in_transaction) {
591593
return dbh->methods->in_transaction(dbh);
592594
}
@@ -684,6 +686,17 @@ PHP_METHOD(PDO, inTransaction)
684686
}
685687
/* }}} */
686688

689+
/* {{{ Determine if connected */
690+
PHP_METHOD(PDO, isConnected)
691+
{
692+
pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
693+
694+
ZEND_PARSE_PARAMETERS_NONE();
695+
696+
RETURN_BOOL(!dbh->is_closed);
697+
}
698+
/* }}} */
699+
687700
PDO_API bool pdo_get_long_param(zend_long *lval, zval *value)
688701
{
689702
switch (Z_TYPE_P(value)) {
@@ -1027,8 +1040,6 @@ PHP_METHOD(PDO, errorCode)
10271040

10281041
ZEND_PARSE_PARAMETERS_NONE();
10291042

1030-
PDO_CONSTRUCT_CHECK;
1031-
10321043
if (dbh->query_stmt) {
10331044
RETURN_STRING(dbh->query_stmt->error_code);
10341045
}
@@ -1056,8 +1067,6 @@ PHP_METHOD(PDO, errorInfo)
10561067

10571068
ZEND_PARSE_PARAMETERS_NONE();
10581069

1059-
PDO_CONSTRUCT_CHECK;
1060-
10611070
array_init(return_value);
10621071

10631072
if (dbh->query_stmt) {
@@ -1068,7 +1077,8 @@ PHP_METHOD(PDO, errorInfo)
10681077
if(!strncmp(dbh->error_code, PDO_ERR_NONE, sizeof(PDO_ERR_NONE))) goto fill_array;
10691078
}
10701079

1071-
if (dbh->methods->fetch_err) {
1080+
/* Driver-implemented error is not available once database is shutdown. */
1081+
if (dbh->driver && dbh->methods->fetch_err) {
10721082
dbh->methods->fetch_err(dbh, dbh->query_stmt, return_value);
10731083
}
10741084

@@ -1366,26 +1376,55 @@ void pdo_dbh_init(int module_number)
13661376
pdo_dbh_object_handlers.get_gc = dbh_get_gc;
13671377
}
13681378

1369-
static void dbh_free(pdo_dbh_t *dbh, bool free_persistent)
1379+
/* Disconnect from the database and free associated driver. */
1380+
static void dbh_shutdown(pdo_dbh_t *dbh)
13701381
{
1371-
int i;
1382+
if (dbh->driver_data && dbh->methods && dbh->methods->rollback && pdo_is_in_transaction(dbh)) {
1383+
dbh->methods->rollback(dbh);
1384+
dbh->in_txn = false;
1385+
}
13721386

1373-
if (dbh->query_stmt) {
1374-
zval_ptr_dtor(&dbh->query_stmt_zval);
1375-
dbh->query_stmt = NULL;
1387+
if (dbh->methods) {
1388+
dbh->methods->closer(dbh);
13761389
}
13771390

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

1387-
if (dbh->methods) {
1388-
dbh->methods->closer(dbh);
1426+
if (dbh->driver) {
1427+
dbh_shutdown(dbh);
13891428
}
13901429

13911430
if (dbh->data_source) {
@@ -1416,25 +1455,45 @@ static void dbh_free(pdo_dbh_t *dbh, bool free_persistent)
14161455
pefree(dbh, dbh->is_persistent);
14171456
}
14181457

1458+
/* Whether the given database handler is presently registered as a resource. */
1459+
static bool pdo_is_persisted(pdo_dbh_t *dbh)
1460+
{
1461+
pdo_dbh_t *pdbh = NULL;
1462+
1463+
if (dbh->persistent_id != NULL) {
1464+
pdbh = pdo_list_entry_from_key(dbh->persistent_id, dbh->persistent_id_len);
1465+
return dbh == pdbh;
1466+
}
1467+
1468+
return false;
1469+
}
1470+
14191471
static void pdo_dbh_free_storage(zend_object *std)
14201472
{
14211473
pdo_dbh_t *dbh = php_pdo_dbh_fetch_inner(std);
14221474

14231475
/* dbh might be null if we OOMed during object initialization. */
1424-
if (!dbh) {
1425-
return;
1426-
}
1476+
if (dbh) {
1477+
if (dbh->is_persistent && dbh->methods && dbh->methods->persistent_shutdown) {
1478+
dbh->methods->persistent_shutdown(dbh);
1479+
}
14271480

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-
}
1481+
/* stmt is not persistent, even if dbh is, so it must be freed with pdo.
1482+
* Consider copying stmt error code to dbh at this point, seemingly the reason
1483+
* that the stmt is even being held, or even better, to do that at the time of
1484+
* error and remove the reference altogether. */
1485+
if (dbh->query_stmt) {
1486+
zval_ptr_dtor(&dbh->query_stmt_zval);
1487+
dbh->query_stmt = NULL;
1488+
}
14321489

1433-
if (dbh->is_persistent && dbh->methods && dbh->methods->persistent_shutdown) {
1434-
dbh->methods->persistent_shutdown(dbh);
1490+
/* a persisted dbh will be freed when the resource is destructed. */
1491+
if (!(--dbh->refcount) && !pdo_is_persisted(dbh)) {
1492+
dbh_free(dbh);
1493+
}
14351494
}
1495+
14361496
zend_object_std_dtor(std);
1437-
dbh_free(dbh, 0);
14381497
}
14391498

14401499
zend_object *pdo_dbh_new(zend_class_entry *ce)
@@ -1447,6 +1506,7 @@ zend_object *pdo_dbh_new(zend_class_entry *ce)
14471506
rebuild_object_properties(&dbh->std);
14481507
dbh->inner = ecalloc(1, sizeof(pdo_dbh_t));
14491508
dbh->inner->def_stmt_ce = pdo_dbstmt_ce;
1509+
dbh->inner->refcount++;
14501510

14511511
return &dbh->std;
14521512
}
@@ -1457,7 +1517,10 @@ ZEND_RSRC_DTOR_FUNC(php_pdo_pdbh_dtor) /* {{{ */
14571517
{
14581518
if (res->ptr) {
14591519
pdo_dbh_t *dbh = (pdo_dbh_t*)res->ptr;
1460-
dbh_free(dbh, 1);
1520+
if (!dbh->refcount) {
1521+
/* do not free if still referenced by pdo */
1522+
dbh_free(dbh);
1523+
}
14611524
res->ptr = NULL;
14621525
}
14631526
}

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)