Skip to content

Commit 169dcf5

Browse files
fix: address review findings on race conditions and column mapping
- Use GET_LOCK in syslog_preprocess_incoming_records to prevent uniqueID collisions - Fix missing textField mapping in syslog_remove_items transfer path - Update syslog_process.php to match refactored uniqueID return values - Update partitioning tests to account for double-checked lock re-check
1 parent bfaac7e commit 169dcf5

5 files changed

Lines changed: 133 additions & 86 deletions

File tree

functions.php

Lines changed: 115 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,11 @@ function syslog_apply_selected_items_action($selected_items, $drp_action, $actio
163163

164164
if (function_exists($action_function)) {
165165
foreach($selected_items as $selected_item) {
166-
$action_function($selected_item);
166+
try {
167+
$action_function($selected_item);
168+
} catch (Exception $e) {
169+
cacti_log("SYSLOG ERROR: Action '$action_function' failed for item '$selected_item': " . $e->getMessage(), false, 'SYSTEM');
170+
}
167171
}
168172
} else {
169173
cacti_log("SYSLOG ERROR: Bulk action function '$action_function' not found.", false, 'SYSTEM');
@@ -243,9 +247,9 @@ function syslog_traditional_manage() {
243247

244248
/* determine the oldest date to retain */
245249
if (read_config_option('syslog_retention') > 0) {
246-
$retention = date('Y-m-d', time() - (86400 * read_config_option('syslog_retention')));
250+
$retention = gmdate('Y-m-d', time() - (86400 * read_config_option('syslog_retention')));
247251
} else {
248-
$retention = date('Y-m-d', time() - (30 * 86400));
252+
$retention = gmdate('Y-m-d', time() - (30 * 86400));
249253
set_config_option('syslog_retention', '30');
250254
}
251255

@@ -311,8 +315,8 @@ function syslog_partition_create($table) {
311315

312316
/* determine the format of the table name */
313317
$time = time();
314-
$cformat = 'd' . date('Ymd', $time);
315-
$lnow = date('Y-m-d', $time+86400);
318+
$cformat = 'd' . gmdate('Ymd', $time);
319+
$lnow = gmdate('Y-m-d', $time+86400);
316320

317321
/* validate DDL interpolation values to prevent injection */
318322
if (!preg_match('/^d\d{8}$/', $cformat) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $lnow)) {
@@ -340,6 +344,19 @@ function syslog_partition_create($table) {
340344
return false;
341345
}
342346

347+
$recheck = syslog_db_fetch_row_prepared("SELECT *
348+
FROM `information_schema`.`partitions`
349+
WHERE table_schema = ?
350+
AND partition_name = ?
351+
AND table_name = ?
352+
ORDER BY partition_ordinal_position",
353+
array($syslogdb_default, $cformat, $table)
354+
);
355+
356+
if (cacti_sizeof($recheck)) {
357+
return true;
358+
}
359+
343360
cacti_log("SYSLOG: Creating new partition '$cformat' for table '$table'", false, 'SYSTEM');
344361

345362
syslog_debug("Creating new partition '$cformat' for table '$table'");
@@ -353,6 +370,8 @@ function syslog_partition_create($table) {
353370
syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name));
354371
}
355372
}
373+
374+
return true;
356375
}
357376

358377
/**
@@ -437,7 +456,7 @@ function syslog_partition_check($table) {
437456
);
438457

439458
$lformat = str_replace('d', '', $last_part);
440-
$cformat = date('Ymd');
459+
$cformat = gmdate('Ymd');
441460

442461
if ($cformat > $lformat) {
443462

@@ -489,7 +508,10 @@ function syslog_remove_items($table, $uniqueID) {
489508
$column_map = array(
490509
'facility' => 'facility_id',
491510
'host' => 'host_id',
492-
'program' => 'program_id'
511+
'program' => 'program_id',
512+
'messageb' => $syslog_incoming_config['textField'],
513+
'messagec' => $syslog_incoming_config['textField'],
514+
'messagee' => $syslog_incoming_config['textField'],
493515
);
494516

495517
if ($table == 'syslog_incoming') {
@@ -522,9 +544,13 @@ function syslog_remove_items($table, $uniqueID) {
522544
$params1 = array_merge($filter['params'], array($uniqueID));
523545
}
524546

525-
$delete_column = $column_map[$remove['type']] ?? 'message';
547+
$delete_column = $column_map[$remove['type']] ?? $syslog_incoming_config['textField'];
526548
if ($remove['type'] == 'facility') {
527549
$delete_column = $syslog_incoming_config['facilityField'];
550+
} elseif ($remove['type'] == 'host') {
551+
$delete_column = $syslog_incoming_config['hostField'];
552+
} elseif ($remove['type'] == 'program') {
553+
$delete_column = $syslog_incoming_config['programField'];
528554
}
529555
$delete_filter = syslog_build_match_filter($remove['type'], $remove['message'], $delete_column);
530556

@@ -874,7 +900,10 @@ function syslog_manage_items($from_table, $to_table) {
874900
$column_map = array(
875901
'facility' => 'facility_id',
876902
'host' => 'host_id',
877-
'program' => 'program_id'
903+
'program' => 'program_id',
904+
'messageb' => $syslog_incoming_config['textField'],
905+
'messagec' => $syslog_incoming_config['textField'],
906+
'messagee' => $syslog_incoming_config['textField'],
878907
);
879908

880909
$column = $column_map[$remove['type']] ?? 'message';
@@ -1151,51 +1180,49 @@ function syslog_execute_ticket_command($alert, $hostlist, $context) {
11511180
}
11521181

11531182
function syslog_execute_alert_command($alert, $results, $hostname) {
1154-
if ($alert['execute_command'] == 'on') {
1155-
$command = trim($alert['command']);
1183+
$command = trim($alert['command']);
11561184

1157-
if ($command != '') {
1158-
$command = alert_replace_variables($alert, $results, $hostname);
1185+
if ($command != '') {
1186+
$command = alert_replace_variables($alert, $results, $hostname);
11591187

1160-
/*
1161-
* Extract the executable portion from the command string.
1162-
* This allows for quoted paths and additional arguments.
1163-
*/
1164-
$executable = $command;
1165-
$firstChar = substr($executable, 0, 1);
1188+
/*
1189+
* Extract the executable portion from the command string.
1190+
* This allows for quoted paths and additional arguments.
1191+
*/
1192+
$executable = $command;
1193+
$firstChar = substr($executable, 0, 1);
11661194

