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
