|
|
Message-ID: <CALrEReZGS86TGNLY8MAfskakDgV_=gmeHZDSiwb-U3XTrmoaEg@mail.gmail.com>
Date: Thu, 19 Mar 2026 22:09:58 +0500
From: Ali Raza <elirazamumtaz@...il.com>
To: oss-security@...ts.openwall.com
Cc: s@...hul.net, info@...ordhuis.nl, GitHub <security-advisories@...hub.com>
Subject: Re: Off-by-one heap buffer overflow in libuv
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@...il.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@...il.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" -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 <elirazamumtaz@...il.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 `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)
>>>
>>
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.