Summary
A coroutine that ends without committing pins its pooled connection (in_txn). When the PDO object is then destroyed, the pool teardown and the coroutine's finalize callback both try to hand that connection back, and the process segfaults.
Seven lines, no framework, 100% reproducible here.
Reproducer
<?php
$dsn = "mysql:host=localhost;unix_socket=/var/run/mysqld/mysqld.sock;dbname=test;charset=utf8";
$pdo = new PDO($dsn, "test", "test", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MIN => 0,
PDO::ATTR_POOL_MAX => 1,
]);
$a = Async\spawn(function () use ($pdo) { $pdo->beginTransaction(); }); // no commit
Async\await($a);
unset($pdo);
$ php m7.php
Segmentation fault (core dumped) # exit 139
Conditions
Both must hold:
binding->conn != NULL at coroutine finalize — i.e. the slot is pinned by an open transaction or a live statement, so it is not returned during the coroutine's normal run.
- The finalizing coroutine holds the last reference to the PDO object, so
pdo_pool_destroy runs from inside that teardown.
Reading of it
pdo_pool_binding_on_coroutine_finish releases the still-held connection, and zend_async_pool_release pushes it into the pool's idle buffer — but the pool is being destroyed on the same stack, so the buffer is already gone.
In debug builds this lands on ZEND_ASSERT(buffer->data != NULL) in ext/async/pool.c. Release builds compile that assert out, so the same path writes through a freed/NULL buffer — heap corruption rather than a clean abort.
pdo_pool_binding_on_coroutine_finish also dereferences binding->dbh->pool_bindings after the release call, and binding->dbh can have been invalidated in the meantime.
Suggested fix
Re-check pool/binding->dbh liveness after the release, or hold a reference on the pool event across the coroutine-finalize release so the teardown cannot complete underneath it.
Environment
PHP 8.6.0-dev (cli) (ZTS DEBUG TrueAsync ABI v0.24.0)
true_async 0.7.10
OS Linux 6.6.114.1-microsoft-standard-WSL2
Found while auditing the pool for #200. The release-at-finalize structure predates that fix.
Summary
A coroutine that ends without committing pins its pooled connection (
in_txn). When the PDO object is then destroyed, the pool teardown and the coroutine's finalize callback both try to hand that connection back, and the process segfaults.Seven lines, no framework, 100% reproducible here.
Reproducer
Conditions
Both must hold:
binding->conn != NULLat coroutine finalize — i.e. the slot is pinned by an open transaction or a live statement, so it is not returned during the coroutine's normal run.pdo_pool_destroyruns from inside that teardown.Reading of it
pdo_pool_binding_on_coroutine_finishreleases the still-held connection, andzend_async_pool_releasepushes it into the pool's idle buffer — but the pool is being destroyed on the same stack, so the buffer is already gone.In debug builds this lands on
ZEND_ASSERT(buffer->data != NULL)inext/async/pool.c. Release builds compile that assert out, so the same path writes through a freed/NULL buffer — heap corruption rather than a clean abort.pdo_pool_binding_on_coroutine_finishalso dereferencesbinding->dbh->pool_bindingsafter the release call, andbinding->dbhcan have been invalidated in the meantime.Suggested fix
Re-check pool/
binding->dbhliveness after the release, or hold a reference on the pool event across the coroutine-finalize release so the teardown cannot complete underneath it.Environment
Found while auditing the pool for #200. The release-at-finalize structure predates that fix.