fix(dbengine): validate extent disk size and fix uncompressed page bounds (#22324)
* fix(dbengine): validate extent disk size and fix uncompressed page bounds Add rrdeng_valid_extent_disk_size() and check at journal restore, open-cache add, and disk read. Fix missing bounds check in the uncompressed page path that could read past payload_length on corrupt extent metadata. Treat short reads from uv_fs_read as errors. * fix(dbengine): improve logging for invalid extent size handling * fix(pdc): enhance boundary checks for page offsets and lengths
Stelios Fragkakis committed
May 2, 2026 at 21:04 UTC
1608f65b04124c7e20384667162195875dad4349
5 files changed
+63
-10
src/database/engine/journalfile.c
+1
-1
@@ -702,7 +702,7 @@ static void journalfile_restore_extent_metadata(struct rrdengine_instance *ctx,
702
count = jf_metric_data->number_of_pages;
703
descr_size = sizeof(*jf_metric_data->descr) * count;
704
payload_length = sizeof(*jf_metric_data) + descr_size;
705
- if (payload_length > max_size) {
705
+ if (payload_length > max_size || !rrdeng_valid_extent_disk_size(jf_metric_data->extent_size)) {
706
netdata_log_error("DBENGINE: corrupted transaction payload.");
707
return;
708
}
src/database/engine/pagecache.c
+8
@@ -1079,6 +1079,14 @@ void pgc_open_add_hot_page(
1079
uint64_t extent_offset,
1080
unsigned extent_size)
1081
{
1082
+ if (unlikely(!rrdeng_valid_extent_disk_size(extent_size))) {
1083
+ nd_log_limit_static_thread_var(erl, 10, 0);
1084
+ nd_log_limit(&erl, NDLS_DAEMON, NDLP_ERR,
1085
+ "DBENGINE: skipped adding open-cache page for datafile %u of tier %u, "
1086
+ "extent at offset %" PRIu64 " has invalid size %u",
1087
+ datafile->fileno, datafile->tier, extent_offset, extent_size);
1088
+ return;
1089
+ }
1090
1091
if(!datafile_acquire(datafile, DATAFILE_ACQUIRE_OPEN_CACHE)) { // for open cache item
1092
nd_log_limit_static_thread_var(erl, 10, 0);
src/database/engine/pdc.c
+31
-8
@@ -1047,7 +1047,7 @@ static bool epdl_populate_pages_from_extent_data(
1047
uLong crc;
1048
1049
bool can_use_data = true;
1050
- if(data_length < sizeof(*header) + sizeof(header->descr[0]) + sizeof(*trailer)) {
1050
+ if(!rrdeng_valid_extent_disk_size(data_length)) {
1051
can_use_data = false;
1052
1053
// added to satisfy the requirements of older compilers (prevent warnings)
@@ -1079,7 +1079,7 @@ static bool epdl_populate_pages_from_extent_data(
1079
}
1080
1081
crc = crc32(0L, Z_NULL, 0);
1082
- crc = crc32(crc, data, epdl->extent_size - sizeof(*trailer));
1082
+ crc = crc32(crc, data, data_length - sizeof(*trailer));
1083
if (unlikely(crc32cmp(trailer->checksum, crc))) {
1084
ctx_io_error(ctx);
1085
have_read_error = true;
@@ -1178,13 +1178,27 @@ static bool epdl_populate_pages_from_extent_data(
1178
}
1179
else {
1180
if (RRDENG_COMPRESSION_NONE == header->compression_algorithm) {
1181
- pgd = pgd_create_from_disk_data(header->descr[i].type,
1182
- data + payload_offset + page_offset,
1183
- vd.page_length);
1184
- stats_load_uncompressed++;
1181
+ if (unlikely(vd.page_length > payload_length ||
1182
+ page_offset > payload_length - vd.page_length)) {
1183
+ char log[200 + 1];
1184
+ snprintfz(log, sizeof(log) - 1, "page %u (out of %u) offset %u + page length %zu, "
1185
+ "exceeds the payload size %" PRIu64,
1186
+ i, count, page_offset, vd.page_length, payload_length);
1187
+ epdl_extent_loading_error_log(ctx, epdl, &header->descr[i], log, NDLP_ERR);
1188
+
1189
+ pgd = PGD_EMPTY;
1190
+ stats_load_invalid_page++;
1191
+ }
1192
+ else {
1193
+ pgd = pgd_create_from_disk_data(header->descr[i].type,
1194
+ data + payload_offset + page_offset,
1195
+ vd.page_length);
1196
+ stats_load_uncompressed++;
1197
+ }
1198
}
1199
else {
1187
- if (unlikely(page_offset + vd.page_length > uncompressed_payload_length)) {
1200
+ if (unlikely(vd.page_length > uncompressed_payload_length ||
1201
+ page_offset > uncompressed_payload_length - vd.page_length)) {
1202
char log[200 + 1];
1203
snprintfz(log, sizeof(log) - 1, "page %u (out of %u) offset %u + page length %zu, "
1204
"exceeds the uncompressed buffer size %u",
@@ -1275,6 +1289,15 @@ static bool epdl_populate_pages_from_extent_data(
1289
1290
static inline void *datafile_extent_read(struct rrdengine_instance *ctx, uv_file file, uint32_t block, unsigned size_bytes)
1291
{
1292
+ if (unlikely(!rrdeng_valid_extent_disk_size(size_bytes))) {
1293
+ nd_log_limit_static_global_var(erl, 1, 0);
1294
+ nd_log_limit(&erl, NDLS_DAEMON, NDLP_ERR,
1295
+ "DBENGINE: refusing to read extent at offset %" PRIu64 " with invalid size %u",
1296
+ BLOCK_TO_OFFSET(block), size_bytes);
1297
+ ctx_io_error(ctx);
1298
+ return NULL;
1299
+ }
1300
+
1301
void *buffer = NULL;
1302
uv_fs_t request;
1303
@@ -1283,7 +1306,7 @@ static inline void *datafile_extent_read(struct rrdengine_instance *ctx, uv_file
1306
1307
uv_buf_t iov = uv_buf_init(buffer, real_io_size);
1308
int ret = uv_fs_read(NULL, &request, file, &iov, 1, (int64_t) BLOCK_TO_OFFSET(block), NULL);
1286
- if (unlikely(-1 == ret)) {
1309
+ if (unlikely(ret < 0 || (unsigned)ret != real_io_size)) {
1310
ctx_io_error(ctx);
1311
posix_memalign_freez(buffer);
1312
buffer = NULL;
src/database/engine/rrdengine.c
+6
-1
@@ -692,6 +692,10 @@ extent_flush_to_open(struct rrdengine_instance *ctx, struct extent_io_descriptor
692
datafile = xt_io_descr->datafile;
693
694
bool still_running = ctx_is_available_for_queries(ctx);
695
+ if (likely(still_running && !have_error))
696
+ internal_fatal(!rrdeng_valid_extent_disk_size(xt_io_descr->bytes),
697
+ "DBENGINE: flushed extent has invalid size %u",
698
+ xt_io_descr->bytes);
699
700
usec_t max_end_time_ut = 0;
701
for (i = 0 ; i < xt_io_descr->descr_count ; ++i) {
@@ -700,7 +704,7 @@ extent_flush_to_open(struct rrdengine_instance *ctx, struct extent_io_descriptor
704
if (descr->end_time_ut > max_end_time_ut)
705
max_end_time_ut = descr->end_time_ut;
706
703
- if (likely(still_running && !have_error))
707
+ if (likely(still_running && !have_error)) {
708
pgc_open_add_hot_page(
709
(Word_t)ctx,
710
descr->metric_id,
@@ -710,6 +714,7 @@ extent_flush_to_open(struct rrdengine_instance *ctx, struct extent_io_descriptor
714
datafile,
715
xt_io_descr->pos,
716
xt_io_descr->bytes);
717
+ }
718
719
page_descriptor_release(descr);
720
}
src/database/engine/rrdengine.h
+17
@@ -58,6 +58,23 @@ struct rrdeng_cmd;
58
59
#define MAX_EXTENT_UNCOMPRESSED_SIZE (MAX_PAGES_PER_EXTENT * (RRDENG_BLOCK_SIZE + RRDENG_GORILLA_32BIT_BUFFER_SIZE))
60
61
+static inline size_t rrdeng_min_extent_disk_size(void) {
62
+ return sizeof(struct rrdeng_df_extent_header) +
63
+ sizeof(struct rrdeng_extent_page_descr) +
64
+ sizeof(struct rrdeng_df_extent_trailer);
65
+}
66
+
67
+static inline size_t rrdeng_max_extent_disk_size(void) {
68
+ return sizeof(struct rrdeng_df_extent_header) +
69
+ sizeof(struct rrdeng_extent_page_descr) * MAX_PAGES_PER_EXTENT +
70
+ MAX_EXTENT_UNCOMPRESSED_SIZE +
71
+ sizeof(struct rrdeng_df_extent_trailer);
72
+}
73
+
74
+static inline bool rrdeng_valid_extent_disk_size(size_t size) {
75
+ return size >= rrdeng_min_extent_disk_size() && size <= rrdeng_max_extent_disk_size();
76
+}
77
+
78
79
#define RRDENG_FILE_NUMBER_SCAN_TMPL "%1u-%10u"
80
#define RRDENG_FILE_NUMBER_PRINT_TMPL "%1.1u-%10.10u"