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)
>>
>

Reply via email to