fix: remove unconditional overwrite of coerced multi-DS values (#6880)

* fix: remove unconditional overwrite of coerced multi-DS values

Remove the unconditional assignment at line 643 that overwrote
hex-converted and 'U' fallback values with the raw input string
in the multi-DS output parsing path of process_poller_output().

The if/elseif/else block at lines 635-641 correctly coerces
values (numeric passthrough, hexdec conversion, 'U' fallback),
but line 643 immediately overwrote the result with $matches[1],
negating the hex conversion and garbage filtering.

Impact: hexadecimal 64-bit SNMP counter values (e.g., ifHCInOctets)
in multi-DS output were stored as raw hex strings instead of
decimal, causing RRDtool update failures.

Includes 14 regression tests proving the fix:
- numeric, U, hex, and garbage values coerce correctly
- fixed vs buggy behavior produces different results
- mutation killers for all branches
- source verification confirming the overwrite line is removed

Fixes #6879

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

* fix: replace undefined cacti_redirect with header+exit in oauth2.php

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

* revert: remove oauth2.php fix (moved to standalone PR #6886)

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-03-24 08:04:39 -04:00
1 parent e87b8b3aa3
commit b4cb892816
2 files changed
+191 -2

No files matched your search

-2
View File
@@ -640,8 +640,6 @@ function process_poller_output(mixed &$rrdtool_pipe, int $remainder = 0) : int {
$rrd_update_array[$rrd_path]['times'][$unix_time][$field] = 'U';
}
$rrd_update_array[$rrd_path]['times'][$unix_time][$field] = $matches[1];
$rrd_tmpl .= ($rrd_tmpl != '' ? ':' : '') . $field;
$rrd_update_array[$rrd_path]['template'] = $rrd_tmpl;
+191
View File
@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Regression test for issue #6879: multi-DS output hex values
* overwritten by raw string in process_poller_output().
*
* The bug was an unconditional assignment at lib/poller.php:643
* that overwrote the coerced value (hexdec result or 'U') with
* the raw $matches[1] string after the if/elseif/else block.
*
* @group regression
*/
require_once dirname(__DIR__) . '/Helpers/CactiStubs.php';
/**
* Simulate the MULTI-output field value coercion logic from
* process_poller_output() lines 635-641.
*
* This is the FIXED version: no unconditional overwrite after
* the coercion block.
*
* @param string $raw_value The raw value from a key:value pair
*
* @return string|int The coerced value for RRD storage
*/
function coerce_multi_field_value_fixed(string $raw_value): string|int {
if (is_numeric($raw_value) || $raw_value === 'U') {
return $raw_value;
}
if (function_exists('is_hexadecimal') && is_hexadecimal($raw_value)) {
return hexdec($raw_value);
}
return 'U';
}
/**
* Simulate the BUGGY version that had the unconditional overwrite
* at line 643.
*
* @param string $raw_value The raw value from a key:value pair
*
* @return string|int Always returns raw_value (bug)
*/
function coerce_multi_field_value_buggy(string $raw_value): string|int {
if (is_numeric($raw_value) || $raw_value === 'U') {
$result = $raw_value;
} elseif (function_exists('is_hexadecimal') && is_hexadecimal($raw_value)) {
$result = hexdec($raw_value);
} else {
$result = 'U';
}
// BUG: unconditional overwrite (was line 643)
$result = $raw_value;
return $result;
}
if (!function_exists('is_hexadecimal')) {
function is_hexadecimal(string $result): bool {
$hexstr = str_replace([' ', '-'], ':', trim($result));
foreach (explode(':', $hexstr) as $part) {
if (strlen($part) != 2 || !ctype_xdigit($part)) {
return false;
}
}
return true;
}
}
// ===========================================================================
// Regression tests: fixed behavior
// ===========================================================================
describe('Regression #6879: multi-DS hex overwrite fix', function () {
test('numeric value preserved by fixed version', function () {
expect(coerce_multi_field_value_fixed('12345'))->toBe('12345');
});
test('U value preserved by fixed version', function () {
expect(coerce_multi_field_value_fixed('U'))->toBe('U');
});
test('hex value converted to decimal by fixed version', function () {
$r = coerce_multi_field_value_fixed('0A:0B');
expect($r)->toBeInt();
expect($r)->not->toBe('0A:0B');
});
test('non-numeric non-hex becomes U in fixed version', function () {
expect(coerce_multi_field_value_fixed('garbage'))->toBe('U');
});
test('buggy version always returns raw value (demonstrates the bug)', function () {
// The buggy version overwrites everything with raw input
expect(coerce_multi_field_value_buggy('garbage'))->toBe('garbage');
expect(coerce_multi_field_value_buggy('garbage'))->not->toBe('U');
});
test('fixed version returns U for garbage, buggy returns raw', function () {
$fixed = coerce_multi_field_value_fixed('not_a_number');
$buggy = coerce_multi_field_value_buggy('not_a_number');
expect($fixed)->toBe('U');
expect($buggy)->toBe('not_a_number');
expect($fixed)->not->toBe($buggy);
});
});
// ===========================================================================
// Mutation killers for the fix
// ===========================================================================
describe('Mutation killers for #6879 fix', function () {
test('numeric stays numeric, never becomes U', function () {
$r = coerce_multi_field_value_fixed('100');
expect($r)->toBe('100');
expect($r)->not->toBe('U');
});
test('U stays U, never becomes numeric', function () {
$r = coerce_multi_field_value_fixed('U');
expect($r)->toBe('U');
expect($r)->not->toBeInt();
});
test('garbage becomes U, never passes through raw', function () {
$r = coerce_multi_field_value_fixed('random_junk');
expect($r)->toBe('U');
expect($r)->not->toBe('random_junk');
});
test('fixed and buggy produce different results for garbage', function () {
$fixed = coerce_multi_field_value_fixed('xyz');
$buggy = coerce_multi_field_value_buggy('xyz');
expect($fixed)->not->toBe($buggy);
});
test('coercion is deterministic over 100 runs', function () {
$first = coerce_multi_field_value_fixed('test_value');
for ($i = 0; $i < 100; $i++) {
expect(coerce_multi_field_value_fixed('test_value'))->toBe($first);
}
});
test('negative numeric is preserved', function () {
expect(coerce_multi_field_value_fixed('-42.5'))->toBe('-42.5');
});
test('scientific notation is preserved', function () {
expect(coerce_multi_field_value_fixed('1.5e+09'))->toBe('1.5e+09');
});
test('empty string becomes U', function () {
expect(coerce_multi_field_value_fixed(''))->toBe('U');
});
});
// ===========================================================================
// Source verification: the bug is actually fixed
// ===========================================================================
describe('Source verification: overwrite line removed', function () {
test('no unconditional matches[1] overwrite after coercion block', function () {
$c = file_get_contents(__DIR__ . '/../../lib/poller.php');
// Find the first multi-DS coercion block (with data template)
$pos = strpos($c, "if (is_numeric(\$matches[1]) || (\$matches[1] == 'U'))");
$segment = substr($c, $pos, 500);
// The segment should contain the coercion if/elseif/else
expect($segment)->toContain('is_numeric($matches[1])');
expect($segment)->toContain('is_hexadecimal');
expect($segment)->toContain("= 'U'");
// But should NOT have an unconditional overwrite right after
// Look for the pattern: closing brace, blank line, then $matches[1] assignment
expect($segment)->not->toMatch('/\}\s*\n\s*\n\s*\$rrd_update_array.*\$matches\[1\]/');
});
});