Skip to content

Commit d22b12e

Browse files
committed
+ additional tests
1 parent 413fd29 commit d22b12e

26 files changed

Lines changed: 1278 additions & 0 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
Curl handle created in global scope without coroutine — clean shutdown
3+
--EXTENSIONS--
4+
curl
5+
--FILE--
6+
<?php
7+
require_once __DIR__ . '/../common/http_server.php';
8+
9+
$server = async_test_server_start();
10+
11+
$ch = curl_init();
12+
curl_setopt($ch, CURLOPT_URL, "http://localhost:{$server->port}/");
13+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
14+
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
15+
16+
$response = curl_exec($ch);
17+
echo "Response: $response\n";
18+
19+
// Do NOT close $ch — let shutdown handle cleanup
20+
async_test_server_stop($server);
21+
echo "Done\n";
22+
?>
23+
--EXPECT--
24+
Response: Hello World
25+
Done
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Socket stream created in global scope without coroutine — clean shutdown
3+
--FILE--
4+
<?php
5+
require_once __DIR__ . '/../stream/stream_helper.php';
6+
7+
$sockets = create_socket_pair();
8+
list($sock1, $sock2) = $sockets;
9+
10+
stream_set_blocking($sock1, false);
11+
stream_set_blocking($sock2, false);
12+
13+
fwrite($sock1, "test data");
14+
$data = fread($sock2, 1024);
15+
echo "Read: $data\n";
16+
17+
// Do NOT close sockets — let shutdown handle cleanup
18+
echo "Done\n";
19+
?>
20+
--EXPECT--
21+
Read: test data
22+
Done
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
File stream created in global scope without coroutine — clean shutdown
3+
--FILE--
4+
<?php
5+
$tmpfile = tempnam(sys_get_temp_dir(), 'async_test_');
6+
7+
$fp = fopen($tmpfile, 'w');
8+
fwrite($fp, "hello world");
9+
// Do NOT close $fp — let shutdown handle cleanup
10+
11+
$fp2 = fopen($tmpfile, 'r');
12+
$data = fread($fp2, 1024);
13+
echo "Read: $data\n";
14+
// Do NOT close $fp2 — let shutdown handle cleanup
15+
16+
@unlink($tmpfile);
17+
echo "Done\n";
18+
?>
19+
--EXPECT--
20+
Read: hello world
21+
Done
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
Mixed resources (curl + socket + file) in global scope without coroutine — clean shutdown
3+
--EXTENSIONS--
4+
curl
5+
--FILE--
6+
<?php
7+
require_once __DIR__ . '/../common/http_server.php';
8+
require_once __DIR__ . '/../stream/stream_helper.php';
9+
10+
// 1. Curl
11+
$server = async_test_server_start();
12+
$ch = curl_init();
13+
curl_setopt($ch, CURLOPT_URL, "http://localhost:{$server->port}/");
14+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
15+
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
16+
$response = curl_exec($ch);
17+
echo "Curl: $response\n";
18+
19+
// 2. Socket pair
20+
$sockets = create_socket_pair();
21+
list($sock1, $sock2) = $sockets;
22+
fwrite($sock1, "socket data");
23+
$data = fread($sock2, 1024);
24+
echo "Socket: $data\n";
25+
26+
// 3. File
27+
$tmpfile = tempnam(sys_get_temp_dir(), 'async_test_');
28+
$fp = fopen($tmpfile, 'w+');
29+
fwrite($fp, "file data");
30+
rewind($fp);
31+
$fdata = fread($fp, 1024);
32+
echo "File: $fdata\n";
33+
34+
// Do NOT close anything — let shutdown handle all cleanup
35+
async_test_server_stop($server);
36+
@unlink($tmpfile);
37+
echo "Done\n";
38+
?>
39+
--EXPECT--
40+
Curl: Hello World
41+
Socket: socket data
42+
File: file data
43+
Done
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
--TEST--
2+
curl_multi cleanup via GC should not cause use-after-free
3+
--DESCRIPTION--
4+
When a CurlMultiHandle is destroyed by GC (without explicit curl_multi_close),
5+
curl_multi_cleanup() internally calls the socket callback (multi_socket_cb)
6+
for connection teardown. If the async event was already freed by curl_async_dtor,
7+
this causes a heap-use-after-free on the poll_list hash table.
8+
--EXTENSIONS--
9+
curl
10+
--FILE--
11+
<?php
12+
require_once __DIR__ . '/../common/http_server.php';
13+
14+
use function Async\spawn;
15+
use function Async\await;
16+
17+
$server = async_test_server_start();
18+
19+
function test_multi_gc_cleanup($port) {
20+
$mh = curl_multi_init();
21+
22+
$ch = curl_init();
23+
curl_setopt($ch, CURLOPT_URL, "http://localhost:{$port}/");
24+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
25+
curl_multi_add_handle($mh, $ch);
26+
27+
$active = null;
28+
do {
29+
$status = curl_multi_exec($mh, $active);
30+
if ($active > 0) {
31+
curl_multi_select($mh, 1.0);
32+
}
33+
} while ($active > 0);
34+
35+
$result = curl_multi_getcontent($ch);
36+
echo "Result: $result\n";
37+
38+
// Do NOT call curl_multi_remove_handle or curl_multi_close.
39+
// Drop all references so GC destroys the multi handle
40+
// while libcurl connections are still in the pool.
41+
unset($ch, $mh);
42+
43+
// Force GC to collect the multi handle
44+
gc_collect_cycles();
45+
46+
echo "GC completed without crash\n";
47+
}
48+
49+
$coroutine = spawn(fn() => test_multi_gc_cleanup($server->port));
50+
await($coroutine);
51+
52+
echo "Done\n";
53+
54+
async_test_server_stop($server);
55+
?>
56+
--EXPECT--
57+
Result: Hello World
58+
GC completed without crash
59+
Done
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
curl_multi_exec loop without curl_multi_select and without coroutines
3+
--EXTENSIONS--
4+
curl
5+
--FILE--
6+
<?php
7+
require_once __DIR__ . '/../common/http_server.php';
8+
9+
$server = async_test_server_start();
10+
11+
$mh = curl_multi_init();
12+
13+
$ch1 = curl_init();
14+
curl_setopt($ch1, CURLOPT_URL, "http://localhost:{$server->port}/");
15+
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
16+
curl_multi_add_handle($mh, $ch1);
17+
18+
$ch2 = curl_init();
19+
curl_setopt($ch2, CURLOPT_URL, "http://localhost:{$server->port}/");
20+
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
21+
curl_multi_add_handle($mh, $ch2);
22+
23+
// Loop with curl_multi_exec only — no curl_multi_select, no coroutines
24+
$active = null;
25+
$iterations = 0;
26+
do {
27+
$status = curl_multi_exec($mh, $active);
28+
$iterations++;
29+
30+
if ($status !== CURLM_OK) {
31+
echo "Error: " . curl_multi_strerror($status) . "\n";
32+
break;
33+
}
34+
} while ($active > 0);
35+
36+
$r1 = curl_multi_getcontent($ch1);
37+
$r2 = curl_multi_getcontent($ch2);
38+
39+
curl_multi_remove_handle($mh, $ch1);
40+
curl_multi_remove_handle($mh, $ch2);
41+
curl_multi_close($mh);
42+
43+
echo "Response 1: $r1\n";
44+
echo "Response 2: $r2\n";
45+
echo "Iterations: " . ($iterations > 0 ? 'ok' : 'none') . "\n";
46+
47+
async_test_server_stop($server);
48+
echo "Done\n";
49+
?>
50+
--EXPECT--
51+
Response 1: Hello World
52+
Response 2: Hello World
53+
Iterations: ok
54+
Done
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
ftell() returns 0 after fopen('a') and writes always go to EOF
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\await;
8+
9+
echo "Start\n";
10+
11+
$tmpfile = tempnam(sys_get_temp_dir(), 'async_io_test_');
12+
file_put_contents($tmpfile, str_repeat('X', 100));
13+
14+
$coroutine = spawn(function() use ($tmpfile) {
15+
$fp = fopen($tmpfile, 'a');
16+
17+
// ftell is 0 on open in append mode; ftell results after writes
18+
// are undefined per documentation, but writes must target EOF.
19+
$pos_open = ftell($fp);
20+
echo "ftell on open: $pos_open\n";
21+
22+
fwrite($fp, "AAA");
23+
$pos_after_write = ftell($fp);
24+
echo "ftell after write(3): $pos_after_write\n";
25+
26+
fseek($fp, 0, SEEK_SET);
27+
echo "ftell after fseek(0): " . ftell($fp) . "\n";
28+
29+
fwrite($fp, "BBB");
30+
$pos_after_second = ftell($fp);
31+
echo "ftell after second write(3): $pos_after_second\n";
32+
33+
fclose($fp);
34+
35+
$content = file_get_contents($tmpfile);
36+
echo "File length: " . strlen($content) . "\n";
37+
// Both writes must have gone to EOF, not to position 0.
38+
echo "Ends with AAABBB: " . (str_ends_with($content, 'AAABBB') ? 'yes' : 'no') . "\n";
39+
});
40+
41+
await($coroutine);
42+
43+
unlink($tmpfile);
44+
echo "End\n";
45+
46+
?>
47+
--EXPECT--
48+
Start
49+
ftell on open: 0
50+
ftell after write(3): 3
51+
ftell after fseek(0): 0
52+
ftell after second write(3): 3
53+
File length: 106
54+
Ends with AAABBB: yes
55+
End
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
fopen('a+') allows reads and writes; ftell tracks position; writes always go to EOF
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\await;
8+
9+
echo "Start\n";
10+
11+
$tmpfile = tempnam(sys_get_temp_dir(), 'async_io_test_');
12+
file_put_contents($tmpfile, 'HELLO');
13+
14+
$coroutine = spawn(function() use ($tmpfile) {
15+
$fp = fopen($tmpfile, 'a+');
16+
17+
// On open in a+ mode ftell must be 0 (POSIX: position at start for reads).
18+
echo "ftell on open: " . ftell($fp) . "\n";
19+
20+
// Read from position 0 — should get the existing content.
21+
$data = fread($fp, 5);
22+
echo "read: '$data'\n";
23+
echo "ftell after read(5): " . ftell($fp) . "\n";
24+
25+
// Write always appends to EOF regardless of read position.
26+
fwrite($fp, '_WORLD');
27+
echo "ftell after write: " . ftell($fp) . "\n";
28+
29+
// Seek to 0 and read the whole file.
30+
fseek($fp, 0, SEEK_SET);
31+
$all = fread($fp, 100);
32+
echo "full content: '$all'\n";
33+
34+
// Another write after seek — must still go to EOF.
35+
fwrite($fp, '!');
36+
echo "ftell after second write: " . ftell($fp) . "\n";
37+
38+
fclose($fp);
39+
40+
echo "file: '" . file_get_contents($tmpfile) . "'\n";
41+
});
42+
43+
await($coroutine);
44+
45+
unlink($tmpfile);
46+
echo "End\n";
47+
48+
?>
49+
--EXPECT--
50+
Start
51+
ftell on open: 0
52+
read: 'HELLO'
53+
ftell after read(5): 5
54+
ftell after write: 11
55+
full content: 'HELLO_WORLD'
56+
ftell after second write: 12
57+
file: 'HELLO_WORLD!'
58+
End

0 commit comments

Comments
 (0)