Skip to content

Commit 4b9e4d9

Browse files
committed
Make PDO connection construction single-shot
Constructing a PDO handle over an already constructed one re-ran connection setup; for a persistent connection the second pass freed the pemalloc'd dbh with efree() and corrupted the heap. Besides a plain second __construct(), the same sink was reachable by reentering during a uri: DSN stream open, by retrying a persistent construct that failed after swapping in the persistent handle, and by a subclass destructor reentering while a failed persistent connect() unwinds. Track construction with an is_constructing flag, set as soon as the handle exists for both __construct() and connect(), carried onto the persistent handle and cleared only on success; construction is rejected once the driver is attached or a construction is in progress. A construct that fails now leaves the handle unusable instead of allowing a retry.
1 parent 742ff0e commit 4b9e4d9

3 files changed

Lines changed: 114 additions & 1 deletion

File tree

ext/pdo/pdo_dbh.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,15 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen
321321
Z_PARAM_ARRAY_OR_NULL(options)
322322
ZEND_PARSE_PARAMETERS_END();
323323

324+
if (current_object != NULL) {
325+
pdo_dbh_t *existing_dbh = php_pdo_dbh_fetch_inner(current_object);
326+
if (existing_dbh->driver != NULL || existing_dbh->is_constructing) {
327+
zend_throw_error(NULL, "%s object is already constructed", ZSTR_VAL(called_scope->name));
328+
RETURN_THROWS();
329+
}
330+
existing_dbh->is_constructing = 1;
331+
}
332+
324333
/* parse the data source name */
325334
colon = strchr(data_source, ':');
326335

@@ -374,6 +383,7 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen
374383

375384
if (new_zval_object) {
376385
dbh = Z_PDO_DBH_P(new_zval_object);
386+
dbh->is_constructing = 1;
377387
} else {
378388
dbh = php_pdo_dbh_fetch_inner(current_object);
379389
}
@@ -427,6 +437,7 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen
427437

428438
pdbh->refcount = 1;
429439
pdbh->is_persistent = 1;
440+
pdbh->is_constructing = dbh->is_constructing;
430441
pdbh->persistent_id = pemalloc(plen + 1, 1);
431442
memcpy((char *)pdbh->persistent_id, hashkey, plen+1);
432443
pdbh->persistent_id_len = plen;
@@ -493,6 +504,7 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen
493504
}
494505

495506
dbh->driver = driver;
507+
dbh->is_constructing = 0;
496508
options:
497509
if (options) {
498510
zval *attr_value;

ext/pdo/php_pdo_driver.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,12 @@ struct _pdo_dbh_t {
465465
/* bitmap for pdo_param_event(s) to skip in dispatch_param_event */
466466
unsigned skip_param_evt:7;
467467

468+
/* set while the constructor runs; rejects reentrant (re)construction */
469+
unsigned is_constructing:1;
470+
468471
/* the sum of the number of bits here and the bit fields preceding should
469472
* equal 32 */
470-
unsigned _reserved_flags:14;
473+
unsigned _reserved_flags:13;
471474

472475
/* data source string used to open this handle */
473476
const char *data_source;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
--TEST--
2+
PDO::__construct() rejects reconstruction, reentrant construction and unsafe retry
3+
--EXTENSIONS--
4+
pdo_sqlite
5+
--FILE--
6+
<?php
7+
$persistent = [PDO::ATTR_PERSISTENT => true];
8+
9+
// Sequential reconstruction of a constructed handle (plain and persistent).
10+
$p = new PDO('sqlite::memory:');
11+
try {
12+
$p->__construct('sqlite::memory:');
13+
} catch (\Error $e) {
14+
echo $e::class, ': ', $e->getMessage(), "\n";
15+
}
16+
17+
$q = new PDO('sqlite::memory:', null, null, $persistent);
18+
try {
19+
$q->__construct('sqlite::memory:', null, null, $persistent);
20+
} catch (\Error $e) {
21+
echo $e::class, ': ', $e->getMessage(), "\n";
22+
}
23+
24+
// Reentrant construction: a uri: DSN opens a stream whose userland wrapper
25+
// calls __construct() again on the same, still-constructing object.
26+
class ReentWrapper
27+
{
28+
public $context;
29+
private int $pos = 0;
30+
private string $data = "sqlite::memory:";
31+
public function stream_open($path, $mode, $options, &$opened): bool
32+
{
33+
global $obj;
34+
try {
35+
$obj->__construct('sqlite::memory:', null, null, [PDO::ATTR_PERSISTENT => true]);
36+
} catch (\Error $e) {
37+
echo $e::class, ': ', $e->getMessage(), "\n";
38+
}
39+
return true;
40+
}
41+
public function stream_read($count): string
42+
{
43+
$chunk = substr($this->data, $this->pos, $count);
44+
$this->pos += strlen($chunk);
45+
return $chunk;
46+
}
47+
public function stream_eof(): bool
48+
{
49+
return $this->pos >= strlen($this->data);
50+
}
51+
public function stream_stat()
52+
{
53+
return [];
54+
}
55+
}
56+
stream_wrapper_register('reent', ReentWrapper::class);
57+
$obj = (new ReflectionClass(PDO::class))->newInstanceWithoutConstructor();
58+
$obj->__construct('uri:reent://x', null, null, $persistent);
59+
60+
// Retry after a failed persistent construct (which already swapped in the
61+
// persistent handle before the driver factory failed).
62+
$r = (new ReflectionClass(PDO::class))->newInstanceWithoutConstructor();
63+
try {
64+
$r->__construct('sqlite:/pdo-reconstruct-nonexistent/x.db', null, null, $persistent);
65+
} catch (\Throwable $e) {
66+
}
67+
try {
68+
$r->__construct('sqlite::memory:', null, null, $persistent);
69+
} catch (\Error $e) {
70+
echo $e::class, ': ', $e->getMessage(), "\n";
71+
}
72+
73+
// Destructor reentry while a failed persistent connect() is unwinding.
74+
class ReconstructOnDestruct extends Pdo\Sqlite
75+
{
76+
public function __destruct()
77+
{
78+
try {
79+
$this->__construct('sqlite::memory:', null, null, [PDO::ATTR_PERSISTENT => true]);
80+
} catch (\Error $e) {
81+
echo $e::class, ': ', $e->getMessage(), "\n";
82+
}
83+
}
84+
}
85+
try {
86+
ReconstructOnDestruct::connect('sqlite:/pdo-reconstruct-nonexistent/x.db', null, null, $persistent);
87+
} catch (\Throwable $e) {
88+
}
89+
90+
echo "done\n";
91+
?>
92+
--EXPECT--
93+
Error: PDO object is already constructed
94+
Error: PDO object is already constructed
95+
Error: PDO object is already constructed
96+
Error: PDO object is already constructed
97+
Error: ReconstructOnDestruct object is already constructed
98+
done

0 commit comments

Comments
 (0)