Skip to content

Commit 86084e0

Browse files
authored
DPL Analysis: harden the native AOD reader against corrupt input (#16031)
1 parent 8c8505b commit 86084e0

9 files changed

Lines changed: 99 additions & 54 deletions

File tree

‎Framework/AODNative/src/Container.cxx‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ constexpr size_t kHeaderReadSize = 128;
2727
constexpr size_t kDirectoryReadSize = 64;
2828
// Largest gap between the TTrees of a DF still read together with them.
2929
constexpr uint64_t kMetadataGap = 4 * 1024;
30-
// Fixed part of a TBasket key, after which come the class name, name and title.
31-
constexpr size_t kBasketHeaderTail = 19;
3230
// Smallest possible key header: 32-bit seeks and three empty strings.
3331
constexpr size_t kMinKeySize = 29;
3432
// Metadata objects (trees, maps) in AO2Ds are a few MB at most. A larger
@@ -184,6 +182,9 @@ std::vector<std::string> RootFile::dataFrames()
184182
for (auto& k : topKeys()) {
185183
std::smatch m;
186184
if (std::regex_match(k.name, m, tfRegex)) {
185+
if (m[1].length() > 19) {
186+
throw CorruptData(mUrl, k.seekKey, "DF number of " + k.name + " does not fit 64 bits");
187+
}
187188
auto n = std::stoull(m[1].str());
188189
if (seen.insert(n).second) {
189190
numbers.push_back(n);
@@ -325,13 +326,8 @@ TreeInfo RootFile::readTree(const KeyInfo& key)
325326
continue;
326327
}
327328
auto buf = readRange(*mSource, basket.seek, std::min<uint64_t>(basket.nbytes, kHeaderReadSize * 4));
328-
auto k = parseKey(buf, basket.seek);
329329
Cursor c{buf, mUrl, basket.seek};
330-
c.seek(k.keylen - kBasketHeaderTail);
331-
c.u16(); // fVersion
332-
c.i32(); // fBufferSize
333-
c.i32(); // fNevBufSize
334-
basket.nEntries = c.i32();
330+
basket.nEntries = readBasketHeader(c).nevbuf;
335331
}
336332
}
337333
return tree;

‎Framework/AODNative/src/Cursor.h‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,48 @@ class Cursor
127127
size_t mPos = 0;
128128
};
129129

130+
/// The key and header of a TBasket.
131+
struct BasketHeader {
132+
uint32_t nbytes;
133+
uint32_t objlen;
134+
uint16_t keylen;
135+
uint16_t version;
136+
int32_t nevbuf;
137+
int32_t last;
138+
uint8_t flag;
139+
};
140+
141+
/// Reads a TBasket key and header forwards from @a c, as TBasket::Streamer
142+
/// writes them: a negative fNevBufSize is followed by one more byte, fIOBits.
143+
/// Leaves @a c at the end of the key.
144+
inline BasketHeader readBasketHeader(Cursor& c)
145+
{
146+
auto start = c.pos();
147+
BasketHeader h;
148+
h.nbytes = c.u32();
149+
bool big = c.i16() > 1000;
150+
h.objlen = c.u32();
151+
c.u32(); // fDatime
152+
h.keylen = c.u16();
153+
c.u16(); // fCycle
154+
c.skip(big ? 16 : 8); // fSeekKey, fSeekPdir
155+
c.string(); // fClassName
156+
c.string(); // fName
157+
c.string(); // fTitle
158+
h.version = c.u16();
159+
c.i32(); // fBufferSize
160+
if (c.i32() < 0) {
161+
c.u8(); // fIOBits
162+
}
163+
h.nevbuf = c.i32();
164+
h.last = c.i32();
165+
h.flag = c.u8();
166+
if (c.pos() != start + h.keylen) {
167+
c.fail("TBasket key of " + std::to_string(c.pos() - start) + " bytes, but fKeylen is " + std::to_string(h.keylen));
168+
}
169+
return h;
170+
}
171+
130172
} // namespace o2::aod::native
131173

132174
#endif // O2_FRAMEWORK_AODNATIVE_CURSOR_H_

‎Framework/AODNative/src/Decoder.cxx‎

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ namespace o2::aod::native
2828

