Skip to content

Commit

Permalink
Merge branch 'PHP-8.4'
Browse files Browse the repository at this point in the history
* PHP-8.4:
  ext/pdo: Fix a UAF when changing default fetch class ctor args
  • Loading branch information
Girgias committed Jan 26, 2025
2 parents ab99693 + 7f321a1 commit 60ee42e
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 1 deletion.
19 changes: 18 additions & 1 deletion ext/pdo/pdo_stmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,7 @@ PHP_METHOD(PDOStatement, fetchAll)
zend_class_entry *old_ce;
zval old_ctor_args, *ctor_args = NULL;
uint32_t old_arg_count;
HashTable *current_ctor = NULL;

ZEND_PARSE_PARAMETERS_START(0, 3)
Z_PARAM_OPTIONAL
Expand All @@ -1217,6 +1218,10 @@ PHP_METHOD(PDOStatement, fetchAll)

old_ce = stmt->fetch.cls.ce;
ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args);
if (Z_TYPE(old_ctor_args) == IS_ARRAY) {
/* Protect against destruction by marking this as immutable: we consider this non-owned temporarily */
Z_TYPE_INFO(stmt->fetch.cls.ctor_args) = IS_ARRAY;
}
old_arg_count = stmt->fetch.cls.fci.param_count;

do_fetch_opt_finish(stmt, 0);
Expand All @@ -1241,7 +1246,13 @@ PHP_METHOD(PDOStatement, fetchAll)
}

if (ctor_args && zend_hash_num_elements(Z_ARRVAL_P(ctor_args)) > 0) {
ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, ctor_args); /* we're not going to free these */
/* We increase the refcount and store it in case usercode has been messing around with the ctor args.
* We need to store current_ctor separately as usercode may change the ctor_args which will cause a leak. */
current_ctor = Z_ARRVAL_P(ctor_args);
ZVAL_COPY(&stmt->fetch.cls.ctor_args, ctor_args);
/* Protect against destruction by marking this as immutable: we consider this non-owned
* as destruction is handled via current_ctor. */
Z_TYPE_INFO(stmt->fetch.cls.ctor_args) = IS_ARRAY;
} else {
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
}
Expand Down Expand Up @@ -1343,9 +1354,15 @@ PHP_METHOD(PDOStatement, fetchAll)
}

do_fetch_opt_finish(stmt, 0);
if (current_ctor) {
zend_array_release(current_ctor);
}

/* Restore defaults which were changed by PDO_FETCH_CLASS mode */
stmt->fetch.cls.ce = old_ce;
/* ctor_args may have been changed to an owned object in the meantime, so destroy it.
* If it was not, then the type flags update will have protected us against destruction. */
zval_ptr_dtor(&stmt->fetch.cls.ctor_args);
ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, &old_ctor_args);
stmt->fetch.cls.fci.param_count = old_arg_count;

Expand Down
59 changes: 59 additions & 0 deletions ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--TEST--
PDO Common: PDO::FETCH_CLASS with a constructor that changes the ctor args within PDO::fetch()
--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();

class Test {
public string $val1;
public string $val2;

public function __construct(mixed $v) {
var_dump($v);
if ($v instanceof PDOStatement) {
$v->setFetchMode(PDO::FETCH_CLASS, 'Test', [$this->val2]);
}
}
}

$db->exec('CREATE TABLE pdo_fetch_class_change_ctor_one(id int NOT NULL PRIMARY KEY, val1 VARCHAR(10), val2 VARCHAR(10))');
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_one VALUES(1, 'A', 'alpha')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_one VALUES(2, 'B', 'beta')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_one VALUES(3, 'C', 'gamma')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_one VALUES(4, 'D', 'delta')");

$stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_one');
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Test', [$stmt]);

$stmt->execute();
var_dump($stmt->fetch());

?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_one");
?>
--EXPECTF--
object(PDOStatement)#%d (1) {
["queryString"]=>
string(54) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_one"
}
object(Test)#%d (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
59 changes: 59 additions & 0 deletions ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--TEST--
PDO Common: PDO::FETCH_CLASS with a constructor that changes the ctor args within PDO::fetchObject()
--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();

class Test {
public string $val1;
public string $val2;

public function __construct(mixed $v) {
var_dump($v);
if ($v instanceof PDOStatement) {
$v->setFetchMode(PDO::FETCH_CLASS, 'Test', [$this->val2]);
}
}
}

// TODO Rename pdo_fetch_class_change_ctor_two table to pdo_fetch_class_change_ctor_two in PHP-8.4
$db->exec('CREATE TABLE pdo_fetch_class_change_ctor_two(id int NOT NULL PRIMARY KEY, val1 VARCHAR(10), val2 VARCHAR(10))');
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_two VALUES(1, 'A', 'alpha')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_two VALUES(2, 'B', 'beta')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_two VALUES(3, 'C', 'gamma')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_two VALUES(4, 'D', 'delta')");

$stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_two');

$stmt->execute();
var_dump($stmt->fetchObject('Test', [$stmt]));

?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_two");
?>
--EXPECTF--
object(PDOStatement)#%s (1) {
["queryString"]=>
string(54) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_two"
}
object(Test)#%s (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
86 changes: 86 additions & 0 deletions ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch3.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
--TEST--
PDO Common: PDO::FETCH_CLASS with a constructor that changes the ctor args within PDO::fetchAll() (no args variation)
--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();

class Test {
public string $val1;
public string $val2;

public function __construct(mixed $v) {
var_dump($v);
if ($v instanceof PDOStatement) {
$v->setFetchMode(PDO::FETCH_CLASS, 'Test', [$this->val2]);
}
}
}

$db->exec('CREATE TABLE pdo_fetch_class_change_ctor_three(id int NOT NULL PRIMARY KEY, val1 VARCHAR(10), val2 VARCHAR(10))');
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_three VALUES(1, 'A', 'alpha')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_three VALUES(2, 'B', 'beta')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_three VALUES(3, 'C', 'gamma')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_three VALUES(4, 'D', 'delta')");

$stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_three');
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Test', [$stmt]);

$stmt->execute();
var_dump($stmt->fetchAll());

?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_three");
?>
--EXPECTF--
object(PDOStatement)#%d (1) {
["queryString"]=>
string(56) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_three"
}
string(5) "alpha"
string(5) "alpha"
string(5) "alpha"
array(4) {
[0]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
[1]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "B"
["val2"]=>
string(4) "beta"
}
[2]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "C"
["val2"]=>
string(5) "gamma"
}
[3]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "D"
["val2"]=>
string(5) "delta"
}
}
85 changes: 85 additions & 0 deletions ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch4.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
--TEST--
PDO Common: PDO::FETCH_CLASS with a constructor that changes the ctor args within PDO::fetchAll() (args in fetchAll)
--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();

class Test {
public string $val1;
public string $val2;

public function __construct(mixed $v) {
var_dump($v);
if ($v instanceof PDOStatement) {
$v->setFetchMode(PDO::FETCH_CLASS, 'Test', [$this->val2]);
}
}
}

$db->exec('CREATE TABLE pdo_fetch_class_change_ctor_four(id int NOT NULL PRIMARY KEY, val1 VARCHAR(10), val2 VARCHAR(10))');
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_four VALUES(1, 'A', 'alpha')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_four VALUES(2, 'B', 'beta')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_four VALUES(3, 'C', 'gamma')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_four VALUES(4, 'D', 'delta')");

$stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_four');

$stmt->execute();
var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'Test', [$stmt]));

?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_four");
?>
--EXPECTF--
object(PDOStatement)#%d (1) {
["queryString"]=>
string(55) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_four"
}
string(5) "alpha"
string(5) "alpha"
string(5) "alpha"
array(4) {
[0]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
[1]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "B"
["val2"]=>
string(4) "beta"
}
[2]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "C"
["val2"]=>
string(5) "gamma"
}
[3]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "D"
["val2"]=>
string(5) "delta"
}
}
55 changes: 55 additions & 0 deletions ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
PDO Common: PDO::FETCH_CLASS with a constructor that changes the ctor args within PDO::fetchAll() (via warning and error handler)
--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();

// Warning to hook into error handler
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);

class B {
public function __construct() {}
}

$db->exec('CREATE TABLE pdo_fetch_class_change_ctor_five(id int NOT NULL PRIMARY KEY, val1 VARCHAR(10), val2 VARCHAR(10))');
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_five VALUES(1, 'A', 'alpha')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_five VALUES(2, 'B', 'beta')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_five VALUES(3, 'C', 'gamma')");
$db->exec("INSERT INTO pdo_fetch_class_change_ctor_five VALUES(4, 'D', 'delta')");

$stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_five');
$stmt->execute();

function stuffingErrorHandler(int $errno, string $errstr, string $errfile, int $errline) {
global $stmt;
$stmt->setFetchMode(PDO::FETCH_CLASS, 'B', [$errstr]);
echo $errstr, PHP_EOL;
}
set_error_handler(stuffingErrorHandler(...));

var_dump($stmt->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_SERIALIZE, 'B', [$stmt]));

?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_five");
?>
--EXPECTF--
PDOStatement::fetchAll(): The PDO::FETCH_SERIALIZE mode is deprecated
PDOStatement::fetchAll(): SQLSTATE[HY000]: General error: cannot unserialize class
PDOStatement::fetchAll(): SQLSTATE[HY000]: General error%S
array(0) {
}
Loading

0 comments on commit 60ee42e

Please sign in to comment.