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>
842 lines
28 KiB
C
842 lines
28 KiB
C
/*
|
|
+-------------------------------------------------------------------------+
|
|
| 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. |
|
|
| |
|
|
| 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 General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
/*******************************************************************************
|
|
ex: set tabstop=4 shiftwidth=4 autoindent:
|
|
* (C) Xenadyne Inc. 2002. All Rights Reserved
|
|
*
|
|
* Permission to use, copy, modify and distribute this software for
|
|
* any purpose and without fee is hereby granted, provided that the
|
|
* above copyright notice appears in all copies. Also note the
|
|
* University of California copyright below.
|
|
*
|
|
* XENADYNE INC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
|
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
|
|
* IN NO EVENT SHALL XENADYNE BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
|
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM THE
|
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
|
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
* File: nft_popen.c
|
|
*
|
|
* Description: A thread-safe replacement for popen()/pclose().
|
|
*
|
|
* This is a thread-safe variant of popen that does unbuffered IO, to
|
|
* avoid running afoul of Solaris's inability to fdopen when fd > 255.
|
|
*
|
|
*******************************************************************************
|
|
*/
|
|
|
|
/*
|
|
* Copyright (c) 1988, 1993
|
|
* The Regents of the University of California. All rights reserved.
|
|
*
|
|
* This code is derived from software written by Ken Arnold and
|
|
* published in UNIX Review, Vol. 6, No. 8.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. All advertising materials mentioning features or use of this software
|
|
* must display the following acknowledgement:
|
|
* This product includes software developed by the University of
|
|
* California, Berkeley and its contributors.
|
|
* 4. Neither the name of the University nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "spine.h"
|
|
#include <spawn.h>
|
|
#include <fcntl.h>
|
|
#include <sys/wait.h>
|
|
|
|
/* An instance of this struct is created for each popen() fd. */
|
|
static struct pid
|
|
{
|
|
struct pid *next;
|
|
int fd;
|
|
pid_t pid;
|
|
} * PidList;
|
|
|
|
/* Serialize access to PidList. */
|
|
static pthread_mutex_t ListMutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
/* Children nft_pclose() gave up waiting for. Nothing else in spine reaps: there
|
|
is no SIGCHLD handler and no waitpid(-1), so a child dropped here would stay
|
|
a zombie for the daemon's lifetime and accumulate once per affected script
|
|
per cycle against RLIMIT_NPROC. SA_NOCLDWAIT would fix the leak but auto-reap
|
|
every child, and spine reads exit status to tell a failed script from a silent
|
|
one, so the pids are parked here and swept with WNOHANG instead. Bounded: past
|
|
the cap the pid is logged and dropped, because an unbounded list trades a pid
|
|
leak for a memory leak. */
|
|
static pid_t AbandonedPids[NFT_ABANDONED_MAX];
|
|
static int AbandonedCount;
|
|
|
|
static void close_cleanup(void *);
|
|
static void nft_sweep_abandoned(void);
|
|
|
|
/* Close and remove an entry from the shared registry, then transfer exclusive
|
|
* ownership to the caller. Closing under ListMutex preserves the invariant
|
|
* that every descriptor still visible to nft_popen() is open, so its
|
|
* posix_spawn addclose walk cannot queue an already-closed descriptor. Once
|
|
* returned, no other thread can find or free the entry.
|
|
* Keep this noinline: GCC 12.2 emits -Wclobbered for the helper local when it
|
|
* is inlined into nft_pclose()'s pthread cleanup macro scope.
|
|
*/
|
|
static __attribute__((noinline)) struct pid *pid_list_close_and_take(int fd)
|
|
{
|
|
struct pid **link;
|
|
struct pid *cur = NULL;
|
|
|
|
pthread_mutex_lock(&ListMutex);
|
|
|
|
for (link = &PidList; *link != NULL; link = &(*link)->next) {
|
|
if ((*link)->fd == fd) {
|
|
cur = *link;
|
|
(void)close(cur->fd);
|
|
cur->fd = -1;
|
|
*link = cur->next;
|
|
cur->next = NULL;
|
|
break;
|
|
}
|
|
}
|
|
|
|
pthread_mutex_unlock(&ListMutex);
|
|
|
|
return cur;
|
|
}
|
|
/* nft_pclose() must not block a poller thread: a script that writes its
|
|
value and then lingers, or that ignores signals, would otherwise pin the
|
|
thread across polling cycles while holding its available_scripts token.
|
|
It gets a brief (~20ms), non-escalating WNOHANG allowance via
|
|
spine_reap_child_bounded() below - just enough to absorb ordinary
|
|
fork/exec/exit scheduling latency (measured flaky under virtualized/loaded
|
|
CI at the original 2ms budget), not to wait out a script that is actually
|
|
still working. Anything still running past that is killed outright and
|
|
handed to nft_abandon_child() so nft_sweep_abandoned() reaps it on a
|
|
later, unrelated nft_popen() call instead of this thread waiting for it. */
|
|
#define NFT_PCLOSE_REAP_USEC 50000
|
|
#define NFT_PCLOSE_SPIN_USEC 200
|
|
#define NFT_PCLOSE_SPIN_ATTEMPTS 100
|
|
|
|
int spine_set_cloexec(int fd) {
|
|
int flags;
|
|
|
|
flags = fcntl(fd, F_GETFD);
|
|
if (flags < 0) {
|
|
SPINE_LOG(("ERROR: Unable to read descriptor flags on fd %d: %s", fd, strerror(errno)));
|
|
return -1;
|
|
}
|
|
|
|
if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != 0) {
|
|
SPINE_LOG(("ERROR: Unable to set close-on-exec on fd %d: %s", fd, strerror(errno)));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int spine_dup_cloexec(int fd) {
|
|
int duplicate;
|
|
int saved_errno;
|
|
|
|
duplicate = dup(fd);
|
|
if (duplicate < 0)
|
|
return -1;
|
|
|
|
if (spine_set_cloexec(duplicate) != 0) {
|
|
saved_errno = errno;
|
|
(void)close(duplicate);
|
|
errno = saved_errno;
|
|
return -1;
|
|
}
|
|
|
|
return duplicate;
|
|
}
|
|
|
|
/*! \fn static int open_pipe_cloexec(int pdes[2])
|
|
* \brief open a pipe whose descriptors are not inherited across exec
|
|
*
|
|
* nft_popen() creates the pipe before taking ListMutex, so a second thread
|
|
* can spawn while these descriptors are live. Without close-on-exec that
|
|
* child holds the first thread's write end, the first thread never sees EOF,
|
|
* and it blocks to script_timeout for a data source that answered.
|
|
*
|
|
* pipe2(pdes, O_CLOEXEC) would set the flag atomically, but it needs
|
|
* _GNU_SOURCE on glibc and spine defines no feature macro, so the fcntl()
|
|
* pair stays. It leaves a window between the two calls, which is narrower
|
|
* than none.
|
|
*
|
|
* \return TRUE on success, FALSE with the descriptors closed on failure
|
|
*/
|
|
int spine_open_pipe_cloexec(int pdes[2]) {
|
|
if (pipe(pdes) < 0) {
|
|
SPINE_LOG(("ERROR: Unable to create a pipe: %s", strerror(errno)));
|
|
return FALSE;
|
|
}
|
|
|
|
/* spine_set_cloexec() has already said which descriptor failed and why;
|
|
* a descriptor that stays inheritable is worse than no pipe at all, so
|
|
* this fails rather than continuing without the flag. */
|
|
if (spine_set_cloexec(pdes[0]) != 0 || spine_set_cloexec(pdes[1]) != 0) {
|
|
(void)close(pdes[0]);
|
|
(void)close(pdes[1]);
|
|
|
|
/* the caller owns nothing on failure, so do not leave it holding two
|
|
descriptor numbers that now belong to whoever opens next; a caller
|
|
with one cleanup path would close them a second time */
|
|
pdes[0] = -1;
|
|
pdes[1] = -1;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
int spine_spawnattr_sigpipe_default(posix_spawnattr_t *attr) {
|
|
sigset_t defaults;
|
|
int rc;
|
|
|
|
if (attr == NULL) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
rc = posix_spawnattr_init(attr);
|
|
if (rc != 0) {
|
|
errno = rc;
|
|
return -1;
|
|
}
|
|
|
|
sigemptyset(&defaults);
|
|
sigaddset(&defaults, SIGPIPE);
|
|
rc = posix_spawnattr_setsigdefault(attr, &defaults);
|
|
if (rc == 0) rc = posix_spawnattr_setflags(attr, POSIX_SPAWN_SETSIGDEF);
|
|
if (rc != 0) {
|
|
posix_spawnattr_destroy(attr);
|
|
errno = rc;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*! \fn static int reap_child_bounded(pid_t pid, int *pstat, int attempts)
|
|
* \return 0 when reaped, 1 when still running after attempts, -1 on error
|
|
*/
|
|
int spine_reap_child_bounded(pid_t pid, int *pstat, int attempts) {
|
|
int attempt;
|
|
int eintr_budget;
|
|
pid_t waited;
|
|
|
|
if (pstat == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
for (attempt = 0; attempt < attempts; attempt++) {
|
|
/* Bounded so a stream of caught signals cannot spin this call
|
|
* forever instead of returning within its attempt budget. */
|
|
eintr_budget = 1000;
|
|
do {
|
|
waited = waitpid(pid, pstat, WNOHANG);
|
|
} while (waited < 0 && errno == EINTR && --eintr_budget > 0);
|
|
|
|
if (waited == pid) {
|
|
return 0;
|
|
}
|
|
|
|
if (waited < 0 && errno == ECHILD) {
|
|
/* someone else reaped it, so no status is available */
|
|
*pstat = 0;
|
|
return 0;
|
|
}
|
|
|
|
if (waited < 0) {
|
|
/* leave errno as waitpid set it; nft_pclose() reports it */
|
|
return -1;
|
|
}
|
|
|
|
/* The delay is load-bearing: without it the attempts are spent in
|
|
nanoseconds and the caller's kill lands before the child can exit.
|
|
|
|
Starting at the full 50ms charged that to every script that exits a
|
|
moment after closing stdout, which is the common case for anything
|
|
that flushes or tears down an interpreter. Spin briefly first, then
|
|
settle, so a caller that opts into this bounded wait does not pay
|
|
the full interval on every reap.
|
|
|
|
usleep() is skipped rather than replaced under SOLAR_THREAD, same as
|
|
everywhere else in the tree - a flat sleep(1) here would blow the
|
|
~20ms budget nft_pclose() relies on (attempts * 1s), so an
|
|
un-delayed retry is the correct bounded behavior on that platform. */
|
|
#ifndef SOLAR_THREAD
|
|
if (attempt < NFT_PCLOSE_SPIN_ATTEMPTS) {
|
|
usleep(NFT_PCLOSE_SPIN_USEC);
|
|
} else {
|
|
usleep(NFT_PCLOSE_REAP_USEC);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*! ------------------------------------------------------------------------------
|
|
*
|
|
* nft_popen
|
|
*
|
|
* The nft_popen() function forks a command in a child process, and returns
|
|
* a pipe that is connected to the child's standard input and output. It is
|
|
* like the standard popen() call, except that it does not dfopen() the pipe
|
|
* file descriptor in order to return a stdio FILE *. This is useful if you
|
|
* wish to use select()- or poll()-driven IO.
|
|
*
|
|
* The mode argument is defined as in standard popen().
|
|
*
|
|
* On success, returns a file descriptor, or -1 on error.
|
|
* On failure, returns -1, with errno set to one of:
|
|
* EINVAL The mode argument is incorrect.
|
|
* EMFILE pipe() failed.
|
|
* ENFILE pipe() failed.
|
|
* ENOMEM malloc() failed.
|
|
* EAGAIN fork() failed.
|
|
*
|
|
*------------------------------------------------------------------------------
|
|
*/
|
|
/* 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;
|
|
int pdes[2];
|
|
int inherit_fd = -1;
|
|
int fd, twoway;
|
|
pid_t pid;
|
|
char *argv[4];
|
|
char *command_copy;
|
|
char shell_cmd[] = "sh";
|
|
char shell_flag[] = "-c";
|
|
int cancel_state;
|
|
extern char **environ;
|
|
int retry_count = 0;
|
|
/* nft_popen() reports why process creation failed through errno, as
|
|
* popen() does; cleanup below must not overwrite it. */
|
|
int failure_errno = 0;
|
|
posix_spawnattr_t attr;
|
|
int attr_valid = FALSE;
|
|
int attr_err;
|
|
|
|
if (command == NULL || type == NULL) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
/* On platforms where pipe() is bidirectional,
|
|
* "r+" gives two-way communication. Only the documented modes are
|
|
* accepted; any other "+" mode is a caller error, not a duplex request.
|
|
*/
|
|
if (strcmp(type, "r+") == 0) {
|
|
twoway = 1;
|
|
}else {
|
|
twoway = 0;
|
|
if ((*type != 'r' && *type != 'w') || type[1]) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* An ordinary pipe is one-way on Linux and macOS, so the duplex mode
|
|
* needs a socket pair to be readable and writable at the parent end. */
|
|
if (twoway) {
|
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, pdes) < 0)
|
|
return -1;
|
|
|
|
if (spine_set_cloexec(pdes[0]) != 0 || spine_set_cloexec(pdes[1]) != 0) {
|
|
(void)close(pdes[0]);
|
|
(void)close(pdes[1]);
|
|
return -1;
|
|
}
|
|
} else if (!spine_open_pipe_cloexec(pdes)) {
|
|
return -1;
|
|
}
|
|
|
|
/* Disable thread cancellation from this point forward. */
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancel_state);
|
|
|
|
if ((cur = malloc(sizeof(struct pid))) == NULL) {
|
|
(void)close(pdes[0]);
|
|
(void)close(pdes[1]);
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
return -1;
|
|
}
|
|
|
|
if ((command_copy = strdup(command)) == NULL) {
|
|
(void)close(pdes[0]);
|
|
(void)close(pdes[1]);
|
|
free(cur);
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
return -1;
|
|
}
|
|
|
|
argv[0] = shell_cmd;
|
|
argv[1] = shell_flag;
|
|
argv[2] = command_copy;
|
|
argv[3] = NULL;
|
|
|
|
/* Lock the list mutex prior to forking, to ensure that
|
|
* the child process sees PidList in a consistent list state.
|
|
*/
|
|
pthread_mutex_lock(&ListMutex);
|
|
|
|
/* Drain anything a previous nft_pclose() gave up on. Doing it here means the
|
|
list empties on the next script poll rather than waiting for another
|
|
failure to trigger a sweep. */
|
|
nft_sweep_abandoned();
|
|
|
|
/* Build file actions for posix_spawn to replace vfork+execve. */
|
|
posix_spawn_file_actions_t fa;
|
|
if (posix_spawn_file_actions_init(&fa) != 0) {
|
|
SPINE_LOG(("ERROR: SCRIPT: posix_spawn_file_actions_init failed"));
|
|
(void)close(pdes[0]);
|
|
(void)close(pdes[1]);
|
|
pthread_mutex_unlock(&ListMutex);
|
|
free(command_copy);
|
|
free(cur);
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
return -1;
|
|
}
|
|
|
|
/* Each script leads its own process group so a timeout can kill the
|
|
* group. Killing only the shell left anything it had started running,
|
|
* still holding the pipe and counting against the process limit. */
|
|
attr_err = posix_spawnattr_init(&attr);
|
|
if (attr_err == 0) {
|
|
attr_valid = TRUE;
|
|
attr_err = posix_spawnattr_setpgroup(&attr, 0);
|
|
}
|
|
if (attr_err == 0) attr_err = posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETPGROUP);
|
|
if (attr_err != 0) {
|
|
failure_errno = attr_err;
|
|
SPINE_LOG(("ERROR: SCRIPT: posix_spawnattr setup failed: %s", strerror(attr_err)));
|
|
goto spawn_failed;
|
|
}
|
|
|
|
/* The pipe ends are close-on-exec, which is the point: another thread
|
|
* spawning in this window must not inherit them. The child needs its own
|
|
* end, and dup2 clears the flag on its target, so the usual paths are
|
|
* fine.
|
|
*
|
|
* When the end already sits on the descriptor it is destined for, there is
|
|
* no dup2 to clear anything and the child would exec with that descriptor
|
|
* closed. That happens whenever stdin or stdout was closed before this
|
|
* call, which for a daemon is not exotic, and the failure is silent: every
|
|
* script data source records U. Duplicate the end to a fresh descriptor,
|
|
* explicitly mark that duplicate close-on-exec so concurrent children
|
|
* cannot inherit it, and let this child dup2 from it. dup2 clears the flag
|
|
* on its target. */
|
|
if (*type == 'r') {
|
|
posix_spawn_file_actions_addclose(&fa, pdes[0]);
|
|
if (pdes[1] != STDOUT_FILENO) {
|
|
posix_spawn_file_actions_adddup2(&fa, pdes[1], STDOUT_FILENO);
|
|
posix_spawn_file_actions_addclose(&fa, pdes[1]);
|
|
if (twoway)
|
|
posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDIN_FILENO);
|
|
} else {
|
|
inherit_fd = spine_dup_cloexec(pdes[1]);
|
|
|
|
if (inherit_fd < 0) {
|
|
failure_errno = errno;
|
|
SPINE_LOG(("ERROR: Unable to duplicate the pipe for the child: %s", strerror(errno)));
|
|
goto spawn_failed;
|
|
}
|
|
|
|
posix_spawn_file_actions_adddup2(&fa, inherit_fd, STDOUT_FILENO);
|
|
posix_spawn_file_actions_addclose(&fa, inherit_fd);
|
|
|
|
if (twoway)
|
|
posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDIN_FILENO);
|
|
}
|
|
} else {
|
|
if (pdes[0] != STDIN_FILENO) {
|
|
posix_spawn_file_actions_adddup2(&fa, pdes[0], STDIN_FILENO);
|
|
posix_spawn_file_actions_addclose(&fa, pdes[0]);
|
|
} else {
|
|
inherit_fd = spine_dup_cloexec(pdes[0]);
|
|
|
|
if (inherit_fd < 0) {
|
|
failure_errno = errno;
|
|
SPINE_LOG(("ERROR: Unable to duplicate the pipe for the child: %s", strerror(errno)));
|
|
goto spawn_failed;
|
|
}
|
|
|
|
posix_spawn_file_actions_adddup2(&fa, inherit_fd, STDIN_FILENO);
|
|
posix_spawn_file_actions_addclose(&fa, inherit_fd);
|
|
}
|
|
posix_spawn_file_actions_addclose(&fa, pdes[1]);
|
|
}
|
|
|
|
/* Close all other pipes in the child (Posix.2 requirement). */
|
|
for (p = PidList; p; p = p->next) {
|
|
/* File actions run in order. Do not close a standard descriptor after
|
|
* this spawn has redirected a fresh pipe onto it merely because an
|
|
* older parent-side pipe happens to use the same descriptor number. */
|
|
if ((*type == 'r' && p->fd == STDOUT_FILENO) ||
|
|
(*type == 'r' && twoway && p->fd == STDIN_FILENO) ||
|
|
(*type == 'w' && p->fd == STDIN_FILENO)) {
|
|
continue;
|
|
}
|
|
posix_spawn_file_actions_addclose(&fa, p->fd);
|
|
}
|
|
|
|
/* Spawn the child process with retry on EAGAIN/ENOMEM. */
|
|
#if defined(__CYGWIN__)
|
|
const char *spawn_shell = (set.cygwinshloc == 0) ? "sh.exe" : "/bin/sh";
|
|
#else
|
|
const char *spawn_shell = "/bin/sh";
|
|
#endif
|
|
|
|
int spawn_err;
|
|
retry:
|
|
spawn_err = posix_spawn(&pid, spawn_shell, &fa, &attr, argv, environ);
|
|
|
|
if (spawn_err != 0) {
|
|
if ((spawn_err == EAGAIN || spawn_err == ENOMEM) && retry_count < 3) {
|
|
retry_count++;
|
|
usleep(50000);
|
|
goto retry;
|
|
}
|
|
|
|
failure_errno = spawn_err;
|
|
SPINE_LOG(("ERROR: SCRIPT: posix_spawn failed: %s", strerror(spawn_err)));
|
|
|
|
spawn_failed:
|
|
/* One teardown for every failure after the file actions exist and the
|
|
* list mutex is held. ListMutex is process-global, so a path that
|
|
* returns still holding it wedges every later nft_popen() and
|
|
* nft_pclose() in every poller thread and the daemon stops collecting
|
|
* script data until it is restarted. */
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
if (attr_valid) posix_spawnattr_destroy(&attr);
|
|
|
|
if (inherit_fd != -1) {
|
|
(void)close(inherit_fd);
|
|
inherit_fd = -1;
|
|
}
|
|
|
|
(void)close(pdes[0]);
|
|
(void)close(pdes[1]);
|
|
pthread_mutex_unlock(&ListMutex);
|
|
free(command_copy);
|
|
free(cur);
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
if (failure_errno != 0) {
|
|
errno = failure_errno;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
posix_spawnattr_destroy(&attr);
|
|
|
|
/* The child holds its own duplicate. Keeping this one would hold the pipe's
|
|
* write end open, so the reader never sees EOF and exec_poll() blocks to
|
|
* script_timeout on a script that already answered. That is the failure the
|
|
* close-on-exec work exists to prevent. */
|
|
if (inherit_fd != -1) {
|
|
(void)close(inherit_fd);
|
|
inherit_fd = -1;
|
|
}
|
|
|
|
/* Parent. */
|
|
if (*type == 'r') {
|
|
fd = pdes[0];
|
|
(void)close(pdes[1]);
|
|
}else {
|
|
fd = pdes[1];
|
|
(void)close(pdes[0]);
|
|
}
|
|
|
|
/* Link into list of file descriptors. */
|
|
cur->fd = fd;
|
|
cur->pid = pid;
|
|
cur->next = PidList;
|
|
PidList = cur;
|
|
|
|
/* Unlock the mutex, and restore caller's cancellation state. */
|
|
pthread_mutex_unlock(&ListMutex);
|
|
free(command_copy);
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
|
|
return fd;
|
|
}
|
|
|
|
/*! ------------------------------------------------------------------------------
|
|
*
|
|
* nft_pchild
|
|
*
|
|
* Get the pid of the child process for an fd created by ntf_popen().
|
|
*
|
|
* On success, the pid of the child process is returned.
|
|
* On failure, nft_pchild() returns -1, with errno set to:
|
|
*
|
|
* EBADF The fd is not an active nft_popen() file descriptor.
|
|
*
|
|
*------------------------------------------------------------------------------
|
|
*/
|
|
int nft_pchild(int fd) {
|
|
struct pid *cur;
|
|
pid_t pid = 0;
|
|
|
|
/* Find the appropriate file descriptor. */
|
|
pthread_mutex_lock(&ListMutex);
|
|
for (cur = PidList; cur; cur = cur->next)
|
|
if (cur->fd == fd) {
|
|
pid = cur->pid;
|
|
break;
|
|
}
|
|
|
|
pthread_mutex_unlock(&ListMutex);
|
|
|
|
if (cur == NULL) {
|
|
errno = EBADF;
|
|
return -1;
|
|
}
|
|
|
|
return pid;
|
|
}
|
|
|
|
/*! ------------------------------------------------------------------------------
|
|
*
|
|
* nft_pclose
|
|
*
|
|
* Close the pipe and check the child's status with a brief (~20ms),
|
|
* non-escalating bounded waitpid(). A child still running past that point,
|
|
* or one whose waitpid() call itself failed, is killed and handed to the
|
|
* abandoned-pid sweep rather than waited for here, so this call never
|
|
* blocks the caller on a lingering script.
|
|
*
|
|
* On success, the exit status of the child process is returned.
|
|
* On failure, nft_pclose() returns -1, with errno set to:
|
|
*
|
|
* EBADF The fd is not an active popen() file descriptor.
|
|
* ETIMEDOUT The child had not exited by the end of the bounded check; it
|
|
* has been killed and parked for the abandoned-pid sweep to reap.
|
|
* (other) The waitpid() call itself failed for a reason other than
|
|
* ECHILD, which is treated as a successful reap; may be EINTR if
|
|
* the bounded EINTR retry budget was exhausted.
|
|
*
|
|
* This call is cancellable.
|
|
*
|
|
*------------------------------------------------------------------------------
|
|
*/
|
|
int
|
|
nft_pclose(int fd)
|
|
{
|
|
struct pid *cur;
|
|
int pstat;
|
|
int cancel_state;
|
|
pid_t pid;
|
|
|
|
/* Cancellation must remain disabled until the detached entry is protected
|
|
* by the cleanup handler. Detaching transfers exclusive ownership here.
|
|
*/
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancel_state);
|
|
cur = pid_list_close_and_take(fd);
|
|
|
|
if (cur == NULL) {
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
errno = EBADF;
|
|
return -1;
|
|
}
|
|
|
|
/* Install the cleanup handler before restoring cancellation. */
|
|
pthread_cleanup_push(close_cleanup, cur);
|
|
|
|
pthread_setcancelstate(cancel_state, NULL);
|
|
|
|
/* The script already had its one chance to write and its pipe is now
|
|
* closed. Give it a brief (~20ms), non-escalating allowance to catch the
|
|
* common case where it has already exited or is about to on seeing EOF -
|
|
* without it, ordinary fork/exec/exit scheduling latency would flag a
|
|
* script that is not actually misbehaving. Anything still running past
|
|
* that is killed and handed to the abandoned-pid sweep instead of this
|
|
* thread waiting for it. */
|
|
switch (spine_reap_child_bounded(cur->pid, &pstat, NFT_PCLOSE_SPIN_ATTEMPTS)) {
|
|
case 0:
|
|
pid = cur->pid;
|
|
break;
|
|
case 1:
|
|
/* The negative pid reaches the script's whole process group. */
|
|
(void)kill(-cur->pid, SIGKILL);
|
|
nft_abandon_child(cur->pid, "did not exit before pipe close");
|
|
errno = ETIMEDOUT;
|
|
pid = -1;
|
|
break;
|
|
default:
|
|
/* waitpid() itself failed, so whether the child exited is unknown;
|
|
* kill it before parking so a still-running child is not left
|
|
* outside the sweep's reach. Preserve waitpid()'s errno. */
|
|
{
|
|
int saved_errno = errno;
|
|
(void)kill(-cur->pid, SIGKILL);
|
|
nft_abandon_child(cur->pid, "waitpid failed");
|
|
errno = saved_errno;
|
|
}
|
|
pid = -1;
|
|
break;
|
|
}
|
|
|
|
pthread_cleanup_pop(1); /* Execute the cleanup handler. */
|
|
|
|
return (pid == -1 ? -1 : pstat);
|
|
}
|
|
|
|
/*! ------------------------------------------------------------------------------
|
|
* nft_sweep_abandoned - reap any child a previous nft_pclose() gave up on.
|
|
*
|
|
* Called with ListMutex held. WNOHANG only: this runs on a poller thread and
|
|
* must never block on a child that is still stuck.
|
|
*------------------------------------------------------------------------------
|
|
*/
|
|
static void
|
|
nft_sweep_abandoned(void)
|
|
{
|
|
int i = 0;
|
|
int status;
|
|
pid_t waited;
|
|
int eintr_budget;
|
|
|
|
while (i < AbandonedCount) {
|
|
eintr_budget = 1000;
|
|
do {
|
|
waited = waitpid(AbandonedPids[i], &status, WNOHANG);
|
|
} while (waited < 0 && errno == EINTR && --eintr_budget > 0);
|
|
|
|
if (waited == AbandonedPids[i] || (waited < 0 && errno == ECHILD)) {
|
|
SPINE_LOG_DEBUG(("DEBUG: Reaped abandoned script child pid %ld", (long) AbandonedPids[i]));
|
|
AbandonedPids[i] = AbandonedPids[AbandonedCount - 1];
|
|
AbandonedCount--;
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*! ------------------------------------------------------------------------------
|
|
* nft_abandoned_pending - sweep, then report how many pids are still parked.
|
|
*
|
|
* Takes ListMutex itself, so a caller that already holds it uses
|
|
* nft_sweep_abandoned() directly.
|
|
*------------------------------------------------------------------------------
|
|
*/
|
|
int
|
|
nft_abandoned_pending(void)
|
|
{
|
|
int remaining;
|
|
int oldstate;
|
|
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
|
|
pthread_mutex_lock(&ListMutex);
|
|
|
|
nft_sweep_abandoned();
|
|
remaining = AbandonedCount;
|
|
|
|
pthread_mutex_unlock(&ListMutex);
|
|
pthread_setcancelstate(oldstate, NULL);
|
|
|
|
return remaining;
|
|
}
|
|
|
|
/*! ------------------------------------------------------------------------------
|
|
* nft_abandon_child - record a child that outlived its kill budget.
|
|
*
|
|
* The pid and the reason are logged either way. A silent drop leaves PID
|
|
* exhaustion with nothing in the log pointing at its cause.
|
|
*------------------------------------------------------------------------------
|
|
*/
|
|
void
|
|
nft_abandon_child(pid_t pid, const char *reason)
|
|
{
|
|
int parked;
|
|
int oldstate;
|
|
|
|
/* nft_pclose() calls this inside its pthread_cleanup_push() region, and
|
|
close_cleanup() takes ListMutex. A cancel delivered while this held the
|
|
lock would run the handler straight into it, so hold it uncancellable. */
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
|
|
|
|
pthread_mutex_lock(&ListMutex);
|
|
|
|
nft_sweep_abandoned();
|
|
|
|
parked = (AbandonedCount < NFT_ABANDONED_MAX);
|
|
|
|
if (parked) {
|
|
AbandonedPids[AbandonedCount++] = pid;
|
|
}
|
|
|
|
pthread_mutex_unlock(&ListMutex);
|
|
|
|
pthread_setcancelstate(oldstate, NULL);
|
|
|
|
if (parked) {
|
|
SPINE_LOG(("WARNING: SCRIPT: pid %ld survived SIGKILL (%s); parked for reaping", (long) pid, reason));
|
|
} else {
|
|
SPINE_LOG(("ERROR: SCRIPT: pid %ld survived SIGKILL (%s) and the abandoned list is full; it will remain a zombie", (long) pid, reason));
|
|
}
|
|
}
|
|
|
|
/*! ------------------------------------------------------------------------------
|
|
* close_cleanup - close the pipe and free the pidlist entry.
|
|
*------------------------------------------------------------------------------
|
|
*/
|
|
static void
|
|
close_cleanup(void * arg)
|
|
{
|
|
struct pid * cur = arg;
|
|
|
|
SPINE_FREE(cur);
|
|
}
|