Skip to content

Commit 43b8aea

Browse files
[io] Bound key and free-list reads
ReadKeys and ReadFree still used the unbounded ReadKeyBuffer overload. A keys record can lie about TString lengths and walk off the heap buffer. ReadFree also had no remaining-size check, so a truncated free list never hit GetLast() > fEND. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6cfe829 commit 43b8aea

6 files changed

Lines changed: 231 additions & 21 deletions

File tree

io/io/inc/TFree.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
//////////////////////////////////////////////////////////////////////////
2323

2424
#include "TObject.h"
25+
#include <cstddef>
2526

2627

2728
class TFree : public TObject {
@@ -41,6 +42,7 @@ class TFree : public TObject {
4142
Long64_t GetLast() const {return fLast;}
4243
void ls(Option_t * = "") const override;
4344
virtual void ReadBuffer(char *&buffer);
45+
bool ReadBuffer(char *&buffer, std::size_t bufsize);
4446
void SetFirst(Long64_t first) {fFirst=first;}
4547
void SetLast(Long64_t last) {fLast=last;}
4648
Int_t Sizeof() const;

io/io/src/TDirectoryFile.cxx

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,26 +1440,52 @@ Int_t TDirectoryFile::ReadKeys(Bool_t forceRead)
14401440

14411441
Int_t nkeys = 0;
14421442
Long64_t fsize = fFile->GetSize();
1443-
if ( fSeekKeys > 0) {
1444-
TKey *headerkey = new TKey(fSeekKeys, fNbytesKeys, this);
1445-
headerkey->ReadFile();
1443+
if (fSeekKeys > 0) {
1444+
if (fNbytesKeys <= 0 || (fsize >= 0 && fNbytesKeys > fsize)) {
1445+
Error("ReadKeys", "illegal keys record size %d", fNbytesKeys);
1446+
return 0;
1447+
}
1448+
TKey *headerkey = new TKey(fSeekKeys, fNbytesKeys, this);
1449+
if (!headerkey->ReadFile()) {
1450+
Error("ReadKeys", "failed to read the keys record");
1451+
delete headerkey;
1452+
return 0;
1453+
}
14461454
buffer = headerkey->GetBuffer();
1447-
headerkey->ReadKeyBuffer(buffer);
1455+
char *const bufbegin = buffer;
1456+
const std::size_t bufsize = static_cast<std::size_t>(headerkey->GetNbytes());
1457+
if (!headerkey->ReadKeyBuffer(buffer, bufsize)) {
1458+
delete headerkey;
1459+
return 0;
1460+
}
1461+
std::size_t remaining = bufsize - static_cast<std::size_t>(buffer - bufbegin);
14481462

1449-
TKey *key;
1463+
if (remaining < sizeof(Int_t)) {
1464+
Error("ReadKeys", "truncated keys list");
1465+
delete headerkey;
1466+
return 0;
1467+
}
14501468
frombuf(buffer, &nkeys);
1469+
remaining -= sizeof(Int_t);
1470+
14511471
for (Int_t i = 0; i < nkeys; i++) {
1452-
key = new TKey(this);
1453-
key->ReadKeyBuffer(buffer);
1472+
TKey *key = new TKey(this);
1473+
char *const before = buffer;
1474+
if (!key->ReadKeyBuffer(buffer, remaining)) {
1475+
delete key;
1476+
nkeys = i;
1477+
break;
1478+
}
1479+
remaining -= static_cast<std::size_t>(buffer - before);
14541480
if (key->GetSeekKey() < 64 || key->GetSeekKey() > fsize) {
1455-
Error("ReadKeys","reading illegal key, exiting after %d keys",i);
1456-
fKeys->Remove(key);
1481+
Error("ReadKeys", "reading illegal key, exiting after %d keys", i);
1482+
delete key;
14571483
nkeys = i;
14581484
break;
14591485
}
14601486
if (key->GetSeekPdir() < 64 || key->GetSeekPdir() > fsize) {
1461-
Error("ReadKeys","reading illegal key, exiting after %d keys",i);
1462-
fKeys->Remove(key);
1487+
Error("ReadKeys", "reading illegal key, exiting after %d keys", i);
1488+
delete key;
14631489
nkeys = i;
14641490
break;
14651491
}

io/io/src/TFile.cxx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,22 +1986,47 @@ Int_t TFile::ReadBufferViaCache(char *buf, Int_t len)
19861986

19871987
void TFile::ReadFree()
19881988
{
1989-
// Avoid problem with file corruption.
1990-
if (fNbytesFree < 0 || fNbytesFree > fEND) {
1989+
// Avoid problems with file corruption.
1990+
if (fNbytesFree <= 0 || fNbytesFree > fEND) {
19911991
fNbytesFree = 0;
19921992
return;
19931993
}
1994+
19941995
TKey *headerfree = new TKey(fSeekFree, fNbytesFree, this);
1995-
headerfree->ReadFile();
1996+
if (!headerfree->ReadFile()) {
1997+
Error("ReadFree", "failed to read the free segment record");
1998+
delete headerfree;
1999+
return;
2000+
}
2001+
19962002
char *buffer = headerfree->GetBuffer();
1997-
headerfree->ReadKeyBuffer(buffer);
1998-
buffer = headerfree->GetBuffer();
1999-
while (1) {
2003+
char *const bufbegin = buffer;
2004+
const std::size_t bufsize = static_cast<std::size_t>(fNbytesFree);
2005+
2006+
if (!headerfree->ReadKeyBuffer(buffer, bufsize)) {
2007+
delete headerfree;
2008+
return;
2009+
}
2010+
2011+
std::size_t remaining = bufsize - static_cast<std::size_t>(buffer - bufbegin);
2012+
2013+
while (remaining > 0) {
20002014
TFree *afree = new TFree();
2001-
afree->ReadBuffer(buffer);
2015+
char *const before = buffer;
2016+
2017+
if (!afree->ReadBuffer(buffer, remaining)) {
2018+
delete afree;
2019+
Error("ReadFree", "truncated free segment list");
2020+
break;
2021+
}
2022+
2023+
remaining -= static_cast<std::size_t>(buffer - before);
20022024
fFree->Add(afree);
2003-
if (afree->GetLast() > fEND) break;
2025+
2026+
if (afree->GetLast() > fEND)
2027+
break;
20042028
}
2029+
20052030
delete headerfree;
20062031
}
20072032

io/io/src/TFree.cxx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,44 @@ void TFree::ReadBuffer(char *&buffer)
177177
}
178178
}
179179

180+
////////////////////////////////////////////////////////////////////////////////
181+
/// Decode one free structure from input buffer.
182+
/// \return true if decoding was successful.
183+
184+
bool TFree::ReadBuffer(char *&buffer, std::size_t bufsize)
185+
{
186+
constexpr std::size_t kVerSize = sizeof(Version_t);
187+
if (bufsize < kVerSize) {
188+
Error("ReadBuffer", "The given buffer is too small to fit a TFree record.");
189+
return false;
190+
}
191+
Version_t version;
192+
frombuf(buffer, &version);
193+
bufsize -= kVerSize;
194+
195+
if (version > 1000) {
196+
constexpr std::size_t kNeed = 2 * sizeof(Long64_t);
197+
if (bufsize < kNeed) {
198+
Error("ReadBuffer", "The given buffer is too small to fit a TFree record.");
199+
return false;
200+
}
201+
frombuf(buffer, &fFirst);
202+
frombuf(buffer, &fLast);
203+
} else {
204+
constexpr std::size_t kNeed = 2 * sizeof(Int_t);
205+
if (bufsize < kNeed) {
206+
Error("ReadBuffer", "The given buffer is too small to fit a TFree record.");
207+
return false;
208+
}
209+
Int_t first, last;
210+
frombuf(buffer, &first);
211+
fFirst = (Long64_t)first;
212+
frombuf(buffer, &last);
213+
fLast = (Long64_t)last;
214+
}
215+
return true;
216+
}
217+
180218
////////////////////////////////////////////////////////////////////////////////
181219
/// return number of bytes occupied by this TFree on permanent storage
182220

io/io/src/TKey.cxx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,9 +1317,10 @@ bool TKey::ReadKeyBuffer(char *&buffer, std::size_t bufsize)
13171317
}
13181318
} ConsumeBufCapacity{this, bufsize};
13191319

1320-
// Min size of the buffer for reading the common key header data
1320+
// Min size of the buffer for reading the common key header data.
1321+
// TDatime is stored as a UInt_t; do not use sizeof(TDatime) (it has a vtable).
13211322
constexpr std::size_t kMinBufSize =
1322-
sizeof(fNbytes) + sizeof(Version_t) + sizeof(fObjlen) + sizeof(fKeylen) + sizeof(fCycle);
1323+
sizeof(fNbytes) + sizeof(Version_t) + sizeof(fObjlen) + sizeof(UInt_t) + sizeof(fKeylen) + sizeof(fCycle);
13231324
if (!ConsumeBufCapacity(kMinBufSize))
13241325
return false;
13251326

io/io/test/TFileTests.cxx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include <vector>
33
#include <string>
44
#include <array>
5+
#include <algorithm>
6+
#include <fstream>
57

68
#include "gtest/gtest.h"
79

@@ -16,6 +18,8 @@
1618
#include "TROOT.h" // gROOT
1719
#include "TSystem.h"
1820
#include "TEnv.h" // gEnv
21+
#include "TFree.h"
22+
#include "TError.h"
1923

2024
TEST(TFile, WriteObjectTObject)
2125
{
@@ -332,3 +336,117 @@ TEST(TFile, UUID)
332336
TMemFile f("uuidtest.root", "RECREATE");
333337
EXPECT_EQ('4', f.GetUUID().AsString()[14]);
334338
}
339+
340+
namespace {
341+
std::string gCollectedDiags;
342+
void CollectDiags(int /*level*/, Bool_t /*abort*/, const char *location, const char *msg)
343+
{
344+
gCollectedDiags += location;
345+
gCollectedDiags += ": ";
346+
gCollectedDiags += msg;
347+
gCollectedDiags += '\n';
348+
}
349+
} // namespace
350+
351+
TEST(TFile, ReadKeysValid)
352+
{
353+
ROOT::TestSupport::FileRaii fileGuard("tfile_readkeys_valid.root");
354+
{
355+
TFile f(fileGuard.GetPath().c_str(), "RECREATE");
356+
TNamed named("short", "t");
357+
named.Write();
358+
}
359+
TFile in(fileGuard.GetPath().c_str());
360+
ASSERT_FALSE(in.IsZombie());
361+
EXPECT_EQ(in.GetNkeys(), 1);
362+
auto *named = in.Get<TNamed>("short");
363+
ASSERT_NE(named, nullptr);
364+
EXPECT_STREQ(named->GetTitle(), "t");
365+
}
366+
367+
TEST(TFile, ReadKeysOversizedString)
368+
{
369+
ROOT::TestSupport::FileRaii fileGuard("tfile_readkeys_oversize.root");
370+
Long64_t seekKeys = 0;
371+
Int_t nbytesKeys = 0;
372+
{
373+
TFile f(fileGuard.GetPath().c_str(), "RECREATE");
374+
TNamed named("short", "t");
375+
named.Write();
376+
f.Write();
377+
seekKeys = f.GetSeekKeys();
378+
nbytesKeys = f.GetNbytesKeys();
379+
}
380+
ASSERT_GT(seekKeys, 0);
381+
ASSERT_GT(nbytesKeys, 0);
382+
383+
{
384+
std::fstream fs(fileGuard.GetPath(), std::ios::in | std::ios::out | std::ios::binary);
385+
ASSERT_TRUE(fs.good());
386+
std::vector<char> rec(static_cast<std::size_t>(nbytesKeys));
387+
fs.seekg(seekKeys);
388+
fs.read(rec.data(), nbytesKeys);
389+
ASSERT_EQ(fs.gcount(), nbytesKeys);
390+
391+
const char needle[] = {'\x05', 's', 'h', 'o', 'r', 't'};
392+
auto it = std::search(rec.begin(), rec.end(), std::begin(needle), std::end(needle));
393+
ASSERT_NE(it, rec.end());
394+
*it = static_cast<char>(255);
395+
fs.seekp(seekKeys);
396+
fs.write(rec.data(), nbytesKeys);
397+
ASSERT_TRUE(fs.good());
398+
}
399+
400+
gCollectedDiags.clear();
401+
{
402+
ROOT::TestSupport::FilterDiagsRAII capture(CollectDiags);
403+
TFile in(fileGuard.GetPath().c_str());
404+
// Opening must return; do not walk off the keys buffer.
405+
EXPECT_TRUE(in.IsZombie() || in.GetNkeys() >= 0);
406+
}
407+
EXPECT_NE(gCollectedDiags.find("given buffer is too small"), std::string::npos);
408+
}
409+
410+
TEST(TFile, ReadFreeValid)
411+
{
412+
ROOT::TestSupport::FileRaii fileGuard("tfile_readfree_valid.root");
413+
{
414+
TFile f(fileGuard.GetPath().c_str(), "RECREATE");
415+
TNamed named("n", "t");
416+
named.Write();
417+
}
418+
{
419+
TFile f(fileGuard.GetPath().c_str(), "UPDATE");
420+
ASSERT_FALSE(f.IsZombie());
421+
TNamed named2("n2", "t");
422+
named2.Write();
423+
}
424+
TFile in(fileGuard.GetPath().c_str());
425+
ASSERT_FALSE(in.IsZombie());
426+
EXPECT_NE(in.Get<TNamed>("n"), nullptr);
427+
EXPECT_NE(in.Get<TNamed>("n2"), nullptr);
428+
}
429+
430+
TEST(TFree, ReadBufferBounds)
431+
{
432+
char packed[10] = {};
433+
char *p = packed;
434+
TFree out;
435+
out.SetFirst(100);
436+
out.SetLast(200);
437+
out.FillBuffer(p);
438+
ASSERT_EQ(p - packed, 10);
439+
440+
p = packed;
441+
TFree in;
442+
EXPECT_TRUE(in.ReadBuffer(p, sizeof(packed)));
443+
EXPECT_EQ(in.GetFirst(), 100);
444+
EXPECT_EQ(in.GetLast(), 200);
445+
446+
char tooSmall[3] = {};
447+
p = tooSmall;
448+
TFree truncated;
449+
ROOT::TestSupport::CheckDiagsRAII diags;
450+
diags.requiredDiag(kError, "TFree::ReadBuffer", "The given buffer is too small", false);
451+
EXPECT_FALSE(truncated.ReadBuffer(p, sizeof(tooSmall)));
452+
}

0 commit comments

Comments
 (0)