Skip to content

Commit b55fe6c

Browse files
committed
Fix heap-use-after-free in stream_socket_accept during coroutine cancellation
Add CHANGELOG entry for 0.6.6 and regression test. The actual fix is in php-src: network_async_accept_incoming() and php_network_accept_incoming_ex() borrowed the exception message string without addref, causing UAF when the caller released it.
1 parent 5ab0f16 commit b55fe6c

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111
- **`Scope::awaitCompletion()` not marking cancellation Future as used**: The cancellation token passed to `awaitCompletion()` was never marked with `RESULT_USED` / `EXC_CAUGHT`, causing a spurious "Future was never used" warning when the Future was destroyed. Additionally, early return paths (scope already finished, closed, or cancelled) skipped the marking entirely. Fixed by setting flags immediately after parameter parsing, before any early returns.
1212
- **`Scope::awaitAfterCancellation()` not marking cancellation Future as used**: Same issue as `awaitCompletion()` — the optional cancellation Future was only marked when the method reached `resume_when`, but early returns bypassed it. Fixed identically.
13+
- **Heap-use-after-free in `stream_socket_accept()` during coroutine cancellation**: When a coroutine blocked in `stream_socket_accept()` was cancelled during graceful shutdown, `network_async_accept_incoming()` extracted the exception's message string into `*error_string` without incrementing its refcount (`*error_string = Z_STR_P(message)`). The caller then called `zend_string_release_ex()`, freeing the string while the exception object still referenced it. On exception destruction, `zend_object_std_dtor` accessed the freed string — heap-use-after-free. Fixed by using `zend_string_copy()` to properly addref the borrowed string. Same bug existed in the synchronous path `php_network_accept_incoming_ex()` in `main/network.c` — fixed there too.
1314

1415
## [0.6.5] - 2026-03-29
1516

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
Stream: stream_socket_accept() cancellation does not cause use-after-free
3+
--DESCRIPTION--
4+
When a coroutine blocked in stream_socket_accept() is cancelled during graceful
5+
shutdown, the error_string from network_async_accept_incoming must properly
6+
addref the exception message string. Otherwise, the caller releases it
7+
(zend_string_release_ex), freeing the string while the exception object still
8+
references it — causing heap-use-after-free on exception destruction.
9+
--FILE--
10+
<?php
11+
12+
use Async\Scope;
13+
14+
$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
15+
if (!$server) {
16+
die("Failed to create server: $errstr ($errno)\n");
17+
}
18+
19+
$address = stream_socket_get_name($server, false);
20+
echo "Server listening on $address\n";
21+
22+
stream_set_blocking($server, false);
23+
24+
$scope = Scope::inherit()->asNotSafely();
25+
26+
// Coroutine that blocks on stream_socket_accept — will be cancelled during
27+
// graceful shutdown when the sibling coroutine throws.
28+
$scope->spawn(function () use ($server) {
29+
echo "Coroutine 1: waiting for connection\n";
30+
$client = @stream_socket_accept($server, 30);
31+
echo "Coroutine 1: done\n";
32+
});
33+
34+
// Coroutine that throws to trigger graceful shutdown of the scope
35+
$scope->spawn(function () {
36+
echo "Coroutine 2: throwing\n";
37+
throw new \RuntimeException("Trigger graceful shutdown");
38+
});
39+
40+
try {
41+
$scope->awaitCompletion(\Async\timeout(5000));
42+
} catch (\Throwable $e) {
43+
echo "Caught: " . $e::class . ": " . $e->getMessage() . "\n";
44+
}
45+
46+
echo "Cleaning up\n";
47+
fclose($server);
48+
echo "Done\n";
49+
50+
?>
51+
--EXPECTF--
52+
Server listening on %s
53+
Coroutine 1: waiting for connection
54+
Coroutine 2: throwing
55+
Caught: RuntimeException: Trigger graceful shutdown
56+
Cleaning up
57+
Done

0 commit comments

Comments
 (0)