Skip to content

Commit 8a1605f

Browse files
Merge branch 'develop' into chore/test-db-sentinel
2 parents 60479cf + d9de6df commit 8a1605f

7 files changed

Lines changed: 276 additions & 30 deletions

File tree

cli/upgrade_database.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@
175175
db_execute_prepared('UPDATE version SET cacti = ?', [$cacti_upgrade_version]);
176176

177177
if (cacti_version_compare(CACTI_VERSION, $cacti_upgrade_version, '=')) {
178-
db_execute("UPDATE version SET cacti = '" . CACTI_VERSION_FULL . "'");
178+
db_execute_prepared('UPDATE version SET cacti = ?', [CACTI_VERSION_FULL]);
179179

180180
break;
181181
}

install/functions.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,13 @@ function install_unlink(string $file) : void {
303303
$full_file = $file;
304304
}
305305

306-
if (!str_contains($full_file, CACTI_PATH_BASE)) {
306+
$real_base = realpath(CACTI_PATH_BASE);
307+
$real_file = realpath($full_file);
308+
309+
if ($real_base === false || $real_file === false || !str_starts_with($real_file, $real_base . DIRECTORY_SEPARATOR)) {
307310
log_install_high('file', "Not Unlinking file: $full_file due to it not being in the Cacti base path.");
311+
312+
return;
308313
}
309314

310315
if (file_exists($full_file) && is_writable($full_file)) {
@@ -321,8 +326,13 @@ function install_rmdir(string $directory) : void {
321326
$directory = CACTI_PATH_BASE . '/' . $directory;
322327
}
323328

324-
if (!str_contains($directory, CACTI_PATH_BASE)) {
329+
$real_base = realpath(CACTI_PATH_BASE);
330+
$real_dir = realpath($directory);
331+
332+
if ($real_base === false || $real_dir === false || !str_starts_with($real_dir, $real_base . DIRECTORY_SEPARATOR)) {
325333
log_install_high('file', "Not Unlinking directory: $directory due to it not being in the Cacti base path.");
334+
335+
return;
326336
}
327337

328338
if (file_exists($directory) && is_writable($directory)) {
@@ -348,8 +358,13 @@ function install_rmdir_recursive(string $directory, bool $del_parent = false) :
348358
$directory = CACTI_PATH_BASE . '/' . $directory;
349359
}
350360

351-
if (!str_contains($directory, CACTI_PATH_BASE)) {
361+
$real_base = realpath(CACTI_PATH_BASE);
362+
$real_dir = realpath($directory);
363+
364+
if ($real_base === false || $real_dir === false || !str_starts_with($real_dir, $real_base . DIRECTORY_SEPARATOR)) {
352365
log_install_high('file', "Not Unlinking directory: $directory due to it not being in the Cacti base path.");
366+
367+
return;
353368
}
354369

355370
$files = glob($directory . '/{,.}[!.,!..]*',GLOB_MARK | GLOB_BRACE);
@@ -1384,12 +1399,16 @@ function import_colors() : bool {
13841399
$hex = $parts[1];
13851400
$name = $parts[2];
13861401

1387-
$id = db_fetch_cell("SELECT hex FROM colors WHERE hex='$hex'");
1402+
if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
1403+
continue;
1404+
}
1405+
1406+
$id = db_fetch_cell_prepared('SELECT hex FROM colors WHERE hex = ?', [$hex]);
13881407

13891408
if (!empty($id)) {
1390-
db_execute("UPDATE colors SET name='$name', read_only='on' WHERE hex='$hex'");
1409+
db_execute_prepared('UPDATE colors SET name = ?, read_only = \'on\' WHERE hex = ?', [$name, $hex]);
13911410
} else {
1392-
db_execute("INSERT IGNORE INTO colors (name, hex, read_only) VALUES ('$name', '$hex', 'on')");
1411+
db_execute_prepared('INSERT IGNORE INTO colors (name, hex, read_only) VALUES (?, ?, \'on\')', [$name, $hex]);
13931412
}
13941413
}
13951414
}

install/upgrades/1_3_0.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function upgrade_to_1_3_0() : void {
4141
db_install_add_column('host', ['name' => 'snmp_retries', 'type' => 'tinyint(3) unsigned', 'NULL' => false, 'default' => '3', 'after' => 'snmp_timeout']);
4242
db_install_add_column('host', ['name' => 'current_errors', 'type' => 'int(10)', 'unsigned' => true, 'default' => '0', 'after' => 'polling_time']);
4343

44-
db_add_index('host', 'INDEX', 'current_errors', ['current_errors']);
44+
db_install_add_key('host', 'INDEX', 'current_errors', ['current_errors']);
4545

4646
db_install_add_column('poller_item', ['name' => 'snmp_retries', 'type' => 'tinyint(3) unsigned', 'NULL' => false, 'default' => '3', 'after' => 'snmp_timeout']);
4747

@@ -82,11 +82,11 @@ function upgrade_to_1_3_0() : void {
8282
db_install_add_column('data_template', ['name' => 'last_updated', 'type' => 'timestamp', 'null' => false, 'default' => 'CURRENT_TIMESTAMP']);
8383
db_install_add_column('snmp_query', ['name' => 'last_updated', 'type' => 'timestamp', 'null' => false, 'default' => 'CURRENT_TIMESTAMP']);
8484

85-
db_add_index('data_input_data', 'INDEX', 'data_template_id', ['data_template_id']);
86-
db_add_index('data_input_data', 'INDEX', 'local_data_id', ['local_data_id']);
87-
db_add_index('data_input_data', 'INDEX', 'host_id', ['host_id']);
85+
db_install_add_key('data_input_data', 'INDEX', 'data_template_id', ['data_template_id']);
86+
db_install_add_key('data_input_data', 'INDEX', 'local_data_id', ['local_data_id']);
87+
db_install_add_key('data_input_data', 'INDEX', 'host_id', ['host_id']);
8888

89-
db_add_index('poller_output_boost', 'INDEX', 'time', ['time']);
89+
db_install_add_key('poller_output_boost', 'INDEX', 'time', ['time']);
9090

9191
db_install_add_column('host_snmp_query', ['name' => 'reindex_last_runtime', 'type' => 'timestamp', 'null' => false, 'default' => 'CURRENT_TIMESTAMP']);
9292
db_install_add_column('host_snmp_query', ['name' => 'reindex_last_duration', 'type' => 'double', 'unsigned' => true, 'null' => false, 'default' => '0']);
@@ -288,8 +288,8 @@ function upgrade_to_1_3_0() : void {
288288
ADD INDEX last_updated(last_updated)');
289289
}
290290

291-
db_install_execute("ALTER TABLE `settings` MODIFY `name` varchar(75) not null default ''");
292-
db_install_execute("ALTER TABLE `settings_user` MODIFY `name` varchar(75) not null default ''");
291+
db_install_execute("ALTER TABLE `settings` MODIFY `name` varchar(255) not null default ''");
292+
db_install_execute("ALTER TABLE `settings_user` MODIFY `name` varchar(255) not null default ''");
293293

294294
$tables = [
295295
'aggregate_graph_templates' => [
@@ -687,10 +687,10 @@ function upgrade_reports() : void {
687687
);
688688

689689
break;
690-
case 10: // Hours
690+
case 11: // Hours
691691
db_execute_prepared('UPDATE reports
692692
SET sched_type = ?,
693-
enabled = ?
693+
enabled = ?,
694694
recur_every = ?,
695695
next_start = ?,
696696
last_started = ?

lib/plugins.php

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,31 +1054,41 @@ function api_plugin_moveup(string $plugin) : void {
10541054
[$plugin]);
10551055

10561056
if (!empty($id)) {
1057-
$temp_id = db_fetch_cell('SELECT MAX(id) FROM plugin_config') + 1;
1058-
10591057
$prior_id = db_fetch_cell_prepared('SELECT MAX(id)
10601058
FROM plugin_config
10611059
WHERE id < ?',
10621060
[$id]);
10631061

1064-
// update the above plugin to the prior temp id
1065-
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$temp_id, $prior_id]);
1066-
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$prior_id, $id]);
1067-
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$id, $temp_id]);
1062+
// MAX() on an empty set returns NULL; without a guard, the UPDATE
1063+
// below would set id = NULL on the current plugin, which non-strict
1064+
// MariaDB/MySQL silently stores as 0, corrupting the primary key.
1065+
if (!empty($prior_id)) {
1066+
$temp_id = db_fetch_cell('SELECT MAX(id) FROM plugin_config') + 1;
1067+
1068+
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$temp_id, $prior_id]);
1069+
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$prior_id, $id]);
1070+
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$id, $temp_id]);
1071+
}
10681072
}
10691073

