Skip to content

fix(thread-channel): a refused value never reaches the buffer - #227

Merged
EdmondDantes merged 1 commit into
mainfrom
fix/thread-channel-refused-value
Aug 13, 2026
Merged

fix(thread-channel): a refused value never reaches the buffer#227
EdmondDantes merged 1 commit into
mainfrom
fix/thread-channel-refused-value

Conversation

@EdmondDantes

@EdmondDantes EdmondDantes commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

ThreadChannel::send() transfers its argument into persistent memory before it takes the lock. The transfer refuses what it cannot copy — a resource, an object with dynamic properties — by releasing the partial graph, leaving the destination IS_UNDEF and throwing. The send did not check for that: it pushed the undefined slot into the buffer and returned true, so the caller got the right exception while the buffer held a value no receiver can interpret.

ThreadPool reads a task as an array, so the next worker to pop that slot died on ZEND_ASSERT(Z_TYPE(task) == IS_ARRAY) at thread_pool.c:347. In a release build the assertion is compiled out and the array fields are read from a value that is not an array.

The send now leaves the buffer untouched and returns false. Every caller already handles that: ThreadChannel::send() rethrows the pending exception, and ThreadPool::submit() and map() release the snapshot, the shared state and the remote future before reporting — thread_pool_throw_closed() keeps a pending exception rather than replacing it, so the transfer error is what the caller sees.

Reproduction

$pool = new Async\\ThreadPool(workers: 1, coroutine: true);
try {
    $pool->submit(static fn () => 1, fopen('php://memory', 'r'));
} catch (Error $e) {
    echo $e->getMessage(), PHP_EOL;   // Cannot transfer a resource between threads
}
$pool->close();

Before: the message is printed and the process aborts (exit 134). After: the message is printed and the process exits 0.

Tests

tests/thread_channel/045-send_refused_value_not_buffered.phpt covers the refusal, the untouched buffer and a following send that succeeds. It fails on main and passes with the fix.

tests/thread_channel, tests/thread_pool and tests/channel — 199 tests, 0 failed. The full suite is unchanged at 3 pre-existing failures in tests/curl and tests/io, which fail identically without this change.

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes
EdmondDantes force-pushed the fix/thread-channel-refused-value branch from 044cc23 to 58aff00 Compare August 13, 2026 16:44
@EdmondDantes
EdmondDantes force-pushed the fix/thread-channel-refused-value branch from 58aff00 to 6cd7ab3 Compare August 13, 2026 18:02
@EdmondDantes
EdmondDantes merged commit b78c5e4 into main Aug 13, 2026
8 checks passed
@EdmondDantes
EdmondDantes deleted the fix/thread-channel-refused-value branch August 13, 2026 18:04
@EdmondDantes
EdmondDantes restored the fix/thread-channel-refused-value branch August 13, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant