From ec0ab5d77d32d836a60b024fa43d54ed3ce3ce87 Mon Sep 17 00:00:00 2001 From: locus-x64 Date: Fri, 13 Mar 2026 15:59:12 +0500 Subject: [PATCH] win: fix off-by-one in utf-16 to wtf-8 conversion (#5050) Reserve one byte for the NUL terminator when passing the buffer size to uv_utf16_to_wtf8() in the TTY line-read path. Without this, when all input characters encode to exactly 3 UTF-8 bytes (e.g. CJK) and the buffer size is divisible by 3, the NUL terminator is written one byte past the allocated buffer. The other two call sites in src/win/util.c already subtract 1 before calling uv_utf16_to_wtf8(). This aligns tty.c with that convention. Fixes commit f3889085 ("win,tty: convert line-read UTF-16 to WTF-8") from October 2023. Refs: https://github.com/libuv/libuv/security/advisories/GHSA-4prr-4742-3ccf --- src/win/tty.c | 3 ++- test/test-idna.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ test/test-list.h | 2 ++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/win/tty.c b/src/win/tty.c index 66ca99cda..f131f4167 100644 --- a/src/win/tty.c +++ b/src/win/tty.c @@ -555,7 +555,8 @@ static DWORD CALLBACK uv_tty_line_read_thread(void* data) { NULL); if (read_console_success) { - read_bytes = bytes; + assert(bytes > 0); + read_bytes = bytes - 1; uv_utf16_to_wtf8(utf16, read_chars, &handle->tty.rd.read_line_buffer.base, diff --git a/test/test-idna.c b/test/test-idna.c index c154bb4de..48684e8ad 100644 --- a/test/test-idna.c +++ b/test/test-idna.c @@ -246,3 +246,47 @@ TEST_IMPL(wtf8) { uv_wtf8_to_utf16(input_max, buf, len); return 0; } + +TEST_IMPL(utf16_to_wtf8_exact_fill) { + /* Regression test for the off-by-one NUL write in uv_utf16_to_wtf8(). + * + * The API contract says target_len_ptr excludes space for the NUL terminator. + * The caller must pass (buffer_size - 1) so that the NUL written at + * target[target_len] stays in bounds. + * + * U+4E2D encodes to 3 UTF-8 bytes (0xE4 0xB8 0xAD). With a buffer of size N + * (divisible by 3) and N/3 input characters, the worst-case output exactly + * fills the data portion. Passing target_len = N - 1 must keep the NUL inside + * the buffer, and passing target_len = N would write one byte past the end. + */ + static const size_t sizes[] = { 3, 6, 48, 96, 192 }; + size_t i; + + for (i = 0; i < ARRAY_SIZE(sizes); i++) { + size_t buf_size = sizes[i]; + size_t num_chars = buf_size / 3; + char mem[200]; + uint16_t utf16[200]; + char* target; + size_t target_len; + size_t j; + + ASSERT_NOT_NULL(mem); + ASSERT_NOT_NULL(utf16); + + /* Fill entire region including canary with 0xAA. */ + memset(mem, 0xAA, buf_size + 1); + for (j = 0; j < num_chars; j++) + utf16[j] = 0x4E2D; /* U+4E2D (中) — 3-byte UTF-8 */ + + /* Correct usage: target_len = buf_size - 1 reserves space for NUL. */ + target = mem; + target_len = buf_size - 1; + uv_utf16_to_wtf8(utf16, num_chars, &target, &target_len); + + /* NUL must land inside the buffer; canary byte must be untouched. */ + ASSERT_EQ((unsigned char) mem[buf_size], 0xAA); + } + + return 0; +} diff --git a/test/test-list.h b/test/test-list.h index b79a4ea74..527ea013c 100644 --- a/test/test-list.h +++ b/test/test-list.h @@ -586,6 +586,7 @@ TEST_DECLARE (fork_threadpool_queue_work_simple) TEST_DECLARE (iouring_pollhup) TEST_DECLARE (wtf8) +TEST_DECLARE (utf16_to_wtf8_exact_fill) TEST_DECLARE (idna_toascii) TEST_DECLARE (utf8_decode1) TEST_DECLARE (utf8_decode1_overrun) @@ -1251,6 +1252,7 @@ TASK_LIST_START TEST_ENTRY (iouring_pollhup) TEST_ENTRY (wtf8) + TEST_ENTRY (utf16_to_wtf8_exact_fill) TEST_ENTRY (utf8_decode1) TEST_ENTRY (utf8_decode1_overrun) TEST_ENTRY (uname)