Skip to content

Fix GH-16131: Prevent mixing PDO sub-classes with different DSN #16167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 52 additions & 28 deletions ext/pdo/pdo_dbh.c
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ static char *dsn_from_uri(char *uri, char *buf, size_t buflen) /* {{{ */
}
/* }}} */

static bool create_driver_specific_pdo_object(pdo_driver_t *driver, zend_class_entry *called_scope, zval *new_object)
static bool create_driver_specific_pdo_object(pdo_driver_t *driver, zend_class_entry *called_scope, zval *new_zval_object)
{
zend_class_entry *ce;
zend_class_entry *ce_based_on_driver_name = NULL, *ce_based_on_called_object = NULL;
Expand All @@ -235,47 +235,70 @@ static bool create_driver_specific_pdo_object(pdo_driver_t *driver, zend_class_e

if (ce_based_on_called_object) {
if (ce_based_on_driver_name) {
if (instanceof_function(ce_based_on_called_object, ce_based_on_driver_name) == false) {
if (!instanceof_function(ce_based_on_called_object, ce_based_on_driver_name)) {
zend_throw_exception_ex(pdo_exception_ce, 0,
"%s::connect() cannot be called when connecting to the \"%s\" driver, "
"either %s::connect() or PDO::connect() must be called instead",
ZSTR_VAL(called_scope->name), driver->driver_name, ZSTR_VAL(ce_based_on_driver_name->name));
"%s::%s() cannot be used for connecting to the \"%s\" driver, "
"either call %s::%s() or PDO::%s() instead",
ZSTR_VAL(called_scope->name),
new_zval_object ? "connect" : "__construct",
driver->driver_name,
ZSTR_VAL(ce_based_on_driver_name->name),
new_zval_object ? "connect" : "__construct",
new_zval_object ? "connect" : "__construct"
);
return false;
}

/* A driver-specific implementation was instantiated via the connect() method of the appropriate driver class */
object_init_ex(new_object, ce_based_on_called_object);
/* A driver-specific implementation is instantiated with the appropriate driver class */
if (new_zval_object) {
object_init_ex(new_zval_object, called_scope);
}
return true;
} else {
zend_throw_exception_ex(pdo_exception_ce, 0,
"%s::connect() cannot be called when connecting to an unknown driver, "
"PDO::connect() must be called instead",
ZSTR_VAL(called_scope->name));
"%s::%s() cannot be used for connecting to an unknown driver, "
"call PDO::%s() instead",
ZSTR_VAL(called_scope->name),
new_zval_object ? "connect" : "__construct",
new_zval_object ? "connect" : "__construct"
);
return false;
}
}

/* A non-driver specific PDO subclass is instantiated via the constructor. This results in the legacy behavior. */
Copy link
Member Author

@kocsismate kocsismate Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this, older tests using legacy "quasi classes" would fail, now that constructor calls are also validated.

if (called_scope != pdo_dbh_ce && new_zval_object == NULL) {
return true;
}

if (ce_based_on_driver_name) {
if (called_scope != pdo_dbh_ce) {
/* A driver-specific implementation was instantiated via the connect method of a wrong driver class */
/* A driver-specific implementation is instantiated with a wrong driver class */
zend_throw_exception_ex(pdo_exception_ce, 0,
"%s::connect() cannot be called when connecting to the \"%s\" driver, "
"either %s::connect() or PDO::connect() must be called instead",
ZSTR_VAL(called_scope->name), driver->driver_name, ZSTR_VAL(ce_based_on_driver_name->name));
"%s::%s() cannot be used for connecting to the \"%s\" driver, "
"either call %s::%s() or PDO::%s() instead",
ZSTR_VAL(called_scope->name),
new_zval_object ? "connect" : "__construct",
driver->driver_name,
ZSTR_VAL(ce_based_on_driver_name->name),
new_zval_object ? "connect" : "__construct",
new_zval_object ? "connect" : "__construct"
);
return false;
}

/* A driver-specific implementation was instantiated via PDO::__construct() */
object_init_ex(new_object, ce_based_on_driver_name);
} else {
if (new_zval_object) {
object_init_ex(new_zval_object, ce_based_on_driver_name);
}
} else if (new_zval_object) {
/* No driver-specific implementation found */
object_init_ex(new_object, called_scope);
object_init_ex(new_zval_object, called_scope);
}

return true;
}

