When I requested that my report be published and asked for a CVE for this vulnerability, the maintainers (CC) closed my advisory report, saying: > “A zero byte is written past the end of the buffer, right? There’s not much attack surface there since it’s in the TTY code. Users don’t regularly pwn themselves, one would hope.”
But I am still in favor as it's still a genuine heap buffer overflow: the NUL byte lands one past the allocated region, which can corrupt heap metadata or an adjacent allocation's first byte. In the right conditions (e.g., an application embedding libuv that reads TTY input into a buffer sized divisible by 3), this could lead to heap corruption. Low severity for sure, but it's a real out-of-bounds write rather than a theoretical one and the PoC confirms it reliably on every run. Even CWE-193 (off-by-one) with a NUL byte has been assigned CVEs in similar libraries before. On Thu, 19 Mar 2026 at 21:56, Ali Raza <[email protected]> wrote: > 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) >>> >>
