Skip to content

A cancelled coroutine leaves its buffer to a thread-pool worker #286

Description

@EdmondDantes

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions