1
0
mirror of https://github.com/samba-team/samba.git synced 2025-01-27 14:04:05 +03:00
Ralph Boehme f5c3904f35 lib/util: fix a Coverity finding in tfork
If dup2() fails, fd is -1 and is later used in sys_write().

Signed-off-by: Ralph Boehme <slow@samba.org>
Reviewed-by: Jeremy Allison <jra@samba.org>
2017-04-25 19:14:11 +02:00

340 lines
7.0 KiB
C

/*
fork on steroids to avoid SIGCHLD and waitpid
Copyright (C) Stefan Metzmacher 2010
Copyright (C) Ralph Boehme 2017
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 3 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.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "replace.h"
#include "system/wait.h"
#include "system/filesys.h"
#include "lib/util/samba_util.h"
#include "lib/util/sys_rw.h"
#include "lib/util/tfork.h"
#include "lib/util/debug.h"
struct tfork_state {
void (*old_sig_chld)(int);
int status_pipe[2];
pid_t *parent;
pid_t level0_pid;
int level0_status;
pid_t level1_pid;
int level1_errno;
pid_t level2_pid;
int level2_errno;
pid_t level3_pid;
};
/*
* TODO: We should make this global thread local
*/
static struct tfork_state *tfork_global;
static void tfork_sig_chld(int signum)
{
if (tfork_global->level1_pid > 0) {
int ret = waitpid(tfork_global->level1_pid,
&tfork_global->level0_status,
WNOHANG);
if (ret == tfork_global->level1_pid) {
tfork_global->level1_pid = -1;
return;
}
}
/*
* Not our child, forward to old handler
*/
if (tfork_global->old_sig_chld == SIG_IGN) {
return;
}
if (tfork_global->old_sig_chld == SIG_DFL) {
return;
}
tfork_global->old_sig_chld(signum);
}
static pid_t level2_fork_and_wait(int child_ready_fd)
{
int status;
ssize_t written;
pid_t pid;
int fd;
bool wait;
/*
* Child level 2.
*
* Do a final fork and if the tfork() caller passed a status_fd, wait
* for child3 and return its exit status via status_fd.
*/
pid = fork();
if (pid == 0) {
/*
* Child level 3, this one finally returns from tfork() as child
* with pid 0.
*
* Cleanup all ressources we allocated before returning.
*/
close(child_ready_fd);
close(tfork_global->status_pipe[1]);
if (tfork_global->parent != NULL) {
/*
* we're in the child and return the level0 parent pid
*/
*tfork_global->parent = tfork_global->level0_pid;
}
anonymous_shared_free(tfork_global);
tfork_global = NULL;
return 0;
}
tfork_global->level3_pid = pid;
if (tfork_global->level3_pid == -1) {
tfork_global->level2_errno = errno;
_exit(0);
}
sys_write(child_ready_fd, &(char){0}, 1);
if (tfork_global->status_pipe[1] == -1) {
_exit(0);
}
wait = true;
/*
* We're going to stay around until child3 exits, so lets close all fds
* other then the pipe fd we may have inherited from the caller.
*/
while (true) {
fd = dup2(tfork_global->status_pipe[1], 0);
if (fd == -1) {
if (errno == EINTR) {
continue;
}
status = errno;
kill(tfork_global->level3_pid, SIGKILL);
written = sys_write(tfork_global->status_pipe[1],
&status, sizeof(status));
if (written != sizeof(status)) {
abort();
}
_exit(0);
}
break;
}
closefrom(1);
while (wait) {
int ret = waitpid(tfork_global->level3_pid, &status, 0);
if (ret == -1) {
if (errno == EINTR) {
continue;
}
status = errno;
}
break;
}
written = sys_write(fd, &status, sizeof(status));
if (written != sizeof(status)) {
abort();
}
_exit(0);
}
pid_t tfork(int *status_fd, pid_t *parent)
{
int ret;
pid_t pid;
pid_t child;
tfork_global = (struct tfork_state *)
anonymous_shared_allocate(sizeof(struct tfork_state));
if (tfork_global == NULL) {
return -1;
}
tfork_global->parent = parent;
tfork_global->status_pipe[0] = -1;
tfork_global->status_pipe[1] = -1;
tfork_global->level0_pid = getpid();
tfork_global->level0_status = -1;
tfork_global->level1_pid = -1;
tfork_global->level1_errno = ECANCELED;
tfork_global->level2_pid = -1;
tfork_global->level2_errno = ECANCELED;
tfork_global->level3_pid = -1;
if (status_fd != NULL) {
ret = pipe(&tfork_global->status_pipe[0]);
if (ret != 0) {
int saved_errno = errno;
anonymous_shared_free(tfork_global);
tfork_global = NULL;
errno = saved_errno;
return -1;
}
*status_fd = tfork_global->status_pipe[0];
}
/*
* We need to set our own signal handler to prevent any existing signal
* handler from reaping our child.
*/
tfork_global->old_sig_chld = CatchSignal(SIGCHLD, tfork_sig_chld);
pid = fork();
if (pid == 0) {
int level2_pipe[2];
char c;
ssize_t nread;
/*
* Child level 1.
*
* Restore SIGCHLD handler
*/
CatchSignal(SIGCHLD, SIG_DFL);
/*
* Close read end of the signal pipe, we don't need it anymore
* and don't want to leak it into childs.
*/
if (tfork_global->status_pipe[0] != -1) {
close(tfork_global->status_pipe[0]);
tfork_global->status_pipe[0] = -1;
}
/*
* Create a pipe for waiting for the child level 2 to finish
* forking.
*/
ret = pipe(&level2_pipe[0]);
if (ret != 0) {
tfork_global->level1_errno = errno;
_exit(0);
}
pid = fork();
if (pid == 0) {
/*
* Child level 2.
*/
close(level2_pipe[0]);
return level2_fork_and_wait(level2_pipe[1]);
}
tfork_global->level2_pid = pid;
if (tfork_global->level2_pid == -1) {
tfork_global->level1_errno = errno;
_exit(0);
}
close(level2_pipe[1]);
level2_pipe[1] = -1;
nread = sys_read(level2_pipe[0], &c, 1);
if (nread != 1) {
abort();
}
_exit(0);
}
tfork_global->level1_pid = pid;
if (tfork_global->level1_pid == -1) {
int saved_errno = errno;
anonymous_shared_free(tfork_global);
tfork_global = NULL;
errno = saved_errno;
return -1;
}
/*
* By using the helper variable pid we avoid a TOCTOU with the signal
* handler that will set tfork_global->level1_pid to -1 (which would
* cause waitpid() to block waiting for another exitted child).
*
* We can't avoid the race waiting for pid twice (in the signal handler
* and then again here in the while loop), but we must avoid waiting for
* -1 and this does the trick.
*/
pid = tfork_global->level1_pid;
while (tfork_global->level1_pid != -1) {
ret = waitpid(pid, &tfork_global->level0_status, 0);
if (ret == -1 && errno == EINTR) {
continue;
}
break;
}
CatchSignal(SIGCHLD, tfork_global->old_sig_chld);
if (tfork_global->level0_status != 0) {
anonymous_shared_free(tfork_global);
tfork_global = NULL;
errno = ECHILD;
return -1;
}
if (tfork_global->level2_pid == -1) {
int saved_errno = tfork_global->level1_errno;
anonymous_shared_free(tfork_global);
tfork_global = NULL;
errno = saved_errno;
return -1;
}
if (tfork_global->level3_pid == -1) {
int saved_errno = tfork_global->level2_errno;
anonymous_shared_free(tfork_global);
tfork_global = NULL;
errno = saved_errno;
return -1;
}
child = tfork_global->level3_pid;
anonymous_shared_free(tfork_global);
tfork_global = NULL;
return child;
}