Give a Windows console read a buffer the handle owns - #301
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
uv_read_stop() is not an ownership barrier for a tty: uv__tty_queue_read_line() hands the allocator's address to a ReadConsoleW worker started with QueueUserWorkItem, and the cancellation is an injected VK_RETURN, so the worker returns successfully and writes into memory the cancelled coroutine has already freed. A pipe and a TCP socket allocate in the loop thread once the bytes are there and are not exposed. A tty handle now carries one 8 KB buffer, allocated at the first read and never resized, and the completion copies out of it into the request. libuv releases the handle only through its close callback, which runs once the pending requests have drained.
EdmondDantes
force-pushed
the
286-windows-stream-read-cancel
branch
from
September 12, 2026 10:59
71f021a to
75ec748
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the second half of #286: a Windows console read handed libuv the caller's buffer.
uv_read_stop()is not an ownership barrier for a tty in line mode.uv__tty_queue_read_line()calls the allocator on the loop thread and hands the address touv_tty_line_read_thread, started withQueueUserWorkItem, which nothing joins. Cancellation is not a stop but an injected keystroke:uv__cancel_read_console()writes aVK_RETURNrecord into the console input, soReadConsoleWreturns successfully and the worker writes the characters into that buffer — which by then is thereadbufa cancelled coroutine has already freed throughphp_stream_free(). libuv drops the result itself, becauseUV_HANDLE_CANCELLATION_PENDINGsuppressesread_cb; that is also why the rendezvous the issue proposed cannot work — there is no completion to wait for.The change
A tty handle carries one 8 KB buffer (
async_io_t.tty_read_buf), allocated at the first read through the reactor's own allocator and never resized; the completion copies out of it into the request, bounded by what that request asked for. libuv releases the handle only through its close callback, which runs oncereqs_pendinghas drained — after the worker has written. A handle disposed with the reactor already down keeps its buffer, leaked on purpose, the tradelibuv_io_closealready makes for the descriptor (#282).The size is a bound, not a guess:
uv_tty_line_read_threadclamps its read toMAX_INPUT_BUFFER_LENGTH= 8192 whatever the allocator offers, so a console can never answer with more.Pipes, TCP sockets and a tty in raw mode are untouched, and pay no copy: all three call the allocator on the loop thread once the bytes have arrived (
uv__pipe_read_data,uv__process_tcp_read_req,uv__tty_queue_read_raw), so the address never outlives the callback. The multishot path, where the owner suppliesbase.alloc_cb, is untouched as well.Evidence
Reproduced, not inferred — but not by a test, because a CI runner has no console. The procedure is in
docs/286-windows-console-read-cancel.md: build libuv with/fsanitize=address, add aSleep(200)inuv_tty_line_read_threadbetweenReadConsoleWanduv_utf16_to_wtf8to widen the race, then cancel a parkedfread(STDIN, 4096)andfclosethe stream in a real console window.Without the change:
With it, the same run is clean, and so are eight rounds without the injected delay.
Worth knowing beyond this issue: the prebuilt
libuv.libthe Windows build links is not compiled with the sanitizer, so a write from a libuv worker thread into freed memory has never been visible to ASAN. Every earlier "clean under Windows ASAN" for this defect — including 3500 rounds — was measuring nothing.ext/async/tests/ioon an ASAN ZTS build against the stock libuv 1.49.3: 98 passed, 4 skipped, 0 failed.