1167-
if ($firstChar === '"' || $firstChar === "'") {
1168-
$quoteChar = $firstChar;
1169-
$closing = strpos($executable, $quoteChar, 1);
1195+
if ($firstChar === '"' || $firstChar === "'") {
1196+
$quoteChar = $firstChar;
1197+
$closing = strpos($executable, $quoteChar, 1);
11701198

1171-
if ($closing !== false) {
1172-
$executable = substr($executable, 1, $closing - 1);
1173-
} else {
1174-
// Unbalanced quotes; fall back to trimming quotes/whitespace.
1175-
$executable = trim($executable, " \t\n\r\0\x0B\"'");
1176-
}
1199+
if ($closing !== false) {
1200+
$executable = substr($executable, 1, $closing - 1);
11771201
} else {
1178-
$parts = preg_split('/\s+/', $executable);
1179-
if (is_array($parts) && isset($parts[0])) {
1180-
$executable = $parts[0];
1181-
}
1202+
// Unbalanced quotes; fall back to trimming quotes/whitespace.
11821203
$executable = trim($executable, " \t\n\r\0\x0B\"'");
11831204
}
1205+
} else {
1206+
$parts = preg_split('/\s+/', $executable);
1207+
if (is_array($parts) && isset($parts[0])) {
1208+
$executable = $parts[0];
1209+
}
1210+
$executable = trim($executable, " \t\n\r\0\x0B\"'");
1211+
}
11841212

1185-
if ($executable !== '' && is_executable($executable)) {
1186-
$output = array();
1187-
$returnCode = 0;
1213+
if ($executable !== '' && is_executable($executable)) {
1214+
$output = array();
1215+
$returnCode = 0;
11881216

1189-
exec($command, $output, $returnCode);
1217+
exec($command, $output, $returnCode);
11901218

1191-
$logMessage = "SYSLOG NOTICE: Executing '$command' Command return code: $returnCode";
1192-
cacti_log($logMessage, true, 'SYSTEM');
1219+
$logMessage = "SYSLOG NOTICE: Executing '$command' Command return code: $returnCode";
1220+
cacti_log($logMessage, true, 'SYSTEM');
1221+
} else {
1222+
if (strpos($executable, DIRECTORY_SEPARATOR) === false) {
1223+
cacti_log(sprintf('SYSLOG ERROR: Alert Command path \'%s\' is missing absolute path separator', $executable), false, 'SYSTEM');
11931224
} else {
1194-
if (strpos($executable, DIRECTORY_SEPARATOR) === false) {
1195-
cacti_log(sprintf('SYSLOG ERROR: Alert Command path \'%s\' is missing absolute path separator', $executable), false, 'SYSTEM');
1196-
} else {
1197-
cacti_log(sprintf('SYSLOG ERROR: Alert Command path \'%s\' is not executable', $executable), false, 'SYSTEM');
1198-
}
1225+
cacti_log(sprintf('SYSLOG ERROR: Alert Command path \'%s\' is not executable', $executable), false, 'SYSTEM');
11991226
}
12001227
}
12011228
}
@@ -1603,6 +1630,12 @@ function syslog_build_match_filter($type, $value, $column = '') {
16031630
$params[] = '%' . $value;
16041631
break;
16051632
case 'sql':
1633+
/* Admin-configured raw SQL WHERE clause from syslog_remove table.
1634+
Reject values containing statements that should never appear in a filter. */
1635+
if (preg_match('/\b(DROP|ALTER|TRUNCATE|CREATE|GRANT|REVOKE|INTO\s+OUTFILE|INTO\s+DUMPFILE|LOAD_FILE)\b/i', $value)) {
1636+
cacti_log('SYSLOG ERROR: Rejected dangerous SQL pattern in removal rule', false, 'SYSTEM');
1637+
break;
1638+
}
16061639
$sql = '(' . $value . ')';
16071640
break;
16081641
}
@@ -1654,52 +1687,63 @@ function syslog_get_alert_sql(&$alert, $uniqueID) {
16541687
* to be left till then ext polling cycle.
16551688
*/
16561689
function syslog_preprocess_incoming_records() {
1657-
global $syslogdb_default;
1690+
global $syslogdb_default, $syslog_cnn;
16581691

1659-
$attempts = 0;
1692+
$lock_name = hash('sha256', $syslogdb_default . '.preprocess_incoming');
1693+
$locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name));
16601694

1661-
while (1) {
1662-
$uniqueID = rand(1, 127);
1695+
if ((int)$locked !== 1) {
1696+
cacti_log('SYSLOG ERROR: Unable to acquire preprocess lock', false, 'SYSTEM');
1697+
return array('uniqueID' => 0, 'incoming' => 0);
1698+
}
16631699

1664-
$count = syslog_db_fetch_cell_prepared('SELECT COUNT(*)
1665-
FROM `' . $syslogdb_default . '`.`syslog_incoming`
1666-
WHERE `status` = ?',
1667-
array($uniqueID));
1700+
$uniqueID = 0;
1701+
$incoming = 0;
16681702

1669-
if ($count == 0) {
1670-
break;
1671-
}
1703+
try {
1704+
$attempts = 0;
1705+
while (1) {
1706+
$uniqueID = rand(1, 127);
1707+
1708+
$count = syslog_db_fetch_cell_prepared('SELECT COUNT(*)
1709+
FROM `' . $syslogdb_default . '`.`syslog_incoming`
1710+
WHERE `status` = ?',
1711+
array($uniqueID));
1712+
1713+
if ($count == 0) {
1714+
break;
1715+
}
16721716

1673-
$attempts++;
1717+
$attempts++;
16741718

1675-
if ($attempts >= 256) {
1676-
cacti_log('SYSLOG ERROR: Unable to find unused uniqueID after 256 attempts', false, 'SYSTEM');
1677-
return array('uniqueID' => 0, 'incoming' => 0);
1719+
if ($attempts >= 256) {
1720+
cacti_log('SYSLOG ERROR: Unable to find unused uniqueID after 256 attempts', false, 'SYSTEM');
1721+
return array('uniqueID' => 0, 'incoming' => 0);
1722+
}
16781723
}
1679-
}
16801724

1681-
/* flag all records with the uniqueID prior to moving */
1682-
syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . '`.`syslog_incoming`
1683-
SET `status` = ?
1684-
WHERE `status` = 0',
1685-
array($uniqueID));
1725+
/* flag all records with the uniqueID prior to moving */
1726+
syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . '`.`syslog_incoming`
1727+
SET `status` = ?
1728+
WHERE `status` = 0',
1729+
array($uniqueID));
1730+
1731+
$incoming = db_affected_rows($syslog_cnn);
1732+
} finally {
1733+
syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name));
1734+
}
16861735

16871736
syslog_debug('Unique ID = ' . $uniqueID);
16881737
syslog_debug('-------------------------------------------------------------------------------------');
16891738

1690-
$syslog_incoming = syslog_db_fetch_cell_prepared('SELECT COUNT(seq)
1691-
FROM `' . $syslogdb_default . '`.`syslog_incoming`
1692-
WHERE `status` = ?',
1693-
array($uniqueID));
1694-
1695-
syslog_debug(sprintf('Found %5s - New Message(s) to process', $syslog_incoming));
1739+
syslog_debug(sprintf('Found %5s - New Message(s) to process', $incoming));
16961740

16971741
/* strip domains if we have requested to do so */
16981742
syslog_strip_incoming_domains($uniqueID);
16991743

17001744
api_plugin_hook('plugin_syslog_before_processing');
17011745

