Skip to content

ext/pdo: Refactor validation of fetch mode in PDO statement #17699

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

Merged
merged 4 commits into from
Feb 9, 2025
Merged
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
7 changes: 7 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ PHP 8.5 UPGRADE NOTES
array value reference assignment: $ctor_args = [&$valByRef]
. Attempting to modify a PDOStatement during a call to PDO::fetch(),
PDO::fetchObject(), PDO::fetchAll() will now throw an Error.
. The value of the constants PDO::FETCH_GROUP, PDO::FETCH_UNIQUE,
PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, and PDO::FETCH_SERIALIZE
has changed.
. A ValueError is now thrown if PDO::FETCH_PROPS_LATE is used with a fetch
mode different than PDO::FETCH_CLASS, consistent with other fetch flags.
. A ValueError is now thrown if PDO::FETCH_INTO is used as a fetch mode in
PDO::fetchAll(), similar to PDO::FETCH_LAZY.

- PDO_FIREBIRD:
. A ValueError is now thrown when trying to set a cursor name that is too
Expand Down
100 changes: 61 additions & 39 deletions ext/pdo/pdo_stmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h
if (how == PDO_FETCH_COLUMN) {
int colno = stmt->fetch.column;

if ((flags & PDO_FETCH_GROUP) && stmt->fetch.column == -1) {
if ((flags & (PDO_FETCH_GROUP|PDO_FETCH_UNIQUE)) && stmt->fetch.column == -1) {
colno = 1;
}

Expand Down Expand Up @@ -946,59 +946,81 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h


// TODO Error on the following cases:
// Using any fetch flag with PDO_FETCH_KEY_PAIR
// Combining PDO_FETCH_UNIQUE and PDO_FETCH_GROUP
// Using PDO_FETCH_UNIQUE or PDO_FETCH_GROUP outside of fetchAll()
// Combining PDO_FETCH_PROPS_LATE with a fetch mode different than PDO_FETCH_CLASS
// Improve error detection when combining PDO_FETCH_USE_DEFAULT with
// Reject PDO_FETCH_INTO mode with fetch_all as no support
// Reject $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, value); bypass
static bool pdo_stmt_verify_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_arg_num, bool fetch_all) /* {{{ */
static bool pdo_verify_fetch_mode(uint32_t default_mode_and_flags, zend_long mode_and_flags, uint32_t mode_arg_num, bool fetch_all) /* {{{ */
{
int flags = mode & PDO_FETCH_FLAGS;

mode = mode & ~PDO_FETCH_FLAGS;

if (mode < 0 || mode >= PDO_FETCH__MAX) {
/* Mode must be a positive */
if (mode_and_flags < 0 || mode_and_flags >= PDO_FIRST_INVALID_FLAG) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this change is right. What if mode is equal to 15? This number doesn't correspond to a real mode but it will not trigger the mode_and_flags >= PDO_FIRST_INVALID_FLAG condition.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I'll add a test for this.

Copy link
Member

Choose a reason for hiding this comment

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

Okay, just this test then and then it's good to go.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually this is properly guarded by the switch (mode) { below, but the test just ensure this continues to be the case.

Copy link
Member

Choose a reason for hiding this comment

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

Well, now we know for sure!

zend_argument_value_error(mode_arg_num, "must be a bitmask of PDO::FETCH_* constants");
return 0;
return false;
}

uint32_t flags = (zend_ulong)mode_and_flags & PDO_FETCH_FLAGS;
enum pdo_fetch_type mode = (zend_ulong)mode_and_flags & ~PDO_FETCH_FLAGS;

if (mode == PDO_FETCH_USE_DEFAULT) {
flags = stmt->default_fetch_type & PDO_FETCH_FLAGS;
mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS;
flags = default_mode_and_flags & PDO_FETCH_FLAGS;
mode = default_mode_and_flags & ~PDO_FETCH_FLAGS;
}

switch(mode) {
/* Flags can only be used in limited circumstances */
if (flags != 0) {
bool has_class_flags = (flags & (PDO_FETCH_CLASSTYPE|PDO_FETCH_PROPS_LATE|PDO_FETCH_SERIALIZE)) != 0;
if (has_class_flags && mode != PDO_FETCH_CLASS) {
zend_argument_value_error(mode_arg_num, "cannot use PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, or PDO::FETCH_SERIALIZE fetch flags with a fetch mode other than PDO::FETCH_CLASS");
return false;
}
// TODO Prevent setting those flags together or not? This would affect PDO::setFetchMode()
//bool has_grouping_flags = flags & (PDO_FETCH_GROUP|PDO_FETCH_UNIQUE);
//if (has_grouping_flags && !fetch_all) {
// zend_argument_value_error(mode_arg_num, "cannot use PDO::FETCH_GROUP, or PDO::FETCH_UNIQUE fetch flags outside of PDOStatement::fetchAll()");
// return false;
//}
if (flags & PDO_FETCH_SERIALIZE) {
php_error_docref(NULL, E_DEPRECATED, "The PDO::FETCH_SERIALIZE mode is deprecated");
if (UNEXPECTED(EG(exception))) {
return false;
}
}
}

switch (mode) {
case PDO_FETCH_FUNC:
if (!fetch_all) {
zend_value_error("Can only use PDO::FETCH_FUNC in PDOStatement::fetchAll()");
return 0;
zend_argument_value_error(mode_arg_num, "PDO::FETCH_FUNC can only be used with PDOStatement::fetchAll()");
return false;
}
return 1;
return true;

case PDO_FETCH_LAZY:
if (fetch_all) {
zend_argument_value_error(mode_arg_num, "cannot be PDO::FETCH_LAZY in PDOStatement::fetchAll()");
return 0;
}
ZEND_FALLTHROUGH;
default:
if ((flags & PDO_FETCH_SERIALIZE) == PDO_FETCH_SERIALIZE) {
zend_argument_value_error(mode_arg_num, "must use PDO::FETCH_SERIALIZE with PDO::FETCH_CLASS");
return 0;
zend_argument_value_error(mode_arg_num, "PDO::FETCH_LAZY cannot be used with PDOStatement::fetchAll()");
return false;
}
if ((flags & PDO_FETCH_CLASSTYPE) == PDO_FETCH_CLASSTYPE) {
zend_argument_value_error(mode_arg_num, "must use PDO::FETCH_CLASSTYPE with PDO::FETCH_CLASS");
return 0;
return true;

case PDO_FETCH_INTO:
if (fetch_all) {
zend_argument_value_error(mode_arg_num, "PDO::FETCH_INTO cannot be used with PDOStatement::fetchAll()");
return false;
}
ZEND_FALLTHROUGH;
return true;

case PDO_FETCH_ASSOC:
case PDO_FETCH_NUM:
case PDO_FETCH_BOTH:
case PDO_FETCH_OBJ:
case PDO_FETCH_BOUND:
case PDO_FETCH_COLUMN:
case PDO_FETCH_CLASS:
if (flags & PDO_FETCH_SERIALIZE) {
php_error_docref(NULL, E_DEPRECATED, "The PDO::FETCH_SERIALIZE mode is deprecated");
}
return 1;
case PDO_FETCH_NAMED:
case PDO_FETCH_KEY_PAIR:
return true;

default:
zend_argument_value_error(mode_arg_num, "must be a bitmask of PDO::FETCH_* constants");
return false;
}
}
/* }}} */
Expand All @@ -1020,7 +1042,7 @@ PHP_METHOD(PDOStatement, fetch)
PHP_STMT_GET_OBJ;
PDO_STMT_CLEAR_ERR();

if (!pdo_stmt_verify_mode(stmt, how, 1, false)) {
if (!pdo_verify_fetch_mode(stmt->default_fetch_type, how, 1, false)) {
RETURN_THROWS();
}

Expand Down Expand Up @@ -1140,7 +1162,7 @@ PHP_METHOD(PDOStatement, fetchAll)
ZEND_PARSE_PARAMETERS_END();

PHP_STMT_GET_OBJ;
if (!pdo_stmt_verify_mode(stmt, how, 1, true)) {
if (!pdo_verify_fetch_mode(stmt->default_fetch_type, how, 1, true)) {
RETURN_THROWS();
}

Expand Down Expand Up @@ -1215,7 +1237,7 @@ PHP_METHOD(PDOStatement, fetchAll)
}
stmt->fetch.column = Z_LVAL_P(arg2);
} else {
stmt->fetch.column = flags & PDO_FETCH_GROUP ? -1 : 0;
stmt->fetch.column = flags & (PDO_FETCH_GROUP|PDO_FETCH_UNIQUE) ? -1 : 0;
}
break;

Expand Down Expand Up @@ -1606,7 +1628,7 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a

flags = mode & PDO_FETCH_FLAGS;

if (!pdo_stmt_verify_mode(stmt, mode, mode_arg_num, false)) {
if (!pdo_verify_fetch_mode(stmt->default_fetch_type, mode, mode_arg_num, false)) {
return false;
}

Expand Down
17 changes: 10 additions & 7 deletions ext/pdo/php_pdo_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ enum pdo_param_type {

#define PDO_PARAM_TYPE(x) ((x) & ~PDO_PARAM_FLAGS)

/* Fetch mode is a bitmask of the fetch type (first 4 bits) with the fetch flags (bit 5 to 9)*/
enum pdo_fetch_type {
PDO_FETCH_USE_DEFAULT,
PDO_FETCH_LAZY,
Expand All @@ -77,15 +78,17 @@ enum pdo_fetch_type {
PDO_FETCH_FUNC, /* fetch into function and return its result */
PDO_FETCH_NAMED, /* like PDO_FETCH_ASSOC, but can handle duplicate names */
PDO_FETCH_KEY_PAIR, /* fetch into an array where the 1st column is a key and all subsequent columns are values */
PDO_FETCH__MAX /* must be last */
};

#define PDO_FETCH_FLAGS 0xFFFF0000 /* fetchAll() modes or'd to PDO_FETCH_XYZ */
#define PDO_FETCH_GROUP 0x00010000 /* fetch into groups */
#define PDO_FETCH_UNIQUE 0x00030000 /* fetch into groups assuming first col is unique */
#define PDO_FETCH_CLASSTYPE 0x00040000 /* fetch class gets its class name from 1st column */
#define PDO_FETCH_SERIALIZE 0x00080000 /* fetch class instances by calling serialize */
#define PDO_FETCH_PROPS_LATE 0x00100000 /* fetch props after calling ctor */
#define PDO_FETCH_FLAGS 0xFFFFFFF0 /* fetch flags mask */
#define PDO_FETCH_GROUP (1u << 5u) /* fetch into groups */
#define PDO_FETCH_UNIQUE (1u << 6u) /* fetch into groups assuming first col is unique */
/* PDO_FETCH_CLASS only flags */
#define PDO_FETCH_CLASSTYPE (1u << 7u) /* fetch class gets its class name from 1st column */
#define PDO_FETCH_PROPS_LATE (1u << 8u) /* fetch props after calling ctor */
#define PDO_FETCH_SERIALIZE (1u << 9u) /* DEPRECATED: fetch class instances by calling serialize */
#define PDO_FIRST_INVALID_FLAG (1u << 10u)


/* fetch orientation for scrollable cursors */
enum pdo_fetch_orientation {
Expand Down
9 changes: 4 additions & 5 deletions ext/pdo/tests/bug_38253.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var_dump($stmt->fetchAll());
$pdo->setAttribute (PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_INTO);
$stmt = $pdo->prepare ("SELECT * FROM test38253");
$stmt->execute();
var_dump($stmt->fetchAll());
var_dump($stmt->fetch());

?>
--CLEAN--
Expand All @@ -53,8 +53,7 @@ Warning: PDOStatement::fetchAll(): SQLSTATE[HY000]: General error in %s on line
array(0) {
}

Warning: PDOStatement::fetchAll(): SQLSTATE[HY000]: General error: No fetch-into object specified. in %s on line %d
Warning: PDOStatement::fetch(): SQLSTATE[HY000]: General error: No fetch-into object specified. in %s on line %d

Warning: PDOStatement::fetchAll(): SQLSTATE[HY000]: General error in %s on line %d
array(0) {
}
Warning: PDOStatement::fetch(): SQLSTATE[HY000]: General error in %s on line %d
bool(false)
88 changes: 88 additions & 0 deletions ext/pdo/tests/pdo_set_fetch_mode_errors.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
--TEST--
PDO Common: PDOStatement::setFetchMode() errors
--EXTENSIONS--
pdo
--SKIPIF--
<?php
$dir = getenv('REDIR_TEST_DIR');
if (false == $dir) die('skip no driver');
require_once $dir . 'pdo_test.inc';
PDOTest::skip();
?>
--FILE--
<?php
if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.__DIR__ . '/../../pdo/tests/');
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();

$db->exec('CREATE TABLE pdo_set_fetch_mode_error(id int NOT NULL PRIMARY KEY, val VARCHAR(10), val2 VARCHAR(10))');
$db->exec("INSERT INTO pdo_set_fetch_mode_error VALUES(1, 'A', 'AA')");
$db->exec("INSERT INTO pdo_set_fetch_mode_error VALUES(2, 'B', 'BB')");
$db->exec("INSERT INTO pdo_set_fetch_mode_error VALUES(3, 'C', 'CC')");

$stmt = $db->prepare('SELECT id, val, val2 from pdo_set_fetch_mode_error');

foreach (range(PDO::FETCH_KEY_PAIR, 16) as $mode) {
try {
echo "Mode: $mode\n";
var_dump($stmt->setFetchMode($mode));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
}
foreach (range(PDO::FETCH_KEY_PAIR, 16) as $mode) {
try {
echo "Mode: $mode\n";
var_dump($stmt->setFetchMode($mode|PDO::FETCH_PROPS_LATE));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
}
try {
var_dump($stmt->setFetchMode(PDO::FETCH_KEY_PAIR|PDO::FETCH_PROPS_LATE));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
try {
var_dump($stmt->setFetchMode(PDO::FETCH_KEY_PAIR|(PDO::FETCH_SERIALIZE*2)));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
try {
var_dump($stmt->setFetchMode(PDO::FETCH_FUNC));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}


?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_set_fetch_mode_error");
?>
--EXPECT--
Mode: 12
bool(true)
Mode: 13
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) must be a bitmask of PDO::FETCH_* constants
Mode: 14
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) must be a bitmask of PDO::FETCH_* constants
Mode: 15
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) must be a bitmask of PDO::FETCH_* constants
Mode: 16
bool(true)
Mode: 12
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) cannot use PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, or PDO::FETCH_SERIALIZE fetch flags with a fetch mode other than PDO::FETCH_CLASS
Mode: 13
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) cannot use PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, or PDO::FETCH_SERIALIZE fetch flags with a fetch mode other than PDO::FETCH_CLASS
Mode: 14
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) cannot use PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, or PDO::FETCH_SERIALIZE fetch flags with a fetch mode other than PDO::FETCH_CLASS
Mode: 15
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) cannot use PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, or PDO::FETCH_SERIALIZE fetch flags with a fetch mode other than PDO::FETCH_CLASS
Mode: 16
bool(true)
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) cannot use PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, or PDO::FETCH_SERIALIZE fetch flags with a fetch mode other than PDO::FETCH_CLASS
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) must be a bitmask of PDO::FETCH_* constants
ValueError: PDOStatement::setFetchMode(): Argument #1 ($mode) PDO::FETCH_FUNC can only be used with PDOStatement::fetchAll()