Skip to content

Commit f45649c

Browse files
committed
Harden LOUDS trie topology validation
Validate LOUDS trie topology-derived leaf counts while loading serialized trie data. Reject malformed blocks whose dense/sparse topology does not match the declared key count, child table count, or chain bitmap count before handle and seqno arrays are trusted. This keeps validation on the cold InitFromData path and preserves the on-disk format, while making malformed custom index blocks fail closed with Corruption instead of allowing release builds to index handle/seqno arrays with unchecked topology-derived leaf ordinals.
1 parent 7e2223f commit f45649c

2 files changed

Lines changed: 711 additions & 28 deletions

File tree

utilities/trie_index/louds_trie.cc

Lines changed: 139 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,10 @@ Status LoudsTrie::InitFromData(const Slice& data) {
10691069
if (max_depth_ > kMaxReasonableDepth) {
10701070
return Status::Corruption("Trie index: max_depth exceeds reasonable limit");
10711071
}
1072+
if (static_cast<uint64_t>(cutoff_level_) >
1073+
static_cast<uint64_t>(max_depth_) + 1) {
1074+
return Status::Corruption("Trie index: cutoff_level exceeds max_depth");
1075+
}
10721076

10731077
memcpy(&dense_leaf_count_, p, 8);
10741078
p += 8;
@@ -1080,6 +1084,17 @@ Status LoudsTrie::InitFromData(const Slice& data) {
10801084
p += 8;
10811085
remaining -= 8;
10821086

1087+
// d_labels_ stores exactly 256 bits per dense node, and Bitvector rejects
1088+
// serialized bitvectors whose logical size exceeds UINT32_MAX bits because
1089+
// its rank LUT entries are uint32_t. Check the matching dense-node bound
1090+
// explicitly before using this header field to bound validation work.
1091+
static constexpr uint64_t kMaxReasonableDenseNodes =
1092+
std::numeric_limits<uint32_t>::max() / 256;
1093+
if (dense_node_count_ > kMaxReasonableDenseNodes) {
1094+
return Status::Corruption(
1095+
"Trie index: dense_node_count exceeds reasonable limit");
1096+
}
1097+
10831098
// Read flags field.
10841099
uint32_t flags;
10851100
memcpy(&flags, p, 4);
@@ -1120,26 +1135,72 @@ Status LoudsTrie::InitFromData(const Slice& data) {
11201135
// ordinal computation during traversal. Inconsistent values would cause
11211136
// incorrect rank arithmetic leading to wrong leaf indices -> wrong block
11221137
// handles.
1123-
if (dense_node_count_ > 0) {
1124-
// Each dense node uses 256 bits in d_labels_.
1125-
if (d_labels_.NumBits() != dense_node_count_ * 256) {
1126-
return Status::Corruption(
1127-
"Trie index: d_labels size inconsistent with dense_node_count");
1128-
}
1129-
// d_has_child_ has one bit per set bit in d_labels_ (one per label).
1130-
if (d_has_child_.NumBits() != d_labels_.NumOnes()) {
1131-
return Status::Corruption(
1132-
"Trie index: d_has_child size inconsistent with d_labels");
1138+
if (dense_node_count_ > std::numeric_limits<uint64_t>::max() / 256 ||
1139+
d_labels_.NumBits() != dense_node_count_ * 256) {
1140+
return Status::Corruption(
1141+
"Trie index: d_labels size inconsistent with dense_node_count");
1142+
}
1143+
// d_has_child_ has one bit per set bit in d_labels_ (one per label).
1144+
if (d_has_child_.NumBits() != d_labels_.NumOnes()) {
1145+
return Status::Corruption(
1146+
"Trie index: d_has_child size inconsistent with d_labels");
1147+
}
1148+
// d_is_prefix_key_ has one bit per dense node.
1149+
if (d_is_prefix_key_.NumBits() != dense_node_count_) {
1150+
return Status::Corruption(
1151+
"Trie index: d_is_prefix_key size inconsistent with "
1152+
"dense_node_count");
1153+
}
1154+
uint64_t computed_dense_leaf_count =
1155+
d_is_prefix_key_.NumOnes() +
1156+
(d_labels_.NumOnes() - d_has_child_.NumOnes());
1157+
if (dense_leaf_count_ != computed_dense_leaf_count) {
1158+
return Status::Corruption(
1159+
"Trie index: dense_leaf_count inconsistent with dense topology");
1160+
}
1161+
if (dense_leaf_count_ > num_keys_) {
1162+
return Status::Corruption("Trie index: dense_leaf_count exceeds num_keys");
1163+
}
1164+
if (cutoff_level_ > 0 && dense_node_count_ == 0) {
1165+
return Status::Corruption(
1166+
"Trie index: cutoff_level inconsistent with dense_node_count");
1167+
}
1168+
1169+
uint64_t expected_dense_child_count = 0;
1170+
if (cutoff_level_ == 0) {
1171+
expected_dense_child_count = num_keys_ == 0 ? 0 : 1;
1172+
} else {
1173+
uint64_t level_start_node = 0;
1174+
uint64_t level_node_count = dense_node_count_ == 0 ? 0 : 1;
1175+
uint64_t dense_nodes_seen = 0;
1176+
for (uint32_t level = 0; level < cutoff_level_; level++) {
1177+
if (level_start_node + level_node_count > dense_node_count_) {
1178+
return Status::Corruption(
1179+
"Trie index: dense_node_count inconsistent with dense topology");
1180+
}
1181+
uint64_t internal_children = 0;
1182+
for (uint64_t i = 0; i < level_node_count; i++) {
1183+
uint64_t node_num = level_start_node + i;
1184+
uint64_t label_begin = d_labels_.Rank1(node_num * 256);
1185+
uint64_t label_end = d_labels_.Rank1((node_num + 1) * 256);
1186+
internal_children +=
1187+
d_has_child_.Rank1(label_end) - d_has_child_.Rank1(label_begin);
1188+
}
1189+
dense_nodes_seen += level_node_count;
1190+
if (level + 1 == cutoff_level_) {
1191+
expected_dense_child_count = internal_children;
1192+
}
1193+
level_start_node += level_node_count;
1194+
level_node_count = internal_children;
11331195
}
1134-
// d_is_prefix_key_ has one bit per dense node.
1135-
if (d_is_prefix_key_.NumBits() != dense_node_count_) {
1196+
if (dense_nodes_seen != dense_node_count_) {
11361197
return Status::Corruption(
1137-
"Trie index: d_is_prefix_key size inconsistent with "
1138-
"dense_node_count");
1198+
"Trie index: dense_node_count inconsistent with dense topology");
11391199
}
11401200
}
1141-
if (dense_leaf_count_ > num_keys_) {
1142-
return Status::Corruption("Trie index: dense_leaf_count exceeds num_keys");
1201+
if (dense_child_count_ != expected_dense_child_count) {
1202+
return Status::Corruption(
1203+
"Trie index: dense_child_count inconsistent with dense topology");
11431204
}
11441205

11451206
// Sparse section.
@@ -1190,15 +1251,30 @@ Status LoudsTrie::InitFromData(const Slice& data) {
11901251
// label has exactly one bit in s_has_child_ and one bit in s_louds_.
11911252
// A mismatch means the serialized data is inconsistent, which would cause
11921253
// GetBit/Rank1 OOB reads during traversal.
1193-
if (s_labels_size_ > 0) {
1194-
if (s_has_child_.NumBits() != s_labels_size_) {
1195-
return Status::Corruption(
1196-
"Trie index: s_has_child size inconsistent with s_labels_size");
1197-
}
1198-
if (s_louds_.NumBits() != s_labels_size_) {
1199-
return Status::Corruption(
1200-
"Trie index: s_louds size inconsistent with s_labels_size");
1201-
}
1254+
if (s_has_child_.NumBits() != s_labels_size_) {
1255+
return Status::Corruption(
1256+
"Trie index: s_has_child size inconsistent with s_labels_size");
1257+
}
1258+
if (s_louds_.NumBits() != s_labels_size_) {
1259+
return Status::Corruption(
1260+
"Trie index: s_louds size inconsistent with s_labels_size");
1261+
}
1262+
if (s_labels_size_ > 0 && !s_louds_.GetBit(0)) {
1263+
return Status::Corruption("Trie index: sparse LOUDS root is missing");
1264+
}
1265+
if (s_is_prefix_key_.NumBits() != s_louds_.NumOnes()) {
1266+
return Status::Corruption(
1267+
"Trie index: s_is_prefix_key size inconsistent with sparse nodes");
1268+
}
1269+
if (dense_child_count_ > s_louds_.NumOnes()) {
1270+
return Status::Corruption(
1271+
"Trie index: dense_child_count exceeds sparse node count");
1272+
}
1273+
uint64_t computed_sparse_leaf_count =
1274+
s_is_prefix_key_.NumOnes() + (s_labels_size_ - s_has_child_.NumOnes());
1275+
if (dense_leaf_count_ + computed_sparse_leaf_count != num_keys_) {
1276+
return Status::Corruption(
1277+
"Trie index: num_keys inconsistent with trie topology");
12021278
}
12031279

12041280
// Child position lookup tables for Select-free sparse traversal.
@@ -1219,6 +1295,18 @@ Status LoudsTrie::InitFromData(const Slice& data) {
12191295
return Status::Corruption(
12201296
"Trie index: num_internal exceeds reasonable limit");
12211297
}
1298+
if (num_internal != s_has_child_.NumOnes()) {
1299+
return Status::Corruption(
1300+
"Trie index: child position table count inconsistent with sparse "
1301+
"topology");
1302+
}
1303+
if (dense_child_count_ >
1304+
std::numeric_limits<uint64_t>::max() - num_internal ||
1305+
s_louds_.NumOnes() != dense_child_count_ + num_internal) {
1306+
return Status::Corruption(
1307+
"Trie index: sparse node count inconsistent with child position "
1308+
"table");
1309+
}
12221310

12231311
if (num_internal > 0) {
12241312
// Read s_child_start_pos_
@@ -1251,10 +1339,26 @@ Status LoudsTrie::InitFromData(const Slice& data) {
12511339
for (uint64_t k = 0; k < num_internal; k++) {
12521340
if (s_child_start_pos_[k] >= s_labels_size_ ||
12531341
s_child_end_pos_[k] > s_labels_size_ ||
1254-
s_child_end_pos_[k] < s_child_start_pos_[k]) {
1342+
s_child_end_pos_[k] <= s_child_start_pos_[k]) {
12551343
return Status::Corruption(
12561344
"Trie index: child position out of range for sparse labels");
12571345
}
1346+
if (!s_louds_.GetBit(s_child_start_pos_[k])) {
1347+
return Status::Corruption(
1348+
"Trie index: child position does not start a sparse node");
1349+
}
1350+
uint64_t child_node = dense_child_count_ + k;
1351+
uint64_t expected_start = s_louds_.FindNthOneBit(child_node);
1352+
uint64_t expected_end = s_louds_.NextSetBit(expected_start + 1);
1353+
if (expected_end > s_labels_size_) {
1354+
expected_end = s_labels_size_;
1355+
}
1356+
if (s_child_start_pos_[k] != expected_start ||
1357+
s_child_end_pos_[k] != expected_end) {
1358+
return Status::Corruption(
1359+
"Trie index: child position table inconsistent with sparse "
1360+
"topology");
1361+
}
12581362
}
12591363
}
12601364
}
@@ -1269,6 +1373,10 @@ Status LoudsTrie::InitFromData(const Slice& data) {
12691373
memcpy(&num_chains, p, 8);
12701374
p += 8;
12711375
remaining -= 8;
1376+
if (num_chains > num_internal) {
1377+
return Status::Corruption(
1378+
"Trie index: chain count exceeds sparse internal node count");
1379+
}
12721380

12731381
if (num_chains > 0) {
12741382
// Read chain bitmap.
@@ -1278,6 +1386,11 @@ Status LoudsTrie::InitFromData(const Slice& data) {
12781386
}
12791387
p += consumed;
12801388
remaining -= consumed;
1389+
if (s_chain_bitmap_.NumBits() != num_internal ||
1390+
s_chain_bitmap_.NumOnes() != num_chains) {
1391+
return Status::Corruption(
1392+
"Trie index: chain bitmap inconsistent with chain count");
1393+
}
12811394

12821395
// Read compact chain_offsets (uint32_t per chain).
12831396
size_t offsets_bytes = num_chains * sizeof(uint32_t);

0 commit comments

Comments
 (0)