static void internal_construct(INTERNAL_FUNCTION_PARAMETERS, zend_object *object, zend_class_entry *current_scope, zval *new_zval_object)
PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zend_object *current_object, zend_class_entry *called_scope, zval *new_zval_object)
{
pdo_dbh_t *dbh = NULL;
bool is_persistent = 0;
Expand Down Expand Up @@ -343,15 +366,16 @@ static void internal_construct(INTERNAL_FUNCTION_PARAMETERS, zend_object *object
RETURN_THROWS();
}

if (new_zval_object != NULL) {
ZEND_ASSERT((driver->driver_name != NULL) && "PDO driver name is null");
if (!create_driver_specific_pdo_object(driver, current_scope, new_zval_object)) {
RETURN_THROWS();
}
ZEND_ASSERT((driver->driver_name != NULL) && "PDO driver name is null");

if (!create_driver_specific_pdo_object(driver, called_scope, new_zval_object)) {
Copy link
Member Author

@kocsismate kocsismate Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_driver_specific_pdo_object() has to be called for the constructor as well. Previously, only connect() calls eere validated...

RETURN_THROWS();
}

if (new_zval_object) {
dbh = Z_PDO_DBH_P(new_zval_object);
} else {
dbh = php_pdo_dbh_fetch_inner(object);
dbh = php_pdo_dbh_fetch_inner(current_object);
}

/* is this supposed to be a persistent connection ? */
Expand Down Expand Up @@ -413,7 +437,7 @@ static void internal_construct(INTERNAL_FUNCTION_PARAMETERS, zend_object *object
if (pdbh) {
efree(dbh);
/* switch over to the persistent one */
php_pdo_dbh_fetch_object(object)->inner = pdbh;
php_pdo_dbh_fetch_object(current_object)->inner = pdbh;
pdbh->refcount++;
dbh = pdbh;
}
Expand Down Expand Up @@ -497,14 +521,14 @@ static void internal_construct(INTERNAL_FUNCTION_PARAMETERS, zend_object *object
/* {{{ */
PHP_METHOD(PDO, __construct)
{
internal_construct(INTERNAL_FUNCTION_PARAM_PASSTHRU, Z_OBJ(EX(This)), EX(This).value.ce, NULL);
php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAM_PASSTHRU, Z_OBJ_P(ZEND_THIS), Z_OBJCE_P(ZEND_THIS), NULL);
}
/* }}} */

/* {{{ */
PHP_METHOD(PDO, connect)
{
internal_construct(INTERNAL_FUNCTION_PARAM_PASSTHRU, Z_OBJ(EX(This)), EX(This).value.ce, return_value);
php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAM_PASSTHRU, NULL, Z_CE_P(ZEND_THIS), return_value);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EX(This).value.ce is Z_OBJCE_P(ZEND_THIS) which is different than Z_CE_P(ZEND_THIS). For a static method, the latter is needed to properly support the static return type

}
/* }}} */

Expand Down
2 changes: 2 additions & 0 deletions ext/pdo/php_pdo_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,8 @@ PDO_API void php_pdo_dbh_delref(pdo_dbh_t *dbh);
PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt);
PDO_API void php_pdo_stmt_set_column_count(pdo_stmt_t *stmt, int new_count);

PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zend_object *current_object, zend_class_entry *called_scope, zval *new_zval_object);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it makes sense to expose this function for extensions which override the PDO constructor (?)


/* Normalization for fetching long param for driver attributes */
PDO_API bool pdo_get_long_param(zend_long *lval, zval *value);
PDO_API bool pdo_get_bool_param(bool *bval, zval *value);
Expand Down
28 changes: 28 additions & 0 deletions ext/pdo_sqlite/tests/subclasses/gh_16131.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Test calling a PDO sub-class constructor with a different DSN
--EXTENSIONS--
pdo_pgsql
pdo_sqlite
--FILE--
<?php

try {
new Pdo\Pgsql('sqlite::memory:');
} catch (PDOException $e) {
echo $e->getMessage() . "\n";
}

class MyPgsql extends Pdo\Pgsql
{
}

try {
new MyPgsql('sqlite::memory:');
} catch (PDOException $e) {
echo $e->getMessage() . "\n";
}

?>
--EXPECT--
Pdo\Pgsql::__construct() cannot be used for connecting to the "sqlite" driver, either call Pdo\Sqlite::__construct() or PDO::__construct() instead
MyPgsql::__construct() cannot be used for connecting to the "sqlite" driver, either call Pdo\Sqlite::__construct() or PDO::__construct() instead
2 changes: 1 addition & 1 deletion ext/pdo_sqlite/tests/subclasses/pdosqlite_005.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ try {

?>
--EXPECT--
Pdo\Pgsql::connect() cannot be called when connecting to the "sqlite" driver, either Pdo\Sqlite::connect() or PDO::connect() must be called instead
Pdo\Pgsql::connect() cannot be used for connecting to the "sqlite" driver, either call Pdo\Sqlite::connect() or PDO::connect() instead
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new wording is more clear IMO.

2 changes: 1 addition & 1 deletion ext/pdo_sqlite/tests/subclasses/pdosqlite_007.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ try {

?>
--EXPECT--
MyPDO::connect() cannot be called when connecting to the "sqlite" driver, either Pdo\Sqlite::connect() or PDO::connect() must be called instead
MyPDO::connect() cannot be used for connecting to the "sqlite" driver, either call Pdo\Sqlite::connect() or PDO::connect() instead
Loading