Skip to content

Commit 2797a39

Browse files
authored
Harden bulk TTree reads against corrupted baskets (#15795)
* Harden bulk TTree reads against corrupted baskets * fmt::format for exception
1 parent 1e21f73 commit 2797a39

3 files changed

Lines changed: 53 additions & 19 deletions

File tree

Framework/AnalysisSupport/src/AODJAlienReaderHelpers.cxx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "Framework/EndOfStreamContext.h"
3333
#include "Framework/DeviceSpec.h"
3434
#include "Framework/RawDeviceService.h"
35+
#include "Framework/RuntimeError.h"
3536
#include "Framework/DataSpecUtils.h"
3637
#include "Framework/MessageContext.h"
3738
#include "Framework/Signpost.h"
@@ -131,7 +132,9 @@ static std::string describeException(std::exception const& exception)
131132
try {
132133
std::rethrow_if_nested(exception);
133134
} catch (std::exception const& nested) {
134-
description += ": " + describeException(nested);
135+
description += fmt::format(": {}", describeException(nested));
136+
} catch (RuntimeErrorRef const& ref) {
137+
description += fmt::format(": {}", error_from_ref(ref).what);
135138
} catch (...) {
136139
description += ": unknown exception";
137140
}
@@ -275,6 +278,7 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const
275278
auto skippedTimeframes = ++totalInvalidReadSkipped;
276279
LOGP(error, "Invalid AOD read for table {}: fileCounter {}, timeFrame {}. Skipping timeframe (skipped timeframes: {}). Reason: {}",
277280
concrete.origin.as<std::string>(), fcnt, ntf, skippedTimeframes, describeException(e));
281+
clean_all_runtime_errors();
278282
didir->markTimeFrameSkipped(header::DataHeader(concrete.description, concrete.origin, concrete.subSpec), ntf);
279283
arrowContext.clear();
280284
messageContext.discard();

Framework/AnalysisSupport/src/DataInputDirector.cxx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,9 @@ bool DataInputDescriptor::readTree(DataAllocator& outputs, header::DataHeader dh
574574
}
575575

576576
auto schemaOpt = format->Inspect(fullpath);
577+
if (!schemaOpt.ok()) {
578+
throw InvalidAODReadError(fmt::format("Unable to inspect tree {}: {}", treename, schemaOpt.status().ToString()));
579+
}
577580
auto physicalSchema = schemaOpt;
578581
std::vector<std::shared_ptr<arrow::Field>> fields;
579582
for (auto& original : (*schemaOpt)->fields()) {

Framework/AnalysisSupport/src/TTreePlugin.cxx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,33 @@ arrow::Result<std::shared_ptr<arrow::Buffer>> TTreeDeferredReadOutputStream::Fin
187187

188188
arrow::Result<int64_t> TTreeDeferredReadOutputStream::Tell() const { return position_; }
189189

190+
// Bulk reads follow the basket boundaries in the file, so a corrupted file must not overrun the target buffers.
191+
auto checkReadRange = [](ReadOps const& op, int readEntries, int readLast) {
192+
if (readLast <= 0) {
193+
throw runtime_error_f("Error while reading branch %s starting from %d: got %d entries.", op.branch->GetName(), readEntries, readLast);
194+
}
195+
if (static_cast<int64_t>(readEntries) + readLast > op.rootBranchEntries) {
196+
throw runtime_error_f("Invalid read range for branch %s: starting from %d, read %d entries, total entries %lld.",
197+
op.branch->GetName(), readEntries, readLast, static_cast<long long>(op.rootBranchEntries));
198+
}
199+
};
200+
201+
auto checkBasketBytes = [](ReadOps const& op, int readEntries, int64_t bytesNeeded, TBufferFile const& rootBuffer) {
202+
int64_t available = static_cast<int64_t>(rootBuffer.BufferSize()) - rootBuffer.Length();
203+
if (bytesNeeded < 0 || bytesNeeded > available) {
204+
throw runtime_error_f("Basket of branch %s starting from %d holds %lld bytes, but %lld are needed.",
205+
op.branch->GetName(), readEntries, static_cast<long long>(available), static_cast<long long>(bytesNeeded));
206+
}
207+
};
208+
190209
auto readValues = [](uint8_t* target, ReadOps& op, TBufferFile& rootBuffer) {
191210
int readEntries = 0;
192211
rootBuffer.Reset();
193212
while (readEntries < op.rootBranchEntries) {
194213
auto readLast = op.branch->GetBulkRead().GetEntriesSerialized(readEntries, rootBuffer);
195-
if (readLast < 0) {
196-
throw runtime_error_f("Error while reading branch %s starting from %zu.", op.branch->GetName(), readEntries);
197-
}
214+
checkReadRange(op, readEntries, readLast);
198215
int size = readLast * op.listSize;
216+
checkBasketBytes(op, readEntries, static_cast<int64_t>(size) * op.typeSize, rootBuffer);
199217
readEntries += readLast;
200218
bigEndianCopy(target, rootBuffer.GetCurrent(), size, op.typeSize);
201219
target += (ptrdiff_t)(size * op.typeSize);
@@ -211,10 +229,9 @@ auto readBoolValues = [](uint8_t* target, ReadOps& op, TBufferFile& rootBuffer)
211229
while (readEntries < op.rootBranchEntries) {
212230
auto beginValue = readEntries;
213231
readLast = op.branch->GetBulkRead().GetBulkEntries(readEntries, rootBuffer);
214-
if (readLast < 0) {
215-
throw runtime_error_f("Error while reading branch %s starting from %d.", op.branch->GetName(), readEntries);
216-
}
232+
checkReadRange(op, readEntries, readLast);
217233
int size = readLast * op.listSize;
234+
checkBasketBytes(op, readEntries, size, rootBuffer);
218235
readEntries += readLast;
219236
for (int i = beginValue; i < beginValue + size; ++i) {
220237
auto value = static_cast<uint8_t>(rootBuffer.GetCurrent()[i - beginValue] << (i % 8));
@@ -225,24 +242,25 @@ auto readBoolValues = [](uint8_t* target, ReadOps& op, TBufferFile& rootBuffer)
225242

226243
auto readVLAValues = [](uint8_t* target, ReadOps& op, ReadOps const& offsetOp, TBufferFile& rootBuffer) {
227244
int readEntries = 0;
245+
// The offsets are only valid for as many entries as the size branch has.
246+
if (op.rootBranchEntries != offsetOp.rootBranchEntries) {
247+
throw runtime_error_f("Branch %s has %lld entries, but its size branch %s has %lld.",
248+
op.branch->GetName(), static_cast<long long>(op.rootBranchEntries),
249+
offsetOp.branch->GetName(), static_cast<long long>(offsetOp.rootBranchEntries));
250+
}
228251
auto* tPtrOffset = reinterpret_cast<const int*>(offsetOp.targetBuffer->data());
229252
std::span<int const> const offsets{tPtrOffset, tPtrOffset + offsetOp.rootBranchEntries + 1};
230253

231254
rootBuffer.Reset();
232255
while (readEntries < op.rootBranchEntries) {
233256
auto readLast = op.branch->GetBulkRead().GetEntriesSerialized(readEntries, rootBuffer);
234-
if (readLast < 0) {
235-
throw runtime_error_f("Error while reading branch %s starting from %d.", op.branch->GetName(), readEntries);
236-
}
237-
if (readEntries + readLast > op.rootBranchEntries) {
238-
throw runtime_error_f("Invalid read range for branch %s: starting from %d, read %d entries, total entries %lld.",
239-
op.branch->GetName(), readEntries, readLast, static_cast<long long>(op.rootBranchEntries));
240-
}
257+
checkReadRange(op, readEntries, readLast);
241258
int size = offsets[readEntries + readLast] - offsets[readEntries];
242259
if (size < 0) {
243260
throw runtime_error_f("Invalid offset range for branch %s: offsets[%d]=%d, offsets[%d]=%d.",
244261
op.branch->GetName(), readEntries, offsets[readEntries], readEntries + readLast, offsets[readEntries + readLast]);
245262
}
263+
checkBasketBytes(op, readEntries, static_cast<int64_t>(size) * op.typeSize, rootBuffer);
246264
readEntries += readLast;
247265
bigEndianCopy(target, rootBuffer.GetCurrent(), size, op.typeSize);
248266
target += (ptrdiff_t)(size * op.typeSize);
@@ -583,7 +601,7 @@ struct BranchFieldMapping {
583601
};
584602

585603
auto readOffsets = [](ReadOps& op, TBufferFile& rootBuffer) {
586-
uint32_t offset = 0;
604+
int64_t offset = 0;
587605
std::span<int> offsets;
588606
int readEntries = 0;
589607
int count = 0;
@@ -594,14 +612,17 @@ auto readOffsets = [](ReadOps& op, TBufferFile& rootBuffer) {
594612
rootBuffer.Reset();
595613
while (readEntries < op.rootBranchEntries) {
596614
auto readLast = op.branch->GetBulkRead().GetEntriesSerialized(readEntries, rootBuffer);
597-
if (readLast == -1) {
598-
throw runtime_error_f("Unable to read from branch %s.", op.branch->GetName());
599-
}
615+
checkReadRange(op, readEntries, readLast);
616+
checkBasketBytes(op, readEntries, static_cast<int64_t>(readLast) * sizeof(uint32_t), rootBuffer);
600617
readEntries += readLast;
601618
for (auto i = 0; i < readLast; ++i) {
602619
offsets[count++] = (int)offset;
603620
uint32_t raw = reinterpret_cast<uint32_t*>(rootBuffer.GetCurrent())[i];
604621
offset += (std::endian::native == std::endian::little) ? __builtin_bswap32(raw) : raw;
622+
// Arrow lists use 32 bit offsets, a larger total can only come from corrupted sizes.
623+
if (offset > INT32_MAX) {
624+
throw runtime_error_f("Invalid sizes for branch %s: offsets overflow at entry %d.", op.branch->GetName(), count - 1);
625+
}
605626
}
606627
}
607628
offsets[count] = (int)offset;
@@ -924,6 +945,9 @@ arrow::Result<std::shared_ptr<arrow::Schema>> TTreeFileFormat::Inspect(const arr
924945
// Notice that we abuse of the API here and do not release the TTree,
925946
// so that it's still managed by ROOT.
926947
auto tree = objectHandler->GetObjectAsOwner<TTree>().release();
948+
if (tree == nullptr) {
949+
return arrow::Status::IOError("Unable to read tree ", source.path());
950+
}
927951

928952
auto branches = tree->GetListOfBranches();
929953
auto n = branches->GetEntries();
@@ -933,6 +957,9 @@ arrow::Result<std::shared_ptr<arrow::Schema>> TTreeFileFormat::Inspect(const arr
933957
bool prevIsSize = false;
934958
for (auto i = 0; i < n; ++i) {
935959
auto branch = static_cast<TBranch*>(branches->At(i));
960+
if (branch == nullptr || branch->GetListOfLeaves()->At(0) == nullptr) {
961+
return arrow::Status::IOError("Invalid branch ", i, " in tree ", source.path());
962+
}
936963
std::string name = branch->GetName();
937964
if (prevIsSize && fields.back()->name() != name + "_size") {
938965
throw runtime_error_f("Unexpected layout for VLA container %s.", branch->GetName());
@@ -956,7 +983,7 @@ arrow::Result<std::shared_ptr<arrow::Schema>> TTreeFileFormat::Inspect(const arr
956983
}
957984
}
958985

959-
if (fields.back()->name().ends_with("_size")) {
986+
if (!fields.empty() && fields.back()->name().ends_with("_size")) {
960987
throw runtime_error_f("Missing values for VLA indices %s.", fields.back()->name().c_str());
961988
}
962989
return std::make_shared<arrow::Schema>(fields);

0 commit comments

Comments
 (0)