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>
1155 lines
37 KiB
C
1155 lines
37 KiB
C
/* Production-linked PHP Script Server runtime tests.
|
|
*
|
|
* These use a real child, real close-on-exec pipes and the shipped php.c.
|
|
* They deliberately avoid a Cacti/PHP installation while covering the hot
|
|
* init -> command -> response -> close lifecycle and deterministic failures.
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <setjmp.h>
|
|
#include <cmocka.h>
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <spawn.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/resource.h>
|
|
#include <unistd.h>
|
|
#include <poll.h>
|
|
|
|
#include "common.h"
|
|
#include "spine.h"
|
|
#include "php.h"
|
|
|
|
static pthread_mutex_t spawn_barrier_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_cond_t spawn_barrier_cond = PTHREAD_COND_INITIALIZER;
|
|
static int block_shell_spawn;
|
|
static int block_php_spawn;
|
|
static int shell_spawn_reached;
|
|
static int release_shell_spawn;
|
|
static int fail_next_dup;
|
|
static int fail_duplicated_cloexec;
|
|
static int fail_next_cloexec;
|
|
static __thread int duplicated_fd = -1;
|
|
static int fail_next_setsigdefault;
|
|
static int track_spawnattr_destroy;
|
|
static int spawnattr_destroyed;
|
|
static int php_spawn_calls;
|
|
static int fail_php_spawn_call;
|
|
static int fail_next_sigmask;
|
|
static int track_write;
|
|
static int write_calls;
|
|
static struct rlimit saved_nofile;
|
|
static int restore_nofile;
|
|
|
|
static int duplicate_at_fdsetsize(int fd) {
|
|
struct rlimit limit;
|
|
struct rlimit raised;
|
|
int duplicate;
|
|
|
|
if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur <= FD_SETSIZE &&
|
|
limit.rlim_max > FD_SETSIZE) {
|
|
raised = limit;
|
|
raised.rlim_cur = FD_SETSIZE + 1;
|
|
if (raised.rlim_cur > raised.rlim_max)
|
|
raised.rlim_cur = raised.rlim_max;
|
|
if (setrlimit(RLIMIT_NOFILE, &raised) == 0) {
|
|
saved_nofile = limit;
|
|
restore_nofile = TRUE;
|
|
}
|
|
}
|
|
|
|
duplicate = fcntl(fd, F_DUPFD, FD_SETSIZE);
|
|
if (duplicate < FD_SETSIZE)
|
|
skip();
|
|
|
|
return duplicate;
|
|
}
|
|
|
|
int __real_pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
|
|
int __wrap_pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset) {
|
|
if (fail_next_sigmask && how == SIG_BLOCK) {
|
|
fail_next_sigmask = FALSE;
|
|
return EAGAIN;
|
|
}
|
|
return __real_pthread_sigmask(how, set, oldset);
|
|
}
|
|
|
|
ssize_t __real_write(int fd, const void *buffer, size_t length);
|
|
ssize_t __wrap_write(int fd, const void *buffer, size_t length) {
|
|
if (track_write) write_calls++;
|
|
return __real_write(fd, buffer, length);
|
|
}
|
|
|
|
int __real_dup(int fd);
|
|
int __wrap_dup(int fd) {
|
|
int result;
|
|
if (fail_next_dup) {
|
|
fail_next_dup = FALSE;
|
|
errno = EMFILE;
|
|
return -1;
|
|
}
|
|
result = __real_dup(fd);
|
|
duplicated_fd = result;
|
|
return result;
|
|
}
|
|
|
|
int __real_fcntl(int fd, int command, ...);
|
|
int __wrap_fcntl(int fd, int command, ...) {
|
|
va_list args;
|
|
int argument;
|
|
|
|
if ((fail_next_cloexec || (fail_duplicated_cloexec && fd == duplicated_fd)) &&
|
|
command == F_SETFD) {
|
|
fail_next_cloexec = FALSE;
|
|
fail_duplicated_cloexec = FALSE;
|
|
errno = EIO;
|
|
return -1;
|
|
}
|
|
if (command == F_GETFD) return __real_fcntl(fd, command);
|
|
va_start(args, command);
|
|
argument = va_arg(args, int);
|
|
va_end(args);
|
|
return __real_fcntl(fd, command, argument);
|
|
}
|
|
|
|
int __real_posix_spawn(pid_t *pid, const char *path,
|
|
const posix_spawn_file_actions_t *actions,
|
|
const posix_spawnattr_t *attributes,
|
|
char *const argv[], char *const envp[]);
|
|
|
|
int __wrap_posix_spawn(pid_t *pid, const char *path,
|
|
const posix_spawn_file_actions_t *actions,
|
|
const posix_spawnattr_t *attributes,
|
|
char *const argv[], char *const envp[]) {
|
|
if (strcmp(path, PHP_TEST_SERVER_PATH) == 0) {
|
|
php_spawn_calls++;
|
|
if (php_spawn_calls == fail_php_spawn_call) return EIO;
|
|
}
|
|
if ((block_shell_spawn && strcmp(path, "/bin/sh") == 0) ||
|
|
(block_php_spawn && strcmp(path, PHP_TEST_SERVER_PATH) == 0)) {
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
shell_spawn_reached = TRUE;
|
|
pthread_cond_broadcast(&spawn_barrier_cond);
|
|
while (!release_shell_spawn) {
|
|
pthread_cond_wait(&spawn_barrier_cond, &spawn_barrier_mutex);
|
|
}
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
}
|
|
|
|
return __real_posix_spawn(pid, path, actions, attributes, argv, envp);
|
|
}
|
|
|
|
int __real_posix_spawnattr_setsigdefault(posix_spawnattr_t *attr, const sigset_t *defaults);
|
|
int __wrap_posix_spawnattr_setsigdefault(posix_spawnattr_t *attr, const sigset_t *defaults) {
|
|
if (fail_next_setsigdefault) {
|
|
fail_next_setsigdefault = FALSE;
|
|
return EIO;
|
|
}
|
|
return __real_posix_spawnattr_setsigdefault(attr, defaults);
|
|
}
|
|
|
|
int __real_posix_spawnattr_destroy(posix_spawnattr_t *attr);
|
|
int __wrap_posix_spawnattr_destroy(posix_spawnattr_t *attr) {
|
|
if (track_spawnattr_destroy) spawnattr_destroyed = TRUE;
|
|
return __real_posix_spawnattr_destroy(attr);
|
|
}
|
|
|
|
static int php_setup(void **state) {
|
|
(void) state;
|
|
memset(&set, 0, sizeof(set));
|
|
init_mutexes();
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
php_processes = calloc(MAX_PHP_SERVERS, sizeof(*php_processes));
|
|
assert_non_null(php_processes);
|
|
php_processes_initialize(php_processes, MAX_PHP_SERVERS);
|
|
|
|
set.php.php_servers = 2;
|
|
set.php.script_timeout = 1;
|
|
set.cacti_version = 1300;
|
|
set.poller.poller_id = 1;
|
|
set.logging.log_destination = LOGDEST_STDOUT;
|
|
block_shell_spawn = FALSE;
|
|
block_php_spawn = FALSE;
|
|
shell_spawn_reached = FALSE;
|
|
release_shell_spawn = FALSE;
|
|
fail_next_dup = FALSE;
|
|
fail_duplicated_cloexec = FALSE;
|
|
fail_next_cloexec = FALSE;
|
|
duplicated_fd = -1;
|
|
fail_next_setsigdefault = FALSE;
|
|
track_spawnattr_destroy = FALSE;
|
|
spawnattr_destroyed = FALSE;
|
|
php_spawn_calls = 0;
|
|
fail_php_spawn_call = 0;
|
|
fail_next_sigmask = FALSE;
|
|
track_write = FALSE;
|
|
write_calls = 0;
|
|
restore_nofile = FALSE;
|
|
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", PHP_TEST_SERVER_PATH);
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "normal");
|
|
return 0;
|
|
}
|
|
|
|
static int php_teardown(void **state) {
|
|
int i;
|
|
|
|
(void) state;
|
|
if (php_processes != NULL) {
|
|
for (i = 0; i < MAX_PHP_SERVERS; i++) {
|
|
if (php_processes[i].php_pid > 1) {
|
|
php_close(i);
|
|
} else {
|
|
if (php_processes[i].php_read_fd > STDERR_FILENO)
|
|
close(php_processes[i].php_read_fd);
|
|
if (php_processes[i].php_write_fd > STDERR_FILENO &&
|
|
php_processes[i].php_write_fd != php_processes[i].php_read_fd)
|
|
close(php_processes[i].php_write_fd);
|
|
}
|
|
}
|
|
free(php_processes);
|
|
php_processes = NULL;
|
|
}
|
|
if (restore_nofile) {
|
|
(void)setrlimit(RLIMIT_NOFILE, &saved_nofile);
|
|
restore_nofile = FALSE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void test_round_robin_wraps_at_server_count(void **state) {
|
|
(void) state;
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_pid = 10;
|
|
php_processes[0].php_read_fd = 10;
|
|
php_processes[0].php_write_fd = 11;
|
|
php_processes[1].php_state = PHP_READY;
|
|
php_processes[1].php_pid = 12;
|
|
php_processes[1].php_read_fd = 12;
|
|
php_processes[1].php_write_fd = 13;
|
|
set.php.php_current_server = set.php.php_servers;
|
|
assert_int_equal(php_get_process(), 0);
|
|
assert_int_equal(php_get_process(), 1);
|
|
assert_int_equal(php_get_process(), 0);
|
|
php_processes[0].php_pid = php_processes[0].php_read_fd = php_processes[0].php_write_fd = -1;
|
|
php_processes[1].php_pid = php_processes[1].php_read_fd = php_processes[1].php_write_fd = -1;
|
|
}
|
|
|
|
static void test_process_slot_initialization_invalidates_zero_descriptors(void **state) {
|
|
php_t slots[3] = {{0}};
|
|
int i;
|
|
|
|
(void) state;
|
|
php_processes_initialize(slots, 3);
|
|
for (i = 0; i < 3; i++) {
|
|
assert_int_equal(slots[i].php_state, PHP_BUSY);
|
|
assert_int_equal(slots[i].php_pid, -1);
|
|
assert_int_equal(slots[i].php_read_fd, -1);
|
|
assert_int_equal(slots[i].php_write_fd, -1);
|
|
}
|
|
}
|
|
|
|
static void test_round_robin_rejects_failed_slots(void **state) {
|
|
(void) state;
|
|
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
|
assert_int_equal(php_get_process(), -1);
|
|
}
|
|
|
|
static void test_round_robin_returns_a_healthy_slot_when_all_are_contended(void **state) {
|
|
int i;
|
|
(void) state;
|
|
|
|
for (i = 0; i < set.php.php_servers; i++) {
|
|
php_processes[i].php_state = PHP_READY;
|
|
php_processes[i].php_pid = 100 + i;
|
|
php_processes[i].php_read_fd = 20 + (i * 2);
|
|
php_processes[i].php_write_fd = 21 + (i * 2);
|
|
thread_mutex_lock(LOCK_PHP_PROC_0 + i);
|
|
}
|
|
|
|
set.php.php_current_server = 0;
|
|
assert_int_equal(php_get_process(), 0);
|
|
|
|
for (i = 0; i < set.php.php_servers; i++) {
|
|
thread_mutex_unlock(LOCK_PHP_PROC_0 + i);
|
|
php_processes[i].php_pid = -1;
|
|
php_processes[i].php_read_fd = -1;
|
|
php_processes[i].php_write_fd = -1;
|
|
}
|
|
}
|
|
|
|
static void *hold_php_slot(void *arg) {
|
|
int slot = *(int *)arg;
|
|
|
|
thread_mutex_lock(LOCK_PHP_PROC_0 + slot);
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
shell_spawn_reached = TRUE;
|
|
pthread_cond_broadcast(&spawn_barrier_cond);
|
|
while (!release_shell_spawn) {
|
|
pthread_cond_wait(&spawn_barrier_cond, &spawn_barrier_mutex);
|
|
}
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
thread_mutex_unlock(LOCK_PHP_PROC_0 + slot);
|
|
return NULL;
|
|
}
|
|
|
|
static void test_dead_slot_is_recovered_while_another_slot_is_contended(void **state) {
|
|
pthread_t holder;
|
|
int held_slot = 0;
|
|
int selected;
|
|
(void) state;
|
|
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_pid = 100;
|
|
php_processes[0].php_read_fd = 20;
|
|
php_processes[0].php_write_fd = 21;
|
|
set.php.php_current_server = 0;
|
|
assert_int_equal(pthread_create(&holder, NULL, hold_php_slot, &held_slot), 0);
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
while (!shell_spawn_reached) {
|
|
pthread_cond_wait(&spawn_barrier_cond, &spawn_barrier_mutex);
|
|
}
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
|
|
selected = php_get_process();
|
|
assert_int_equal(selected, 1);
|
|
assert_int_equal(php_processes[1].php_state, PHP_READY);
|
|
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
release_shell_spawn = TRUE;
|
|
pthread_cond_broadcast(&spawn_barrier_cond);
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
assert_int_equal(pthread_join(holder, NULL), 0);
|
|
php_processes[0].php_pid = -1;
|
|
php_processes[0].php_read_fd = -1;
|
|
php_processes[0].php_write_fd = -1;
|
|
}
|
|
|
|
static void test_failed_recovery_falls_back_to_the_contended_slot(void **state) {
|
|
pthread_t holder;
|
|
int held_slot = 0;
|
|
int selected;
|
|
(void) state;
|
|
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_pid = 100;
|
|
php_processes[0].php_read_fd = 20;
|
|
php_processes[0].php_write_fd = 21;
|
|
set.php.php_current_server = 0;
|
|
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
|
assert_int_equal(pthread_create(&holder, NULL, hold_php_slot, &held_slot), 0);
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
while (!shell_spawn_reached) {
|
|
pthread_cond_wait(&spawn_barrier_cond, &spawn_barrier_mutex);
|
|
}
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
|
|
selected = php_get_process();
|
|
assert_int_equal(selected, 0);
|
|
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
release_shell_spawn = TRUE;
|
|
pthread_cond_broadcast(&spawn_barrier_cond);
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
assert_int_equal(pthread_join(holder, NULL), 0);
|
|
php_processes[0].php_pid = -1;
|
|
php_processes[0].php_read_fd = -1;
|
|
php_processes[0].php_write_fd = -1;
|
|
}
|
|
|
|
static void noop_sigpipe_handler(int signal_number) {
|
|
(void) signal_number;
|
|
}
|
|
|
|
static void test_broken_pipe_with_runtime_sigpipe_handler_does_not_block(void **state) {
|
|
struct sigaction action;
|
|
int pdes[2];
|
|
pid_t child;
|
|
int status;
|
|
int attempts;
|
|
(void) state;
|
|
|
|
assert_int_equal(pipe(pdes), 0);
|
|
close(pdes[0]);
|
|
child = fork();
|
|
assert_true(child >= 0);
|
|
if (child == 0) {
|
|
memset(&action, 0, sizeof(action));
|
|
action.sa_handler = noop_sigpipe_handler;
|
|
sigemptyset(&action.sa_mask);
|
|
if (sigaction(SIGPIPE, &action, NULL) != 0) _exit(2);
|
|
errno = 0;
|
|
if (php_write_no_sigpipe_for_test(pdes[1], "x", 1) != -1 || errno != EPIPE)
|
|
_exit(3);
|
|
_exit(0);
|
|
}
|
|
close(pdes[1]);
|
|
for (attempts = 0; attempts < 100; attempts++) {
|
|
pid_t waited = waitpid(child, &status, WNOHANG);
|
|
if (waited == child) break;
|
|
assert_true(waited == 0 || (waited < 0 && errno == EINTR));
|
|
usleep(10000);
|
|
}
|
|
if (attempts == 100) {
|
|
kill(child, SIGKILL);
|
|
waitpid(child, &status, 0);
|
|
fail_msg("SIGPIPE-protected write blocked for more than one second");
|
|
}
|
|
assert_true(WIFEXITED(status));
|
|
assert_int_equal(WEXITSTATUS(status), 0);
|
|
}
|
|
|
|
static void test_sigmask_failure_prevents_the_pipe_write(void **state) {
|
|
(void) state;
|
|
|
|
fail_next_sigmask = TRUE;
|
|
track_write = TRUE;
|
|
errno = 0;
|
|
assert_int_equal(php_write_no_sigpipe_for_test(-1, "x", 1), -1);
|
|
assert_int_equal(errno, EAGAIN);
|
|
assert_int_equal(write_calls, 0);
|
|
track_write = FALSE;
|
|
}
|
|
|
|
static void test_preexisting_pending_sigpipe_is_preserved(void **state) {
|
|
struct sigaction saved_action;
|
|
struct sigaction default_action;
|
|
sigset_t blocked;
|
|
sigset_t old_mask;
|
|
sigset_t pending;
|
|
int pdes[2];
|
|
int received_signal = 0;
|
|
int result;
|
|
int saved_errno;
|
|
int pending_before;
|
|
int pending_after;
|
|
|
|
(void) state;
|
|
assert_int_equal(sigaction(SIGPIPE, NULL, &saved_action), 0);
|
|
sigemptyset(&blocked);
|
|
sigaddset(&blocked, SIGPIPE);
|
|
assert_int_equal(pthread_sigmask(SIG_BLOCK, &blocked, &old_mask), 0);
|
|
memset(&default_action, 0, sizeof(default_action));
|
|
default_action.sa_handler = SIG_DFL;
|
|
sigemptyset(&default_action.sa_mask);
|
|
assert_int_equal(sigaction(SIGPIPE, &default_action, NULL), 0);
|
|
assert_int_equal(raise(SIGPIPE), 0);
|
|
assert_int_equal(sigpending(&pending), 0);
|
|
pending_before = sigismember(&pending, SIGPIPE);
|
|
|
|
assert_int_equal(pipe(pdes), 0);
|
|
close(pdes[0]);
|
|
errno = 0;
|
|
result = (int)php_write_no_sigpipe_for_test(pdes[1], "x", 1);
|
|
saved_errno = errno;
|
|
assert_int_equal(sigpending(&pending), 0);
|
|
pending_after = sigismember(&pending, SIGPIPE);
|
|
close(pdes[1]);
|
|
assert_int_equal(sigwait(&blocked, &received_signal), 0);
|
|
assert_int_equal(sigaction(SIGPIPE, &saved_action, NULL), 0);
|
|
assert_int_equal(pthread_sigmask(SIG_SETMASK, &old_mask, NULL), 0);
|
|
|
|
assert_true(pending_before);
|
|
assert_int_equal(result, -1);
|
|
assert_int_equal(saved_errno, EPIPE);
|
|
assert_true(pending_after);
|
|
assert_int_equal(received_signal, SIGPIPE);
|
|
}
|
|
|
|
static void test_spawnattr_sigpipe_failure_destroys_initialized_attr(void **state) {
|
|
posix_spawnattr_t attr;
|
|
(void) state;
|
|
|
|
fail_next_setsigdefault = TRUE;
|
|
track_spawnattr_destroy = TRUE;
|
|
errno = 0;
|
|
assert_int_equal(spine_spawnattr_sigpipe_default(&attr), -1);
|
|
assert_int_equal(errno, EIO);
|
|
assert_true(spawnattr_destroyed);
|
|
track_spawnattr_destroy = FALSE;
|
|
}
|
|
|
|
static void test_full_script_server_lifecycle(void **state) {
|
|
char *result;
|
|
|
|
(void) state;
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
|
assert_true(php_processes[0].php_pid > 1);
|
|
assert_true(php_processes[0].php_read_fd >= 0);
|
|
assert_true(php_processes[0].php_write_fd >= 0);
|
|
|
|
result = php_cmd("poll 7", 0);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "42\n");
|
|
free(result);
|
|
|
|
php_close(0);
|
|
assert_int_equal(php_processes[0].php_pid, -1);
|
|
assert_int_equal(php_processes[0].php_read_fd, -1);
|
|
assert_int_equal(php_processes[0].php_write_fd, -1);
|
|
}
|
|
|
|
static void test_legacy_cacti_version_uses_positional_poller_id(void **state) {
|
|
(void) state;
|
|
set.cacti_version = 1222;
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
|
php_close(0);
|
|
}
|
|
|
|
static void test_remote_poller_id_uses_environ_argument(void **state) {
|
|
(void) state;
|
|
set.poller.poller_id = 2;
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
|
php_close(0);
|
|
}
|
|
|
|
static void test_php_child_restores_sigpipe_default(void **state) {
|
|
(void) state;
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "check-sigpipe");
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
|
}
|
|
|
|
static void test_script_server_lifecycle_with_stdio_closed(void **state) {
|
|
int saved_stdin;
|
|
int saved_stdout;
|
|
int init_result;
|
|
char *result = NULL;
|
|
|
|
(void) state;
|
|
saved_stdin = dup(STDIN_FILENO);
|
|
saved_stdout = dup(STDOUT_FILENO);
|
|
assert_true(saved_stdin >= 0);
|
|
assert_true(saved_stdout >= 0);
|
|
|
|
close(STDIN_FILENO);
|
|
close(STDOUT_FILENO);
|
|
init_result = php_init(0);
|
|
if (init_result == TRUE) {
|
|
result = php_cmd("poll 7", 0);
|
|
php_close(0);
|
|
}
|
|
|
|
dup2(saved_stdin, STDIN_FILENO);
|
|
dup2(saved_stdout, STDOUT_FILENO);
|
|
close(saved_stdin);
|
|
close(saved_stdout);
|
|
|
|
assert_int_equal(init_result, TRUE);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "42\n");
|
|
free(result);
|
|
}
|
|
|
|
struct popen_thread_result {
|
|
int fd;
|
|
};
|
|
|
|
static void *open_script_while_blocked(void *arg) {
|
|
struct popen_thread_result *result = arg;
|
|
result->fd = nft_popen("printf concurrent-visible", "r");
|
|
return NULL;
|
|
}
|
|
|
|
static void test_php_spawn_does_not_inherit_an_nft_collision_descriptor(void **state) {
|
|
struct popen_thread_result opened = { .fd = -1 };
|
|
pthread_t thread;
|
|
struct pollfd pfd;
|
|
char output[128] = {0};
|
|
ssize_t n;
|
|
int total = 0;
|
|
int reached_eof = FALSE;
|
|
int saved_stdin;
|
|
int saved_stdout;
|
|
int init_result;
|
|
|
|
(void) state;
|
|
saved_stdin = dup(STDIN_FILENO);
|
|
saved_stdout = dup(STDOUT_FILENO);
|
|
assert_true(saved_stdin >= 0);
|
|
assert_true(saved_stdout >= 0);
|
|
close(STDIN_FILENO);
|
|
close(STDOUT_FILENO);
|
|
|
|
block_shell_spawn = TRUE;
|
|
assert_int_equal(pthread_create(&thread, NULL, open_script_while_blocked, &opened), 0);
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
while (!shell_spawn_reached) {
|
|
pthread_cond_wait(&spawn_barrier_cond, &spawn_barrier_mutex);
|
|
}
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
|
|
/* The nft_popen collision duplicate exists but has not spawned yet. A PHP
|
|
* child started in this window must not inherit that pipe write end. */
|
|
init_result = php_init(0);
|
|
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
release_shell_spawn = TRUE;
|
|
pthread_cond_broadcast(&spawn_barrier_cond);
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
assert_int_equal(pthread_join(thread, NULL), 0);
|
|
|
|
if (opened.fd >= 0) {
|
|
pfd.fd = opened.fd;
|
|
pfd.events = POLLIN;
|
|
while (poll(&pfd, 1, 2000) > 0) {
|
|
n = read(opened.fd, output + total, sizeof(output) - 1 - (size_t)total);
|
|
if (n == 0) {
|
|
reached_eof = TRUE;
|
|
break;
|
|
}
|
|
if (n < 0) break;
|
|
total += (int)n;
|
|
if ((size_t)total >= sizeof(output) - 1) break;
|
|
}
|
|
output[total] = '\0';
|
|
}
|
|
|
|
if (init_result == TRUE) php_close(0);
|
|
if (opened.fd >= 0) nft_pclose(opened.fd);
|
|
dup2(saved_stdin, STDIN_FILENO);
|
|
dup2(saved_stdout, STDOUT_FILENO);
|
|
close(saved_stdin);
|
|
close(saved_stdout);
|
|
|
|
assert_int_equal(init_result, TRUE);
|
|
assert_true(opened.fd >= 0);
|
|
assert_non_null(strstr(output, "concurrent-visible"));
|
|
assert_true(reached_eof);
|
|
}
|
|
|
|
static void test_init_marks_an_unexpected_handshake_busy(void **state) {
|
|
(void) state;
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "bad-start");
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
|
}
|
|
|
|
static void test_busy_handshake_is_recovered_on_the_next_poll(void **state) {
|
|
char *result;
|
|
int process;
|
|
pid_t failed_pid;
|
|
|
|
(void) state;
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "bad-start");
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
|
failed_pid = php_processes[0].php_pid;
|
|
assert_true(failed_pid > 1);
|
|
assert_true(php_processes[0].php_read_fd >= 0);
|
|
assert_true(php_processes[0].php_write_fd >= 0);
|
|
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "normal");
|
|
process = php_get_process();
|
|
assert_int_equal(process, 0);
|
|
assert_true(php_processes[0].php_pid > 1);
|
|
assert_int_not_equal(php_processes[0].php_pid, failed_pid);
|
|
errno = 0;
|
|
assert_int_equal(waitpid(failed_pid, NULL, WNOHANG), -1);
|
|
assert_int_equal(errno, ECHILD);
|
|
result = php_cmd("poll 7", process);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "42\n");
|
|
free(result);
|
|
}
|
|
|
|
struct php_thread_result {
|
|
int process;
|
|
char *command_result;
|
|
};
|
|
|
|
static int command_finished;
|
|
|
|
static void *get_php_process_thread(void *arg) {
|
|
struct php_thread_result *result = arg;
|
|
result->process = php_get_process();
|
|
return NULL;
|
|
}
|
|
|
|
static void *run_php_command_thread(void *arg) {
|
|
struct php_thread_result *result = arg;
|
|
result->command_result = php_cmd("poll 7", 0);
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
command_finished = TRUE;
|
|
pthread_cond_broadcast(&spawn_barrier_cond);
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
return NULL;
|
|
}
|
|
|
|
static void test_recovery_and_command_share_the_slot_lock(void **state) {
|
|
struct php_thread_result recovery = { .process = -1, .command_result = NULL };
|
|
struct php_thread_result command = { .process = -1, .command_result = NULL };
|
|
pthread_t recovery_thread;
|
|
pthread_t command_thread;
|
|
(void) state;
|
|
|
|
set.php.php_servers = 1;
|
|
block_php_spawn = TRUE;
|
|
command_finished = FALSE;
|
|
assert_int_equal(pthread_create(&recovery_thread, NULL, get_php_process_thread, &recovery), 0);
|
|
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
while (!shell_spawn_reached) {
|
|
pthread_cond_wait(&spawn_barrier_cond, &spawn_barrier_mutex);
|
|
}
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
|
|
/* A blocked handshake must not retain the process-global round-robin lock. */
|
|
assert_int_equal(thread_mutex_trylock(LOCK_PHP), 0);
|
|
thread_mutex_unlock(LOCK_PHP);
|
|
|
|
assert_int_equal(pthread_create(&command_thread, NULL, run_php_command_thread, &command), 0);
|
|
usleep(20000);
|
|
pthread_mutex_lock(&spawn_barrier_mutex);
|
|
assert_false(command_finished);
|
|
release_shell_spawn = TRUE;
|
|
pthread_cond_broadcast(&spawn_barrier_cond);
|
|
pthread_mutex_unlock(&spawn_barrier_mutex);
|
|
|
|
assert_int_equal(pthread_join(recovery_thread, NULL), 0);
|
|
assert_int_equal(pthread_join(command_thread, NULL), 0);
|
|
assert_int_equal(recovery.process, 0);
|
|
assert_non_null(command.command_result);
|
|
assert_string_equal(command.command_result, "42\n");
|
|
free(command.command_result);
|
|
}
|
|
|
|
static void test_recovery_attempts_only_one_failed_slot_per_call(void **state) {
|
|
double started;
|
|
double elapsed;
|
|
(void) state;
|
|
|
|
set.php.php_servers = 3;
|
|
set.php.script_timeout = 1;
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "silent");
|
|
started = get_time_as_double();
|
|
assert_int_equal(php_get_process(), -1);
|
|
elapsed = get_time_as_double() - started;
|
|
|
|
assert_true(elapsed < 2.5);
|
|
}
|
|
|
|
static void test_child_exit_during_startup_fails_closed_without_restart(void **state) {
|
|
(void) state;
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "exit-before-start");
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
|
}
|
|
|
|
static void test_php_init_later_failure_preserves_earlier_server(void **state) {
|
|
pid_t first_pid;
|
|
int first_read_fd;
|
|
int first_write_fd;
|
|
(void) state;
|
|
|
|
set.php.php_servers = 2;
|
|
fail_php_spawn_call = 2;
|
|
assert_int_equal(php_init(PHP_INIT), FALSE);
|
|
|
|
first_pid = php_processes[0].php_pid;
|
|
first_read_fd = php_processes[0].php_read_fd;
|
|
first_write_fd = php_processes[0].php_write_fd;
|
|
assert_true(first_pid > 1);
|
|
assert_true(first_read_fd >= 0);
|
|
assert_true(first_write_fd >= 0);
|
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
|
assert_int_equal(php_processes[0].php_pid, first_pid);
|
|
assert_int_equal(php_processes[0].php_read_fd, first_read_fd);
|
|
assert_int_equal(php_processes[0].php_write_fd, first_write_fd);
|
|
assert_int_equal(php_processes[1].php_pid, -1);
|
|
assert_int_equal(php_processes[1].php_read_fd, -1);
|
|
assert_int_equal(php_processes[1].php_write_fd, -1);
|
|
assert_int_equal(php_processes[1].php_state, PHP_BUSY);
|
|
}
|
|
|
|
static void assert_stdio_collision_failure_is_clean(int fail_dup, int fail_cloexec) {
|
|
int saved_stdin = dup(STDIN_FILENO);
|
|
int result;
|
|
assert_true(saved_stdin >= 0);
|
|
close(STDIN_FILENO);
|
|
fail_next_dup = fail_dup;
|
|
fail_duplicated_cloexec = fail_cloexec;
|
|
result = php_init(0);
|
|
dup2(saved_stdin, STDIN_FILENO);
|
|
close(saved_stdin);
|
|
assert_int_equal(result, FALSE);
|
|
assert_int_equal(php_processes[0].php_pid, -1);
|
|
assert_int_equal(php_processes[0].php_read_fd, -1);
|
|
assert_int_equal(php_processes[0].php_write_fd, -1);
|
|
}
|
|
|
|
static void test_stdio_collision_dup_failure_is_clean(void **state) {
|
|
(void) state;
|
|
assert_stdio_collision_failure_is_clean(TRUE, FALSE);
|
|
}
|
|
|
|
static void test_stdio_collision_cloexec_failure_is_clean(void **state) {
|
|
(void) state;
|
|
assert_stdio_collision_failure_is_clean(FALSE, TRUE);
|
|
}
|
|
|
|
static void assert_nft_collision_failure_is_clean(const char *type, int fail_dup, int fail_cloexec) {
|
|
int saved_stdin = dup(STDIN_FILENO);
|
|
int saved_stdout = dup(STDOUT_FILENO);
|
|
int result;
|
|
|
|
assert_true(saved_stdin >= 0);
|
|
assert_true(saved_stdout >= 0);
|
|
close(STDIN_FILENO);
|
|
close(STDOUT_FILENO);
|
|
fail_next_dup = fail_dup;
|
|
fail_duplicated_cloexec = fail_cloexec;
|
|
result = nft_popen("exit 0", type);
|
|
dup2(saved_stdin, STDIN_FILENO);
|
|
dup2(saved_stdout, STDOUT_FILENO);
|
|
close(saved_stdin);
|
|
close(saved_stdout);
|
|
assert_int_equal(result, -1);
|
|
}
|
|
|
|
static void test_nft_read_collision_cloexec_failure_is_clean(void **state) {
|
|
(void) state;
|
|
assert_nft_collision_failure_is_clean("r", FALSE, TRUE);
|
|
}
|
|
|
|
static void test_nft_write_collision_cloexec_failure_is_clean(void **state) {
|
|
(void) state;
|
|
assert_nft_collision_failure_is_clean("w", FALSE, TRUE);
|
|
}
|
|
|
|
static void test_cloexec_pipe_failure_releases_both_descriptors(void **state) {
|
|
int pdes[2] = {-2, -2};
|
|
int first_reused;
|
|
int second_reused;
|
|
|
|
(void) state;
|
|
fail_next_cloexec = TRUE;
|
|
assert_false(spine_open_pipe_cloexec(pdes));
|
|
assert_int_equal(pdes[0], -1);
|
|
assert_int_equal(pdes[1], -1);
|
|
first_reused = open("/dev/null", O_RDONLY);
|
|
second_reused = open("/dev/null", O_RDONLY);
|
|
assert_true(first_reused >= 0);
|
|
assert_true(second_reused >= 0);
|
|
close(first_reused);
|
|
close(second_reused);
|
|
}
|
|
|
|
static void test_init_timeout_does_not_recurse(void **state) {
|
|
(void) state;
|
|
set.php.script_timeout = 0;
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "silent");
|
|
assert_int_equal(php_init(0), TRUE);
|
|
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
|
}
|
|
|
|
static void test_spawn_failure_releases_every_resource(void **state) {
|
|
(void) state;
|
|
/* calloc-like zeroes reproduce the process-wide initialization that used to
|
|
* let a failed slot masquerade as stdin. php_init() must replace them. */
|
|
memset(&php_processes[0], 0, sizeof(php_processes[0]));
|
|
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
|
assert_int_equal(php_init(0), FALSE);
|
|
assert_int_equal(php_processes[0].php_pid, -1);
|
|
assert_int_equal(php_processes[0].php_read_fd, -1);
|
|
assert_int_equal(php_processes[0].php_write_fd, -1);
|
|
}
|
|
|
|
static void test_command_rejects_a_writable_poisoned_slot(void **state) {
|
|
char *result;
|
|
int fd;
|
|
|
|
(void) state;
|
|
fd = open("/dev/null", O_RDWR);
|
|
assert_true(fd >= 0);
|
|
memset(&php_processes[0], 0, sizeof(php_processes[0]));
|
|
php_processes[0].php_write_fd = fd;
|
|
|
|
result = php_cmd("poll 9", 0);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
close(fd);
|
|
php_processes[0].php_read_fd = php_processes[0].php_write_fd = -1;
|
|
}
|
|
|
|
static void assert_invalid_reader_result(int slot) {
|
|
char command[] = "test";
|
|
char *result;
|
|
|
|
result = php_readpipe(slot, command);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
result = php_read_result_for_test(slot, command, FALSE);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
}
|
|
|
|
static void test_readpipe_rejects_invalid_slots(void **state) {
|
|
php_t *owned_processes = php_processes;
|
|
int invalid_slots[] = {-1, 2, MAX_PHP_SERVERS, INT_MAX};
|
|
size_t index;
|
|
|
|
(void) state;
|
|
track_write = TRUE;
|
|
php_processes = NULL;
|
|
assert_invalid_reader_result(0);
|
|
php_processes = owned_processes;
|
|
for (index = 0; index < sizeof(invalid_slots) / sizeof(invalid_slots[0]); index++) {
|
|
assert_invalid_reader_result(invalid_slots[index]);
|
|
}
|
|
set.php.php_servers = 0;
|
|
assert_invalid_reader_result(0);
|
|
set.php.php_servers = -1;
|
|
assert_invalid_reader_result(0);
|
|
/* An invalid configured count cannot authorize an out-of-array slot. */
|
|
set.php.php_servers = MAX_PHP_SERVERS + 1;
|
|
assert_invalid_reader_result(MAX_PHP_SERVERS);
|
|
assert_int_equal(php_spawn_calls, 0);
|
|
assert_int_equal(write_calls, 0);
|
|
}
|
|
|
|
static void test_readpipe_rejects_fd_at_fd_setsize(void **state) {
|
|
char command[] = "test";
|
|
char *result;
|
|
int oversized_fd;
|
|
int pdes[2];
|
|
|
|
(void) state;
|
|
assert_int_equal(pipe(pdes), 0);
|
|
oversized_fd = duplicate_at_fdsetsize(pdes[0]);
|
|
close(pdes[0]);
|
|
close(pdes[1]);
|
|
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_read_fd = oversized_fd;
|
|
result = php_readpipe(0, command);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
assert_int_not_equal(php_processes[0].php_state, PHP_READY);
|
|
assert_int_equal(php_processes[0].php_read_fd, -1);
|
|
assert_int_equal(php_get_process(), -1);
|
|
}
|
|
|
|
static void test_startup_read_rejects_fd_at_fd_setsize_without_restart(void **state) {
|
|
char command[] = "INIT";
|
|
char *result;
|
|
int oversized_fd;
|
|
int pdes[2];
|
|
(void) state;
|
|
|
|
assert_int_equal(pipe(pdes), 0);
|
|
oversized_fd = duplicate_at_fdsetsize(pdes[0]);
|
|
close(pdes[0]);
|
|
close(pdes[1]);
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_read_fd = oversized_fd;
|
|
php_processes[0].php_write_fd = -1;
|
|
|
|
result = php_read_result_for_test(0, command, FALSE);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
|
assert_int_equal(php_processes[0].php_read_fd, oversized_fd);
|
|
assert_int_equal(php_spawn_calls, 0);
|
|
close(oversized_fd);
|
|
php_processes[0].php_read_fd = -1;
|
|
}
|
|
|
|
static void test_command_retires_fd_at_fd_setsize(void **state) {
|
|
char *result;
|
|
int oversized_fd;
|
|
int pdes[2];
|
|
|
|
(void) state;
|
|
assert_int_equal(pipe(pdes), 0);
|
|
oversized_fd = duplicate_at_fdsetsize(pdes[0]);
|
|
close(pdes[0]);
|
|
|
|
php_processes[0].php_pid = fork();
|
|
assert_true(php_processes[0].php_pid >= 0);
|
|
if (php_processes[0].php_pid == 0) {
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
set.php.php_servers = 1;
|
|
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_read_fd = oversized_fd;
|
|
php_processes[0].php_write_fd = pdes[1];
|
|
|
|
result = php_cmd("poll 9", 0);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
assert_int_not_equal(php_processes[0].php_state, PHP_READY);
|
|
assert_int_equal(php_processes[0].php_pid, -1);
|
|
assert_int_equal(php_processes[0].php_read_fd, -1);
|
|
assert_int_equal(php_processes[0].php_write_fd, -1);
|
|
assert_int_equal(php_get_process(), -1);
|
|
}
|
|
|
|
static void test_readpipe_rejects_an_oversized_response(void **state) {
|
|
char payload[RESULTS_BUFFER];
|
|
char command[] = "test";
|
|
char *result;
|
|
int pdes[2];
|
|
|
|
(void) state;
|
|
memset(payload, 'x', sizeof(payload));
|
|
assert_int_equal(pipe(pdes), 0);
|
|
assert_int_equal(write(pdes[1], payload, sizeof(payload)), (ssize_t)sizeof(payload));
|
|
close(pdes[1]);
|
|
php_processes[0].php_read_fd = pdes[0];
|
|
|
|
result = php_readpipe(0, command);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
|
assert_true(php_processes[0].php_pid > 1);
|
|
result = php_cmd("poll 7", 0);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "42\n");
|
|
free(result);
|
|
}
|
|
|
|
static void prepare_broken_ready_slot(void) {
|
|
int pdes[2];
|
|
|
|
assert_int_equal(pipe(pdes), 0);
|
|
close(pdes[0]);
|
|
php_processes[0].php_pid = fork();
|
|
assert_true(php_processes[0].php_pid >= 0);
|
|
if (php_processes[0].php_pid == 0) {
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_read_fd = open("/dev/null", O_RDONLY);
|
|
assert_true(php_processes[0].php_read_fd >= 0);
|
|
php_processes[0].php_write_fd = pdes[1];
|
|
track_write = TRUE;
|
|
}
|
|
|
|
static void test_failed_write_stops_when_restart_handshake_is_not_ready(void **state) {
|
|
char *result;
|
|
|
|
(void) state;
|
|
prepare_broken_ready_slot();
|
|
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "bad-start");
|
|
result = php_cmd("poll 9", 0);
|
|
track_write = FALSE;
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
assert_int_equal(php_spawn_calls, 1);
|
|
assert_int_equal(write_calls, 2);
|
|
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
|
}
|
|
|
|
static void test_failed_write_stops_when_restart_spawn_fails(void **state) {
|
|
char *result;
|
|
|
|
(void) state;
|
|
prepare_broken_ready_slot();
|
|
fail_php_spawn_call = 1;
|
|
result = php_cmd("poll 9", 0);
|
|
track_write = FALSE;
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
assert_int_equal(php_spawn_calls, 1);
|
|
assert_int_equal(write_calls, 2);
|
|
assert_int_equal(php_processes[0].php_pid, -1);
|
|
}
|
|
|
|
static void test_command_gives_up_after_three_failed_writes(void **state) {
|
|
struct sigaction saved_sigpipe;
|
|
struct sigaction default_sigpipe;
|
|
char *result;
|
|
int pdes[2];
|
|
|
|
(void) state;
|
|
assert_int_equal(pipe(pdes), 0);
|
|
close(pdes[0]);
|
|
php_processes[0].php_state = PHP_READY;
|
|
php_processes[0].php_pid = fork();
|
|
assert_true(php_processes[0].php_pid >= 0);
|
|
if (php_processes[0].php_pid == 0) {
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
php_processes[0].php_read_fd = pdes[0];
|
|
php_processes[0].php_write_fd = pdes[1];
|
|
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
|
assert_int_equal(sigaction(SIGPIPE, NULL, &saved_sigpipe), 0);
|
|
memset(&default_sigpipe, 0, sizeof(default_sigpipe));
|
|
default_sigpipe.sa_handler = SIG_DFL;
|
|
assert_int_equal(sigaction(SIGPIPE, &default_sigpipe, NULL), 0);
|
|
|
|
result = php_cmd("poll 9", 0);
|
|
sigaction(SIGPIPE, &saved_sigpipe, NULL);
|
|
assert_non_null(result);
|
|
assert_string_equal(result, "U");
|
|
free(result);
|
|
}
|
|
|
|
int main(void) {
|
|
const struct CMUnitTest tests[] = {
|
|
cmocka_unit_test_setup_teardown(test_round_robin_wraps_at_server_count, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_process_slot_initialization_invalidates_zero_descriptors, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_round_robin_rejects_failed_slots, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_round_robin_returns_a_healthy_slot_when_all_are_contended, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_dead_slot_is_recovered_while_another_slot_is_contended, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_failed_recovery_falls_back_to_the_contended_slot, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_broken_pipe_with_runtime_sigpipe_handler_does_not_block, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_sigmask_failure_prevents_the_pipe_write, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_preexisting_pending_sigpipe_is_preserved, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_spawnattr_sigpipe_failure_destroys_initialized_attr, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_full_script_server_lifecycle, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_legacy_cacti_version_uses_positional_poller_id, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_remote_poller_id_uses_environ_argument, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_php_child_restores_sigpipe_default, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_script_server_lifecycle_with_stdio_closed, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_php_spawn_does_not_inherit_an_nft_collision_descriptor, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_init_marks_an_unexpected_handshake_busy, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_busy_handshake_is_recovered_on_the_next_poll, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_recovery_and_command_share_the_slot_lock, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_recovery_attempts_only_one_failed_slot_per_call, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_child_exit_during_startup_fails_closed_without_restart, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_php_init_later_failure_preserves_earlier_server, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_stdio_collision_dup_failure_is_clean, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_stdio_collision_cloexec_failure_is_clean, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_nft_read_collision_cloexec_failure_is_clean, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_nft_write_collision_cloexec_failure_is_clean, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_cloexec_pipe_failure_releases_both_descriptors, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_init_timeout_does_not_recurse, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_spawn_failure_releases_every_resource, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_command_rejects_a_writable_poisoned_slot, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_readpipe_rejects_invalid_slots, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_readpipe_rejects_fd_at_fd_setsize, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_startup_read_rejects_fd_at_fd_setsize_without_restart, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_command_retires_fd_at_fd_setsize, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_readpipe_rejects_an_oversized_response, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_failed_write_stops_when_restart_handshake_is_not_ready, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_failed_write_stops_when_restart_spawn_fails, php_setup, php_teardown),
|
|
cmocka_unit_test_setup_teardown(test_command_gives_up_after_three_failed_writes, php_setup, php_teardown),
|
|
};
|
|
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
}
|