Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <CA+7nKoUW7p1=zC=b2C5j7+27jPtHRFyD-fo4BWO9AV4yndHLLA@mail.gmail.com>
Date: Sun, 24 May 2026 22:07:07 +0700
From: Manopakorn Kooharueangrong <manopakorn.sec@...il.com>
To: oss-security@...ts.openwall.com
Subject: root-project/root: Heap buffer overflow in TKey::Streamer / TBasket::ReadBasketBuffers

Hello oss-security,

I am writing to report a confirmed memory safety vulnerability in
root-project/root (ROOT, the CERN C++ scientific computing framework)
version v6-40-00 and below. The issue was confirmed end-to-end against
the real ROOT library (v6-36-04, snap root-framework) with a
deterministic 3/3 reproducer.

I am requesting that you coordinate a CVE assignment. The maintainers
confirmed the bug and merged a fix, but declined to classify it as a
security advisory; I disagree for the reasons below.

== Summary ==

Issue: Heap buffer overflow in TBasket::ReadBasketBuffers via missing
fObjlen + fKeylen additive-overflow check in TKey::Streamer. An
attacker-controlled .root file triggers up to 32,767 bytes of OOB read and
OOB write on the heap when opened by a victim process.
Affected versions: v6-00-00 through v6-40-00 (262 release tags ship the
vulnerable path).
CVSS 3.1: AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H = 7.8 (High)
CWE: CWE-20, CWE-122, CWE-190
Authentication required: None.
Fix: Merged upstream in PR #22377 —
https://github.com/root-project/root/pull/22377

== Root cause ==

io/io/src/TKey.cxx:1375-1426 (TKey::Streamer) clamps fNbytes, fObjlen,
fKeylen to be non-negative but does not validate that fObjlen + fKeylen
fits in Int_t. The sibling routine TKey::ReadKeyBuffer at
io/io/src/TKey.cxx:1249-1254 does enforce this check (added in commit
2a596309bf, April 2026) but the fix was not propagated to Streamer until PR
#22377.

The unvalidated values flow to tree/tree/src/TBasket.cxx:583:

  uncompressedBufferLen = len > fObjlen+fKeylen ? len : fObjlen+fKeylen;

When fObjlen + fKeylen wraps to a negative value under signed overflow, the
small attacker-supplied len is chosen as the destination allocation size.
The subsequent memcpy at line 601 writes fKeylen bytes (up to 32,767, the
Short_t range) into the undersized buffer:

  memcpy(rawUncompressedBuffer, rawCompressedBuffer, fKeylen);

This is OOB read on the source and OOB write on the destination.

== PoC ==

Reproducible 3/3 against ROOT v6-36-04 (snap root-framework). Steps:

  $ /snap/bin/root -l -b -q make_good.C   # writes good.root, one TTree,
one basket
  $ python3 patch_basket.py               # 6-byte patch to basket header
  $ /snap/bin/root -l -b -q trigger.C     # opens bad.root, calls
tree->GetEntry(0)

The patch overwrites two fields at file offset 0xde:
  fObjlen (offset 0xe4): 0x00000020 -> 0x7fff8001
  fKeylen (offset 0xec): 0x0041     -> 0x7fff (32767)
  Result: fObjlen + fKeylen = 0x80000000 (signed wrap to INT_MIN)

ROOT runtime output (smoking gun showing attacker values reaching the sink):

  Processing trigger.C...
  Error R__unzip_header: error in header.  Values: 00
  Error in <TBasket::ReadBasketBuffers>: Inconsistency found in header
(nin=0, nbuf=0)
  Error in <TBasket::ReadBasketBuffers>: fNbytes = 97, fKeylen = 32767,
fObjlen = 2147450881, noutot = 0, nout=0, nin=0, nbuf=0
  Error in <TBranch::GetBasket>: File: bad.root at byte:222, branch:x,
entry:0, badread=1, nerrors=1, basketnumber=0
  double free or corruption (!prev)

The middle line proves TKey::Streamer accepted fObjlen = 0x7fff8001 and
fKeylen = 0x7fff without raising. The glibc abort line is heap metadata
corruption from the memcpy at TBasket.cxx:601.

gdb backtrace at abort through real ROOT symbols:

  #3  __GI_abort ()
  #5  malloc_printerr (str="double free or corruption (!prev)")
  #6  _int_free_merge_chunk (size=389952)
  #8  TBuffer::~TBuffer ()         from libCore.so
  #9  TBufferIO::~TBufferIO ()     from libRIO.so
  #10 TBufferFile::~TBufferFile () from libRIO.so
  #12 TBranch::~TBranch ()         from libTree.so
  #16 TTree::~TTree ()             from libTree.so
  #22 TFile::Close ()              from libRIO.so

Negative control: same flow against the original unpatched good.root prints
"x = 0" (the legitimate branch value) and exits cleanly with no glibc abort
and no error messages.

== Maintainer response ==

The bug was confirmed by @dpiparo on the GHSA-58gv-q2vp-fv8f draft advisory:

  "We confirm a crafted ROOT file could trigger the bug. Now, in
  presence of a tampered input, a crash would occur. Checks were put
  in place thanks to your report, see #22377 and backports."

The advisory was closed with: "We don't consider this item a security
advisory."

I disagree because:

1. It is a confirmed heap buffer overflow (CWE-122) triggered by integer
overflow (CWE-190) on attacker-controlled bytes parsed from a file. This
matches standard CVE criteria for a memory safety issue in a parser.

2. ROOT is routinely used to open files from multi-tenant and
network-reachable sources: CERN SWAN, JupyterLab-ROOT, batch hadd workers,
XRootD/EOS/CernVM-FS grid storage, CI artifacts, and downstream frameworks
(CMSSW, Gaudi, Athena). A single crafted .root file delivered to any of
these surfaces can compromise a long-running, grid-credentialed analysis
process.

3. The ~32 KB OOB write spans many glibc heap chunks and provides a
primitive for vtable or function-pointer overwrite, not just a crash.

== Recommended fix (matches upstream PR #22377) ==

In io/io/src/TKey.cxx (TKey::Streamer), mirror the check that already
exists in TKey::ReadKeyBuffer:

  constexpr auto maxInt_t = std::numeric_limits<Int_t>::max();
  if (fKeylen > (maxInt_t - fObjlen)) {
     Error("Streamer", "fObjlen (%d) + fKeylen (%d) > max int (%d)",
           fObjlen, fKeylen, maxInt_t);
     MakeZombie();
     return;
  }

Defence-in-depth: in tree/tree/src/TBasket.cxx, assert fKeylen <= len and
fKeylen <= uncompressedBufferLen before the memcpy at line 601.

== Credit ==

Please credit: manop55555 (https://github.com/manop55555), Finder /
Reporter.

== Disclosure ==

The fix is already public via PR #22377. I plan to publish this advisory
once a CVE is assigned, or after 90 days from today if no CVE is assigned.
Please acknowledge receipt.

== References ==

Upstream fix (merged): https://github.com/root-project/root/pull/22377
Sibling-fix commit: 2a596309bf (TKey::ReadKeyBuffer hardening, April 2026)
Vulnerable files: io/io/src/TKey.cxx (TKey::Streamer, lines 1375-1426)
tree/tree/src/TBasket.cxx (TBasket::ReadBasketBuffers, line 601)
Affected range: v6-00-00 through v6-40-00
Closed GHSA draft: GHSA-58gv-q2vp-fv8f (root-project/root)

Best regards,
manop55555

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.