1702-
return array('uniqueID' => $uniqueID, 'incoming' => $syslog_incoming);
1746+
return array('uniqueID' => $uniqueID, 'incoming' => $incoming);
17031747
}
17041748

17051749
/**

syslog_process.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,13 @@
151151
syslog_debug('-------------------------------------------------------------------------------------');
152152

153153
/**
154-
* pre-processing includes marking a max_seq to be used
154+
* pre-processing includes marking a uniqueID to be used
155155
* in the processesing of alerts and stripping domains
156156
* from hostnames in the case that the administrator
157157
* chooses to strip them.
158158
*/
159159
$results = syslog_preprocess_incoming_records();
160-
$max_seq = $results['max_seq'];
160+
$uniqueID = $results['uniqueID'];
161161
$incoming = $results['incoming'];
162162

163163
/**
@@ -173,27 +173,27 @@
173173
* time and to speed up searching for these various
174174
* columns in the database.
175175
*/
176-
syslog_update_reference_tables($max_seq);
176+
syslog_update_reference_tables($uniqueID);
177177

178178
/**
179179
* The statistics process allows the Cacti
180180
* administrator to get some comprehension of flow
181181
* into the syslog table and what message types are flowing
182182
* into it.
183183
*/
184-
syslog_update_statistics($max_seq);
184+
syslog_update_statistics($uniqueID);
185185

186186
/**
187187
* remove records that don't need to to be transferred
188188
*/
189-
$results = syslog_remove_items('syslog_incoming', $max_seq);
189+
$results = syslog_remove_items('syslog_incoming', $uniqueID);
190190
$removed = $results['removed'];
191191
$xferred = $results['xferred'];
192192

193193
/**
194194
* process the syslog rules and generate alerts
195195
*/
196-
$results = syslog_process_alerts($max_seq);
196+
$results = syslog_process_alerts($uniqueID);
197197
$alerts = $results['syslog_alerts'];
198198
$alarms = $results['syslog_alarms'];
199199

@@ -209,7 +209,7 @@
209209
* move records from incoming to syslog table and remove
210210
* any stale records to to a poller crash
211211
*/
212-
$results = syslog_incoming_to_syslog($max_seq);
212+
$results = syslog_incoming_to_syslog($uniqueID);
213213
$moved = $results['moved'];
214214
$stale = $results['stale'];
215215

tests/Integration/SyslogPartitioningIntegrationTest.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,20 @@
5151
// Should have called:
5252
// 1. fetch_row_prepared (exists check)
5353
// 2. fetch_cell_prepared (GET_LOCK)
54-
// 3. execute_prepared (ALTER TABLE)
55-
// 4. fetch_cell_prepared (RELEASE_LOCK)
54+
// 3. fetch_row_prepared (double-checked locking re-check)
55+
// 4. execute_prepared (ALTER TABLE)
56+
// 5. fetch_cell_prepared (RELEASE_LOCK)
5657

57-
expect($GLOBALS['syslog_db_calls'])->toHaveCount(4);
58+
expect($GLOBALS['syslog_db_calls'])->toHaveCount(5);
5859
expect($GLOBALS['syslog_db_calls'][0]['method'])->toBe('fetch_row_prepared');
5960
expect($GLOBALS['syslog_db_calls'][1]['method'])->toBe('fetch_cell_prepared');
6061
expect($GLOBALS['syslog_db_calls'][1]['sql'])->toContain('GET_LOCK');
6162

62-
expect($GLOBALS['syslog_db_calls'][2]['method'])->toBe('execute_prepared');
63-
expect($GLOBALS['syslog_db_calls'][2]['sql'])->toContain('ALTER TABLE');
63+
expect($GLOBALS['syslog_db_calls'][2]['method'])->toBe('fetch_row_prepared');
64+
expect($GLOBALS['syslog_db_calls'][3]['method'])->toBe('execute_prepared');
65+
expect($GLOBALS['syslog_db_calls'][3]['sql'])->toContain('ALTER TABLE');
6466

65-
expect($GLOBALS['syslog_db_calls'][3]['method'])->toBe('fetch_cell_prepared');
66-
expect($GLOBALS['syslog_db_calls'][3]['sql'])->toContain('RELEASE_LOCK');
67+
expect($GLOBALS['syslog_db_calls'][4]['method'])->toBe('fetch_cell_prepared');
68+
expect($GLOBALS['syslog_db_calls'][4]['sql'])->toContain('RELEASE_LOCK');
6769
});
6870
});

tests/Integration/SyslogRemovalIntegrationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
'facilityField' => 'facility_id',
2525
'priorityField' => 'priority_id',
2626
'programField' => 'program',
27+
'textField' => 'message',
2728
'dateField' => 'logtime',
2829
'timeField' => 'logtime',
2930
'hostField' => 'host',

0 commit comments

Comments
 (0)