10701074
api_plugin_replicate_config();
10711075
}
10721076

10731077
function api_plugin_movedown(string $plugin) : void {
1074-
$id = db_fetch_cell_prepared('SELECT id FROM plugin_config WHERE directory = ?', [$plugin]);
1075-
$temp_id = db_fetch_cell('SELECT MAX(id) FROM plugin_config') + 1;
1076-
$next_id = db_fetch_cell_prepared('SELECT MIN(id) FROM plugin_config WHERE id > ?', [$id]);
1077-
1078-
// update the above plugin to the prior temp id
1079-
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$temp_id, $next_id]);
1080-
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$next_id, $id]);
1081-
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$id, $temp_id]);
1078+
$id = db_fetch_cell_prepared('SELECT id FROM plugin_config WHERE directory = ?', [$plugin]);
1079+
1080+
if (!empty($id)) {
1081+
$next_id = db_fetch_cell_prepared('SELECT MIN(id) FROM plugin_config WHERE id > ?', [$id]);
1082+
1083+
// MIN() on an empty set returns NULL; same NULL→0 corruption risk as moveup.
1084+
if (!empty($next_id)) {
1085+
$temp_id = db_fetch_cell('SELECT MAX(id) FROM plugin_config') + 1;
1086+
1087+
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$temp_id, $next_id]);
1088+
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$next_id, $id]);
1089+
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$id, $temp_id]);
1090+
}
1091+
}
10821092

