mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
Open and own the required ICMP sockets before permanently dropping setuid root, then parse input without elevated identity. Close-on-exec socket ownership, shared IPv4/IPv6 dispatch and fault-injected identity/capability calls are covered. The owned fixture tests check deny/allow behavior without contacting external devices. Step 2/14 of the complete C17/native-correctness series. Base: the preceding series branch; merge/review in order so this PR shows only its own step. Earlier validation, before this review follow-up, in isolated copied snapshots on Linux arm64: - Clean GCC build and make check: 13 passed, 1 existing prerequisite skips, 0 failures. - ASan/UBSan: 12 passed, 2 existing privilege/allocator-interposition prerequisite skips, 0 failures; no sanitizer findings. - Production source/profile integrity verification against disposable MariaDB and loopback SNMP fixtures passed. - Release archive extracted into a separate source tree and built/tested out of tree; fuzz checks passed. Earlier local profiles retain source hashes for the copied validation tree; GitHub checks validate the published commit. No production database/device, host installation or live privilege change is part of validation. Existing prerequisite skips were retained; no test or finding was suppressed to obtain a pass. Series navigation: [Series index](https://github.com/Cacti/spine/pull/638); [previous step](https://github.com/Cacti/spine/pull/638). [Next step](https://github.com/Cacti/spine/pull/640). Review follow-up (2026-10-09): Restacked on the corrected preceding branches. The SNMP status fix is introduced in #638, the reindex/completion fixes in #640, and focused regression coverage in #650. Restacked tip validation: isolated Linux arm64 build and make check passed (18 passed, 1 setuid prerequisite skip, no failures). GitHub checks validate the updated published commits; earlier profile/archive/sanitizer evidence above belongs to the previous revisions. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
979 lines
33 KiB
C
979 lines
33 KiB
C
/*
|
|
ex: set tabstop=4 shiftwidth=4 autoindent:
|
|
+-------------------------------------------------------------------------+
|
|
| 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 Lesser General Public |
|
|
| License as published by the Free Software Foundation; either |
|
|
| version 2.1 of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU Lesser General Public License for more details. |
|
|
| |
|
|
| You should have received a copy of the GNU Lesser General Public |
|
|
| License along with this library; if not, write to the Free Software |
|
|
| Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|
|
| 02110-1301, USA |
|
|
| |
|
|
+-------------------------------------------------------------------------+
|
|
| spine: a backend data gatherer for cacti |
|
|
+-------------------------------------------------------------------------+
|
|
| This poller would not have been possible without: |
|
|
| - Larry Adams (current development and enhancements) |
|
|
| - Rivo Nurges (rrd support, mysql poller cache, misc functions) |
|
|
| - RTG (core poller code, pthreads, snmp, autoconf examples) |
|
|
| - Brady Alleman/Doug Warner (threading ideas, implementation details) |
|
|
+-------------------------------------------------------------------------+
|
|
| - Cacti - http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "spine.h"
|
|
#include <spawn.h>
|
|
#include <sys/wait.h>
|
|
|
|
extern char **environ;
|
|
|
|
/*! \fn static int php_addclose_unless_std(posix_spawn_file_actions_t *fa, int fd)
|
|
* \brief queue a close for a pipe end unless it is stdin or stdout
|
|
*
|
|
* After the dup2 redirects, descriptors 0 and 1 hold the child's ends. Closing
|
|
* them here would undo the redirect that was just set up.
|
|
*/
|
|
/*! \fn static void php_close_fd(int *fd)
|
|
* \brief close a descriptor once and mark it gone
|
|
*
|
|
* php_init() has one cleanup path for six descriptors, some of which are
|
|
* handed to php_processes[] on the way out. Clearing as it closes is what
|
|
* keeps the shared teardown from closing a descriptor the parent still owns,
|
|
* or one that another thread has since been given.
|
|
*/
|
|
static void php_close_fd(int *fd) {
|
|
if (*fd >= 0) {
|
|
(void) close(*fd);
|
|
*fd = -1;
|
|
}
|
|
}
|
|
|
|
static int php_addclose_unless_std(posix_spawn_file_actions_t *fa, int fd) {
|
|
if (fd == STDIN_FILENO || fd == STDOUT_FILENO) {
|
|
return 0;
|
|
}
|
|
|
|
return posix_spawn_file_actions_addclose(fa, fd);
|
|
}
|
|
|
|
static void php_process_lock(int php_process) {
|
|
thread_mutex_lock(LOCK_PHP_PROC_0 + php_process);
|
|
}
|
|
|
|
static void php_process_unlock(int php_process) {
|
|
thread_mutex_unlock(LOCK_PHP_PROC_0 + php_process);
|
|
}
|
|
|
|
static char *php_undefined_result(void) {
|
|
char *result = strdup("U");
|
|
|
|
if (result == NULL)
|
|
die("ERROR: Fatal malloc error: php.c php_cmd!");
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Script and script server arguments can carry SNMP communities and v3
|
|
* passphrases, so logs name only the first token, the script, never the
|
|
* arguments. */
|
|
void php_command_script(const char *command, char *script, size_t capacity) {
|
|
size_t length = strcspn(command, " \t\r\n");
|
|
|
|
if (length >= capacity) length = capacity - 1;
|
|
memcpy(script, command, length);
|
|
script[length] = '\0';
|
|
}
|
|
|
|
static void php_fail_read(int php_process, int allow_restart) {
|
|
php_processes[php_process].php_state = PHP_BUSY;
|
|
if (allow_restart) {
|
|
php_close(php_process);
|
|
php_init(php_process);
|
|
}
|
|
}
|
|
|
|
void php_processes_initialize(php_t *processes, int count) {
|
|
int i;
|
|
|
|
if (processes == NULL || count <= 0)
|
|
return;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
processes[i].php_state = PHP_BUSY;
|
|
processes[i].php_pid = -1;
|
|
processes[i].php_read_fd = -1;
|
|
processes[i].php_write_fd = -1;
|
|
}
|
|
}
|
|
|
|
static char *php_read_result(int php_process, const char *command, int allow_restart);
|
|
|
|
/* Block SIGPIPE in the calling thread around Spine's two pipe writes. The
|
|
* daemon normally catches SIGPIPE with a no-op handler process-wide, but this local guard
|
|
* makes these writes safe even before signal initialization or in unit tests
|
|
* that temporarily restore SIG_DFL. Only the default disposition needs its
|
|
* generated signal drained; a caught signal is safe when the old mask returns. */
|
|
static ssize_t php_write_no_sigpipe(int fd, const void *buffer, size_t length) {
|
|
sigset_t blocked;
|
|
sigset_t old_mask;
|
|
sigset_t pending;
|
|
struct sigaction sigpipe_action;
|
|
ssize_t result;
|
|
int saved_errno;
|
|
int mask_error;
|
|
int received_signal;
|
|
int should_drain = FALSE;
|
|
int was_pending = FALSE;
|
|
|
|
sigemptyset(&blocked);
|
|
sigaddset(&blocked, SIGPIPE);
|
|
mask_error = pthread_sigmask(SIG_BLOCK, &blocked, &old_mask);
|
|
if (mask_error != 0) {
|
|
errno = mask_error;
|
|
return -1;
|
|
}
|
|
if (sigpending(&pending) == 0) was_pending = sigismember(&pending, SIGPIPE);
|
|
if (sigaction(SIGPIPE, NULL, &sigpipe_action) == 0 &&
|
|
sigpipe_action.sa_handler == SIG_DFL) {
|
|
should_drain = TRUE;
|
|
}
|
|
|
|
result = write(fd, buffer, length);
|
|
saved_errno = errno;
|
|
if (result < 0 && saved_errno == EPIPE && !was_pending && should_drain) {
|
|
do {
|
|
mask_error = sigwait(&blocked, &received_signal);
|
|
} while (mask_error == EINTR);
|
|
}
|
|
pthread_sigmask(SIG_SETMASK, &old_mask, NULL);
|
|
errno = saved_errno;
|
|
return result;
|
|
}
|
|
|
|
/*! \fn char *php_cmd(const char *php_command, int php_process)
|
|
* \brief calls the script server and executes a script command
|
|
* \param php_command the formatted php script server command
|
|
* \param php_process the php script server process to call
|
|
*
|
|
* This function is called directly by the spine poller when a script server
|
|
* request has been initiated for a host. It will place the PHP Script Server
|
|
* command on it's output pipe and then wait the pre-defined timeout period for
|
|
* a response on the PHP Script Servers output pipe.
|
|
*
|
|
* \return pointer to the string results. Must be freed by the parent.
|
|
*
|
|
*/
|
|
char *php_cmd(const char *php_command, int php_process) {
|
|
char *result_string;
|
|
char command[BUFSIZE];
|
|
ssize_t bytes;
|
|
int retries = 0;
|
|
|
|
assert(php_command != 0);
|
|
if (php_processes == NULL || php_process < 0 || php_process >= set.php.php_servers ||
|
|
php_process >= MAX_PHP_SERVERS) {
|
|
SPINE_LOG(("ERROR: SS[%i] PHP Script Server slot is unavailable", php_process));
|
|
return php_undefined_result();
|
|
}
|
|
|
|
/* pad command with CR-LF */
|
|
snprintf(command, BUFSIZE, "%s\r\n", php_command);
|
|
|
|
php_process_lock(php_process);
|
|
|
|
retry:
|
|
/* Validate every attempt under the same per-slot lock that protects
|
|
* close/restart. A
|
|
* check before the lock races a recovery and can use a descriptor after it
|
|
* has been closed and reused by another thread. A failed restart may also
|
|
* leave live descriptors in a BUSY slot, which must not receive a command. */
|
|
if (php_processes[php_process].php_state != PHP_READY ||
|
|
php_processes[php_process].php_pid <= 1 ||
|
|
php_processes[php_process].php_read_fd < 0 ||
|
|
php_processes[php_process].php_write_fd < 0) {
|
|
php_process_unlock(php_process);
|
|
SPINE_LOG(("ERROR: SS[%i] PHP Script Server slot is unavailable", php_process));
|
|
return php_undefined_result();
|
|
}
|
|
|
|
/* send command to the script server */
|
|
bytes = php_write_no_sigpipe(php_processes[php_process].php_write_fd, command, strlen(command));
|
|
|
|
/* if write status is <= 0 then the script server may be hung */
|
|
if (bytes <= 0) {
|
|
char script[SMALL_BUFSIZE];
|
|
|
|
php_command_script(command, script, sizeof(script));
|
|
SPINE_LOG(("ERROR: SS[%i] PHP Script Server communications lost sending a command. Restarting PHP Script Server", php_process));
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] Unsent command for script '%s'", php_process, script));
|
|
|
|
php_close(php_process);
|
|
retries++;
|
|
if (retries < 3 && php_init(php_process) == TRUE &&
|
|
php_processes[php_process].php_state == PHP_READY) {
|
|
goto retry;
|
|
}
|
|
|
|
/* allocated only once the retry budget is spent: a successful retry
|
|
reassigns result_string below and would orphan an earlier copy */
|
|
result_string = php_undefined_result();
|
|
} else {
|
|
/* read the result from the php_command */
|
|
result_string = php_readpipe(php_process, command);
|
|
|
|
/* check for a null */
|
|
if (!strlen(result_string)) {
|
|
SET_UNDEFINED(result_string);
|
|
}
|
|
}
|
|
|
|
php_process_unlock(php_process);
|
|
|
|
return result_string;
|
|
}
|
|
|
|
/*! \fn in php_get_process()
|
|
* \brief returns the next php script server process to utilize
|
|
*
|
|
* This very simple function simply returns the next PHP Script Server
|
|
* process id to poll using a round robin algorithm.
|
|
*
|
|
* \return the next usable script server, or -1 if none is available
|
|
*
|
|
*/
|
|
int php_get_process(void) {
|
|
int candidate;
|
|
int checked;
|
|
int contended_candidate = -1;
|
|
int recovery_candidate = -1;
|
|
int start_candidate;
|
|
int server_count;
|
|
|
|
if (php_processes == NULL || set.php.php_servers <= 0) return -1;
|
|
server_count = set.php.php_servers > MAX_PHP_SERVERS ? MAX_PHP_SERVERS : set.php.php_servers;
|
|
|
|
/* LOCK_PHP protects only the round-robin cursor. A startup handshake can
|
|
* wait for script_timeout, so it must never run under this process-global
|
|
* lock. */
|
|
thread_mutex_lock(LOCK_PHP);
|
|
if (set.php.php_current_server >= server_count) set.php.php_current_server = 0;
|
|
start_candidate = set.php.php_current_server++;
|
|
thread_mutex_unlock(LOCK_PHP);
|
|
|
|
/* Prefer any ready slot. Each snapshot uses the same per-slot mutex as
|
|
* php_cmd() and recovery, so descriptors cannot change under the check. */
|
|
for (checked = 0; checked < server_count; checked++) {
|
|
candidate = (start_candidate + checked) % server_count;
|
|
if (thread_mutex_trylock(LOCK_PHP_PROC_0 + candidate) != 0) {
|
|
if (contended_candidate < 0) contended_candidate = candidate;
|
|
continue;
|
|
}
|
|
if (php_processes[candidate].php_state == PHP_READY &&
|
|
php_processes[candidate].php_pid > 1 &&
|
|
php_processes[candidate].php_read_fd >= 0 &&
|
|
php_processes[candidate].php_write_fd >= 0) {
|
|
php_process_unlock(candidate);
|
|
return candidate;
|
|
}
|
|
if (recovery_candidate < 0) recovery_candidate = candidate;
|
|
php_process_unlock(candidate);
|
|
}
|
|
|
|
/* A locked slot is normally a healthy server executing another command.
|
|
* Return it so php_cmd() queues on the slot mutex instead of turning routine
|
|
* contention into an undefined data point, but first repair any failed slot
|
|
* observed by this scan so steady contention cannot starve pool recovery.
|
|
* Validation still happens after the contended lock is acquired. */
|
|
if (contended_candidate >= 0 && recovery_candidate < 0)
|
|
return contended_candidate;
|
|
|
|
if (recovery_candidate < 0 ||
|
|
thread_mutex_trylock(LOCK_PHP_PROC_0 + recovery_candidate) != 0) {
|
|
return contended_candidate;
|
|
}
|
|
|
|
/* Recover at most one slot per request. The per-slot lock prevents a
|
|
* simultaneous php_cmd() or another recovery from closing the same fd or
|
|
* signalling a recycled pid. */
|
|
if (php_processes[recovery_candidate].php_state == PHP_READY &&
|
|
php_processes[recovery_candidate].php_pid > 1 &&
|
|
php_processes[recovery_candidate].php_read_fd >= 0 &&
|
|
php_processes[recovery_candidate].php_write_fd >= 0) {
|
|
php_process_unlock(recovery_candidate);
|
|
return recovery_candidate;
|
|
}
|
|
if (php_processes[recovery_candidate].php_pid > 1 ||
|
|
php_processes[recovery_candidate].php_read_fd >= 0 ||
|
|
php_processes[recovery_candidate].php_write_fd >= 0) {
|
|
php_close(recovery_candidate);
|
|
}
|
|
if (php_init(recovery_candidate) == TRUE &&
|
|
php_processes[recovery_candidate].php_state == PHP_READY) {
|
|
php_process_unlock(recovery_candidate);
|
|
return recovery_candidate;
|
|
}
|
|
php_process_unlock(recovery_candidate);
|
|
|
|
return contended_candidate;
|
|
}
|
|
|
|
#ifdef SPINE_PHP_RUNTIME_TESTING
|
|
ssize_t php_write_no_sigpipe_for_test(int fd, const void *buffer, size_t length) {
|
|
return php_write_no_sigpipe(fd, buffer, length);
|
|
}
|
|
|
|
char *php_read_result_for_test(int php_process, char *command, int allow_restart) {
|
|
return php_read_result(php_process, command, allow_restart);
|
|
}
|
|
#endif
|
|
|
|
/*! \fn char *php_readpipe(int php_process, const char *command)
|
|
* \brief read a line from a PHP Script Server process
|
|
* \param php_process the PHP Script Server process to obtain output from
|
|
*
|
|
* This function will read the output pipe from the PHP Script Server process
|
|
* and return that string to the Spine thread requesting the output. If for
|
|
* some reason the PHP Script Server process does not respond in time, it will
|
|
* be closed using the php_close function, then restarted.
|
|
*
|
|
* \return a string pointer to the PHP Script Server response
|
|
*/
|
|
/*! \fn static char *php_read_result(int php_process, const char *command, int allow_restart)
|
|
* \brief reads one script server response.
|
|
*
|
|
* allow_restart is FALSE for the startup handshake. php_init() calls this to
|
|
* confirm the server it just spawned is answering, and a restart from inside
|
|
* that read would call php_init() again, which reads again: a server that
|
|
* starts but never answers put a poller thread into unbounded mutual
|
|
* recursion, spawning a fresh server at every level. Refusing the restart on
|
|
* the handshake bounds the depth at one by construction.
|
|
*/
|
|
static char *php_read_result(int php_process, const char *command, int allow_restart) {
|
|
fd_set fds;
|
|
struct timeval timeout;
|
|
double begin_time = 0;
|
|
double end_time = 0;
|
|
double remaining_usec = 0;
|
|
char *result_string;
|
|
int response_timeout;
|
|
double read_deadline;
|
|
char script[SMALL_BUFSIZE];
|
|
|
|
ssize_t i;
|
|
char *cp;
|
|
char *bptr;
|
|
|
|
if (!(result_string = (char *)malloc(RESULTS_BUFFER))) {
|
|
die("ERROR: Fatal malloc error: php.c php_readpipe!");
|
|
}
|
|
result_string[0] = '\0';
|
|
|
|
/* Public readers may bypass php_cmd()'s process-slot validation. Keep
|
|
* both command and startup readers inside the allocated process table. */
|
|
if (php_processes == NULL || php_process < 0 ||
|
|
php_process >= set.php.php_servers || php_process >= MAX_PHP_SERVERS) {
|
|
SET_UNDEFINED(result_string);
|
|
return result_string;
|
|
}
|
|
|
|
/* record start time */
|
|
begin_time = get_time_as_double();
|
|
|
|
/* establish timeout value for the PHP script server to respond */
|
|
/* Selection-path recovery runs this handshake while holding the slot lock.
|
|
* Bound startup independently so a bad PHP configuration cannot pin every
|
|
* selector for the full per-command timeout. */
|
|
response_timeout = allow_restart || set.php.script_timeout < 2 ? set.php.script_timeout : 2;
|
|
timeout.tv_sec = response_timeout;
|
|
timeout.tv_usec = 0;
|
|
|
|
/* select() only says the first bytes arrived. A server that writes part of
|
|
* a line and stalls must not hold this thread past the same deadline. */
|
|
read_deadline = spine_monotonic_time() + response_timeout;
|
|
|
|
/* check to see which pipe talked and take action
|
|
* should only be the READ pipe */
|
|
retry:
|
|
|
|
/* FD_SET on a descriptor at or past FD_SETSIZE writes outside fds, which is
|
|
a stack object here. ping_icmp() guards its socket the same way. */
|
|
if (php_processes[php_process].php_read_fd < 0 || php_processes[php_process].php_read_fd >= FD_SETSIZE) {
|
|
SPINE_LOG(("ERROR: SS[%i] Script server descriptor %d exceeds FD_SETSIZE %d", php_process, php_processes[php_process].php_read_fd, FD_SETSIZE));
|
|
|
|
SET_UNDEFINED(result_string);
|
|
/* A descriptor that select() cannot represent makes this slot unusable.
|
|
* Mark it unhealthy even during the startup handshake, where restarting
|
|
* recursively is deliberately disabled, so the scheduler cannot keep
|
|
* handing out a permanently poisoned READY slot. */
|
|
php_fail_read(php_process, allow_restart);
|
|
|
|
return result_string;
|
|
}
|
|
|
|
/* initialize file descriptors to review for input/output */
|
|
FD_ZERO(&fds);
|
|
FD_SET(php_processes[php_process].php_read_fd,&fds);
|
|
|
|
switch (select(php_processes[php_process].php_read_fd+1, &fds, NULL, NULL, &timeout)) {
|
|
case -1:
|
|
switch (errno) {
|
|
case EBADF:
|
|
SPINE_LOG(("ERROR: SS[%i] An invalid file descriptor was given in one of the sets.", php_process));
|
|
break;
|
|
case EINTR:
|
|
#ifndef SOLAR_THREAD
|
|
/* take a moment */
|
|
usleep(2000);
|
|
#endif
|
|
|
|
/* record end time */
|
|
end_time = get_time_as_double();
|
|
|
|
/* re-establish new timeout value */
|
|
timeout.tv_sec = rint(floor(response_timeout - (end_time - begin_time)));
|
|
remaining_usec = response_timeout - timeout.tv_sec - (end_time - begin_time);
|
|
|
|
if (remaining_usec > 0) {
|
|
timeout.tv_usec = rint(remaining_usec * 1000000);
|
|
} else {
|
|
timeout.tv_usec = 0;
|
|
}
|
|
|
|
if (timeout.tv_sec + timeout.tv_usec > 0) {
|
|
goto retry;
|
|
} else {
|
|
SPINE_LOG(("WARNING: SS[%i] The Script Server script timed out while processing EINTR's.", php_process));
|
|
}
|
|
|
|
break;
|
|
case EINVAL:
|
|
SPINE_LOG(("ERROR: SS[%i] N is negative or the value contained within timeout is invalid.", php_process));
|
|
break;
|
|
case ENOMEM:
|
|
SPINE_LOG(("ERROR: SS[%i] Select was unable to allocate memory for internal tables.", php_process));
|
|
break;
|
|
default:
|
|
SPINE_LOG(("ERROR: SS[%i] Unknown fatal select() error", php_process));
|
|
break;
|
|
}
|
|
|
|
SET_UNDEFINED(result_string);
|
|
php_fail_read(php_process, allow_restart);
|
|
break;
|
|
case 0:
|
|
/* record end time */
|
|
end_time = get_time_as_double();
|
|
SPINE_LOG(("WARNING: SS[%i] The PHP Script Server did not respond in time for Timeout[%0.2f] and will therefore be restarted", php_process, end_time - begin_time));
|
|
php_command_script(command, script, sizeof(script));
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] Unanswered command for script '%s'", php_process, script));
|
|
SET_UNDEFINED(result_string);
|
|
php_fail_read(php_process, allow_restart);
|
|
break;
|
|
default:
|
|
{
|
|
int read_ok = TRUE;
|
|
|
|
if (FD_ISSET(php_processes[php_process].php_read_fd, &fds)) {
|
|
bptr = result_string;
|
|
|
|
while (1) {
|
|
/* reserve one byte for the trailing '\0' written below */
|
|
size_t used = (size_t)(bptr - result_string);
|
|
|
|
if (used >= RESULTS_BUFFER - 1) {
|
|
SPINE_LOG(("ERROR: SS[%i] The Script Server result was longer than the acceptable range", php_process));
|
|
SET_UNDEFINED(result_string);
|
|
read_ok = FALSE;
|
|
break;
|
|
}
|
|
|
|
size_t space = (size_t)RESULTS_BUFFER - 1 - used;
|
|
|
|
if (used > 0) {
|
|
int ready = spine_wait_readable(php_processes[php_process].php_read_fd, read_deadline);
|
|
|
|
if (ready <= 0) {
|
|
SPINE_LOG(("WARNING: SS[%i] The PHP Script Server sent a partial response and %s", php_process, ready == 0 ? "timed out" : "failed"));
|
|
php_command_script(command, script, sizeof(script));
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] Unanswered command for script '%s'", php_process, script));
|
|
SET_UNDEFINED(result_string);
|
|
read_ok = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
i = read(php_processes[php_process].php_read_fd, bptr, space);
|
|
|
|
if (i < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
|
|
if (i <= 0) {
|
|
SET_UNDEFINED(result_string);
|
|
read_ok = FALSE;
|
|
break;
|
|
}
|
|
|
|
bptr += i;
|
|
*bptr = '\0'; /* make what we've got into a string */
|
|
|
|
if ((cp = strstr(result_string,"\n")) != 0) {
|
|
break;
|
|
}
|
|
|
|
if (bptr >= result_string + RESULTS_BUFFER - 1) {
|
|
SPINE_LOG(("ERROR: SS[%i] The Script Server result was longer than the acceptable range", php_process));
|
|
SET_UNDEFINED(result_string);
|
|
read_ok = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
SPINE_LOG(("ERROR: SS[%i] The FD was not set as expected", php_process));
|
|
SET_UNDEFINED(result_string);
|
|
read_ok = FALSE;
|
|
}
|
|
|
|
if (read_ok) {
|
|
php_processes[php_process].php_state = PHP_READY;
|
|
} else {
|
|
php_fail_read(php_process, allow_restart);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return result_string;
|
|
}
|
|
|
|
/*! \fn char *php_readpipe(int php_process, const char *command)
|
|
* \brief reads a script server response, restarting a server that stops
|
|
* answering.
|
|
*
|
|
* \return a string pointer to the PHP Script Server response
|
|
*/
|
|
char *php_readpipe(int php_process, const char *command) {
|
|
return php_read_result(php_process, command, TRUE);
|
|
}
|
|
|
|
/*! \fn int php_init(int php_process)
|
|
* \brief initialize either a specific PHP Script Server or all of them.
|
|
* \param php_process the process number to start or PHP_INIT
|
|
*
|
|
* This function will either start an individual PHP Script Server process
|
|
* or all of them if the input parameter is the PHP_INIT constant. The function
|
|
* will check the status of the process to verify that it is ready to process
|
|
* scripts as well.
|
|
*
|
|
* \return TRUE if the PHP Script Server is know running or FALSE otherwise
|
|
*/
|
|
int php_init(int php_process) {
|
|
int cacti2php_pdes[2] = { -1, -1 };
|
|
int php2cacti_pdes[2] = { -1, -1 };
|
|
pid_t pid;
|
|
char poller_id[SMALL_BUFSIZE];
|
|
char *argv[7];
|
|
char arg_q[] = "-q";
|
|
char arg_spine[] = "spine";
|
|
char arg_environ_spine[] = "--environ=spine";
|
|
char arg_mode_online[] = "--mode=online";
|
|
char arg_mode_offline[] = "--mode=offline";
|
|
posix_spawn_file_actions_t fa;
|
|
posix_spawnattr_t attr;
|
|
int fa_valid = FALSE;
|
|
int attr_valid = FALSE;
|
|
int cancel_state = 0;
|
|
int cancel_held = FALSE;
|
|
int child_stdin;
|
|
int child_stdout;
|
|
int dup_stdin = -1;
|
|
int dup_stdout = -1;
|
|
char *result_string = NULL;
|
|
int num_processes;
|
|
int slot = -1;
|
|
int i;
|
|
int rc = FALSE;
|
|
char *command = strdup("INIT");
|
|
|
|
if (command == NULL) {
|
|
SPINE_LOG(("ERROR: Fatal malloc error: php.c php_init!"));
|
|
return FALSE;
|
|
}
|
|
|
|
/* An out-of-range slot would index past php_processes. */
|
|
if (php_processes == NULL || set.php.php_servers < 0 || set.php.php_servers > MAX_PHP_SERVERS ||
|
|
(php_process != PHP_INIT && (php_process < 0 || php_process >= set.php.php_servers))) {
|
|
SPINE_LOG(("ERROR: SS[%i] PHP Script Server slot is unavailable", php_process));
|
|
goto cleanup;
|
|
}
|
|
|
|
/* special code to start all PHP Servers */
|
|
if (php_process == PHP_INIT) {
|
|
num_processes = set.php.php_servers;
|
|
} else {
|
|
num_processes = 1;
|
|
}
|
|
|
|
for (i=0; i < num_processes; i++) {
|
|
/* the spawn retry budget is per server. Sharing one counter across the
|
|
loop meant that once the first server spent it on EAGAIN, every
|
|
server after it got none, under exactly the resource pressure the
|
|
retry exists to ride out. */
|
|
int retry_count = 0;
|
|
|
|
slot = (php_process == PHP_INIT) ? i : php_process;
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] PHP Script Server Routine Starting", i));
|
|
|
|
/* create the output pipes from Spine to php*/
|
|
if (!spine_open_pipe_cloexec(cacti2php_pdes)) {
|
|
SPINE_LOG(("ERROR: SS[%i] Could not allocate php server pipes", i));
|
|
goto cleanup;
|
|
}
|
|
|
|
/* create the input pipes from php to Spine */
|
|
if (!spine_open_pipe_cloexec(php2cacti_pdes)) {
|
|
SPINE_LOG(("ERROR: SS[%i] Could not allocate php server pipes", i));
|
|
goto cleanup;
|
|
}
|
|
|
|
/* disable thread cancellation from this point forward. */
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancel_state);
|
|
cancel_held = TRUE;
|
|
|
|
/* establish arguments for script server execution */
|
|
if (set.cacti_version <= 1222) {
|
|
argv[0] = set.php.path_php;
|
|
argv[1] = arg_q;
|
|
argv[2] = set.php.path_php_server;
|
|
argv[3] = arg_spine;
|
|
snprintf(poller_id, SMALL_BUFSIZE, "%d", set.poller.poller_id);
|
|
argv[4] = poller_id;
|
|
argv[5] = NULL;
|
|
} else if (set.poller.poller_id > 1) {
|
|
argv[0] = set.php.path_php;
|
|
argv[1] = arg_q;
|
|
argv[2] = set.php.path_php_server;
|
|
argv[3] = arg_environ_spine;
|
|
|
|
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller.poller_id);
|
|
argv[4] = poller_id;
|
|
|
|
if (set.poller.mode == REMOTE_ONLINE) {
|
|
argv[5] = arg_mode_online;
|
|
} else {
|
|
argv[5] = arg_mode_offline;
|
|
}
|
|
|
|
argv[6] = NULL;
|
|
} else {
|
|
argv[0] = set.php.path_php;
|
|
argv[1] = arg_q;
|
|
argv[2] = set.php.path_php_server;
|
|
argv[3] = arg_environ_spine;
|
|
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller.poller_id);
|
|
argv[4] = poller_id;
|
|
|
|
argv[5] = NULL;
|
|
}
|
|
|
|
/* spawn a child process */
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] PHP Script Server About to spawn Child Process", i));
|
|
|
|
{
|
|
int spawn_err;
|
|
|
|
if (posix_spawn_file_actions_init(&fa) != 0) {
|
|
SPINE_LOG(("ERROR: SS[%i] posix_spawn_file_actions_init failed", i));
|
|
goto cleanup;
|
|
}
|
|
|
|
fa_valid = TRUE;
|
|
if (spine_spawnattr_sigpipe_default(&attr) != 0) {
|
|
SPINE_LOG(("ERROR: SS[%i] posix_spawnattr setup failed: %s", i, strerror(errno)));
|
|
goto cleanup;
|
|
}
|
|
attr_valid = TRUE;
|
|
|
|
/* wire cacti->php read end to child stdin, php->cacti write end to child stdout */
|
|
/* The pipe ends are close-on-exec, and dup2 clears that on its target, so
|
|
* the usual case is fine. When an end already sits on the descriptor it is
|
|
* destined for, dup2(fd, fd) is a no-op that clears nothing and the
|
|
* unconditional close below would then shut the child's stdin or stdout.
|
|
* The script server would exec with it closed, never answer, and every
|
|
* script-server data source would record U with no diagnostic. Reaching it
|
|
* needs spine to start with fd 0 or 1 closed, which a daemon can do.
|
|
* Same treatment as nft_popen(): dup() to a fresh descriptor, mark the
|
|
* temporary copy close-on-exec, then use a child file action to dup2 it. */
|
|
child_stdin = cacti2php_pdes[0];
|
|
child_stdout = php2cacti_pdes[1];
|
|
|
|
if (child_stdin == STDIN_FILENO) {
|
|
dup_stdin = dup(child_stdin);
|
|
if (dup_stdin >= 0 && spine_set_cloexec(dup_stdin) != 0) {
|
|
php_close_fd(&dup_stdin);
|
|
}
|
|
child_stdin = dup_stdin;
|
|
}
|
|
|
|
if (child_stdout == STDOUT_FILENO) {
|
|
dup_stdout = dup(child_stdout);
|
|
if (dup_stdout >= 0 && spine_set_cloexec(dup_stdout) != 0) {
|
|
php_close_fd(&dup_stdout);
|
|
}
|
|
child_stdout = dup_stdout;
|
|
}
|
|
|
|
if (child_stdin < 0 || child_stdout < 0 ||
|
|
posix_spawn_file_actions_adddup2(&fa, child_stdin, STDIN_FILENO) != 0 ||
|
|
posix_spawn_file_actions_adddup2(&fa, child_stdout, STDOUT_FILENO) != 0 ||
|
|
/* close the pipe ends the child does not need. Skip fd 0 and 1: after
|
|
the redirects above they hold the copies the child polls on. */
|
|
php_addclose_unless_std(&fa, cacti2php_pdes[0]) != 0 ||
|
|
php_addclose_unless_std(&fa, cacti2php_pdes[1]) != 0 ||
|
|
php_addclose_unless_std(&fa, php2cacti_pdes[0]) != 0 ||
|
|
php_addclose_unless_std(&fa, php2cacti_pdes[1]) != 0 ||
|
|
(dup_stdin != -1 && posix_spawn_file_actions_addclose(&fa, dup_stdin) != 0) ||
|
|
(dup_stdout != -1 && posix_spawn_file_actions_addclose(&fa, dup_stdout) != 0)) {
|
|
SPINE_LOG(("ERROR: SS[%i] posix_spawn_file_actions setup failed", i));
|
|
goto cleanup;
|
|
}
|
|
|
|
do {
|
|
spawn_err = posix_spawn(&pid, argv[0], &fa, &attr, argv, environ);
|
|
if ((spawn_err == EAGAIN || spawn_err == ENOMEM) && retry_count < 3) {
|
|
retry_count++;
|
|
#ifndef SOLAR_THREAD
|
|
usleep(50000);
|
|
#endif
|
|
continue;
|
|
}
|
|
break;
|
|
} while (1);
|
|
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
fa_valid = FALSE;
|
|
posix_spawnattr_destroy(&attr);
|
|
attr_valid = FALSE;
|
|
|
|
/* the child holds its own copies now */
|
|
php_close_fd(&dup_stdin);
|
|
php_close_fd(&dup_stdout);
|
|
|
|
if (spawn_err != 0) {
|
|
if (spawn_err == EAGAIN) {
|
|
SPINE_LOG(("ERROR: SS[%i] Could not spawn PHP Script Server Out of Resources", i));
|
|
} else if (spawn_err == ENOMEM) {
|
|
SPINE_LOG(("ERROR: SS[%i] Could not spawn PHP Script Server Out of Memory", i));
|
|
} else {
|
|
SPINE_LOG(("ERROR: SS[%i] Could not spawn PHP Script Server Unknown Reason", i));
|
|
}
|
|
|
|
SPINE_LOG(("ERROR: SS[%i] Could not spawn PHP Script Server", i));
|
|
goto cleanup;
|
|
}
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] PHP Script Server Child spawn Success", i));
|
|
}
|
|
|
|
/* Parent */
|
|
/* close unneeded pipes */
|
|
php_close_fd(&cacti2php_pdes[0]);
|
|
php_close_fd(&php2cacti_pdes[1]);
|
|
|
|
php_processes[slot].php_pid = pid;
|
|
php_processes[slot].php_write_fd = cacti2php_pdes[1];
|
|
php_processes[slot].php_read_fd = php2cacti_pdes[0];
|
|
|
|
/* php_processes[] owns these now; the cleanup below must not close them */
|
|
cacti2php_pdes[1] = -1;
|
|
php2cacti_pdes[0] = -1;
|
|
|
|
/* restore caller's cancellation state. */
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
cancel_held = FALSE;
|
|
|
|
/* check pipe to insure startup took place */
|
|
result_string = php_read_result(slot, command, FALSE);
|
|
|
|
if (strstr(result_string, "Started")) {
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] Confirmed PHP Script Server running using readfd[%i], writefd[%i]", slot, php_processes[slot].php_read_fd, php_processes[slot].php_write_fd));
|
|
|
|
php_processes[slot].php_state = PHP_READY;
|
|
} else {
|
|
SPINE_LOG(("ERROR: SS[%i] Script Server did not start properly return message was: '%s'", slot, result_string));
|
|
|
|
php_processes[slot].php_state = PHP_BUSY;
|
|
}
|
|
|
|
SPINE_FREE(result_string);
|
|
}
|
|
|
|
rc = TRUE;
|
|
|
|
cleanup:
|
|
/* One owner for everything this function allocates. The five exits used to
|
|
* spell their own teardown out and they had already drifted: every one of
|
|
* them leaked `command`, and each carried a slightly different subset of
|
|
* the closes. See ping_icmp() and #593 for the same shape. */
|
|
if (fa_valid) {
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
}
|
|
if (attr_valid) {
|
|
posix_spawnattr_destroy(&attr);
|
|
}
|
|
|
|
php_close_fd(&dup_stdin);
|
|
php_close_fd(&dup_stdout);
|
|
php_close_fd(&cacti2php_pdes[0]);
|
|
php_close_fd(&cacti2php_pdes[1]);
|
|
php_close_fd(&php2cacti_pdes[0]);
|
|
php_close_fd(&php2cacti_pdes[1]);
|
|
|
|
if (cancel_held) {
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
}
|
|
if (!rc && php_processes != NULL && slot >= 0 && slot < MAX_PHP_SERVERS) {
|
|
php_processes[slot].php_pid = -1;
|
|
php_processes[slot].php_read_fd = -1;
|
|
php_processes[slot].php_write_fd = -1;
|
|
php_processes[slot].php_state = PHP_BUSY;
|
|
}
|
|
|
|
SPINE_FREE(result_string);
|
|
free(command);
|
|
|
|
return rc;
|
|
}
|
|
static int php_terminate_and_reap(pid_t pid) {
|
|
int status;
|
|
pid_t waited;
|
|
|
|
do {
|
|
waited = waitpid(pid, &status, WNOHANG);
|
|
} while (waited < 0 && errno == EINTR);
|
|
|
|
if (waited == pid || (waited < 0 && errno == ECHILD)) {
|
|
return TRUE;
|
|
}
|
|
|
|
if (waited < 0) {
|
|
SPINE_LOG(("WARNING: Unable to reap PHP Script Server PID[%ld]: %s", (long)pid, strerror(errno)));
|
|
return FALSE;
|
|
}
|
|
|
|
/* Still running after its grace period: kill outright rather than
|
|
* waiting to see whether a gentler signal works. */
|
|
if (kill(pid, SIGKILL) < 0 && errno != ESRCH) {
|
|
SPINE_LOG(("WARNING: Unable to signal PHP Script Server PID[%ld]: %s", (long)pid, strerror(errno)));
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/*! \fn void php_close(int php_process)
|
|
* \brief close the php script server process
|
|
* \param php_process the process to close or PHP_INIT
|
|
*
|
|
* This function will take an input parameter of either a specially coded
|
|
* PHP_INIT parameter or an integer stating the process number. With that
|
|
* information is will close and/or terminate the child PHP Script Server
|
|
* process and then return to the calling function.
|
|
*
|
|
* Shutdown gives the server one grace period (the "quit" write plus a
|
|
* fixed delay below) to exit on its own, then checks once with a
|
|
* non-blocking waitpid(). A child still running at that point is killed
|
|
* outright and handed to the abandoned-pid sweep instead of waiting for
|
|
* it here.
|
|
*/
|
|
void php_close(int php_process) {
|
|
int i;
|
|
int num_processes;
|
|
int len;
|
|
|
|
/* An out-of-range slot would index past php_processes. */
|
|
if (php_processes == NULL || set.php.php_servers < 0 || set.php.php_servers > MAX_PHP_SERVERS ||
|
|
(php_process != PHP_INIT && (php_process < 0 || php_process >= set.php.php_servers))) {
|
|
return;
|
|
}
|
|
|
|
if (php_process == PHP_INIT) {
|
|
num_processes = set.php.php_servers;
|
|
} else {
|
|
num_processes = 1;
|
|
}
|
|
|
|
for(i = 0; i < num_processes; i++) {
|
|
php_t *phpp;
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] Script Server Shutdown Started", i));
|
|
|
|
/* tell the script server to close */
|
|
if (php_process == PHP_INIT) {
|
|
phpp = &php_processes[i];
|
|
} else {
|
|
phpp = &php_processes[php_process];
|
|
}
|
|
|
|
/* If we still have a valid write pipe, tell PHP to close down
|
|
* by sending a "quit" message, then closing the input channel
|
|
* so it gets an EOF.
|
|
*
|
|
* Then we wait a moment before actually killing it to allow for
|
|
* a clean shutdown.
|
|
*/
|
|
if (phpp->php_write_fd >= 0) {
|
|
static const char quit[] = "quit\r\n";
|
|
/* A hung server can leave the request pipe full; the quit message
|
|
* is best effort and must not block shutdown. */
|
|
int flags = fcntl(phpp->php_write_fd, F_GETFL);
|
|
|
|
if (flags >= 0) {
|
|
(void)fcntl(phpp->php_write_fd, F_SETFL, flags | O_NONBLOCK);
|
|
}
|
|
|
|
len = php_write_no_sigpipe(phpp->php_write_fd, quit, strlen(quit));
|
|
|
|
if (len < 0) {
|
|
SPINE_LOG_DEBUG(("DEBUG: SS[%i] Script Server quit write failed, closing anyway", i));
|
|
}
|
|
|
|
/* Close regardless of the write result. A dead child makes the
|
|
* write fail with EPIPE, and skipping the close leaked one
|
|
* descriptor per script server restart. */
|
|
close(phpp->php_write_fd);
|
|
phpp->php_write_fd = -1;
|
|
|
|
/* wait before killing php */
|
|
usleep(50000); /* 50 msec */
|
|
}
|
|
|
|
/* only try to kill the process if the PID looks valid.
|
|
* Trying to kill a negative number is bad news (it's
|
|
* a process group leader), and PID 1 is "init".
|
|
*/
|
|
if (phpp->php_pid > 1) {
|
|
if (!php_terminate_and_reap(phpp->php_pid)) {
|
|
nft_abandon_child(phpp->php_pid, "PHP child survived shutdown budget");
|
|
}
|
|
|
|
phpp->php_pid = -1;
|
|
}
|
|
|
|
/* close file descriptors */
|
|
close(phpp->php_read_fd);
|
|
phpp->php_read_fd = -1;
|
|
}
|
|
}
|