uv_cancel cannot stop a worker that has started, so a coroutine cancelled mid-operation returns while the worker still names the caller's memory.
Read. The coroutine leaves the loop in php_stdiop_read, libuv_io_req_dispose only marks DISPOSE_PENDING, and the pin is dropped without stream->pending_free — no close happened. The next fclose frees stream->readbuf while uv_fs_read is still writing into it. ASAN, 100 rounds of a cancelled 4 MB read: WRITE of size 4194304 from uv__fs_read in a worker thread, freed by php_stream_free.
Write. Same shape: php_stdiop_write hands the caller's buffer to uv_fs_write, and a cancelled writer's buffer — a filter bucket, a zend_string — is freed under the worker. strace of 20 rounds of a cancelled 4 MB fwrite: 18 write(...) = -1 EFAULT. EFAULT is the lucky outcome; a recycled block puts another allocation's bytes in the file.
Fixed for the thread pool
The buffer the worker names belongs to the request, not to the caller: a read lands in it and the completion copies across unless the request was abandoned, a write copies its payload at submit. Both are freed by libuv_io_req_dispose, which already defers while the operation is in flight. Nothing in php-src changes. Evidence: tests/io/099-cancel_during_io.phpt, red under ASAN before, green after.
Open: a Windows stream read
libuv_io_alloc_cb hands req->base.buf — the caller's buffer — to libuv. On Windows a console line read runs on a thread of libuv's own that uv_read_stop does not join, and an overlapped read keeps the address after the cancel; a cancelled fread on a tty, and possibly on a pipe or a socket, leaves that buffer under a foreign thread.
The fix above does not transfer. A stream read never sets ASYNC_IO_REQ_F_UV_IN_FLIGHT, so libuv_io_req_dispose frees the request's buffer in the same call rather than deferring it: the window would move from fclose to the cancel itself and get wider. Closing this needs a rendezvous for the stream read — the request kept alive until libuv reports the cancelled read — and a Windows machine to prove it on, which the current setup does not have.
uv_cancelcannot stop a worker that has started, so a coroutine cancelled mid-operation returns while the worker still names the caller's memory.Read. The coroutine leaves the loop in
php_stdiop_read,libuv_io_req_disposeonly marksDISPOSE_PENDING, and the pin is dropped withoutstream->pending_free— no close happened. The nextfclosefreesstream->readbufwhileuv_fs_readis still writing into it. ASAN, 100 rounds of a cancelled 4 MB read:WRITE of size 4194304fromuv__fs_readin a worker thread, freed byphp_stream_free.Write. Same shape:
php_stdiop_writehands the caller's buffer touv_fs_write, and a cancelled writer's buffer — a filter bucket, azend_string— is freed under the worker.straceof 20 rounds of a cancelled 4 MBfwrite: 18write(...) = -1 EFAULT. EFAULT is the lucky outcome; a recycled block puts another allocation's bytes in the file.Fixed for the thread pool
The buffer the worker names belongs to the request, not to the caller: a read lands in it and the completion copies across unless the request was abandoned, a write copies its payload at submit. Both are freed by
libuv_io_req_dispose, which already defers while the operation is in flight. Nothing in php-src changes. Evidence:tests/io/099-cancel_during_io.phpt, red under ASAN before, green after.Open: a Windows stream read
libuv_io_alloc_cbhandsreq->base.buf— the caller's buffer — to libuv. On Windows a console line read runs on a thread of libuv's own thatuv_read_stopdoes not join, and an overlapped read keeps the address after the cancel; a cancelledfreadon a tty, and possibly on a pipe or a socket, leaves that buffer under a foreign thread.The fix above does not transfer. A stream read never sets
ASYNC_IO_REQ_F_UV_IN_FLIGHT, solibuv_io_req_disposefrees the request's buffer in the same call rather than deferring it: the window would move fromfcloseto the cancel itself and get wider. Closing this needs a rendezvous for the stream read — the request kept alive until libuv reports the cancelled read — and a Windows machine to prove it on, which the current setup does not have.