10831093
api_plugin_replicate_config();
10841094
}

lib/utility.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,20 @@ function utilities_get_mysql_recommendations() : int {
13621362
];
13631363
}
13641364

1365+
if ($database == 'MariaDB' && version_compare($version, '11.8', '>=')) {
1366+
// MariaDB 11.8 enabled innodb_snapshot_isolation=ON by default (MDEV-39628).
1367+
// Under this mode, concurrent UPDATE statements on any InnoDB table can block
1368+
// for tens of thousands of seconds even when the table has only a handful of
1369+
// rows. Cacti's poller cycle issues many rapid UPDATEs; this setting makes
1370+
// those updates contend in ways that can freeze the entire poller.
1371+
$recommendations['innodb_snapshot_isolation'] = [
1372+
'value' => 'OFF',
1373+
'measure' => 'equalint',
1374+
'class' => 'error',
1375+
'comment' => __('MariaDB 11.8 enabled innodb_snapshot_isolation=ON by default. This causes severe lock contention: UPDATE statements can block for tens of thousands of seconds on small tables under concurrent load, which stalls the Cacti poller. Set innodb_snapshot_isolation=OFF in your [mysqld] configuration. See MDEV-39628.')
1376+
];
1377+
}
1378+
13651379
if (file_exists('/etc/my.cnf.d/server.cnf')) {
13661380
$location = '/etc/my.cnf.d/server.cnf';
13671381
} else {

plugins.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,15 @@ function plugins_load_temp_table() : string {
389389

390390
db_execute("CREATE TEMPORARY TABLE IF NOT EXISTS $table LIKE plugin_config");
391391
db_execute("TRUNCATE $table");
392+
393+
// Cacti strips NO_AUTO_VALUE_ON_ZERO on connect (database.php). Without it,
394+
// a row with id=0 in plugin_config (e.g. from a plugin upgrade script) is
395+
// reassigned by AUTO_INCREMENT to the next sequence value, causing a 1062
396+
// collision when another row already holds that id.
397+
$orig_sql_mode = db_fetch_cell('SELECT @@SESSION.sql_mode');
398+
db_execute("SET SESSION sql_mode = CONCAT_WS(',', @@SESSION.sql_mode, 'NO_AUTO_VALUE_ON_ZERO')");
392399
db_execute("INSERT INTO $table SELECT * FROM plugin_config");
400+
db_execute_prepared('SET SESSION sql_mode = ?', [$orig_sql_mode]);
393401

394402
break;
395403
} else {

0 commit comments

Comments
 (0)