Repository navigation
Description
Summary
Calling msgpack_zbuffer_init(zbuf, level, 0) — passing init_size == 0 — followed by any write via msgpack_zbuffer_write() leads to a double-free (confirmed) or a hang/infinite loop (observed via fuzzing, allocator-dependent), because msgpack_zbuffer_init() stores init_size verbatim with no substitution of a sane default, unlike its sibling msgpack_vrefbuffer_init(), which does substitute defaults for zero arguments.
- File:
include/msgpack/zbuffer.h - Functions:
msgpack_zbuffer_init(lines 66–76),msgpack_zbuffer_expand(lines 102–119),msgpack_zbuffer_destroy(lines 78–82) - Affected version:
c_masterbranch, HEAD82a6cc03 - Bug class: Double-free (CWE-415) / potential infinite loop (CWE-835), rooted in missing input validation (CWE-20) and an inconsistent API convention across sibling buffer types
Root cause
static inline bool msgpack_zbuffer_init(msgpack_zbuffer* zbuf,
int level, size_t init_size)
{
memset(zbuf, 0, sizeof(msgpack_zbuffer));
zbuf->init_size = init_size; // <-- stored verbatim; 0 is NOT replaced with a default
if(deflateInit(&zbuf->stream, level) != Z_OK) {
free(zbuf->data);
return false;
}
return true;
}Compare with the sibling msgpack_vrefbuffer_init() (src/vrefbuffer.c), which explicitly substitutes MSGPACK_VREFBUFFER_REF_SIZE / MSGPACK_VREFBUFFER_CHUNK_SIZE whenever its size arguments are 0 — establishing an API convention (across this codebase) that 0 conventionally means "use the default." msgpack_zbuffer_init() breaks that convention silently: it provides no validation or substitution, so a caller reasonably assuming 0 means "use the default" (as it does for the sibling type) instead gets exact, literal zero stored into init_size.
static inline bool msgpack_zbuffer_expand(msgpack_zbuffer* zbuf)
{
size_t used = (size_t)((char *)(zbuf->stream.next_out) - zbuf->data);
size_t csize = used + zbuf->stream.avail_out;
size_t nsize = (csize == 0) ? zbuf->init_size : csize * 2; // nsize = 0 when init_size=0 and csize=0
char* tmp = (char*)realloc(zbuf->data, nsize); // realloc(ptr, 0) — implementation-defined
if(tmp == NULL) {
return false; // zbuf->data is NOT reset to NULL here
}
zbuf->data = tmp;
zbuf->stream.next_out = (Bytef*)(tmp + used);
zbuf->stream.avail_out = (uInt)(nsize - used);
return true;
}
static inline void msgpack_zbuffer_destroy(msgpack_zbuffer* zbuf)
{
deflateEnd(&zbuf->stream);
free(zbuf->data); // double-free site when zbuf->data is stale
}Before any output has been produced, zbuf->data and zbuf->stream.next_out are both NULL (from the initial memset), so used == 0. With csize == 0, nsize becomes zbuf->init_size — literally 0 when the caller passed 0. This produces realloc(zbuf->data, 0), whose behavior is implementation-defined:
- glibc's realloc(ptr, 0) frees
ptrand returnsNULL—msgpack_zbuffer_expandthen returnsfalsewithout resettingzbuf->datato NULL, leaving a dangling pointer to already-freed memory. The subsequentmsgpack_zbuffer_destroy()(or a secondmsgpack_zbuffer_write()call) then operates on/frees that stale pointer → double-free. - On allocators where
realloc(ptr, 0)instead returns a new, valid zero-size pointer each time withoutavail_outever growing pastMSGPACK_ZBUFFER_RESERVE_SIZE(512):msgpack_zbuffer_write()'swhile(avail_in > 0) { if (avail_out < RESERVE) expand(); deflate(...); }loop can spin callingexpand()repeatedly with the exit condition never satisfied → hang.
Confirmed no in-tree caller passes init_size == 0 (the only in-tree call, test/buffer_c.cpp:28, explicitly uses MSGPACK_ZBUFFER_INIT_SIZE), so this requires an out-of-tree caller to pass 0 — but the function provides no validation/rejection of 0, and the sibling vrefbuffer API's own convention makes 0 a very plausible value for a caller to pass expecting default behavior.
Trigger / reproduction
Minimal, clean, 3-call reproducer using only the public API:
#include <msgpack/zbuffer.h>
#include <stdio.h>
int main(void) {
msgpack_zbuffer z;
msgpack_zbuffer_init(&z, -1, 0); /* Z_DEFAULT_COMPRESSION, init_size = 0 */
msgpack_zbuffer_write(&z, "ZZZ", 3);
msgpack_zbuffer_write(&z, "ABC", 3);
msgpack_zbuffer_destroy(&z); /* double-free here */
return 0;
}clang -fsanitize=address,undefined -g -O0 -I include -o poc poc.c -lz
./pocObserved ASan output (exact run, this audit):
=================================================================
==131690==ERROR: AddressSanitizer: attempting double-free on 0x502000000010 in thread T0:
#0 ... in free
#1 ... in msgpack_zbuffer_destroy include/msgpack/zbuffer.h:81:5
#2 ... in main poc.c:11:5
0x502000000010 is located 0 bytes inside of 1-byte region [0x502000000010,0x502000000011)
freed by thread T0 here:
#0 ... in realloc
#1 ... in msgpack_zbuffer_expand include/msgpack/zbuffer.h:109:24
#2 ... in msgpack_zbuffer_write include/msgpack/zbuffer.h:133:17
#3 ... in main poc.c:9:15
previously allocated by thread T0 here:
#0 ... in realloc
#1 ... in msgpack_zbuffer_expand include/msgpack/zbuffer.h:109:24
#2 ... in msgpack_zbuffer_write include/msgpack/zbuffer.h:133:17
#3 ... in main poc.c:7:15
SUMMARY: AddressSanitizer: double-free ... in free
==131690==ABORTING
Negative control (same two-write sequence, using the documented default MSGPACK_ZBUFFER_INIT_SIZE instead of 0) runs clean:
init ok, data=(nil) avail_out=0
write1 rc=0 data=0x525000000100 avail_out=8190
write2 rc=0 data=0x525000000100 avail_out=8190
destroy ok - no corruption
This isolates the precondition to exactly init_size == 0, not something inherent to msgpack_zbuffer_write.
The bug reproduces regardless of the level argument to deflateInit (any valid -1..9 / Z_DEFAULT_COMPRESSION value) — the trigger is entirely a function of init_size, independent of compression level.
Fuzzing evidence
Found independently via AFL++ fuzzing of a harness exercising the public msgpack_vrefbuffer/msgpack_zbuffer API directly: 13 double-free crashes and 2 hangs in a ~6-minute campaign, all tracing back to this one root cause via identical ASan stack traces (freed-by/previously-allocated-by both pointing at msgpack_zbuffer_expand's realloc at zbuffer.h:109, final double-free in msgpack_zbuffer_destroy's free(zbuf->data) at zbuffer.h:81).
Impact
Double-free (heap corruption, exploitable for further memory-corruption impact depending on allocator state at the time of the second free) or an unbounded hang/DoS, triggered purely by passing 0 as init_size to a public, documented API function — no malicious/attacker-controlled input is required beyond the caller's own choice of constructor argument, though a library that forwards a size value from configuration or user input into this parameter would allow external triggering. This is arguably easier to hit than the other two findings I report — no size-imbalance crafting needed, just an ordinary constructor call with 0 followed by two ordinary writes.
Suggested fix
Mirror msgpack_vrefbuffer_init()'s own convention — substitute a documented default when 0 is passed:
static inline bool msgpack_zbuffer_init(msgpack_zbuffer* zbuf,
int level, size_t init_size)
{
memset(zbuf, 0, sizeof(msgpack_zbuffer));
- zbuf->init_size = init_size;
+ zbuf->init_size = (init_size == 0) ? MSGPACK_ZBUFFER_INIT_SIZE : init_size;
if(deflateInit(&zbuf->stream, level) != Z_OK) {
free(zbuf->data);
return false;
}
return true;
}As defense in depth, msgpack_zbuffer_expand() should also reset zbuf->data = NULL; before returning false on realloc failure, so that any future double-free path (from an unrelated cause) degrades to a NULL-pointer free (a no-op) rather than a double-free.
Full ASan trace
init ok, data=(nil) avail_out=0
write1 rc=-1 data=0x502000000010 avail_out=0
write2 rc=-1 data=0x502000000010 avail_out=0
=================================================================
==194594==ERROR: AddressSanitizer: attempting double-free on 0x502000000010 in thread T0:
#0 0x56035aa8cf3a in free (/home/mir/projects/msgpack-c/poc-finding3/poc+0xcaf3a) (BuildId: 8eb2a83163fa7b0534bfd8e69de66be93de4de34)
#1 0x56035aacc346 in msgpack_zbuffer_destroy /home/mir/projects/msgpack-c/poc-finding3/../include/msgpack/zbuffer.h:81:5
#2 0x56035aacbab3 in main /home/mir/projects/msgpack-c/poc-finding3/zbuffer_double_free_poc.c:11:5
#3 0x7d59db82a1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#4 0x7d59db82a28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#5 0x56035a9f2384 in _start (/home/mir/projects/msgpack-c/poc-finding3/poc+0x30384) (BuildId: 8eb2a83163fa7b0534bfd8e69de66be93de4de34)
0x502000000010 is located 0 bytes inside of 1-byte region [0x502000000010,0x502000000011)
freed by thread T0 here:
#0 0x56035aa8d5f0 in realloc (/home/mir/projects/msgpack-c/poc-finding3/poc+0xcb5f0) (BuildId: 8eb2a83163fa7b0534bfd8e69de66be93de4de34)
#1 0x56035aacc740 in msgpack_zbuffer_expand /home/mir/projects/msgpack-c/poc-finding3/../include/msgpack/zbuffer.h:109:24
#2 0x56035aacc1bb in msgpack_zbuffer_write /home/mir/projects/msgpack-c/poc-finding3/../include/msgpack/zbuffer.h:133:17
#3 0x56035aacba11 in main /home/mir/projects/msgpack-c/poc-finding3/zbuffer_double_free_poc.c:9:15
#4 0x7d59db82a1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#5 0x7d59db82a28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#6 0x56035a9f2384 in _start (/home/mir/projects/msgpack-c/poc-finding3/poc+0x30384) (BuildId: 8eb2a83163fa7b0534bfd8e69de66be93de4de34)
previously allocated by thread T0 here:
#0 0x56035aa8d5f0 in realloc (/home/mir/projects/msgpack-c/poc-finding3/poc+0xcb5f0) (BuildId: 8eb2a83163fa7b0534bfd8e69de66be93de4de34)
#1 0x56035aacc740 in msgpack_zbuffer_expand /home/mir/projects/msgpack-c/poc-finding3/../include/msgpack/zbuffer.h:109:24
#2 0x56035aacc1bb in msgpack_zbuffer_write /home/mir/projects/msgpack-c/poc-finding3/../include/msgpack/zbuffer.h:133:17
#3 0x56035aacb963 in main /home/mir/projects/msgpack-c/poc-finding3/zbuffer_double_free_poc.c:7:15
#4 0x7d59db82a1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#5 0x7d59db82a28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#6 0x56035a9f2384 in _start (/home/mir/projects/msgpack-c/poc-finding3/poc+0x30384) (BuildId: 8eb2a83163fa7b0534bfd8e69de66be93de4de34)
SUMMARY: AddressSanitizer: double-free (/home/mir/projects/msgpack-c/poc-finding3/poc+0xcaf3a) (BuildId: 8eb2a83163fa7b0534bfd8e69de66be93de4de34) in free
==194594==ABORTING

Activity
zbuffer: handle a zero init_size and sizes wider than zlib's counters
Guard zbuffer::expand against zero and wrapped sizes