Files
Thomas VincentandTheWitness d3d99bb08a test(cdef): enforce resolver coverage (#7974)
* test(cdef): enforce resolver coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci(cdef): pin cache action correctly

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: isolate CDEF coverage bootstrap

* fix(cdef): fail closed on invalid recursive definitions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): log corrupt resolver records

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): bound and isolate recursive resolution

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): preserve resolver failure state

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): isolate invalid graph expressions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): abort unsafe consumer operations

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): close consumer failure paths

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): make aggregate updates atomic

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): keep repair queries auditable

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): bind bulk deletes and escape item types

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): guard corrupt types and repair reporting

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(aggregate): propagate push-out failures

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(aggregate): document mutable message checks

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-09-23 13:38:51 -04:00

277 lines
8.8 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/**
* Retrieves the name of a CDEF item based on its ID.
*
* This function fetches the type and value of a CDEF item from the database and returns
* the corresponding name or value based on the item's type.
*
* @param int $cdef_item_id The ID of the CDEF item.
*
* @return string|null The name/value, or null when the stored item is invalid.
*/
function get_cdef_item_name(int $cdef_item_id) : ?string {
global $cdef_functions, $cdef_operators;
$cdef_item = db_fetch_row_prepared('SELECT type, value FROM cdef_items WHERE id = ?', [$cdef_item_id]);
if (!is_array($cdef_item) || !array_key_exists('type', $cdef_item) || !array_key_exists('value', $cdef_item)) {
cacti_log(sprintf('ERROR: CDEF item %d is missing or corrupt.', $cdef_item_id), false, 'CDEF');
return null;
}
$current_cdef_value = $cdef_item['value'];
switch ($cdef_item['type']) {
case '1':
if (!isset($cdef_functions[$current_cdef_value])) {
cacti_log(sprintf('ERROR: CDEF item %d references an unknown function.', $cdef_item_id), false, 'CDEF');
return null;
}
return (string) $cdef_functions[$current_cdef_value];
case '2':
if (!isset($cdef_operators[$current_cdef_value])) {
cacti_log(sprintf('ERROR: CDEF item %d references an unknown operator.', $cdef_item_id), false, 'CDEF');
return null;
}
return (string) $cdef_operators[$current_cdef_value];
case '4':
return (string) $current_cdef_value;
case '5':
$cdef_name = db_fetch_cell_prepared('SELECT name FROM cdef WHERE id = ?', [$current_cdef_value]);
if ($cdef_name === false || $cdef_name === null || $cdef_name === '') {
cacti_log(sprintf('ERROR: CDEF item %d references a missing definition.', $cdef_item_id), false, 'CDEF');
return null;
}
return (string) $cdef_name;
case '6':
return (string) $current_cdef_value;
}
cacti_log(sprintf('ERROR: CDEF item %d has an unknown type.', $cdef_item_id), false, 'CDEF');
return null;
}
/**
* Resolves an entire CDEF into its text-based representation for use in the RRDtool 'graph'
* string. this name will be resolved recursively if necessary
*
* This function fetches the CDEF items associated with the provided CDEF ID from the database,
* constructs the CDEF string by iterating through the items, and handles nested CDEFs recursively.
*
* @param int $cdef_id The ID of the CDEF to retrieve.
*
* @return string|null The constructed CDEF string, or null on invalid data.
*/
function get_cdef(int $cdef_id) : ?string {
$visited = [];
$expansion = 0;
$cache = [];
$cache_bytes = 0;
return get_cdef_recursive($cdef_id, $visited, $expansion, $cache, $cache_bytes);
}
/**
* Resolves nested CDEFs while rejecting cycles and unreasonable depth.
*
* @param int $cdef_id The ID of the CDEF to retrieve.
* @param array<int,true> $visited IDs active in the current recursion path.
* @param int $expansion Number of definitions expanded by this call.
* @param array<int,string> $cache Successfully resolved definitions.
* @param int $cache_bytes Total bytes retained by the resolver cache.
*
* @return string|null The CDEF string, or null when recursion is unsafe.
*/
function get_cdef_recursive(int $cdef_id, array &$visited, int &$expansion, array &$cache, int &$cache_bytes) : ?string {
if (isset($visited[$cdef_id])) {
cacti_log(sprintf('ERROR: CDEF %d contains a recursive cycle.', $cdef_id), false, 'CDEF');
return null;
}
if (array_key_exists($cdef_id, $cache)) {
return $cache[$cdef_id];
}
if (count($visited) >= 64) {
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver nesting depth.', $cdef_id), false, 'CDEF');
return null;
}
if (++$expansion > 4096) {
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver expansion budget.', $cdef_id), false, 'CDEF');
return null;
}
$visited[$cdef_id] = true;
$cdef_items = db_fetch_assoc_prepared('SELECT id, type, value FROM cdef_items WHERE cdef_id = ? ORDER BY sequence', [$cdef_id]);
if ($cdef_items === false) {
unset($visited[$cdef_id]);
cacti_log(sprintf('ERROR: Unable to load CDEF %d.', $cdef_id), false, 'CDEF');
return null;
}
if (cacti_sizeof($cdef_items) === 0) {
$cdef_exists = db_fetch_assoc_prepared('SELECT id FROM cdef WHERE id = ?', [$cdef_id]);
unset($visited[$cdef_id]);
if ($cdef_exists === false) {
cacti_log(sprintf('ERROR: Unable to verify CDEF %d.', $cdef_id), false, 'CDEF');
return null;
}
if (cacti_sizeof($cdef_exists) === 0) {
cacti_log(sprintf('ERROR: CDEF %d does not exist.', $cdef_id), false, 'CDEF');
return null;
}
$cache[$cdef_id] = '';
return $cache[$cdef_id];
}
$parts = [];
$length = 0;
foreach ($cdef_items as $cdef_item) {
if ($cdef_item['type'] == 5) {
$current_cdef_id = $cdef_item['value'];
$nested = get_cdef_recursive((int) $current_cdef_id, $visited, $expansion, $cache, $cache_bytes);
if ($nested === null) {
unset($visited[$cdef_id]);
return null;
}
if ($nested === '') {
cacti_log(sprintf('ERROR: CDEF %d references an empty definition.', $cdef_id), false, 'CDEF');
unset($visited[$cdef_id]);
return null;
}
$length += strlen($nested) + ($parts === [] ? 0 : 1);
if ($length > 1048576) {
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver output budget.', $cdef_id), false, 'CDEF');
unset($visited[$cdef_id]);
return null;
}
$parts[] = $nested;
} else {
$item_name = get_cdef_item_name($cdef_item['id']);
if ($item_name === null) {
unset($visited[$cdef_id]);
return null;
}
if ($item_name === '') {
cacti_log(sprintf('ERROR: CDEF %d contains an empty item.', $cdef_id), false, 'CDEF');
unset($visited[$cdef_id]);
return null;
}
$length += strlen($item_name) + ($parts === [] ? 0 : 1);
if ($length > 1048576) {
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver output budget.', $cdef_id), false, 'CDEF');
unset($visited[$cdef_id]);
return null;
}
$parts[] = $item_name;
}
}
unset($visited[$cdef_id]);
$resolved = implode(',', $parts);
if ($cache_bytes + strlen($resolved) <= 8388608) {
$cache_bytes += strlen($resolved);
$cache[$cdef_id] = $resolved;
}
return $resolved;
}
/**
* Determines whether deleting a CDEF would leave a live reference behind.
*
* References owned by another CDEF in the same delete request are ignored.
* Database errors fail closed so an uncertain dependency cannot be deleted.
*
* @param int $cdef_id CDEF being considered for deletion.
* @param array<int> $deleting_ids All CDEF IDs in the delete request.
*/
function cdef_is_in_use(int $cdef_id, array $deleting_ids = []) : bool {
foreach (['graph_templates_item', 'aggregate_graph_templates_item', 'aggregate_graphs_graph_item'] as $table) {
$references = db_fetch_assoc_prepared("SELECT cdef_id FROM $table WHERE cdef_id = ? LIMIT 1", [$cdef_id]);
if ($references === false || cacti_sizeof($references) > 0) {
return true;
}
}
$parents = db_fetch_assoc_prepared('SELECT DISTINCT cdef_id FROM cdef_items WHERE type = 5 AND value = ?', [$cdef_id]);
if ($parents === false) {
return true;
}
$deleting = array_fill_keys(array_map('intval', $deleting_ids), true);
foreach ($parents as $parent) {
if (!isset($deleting[(int) $parent['cdef_id']])) {
return true;
}
}
return false;
}