2929
namespace
3030
{
31-
// Fixed part of a TBasket key, after which come the class name, name and title.
32-
constexpr size_t kBasketHeaderTail = 19;
3331

3432
template <typename T>
3533
void byteswapAll(uint8_t* data, size_t count)
@@ -132,21 +130,12 @@ void BranchDecoder::forEachBasket(BranchInfo const& branch, bool parallel, F&& f
132130
bytes = storage;
133131
}
134132
Cursor c{bytes, mUrl, ref.seek};
135-
auto nbytes = c.u32();
136-
c.i16(); // key fVersion
137-
auto objlen = c.u32();
138-
c.u32(); // fDatime
139-
auto keylen = c.u16();
140-
if (nbytes != ref.nbytes || keylen < kBasketHeaderTail || keylen > nbytes) {
141-
c.fail("basket header of branch " + branch.name + " does not match its TBranch");
142-
}
143-
c.seek(keylen - kBasketHeaderTail);
144-
c.u16(); // fVersion
145-
c.i32(); // fBufferSize
146-
c.i32(); // fNevBufSize
147-
auto nevbuf = c.i32();
148-
auto last = c.i32();
149-
if (nevbuf != ref.nEntries || last < keylen || (uint32_t)(last - keylen) > objlen) {
133+
auto header = readBasketHeader(c);
134+
auto nbytes = header.nbytes;
135+
auto objlen = header.objlen;
136+
auto keylen = header.keylen;
137+
auto last = header.last;
138+
if (nbytes != ref.nbytes || keylen > nbytes || header.nevbuf != ref.nEntries || last < keylen || (uint32_t)(last - keylen) > objlen) {
150139
c.fail("basket header of branch " + branch.name + " does not match its TBranch");
151140
}
152141
values.border = last - keylen;
@@ -278,6 +267,8 @@ std::vector<int32_t> BranchDecoder::decodeOffsets(BranchInfo const& sizeBranch)
278267
if (sizeBranch.leafClass != 'I' || sizeBranch.fixedLen != 1) {
279268
throw UnsupportedLayout("size branch " + sizeBranch.name + " is not a scalar 32-bit integer");
280269
}
270+
// fEntries sizes the allocation: check it against the baskets first.
271+
checkBaskets(sizeBranch);
281272
// Decode the sizes after the leading 0, then prefix-sum in place.
282273
std::vector<int32_t> offsets(sizeBranch.entries + 1);
283274
decodeValues(sizeBranch, (uint8_t*)(offsets.data() + 1), sizeBranch.entries * sizeof(int32_t));

‎Framework/AODNative/src/Objects.cxx‎

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ constexpr uint32_t kNewClassTag = 0xFFFFFFFF;
2727
constexpr uint16_t kStreamedMemberWise = 0x4000;
2828
constexpr uint32_t kIsReferenced = 1 << 4;
2929
constexpr int64_t kMapOffset = 2;
30-
// Fixed part of a TBasket key, after which come the class name, name and title.
31-
constexpr size_t kBasketHeaderTail = 19;
3230

3331
struct LeafInfo {
3432
std::string name;
@@ -256,6 +254,9 @@ std::shared_ptr<LeafInfo> ObjectReader::tleaf(std::string const& className, int6
256254
leaf->className = className;
257255
leaf->name = tnamed();
258256
leaf->len = mC.i32();
257+
if (leaf->len <= 0) {
258+
mC.fail("leaf " + leaf->name + " with length " + std::to_string(leaf->len));
259+
}
259260
mC.i32(); // fLenType
260261
mC.i32(); // fOffset
261262
mC.u8(); // fIsRange
@@ -277,23 +278,12 @@ std::shared_ptr<EmbeddedBasket> ObjectReader::tbasket(int64_t mapAt)
277278
// TBasket has no versioned header of its own: it starts with its TKey.
278279
auto basket = std::make_shared<EmbeddedBasket>();
279280
mObjects[mapAt] = basket;
280-
auto start = mC.pos();
281-
mC.i32(); // fNbytes
282-
mC.i16(); // key fVersion
283-
mC.i32(); // fObjlen
284-
mC.u32(); // fDatime
285-
auto keylen = mC.u16();
286-
mC.u16(); // fCycle
287-
if (keylen < kBasketHeaderTail) {
288-
mC.fail("embedded basket with key length " + std::to_string(keylen));
289-
}
290-
mC.seek(start + keylen - kBasketHeaderTail);
291-
checkVersion("TBasket", mC.u16() % 1000);
292-
mC.i32(); // fBufferSize
293-
mC.i32(); // fNevBufSize
294-
auto nevbuf = mC.i32();
295-
auto last = mC.i32();
296-
int flag = mC.u8();
281+
auto header = readBasketHeader(mC);
282+
checkVersion("TBasket", header.version % 1000);
283+
auto keylen = header.keylen;
284+
auto nevbuf = header.nevbuf;
285+
auto last = header.last;
286+
int flag = header.flag;
297287
if (nevbuf < 0 || last < keylen) {
298288
mC.fail("inconsistent embedded basket header");
299289
}

‎Framework/AODNative/test/RootFraming.h‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ struct BigEndianWriter {
110110
};
111111

112112
/// A TBasket key as written on disk: key header, TBasket fields, then the
113-
/// payload holding @a values followed by a fake entry-offset tail.
114-
inline std::vector<uint8_t> makeBasket(Algo algo, std::span<const uint8_t> values, int32_t nevbuf, size_t tailBytes = 0, size_t chunkSize = 0xffffff)
113+
/// payload holding @a values followed by a fake entry-offset tail. With
114+
/// @a ioBits, the header has the layout of TTree::kGenerateOffsetMap: a
115+
/// negative fNevBufSize followed by one fIOBits byte.
116+
inline std::vector<uint8_t> makeBasket(Algo algo, std::span<const uint8_t> values, int32_t nevbuf, size_t tailBytes = 0, size_t chunkSize = 0xffffff, bool ioBits = false)
115117
{
116118
std::vector<uint8_t> raw(values.begin(), values.end());
117119
// The tail is poisoned: a decoder that reads past the value region sees it.
@@ -132,9 +134,12 @@ inline std::vector<uint8_t> makeBasket(Algo algo, std::span<const uint8_t> value
132134
h.string("TBasket"); // class, name, title
133135
h.string("fX");
134136
h.string("O2track");
135-
h.put<uint16_t>(3); // fVersion
136-
h.put<int32_t>(32000); // fBufferSize
137-
h.put<int32_t>(0); // fNevBufSize
137+
h.put<uint16_t>(3); // fVersion
138+
h.put<int32_t>(32000); // fBufferSize
139+
h.put<int32_t>(ioBits ? -4 : 0); // fNevBufSize
140+
if (ioBits) {
141+
h.put<uint8_t>(0); // fIOBits
142+
}
138143
h.put<int32_t>(nevbuf); // fNevBuf
139144
h.put<int32_t>(keylen + values.size()); // fLast
140145
h.put<uint8_t>(tailBytes ? 1 : 2); // flag

‎Framework/AODNative/test/test_Decoder.cxx‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ struct BranchBuilder {
4545
branch.entries = branch.totBytes = branch.zipBytes = 0;
4646
}
4747

48-
void add(Algo algo, std::vector<uint8_t> const& values, int32_t nEntries, size_t tail = 0, size_t chunkSize = 0xffffff)
48+
void add(Algo algo, std::vector<uint8_t> const& values, int32_t nEntries, size_t tail = 0, size_t chunkSize = 0xffffff, bool ioBits = false)
4949
{
50-
auto basket = makeBasket(algo, values, nEntries, tail, chunkSize);
50+
auto basket = makeBasket(algo, values, nEntries, tail, chunkSize, ioBits);
5151
branch.baskets.push_back(BasketRef{file.size(), (uint32_t)basket.size(), branch.entries, nEntries, false, {}});
5252
file.insert(file.end(), basket.begin(), basket.end());
5353
branch.entries += nEntries;
@@ -103,6 +103,25 @@ TEST_CASE("float values across codecs, uncompressed and embedded baskets")
103103
}
104104
}
105105

106+
TEST_CASE("baskets with fIOBits in their header decode like the others")
107+
{
108+
BranchBuilder b{'F'};
109+
std::vector<float> expected;
110+
for (auto [algo, ioBits] : {std::pair{Algo::ZSTD, true}, std::pair{Algo::None, false}, std::pair{Algo::LZ4, true}}) {
111+
std::vector<float> values;
112+
for (int i = 0; i < 100; ++i) {
113+
values.push_back(expected.size() + i);
114+
}
115+
b.add(algo, bigEndian(values), values.size(), 404, 0xffffff, ioBits);
116+
expected.insert(expected.end(), values.begin(), values.end());
117+
}
118+
MemoryByteSource source{b.file};
119+
BranchDecoder decoder{source, Options{}};
120+
std::vector<float> out(expected.size(), 42.f);
121+
decoder.decodeValues(b.branch, (uint8_t*)out.data(), out.size() * sizeof(float));
122+
CHECK(out == expected);
123+
}
124+
106125
TEST_CASE("decoding stops exactly at the value region for every codec")
107126
{
108127
// A poisoned entry-offset tail after the values: nothing past fLast - fKeylen

‎Framework/AnalysisSupport/src/DataInputDirector.cxx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ bool DataInputDescriptor::setFile(int counter, int wantedParentLevel, std::strin
161161
if (backend != "root") {
162162
try {
163163
mCurrentFile = openNativeAOD(filename, mFactory, mContext.nativeOptions);
164-
} catch (o2::aod::native::FormatError const& e) {
164+
} catch (std::exception const& e) {
165165
if (backend == "native") {
166166
throw AODReadError(AODReadError::Kind::FileOpen, fmt::format("Couldn't open file \"{}\" with the native reader: {}", filename, e.what()));
167167
}

‎Framework/AnalysisSupport/src/NativeAOD.cxx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ void NativeAODFileSystem::addToPlan(std::string const& treeName)
206206
NativeTreeHandle* handle = nullptr;
207207
try {
208208
handle = findTree(mPlanDF, treeName);
209-
} catch (FormatError const&) {
209+
} catch (std::exception const&) {
210210
// Reported when the tree itself is asked for.
211211
}
212212
if (handle) {

‎Framework/AnalysisSupport/src/NativeAODPlugin.cxx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,9 @@ struct NativeAODObjectReadingCapability : o2::framework::RootObjectReadingCapabi
324324
}
325325
try {
326326
return nativeFS->getTreeHandle(path);
327-
} catch (o2::aod::native::FormatError const& e) {
327+
} catch (AODReadError const&) {
328+
throw;
329+
} catch (std::exception const& e) {
328330
// A DF which cannot be parsed: --aod-skip-invalid-reads skips it.
329331
throw AODReadError(AODReadError::Kind::InvalidTimeframe, e.what());
330332
}

0 commit comments

Comments
 (0)