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

Reply via email to