From 9bf4b61e1fa770a7f0e2929a788c3b34f01fafeb Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:04:33 +0000 Subject: [PATCH] #200: changelog for the pooled PDO connection double-release and UAF fixes Covers the key-drift double release across the lazy scheduler launch and the use-after-free when GC destroys a pooled PDO before one of its statements. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec59090..f06653f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`Context::get()` and `getLocal()` now throw `Async\ContextException` when the key is missing, instead of returning `null`.** They were exact duplicates of `find()`/`findLocal()`, which made them pointless. `get()` is the mandatory-value form; `find()` remains the one that answers `null`. ### Fixed +- **Segfault: a `PDOStatement` outliving a pooled `PDO` after the first async stream operation (#200).** The scheduler promotes the running main flow into the main coroutine on the fly, so a connection acquired before that (`ATTR_POOL_ENABLED`) was keyed to a context that no longer matched afterwards. The statement's destructor then saw it as orphaned and returned it to the pool, while the coroutine binding still owned it and returned it a second time — the pool destroyed the same connection twice. The main flow now keeps one binding across the launch, and every release path detaches the owning binding first. + +- **Use-after-free when the cycle collector freed a pooled `PDO` before one of its statements (#200).** Releasing a borrowed connection read the pool through `stmt->dbh`, which GC may already have freed. A statement now reaches the pool through the connection itself, which holds its own reference, so the release is safe in any destruction order. + - **`delay()`/`timeout()` fired almost instantly after synchronous CPU work in the same coroutine (#185).** libuv computes a timer's deadline from `loop->time`, its clock cached once per loop iteration. A coroutine that burned CPU without yielding for longer than the timeout it then armed left that clock stale, so the deadline was already in the past and the timer fired at once — silently skipping the wait. Relative userland timeouts now refresh the reactor clock at arm time; hot per-I/O deadline timers are unaffected. - **`foreach` over a `Channel` broke on the ordinary producer/consumer pattern.** Closing a channel while a consumer was parked in the loop left the wake-up `ChannelException` pending, so it escaped `foreach` uncaught — and surfaced against the producer's `close()`, which had done nothing wrong. An explicit `close()` now ends the iteration cleanly, while a deadlock or a disposal still propagates.