Files
spine/nft_popen.h
TheWitness ca662c1be7 fix: cap Solaris reap budget, bound EINTR retries, kill before parking on waitpid error (#635)
## What
Follow-up to #633 (already merged), from the code review findings on the
equivalent 1.2.x change (#634):

- **SOLAR_THREAD sleep budget**: `spine_reap_child_bounded()` has no
`usleep()`-paced spin phase on `SOLAR_THREAD` builds - it sleeps a flat
1s per attempt. #633 raised the shared attempt budget
(`NFT_PCLOSE_SPIN_ATTEMPTS`) to 100 for `nft_pclose()`'s brief
allowance, which made it sleep up to **~100 seconds** on that platform
instead of the intended ~20ms, reintroducing the exact poller stall #633
was fixing. `NFT_PCLOSE_SPIN_ATTEMPTS` is now platform-specific (100
elsewhere, 2 under `SOLAR_THREAD`).
- **Unbounded EINTR retries**: bounded the `EINTR` retry loop in
`spine_reap_child_bounded()` and in `nft_sweep_abandoned()` (which runs
with `ListMutex` held), so a stream of caught signals can no longer spin
either indefinitely.
- **Kill before park on waitpid() error**: `nft_pclose()` now SIGKILLs
the child before parking it when `waitpid()` itself errors (previously
only done when the child was confirmed still running), so a
still-running child isn't left outside the abandoned-pid sweep's reach.
Preserves `waitpid()`'s errno across the call.
- **Docs**: `nft_popen.h`'s `nft_pclose()` comment still described the
old blocking-wait behavior and didn't mention `ETIMEDOUT` - #633 updated
`nft_popen.c`'s comment but missed the header. Both now document the
bounded-check/parked-child contract.

## Why
These were flagged by review of #634 (the same fix ported to 1.2.x,
where the affected code showed up as new rather than a diff against
pre-existing lines). Fixing here too since develop has the identical
code.

## What was tested
- `./bootstrap && ./configure --enable-warnings && make` - clean build,
no new warnings.
- `make check` - all 11 suites pass.
- `cppcheck --enable=all --std=c11` on `nft_popen.c` - no new errors.

Closes #632.

---------

Signed-off-by: TheWitness <thewitness@cacti.net>
2026-09-28 15:21:22 -04:00

175 lines
6.9 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/ |
+-------------------------------------------------------------------------+
*/
#ifndef SPINE_NFT_POPEN_H
#define SPINE_NFT_POPEN_H
/******************************************************************************
ex: set tabstop=4 shiftwidth=4 autoindent:
*
* (C) Copyright 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.
*
* 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.h
*
* PURPOSE
*
* Thread-safe substitute for popen() that doesn't use stdio streams.
*
******************************************************************************
*/
#include <spawn.h>
/*!
* 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 returns the file descriptor,
* instead of a stdio stream created by fdopen(). The file descriptor can be
* used with select() or poll(), or the caller can use fdopen() if a stdio
* FILE* is preferable.
*
* 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.
*/
extern int nft_popen(const char * command, const char * mode);
/*!
* 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.
*/
extern int nft_pchild(int fd);
/*!
* nft_pclose
*
* Close the pipe and check the child's status with a brief, bounded,
* non-blocking waitpid(). A child still running past that point, or one
* whose waitpid() call itself failed, is killed and parked for later,
* asynchronous reaping rather than waited for here.
*
* 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 later reaping.
* (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.
*/
extern int nft_pclose(int fd);
/*!
* spine_set_cloexec
*
* Mark a descriptor close-on-exec.
*
* Returns 0 on success, -1 on failure with errno set by fcntl().
*/
extern int spine_set_cloexec(int fd);
/* Duplicate a descriptor and mark the duplicate close-on-exec. On failure,
* no descriptor is returned and errno describes dup() or fcntl(). */
extern int spine_dup_cloexec(int fd);
/*!
* spine_open_pipe_cloexec
*
* Open a pipe whose descriptors are not inherited across exec. Spine spawns
* children from several threads, so a descriptor left inheritable is held by
* an unrelated child and the reader never sees EOF.
*
* Returns TRUE on success. On failure the descriptors are closed and FALSE is
* returned, so the caller owns nothing.
*/
extern int spine_open_pipe_cloexec(int pdes[2]);
/* Restore SIGPIPE's default disposition in posix_spawned children while the
* Spine parent handles broken pipes itself. */
extern int spine_spawnattr_sigpipe_default(posix_spawnattr_t *attr);
/*!
* spine_reap_child_bounded
*
* Reap a child with WNOHANG, sleeping between attempts, so a wedged script
* cannot pin a poller thread indefinitely.
*
* Returns 0 when the child was reaped - including ECHILD, where another
* waiter already collected it and pstat is set to 0 - and 1 when it is
* still running after attempts. Returns -1 on a waitpid() error, leaving
* errno as waitpid() set it; EINTR is retried within a bounded per-attempt
* budget, so it too surfaces as -1 with errno == EINTR once that budget is
* exhausted.
*/
extern int spine_reap_child_bounded(pid_t pid, int *pstat, int attempts);
/*!
* The cap on parked pids. Past it a child is logged and dropped, because an
* unbounded list would trade a pid leak for a memory leak.
*/
#define NFT_ABANDONED_MAX 64
/*!
* nft_abandon_child
*
* Record a child that outlived nft_pclose()'s kill budget. Nothing else in
* spine reaps, so a dropped child would stay a zombie for the daemon's
* lifetime; parked pids are swept on the next script poll. The pid and the
* reason are logged either way.
*/
extern void nft_abandon_child(pid_t pid, const char *reason);
/*!
* nft_abandoned_pending
*
* Sweep the parked pids with WNOHANG and return how many are still running.
* Zero means nothing is leaking.
*/
extern int nft_abandoned_pending(void);
#endif /* SPINE_NFT_POPEN_H */