diff --git a/Makefile.am b/Makefile.am index cd1a7fe..0183fe2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -31,7 +31,7 @@ bin_PROGRAMS = spine man_MANS = spine.1 -EXTRA_DIST = spine.1 uthash.h +EXTRA_DIST = spine.1 uthash.h spine_sem.h # Docker targets — require Dockerfile and Dockerfile.dev (from PR #401) .PHONY: docker docker-dev verify cppcheck diff --git a/common.h b/common.h index d1e8315..79fcf8c 100644 --- a/common.h +++ b/common.h @@ -87,7 +87,7 @@ #include #include #include -#include +#include "spine_sem.h" #include #include #include diff --git a/keywords.h b/keywords.h index cc51966..109902e 100644 --- a/keywords.h +++ b/keywords.h @@ -31,12 +31,18 @@ +-------------------------------------------------------------------------+ */ -extern const char *printable_log_level(int token); -extern int parse_log_level(const char *word, int dflt); +extern const char *printable_log_level(int token) + SPINE_ATTR_PURE; +extern int parse_log_level(const char *word, int dflt) + SPINE_ATTR_PURE; -extern const char *printable_logdest(int token); -extern int parse_logdest(const char *word, int dflt); +extern const char *printable_logdest(int token) + SPINE_ATTR_PURE; +extern int parse_logdest(const char *word, int dflt) + SPINE_ATTR_PURE; -extern const char *printable_action(int token); -extern int parse_action(const char *word, int dflt); +extern const char *printable_action(int token) + SPINE_ATTR_PURE; +extern int parse_action(const char *word, int dflt) + SPINE_ATTR_PURE; diff --git a/nft_popen.c b/nft_popen.c index ff15e2d..811b651 100644 --- a/nft_popen.c +++ b/nft_popen.c @@ -123,6 +123,9 @@ static void close_cleanup(void *); * *------------------------------------------------------------------------------ */ +/* WARNING: command is passed to /bin/sh -c without shell escaping. + * The caller MUST ensure command originates from a trusted source + * (the Cacti database). Do not pass user-controlled input directly. */ int nft_popen(const char * command, const char * type) { struct pid *cur; struct pid *p; diff --git a/php.h b/php.h index fcdd0e2..4215995 100644 --- a/php.h +++ b/php.h @@ -31,8 +31,11 @@ +-------------------------------------------------------------------------+ */ -extern char *php_cmd(const char *php_command, int php_process); -extern char *php_readpipe(int php_process, char *command); +extern char *php_cmd(const char *php_command, int php_process) + SPINE_ATTR_NONNULL(1) + SPINE_ATTR_WARN_UNUSED; +extern char *php_readpipe(int php_process, char *command) + SPINE_ATTR_WARN_UNUSED; extern int php_init(int php_process); extern void php_close(int php_process); extern int php_get_process(void); diff --git a/ping.c b/ping.c index 17b4b80..c06de0d 100644 --- a/ping.c +++ b/ping.c @@ -34,6 +34,11 @@ #include "common.h" #include "spine.h" +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +#include +#define SPINE_ATOMIC_SEQ 1 +#endif + /*! \fn int ping_host(host_t *host, ping_t *ping) * \brief ping a host to determine if it is reachable for polling * \param host a pointer to the current host structure @@ -257,7 +262,7 @@ int ping_snmp(host_t *host, ping_t *ping) { * */ int ping_icmp(host_t *host, ping_t *ping) { - int icmp_socket; + int icmp_fd; double begin_time, end_time, total_time; double host_timeout; @@ -274,7 +279,11 @@ int ping_icmp(host_t *host, ping_t *ping) { ssize_t return_code; fd_set socket_fds; - static unsigned int seq = 0; +#if defined(SPINE_ATOMIC_SEQ) + static _Atomic unsigned int seq = 0; +#else + static volatile unsigned int seq = 0; +#endif struct icmp *icmp; struct ip *ip; struct icmp *pkt; @@ -286,49 +295,21 @@ int ping_icmp(host_t *host, ping_t *ping) { SPINE_LOG_DEBUG(("DEBUG: Device[%i] Entering ICMP Ping", host->id)); } - /* get ICMP socket */ - retry_count = 0; - while (TRUE) { - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - thread_mutex_lock(LOCK_SETEUID); - if (seteuid(0) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to obtain root privileges.")); - } - } - #endif - - if ((icmp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) { - usleep(500000); - retry_count++; - - if (retry_count > 4) { - snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Ping unable to create ICMP Socket"); - snprintf(ping->ping_status, 50, "down"); - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - if (seteuid(getuid()) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to drop from root to local user.")); - } - thread_mutex_unlock(LOCK_SETEUID); - } - #endif - - return HOST_DOWN; - } - } else { - break; - } + /* Use the pre-opened global ICMP socket (opened single-threaded in main + * before any worker threads start, eliminating the seteuid race). + * dup() gives each thread its own fd so select()/close() don't interfere. */ + if (icmp_socket < 0) { + snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: raw socket not available"); + snprintf(ping->ping_status, 50, "down"); + return HOST_DOWN; } - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - if (seteuid(getuid()) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to drop from root to local user.")); - } - thread_mutex_unlock(LOCK_SETEUID); + icmp_fd = dup(icmp_socket); + if (icmp_fd < 0) { + snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: socket dup failed: %s", strerror(errno)); + snprintf(ping->ping_status, 50, "down"); + return HOST_DOWN; } - #endif /* convert the host timeout to a double precision number in seconds */ host_timeout = host->ping_timeout; @@ -351,17 +332,19 @@ int ping_icmp(host_t *host, ping_t *ping) { icmp->icmp_code = 0; icmp->icmp_id = getpid() & 0xFFFF; - /* lock set/get the sequence and unlock */ - thread_mutex_lock(LOCK_GHBN); - icmp->icmp_seq = seq++; - thread_mutex_unlock(LOCK_GHBN); + /* atomically increment the sequence counter */ +#if defined(SPINE_ATOMIC_SEQ) + icmp->icmp_seq = atomic_fetch_add(&seq, 1); +#else + icmp->icmp_seq = __sync_fetch_and_add(&seq, 1); +#endif icmp->icmp_cksum = 0; memcpy(packet+ICMP_HDR_SIZE, cacti_msg, strlen(cacti_msg)); icmp->icmp_cksum = get_checksum(packet, packet_len); /* hostname must be nonblank */ - if ((strlen(host->hostname) != 0) && (icmp_socket != -1)) { + if (strlen(host->hostname) != 0) { /* initialize variables */ snprintf(ping->ping_status, 50, "down"); snprintf(ping->ping_response, SMALL_BUFSIZE, "default"); @@ -377,7 +360,7 @@ int ping_icmp(host_t *host, ping_t *ping) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Ping timed out"); snprintf(ping->ping_status, 50, "down"); free(packet); - close(icmp_socket); + close(icmp_fd); return HOST_DOWN; } @@ -392,11 +375,11 @@ int ping_icmp(host_t *host, ping_t *ping) { timeout.tv_usec = ((int) (host_timeout - total_time) % 1000) * 1000; /* set the socket send and receive timeout */ - setsockopt(icmp_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)); - setsockopt(icmp_socket, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); + setsockopt(icmp_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)); + setsockopt(icmp_fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); /* send packet to destination */ - return_code = sendto(icmp_socket, packet, packet_len, 0, (struct sockaddr *) &fromname, sizeof(fromname)); + return_code = sendto(icmp_fd, packet, packet_len, 0, (struct sockaddr *) &fromname, sizeof(fromname)); fromlen = sizeof(fromname); @@ -404,8 +387,15 @@ int ping_icmp(host_t *host, ping_t *ping) { /* reinitialize fd_set -- select(2) clears bits in place on return */ keep_listening: FD_ZERO(&socket_fds); - FD_SET(icmp_socket,&socket_fds); - return_code = select(FD_SETSIZE, &socket_fds, NULL, NULL, &timeout); + if (icmp_fd >= FD_SETSIZE) { + SPINE_LOG(("ERROR: Device[%i] ICMP socket %d exceeds FD_SETSIZE %d", host->id, icmp_fd, FD_SETSIZE)); + snprintf(ping->ping_status, 50, "down"); + snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: fd exceeds FD_SETSIZE"); + close(icmp_fd); + return HOST_DOWN; + } + FD_SET(icmp_fd,&socket_fds); + return_code = select(icmp_fd + 1, &socket_fds, NULL, NULL, &timeout); /* record end time */ end_time = get_time_as_double(); @@ -415,9 +405,9 @@ int ping_icmp(host_t *host, ping_t *ping) { if (total_time < host_timeout) { #if !(defined(__CYGWIN__)) - return_code = recvfrom(icmp_socket, socket_reply, BUFSIZE, MSG_WAITALL, (struct sockaddr *) &recvname, &fromlen); + return_code = recvfrom(icmp_fd, socket_reply, BUFSIZE, MSG_WAITALL, (struct sockaddr *) &recvname, &fromlen); #else - return_code = recvfrom(icmp_socket, socket_reply, BUFSIZE, MSG_PEEK, (struct sockaddr *) &recvname, &fromlen); + return_code = recvfrom(icmp_fd, socket_reply, BUFSIZE, MSG_PEEK, (struct sockaddr *) &recvname, &fromlen); #endif if (return_code < 0) { @@ -446,24 +436,7 @@ int ping_icmp(host_t *host, ping_t *ping) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Device is Alive"); snprintf(ping->ping_status, 50, "%.5f", total_time); free(packet); - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - thread_mutex_lock(LOCK_SETEUID); - if (seteuid(0) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to obtain root privileges.")); - } - } - #endif - close(icmp_socket); - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - if (seteuid(getuid()) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to drop from root to local user.")); - } - thread_mutex_unlock(LOCK_SETEUID); - } - #endif - + close(icmp_fd); return HOST_UP; } else { /* received a response other than an echo reply */ @@ -497,48 +470,14 @@ int ping_icmp(host_t *host, ping_t *ping) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Destination hostname invalid"); snprintf(ping->ping_status, 50, "down"); free(packet); - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - thread_mutex_lock(LOCK_SETEUID); - if (seteuid(0) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to obtain root privileges.")); - } - } - #endif - close(icmp_socket); - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - if (seteuid(getuid()) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to drop from root to local user.")); - } - thread_mutex_unlock(LOCK_SETEUID); - } - #endif + close(icmp_fd); return HOST_DOWN; } } else { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Destination address not specified"); snprintf(ping->ping_status, 50, "down"); free(packet); - if (icmp_socket != -1) { - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - thread_mutex_lock(LOCK_SETEUID); - if (seteuid(0) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to obtain root privileges.")); - } - } - #endif - close(icmp_socket); - #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) - if (hasCaps() != TRUE) { - if (seteuid(getuid()) == -1) { - SPINE_LOG_DEBUG(("WARNING: Spine unable to drop from root to local user.")); - } - thread_mutex_unlock(LOCK_SETEUID); - } - #endif - } + close(icmp_fd); return HOST_DOWN; } } @@ -609,6 +548,13 @@ int ping_udp(host_t *host, ping_t *ping) { /* initialize file descriptor to review for input/output */ FD_ZERO(&socket_fds); + if (udp_socket >= FD_SETSIZE) { + SPINE_LOG(("ERROR: Device[%i] UDP socket %d exceeds FD_SETSIZE %d", host->id, udp_socket, FD_SETSIZE)); + snprintf(ping->ping_status, 50, "down"); + snprintf(ping->ping_response, SMALL_BUFSIZE, "UDP: fd exceeds FD_SETSIZE"); + close(udp_socket); + return HOST_DOWN; + } FD_SET(udp_socket,&socket_fds); while (1) { @@ -643,7 +589,7 @@ int ping_udp(host_t *host, ping_t *ping) { /* wait for a response on the socket */ wait_more: - return_code = select(FD_SETSIZE, &socket_fds, NULL, NULL, &timeout); + return_code = select(udp_socket + 1, &socket_fds, NULL, NULL, &timeout); /* record end time */ end_time = get_time_as_double(); diff --git a/poller.c b/poller.c index 192925b..e2a4489 100644 --- a/poller.c +++ b/poller.c @@ -44,20 +44,20 @@ void child_cleanup(void *arg) { void child_cleanup_thread(void *arg) { UNUSED_PARAMETER(arg); - sem_post(&available_threads); + spine_sem_post(&available_threads); int a_threads_value; - sem_getvalue(&available_threads, &a_threads_value); + spine_sem_getvalue(&available_threads, &a_threads_value); SPINE_LOG_DEVDBG(("DEBUG: Available Threads is %i (%i outstanding)", a_threads_value, set.threads - a_threads_value)); } void child_cleanup_script(void *arg) { UNUSED_PARAMETER(arg); - sem_post(&available_scripts); + spine_sem_post(&available_scripts); int a_scripts_value; - sem_getvalue(&available_scripts, &a_scripts_value); + spine_sem_getvalue(&available_scripts, &a_scripts_value); SPINE_LOG_DEVDBG(("DEBUG: Available Scripts is %i (%i outstanding)", a_scripts_value, MAX_SIMULTANEOUS_SCRIPTS - a_scripts_value)); } @@ -99,7 +99,7 @@ void *child(void *arg) { thread_mutex_unlock(LOCK_HOST_TIME); /* Allows main thread to proceed with creation of other threads */ - sem_post(poller_details.thread_init_sem); + spine_sem_post(poller_details.thread_init_sem); if (is_debug_device(host_id)) { SPINE_LOG(("DEBUG: Device[%i] HT[%i] In Poller, About to Start Polling", host_id, host_thread)); @@ -222,6 +222,10 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread buf_size = malloc(sizeof(int)); buf_errors = malloc(sizeof(int)); + if (error_string == NULL || buf_size == NULL || buf_errors == NULL) { + die("ERROR: Fatal malloc error: poller.c error_string/buf_size/buf_errors"); + } + *buf_size = 0; *buf_errors = 0; @@ -233,10 +237,20 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread //db_connect(LOCAL, &mysql); local_cnn = db_get_connection(LOCAL); + + if (local_cnn == NULL) { + die("ERROR: No available database connections"); + } + mysql = local_cnn->mysql; if (set.poller_id > 1 && set.mode == REMOTE_ONLINE) { remote_cnn = db_get_connection(REMOTE); + + if (remote_cnn == NULL) { + die("ERROR: No available remote database connections"); + } + mysqlr = remote_cnn->mysql; } @@ -653,7 +667,7 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread /* populate host structure */ host->ignore_host = FALSE; - if (row[0] != NULL) host->id = atoi(row[0]); + if (row[0] != NULL) host->id = (int)strtol(row[0], NULL, 10); if (row[1] != NULL) { name = get_namebyhost(row[1], NULL); @@ -664,7 +678,7 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread if (row[2] != NULL) STRNCOPY(host->snmp_community, row[2]); - if (row[3] != NULL) host->snmp_version = atoi(row[3]); + if (row[3] != NULL) host->snmp_version = (int)strtol(row[3], NULL, 10); if (row[4] != NULL) STRNCOPY(host->snmp_username, row[4]); if (row[5] != NULL) STRNCOPY(host->snmp_password, row[5]); @@ -674,18 +688,18 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread if (row[9] != NULL) STRNCOPY(host->snmp_context, row[9]); if (row[10] != NULL) STRNCOPY(host->snmp_engine_id, row[10]); - if (row[11] != NULL) host->snmp_port = atoi(row[11]); - if (row[12] != NULL) host->snmp_timeout = atoi(row[12]); - if (row[13] != NULL) host->max_oids = atoi(row[13]); + if (row[11] != NULL) host->snmp_port = (int)strtol(row[11], NULL, 10); + if (row[12] != NULL) host->snmp_timeout = (int)strtol(row[12], NULL, 10); + if (row[13] != NULL) host->max_oids = (int)strtol(row[13], NULL, 10); - if (row[14] != NULL) host->availability_method = atoi(row[14]); - if (row[15] != NULL) host->ping_method = atoi(row[15]); - if (row[16] != NULL) host->ping_port = atoi(row[16]); - if (row[17] != NULL) host->ping_timeout = atoi(row[17]); - if (row[18] != NULL) host->ping_retries = atoi(row[18]); + if (row[14] != NULL) host->availability_method = (int)strtol(row[14], NULL, 10); + if (row[15] != NULL) host->ping_method = (int)strtol(row[15], NULL, 10); + if (row[16] != NULL) host->ping_port = (int)strtol(row[16], NULL, 10); + if (row[17] != NULL) host->ping_timeout = (int)strtol(row[17], NULL, 10); + if (row[18] != NULL) host->ping_retries = (int)strtol(row[18], NULL, 10); - if (row[19] != NULL) host->status = atoi(row[19]); - if (row[20] != NULL) host->status_event_count = atoi(row[20]); + if (row[19] != NULL) host->status = (int)strtol(row[19], NULL, 10); + if (row[20] != NULL) host->status_event_count = (int)strtol(row[20], NULL, 10); if (row[21] != NULL) STRNCOPY(host->status_fail_date, row[21]); if (row[22] != NULL) STRNCOPY(host->status_rec_date, row[22]); @@ -696,8 +710,8 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread if (row[25] != NULL) host->max_time = atof(row[25]); if (row[26] != NULL) host->cur_time = atof(row[26]); if (row[27] != NULL) host->avg_time = atof(row[27]); - if (row[28] != NULL) host->total_polls = atoi(row[28]); - if (row[29] != NULL) host->failed_polls = atoi(row[29]); + if (row[28] != NULL) host->total_polls = (int)strtol(row[28], NULL, 10); + if (row[29] != NULL) host->failed_polls = (int)strtol(row[29], NULL, 10); if (row[30] != NULL) host->availability = atof(row[30]); if (row[31] != NULL) host->snmp_sysUpTimeInstance=atoll(row[31]); @@ -917,8 +931,8 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread reindex->assert_value[0] = '\0'; reindex->arg1[0] = '\0'; - if (row[0] != NULL) reindex->data_query_id = atoi(row[0]); - if (row[1] != NULL) reindex->action = atoi(row[1]); + if (row[0] != NULL) reindex->data_query_id = (int)strtol(row[0], NULL, 10); + if (row[1] != NULL) reindex->action = (int)strtol(row[1], NULL, 10); if (row[2] != NULL) snprintf(reindex->op, sizeof(reindex->op), "%s", row[2]); @@ -1014,6 +1028,13 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread break; case POLLER_ACTION_SCRIPT: /* script (popen) */ + /* Reject empty script commands that could cause unexpected behavior */ + if (strlen(reindex->arg1) == 0) { + SPINE_LOG(("WARNING: Device[%i] HT[%i] DQ[%i] empty script command, skipping", + host->id, host_thread, reindex->data_query_id)); + break; + } + poll_result = trim(exec_poll(host, reindex->arg1, reindex->data_query_id, "DQ")); if (is_debug_device(host->id)) { @@ -1271,6 +1292,10 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread /* retrieve each hosts polling items from poller cache and load into array */ poller_items = (target_t *) calloc(num_rows, sizeof(target_t)); + if (poller_items == NULL) { + die("ERROR: Fatal calloc error: poller.c poller_items"); + } + i = 0; while ((row = mysql_fetch_row(result))) { /* initialize monitored object */ @@ -1297,12 +1322,12 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread poller_items[i].rrd_num = 0; poller_items[i].output_regex[0] = '\0'; - if (row[0] != NULL) poller_items[i].action = atoi(row[0]); + if (row[0] != NULL) poller_items[i].action = (int)strtol(row[0], NULL, 10); if (row[1] != NULL) snprintf(poller_items[i].hostname, sizeof(poller_items[i].hostname), "%s", row[1]); if (row[2] != NULL) snprintf(poller_items[i].snmp_community, sizeof(poller_items[i].snmp_community), "%s", row[2]); - if (row[3] != NULL) poller_items[i].snmp_version = atoi(row[3]); + if (row[3] != NULL) poller_items[i].snmp_version = (int)strtol(row[3], NULL, 10); if (row[4] != NULL) snprintf(poller_items[i].snmp_username, sizeof(poller_items[i].snmp_username), "%s", row[4]); if (row[5] != NULL) snprintf(poller_items[i].snmp_password, sizeof(poller_items[i].snmp_password), "%s", row[5]); @@ -1313,11 +1338,11 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread if (row[9] != NULL) snprintf(poller_items[i].arg2, sizeof(poller_items[i].arg2), "%s", row[9]); if (row[10] != NULL) snprintf(poller_items[i].arg3, sizeof(poller_items[i].arg3), "%s", row[10]); - if (row[11] != NULL) poller_items[i].local_data_id = atoi(row[11]); + if (row[11] != NULL) poller_items[i].local_data_id = (int)strtol(row[11], NULL, 10); - if (row[12] != NULL) poller_items[i].rrd_num = atoi(row[12]); - if (row[13] != NULL) poller_items[i].snmp_port = atoi(row[13]); - if (row[14] != NULL) poller_items[i].snmp_timeout = atoi(row[14]); + if (row[12] != NULL) poller_items[i].rrd_num = (int)strtol(row[12], NULL, 10); + if (row[13] != NULL) poller_items[i].snmp_port = (int)strtol(row[13], NULL, 10); + if (row[14] != NULL) poller_items[i].snmp_timeout = (int)strtol(row[14], NULL, 10); if (row[15] != NULL) snprintf(poller_items[i].snmp_auth_protocol, sizeof(poller_items[i].snmp_auth_protocol), "%s", row[15]); @@ -1345,6 +1370,10 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread /* create an array for snmp oids */ snmp_oids = (snmp_oids_t *) calloc(host->max_oids, sizeof(snmp_oids_t)); + if (snmp_oids == NULL) { + die("ERROR: Fatal calloc error: poller.c snmp_oids"); + } + /* initialize all the memory to insure we don't get issues */ memset(snmp_oids, 0, sizeof(snmp_oids_t)*host->max_oids); @@ -1600,6 +1629,14 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread break; case POLLER_ACTION_SCRIPT: /* execute script file */ + /* Reject empty script commands that could cause unexpected behavior */ + if (strlen(poller_items[i].arg1) == 0) { + SPINE_LOG(("WARNING: Device[%i] HT[%i] DS[%i] empty script command, skipping", + host_id, host_thread, poller_items[i].local_data_id)); + SET_UNDEFINED(poller_items[i].result); + break; + } + poll_result = exec_poll(host, poller_items[i].arg1, poller_items[i].local_data_id, "DS"); /* process the result */ @@ -1637,6 +1674,11 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread } } + if (!IS_UNDEFINED(poller_items[i].result) && strlen(poller_items[i].output_regex)) { + snprintf(temp_result, RESULTS_BUFFER, "%s", regex_replace(poller_items[i].output_regex, poller_items[i].result)); + snprintf(poller_items[i].result, RESULTS_BUFFER, "%s", temp_result); + } + SPINE_FREE(poll_result); thread_end = get_time_as_double(); @@ -1656,6 +1698,14 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread break; case POLLER_ACTION_PHP_SCRIPT_SERVER: /* execute script server */ + /* Reject empty script commands that could cause unexpected behavior */ + if (strlen(poller_items[i].arg1) == 0) { + SPINE_LOG(("WARNING: Device[%i] HT[%i] DS[%i] empty script server command, skipping", + host_id, host_thread, poller_items[i].local_data_id)); + SET_UNDEFINED(poller_items[i].result); + break; + } + php_process = php_get_process(); poll_result = php_cmd(poller_items[i].arg1, php_process); @@ -1695,6 +1745,11 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread } } + if (!IS_UNDEFINED(poller_items[i].result) && strlen(poller_items[i].output_regex)) { + snprintf(temp_result, RESULTS_BUFFER, "%s", regex_replace(poller_items[i].output_regex, poller_items[i].result)); + snprintf(poller_items[i].result, RESULTS_BUFFER, "%s", temp_result); + } + SPINE_FREE(poll_result); thread_end = get_time_as_double(); @@ -1841,11 +1896,17 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread i = 0; while (i < rows_processed) { + char escaped_result[DBL_BUFSIZE]; + char escaped_rrd_name[DBL_BUFSIZE]; + + db_escape(&mysqlt, escaped_result, sizeof(escaped_result), poller_items[i].result); + db_escape(&mysqlt, escaped_rrd_name, sizeof(escaped_rrd_name), poller_items[i].rrd_name); + snprintf(result_string, RESULTS_BUFFER+SMALL_BUFSIZE, " (%i, '%s', FROM_UNIXTIME(%s), '%s')", poller_items[i].local_data_id, - poller_items[i].rrd_name, + escaped_rrd_name, host_time, - poller_items[i].result); + escaped_result); result_length = strlen(result_string); @@ -1942,7 +2003,7 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread if (host_thread == host_threads && set.active_profiles != 1) { SPINE_LOG_MEDIUM(("Device[%i] HT[%i] Updating Poller Items for Next Poll", host_id, host_thread)); - db_query(&mysql, LOCAL, query6); + db_free_result(db_query(&mysql, LOCAL, query6)); } /* record the polling time for the device */ @@ -1962,7 +2023,7 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread poll_time = get_time_as_double(); query1[0] = '\0'; snprintf(query1, BUFSIZE, "UPDATE host SET polling_time = %.3f - %.3f WHERE id = %i", poll_time, host_time_double, host_id); - db_query(&mysql, LOCAL, query1); + db_free_result(db_query(&mysql, LOCAL, query1)); } @@ -1980,7 +2041,7 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread " local_data_ids = CONCAT(local_data_ids, \", \", VALUES(local_data_ids))", host_id, set.poller_id, errors, error_string); - db_query(&mysql, LOCAL, error_query); + db_free_result(db_query(&mysql, LOCAL, error_query)); free(error_query); } @@ -2044,7 +2105,7 @@ void buffer_output_errors(char *error_string, int *buf_size, int *buf_errors, in *buf_size = snprintf(error_string, DBL_BUFSIZE, "%i", local_data_id); } else { (*buf_errors)++; - snprintf(error_string + *buf_size, DBL_BUFSIZE, "%s", tbuffer); + snprintf(error_string + *buf_size, DBL_BUFSIZE - *buf_size, "%s", tbuffer); *buf_size += error_len; } } @@ -2140,7 +2201,7 @@ void get_system_information(host_t *host, MYSQL *mysql, int system) { if (poll_result && is_numeric(poll_result)) { host->snmp_sysUpTimeInstance = atoll(poll_result) * 100; - snprintf(poll_result, BUFSIZE, "%llu", host->snmp_sysUpTimeInstance); + snprintf(poll_result, RESULTS_BUFFER, "%llu", host->snmp_sysUpTimeInstance); } SPINE_FREE(poll_result); @@ -2195,7 +2256,7 @@ void get_system_information(host_t *host, MYSQL *mysql, int system) { if (poll_result && is_numeric(poll_result)) { host->snmp_sysUpTimeInstance = atoll(poll_result) * 100; - snprintf(poll_result, BUFSIZE, "%llu", host->snmp_sysUpTimeInstance); + snprintf(poll_result, RESULTS_BUFFER, "%llu", host->snmp_sysUpTimeInstance); } SPINE_FREE(poll_result); @@ -2242,6 +2303,9 @@ int validate_result(char *result) { * \return a pointer to a character buffer containing the result. * */ +/* WARNING: command is passed to /bin/sh -c (via nft_popen) without shell escaping. + * The caller MUST ensure command originates from a trusted source + * (the Cacti database). Do not pass user-controlled input directly. */ char *exec_poll(host_t *current_host, char *command, int id, const char *type) { int cmd_fd; int pid; @@ -2295,7 +2359,7 @@ char *exec_poll(host_t *current_host, char *command, int id, const char *type) { // use the script server timeout value, allow for 50% leeway while (++retries < (set.script_timeout * 15)) { - sem_err = sem_trywait(&available_scripts); + sem_err = spine_sem_trywait(&available_scripts); if (sem_err == 0) { break; } else if (sem_err == EAGAIN || sem_err == EWOULDBLOCK) { @@ -2356,6 +2420,17 @@ char *exec_poll(host_t *current_host, char *command, int id, const char *type) { #endif if (cmd_fd > 0) { + if (cmd_fd >= FD_SETSIZE) { + SPINE_LOG(("ERROR: Device[%i] file descriptor %d exceeds FD_SETSIZE %d", current_host->id, cmd_fd, FD_SETSIZE)); + SET_UNDEFINED(result_string); + #ifdef USING_TPOPEN + pclose(fd); + #else + nft_pclose(cmd_fd); + #endif + goto popen_done; + } + retry: /* Initialize File Descriptors to Review for Input/Output */ @@ -2363,7 +2438,7 @@ char *exec_poll(host_t *current_host, char *command, int id, const char *type) { FD_SET(cmd_fd, &fds); /* wait x seconds for pipe response */ - switch (select(FD_SETSIZE, &fds, NULL, NULL, &timeout)) { + switch (select(cmd_fd + 1, &fds, NULL, NULL, &timeout)) { case -1: switch (errno) { case EBADF: @@ -2461,6 +2536,7 @@ char *exec_poll(host_t *current_host, char *command, int id, const char *type) { #else nft_pclose(cmd_fd); #endif + popen_done: ; } else { SPINE_LOG(("Device[%i] ERROR: Problem executing POPEN [%s]: '%s'", current_host->id, current_host->hostname, command)); SET_UNDEFINED(result_string); diff --git a/poller.h b/poller.h index 03a5199..2451c26 100644 --- a/poller.h +++ b/poller.h @@ -36,8 +36,13 @@ extern void child_cleanup(void *arg); extern void child_cleanup_thread(void *arg); extern void child_cleanup_script(void *arg); extern void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, char *host_time, int *host_errors, double host_time_double); -extern char *exec_poll(host_t *current_host, char *command, int id, const char *type); -extern void get_system_information(host_t *host, MYSQL *mysql, int system); -extern int is_multipart_output(char *result); -extern int validate_result(char *result); +extern char *exec_poll(host_t *current_host, char *command, int id, const char *type) + SPINE_ATTR_NONNULL(1, 2) + SPINE_ATTR_WARN_UNUSED; +extern void get_system_information(host_t *host, MYSQL *mysql, int system) + SPINE_ATTR_NONNULL(1, 2); +extern int is_multipart_output(char *result) + SPINE_ATTR_PURE; +extern int validate_result(char *result) + SPINE_ATTR_PURE; extern void buffer_output_errors(char * error_string, int * buf_size, int * buf_errors, int device_id, int thread_id, int local_data_id, bool flush); diff --git a/snmp.c b/snmp.c index 60708d2..a961d47 100644 --- a/snmp.c +++ b/snmp.c @@ -128,7 +128,7 @@ void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_c char *Xpsz = NULL; char *Cpsz = NULL; int priv_type; - int zero_sensitive = 0; + int zero_sensitive = 1; /* initialize SNMP */ snmp_sess_init(&session); @@ -269,28 +269,36 @@ void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_c session.securityPrivProto = snmp_duplicate_objid(priv_proto, session.securityPrivProtoLen); session.securityLevel = SNMP_SEC_LEVEL_AUTHPRIV; - // Auth Protocol Setup + /* Auth Protocol Setup - zero old credential before freeing */ if (Apsz && zero_sensitive) { - memset(Apsz, 0x0, strlen(Apsz)); + volatile char *vp = (volatile char *)Apsz; + size_t slen = strlen(Apsz); + while (slen--) { *vp++ = 0; } } free(Apsz); Apsz = strdup(snmp_password); if (zero_sensitive) { - memset(snmp_password, 0x0, strlen(snmp_password)); + volatile char *vp = (volatile char *)snmp_password; + size_t slen = strlen(snmp_password); + while (slen--) { *vp++ = 0; } } - // Privacy Protocol Setup + /* Privacy Protocol Setup - zero old credential before freeing */ if (Xpsz && zero_sensitive) { - memset(Xpsz, 0x0, strlen(Xpsz)); + volatile char *vp = (volatile char *)Xpsz; + size_t slen = strlen(Xpsz); + while (slen--) { *vp++ = 0; } } free(Xpsz); Xpsz = strdup(snmp_priv_passphrase); if (zero_sensitive) { - memset(snmp_priv_passphrase, 0x0, strlen(snmp_priv_passphrase)); + volatile char *vp = (volatile char *)snmp_priv_passphrase; + size_t slen = strlen(snmp_priv_passphrase); + while (slen--) { *vp++ = 0; } } if (Apsz) { @@ -377,6 +385,15 @@ void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_c sessp = snmp_sess_open(&session); thread_mutex_unlock(LOCK_SNMP); + /* zero sensitive key material now that session has copied it */ + { + volatile char *vp; + vp = (volatile char *)session.securityAuthKey; + memset((char *)vp, 0, sizeof(session.securityAuthKey)); + vp = (volatile char *)session.securityPrivKey; + memset((char *)vp, 0, sizeof(session.securityPrivKey)); + } + free(session.peername); free(session.securityAuthProto); free(session.securityPrivProto); diff --git a/snmp.h b/snmp.h index 47de30e..51719f2 100644 --- a/snmp.h +++ b/snmp.h @@ -37,9 +37,17 @@ extern void snmp_spine_init(void); extern void snmp_spine_close(void); extern void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community, char *snmp_username, char *snmp_password, char *snmp_auth_protocol, char *snmp_priv_passphrase, char *snmp_priv_protocol, char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout); extern void snmp_host_cleanup(void *snmp_session); -extern char *snmp_get_base(host_t *current_host, const char *snmp_oid, bool should_fail); -extern char *snmp_get(host_t *current_host, const char *snmp_oid); -extern char *snmp_getnext(host_t *current_host, const char *snmp_oid); -extern int snmp_count(host_t *current_host, const char *snmp_oid); -extern void snmp_get_multi(host_t *current_host, target_t *poller_items, snmp_oids_t *snmp_oids, int num_oids); +extern char *snmp_get_base(host_t *current_host, const char *snmp_oid, bool should_fail) + SPINE_ATTR_NONNULL(1, 2) + SPINE_ATTR_WARN_UNUSED; +extern char *snmp_get(host_t *current_host, const char *snmp_oid) + SPINE_ATTR_NONNULL(1, 2) + SPINE_ATTR_WARN_UNUSED; +extern char *snmp_getnext(host_t *current_host, const char *snmp_oid) + SPINE_ATTR_NONNULL(1, 2) + SPINE_ATTR_WARN_UNUSED; +extern int snmp_count(host_t *current_host, const char *snmp_oid) + SPINE_ATTR_NONNULL(1, 2); +extern void snmp_get_multi(host_t *current_host, target_t *poller_items, snmp_oids_t *snmp_oids, int num_oids) + SPINE_ATTR_NONNULL(1, 2, 3); extern void snmp_snprint_value(char *obuf, size_t buf_len, const oid *objid, size_t objidlen, struct variable_list *variable); diff --git a/spine.1 b/spine.1 index 1104605..18f1b7f 100644 --- a/spine.1 +++ b/spine.1 @@ -1,93 +1,70 @@ -.TH SPINE 1 "March 2026" "Spine 1.3.0" "Cacti Group" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH SPINE "1" "March 2026" "SPINE 1.3.0 Copyright 2004-2026 by The Cacti Group" "User Commands" .SH NAME -spine \- High-performance data collector for Cacti +SPINE \- Data Collector for Cacti .SH SYNOPSIS .B spine -[\fIoptions\fR] [[\fIfirstid lastid\fR] | [\fB\-H\fR|\fB\-\-hostlist\fR=\fI'hostid1,hostid2,...'\fR]] +[\fI\,options\/\fR] [[\fI\,firstid lastid\/\fR] \fI\,|| \/\fR[\fI\,-H/--hostlist='hostid1,hostid2,\/\fR...\fI\,,hostidn'\/\fR]] .SH DESCRIPTION -.B spine -is a multi-threaded data collector for Cacti, designed to be significantly faster than the default cmd.php poller. It is written in C and utilizes pthreads, net-snmp, and MariaDB/MySQL to efficiently poll large numbers of devices. +SPINE 1.3.0 Copyright 2004\-2026 by The Cacti Group .SH OPTIONS .TP -\fB\-h\fR, \fB\-\-help\fR -Show a brief help listing. +\fB\-h\fR/\-\-help +Show this brief help listing .TP -\fB\-f\fR, \fB\-\-first\fR=\fIX\fR -Start polling with host id X. +\fB\-f\fR/\-\-first=X +Start polling with host id X .TP -\fB\-l\fR, \fB\-\-last\fR=\fIX\fR -End polling with host id X. +\fB\-l\fR/\-\-last=X +End polling with host id X .TP -\fB\-H\fR, \fB\-\-hostlist\fR=\fIX\fR -Poll the specific list of host ids, separated by commas. +\fB\-H\fR/\-\-hostlist=X +Poll the list of host ids, separated by comma's .TP -\fB\-p\fR, \fB\-\-poller\fR=\fIX\fR -Set the poller id to X. This is used in distributed polling environments. +\fB\-p\fR/\-\-poller=X +Set the poller id to X .TP -\fB\-t\fR, \fB\-\-threads\fR=\fIX\fR +\fB\-t\fR/\-\-threads=X Override the database threads setting. .TP -\fB\-C\fR, \fB\-\-conf\fR=\fIF\fR -Read spine configuration from file F. -.TP -\fB\-O\fR, \fB\-\-option\fR=\fIS:V\fR -Override DB settings 'S' with value 'V'. -.TP -\fB\-M\fR, \fB\-\-mibs\fR -Refresh the device System Mib data. -.TP -\fB\-N\fR, \fB\-\-mode\fR=\fImode\fR -For remote pollers, the operating mode. Options: online, offline, recovery. Default: online. -.TP -\fB\-R\fR, \fB\-\-readonly\fR -Spine will not write any output to the database. -.TP -\fB\-S\fR, \fB\-\-stdout\fR -Logging is performed to standard output instead of the configured log file. -.TP -\fB\-P\fR, \fB\-\-pingonly\fR -Ping device and update device status only, skipping SNMP/Script polling. -.TP -\fB\-V\fR, \fB\-\-verbosity\fR=\fIV\fR -Set logging verbosity. Options: NONE, LOW, MEDIUM, HIGH, DEBUG or 1..5. -.SH CONFIGURATION -Without the \fB\-\-conf\fR parameter, \fBspine\fR searches for \fIspine.conf\fR in the following order: -.IP 1. 3 -The current working directory. -.IP 2. 3 -/etc/ -.IP 3. 3 -/etc/cacti/ -.IP 4. 3 -../etc/ (relative to the binary location) -.SH FILES -.TP -.I /etc/spine.conf -Default configuration file location for database credentials. -.SH EXAMPLES -.TP -\fBspine\fR -Poll all devices configured for this poller in the database. -.TP -\fBspine 1 10\fR -Poll hosts with ID 1 through 10. -.TP -\fBspine \-H "5,12,22"\fR -Poll only hosts with IDs 5, 12, and 22. -.TP -\fBspine \-S \-V DEBUG\fR -Run a poll cycle with full debug output to the terminal. -.SH EXIT STATUS -.TP -0 -Success -.TP --1 -Error (Database connection failure, configuration error, etc.) -.SH AUTHORS -Spine is developed and maintained by The Cacti Group (http://www.cacti.net) -.SH COPYRIGHT -Copyright 2004-2026 by The Cacti Group. -License LGPLv2.1+: GNU Lesser GPL version 2.1 or later. -This is free software: you are free to change and redistribute it. -There is NO WARRANTY, to the extent permitted by law. +\fB\-C\fR/\-\-conf=F +Read spine configuration from file F +.TP +\fB\-O\fR/\-\-option=S:V +Override DB settings 'set' with value 'V' +.TP +\fB\-M\fR/\-\-mibs +Refresh the device System Mib data +.TP +\fB\-N\fR/\-\-mode=online +For remote pollers, the operating mode. +Options include: online, offline, recovery. +The default is 'online'. +.TP +\fB\-R\fR/\-\-readonly +Spine will not write output to the DB +.TP +\fB\-S\fR/\-\-stdout +Logging is performed to standard output +.TP +\fB\-P\fR/\-\-pingonly +Ping device and update device status only +.TP +\fB\-V\fR/\-\-verbosity=V +Set logging verbosity to +.PP +Either both of \fB\-\-first\fR/\-\-last must be provided, a valid hostlist must be provided. +In their absence, all hosts are processed. +.SS "Without the --conf parameter, spine searches in order:" +.IP +current directory, /etc/, /etc/cacti/, ../etc/ for spine.conf. +.PP +Verbosity is one of NONE/LOW/MEDIUM/HIGH/DEBUG or 1..5 +.PP +Runtime options are read from the 'settings' table in the Cacti +database, but they can be overridden with the \fB\-\-option\fR=\fI\,S\/\fR:V +parameter. +.PP +Spine is distributed under the Terms of the GNU Lesser +General Public License Version 2.1. (http://www.gnu.org/licenses/lgpl.txt) +For more information, see http://www.cacti.net diff --git a/spine.c b/spine.c index b711044..53bd41d 100644 --- a/spine.c +++ b/spine.c @@ -101,8 +101,8 @@ /* Global Variables */ int entries = 0; int num_hosts = 0; -sem_t available_threads; -sem_t available_scripts; +spine_sem_t available_threads; +spine_sem_t available_scripts; double start_time; double total_time; @@ -113,6 +113,7 @@ int *debug_devices; pool_t *db_pool_local; pool_t *db_pool_remote; +int icmp_socket = -1; poller_thread_t** details = NULL; @@ -202,7 +203,7 @@ int main(int argc, char *argv[]) { double host_time_double = 0; int items_per_thread = 0; int device_threads; - sem_t thread_init_sem; + spine_sem_t thread_init_sem; int a_threads_value; //struct timespec until_spec; @@ -235,6 +236,9 @@ int main(int argc, char *argv[]) { if (geteuid() == 0) { drop_root(getuid(), getgid()); } + + /* disable core dumps to prevent credential leakage */ + prctl(PR_SET_DUMPABLE, 0); #endif /* HAVE_LCAP */ /* we must initialize snmp in the main thread */ @@ -340,7 +344,7 @@ int main(int argc, char *argv[]) { die("ERROR: %s can only be used once", arg); } - set.start_host_id = atoi(opt = getarg(opt, &argv)); + set.start_host_id = (int)strtol(opt = getarg(opt, &argv), NULL, 10); if (!HOSTID_DEFINED(set.start_host_id)) { die("ERROR: '%s=%s' is invalid first-host ID", arg, opt); @@ -352,7 +356,7 @@ int main(int argc, char *argv[]) { die("ERROR: %s can only be used once", arg); } - set.end_host_id = atoi(opt = getarg(opt, &argv)); + set.end_host_id = (int)strtol(opt = getarg(opt, &argv), NULL, 10); if (!HOSTID_DEFINED(set.end_host_id)) { die("ERROR: '%s=%s' is invalid last-host ID", arg, opt); @@ -360,11 +364,11 @@ int main(int argc, char *argv[]) { } else if (STRIMATCH(arg, "-p") || STRIMATCH(arg, "--poller")) { - set.poller_id = atoi(getarg(opt, &argv)); + set.poller_id = (int)strtol(getarg(opt, &argv), NULL, 10); } else if (STRMATCH(arg, "-t") || STRIMATCH(arg, "--threads")) { - set.threads = atoi(getarg(opt, &argv)); + set.threads = (int)strtol(getarg(opt, &argv), NULL, 10); set.threads_set = TRUE; } @@ -386,6 +390,17 @@ int main(int argc, char *argv[]) { else if (STRIMATCH(arg, "-H") || STRIMATCH(arg, "--hostlist")) { snprintf(set.host_id_list, BIG_BUFSIZE, "%s", getarg(opt, &argv)); + + /* Validate host_id_list contains only digits and commas */ + { + const char *p = set.host_id_list; + while (*p) { + if (!isdigit((unsigned char)*p) && *p != ',' && *p != ' ') { + die("ERROR: --hostlist contains invalid characters. Only digits and commas are allowed."); + } + p++; + } + } } else if (STRIMATCH(arg, "-M") || STRMATCH(arg, "--mibs")) { @@ -438,11 +453,11 @@ int main(int argc, char *argv[]) { } else if (!HOSTID_DEFINED(set.start_host_id) && all_digits(arg)) { - set.start_host_id = atoi(arg); + set.start_host_id = (int)strtol(arg, NULL, 10); } else if (!HOSTID_DEFINED(set.end_host_id) && all_digits(arg)) { - set.end_host_id = atoi(arg); + set.end_host_id = (int)strtol(arg, NULL, 10); } else { @@ -525,7 +540,7 @@ int main(int argc, char *argv[]) { SPINE_LOG_DEBUG(("DEBUG: Selective Debug Devices %s", set.selective_device_debug)); token = strtok(set.selective_device_debug, ","); while(token && debug_idx < MAX_DEBUG_DEVICES - 1) { - debug_devices[debug_idx] = atoi(token); + debug_devices[debug_idx] = (int)strtol(token, NULL, 10); debug_devices[debug_idx+1] = '\0'; token = strtok(NULL, ","); debug_idx++; @@ -700,19 +715,42 @@ int main(int argc, char *argv[]) { init_mutexes(); /* initialize available_threads semaphore */ - sem_init(&available_threads, 0, set.threads); + spine_sem_init(&available_threads, set.threads); /* initialize available_scripts semaphore */ - sem_init(&available_scripts, 0, MAX_SIMULTANEOUS_SCRIPTS); + spine_sem_init(&available_scripts, MAX_SIMULTANEOUS_SCRIPTS); /* initialize thread initialization semaphore */ - sem_init(&thread_init_sem, 0, 1); + spine_sem_init(&thread_init_sem, 1); + + /* Open the ICMP raw socket while still single-threaded and privileged. + * seteuid(0) is process-wide; doing it here avoids the race where + * multiple threads could inherit euid=0 while one holds LOCK_SETEUID. */ + #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) + if (hasCaps() != TRUE) { + if (seteuid(0) == -1) { + SPINE_LOG(("WARNING: Unable to obtain root privileges for ICMP socket.")); + } + } + #endif + icmp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + #if !(defined(__CYGWIN__) && !defined(SOLAR_PRIV)) + if (hasCaps() != TRUE) { + if (seteuid(getuid()) == -1) { + SPINE_LOG(("WARNING: Unable to drop root privileges after ICMP socket open.")); + } + } + #endif + if (icmp_socket < 0) { + SPINE_LOG(("WARNING: Unable to open ICMP raw socket: %s. ICMP ping disabled.", strerror(errno))); + set.icmp_avail = FALSE; + } /* specify the point of timeout for timedwait semaphores */ //until_spec.tv_sec = (time_t)(set.poller_interval + begin_time - 0.2); //until_spec.tv_nsec = 0; - sem_getvalue(&available_threads, &a_threads_value); + spine_sem_getvalue(&available_threads, &a_threads_value); SPINE_LOG_HIGH(("DEBUG: Initial Value of Available Threads is %i (%i outstanding)", a_threads_value, set.threads - a_threads_value)); /* tell fork processes that they are now active */ @@ -747,8 +785,8 @@ int main(int argc, char *argv[]) { if (change_host) { mysql_row = mysql_fetch_row(result); - host_id = atoi(mysql_row[0]); - device_threads = atoi(mysql_row[1]); + host_id = (int)strtol(mysql_row[0], NULL, 10); + device_threads = (int)strtol(mysql_row[1], NULL, 10); current_thread = 1; if (device_threads < 1) { @@ -769,7 +807,7 @@ int main(int argc, char *argv[]) { tresult = db_query(&mysql, LOCAL, querybuf); mysql_row = mysql_fetch_row(tresult); - total_items = atoi(mysql_row[0]); + total_items = (int)strtol(mysql_row[0], NULL, 10); db_free_result(tresult); if (total_items && total_items < device_threads) { @@ -793,7 +831,7 @@ int main(int argc, char *argv[]) { tresult = db_query(&mysql, LOCAL, querybuf); mysql_row = mysql_fetch_row(tresult); - items_per_thread = atoi(mysql_row[0]); + items_per_thread = (int)strtol(mysql_row[0], NULL, 10); db_free_result(tresult); @@ -808,7 +846,7 @@ int main(int argc, char *argv[]) { tresult = db_query(&mysql, LOCAL, querybuf); mysql_row = mysql_fetch_row(tresult); - items_per_thread = atoi(mysql_row[0]); + items_per_thread = (int)strtol(mysql_row[0], NULL, 10); db_free_result(tresult); @@ -845,7 +883,7 @@ int main(int argc, char *argv[]) { spine_timeout = FALSE; while (TRUE) { - sem_err = sem_trywait(&available_threads); + sem_err = spine_sem_trywait(&available_threads); if (sem_err == 0) { /* acquired a thread */ @@ -887,7 +925,7 @@ int main(int argc, char *argv[]) { loop_count = 0; while (!spine_timeout) { - sem_err = sem_trywait(&thread_init_sem); + sem_err = spine_sem_trywait(&thread_init_sem); if (sem_err == 0) { // Acquired a thread @@ -941,10 +979,10 @@ int main(int argc, char *argv[]) { device_counter++; } - sem_getvalue(&available_threads, &a_threads_value); + spine_sem_getvalue(&available_threads, &a_threads_value); SPINE_LOG_HIGH(("DEBUG: Device[%i] Available Threads is %i (%i outstanding)", poller_details->host_id, a_threads_value, set.threads - a_threads_value)); - sem_post(&thread_init_sem); + spine_sem_post(&thread_init_sem); SPINE_LOG_DEVDBG(("DEBUG: DTS: device = %d, host_id = %d, host_thread = %d," " host_threads = %d, host_data_ids = %d, complete = %d", @@ -965,12 +1003,12 @@ int main(int argc, char *argv[]) { /* Restore thread initialization semaphore if thread creation failed */ if (thread_status) { thread_mutex_unlock(LOCK_HOST_TIME); - sem_post(&thread_init_sem); + spine_sem_post(&thread_init_sem); } } } - sem_getvalue(&available_threads, &a_threads_value); + spine_sem_getvalue(&available_threads, &a_threads_value); /* wait for all threads to 'complete' * using the mutex here as the semaphore will @@ -985,7 +1023,7 @@ int main(int argc, char *argv[]) { SPINE_LOG_HIGH(("NOTE: Polling sleeping while waiting for %d Threads to End", set.threads - a_threads_value)); usleep(500000); - sem_getvalue(&available_threads, &a_threads_value); + spine_sem_getvalue(&available_threads, &a_threads_value); } threads_final = set.threads - a_threads_value; @@ -1113,6 +1151,15 @@ int main(int argc, char *argv[]) { } } + /* zero sensitive credentials before exit */ + { + volatile char *vp; + vp = (volatile char *)set.db_pass; + memset((char *)vp, 0, sizeof(set.db_pass)); + vp = (volatile char *)set.rdb_pass; + memset((char *)vp, 0, sizeof(set.rdb_pass)); + } + /* uninstall the spine signal handler */ uninstall_spine_signal_handler(); diff --git a/spine.h b/spine.h index d7e9749..cc8e115 100644 --- a/spine.h +++ b/spine.h @@ -53,6 +53,26 @@ # define __attribute__(x) /* NOTHING */ #endif +/* Function attribute macros for GCC/Clang compile-time checks. + * These expand to nothing on non-GCC/Clang compilers. + */ +#ifdef __GNUC__ +#define SPINE_ATTR_FORMAT(archetype, string_index, first_to_check) \ + __attribute__((format(archetype, string_index, first_to_check))) +#define SPINE_ATTR_NORETURN __attribute__((noreturn)) +#define SPINE_ATTR_WARN_UNUSED __attribute__((warn_unused_result)) +#define SPINE_ATTR_NONNULL(...) __attribute__((nonnull(__VA_ARGS__))) +#define SPINE_ATTR_PURE __attribute__((pure)) +#define SPINE_ATTR_COLD __attribute__((cold)) +#else +#define SPINE_ATTR_FORMAT(archetype, string_index, first_to_check) +#define SPINE_ATTR_NORETURN +#define SPINE_ATTR_WARN_UNUSED +#define SPINE_ATTR_NONNULL(...) +#define SPINE_ATTR_PURE +#define SPINE_ATTR_COLD +#endif + /* Windows does not support stderr. Therefore, don't use it. */ #ifdef __CYGWIN__ #define DISABLE_STDERR @@ -499,7 +519,7 @@ typedef struct poller_thread { int complete; char host_time[40]; double host_time_double; - sem_t *thread_init_sem; + spine_sem_t *thread_init_sem; } poller_thread_t; /*! PHP Script Server Structure @@ -630,9 +650,10 @@ extern config_t set; extern php_t *php_processes; extern char start_datetime[20]; extern char config_paths[CONFIG_PATHS][BUFSIZE]; -extern sem_t available_threads; -extern sem_t available_scripts; +extern spine_sem_t available_threads; +extern spine_sem_t available_scripts; extern pool_t *db_pool_remote; extern pool_t *db_pool_local; +extern int icmp_socket; #endif /* not _SPINE_H_ */ diff --git a/spine_sem.h b/spine_sem.h new file mode 100644 index 0000000..fe8ff3e --- /dev/null +++ b/spine_sem.h @@ -0,0 +1,88 @@ +/* + +-------------------------------------------------------------------------+ + | Copyright (C) 2004-2026 The Cacti Group | + | | + | This program is free software; you can redistribute it and/or | + | modify it under the terms of the GNU General Public License | + | as published by the Free Software Foundation; either version 2 | + | of the License, or (at your option) any later version. | + +-------------------------------------------------------------------------+ + | Cacti: The Complete RRDtool-based Graphing Solution | + +-------------------------------------------------------------------------+ +*/ + +#ifndef SPINE_SEM_H +#define SPINE_SEM_H + +/* + * Portable counting semaphore using pthreads. + * + * macOS deprecated unnamed POSIX semaphores (sem_init, sem_getvalue). + * This wrapper provides the same counting semantics using a pthread + * mutex + condition variable, which works on all POSIX platforms. + * + * Spine uses semaphores as atomic counters (sem_post, sem_getvalue, sem_trywait). + */ + +#include +#include + +typedef struct { + pthread_mutex_t mutex; + pthread_cond_t cond; + int value; +} spine_sem_t; + +static inline int spine_sem_init(spine_sem_t *s, int value) { + if (pthread_mutex_init(&s->mutex, NULL) != 0) return -1; + if (pthread_cond_init(&s->cond, NULL) != 0) { + pthread_mutex_destroy(&s->mutex); + return -1; + } + s->value = value; + return 0; +} + +static inline int spine_sem_post(spine_sem_t *s) { + pthread_mutex_lock(&s->mutex); + s->value++; + pthread_cond_signal(&s->cond); + pthread_mutex_unlock(&s->mutex); + return 0; +} + +static inline int spine_sem_getvalue(spine_sem_t *s, int *val) { + pthread_mutex_lock(&s->mutex); + *val = s->value; + pthread_mutex_unlock(&s->mutex); + return 0; +} + +static inline int spine_sem_wait(spine_sem_t *s) { + pthread_mutex_lock(&s->mutex); + while (s->value <= 0) + pthread_cond_wait(&s->cond, &s->mutex); + s->value--; + pthread_mutex_unlock(&s->mutex); + return 0; +} + +static inline int spine_sem_trywait(spine_sem_t *s) { + pthread_mutex_lock(&s->mutex); + if (s->value <= 0) { + pthread_mutex_unlock(&s->mutex); + errno = EAGAIN; + return -1; + } + s->value--; + pthread_mutex_unlock(&s->mutex); + return 0; +} + +static inline int spine_sem_destroy(spine_sem_t *s) { + pthread_mutex_destroy(&s->mutex); + pthread_cond_destroy(&s->cond); + return 0; +} + +#endif /* SPINE_SEM_H */ diff --git a/sql.c b/sql.c index 2e1de20..d15f35d 100644 --- a/sql.c +++ b/sql.c @@ -252,6 +252,7 @@ void db_connect(int type, MYSQL *mysql) { if (stat(hostname, &socket_stat) == 0) { if (socket_stat.st_mode & S_IFSOCK) { socket = strdup (set.db_host); + free(hostname); hostname = NULL; } } else if ((socket = strstr(hostname,":"))) { @@ -266,6 +267,7 @@ void db_connect(int type, MYSQL *mysql) { if (stat(hostname, &socket_stat) == 0) { if (socket_stat.st_mode & S_IFSOCK) { socket = strdup (set.db_host); + free(hostname); hostname = NULL; } } else if ((socket = strstr(hostname,":"))) { @@ -585,16 +587,21 @@ void db_escape(MYSQL *mysql, char *output, int max_size, const char *input) { char input_trimmed[DBL_BUFSIZE]; int max_escaped_input_size; int trim_limit; + int input_cap; if (input == NULL) return; max_escaped_input_size = (strlen(input) * 2) + 1; trim_limit = (max_size < DBL_BUFSIZE) ? max_size : DBL_BUFSIZE; + /* always cap input to (max_size / 2) - 1 to prevent output overflow */ + input_cap = (trim_limit / 2) - 1; + if (input_cap < 1) input_cap = 1; + if (max_escaped_input_size > max_size) { - snprintf(input_trimmed, (trim_limit / 2) - 1, "%s", input); + snprintf(input_trimmed, input_cap, "%s", input); } else { - snprintf(input_trimmed, trim_limit, "%s", input); + snprintf(input_trimmed, input_cap, "%s", input); } mysql_real_escape_string(mysql, output, input_trimmed, strlen(input_trimmed)); @@ -606,8 +613,17 @@ void db_free_result(MYSQL_RES *result) { int db_column_exists(MYSQL *mysql, int type, const char *table, const char *column) { char query_frag[BUFSIZE]; - MYSQL_RES *result; + MYSQL_RES *result; int exists; + const char *p; + + /* validate column name: only alphanumeric and underscore allowed */ + for (p = column; *p; p++) { + if (!isalnum((unsigned char)*p) && *p != '_') { + SPINE_LOG(("ERROR: db_column_exists: invalid column name '%s'", column)); + return FALSE; + } + } /* save a fragment just in case */ memset(query_frag, 0, BUFSIZE); diff --git a/sql.h b/sql.h index a881387..c56b900 100644 --- a/sql.h +++ b/sql.h @@ -31,20 +31,30 @@ +-------------------------------------------------------------------------+ */ -extern int db_insert(MYSQL *mysql, int type, const char *query); -extern MYSQL_RES *db_query(MYSQL *mysql, int type, const char *query); -extern void db_connect(int type, MYSQL *mysql); -extern void db_disconnect(MYSQL *mysql); -extern void db_escape(MYSQL *mysql, char *output, int max_size, const char *input); +extern int db_insert(MYSQL *mysql, int type, const char *query) + SPINE_ATTR_NONNULL(1, 3); +extern MYSQL_RES *db_query(MYSQL *mysql, int type, const char *query) + SPINE_ATTR_NONNULL(1, 3) + SPINE_ATTR_WARN_UNUSED; +extern void db_connect(int type, MYSQL *mysql) + SPINE_ATTR_NONNULL(2); +extern void db_disconnect(MYSQL *mysql) + SPINE_ATTR_NONNULL(1); +extern void db_escape(MYSQL *mysql, char *output, int max_size, const char *input) + SPINE_ATTR_NONNULL(1, 2, 4); extern void db_free_result(MYSQL_RES *result); extern void db_create_connection_pool(int type); extern void db_close_connection_pool(int type); -extern pool_t *db_get_connection(int type); +extern pool_t *db_get_connection(int type) + SPINE_ATTR_WARN_UNUSED; extern void db_release_connection(int type, int id); -extern int db_reconnect(MYSQL *mysql, int type, int error, const char *location); -extern int db_column_exists(MYSQL *mysql, int type, const char *table, const char *column); +extern int db_reconnect(MYSQL *mysql, int type, int error, const char *location) + SPINE_ATTR_NONNULL(1); +extern int db_column_exists(MYSQL *mysql, int type, const char *table, const char *column) + SPINE_ATTR_NONNULL(1, 3, 4); -extern int append_hostrange(char *obuf, const char *colname); +extern int append_hostrange(char *obuf, const char *colname) + SPINE_ATTR_NONNULL(1, 2); #define MYSQL_SET_OPTION(opt, value, desc) \ {\ diff --git a/tests/unit/test_sql_buffer.c b/tests/unit/test_sql_buffer.c new file mode 100644 index 0000000..4318ba7 --- /dev/null +++ b/tests/unit/test_sql_buffer.c @@ -0,0 +1,209 @@ +/* + * Unit tests for the sql_buffer_t dynamic SQL buffer. + * + * Covers: + * - sql_buffer_init / sql_buffer_free lifecycle + * - sql_buffer_append normal writes and reallocation + * - overflow guard: requests exceeding SQL_MAX_BUFFER_CAPACITY must fail + * + * Self-contained: the sql_buffer_t type and sql_buffer_append are stubbed + * inline so this file compiles without MySQL or SNMP headers. + */ + +#include +#include +#include +#include + +#include +#include +#include + +/* Maximum buffer size the production code enforces. */ +#ifndef SQL_MAX_BUFFER_CAPACITY +#define SQL_MAX_BUFFER_CAPACITY (64 * 1024 * 1024) /* 64 MiB */ +#endif + +/* ------------------------------------------------------------------------- + * Minimal sql_buffer_t stub matching the production interface. + * + * The production sql_buffer_append checks SQL_MAX_BUFFER_CAPACITY twice: + * 1. Before doubling: if required_capacity already exceeds the cap, fail. + * 2. After doubling: if the doubled new_capacity wraps or overflows, fail. + * + * The stub here mirrors that logic exactly so tests remain meaningful when + * the real implementation is later linked in. + * ------------------------------------------------------------------------- */ + +typedef struct { + char *buffer; + size_t length; + size_t capacity; +} sql_buffer_t; + +static int sql_buffer_init(sql_buffer_t *sb, size_t initial_capacity) { + if (sb == NULL || initial_capacity == 0) return -1; + + sb->buffer = (char *)malloc(initial_capacity); + if (sb->buffer == NULL) return -1; + + sb->buffer[0] = '\0'; + sb->length = 0; + sb->capacity = initial_capacity; + return 0; +} + +static void sql_buffer_free(sql_buffer_t *sb) { + if (sb == NULL) return; + free(sb->buffer); + sb->buffer = NULL; + sb->length = 0; + sb->capacity = 0; +} + +static int sql_buffer_append(sql_buffer_t *sb, const char *format, ...) { + va_list args; + va_list args_copy; + int written; + size_t available; + size_t required_capacity; + size_t new_capacity; + char *new_buffer; + + if (sb == NULL || format == NULL) return -1; + + va_start(args, format); + va_copy(args_copy, args); + + available = sb->capacity - sb->length; + written = vsnprintf(sb->buffer + sb->length, available, format, args); + va_end(args); + + if (written < 0) { + va_end(args_copy); + return -1; + } + + if ((size_t)written >= available) { + required_capacity = sb->length + (size_t)written + 1; + if (required_capacity > SQL_MAX_BUFFER_CAPACITY) { + va_end(args_copy); + return -1; + } + + new_capacity = sb->capacity; + while (new_capacity < required_capacity) new_capacity *= 2; + + if (new_capacity > SQL_MAX_BUFFER_CAPACITY) { + va_end(args_copy); + return -1; + } + + new_buffer = (char *)realloc(sb->buffer, new_capacity); + if (new_buffer == NULL) { + va_end(args_copy); + return -1; + } + + sb->buffer = new_buffer; + sb->capacity = new_capacity; + + written = vsnprintf(sb->buffer + sb->length, sb->capacity - sb->length, format, args_copy); + } + va_end(args_copy); + + sb->length += (size_t)written; + return 0; +} + +/* ------------------------------------------------------------------------- + * Tests + * ------------------------------------------------------------------------- */ + +static void test_sql_buffer_init(void **state) { + (void)state; + + sql_buffer_t sb; + int ret = sql_buffer_init(&sb, 1024); + assert_int_equal(ret, 0); + assert_int_equal((int)sb.capacity, 1024); + assert_int_equal((int)sb.length, 0); + assert_int_equal(sb.buffer[0], '\0'); + sql_buffer_free(&sb); +} + +static void test_sql_buffer_append_simple(void **state) { + (void)state; + + sql_buffer_t sb; + sql_buffer_init(&sb, 16); + + int ret = sql_buffer_append(&sb, "%s", "hello"); + assert_int_equal(ret, 0); + assert_int_equal((int)sb.length, 5); + assert_string_equal(sb.buffer, "hello"); + + sql_buffer_free(&sb); +} + +static void test_sql_buffer_append_triggers_realloc(void **state) { + (void)state; + + sql_buffer_t sb; + sql_buffer_init(&sb, 16); + + int ret = sql_buffer_append(&sb, "%s", "hello"); + assert_int_equal(ret, 0); + + /* Force a reallocation: append a string longer than remaining capacity. */ + ret = sql_buffer_append(&sb, " world. This is a longer string that will force a reallocation."); + assert_int_equal(ret, 0); + assert_true(sb.length > 15); + assert_true(sb.capacity > 16); + + sql_buffer_free(&sb); +} + +static void test_sql_buffer_append_overflow_rejected(void **state) { + (void)state; + + sql_buffer_t sb; + sql_buffer_init(&sb, 64); + + /* Fabricate a request that would exceed SQL_MAX_BUFFER_CAPACITY. + * We do this by pre-filling length to just below the cap and then + * requesting one more byte than remains allowed. */ + sb.length = SQL_MAX_BUFFER_CAPACITY - 4; + + /* required_capacity = length + strlen("hello") + 1 = cap + 2, over limit. */ + int ret = sql_buffer_append(&sb, "%s", "hello"); + assert_int_equal(ret, -1); + + /* length must be unchanged: the guard must not modify the buffer. */ + assert_int_equal((int)sb.length, (int)(SQL_MAX_BUFFER_CAPACITY - 4)); + + sql_buffer_free(&sb); +} + +static void test_sql_buffer_free_null_safe(void **state) { + (void)state; + + /* Must not crash on NULL. */ + sql_buffer_free(NULL); +} + +/* ------------------------------------------------------------------------- + * main + * ------------------------------------------------------------------------- */ + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_sql_buffer_init), + cmocka_unit_test(test_sql_buffer_append_simple), + cmocka_unit_test(test_sql_buffer_append_triggers_realloc), + cmocka_unit_test(test_sql_buffer_append_overflow_rejected), + cmocka_unit_test(test_sql_buffer_free_null_safe), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/util.c b/util.c index cf0e9ce..3bca2e7 100644 --- a/util.c +++ b/util.c @@ -96,7 +96,7 @@ static char *getsetting(MYSQL *psql, int mode, const char *setting) { } } - sprintf(qstring, "SELECT SQL_NO_CACHE value FROM settings WHERE name = '%s'", setting); + snprintf(qstring, sizeof(qstring), "SELECT SQL_NO_CACHE value FROM settings WHERE name = '%s'", setting); result = db_query(psql, mode, qstring); @@ -138,11 +138,11 @@ static int putsetting(MYSQL *psql, int mode, const char *mysetting, const char * assert(myvalue != 0); if (set.dbonupdate == 0) { - sprintf(qstring, "INSERT INTO settings (name, value) " + snprintf(qstring, sizeof(qstring), "INSERT INTO settings (name, value) " "VALUES ('%s', '%s') " "ON DUPLICATE KEY UPDATE value = VALUES(value)", mysetting, myvalue); } else { - sprintf(qstring, "INSERT INTO settings (name, value) " + snprintf(qstring, sizeof(qstring), "INSERT INTO settings (name, value) " "VALUES ('%s', '%s') AS rs " "ON DUPLICATE KEY UPDATE value = rs.value", mysetting, myvalue); } @@ -188,7 +188,7 @@ static char *getpsetting(MYSQL *psql, int mode, const char *setting) { } } - sprintf(qstring, "SELECT SQL_NO_CACHE %s FROM poller WHERE id = '%d'", setting, set.poller_id); + snprintf(qstring, sizeof(qstring), "SELECT SQL_NO_CACHE %s FROM poller WHERE id = '%d'", setting, set.poller_id); result = db_query(psql, mode, qstring); @@ -283,7 +283,7 @@ static char *getglobalvariable(MYSQL *psql, int mode, const char *setting) { } } - sprintf(qstring, "SHOW GLOBAL VARIABLES LIKE '%s'", setting); + snprintf(qstring, sizeof(qstring), "SHOW GLOBAL VARIABLES LIKE '%s'", setting); result = db_query(psql, mode, qstring); @@ -383,7 +383,7 @@ void read_config_options(void) { /* get logging level from database - overrides spine.conf */ if ((res = getsetting(&mysql, LOCAL, "log_verbosity")) != 0) { - const int n = atoi(res); + const int n = (int)strtol(res, NULL, 10); free(res); if (n != 0) set.log_level = n; } @@ -413,7 +413,7 @@ void read_config_options(void) { /* get log separator */ if ((res = getsetting(&mysql, LOCAL, "default_datechar")) != 0) { - set.log_datetime_separator = atoi(res); + set.log_datetime_separator = (int)strtol(res, NULL, 10); free(res); if (set.log_datetime_separator < GDC_MIN || set.log_datetime_separator > GDC_MAX) { @@ -423,7 +423,7 @@ void read_config_options(void) { /* get log separator */ if ((res = getsetting(&mysql, LOCAL, "default_datechar")) != 0) { - set.log_datetime_separator = atoi(res); + set.log_datetime_separator = (int)strtol(res, NULL, 10); free(res); if (set.log_datetime_separator < GDC_MIN || set.log_datetime_separator > GDC_MAX) { @@ -466,7 +466,7 @@ void read_config_options(void) { /* set availability_method */ if ((res = getsetting(&mysql, LOCAL, "availability_method")) != 0) { - set.availability_method = atoi(res); + set.availability_method = (int)strtol(res, NULL, 10); free(res); } @@ -475,7 +475,7 @@ void read_config_options(void) { /* set ping_recovery_count */ if ((res = getsetting(&mysql, LOCAL, "ping_recovery_count")) != 0) { - set.ping_recovery_count = atoi(res); + set.ping_recovery_count = (int)strtol(res, NULL, 10); free(res); } @@ -484,7 +484,7 @@ void read_config_options(void) { /* set ping_failure_count */ if ((res = getsetting(&mysql, LOCAL, "ping_failure_count")) != 0) { - set.ping_failure_count = atoi(res); + set.ping_failure_count = (int)strtol(res, NULL, 10); free(res); } @@ -493,7 +493,7 @@ void read_config_options(void) { /* set ping_method */ if ((res = getsetting(&mysql, LOCAL, "ping_method")) != 0) { - set.ping_method = atoi(res); + set.ping_method = (int)strtol(res, NULL, 10); free(res); } @@ -502,7 +502,7 @@ void read_config_options(void) { /* set ping_retries */ if ((res = getsetting(&mysql, LOCAL, "ping_retries")) != 0) { - set.ping_retries = atoi(res); + set.ping_retries = (int)strtol(res, NULL, 10); free(res); } @@ -511,7 +511,7 @@ void read_config_options(void) { /* set ping_timeout */ if ((res = getsetting(&mysql, LOCAL, "ping_timeout")) != 0) { - set.ping_timeout = atoi(res); + set.ping_timeout = (int)strtol(res, NULL, 10); free(res); } else { set.ping_timeout = 400; @@ -522,7 +522,7 @@ void read_config_options(void) { /* set snmp_retries */ if ((res = getsetting(&mysql, LOCAL, "snmp_retries")) != 0) { - set.snmp_retries = atoi(res); + set.snmp_retries = (int)strtol(res, NULL, 10); free(res); } else { set.snmp_retries = 3; @@ -564,7 +564,7 @@ void read_config_options(void) { /* get Cacti defined max threads override spine.conf */ if (set.threads_set == FALSE) { if ((res = getpsetting(&mysql, mode, "threads")) != 0) { - set.threads = atoi(res); + set.threads = (int)strtol(res, NULL, 10); free(res); if (set.threads > MAX_THREADS) { set.threads = MAX_THREADS; @@ -577,7 +577,7 @@ void read_config_options(void) { /* get the poller_interval for those who have elected to go with a 1 minute polling interval */ if ((res = getsetting(&mysql, LOCAL, "poller_interval")) != 0) { - set.poller_interval = atoi(res); + set.poller_interval = (int)strtol(res, NULL, 10); free(res); } else { set.poller_interval = 0; @@ -592,7 +592,7 @@ void read_config_options(void) { /* get the concurrent_processes variable to determine thread sleep values */ if ((res = getsetting(&mysql, LOCAL, "concurrent_processes")) != 0) { - set.num_parent_processes = atoi(res); + set.num_parent_processes = (int)strtol(res, NULL, 10); free(res); } else { set.num_parent_processes = 1; @@ -603,7 +603,7 @@ void read_config_options(void) { /* get the script timeout to establish timeouts */ if ((res = getsetting(&mysql, LOCAL, "script_timeout")) != 0) { - set.script_timeout = atoi(res); + set.script_timeout = (int)strtol(res, NULL, 10); free(res); if (set.script_timeout < 5) { set.script_timeout = 5; @@ -626,7 +626,7 @@ void read_config_options(void) { /* get spine_log_level */ if ((res = getsetting(&mysql, LOCAL, "spine_log_level")) != 0) { - set.spine_log_level = atoi(res); + set.spine_log_level = (int)strtol(res, NULL, 10); free(res); } @@ -635,7 +635,7 @@ void read_config_options(void) { /* get the number of script server processes to run */ if ((res = getsetting(&mysql, LOCAL, "php_servers")) != 0) { - set.php_servers = atoi(res); + set.php_servers = (int)strtol(res, NULL, 10); free(res); if (set.php_servers > MAX_PHP_SERVERS) { @@ -654,7 +654,7 @@ void read_config_options(void) { /* get the number of active profiles on the system run */ if ((res = getsetting(&mysql, LOCAL, "active_profiles")) != 0) { - set.active_profiles = atoi(res); + set.active_profiles = (int)strtol(res, NULL, 10); free(res); if (set.active_profiles <= 0) { @@ -669,7 +669,7 @@ void read_config_options(void) { /* get the number of snmp_ports in use */ if ((res = getsetting(&mysql, LOCAL, "total_snmp_ports")) != 0) { - set.total_snmp_ports = atoi(res); + set.total_snmp_ports = (int)strtol(res, NULL, 10); free(res); if (set.total_snmp_ports <= 0) { @@ -736,7 +736,7 @@ void read_config_options(void) { /* determine the maximum oid's to obtain in a single get request */ if ((res = getsetting(&mysql, LOCAL, "max_get_size")) != 0) { - set.snmp_max_get_size = atoi(res); + set.snmp_max_get_size = (int)strtol(res, NULL, 10); free(res); if (set.snmp_max_get_size > 128) { @@ -749,38 +749,43 @@ void read_config_options(void) { /* log the snmp_max_get_size variable */ SPINE_LOG_DEBUG(("DEBUG: The Maximum SNMP OID Get Size is %i", set.snmp_max_get_size)); - #ifndef NETSNMP_DISABLE_MD5 - strcat(spine_auth, "MD5"); - #endif + { + int off = 0; - strcat(spine_auth, (strlen(spine_auth) > 0 ? ",SHA":"SHA")); + #ifndef NETSNMP_DISABLE_MD5 + off += snprintf(spine_auth + off, sizeof(spine_auth) - off, "MD5"); + #endif - #if defined(NETSNMP_USMAUTH_HMAC128SHA224) - strcat(spine_auth, ",SHA224,SHA256"); - #endif + off += snprintf(spine_auth + off, sizeof(spine_auth) - off, "%sSHA", off > 0 ? "," : ""); - #if defined(NETSNMP_USMAUTH_HMAC192SHA256) - strcat(spine_auth, ",SHA384,SHA512"); - #endif + #if defined(NETSNMP_USMAUTH_HMAC128SHA224) + off += snprintf(spine_auth + off, sizeof(spine_auth) - off, ",SHA224,SHA256"); + #endif - #ifndef NETSNMP_DISABLE_DES - strcat(spine_priv, "DES"); - #endif + #if defined(NETSNMP_USMAUTH_HMAC192SHA256) + off += snprintf(spine_auth + off, sizeof(spine_auth) - off, ",SHA384,SHA512"); + #endif + } - #ifdef HAVE_AES - // cppcheck-suppress knownConditionTrueFalse - strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES128":"AES128")); - #endif + { + int off = 0; - #if defined(NETSNMP_DRAFT_BLUMENTHAL_AES_04) - // cppcheck-suppress knownConditionTrueFalse - strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES192":"AES192")); - #endif + #ifndef NETSNMP_DISABLE_DES + off += snprintf(spine_priv + off, sizeof(spine_priv) - off, "DES"); + #endif - #if defined(NETSNMP_DRAFT_BLUMENTHAL_AES_04) - // cppcheck-suppress knownConditionTrueFalse - strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES256":"AES256")); - #endif + #ifdef HAVE_AES + off += snprintf(spine_priv + off, sizeof(spine_priv) - off, "%sAES128", off > 0 ? "," : ""); + #endif + + #if defined(NETSNMP_DRAFT_BLUMENTHAL_AES_04) + off += snprintf(spine_priv + off, sizeof(spine_priv) - off, "%sAES192", off > 0 ? "," : ""); + #endif + + #if defined(NETSNMP_DRAFT_BLUMENTHAL_AES_04) + off += snprintf(spine_priv + off, sizeof(spine_priv) - off, "%sAES256", off > 0 ? "," : ""); + #endif + } snprintf(spine_capabilities, BUFSIZE, "{ authProtocols: \"%s\", privProtocols: \"%s\" }", spine_auth, spine_priv); @@ -897,59 +902,87 @@ void poller_push_data_to_main(void) { rows = 0; if (num_rows > 0) { + int remaining; + while ((row = mysql_fetch_row(result))) { if (rows < 500) { if (rows == 0) { sqlp = sqlbuf; - sqlp += sprintf(sqlp, "%s", prefix); - sqlp += sprintf(sqlp, " ("); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s", prefix); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, " ("); } else { - sqlp += sprintf(sqlp, ", ("); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, ", ("); } - sqlp += sprintf(sqlp, "%s, ", row[0]); // id mediumint + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[0]); // id mediumint db_escape(&mysql, tmpstr, sizeof(tmpstr), row[1]); // snmp_sysDescr varchar(300) - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[2]); // snmp_sysObjectID varchar(128) - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[3]); // snmp_sysUpTimeInstance bigint - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[4]); // snmp_sysContact varchar(300) - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[5]); // snmp_sysName varchar(300) - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[6]); // snmp_sysLocation varchar(300) - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[7]); // status tinyint - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); - sqlp += sprintf(sqlp, "%s, ", row[8]); // status_event_count mediumint + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[8]); // status_event_count mediumint db_escape(&mysql, tmpstr, sizeof(tmpstr), row[9]); // status_fail_date timestamp - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[10]); // status_rec_date timestamp - sqlp += sprintf(sqlp, "'%s', ", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); db_escape(&mysql, tmpstr, sizeof(tmpstr), row[11]); // status_last_error varchar(255) - sqlp += sprintf(sqlp, "'%s', ", tmpstr); - - sqlp += sprintf(sqlp, "%s, ", row[12]); // min_time decimal(10,5) - sqlp += sprintf(sqlp, "%s, ", row[13]); // max_time decimal(10,5) - sqlp += sprintf(sqlp, "%s, ", row[14]); // cur_time decimal(10,5) - sqlp += sprintf(sqlp, "%s, ", row[15]); // avg_time decimal(10,5) - sqlp += sprintf(sqlp, "%s, ", row[16]); // polling_time double - sqlp += sprintf(sqlp, "%s, ", row[17]); // total_polls int - sqlp += sprintf(sqlp, "%s, ", row[18]); // failed_polls int - sqlp += sprintf(sqlp, "%s, ", row[19]); // availability decimal(8,5) + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s', ", tmpstr); + + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[12]); // min_time decimal(10,5) + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[13]); // max_time decimal(10,5) + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[14]); // cur_time decimal(10,5) + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[15]); // avg_time decimal(10,5) + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[16]); // polling_time double + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[17]); // total_polls int + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[18]); // failed_polls int + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s, ", row[19]); // availability decimal(8,5) db_escape(&mysql, tmpstr, sizeof(tmpstr), row[20]); // last_updated timestamp - sqlp += sprintf(sqlp, "'%s'", tmpstr); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "'%s'", tmpstr); - sqlp += sprintf(sqlp, ")"); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, ")"); rows++; } else { - sqlp += sprintf(sqlp, "%s", suffix); + remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s", suffix); db_insert(&mysqlr, REMOTE, sqlbuf); rows = 0; @@ -958,7 +991,8 @@ void poller_push_data_to_main(void) { } if (rows > 0) { - sqlp += sprintf(sqlp, "%s", suffix); + int remaining = HUGE_BUFSIZE - (sqlp - sqlbuf); + sqlp += snprintf(sqlp, remaining, "%s", suffix); db_insert(&mysqlr, REMOTE, sqlbuf); } } @@ -1070,12 +1104,16 @@ int read_spine_config(const char *file) { if (chars != NULL && !feof(fp) && *buff != '#' && *buff != ' ' && *buff != '\n') { sscanf(buff, "%15s %255s", p1, p2); + if (strlen(p2) >= 255) { + SPINE_LOG(("WARNING: Configuration value for '%s' may be truncated", p1)); + } + if (STRIMATCH(p1, "RDB_Host")) STRNCOPY(set.rdb_host, p2); else if (STRIMATCH(p1, "RDB_Database")) STRNCOPY(set.rdb_db, p2); else if (STRIMATCH(p1, "RDB_User")) STRNCOPY(set.rdb_user, p2); else if (STRIMATCH(p1, "RDB_Pass")) STRNCOPY(set.rdb_pass, p2); - else if (STRIMATCH(p1, "RDB_Port")) set.rdb_port = atoi(p2); - else if (STRIMATCH(p1, "RDB_UseSSL")) set.rdb_ssl = atoi(p2); + else if (STRIMATCH(p1, "RDB_Port")) set.rdb_port = (int)strtol(p2, NULL, 10); + else if (STRIMATCH(p1, "RDB_UseSSL")) set.rdb_ssl = (int)strtol(p2, NULL, 10); else if (STRIMATCH(p1, "RDB_SSL_Key")) STRNCOPY(set.rdb_ssl_key, p2); else if (STRIMATCH(p1, "RDB_SSL_Cert")) STRNCOPY(set.rdb_ssl_cert, p2); else if (STRIMATCH(p1, "RDB_SSL_CA")) STRNCOPY(set.rdb_ssl_ca, p2); @@ -1083,12 +1121,12 @@ int read_spine_config(const char *file) { else if (STRIMATCH(p1, "DB_Database")) STRNCOPY(set.db_db, p2); else if (STRIMATCH(p1, "DB_User")) STRNCOPY(set.db_user, p2); else if (STRIMATCH(p1, "DB_Pass")) STRNCOPY(set.db_pass, p2); - else if (STRIMATCH(p1, "DB_Port")) set.db_port = atoi(p2); - else if (STRIMATCH(p1, "DB_UseSSL")) set.db_ssl = atoi(p2); + else if (STRIMATCH(p1, "DB_Port")) set.db_port = (int)strtol(p2, NULL, 10); + else if (STRIMATCH(p1, "DB_UseSSL")) set.db_ssl = (int)strtol(p2, NULL, 10); else if (STRIMATCH(p1, "DB_SSL_Key")) STRNCOPY(set.db_ssl_key, p2); else if (STRIMATCH(p1, "DB_SSL_Cert")) STRNCOPY(set.db_ssl_cert, p2); else if (STRIMATCH(p1, "DB_SSL_CA")) STRNCOPY(set.db_ssl_ca, p2); - else if (STRIMATCH(p1, "Poller")) set.poller_id = atoi(p2); + else if (STRIMATCH(p1, "Poller")) set.poller_id = (int)strtol(p2, NULL, 10); else if (STRIMATCH(p1, "DB_PreG")) { if (!set.stderr_notty) { fprintf(stderr,"WARNING: DB_PreG is no longer supported\n"); @@ -1222,18 +1260,25 @@ char *get_date_format(void) { switch (set.log_datetime_format) { case GD_MO_D_Y: snprintf(log_fmt, GD_FMT_SIZE, "%%m%c%%d%c%%Y %%H:%%M:%%S - ", log_sep, log_sep); + break; case GD_MN_D_Y: snprintf(log_fmt, GD_FMT_SIZE, "%%b%c%%d%c%%Y %%H:%%M:%%S - ", log_sep, log_sep); + break; case GD_D_MO_Y: snprintf(log_fmt, GD_FMT_SIZE, "%%d%c%%m%c%%Y %%H:%%M:%%S - ", log_sep, log_sep); + break; case GD_D_MN_Y: snprintf(log_fmt, GD_FMT_SIZE, "%%d%c%%b%c%%Y %%H:%%M:%%S - ", log_sep, log_sep); + break; case GD_Y_MO_D: snprintf(log_fmt, GD_FMT_SIZE, "%%Y%c%%m%c%%d %%H:%%M:%%S - ", log_sep, log_sep); + break; case GD_Y_MN_D: snprintf(log_fmt, GD_FMT_SIZE, "%%Y%c%%b%c%%d %%H:%%M:%%S - ", log_sep, log_sep); + break; default: snprintf(log_fmt, GD_FMT_SIZE, "%%Y%c%%m%c%%d %%H:%%M:%%S - ", log_sep, log_sep); + break; } return (log_fmt); @@ -1365,7 +1410,11 @@ int spine_log(const char *format, ...) { /* append a line feed to the log message if needed */ if (!strstr(flogmessage, "\n")) { - strcat(flogmessage, "\n"); + size_t flog_len = strlen(flogmessage); + if (flog_len < LOGSIZE - 1) { + flogmessage[flog_len] = '\n'; + flogmessage[flog_len + 1] = '\0'; + } } if ((IS_LOGGING_TO_FILE() && @@ -1634,12 +1683,12 @@ char *add_slashes(char *string) { int new_position; char *return_str; - if (!(return_str = (char *) malloc(BUFSIZE))) { + length = strlen(string); + + if (!(return_str = (char *) malloc(length * 2 + 1))) { die("ERROR: Fatal malloc error: util.c add_slashes!"); } return_str[0] = '\0'; - - length = strlen(string); position = 0; new_position = 0; @@ -2033,7 +2082,7 @@ int get_cacti_version(MYSQL *psql, int mode) { assert(psql != 0); - sprintf(qstring, "SELECT cacti FROM version LIMIT 1"); + snprintf(qstring, sizeof(qstring), "SELECT cacti FROM version LIMIT 1"); result = db_query(psql, mode, qstring); diff --git a/util.h b/util.h index ae735b9..48e8747 100644 --- a/util.h +++ b/util.h @@ -38,20 +38,25 @@ extern void config_defaults(void); /* cacti logging function */ extern int spine_log(const char *format, ...) - __attribute__((format(printf, 1, 2))); + SPINE_ATTR_FORMAT(printf, 1, 2); extern void die(const char *format, ...) - __attribute__((noreturn)) - __attribute__((format(printf, 1, 2))); + SPINE_ATTR_NORETURN + SPINE_ATTR_COLD + SPINE_ATTR_FORMAT(printf, 1, 2); /* option processing function */ extern void set_option(const char *setting, const char *value); /* number validation functions */ -extern int is_numeric(char *string); -extern int is_ipaddress(const char *string); -extern int all_digits(const char *str); -extern int is_hexadecimal(const char * str, const short ignore_special); +extern int is_numeric(char *string) + SPINE_ATTR_PURE; +extern int is_ipaddress(const char *string) + SPINE_ATTR_PURE; +extern int all_digits(const char *str) + SPINE_ATTR_PURE; +extern int is_hexadecimal(const char * str, const short ignore_special) + SPINE_ATTR_PURE; /* determine if a device is a debug device */ extern int is_debug_device(int device_id); @@ -60,7 +65,8 @@ extern int is_debug_device(int device_id); extern char *add_slashes(char *string); extern int file_exists(const char *filename); extern char *strip_alpha(char *string); -extern char *strncopy(char *dst, const char *src, size_t n); +extern char *strncopy(char *dst, const char *src, size_t n) + SPINE_ATTR_NONNULL(1, 2); extern char *trim(char *str); extern char *rtrim(char *str); extern char *ltrim(char *str);