It got patched and merged via this PR https://github.com/libuv/libuv/commit/ec0ab5d77d32d836a60b024fa43d54ed3ce3ce87
Best, Ali Raza (@locus-x64) On Thu, 19 Mar 2026 at 21:48, Ali Raza <[email protected]> wrote: > ### PoC > > Tested on Windows 10 22H2 x64, Visual Studio 2022 Build Tools, libuv v1.x > HEAD. > > This is a detection-only PoC — it places a canary byte after the buffer > and checks if the NUL overwrites it. No exploitation attempted. > > **1. Build libuv:** > > ```cmd > git clone https://github.com/libuv/libuv.git > cd libuv && git checkout v1.x > mkdir build && cd build > cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release > -DBUILD_TESTING=OFF -DLIBUV_BUILD_SHARED=OFF > nmake > cd .. > ``` > > **2. Save as `poc\poc.c`:** > > ```c > #include <stdio.h> > #include <stdlib.h> > #include <string.h> > #include <stdint.h> > #include "uv.h" > > #define CJK_CHAR 0x4E2D /* U+4E2D (中) — 3 UTF-8 bytes */ > > static int test_overflow(size_t buf_size) { > size_t num_chars = buf_size / 3; > char *mem = (char *)malloc(buf_size + 16); > uint16_t *utf16 = (uint16_t *)malloc(num_chars * sizeof(uint16_t)); > char *target; > size_t target_len; > unsigned char canary; > int rc; > > if (!mem || !utf16) { free(mem); free(utf16); return -1; } > > memset(mem, 0xAA, buf_size + 16); > for (size_t i = 0; i < num_chars; i++) > utf16[i] = CJK_CHAR; > > target = mem; > target_len = buf_size; /* reproduces the tty.c:558 pattern — no -1 */ > rc = uv_utf16_to_wtf8(utf16, (ssize_t)num_chars, &target, &target_len); > canary = (unsigned char)mem[buf_size]; > > printf(" buf=%-5zu chars=%-4zu rc=%-3d byte_after=0x%02X %s\n", > buf_size, num_chars, rc, canary, > canary == 0x00 ? "OVERFLOW" : "ok"); > > free(utf16); > free(mem); > return canary == 0x00 ? 1 : 0; > } > > int main(void) { > size_t sizes[] = {48, 96, 192, 384, 768, 1536, 3072, 6144}; > int n = sizeof(sizes) / sizeof(sizes[0]); > int hits = 0; > > printf("=== uv_utf16_to_wtf8() off-by-one PoC ===\n\n"); > printf("Test: buffer sizes divisible by 3 (should overflow):\n"); > for (int i = 0; i < n; i++) > hits += test_overflow(sizes[i]); > > printf("\nControl: buffer size NOT divisible by 3:\n"); > test_overflow(100); > > printf("\n%s: %d/%d overflows detected\n", > hits > 0 ? "VULNERABLE" : "NOT VULNERABLE", hits, n); > return hits > 0 ? 1 : 0; > } > ``` > > **3. Compile and run (from x64 Native Tools Command Prompt for VS 2022):** > > ```cmd > mkdir poc && cd poc > cl /nologo /W3 /MD poc.c /I ..\include /link /LIBPATH:..\build libuv.lib > advapi32.lib iphlpapi.lib psapi.lib shell32.lib user32.lib userenv.lib > ws2_32.lib dbghelp.lib ole32.lib uuid.lib > poc.exe > ``` > > **Output:** > SS Attached > > ### Impact > > Out-of-bounds heap write (1 NUL byte) triggered by console input on > Windows. The practical impact depends on heap layout and allocator behavior > in the consuming application. Any Windows application using libuv's TTY > line reading with a read buffer size divisible by 3 is affected. Versions > v1.47.0 through current v1.x HEAD. > > Best, > > Ali Raza (@locus-x64) > > On Thu, 19 Mar 2026 at 21:45, Ali Raza <[email protected]> wrote: > >> Last few days ago I found an off-by-one heap buffer overflow in libuv. >> Off-by-one NUL write past a heap buffer in `uv_utf16_to_wtf8()` when >> called from the Windows TTY line-read path. When a user types or pastes CJK >> characters into a Windows console application backed by libuv, a 1-byte >> out-of-bounds NUL write occurs if the read buffer size is divisible by 3. >> >> I found this while reading through the TTY code. `uv_utf16_to_wtf8()` in >> src/idna.c unconditionally writes a NUL terminator at: >> ```c >> *target++ = '\0'; // idna.c:550 -- writes at target[target_len] when >> buffer is full >> ``` >> >> The function's own comment says `*target_len_ptr` should be the length >> _excluding_ space for NUL. Two callers in util.c handle this correctly: >> ```c >> utf8_len = *size_ptr - 1; /* Reserve space for NUL */ // util.c:126 >> *size -= 1; /* Reserve space for NUL. */ // util.c:1121 >> ``` >> >> But the TTY line-read path passes the full buffer size without the >> subtraction: >> ```c >> read_bytes = bytes; // tty.c:558 — should be bytes - 1 >> uv_utf16_to_wtf8(utf16, read_chars, >> &handle->tty.rd.read_line_buffer.base, >> &read_bytes); >> ``` >> >> The overflow happens when all the input characters encode to exactly 3 >> UTF-8 bytes each (BMP characters in U+0800–U+FFFF range, like CJK >> ideographs). The TTY code computes `chars = bytes / 3` (tty.c:540), so when >> `bytes % 3 == 0`, the worst-case output `chars * 3` equals `bytes` exactly, >> and the NUL terminator writes one byte past the buffer. >> >> The buffer size comes from the application's `alloc_cb`. libuv suggests >> 8192 (not divisible by 3), but any application returning a size that's >> divisible by 3 hits this. >> >> Introduced in v1.47.0 (commit f3889085, PR #4021), still present on v1.x >> HEAD. >> >> >> Best >> >> Ali Raza (@locus-x64) >> >
