mirror of
https://github.com/Cacti/spine.git
synced 2026-11-12 15:34:44 +09:00
The runtime/probe changes from #642 and GNU C17/build changes from #643 were squash-merged into #641's source branch, which made the result-normalization review include later stages. This follow-up preserves those approved changes while restoring #641's focused diff against develop. No force-push or approved-code deletion is required. Merge after #641 and before #644. #644 will target this branch. This combines the already reviewed steps 5 and 6; it introduces no new worker-stack or module-extraction changes. The UDP follow-up also corrects a validated finding: EHOSTUNREACH on a connected socket is ambiguous and can represent an unreachable host or failed neighbour lookup. It now reports down; ECONNREFUSED/ECONNRESET and actual peer datagrams retain their existing alive behavior. The production-linked test rejects the original EHOSTUNREACH behavior. Validation: Ubuntu 24.04/aarch64, clean GCC build and make check: 17 passed, one existing setuid prerequisite skip, zero failures. The full tracked source tree is byte-identical to the independently validated #641 functional-fix snapshot. The UDP and poll-host regressions fail against the original code and pass after the fixes. The isolated step-4 tree independently passes 15 tests with one existing skip. No live endpoints, host installation, privilege changes, or test suppression. Repository squash merges still require restacking/retargeting the next child after each merge; clean ordinary merge simulations are insufficient by themselves. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
256 lines
8.0 KiB
C
256 lines
8.0 KiB
C
/* spine_log() stack use and the spine.conf tokenizer.
|
|
*
|
|
* Poller threads run on the platform's default stack: 128 KiB on musl,
|
|
* 512 KiB on macOS. The logging cases run on a thread of the musl size in a
|
|
* child process, so an overflow is a reported failure rather than a crash of
|
|
* the runner.
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <setjmp.h>
|
|
#include <cmocka.h>
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include "common.h"
|
|
#include "spine.h"
|
|
|
|
#define MUSL_DEFAULT_STACK (128 * 1024)
|
|
|
|
static char log_path[] = "spine-log-stack-XXXXXX";
|
|
|
|
static void *log_from_thread(void *argument) {
|
|
char *message;
|
|
|
|
(void) argument;
|
|
message = malloc(LOGSIZE);
|
|
if (message == NULL) return argument;
|
|
memset(message, 'x', LOGSIZE - 1);
|
|
message[LOGSIZE - 1] = '\0';
|
|
|
|
set.logging.log_destination = LOGDEST_FILE;
|
|
spine_log("stack check %s", message);
|
|
set.logging.log_destination = LOGDEST_STDOUT;
|
|
spine_log("stack check %s", message);
|
|
|
|
free(message);
|
|
return NULL;
|
|
}
|
|
|
|
static void test_spine_log_fits_a_musl_thread_stack(void **state) {
|
|
pid_t child;
|
|
int status;
|
|
int fd;
|
|
|
|
(void) state;
|
|
fd = mkstemp(log_path);
|
|
assert_true(fd >= 0);
|
|
assert_int_equal(close(fd), 0);
|
|
|
|
fflush(NULL);
|
|
child = fork();
|
|
assert_true(child >= 0);
|
|
|
|
if (child == 0) {
|
|
pthread_attr_t attr;
|
|
pthread_t thread;
|
|
void *result = &status;
|
|
|
|
/* cmocka traps SIGSEGV; let an overflow kill the child visibly */
|
|
signal(SIGSEGV, SIG_DFL);
|
|
signal(SIGBUS, SIG_DFL);
|
|
if (freopen("/dev/null", "w", stdout) == NULL) _exit(3);
|
|
memset(&set, 0, sizeof(set));
|
|
config_defaults();
|
|
set.logging.log_level = POLLER_VERBOSITY_LOW;
|
|
set.logging.logfile_processed = TRUE;
|
|
set.console.stdout_notty = FALSE;
|
|
set.console.stderr_notty = TRUE;
|
|
STRNCOPY(set.logging.path_logfile, log_path);
|
|
|
|
if (pthread_attr_init(&attr) != 0) _exit(4);
|
|
if (pthread_attr_setstacksize(&attr, MUSL_DEFAULT_STACK) != 0) _exit(5);
|
|
if (pthread_create(&thread, &attr, log_from_thread, NULL) != 0) _exit(6);
|
|
if (pthread_join(thread, &result) != 0 || result != NULL) _exit(7);
|
|
_exit(0);
|
|
}
|
|
|
|
assert_int_equal(waitpid(child, &status, 0), child);
|
|
unlink(log_path);
|
|
if (!WIFEXITED(status)) {
|
|
fail_msg("spine_log() killed a 128 KiB thread with signal %d", WTERMSIG(status));
|
|
}
|
|
assert_int_equal(WEXITSTATUS(status), 0);
|
|
}
|
|
|
|
/* --- spine.conf parsing -------------------------------------------------- */
|
|
|
|
static char config_path[64];
|
|
|
|
static void write_config(const char *contents) {
|
|
FILE *fp;
|
|
int fd;
|
|
|
|
strcpy(config_path, "spine-conf-test-XXXXXX");
|
|
fd = mkstemp(config_path);
|
|
assert_true(fd >= 0);
|
|
fp = fdopen(fd, "w");
|
|
assert_non_null(fp);
|
|
assert_true(fputs(contents, fp) != EOF);
|
|
assert_int_equal(fclose(fp), 0);
|
|
}
|
|
|
|
static int config_setup(void **state) {
|
|
(void) state;
|
|
memset(&set, 0, sizeof(set));
|
|
config_defaults();
|
|
set.console.stdout_notty = TRUE;
|
|
set.console.stderr_notty = TRUE;
|
|
config_path[0] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
static int config_teardown(void **state) {
|
|
(void) state;
|
|
if (config_path[0] != '\0') unlink(config_path);
|
|
return 0;
|
|
}
|
|
|
|
/* sscanf("%15s") cut a long key short and handed its tail over as the value,
|
|
* so a misspelt key could set a real one. */
|
|
static void test_config_long_key_is_not_truncated_into_a_known_key(void **state) {
|
|
(void) state;
|
|
write_config("SNMP_ClientaddrX 10.9.9.9\nDB_Port 3311\n");
|
|
assert_int_equal(read_spine_config(config_path), 0);
|
|
assert_string_equal(set.snmp.snmp_clientaddr, "");
|
|
assert_int_equal(set.database.port, 3311);
|
|
}
|
|
|
|
static void test_config_every_known_key_is_accepted(void **state) {
|
|
static const char *const keys[] = {
|
|
"RDB_Host", "RDB_Database", "RDB_User", "RDB_Pass", "RDB_SSL_Key",
|
|
"RDB_SSL_Cert", "RDB_SSL_CA", "DB_Host", "DB_Database", "DB_User",
|
|
"DB_Pass", "DB_SSL_Key", "DB_SSL_Cert", "DB_SSL_CA", "SNMP_Clientaddr",
|
|
"RDB_Port", "RDB_UseSSL", "DB_Port", "DB_UseSSL", "Poller", "Cacti_Log",
|
|
};
|
|
char contents[2048];
|
|
char *cursor = contents;
|
|
size_t remaining = sizeof(contents);
|
|
size_t i;
|
|
|
|
(void) state;
|
|
for (i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
|
assert_true(spine_appendf(&cursor, &remaining, "%s 1\n", keys[i]));
|
|
}
|
|
write_config(contents);
|
|
assert_int_equal(read_spine_config(config_path), 0);
|
|
assert_string_equal(set.snmp.snmp_clientaddr, "1");
|
|
assert_string_equal(set.remote_database.ssl_ca, "1");
|
|
assert_int_equal(set.remote_database.ssl, 1);
|
|
assert_int_equal(set.poller.poller_id, 1);
|
|
}
|
|
|
|
/* %255s cut a long certificate path, and the shortened path was used. */
|
|
static void test_config_long_value_is_kept_whole(void **state) {
|
|
char contents[BUFSIZE];
|
|
char path[700];
|
|
|
|
(void) state;
|
|
memset(path, 'p', sizeof(path) - 1);
|
|
path[0] = '/';
|
|
path[sizeof(path) - 1] = '\0';
|
|
snprintf(contents, sizeof(contents), "DB_SSL_CA %s\n", path);
|
|
write_config(contents);
|
|
assert_int_equal(read_spine_config(config_path), 0);
|
|
assert_string_equal(set.database.ssl_ca, path);
|
|
}
|
|
|
|
/* A line longer than the read buffer came back from fgets() in pieces, and
|
|
* the tail was parsed as a directive of its own. */
|
|
static void test_config_overlong_line_is_ignored_whole(void **state) {
|
|
char *contents;
|
|
size_t used;
|
|
|
|
(void) state;
|
|
contents = malloc(4 * BUFSIZE);
|
|
assert_non_null(contents);
|
|
strcpy(contents, "# ");
|
|
used = strlen(contents);
|
|
memset(contents + used, 'c', BUFSIZE - 1 - used);
|
|
used = BUFSIZE - 1;
|
|
strcpy(contents + used, "DB_Host injected\nDB_Port 3312\n");
|
|
write_config(contents);
|
|
free(contents);
|
|
|
|
assert_int_equal(read_spine_config(config_path), 0);
|
|
assert_string_equal(set.database.host, DEFAULT_DB_HOST);
|
|
assert_int_equal(set.database.port, 3312);
|
|
}
|
|
|
|
static void test_config_line_filling_the_buffer_exactly_is_read(void **state) {
|
|
char contents[2 * BUFSIZE];
|
|
char value[BUFSIZE];
|
|
size_t length;
|
|
|
|
(void) state;
|
|
/* "DB_User " plus the value is BUFSIZE - 1 characters before the newline */
|
|
length = BUFSIZE - 1 - strlen("DB_User ");
|
|
memset(value, 'u', length);
|
|
value[length] = '\0';
|
|
snprintf(contents, sizeof(contents), "DB_User %s\nDB_Port 3313\n", value);
|
|
write_config(contents);
|
|
assert_int_equal(read_spine_config(config_path), 0);
|
|
assert_string_equal(set.database.user, value);
|
|
assert_int_equal(set.database.port, 3313);
|
|
}
|
|
|
|
/* strchr() stopped at the NUL, so the line looked unfinished and the next
|
|
* one was thrown away as its remainder. */
|
|
static void test_config_embedded_nul_does_not_swallow_the_next_line(void **state) {
|
|
static const char contents[] = "DB_Host a\0junk\nDB_Port 3307\n";
|
|
FILE *fp;
|
|
int fd;
|
|
|
|
(void) state;
|
|
strcpy(config_path, "spine-conf-test-XXXXXX");
|
|
fd = mkstemp(config_path);
|
|
assert_true(fd >= 0);
|
|
fp = fdopen(fd, "w");
|
|
assert_non_null(fp);
|
|
assert_int_equal(fwrite(contents, 1, sizeof(contents) - 1, fp), sizeof(contents) - 1);
|
|
assert_int_equal(fclose(fp), 0);
|
|
|
|
assert_int_equal(read_spine_config(config_path), 0);
|
|
assert_string_equal(set.database.host, "a");
|
|
assert_int_equal(set.database.port, 3307);
|
|
}
|
|
|
|
static void test_config_last_line_without_newline_is_read(void **state) {
|
|
(void) state;
|
|
write_config("DB_Host first\nDB_Port 3399");
|
|
assert_int_equal(read_spine_config(config_path), 0);
|
|
assert_string_equal(set.database.host, "first");
|
|
assert_int_equal(set.database.port, 3399);
|
|
}
|
|
|
|
int main(void) {
|
|
const struct CMUnitTest tests[] = {
|
|
cmocka_unit_test(test_spine_log_fits_a_musl_thread_stack),
|
|
cmocka_unit_test_setup_teardown(test_config_long_key_is_not_truncated_into_a_known_key, config_setup, config_teardown),
|
|
cmocka_unit_test_setup_teardown(test_config_every_known_key_is_accepted, config_setup, config_teardown),
|
|
cmocka_unit_test_setup_teardown(test_config_long_value_is_kept_whole, config_setup, config_teardown),
|
|
cmocka_unit_test_setup_teardown(test_config_overlong_line_is_ignored_whole, config_setup, config_teardown),
|
|
cmocka_unit_test_setup_teardown(test_config_line_filling_the_buffer_exactly_is_read, config_setup, config_teardown),
|
|
cmocka_unit_test_setup_teardown(test_config_embedded_nul_does_not_swallow_the_next_line, config_setup, config_teardown),
|
|
cmocka_unit_test_setup_teardown(test_config_last_line_without_newline_is_read, config_setup, config_teardown),
|
|
};
|
|
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
}
|