@cryptotaxi247 / netdata / commits / b5e92cb48

Protect v2 journal populate walk from SIGBUS (#22514)

* fix(dbengine): protect v2 journal populate walk from SIGBUS journalfile_v2_populate_retention_to_mrg() walked the mmap'd v2 journal in two phases: an optional CRC check (when JOURNALFILE_FLAG_METRIC_CRC_CHECK is set -- the default cheap-load path on every startup) followed by the mrg update. Only the mrg update ran inside PROTECTED_ACCESS_SETUP. If a backing page could not be paged in (file truncated, sparse hole, transient I/O error) the CRC walk hit SIGBUS and the process aborted with no recovery handler armed. Move PROTECTED_ACCESS_SETUP above the CRC check so one region covers both phases. On signal recovery, clear JOURNALFILE_FLAG_IS_AVAILABLE so the next pass rebuilds the journal -- same effect as a CRC failure. Drop the bare early-return on CRC failure, which leaked the acquire refcount, and fall through to the single journalfile_v2_data_release at the end. Mirrors the pattern in journalfile_v2_validate() at the load site. * fix(dbengine): ensure clean teardown of v2 journal on failure Add immediate unmap of fd/mmap and cleanup of njfv2idx entry when v2 journals fail during critical paths. Prevents leaving journals marked unavailable but partially initialized, which could lead to fd/mmap overwrite and duplicate index entries during rebuilds. * fix(dbengine): validate v2 journal header offsets to prevent over-read and ensure clean failure handling Add bounds checks for header-controlled offsets against the mmap size to prevent corruption-driven over-reads. Centralize teardown logic by clearing IS_AVAILABLE under a spinlock and unmapping permanently, avoiding dangling index entries or fd/mmap leaks during rebuild. * fix(dbengine): improve v2 header validation to prevent underflow and size wrapping Refine bounds checks for metric offsets and metric count in v2 journal headers. Avoid underflow and prevent size_t wrapping on 32-bit builds by reordering checks and using division instead of multiplication for capacity validation. * fix(dbengine): enhance v2 journal validation and clarify teardown logic Add stricter checks for trailer offsets to detect malformed v2 journal headers and prevent corrupted metric lists. Adjust header-controlled size bounds in CRC checks for future-proofing. Refactor IS_AVAILABLE clearing and refcount release sequence for robust teardown, avoiding dangling mappings or index entries. * protect(dbengine): safeguard v2 journal retention updates from SIGBUS Add PROTECTED_ACCESS_SETUP to safely handle mmap walks during metric retention updates. Ensure clean failure handling and resource release in case of journal access errors. * fix(dbengine): mark volatile variables in v2 journal access to ensure safety after recovery Declare uuid_first_entry_list, count, and added as volatile to maintain well-defined behavior across setjmp/longjmp in PROTECTED_ACCESS_SETUP regions. * protect(dbengine): safeguard v2 journal size statistics walk from SIGBUS Add PROTECTED_ACCESS_SETUP to handle mmap walks during the collection of size statistics. Ensure clean failure handling and resource release in case of journal access errors. * fix(dbengine): initialize `has_references` to false in v2 journal setup Ensure `has_references` is explicitly set to false during v2 journal initialization for consistent reference tracking. * protect(dbengine): validate v2 journal bounds during size stats walk Add detailed bounds checks for extent and metric lists in v2 journals to prevent over-reads and ensure safe traversal. Enhance failure handling with consistent resource release on invalid mappings. * fix(dbengine): use uint8_t* for v2 journal byte arithmetic populate_v2_statistics() declared data_start as void* and walked the mmap'd v2 journal with `(void *)(data_start + offset)` pointer arithmetic. Pointer arithmetic on void* is a GCC extension, undefined under standards-strict semantics, and CodeQL's cpp/suspicious-pointer-scaling-void rule flags every such site (alerts 3315, 3317-3320 on this PR). Declaring data_start as uint8_t* makes the arithmetic well-defined (1 byte per step, exactly what the GCC void* extension produced) and silences the 5 alerts at no semantic cost. The existing (void *) casts on the rvalues compile unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dbengine): mark `journal_access_failed` as volatile and extend bounds checks Declare `journal_access_failed` as volatile to ensure safety in recovery scenarios and prevent future invariance breaks. Add checks to avoid overflow in uuid entry list size computations during v2 journal setup. * protect(dbengine): isolate UUID stack copies to prevent SIGBUS during mmap access Safeguard `journalfile` and `rrdengine` operations by copying UUIDs to the stack before invoking mrg functions. Ensures clean recovery in protected regions on unreadable backing pages and avoids lock state corruption during SIGBUS errors. * fix(dbengine): improve v2 header validation and clarify static analysis intent Enhance error logging for out-of-range v2 journal header offsets, enabling clearer diagnostics during rebuild scenarios. Add a comment clarifying tautological bounds for `descr->type` to assist static analyzers. * protect(dbengine): scope protected frame tightly in v2 journal retention updates Reduce nesting depth and mask of unrelated faults by tightly scoping the PROTECTED_ACCESS_SETUP frame to mmap walks only. Ensure proper cleanup and avoid inflated nesting in subsequent operations. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Stelios Fragkakis committed Jun 9, 2026 at 13:20 UTC b5e92cb4862c5034c57e84888294d14f71b70f56
3 files changed +301 -90
src/database/engine/journalfile.c
+112 -34
@@ -497,6 +497,7 @@ static void journalfile_v2_data_unmap_permanently(struct rrdengine_journalfile *
497 journalfile->v2.first_time_s = 0;
498 journalfile->v2.last_time_s = 0;
499 journalfile->v2.flags = 0;
500 + has_references = false;
501 }
502 else {
503 has_references = true;
@@ -1052,52 +1053,129 @@ void journalfile_v2_populate_retention_to_mrg(struct rrdengine_instance *ctx, st
1053
1054 uint8_t *data_start = (uint8_t *)j2_header;
1055
1055 - if (journalfile->v2.flags & JOURNALFILE_FLAG_METRIC_CRC_CHECK) {
1056 - journalfile->v2.flags &= ~JOURNALFILE_FLAG_METRIC_CRC_CHECK;
1057 - if (journalfile_check_v2_metric_list(data_start, j2_header->journal_v2_file_size)) {
1058 - journalfile->v2.flags &= ~JOURNALFILE_FLAG_IS_AVAILABLE;
1059 - // needs rebuild
1060 - return;
1061 - }
1062 - }
1063 -
1056 char path_v2[RRDENG_PATH_MAX];
1057 journalfile_v2_generate_path(journalfile->datafile, path_v2, sizeof(path_v2));
1066 - time_t global_first_time_s;
1058 + time_t global_first_time_s = 0;
1059 bool failed = false;
1068 - uint32_t entries;
1060 + uint32_t entries = 0;
1061 // Calculate number of samples here and update once the file is loaded
1062 uint64_t journal_samples = 0;
1071 - PROTECTED_ACCESS_SETUP(data_start, journalfile->mmap.size, path_v2, "mrg-load");
1063 +
1064 + // Protect the whole walk -- both the optional CRC check and the mrg update
1065 + // read into the mmap'd v2 journal. If a backing page cannot be paged in
1066 + // (file truncated, sparse hole, transient I/O error) the kernel raises
1067 + // SIGBUS; without a protected region active the process aborts. Same
1068 + // pattern as journalfile_v2_validate() in journalfile_v2_load().
1069 + PROTECTED_ACCESS_SETUP(journalfile->mmap.data, journalfile->mmap.size, path_v2, "mrg-load");
1070 if(no_signal_received) {
1073 - entries = j2_header->metric_count;
1074 - struct journal_metric_list *metric = (struct journal_metric_list *) (data_start + j2_header->metric_offset);
1075 - time_t header_start_time_s = (time_t) (j2_header->start_time_ut / USEC_PER_SEC);
1076 - global_first_time_s = header_start_time_s;
1077 - time_t now_s = max_acceptable_collected_time();
1078 - for (size_t i=0; i < entries; i++) {
1079 - time_t start_time_s = header_start_time_s + metric->delta_start_s;
1080 - time_t end_time_s = header_start_time_s + metric->delta_end_s;
1081 -
1082 - mrg_update_metric_retention_and_granularity_by_uuid(
1083 - main_mrg,
1084 - (Word_t)ctx,
1085 - &metric->uuid,
1086 - start_time_s,
1087 - end_time_s,
1088 - metric->update_every_s,
1089 - now_s,
1090 - &journal_samples);
1091 -
1092 - metric++;
1071 + // Validate header-controlled offsets against the actual mapping size
1072 + // BEFORE reading through them. PROTECTED_ACCESS_SETUP only catches
1073 + // faults whose address falls within [data_start, data_start+mmap.size);
1074 + // an over-read driven by a corrupted offset past the registered range
1075 + // would not be recovered and the process would still abort. Out-of-
1076 + // range offsets are treated as a rebuild trigger (same outcome as a
1077 + // CRC failure).
1078 + size_t mmap_size = journalfile->mmap.size;
1079 + // Order matters: short-circuit on metric_offset > mmap_size first so the
1080 + // subtraction below cannot underflow, then compare metric_count against
1081 + // the available slot capacity via division rather than multiplying out
1082 + // metric_count * sizeof(...), which would wrap size_t on 32-bit builds
1083 + // and silently pass a malformed header. Only after those two checks is
1084 + // metric_count * sizeof(...) known to fit in size_t, which lets the
1085 + // trailer-ordering check below compute the expected trailer offset
1086 + // safely. The on-disk layout (see journalfile_migrate_to_v2_callback)
1087 + // places the metric-list trailer immediately after the metric list, so
1088 + // any deviation indicates a corrupted header that would otherwise let
1089 + // the CRC compare against bytes inside the metric list itself.
1090 + if ((size_t)j2_header->metric_offset > mmap_size ||
1091 + (size_t)j2_header->metric_count > (mmap_size - (size_t)j2_header->metric_offset) / sizeof(struct journal_metric_list) ||
1092 + (size_t)j2_header->metric_trailer_offset > mmap_size ||
1093 + mmap_size - (size_t)j2_header->metric_trailer_offset < sizeof(struct journal_v2_block_trailer) ||
1094 + (size_t)j2_header->metric_trailer_offset != (size_t)j2_header->metric_offset + (size_t)j2_header->metric_count * sizeof(struct journal_metric_list)) {
1095 + // header offsets out of range -- needs rebuild
1096 + nd_log_daemon(NDLP_ERR,
1097 + "DBENGINE: journal v2 \"%s\" has out-of-range header offsets "
1098 + "(metric_offset=%u, metric_count=%u, metric_trailer_offset=%u, mmap_size=%zu); "
1099 + "marking unavailable for rebuild",
1100 + path_v2,
1101 + j2_header->metric_offset,
1102 + j2_header->metric_count,
1103 + j2_header->metric_trailer_offset,
1104 + mmap_size);
1105 + failed = true;
1106 + }
1107 + else if (journalfile->v2.flags & JOURNALFILE_FLAG_METRIC_CRC_CHECK) {
1108 + journalfile->v2.flags &= ~JOURNALFILE_FLAG_METRIC_CRC_CHECK;
1109 + // Pass the verified mmap_size, not the header-controlled
1110 + // j2_header->journal_v2_file_size; the helper currently ignores the
1111 + // size argument (UNUSED) but the value at the call site should
1112 + // still reflect the trusted bound for clarity and future-proofing.
1113 + if (journalfile_check_v2_metric_list(data_start, mmap_size)) {
1114 + // needs rebuild
1115 + failed = true;
1116 + }
1117 + }
1118 +
1119 + if (!failed) {
1120 + entries = j2_header->metric_count;
1121 + struct journal_metric_list *metric = (struct journal_metric_list *) (data_start + j2_header->metric_offset);
1122 + time_t header_start_time_s = (time_t) (j2_header->start_time_ut / USEC_PER_SEC);
1123 + global_first_time_s = header_start_time_s;
1124 + time_t now_s = max_acceptable_collected_time();
1125 + for (size_t i=0; i < entries; i++) {
1126 + // Copy uuid out of the mmap onto the stack BEFORE calling mrg.
1127 + // If a backing page is unreadable, uuid_copy SIGBUSes here and
1128 + // the protected region recovers cleanly; the mrg call then
1129 + // never executes. If we passed &metric->uuid into mrg, a
1130 + // SIGBUS could fire INSIDE mrg while it holds internal locks,
1131 + // and siglongjmp would skip mrg's unlock paths.
1132 + nd_uuid_t local_uuid;
1133 + uuid_copy(local_uuid, metric->uuid);
1134 + time_t start_time_s = header_start_time_s + metric->delta_start_s;
1135 + time_t end_time_s = header_start_time_s + metric->delta_end_s;
1136 + uint32_t update_every_s = metric->update_every_s;
1137 +
1138 + mrg_update_metric_retention_and_granularity_by_uuid(
1139 + main_mrg,
1140 + (Word_t)ctx,
1141 + &local_uuid,
1142 + start_time_s,
1143 + end_time_s,
1144 + update_every_s,
1145 + now_s,
1146 + &journal_samples);
1147 +
1148 + metric++;
1149 + }
1150 }
1094 - } else
1151 + }
1152 + else {
1153 + // SIGBUS/SIGSEGV inside the mmap walk. The PROTECTED_ACCESS_SETUP
1154 + // macro already rate-limits the error log.
1155 failed = true;
1156 + }
1157 +
1158 + if (unlikely(failed)) {
1159 + // Clear IS_AVAILABLE under the data spinlock BEFORE releasing our
1160 + // refcount, so no concurrent journalfile_v2_data_acquire_with_hint()
1161 + // can succeed and walk the (potentially corrupted) mmap between here
1162 + // and the permanent unmap. The bounds, CRC, and signal-recovery paths
1163 + // all converge through this single transition; without it, teardown
1164 + // (journalfile_close / journalfile_destroy_unsafe) would gate on
1165 + // IS_AVAILABLE and skip journalfile_v2_data_unmap_permanently(),
1166 + // leaving a dangling njfv2idx entry, an unclosed fd, and an unmapped
1167 + // region.
1168 + spinlock_lock(&journalfile->data_spinlock);
1169 + journalfile->v2.flags &= ~JOURNALFILE_FLAG_IS_AVAILABLE;
1170 + spinlock_unlock(&journalfile->data_spinlock);
1171 + }
1172
1173 journalfile_v2_data_release(journalfile);
1174
1099 - if (unlikely(failed))
1175 + if (unlikely(failed)) {
1176 + journalfile_v2_data_unmap_permanently(journalfile);
1177 return;
1178 + }
1179
1180 __atomic_add_fetch(&ctx->atomic.samples, journal_samples, __ATOMIC_RELAXED);
1181
src/database/engine/rrdengine.c
+95 -15
@@ -1387,24 +1387,96 @@ static void update_metrics_first_time_s(struct rrdengine_instance *ctx, struct r
1387
1388 __atomic_add_fetch(&rrdeng_cache_efficiency_stats.metrics_retention_started, 1, __ATOMIC_RELAXED);
1389
1390 - struct journal_metric_list *uuid_list = (struct journal_metric_list *)((uint8_t *) j2_header + j2_header->metric_offset);
1390 + char file_path[RRDENG_PATH_MAX];
1391 + journalfile_v2_generate_path(datafile_to_delete, file_path, sizeof(file_path));
1392
1392 - size_t count = j2_header->metric_count;
1393 struct uuid_first_time_s *uuid_first_t_entry;
1394 - struct uuid_first_time_s *uuid_first_entry_list = callocz(count, sizeof(struct uuid_first_time_s));
1395 -
1396 - size_t added = 0;
1397 - for (size_t index = 0; index < count; ++index) {
1398 - METRIC *metric = mrg_metric_get_and_acquire_by_uuid(main_mrg, &uuid_list[index].uuid, (Word_t)ctx);
1399 - if (!metric)
1400 - continue;
1394 + // PROTECTED_ACCESS_SETUP below uses sigsetjmp/siglongjmp (see
1395 + // src/daemon/protected-access.h). Per C11 7.13.2.1, non-volatile locals
1396 + // that are modified between setjmp and longjmp have indeterminate values
1397 + // on the recovery path. uuid_first_entry_list / count / added are all
1398 + // mutated inside the protected region and then read afterwards (the
1399 + // unconditional log line and the journal_access_failed cleanup loop), so
1400 + // they must be volatile to keep the recovery path well-defined.
1401 + // journal_access_failed is also marked volatile defensively: although it
1402 + // is only assigned on a path that does not subsequently SIGBUS, future
1403 + // edits could break that invariant, and the bool is read once on cleanup.
1404 + struct uuid_first_time_s * volatile uuid_first_entry_list = NULL;
1405 + volatile size_t count = 0;
1406 + volatile size_t added = 0;
1407 + volatile bool journal_access_failed = false;
1408 +
1409 + // Protect the mmap walk: reading j2_header->metric_offset, metric_count,
1410 + // and the per-metric uuid_list[] entries can SIGBUS if the underlying v2
1411 + // file has any unreadable page (truncated, sparse hole, transient I/O
1412 + // error). Without a protected region active the process aborts. Same
1413 + // pattern as find_uuid_first_time() added by commit 26b26ac25a (#22310);
1414 + // this caller was missed at the time.
1415 + // Scope the protected frame tightly to the mmap walk only. The
1416 + // PROTECTED_ACCESS_AUTO_CLEANUP() guard inside PROTECTED_ACCESS_SETUP
1417 + // declares a __attribute__((cleanup)) local; when this inner block exits
1418 + // (normally or via the SIGBUS recovery else-branch falling through), the
1419 + // cleanup runs and the protected-access depth drops back to its prior
1420 + // value. Without this scoping, the frame would stay live through the
1421 + // post-walk log, the data_release, the cleanup loop, the nested
1422 + // find_uuid_first_time() (which registers its own frame), and the final
1423 + // cleanup -- masking unrelated faults that might land in the mmap range
1424 + // and inflating nesting depth unnecessarily.
1425 + {
1426 + PROTECTED_ACCESS_SETUP(journalfile->mmap.data, journalfile->mmap.size, file_path, "mrg-retention");
1427 + if(no_signal_received) {
1428 + size_t journal_v2_file_size = journalfile->mmap.size;
1429 + size_t metric_offset = j2_header->metric_offset;
1430 + count = j2_header->metric_count;
1431 + size_t metric_list_size;
1432 + size_t entry_list_size;
1433 + // Also check count * sizeof(uuid_first_time_s) -- the allocation below
1434 + // sizes the working array by count, and on 32-bit builds count can
1435 + // pass the metric_list_size bound while still overflowing the entry
1436 + // list multiplication (struct uuid_first_time_s is larger than
1437 + // struct journal_metric_list).
1438 + if (__builtin_mul_overflow(count, sizeof(struct journal_metric_list), &metric_list_size) ||
1439 + __builtin_mul_overflow(count, sizeof(struct uuid_first_time_s), &entry_list_size) ||
1440 + metric_offset > journal_v2_file_size ||
1441 + metric_list_size > journal_v2_file_size - metric_offset) {
1442 + nd_log_daemon(NDLP_ERR,
1443 + "DBENGINE: metric list exceeds journal file size in journalfile \"%s\" "
1444 + "(metric_offset=%zu, list_size=%zu, file_size=%zu), skipping retention update",
1445 + file_path, metric_offset, metric_list_size, journal_v2_file_size);
1446 + journal_access_failed = true;
1447 + }
1448 + else {
1449 + struct journal_metric_list *uuid_list = (struct journal_metric_list *)((uint8_t *) j2_header + metric_offset);
1450 + uuid_first_entry_list = callocz(count, sizeof(struct uuid_first_time_s));
1451 +
1452 + for (size_t index = 0; index < count; ++index) {
1453 + // Copy uuid out of the mmap onto the stack BEFORE calling mrg.
1454 + // If a backing page is unreadable, uuid_copy SIGBUSes here and
1455 + // the protected region recovers cleanly; the mrg call then
1456 + // never executes. If we passed &uuid_list[index].uuid into
1457 + // mrg, a SIGBUS could fire INSIDE mrg while it holds internal
1458 + // locks, and siglongjmp would skip mrg's unlock paths.
1459 + nd_uuid_t local_uuid;
1460 + uuid_copy(local_uuid, uuid_list[index].uuid);
1461 +
1462 + METRIC *metric = mrg_metric_get_and_acquire_by_uuid(main_mrg, &local_uuid, (Word_t)ctx);
1463 + if (!metric)
1464 + continue;
1465
1402 - uuid_first_entry_list[added].metric = metric;
1403 - uuid_first_entry_list[added].first_time_s = LONG_MAX;
1404 - uuid_first_entry_list[added].df_matched = 0;
1405 - uuid_first_entry_list[added].df_index_oldest = 0;
1406 - uuid_first_entry_list[added].uuid = mrg_metric_uuid(main_mrg, metric);
1407 - added++;
1466 + uuid_first_entry_list[added].metric = metric;
1467 + uuid_first_entry_list[added].first_time_s = LONG_MAX;
1468 + uuid_first_entry_list[added].df_matched = 0;
1469 + uuid_first_entry_list[added].df_index_oldest = 0;
1470 + uuid_first_entry_list[added].uuid = mrg_metric_uuid(main_mrg, metric);
1471 + added++;
1472 + }
1473 + }
1474 + }
1475 + else {
1476 + // SIGBUS/SIGSEGV inside the mmap walk -- bail cleanly. The
1477 + // PROTECTED_ACCESS_SETUP macro already rate-limits the error log.
1478 + journal_access_failed = true;
1479 + }
1480 }
1481
1482 netdata_log_info(
@@ -1415,6 +1487,14 @@ static void update_metrics_first_time_s(struct rrdengine_instance *ctx, struct r
1487
1488 journalfile_v2_data_release(journalfile);
1489
1490 + if (unlikely(journal_access_failed)) {
1491 + // Release any partially-acquired metrics; uuid_first_entry_list may
1492 + // be NULL (signal received before callocz).
1493 + for (size_t index = 0; index < added; ++index)
1494 + mrg_metric_release(main_mrg, uuid_first_entry_list[index].metric);
1495 + goto done;
1496 + }
1497 +
1498 // Update the first time / last time for all metrics we plan to delete
1499
1500 if(worker)
src/database/engine/rrdengineapi.c
+94 -41
@@ -1293,68 +1293,121 @@ void rrdeng_quiesce(struct rrdengine_instance *ctx)
1293 static void populate_v2_statistics(struct rrdengine_datafile *datafile, RRDENG_SIZE_STATS *stats)
1294 {
1295 struct journal_v2_header *j2_header = journalfile_v2_data_acquire(datafile->journalfile, NULL, 0, 0);
1296 - void *data_start = (void *)j2_header;
1296 + uint8_t *data_start = (uint8_t *)j2_header;
1297
1298 if(unlikely(!j2_header))
1299 return;
1300
1301 - stats->extents += j2_header->extent_count;
1301 + char file_path[RRDENG_PATH_MAX];
1302 + journalfile_v2_generate_path(datafile, file_path, sizeof(file_path));
1303 +
1304 + // Protect the mmap walk: every j2_header->*, extent_list, metric, and
1305 + // descr access reads the mmap'd v2 journal. If the underlying file has
1306 + // any unreadable page (truncated, sparse hole, transient I/O error), the
1307 + // walk SIGBUSes and the process aborts. Same pattern as the sister sites
1308 + // (populate_retention_to_mrg, find_uuid_first_time, update_metrics_first_time_s).
1309 + // Partial accumulator increments on signal recovery are acceptable:
1310 + // size statistics are best-effort across datafiles, the caller continues
1311 + // to the next datafile in rrdeng_size_statistics().
1312 + PROTECTED_ACCESS_SETUP(datafile->journalfile->mmap.data, datafile->journalfile->mmap.size, file_path, "size-stats");
1313 + if(no_signal_received) {
1314 + size_t mmap_size = datafile->journalfile->mmap.size;
1315 +
1316 + // Bounds-check the extent list array against the mapping size before
1317 + // walking through header-controlled offsets. PROTECTED_ACCESS_SETUP
1318 + // only catches faults in [data_start, data_start+mmap_size); a
1319 + // corrupted offset that points past mmap_size would over-read into
1320 + // unrelated memory and abort. Same idiom as the bounds check in
1321 + // journalfile_v2_populate_retention_to_mrg.
1322 + bool extents_in_bounds = (size_t)j2_header->extent_offset <= mmap_size &&
1323 + (size_t)j2_header->extent_count <= (mmap_size - (size_t)j2_header->extent_offset) / sizeof(struct journal_extent_list);
1324 + if (extents_in_bounds) {
1325 + stats->extents += j2_header->extent_count;
1326 +
1327 + struct journal_extent_list *extent_list = (void *) (data_start + j2_header->extent_offset);
1328 + for (unsigned entries = 0; entries < j2_header->extent_count; entries++) {
1329 + stats->extents_compressed_bytes += extent_list->datafile_size;
1330 + stats->extents_pages += extent_list->pages;
1331 + extent_list++;
1332 + }
1333 + }
1334
1303 - unsigned entries;
1304 - struct journal_extent_list *extent_list = (void *) (data_start + j2_header->extent_offset);
1305 - for (entries = 0; entries < j2_header->extent_count; entries++) {
1306 - stats->extents_compressed_bytes += extent_list->datafile_size;
1307 - stats->extents_pages += extent_list->pages;
1308 - extent_list++;
1309 - }
1335 + bool metrics_in_bounds = (size_t)j2_header->metric_offset <= mmap_size &&
1336 + (size_t)j2_header->metric_count <= (mmap_size - (size_t)j2_header->metric_offset) / sizeof(struct journal_metric_list);
1337 + if (!metrics_in_bounds)
1338 + goto release;
1339
1311 - struct journal_metric_list *metric = (void *) (data_start + j2_header->metric_offset);
1312 - time_t journal_start_time_s = (time_t) (j2_header->start_time_ut / USEC_PER_SEC);
1340 + struct journal_metric_list *metric = (void *) (data_start + j2_header->metric_offset);
1341 + time_t journal_start_time_s = (time_t) (j2_header->start_time_ut / USEC_PER_SEC);
1342
1314 - stats->metrics += j2_header->metric_count;
1315 - for (entries = 0; entries < j2_header->metric_count; entries++) {
1343 + stats->metrics += j2_header->metric_count;
1344 + for (unsigned entries = 0; entries < j2_header->metric_count; entries++) {
1345
1317 - struct journal_page_header *metric_list_header = (void *) (data_start + metric->page_offset);
1318 - stats->metrics_pages += metric_list_header->entries;
1319 - struct journal_page_list *descr = (void *) (data_start + metric->page_offset + sizeof(struct journal_page_header));
1320 - for (uint32_t idx=0; idx < metric_list_header->entries; idx++) {
1346 + // Per-metric: page_offset is header-controlled. Validate it points
1347 + // into the mapping and that there is room for the page_header AND
1348 + // its trailing page_list[] before dereferencing through it.
1349 + if ((size_t)metric->page_offset > mmap_size ||
1350 + mmap_size - (size_t)metric->page_offset < sizeof(struct journal_page_header)) {
1351 + metric++;
1352 + continue;
1353 + }
1354
1322 - time_t update_every_s;
1355 + struct journal_page_header *metric_list_header = (void *) (data_start + metric->page_offset);
1356
1324 - size_t points = descr->page_length / CTX_POINT_SIZE_BYTES(datafile_ctx(datafile));
1357 + size_t page_list_room = mmap_size - (size_t)metric->page_offset - sizeof(struct journal_page_header);
1358 + if ((size_t)metric_list_header->entries > page_list_room / sizeof(struct journal_page_list)) {
1359 + metric++;
1360 + continue;
1361 + }
1362
1326 - time_t start_time_s = journal_start_time_s + descr->delta_start_s;
1327 - time_t end_time_s = journal_start_time_s + descr->delta_end_s;
1363 + stats->metrics_pages += metric_list_header->entries;
1364 + struct journal_page_list *descr = (void *) (data_start + metric->page_offset + sizeof(struct journal_page_header));
1365 + for (uint32_t idx=0; idx < metric_list_header->entries; idx++) {
1366
1329 - if(likely(points > 1))
1330 - update_every_s = (time_t) ((end_time_s - start_time_s) / (points - 1));
1331 - else {
1332 - update_every_s = (time_t) (nd_profile.update_every * get_tier_grouping(datafile_ctx(datafile)->config.tier));
1333 - stats->single_point_pages++;
1334 - }
1367 + time_t update_every_s;
1368
1336 - time_t duration_s = (time_t)((end_time_s - start_time_s + update_every_s));
1369 + size_t points = descr->page_length / CTX_POINT_SIZE_BYTES(datafile_ctx(datafile));
1370
1338 - stats->pages_uncompressed_bytes += descr->page_length;
1339 - stats->pages_duration_secs += duration_s;
1340 - stats->points += points;
1371 + time_t start_time_s = journal_start_time_s + descr->delta_start_s;
1372 + time_t end_time_s = journal_start_time_s + descr->delta_end_s;
1373
1342 - stats->page_types[descr->type].pages++;
1343 - stats->page_types[descr->type].pages_uncompressed_bytes += descr->page_length;
1344 - stats->page_types[descr->type].pages_duration_secs += duration_s;
1345 - stats->page_types[descr->type].points += points;
1374 + if(likely(points > 1))
1375 + update_every_s = (time_t) ((end_time_s - start_time_s) / (points - 1));
1376 + else {
1377 + update_every_s = (time_t) (nd_profile.update_every * get_tier_grouping(datafile_ctx(datafile)->config.tier));
1378 + stats->single_point_pages++;
1379 + }
1380 +
1381 + time_t duration_s = (time_t)((end_time_s - start_time_s + update_every_s));
1382
1347 - if(!stats->first_time_s || (start_time_s - update_every_s) < stats->first_time_s)
1348 - stats->first_time_s = (start_time_s - update_every_s);
1383 + stats->pages_uncompressed_bytes += descr->page_length;
1384 + stats->pages_duration_secs += duration_s;
1385 + stats->points += points;
1386
1350 - if(!stats->last_time_s || end_time_s > stats->last_time_s)
1351 - stats->last_time_s = end_time_s;
1387 + // descr->type is uint8_t (range [0, 255]); page_types is sized
1388 + // [256]. The index is bounded by the type width, so no runtime
1389 + // check is needed -- a `descr->type < 256` guard would be a
1390 + // tautology. Note for static analyzers flagging this site.
1391 + stats->page_types[descr->type].pages++;
1392 + stats->page_types[descr->type].pages_uncompressed_bytes += descr->page_length;
1393 + stats->page_types[descr->type].pages_duration_secs += duration_s;
1394 + stats->page_types[descr->type].points += points;
1395
1353 - descr++;
1396 + if(!stats->first_time_s || (start_time_s - update_every_s) < stats->first_time_s)
1397 + stats->first_time_s = (start_time_s - update_every_s);
1398 +
1399 + if(!stats->last_time_s || end_time_s > stats->last_time_s)
1400 + stats->last_time_s = end_time_s;
1401 +
1402 + descr++;
1403 + }
1404 + metric++;
1405 }
1355 - metric++;
1406 }
1407 + // On SIGBUS/SIGSEGV the PROTECTED_ACCESS_SETUP macro already
1408 + // rate-limits the error log; fall through to release the journal.
1409
1410 +release:
1411 journalfile_v2_data_release(datafile->journalfile);
1412 }
1413