Skip to content

Commit b4cb892

Browse files
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>
1 parent e87b8b3 commit b4cb892

File tree

2 files changed

+191
-2
lines changed

2 files changed

+191
-2
lines changed

lib/poller.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,8 +640,6 @@ function process_poller_output(mixed &$rrdtool_pipe, int $remainder = 0) : int {
640640
$rrd_update_array[$rrd_path]['times'][$unix_time][$field] = 'U';
641641
}
642642

643-
$rrd_update_array[$rrd_path]['times'][$unix_time][$field] = $matches[1];
644-
645643
$rrd_tmpl .= ($rrd_tmpl != '' ? ':' : '') . $field;
646644

647645
$rrd_update_array[$rrd_path]['template'] = $rrd_tmpl;
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
declare(strict_types=1);
3+
/*
4+
+-------------------------------------------------------------------------+
5+
| Copyright (C) 2004-2026 The Cacti Group |
6+
+-------------------------------------------------------------------------+
7+
| Cacti: The Complete RRDtool-based Graphing Solution |
8+
+-------------------------------------------------------------------------+
9+
*/
10+
11+
/*
12+
* Regression test for issue #6879: multi-DS output hex values
13+
* overwritten by raw string in process_poller_output().
14+
*
15+
* The bug was an unconditional assignment at lib/poller.php:643
16+
* that overwrote the coerced value (hexdec result or 'U') with
17+
* the raw $matches[1] string after the if/elseif/else block.
18+
*
19+
* @group regression
20+
*/
21+
22+
require_once dirname(__DIR__) . '/Helpers/CactiStubs.php';
23+
24+
/**
25+
* Simulate the MULTI-output field value coercion logic from
26+
* process_poller_output() lines 635-641.
27+
*
28+
* This is the FIXED version: no unconditional overwrite after
29+
* the coercion block.
30+
*
31+
* @param string $raw_value The raw value from a key:value pair
32+
*
33+
* @return string|int The coerced value for RRD storage
34+
*/
35+
function coerce_multi_field_value_fixed(string $raw_value): string|int {
36+
if (is_numeric($raw_value) || $raw_value === 'U') {
37+
return $raw_value;
38+
}
39+
40+
if (function_exists('is_hexadecimal') && is_hexadecimal($raw_value)) {
41+
return hexdec($raw_value);
42+
}
43+
44+
return 'U';
45+
}
46+
47+
/**
48+
* Simulate the BUGGY version that had the unconditional overwrite
49+
* at line 643.
50+
*
51+
* @param string $raw_value The raw value from a key:value pair
52+
*
53+
* @return string|int Always returns raw_value (bug)
54+
*/
55+
function coerce_multi_field_value_buggy(string $raw_value): string|int {
56+
if (is_numeric($raw_value) || $raw_value === 'U') {
57+
$result = $raw_value;
58+
} elseif (function_exists('is_hexadecimal') && is_hexadecimal($raw_value)) {
59+
$result = hexdec($raw_value);
60+
} else {
61+
$result = 'U';
62+
}
63+
64+
// BUG: unconditional overwrite (was line 643)
65+
$result = $raw_value;
66+
67+
return $result;
68+
}
69+
70+
if (!function_exists('is_hexadecimal')) {
71+
function is_hexadecimal(string $result): bool {
72+
$hexstr = str_replace([' ', '-'], ':', trim($result));
73+
foreach (explode(':', $hexstr) as $part) {
74+
if (strlen($part) != 2 || !ctype_xdigit($part)) {
75+
return false;
76+
}
77+
}
78+
return true;
79+
}
80+
}
81+
82+
// ===========================================================================
83+
// Regression tests: fixed behavior
84+
// ===========================================================================
85+
86+
describe('Regression #6879: multi-DS hex overwrite fix', function () {
87+
test('numeric value preserved by fixed version', function () {
88+
expect(coerce_multi_field_value_fixed('12345'))->toBe('12345');
89+
});
90+
91+
test('U value preserved by fixed version', function () {
92+
expect(coerce_multi_field_value_fixed('U'))->toBe('U');
93+
});
94+
95+
test('hex value converted to decimal by fixed version', function () {
96+
$r = coerce_multi_field_value_fixed('0A:0B');
97+
expect($r)->toBeInt();
98+
expect($r)->not->toBe('0A:0B');
99+
});
100+
101+
test('non-numeric non-hex becomes U in fixed version', function () {
102+
expect(coerce_multi_field_value_fixed('garbage'))->toBe('U');
103+
});
104+
105+
test('buggy version always returns raw value (demonstrates the bug)', function () {
106+
// The buggy version overwrites everything with raw input
107+
expect(coerce_multi_field_value_buggy('garbage'))->toBe('garbage');
108+
expect(coerce_multi_field_value_buggy('garbage'))->not->toBe('U');
109+
});
110+
111+
test('fixed version returns U for garbage, buggy returns raw', function () {
112+
$fixed = coerce_multi_field_value_fixed('not_a_number');
113+
$buggy = coerce_multi_field_value_buggy('not_a_number');
114+
115+
expect($fixed)->toBe('U');
116+
expect($buggy)->toBe('not_a_number');
117+
expect($fixed)->not->toBe($buggy);
118+
});
119+
});
120+
121+
// ===========================================================================
122+
// Mutation killers for the fix
123+
// ===========================================================================
124+
125+
describe('Mutation killers for #6879 fix', function () {
126+
test('numeric stays numeric, never becomes U', function () {
127+
$r = coerce_multi_field_value_fixed('100');
128+
expect($r)->toBe('100');
129+
expect($r)->not->toBe('U');
130+
});
131+
132+
test('U stays U, never becomes numeric', function () {
133+
$r = coerce_multi_field_value_fixed('U');
134+
expect($r)->toBe('U');
135+
expect($r)->not->toBeInt();
136+
});
137+
138+
test('garbage becomes U, never passes through raw', function () {
139+
$r = coerce_multi_field_value_fixed('random_junk');
140+
expect($r)->toBe('U');
141+
expect($r)->not->toBe('random_junk');
142+
});
143+
144+
test('fixed and buggy produce different results for garbage', function () {
145+
$fixed = coerce_multi_field_value_fixed('xyz');
146+
$buggy = coerce_multi_field_value_buggy('xyz');
147+
expect($fixed)->not->toBe($buggy);
148+
});
149+
150+
test('coercion is deterministic over 100 runs', function () {
151+
$first = coerce_multi_field_value_fixed('test_value');
152+
for ($i = 0; $i < 100; $i++) {
153+
expect(coerce_multi_field_value_fixed('test_value'))->toBe($first);
154+
}
155+
});
156+
157+
test('negative numeric is preserved', function () {
158+
expect(coerce_multi_field_value_fixed('-42.5'))->toBe('-42.5');
159+
});
160+
161+
test('scientific notation is preserved', function () {
162+
expect(coerce_multi_field_value_fixed('1.5e+09'))->toBe('1.5e+09');
163+
});
164+
165+
test('empty string becomes U', function () {
166+
expect(coerce_multi_field_value_fixed(''))->toBe('U');
167+
});
168+
});
169+
170+
// ===========================================================================
171+
// Source verification: the bug is actually fixed
172+
// ===========================================================================
173+
174+
describe('Source verification: overwrite line removed', function () {
175+
test('no unconditional matches[1] overwrite after coercion block', function () {
176+
$c = file_get_contents(__DIR__ . '/../../lib/poller.php');
177+
178+
// Find the first multi-DS coercion block (with data template)
179+
$pos = strpos($c, "if (is_numeric(\$matches[1]) || (\$matches[1] == 'U'))");
180+
$segment = substr($c, $pos, 500);
181+
182+
// The segment should contain the coercion if/elseif/else
183+
expect($segment)->toContain('is_numeric($matches[1])');
184+
expect($segment)->toContain('is_hexadecimal');
185+
expect($segment)->toContain("= 'U'");
186+
187+
// But should NOT have an unconditional overwrite right after
188+
// Look for the pattern: closing brace, blank line, then $matches[1] assignment
189+
expect($segment)->not->toMatch('/\}\s*\n\s*\n\s*\$rrd_update_array.*\$matches\[1\]/');
190+
});
191+
});

0 commit comments

Comments
 (0)