mirror of https://gerrit.osmocom.org/libosmocore
Introduce helper functions for safe fork+exec of processes
In some situations, we want to execute an external shell command in a non-blocking way. Similar to 'system', but without waiting for the child to complete. We also want to close all file descriptors ahead of the exec() and filter + modify the environment. Change-Id: Ib24ac8a083db32e55402ce496a5eabd8749cc888 Related: OS#4332
This commit is contained in:
parent
22c7ec3b60
commit
c6a8697800
|
@ -282,7 +282,7 @@ AC_ARG_ENABLE(embedded,
|
|||
)],
|
||||
[embedded=$enableval], [embedded="no"])
|
||||
|
||||
AM_CONDITIONAL(ENABLE_STATS_TEST, true)
|
||||
AM_CONDITIONAL(EMBEDDED, false)
|
||||
AM_CONDITIONAL(ENABLE_SERCOM_STUB, false)
|
||||
|
||||
if test x"$embedded" = x"yes"
|
||||
|
@ -301,7 +301,7 @@ then
|
|||
AM_CONDITIONAL(ENABLE_PCSC, false)
|
||||
AM_CONDITIONAL(ENABLE_PSEUDOTALLOC, true)
|
||||
AM_CONDITIONAL(ENABLE_SERCOM_STUB, true)
|
||||
AM_CONDITIONAL(ENABLE_STATS_TEST, false)
|
||||
AM_CONDITIONAL(EMBEDDED, true)
|
||||
AC_DEFINE([USE_GNUTLS], [0])
|
||||
AC_DEFINE([PANIC_INFLOOP],[1],[Use infinite loop on panic rather than fprintf/abort])
|
||||
fi
|
||||
|
|
|
@ -23,6 +23,7 @@ nobase_include_HEADERS = \
|
|||
osmocom/core/crcgen.h \
|
||||
osmocom/core/endian.h \
|
||||
osmocom/core/defs.h \
|
||||
osmocom/core/exec.h \
|
||||
osmocom/core/fsm.h \
|
||||
osmocom/core/gsmtap.h \
|
||||
osmocom/core/gsmtap_util.h \
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
/* (C) 2019 by Harald Welte <laforge@gnumonks.org>
|
||||
*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0+
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
*/
|
||||
extern const char *osmo_environment_whitelist[];
|
||||
|
||||
int osmo_environment_filter(char **out, size_t out_len, char **in, const char **whitelist);
|
||||
int osmo_environment_append(char **out, size_t out_len, char **in);
|
||||
int osmo_close_all_fds_above(int last_fd_to_keep);
|
||||
int osmo_system_nowait(const char *command, const char **env_whitelist, char **addl_env);
|
|
@ -27,6 +27,7 @@ libosmocore_la_SOURCES = context.c timer.c timer_gettimeofday.c timer_clockgetti
|
|||
tdef.c \
|
||||
sockaddr_str.c \
|
||||
use_count.c \
|
||||
exec.c \
|
||||
$(NULL)
|
||||
|
||||
if HAVE_SSSE3
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
/* (C) 2019 by Harald Welte <laforge@gnumonks.org>
|
||||
*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0+
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#ifndef EMBEDDED
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <osmocom/core/logging.h>
|
||||
#include <osmocom/core/utils.h>
|
||||
#include <osmocom/core/exec.h>
|
||||
|
||||
/*! suggested list of environment variables to pass (if they exist) to a sub-process/script */
|
||||
const char *osmo_environment_whitelist[] = {
|
||||
"USER", "LOGNAME", "HOME",
|
||||
"LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
|
||||
"PATH",
|
||||
"PWD",
|
||||
"SHELL",
|
||||
"TERM",
|
||||
"TMPDIR",
|
||||
"LD_LIBRARY_PATH",
|
||||
"LD_PRELOAD",
|
||||
"POSIXLY_CORRECT",
|
||||
"HOSTALIASES",
|
||||
"TZ", "TZDIR",
|
||||
"TERMCAP",
|
||||
"COLUMNS", "LINES",
|
||||
NULL
|
||||
};
|
||||
|
||||
static bool str_in_list(const char **list, const char *key)
|
||||
{
|
||||
const char **ent;
|
||||
|
||||
for (ent = list; *ent; ent++) {
|
||||
if (!strcmp(*ent, key))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*! filtered a process environment by whitelist; only copying pointers, no actual strings.
|
||||
*
|
||||
* This function is useful if you'd like to generate an environment to pass exec*e()
|
||||
* functions. It will create a new environment containing only those entries whose
|
||||
* keys (as per environment convention KEY=VALUE) are contained in the whitelist. The
|
||||
* function will not copy the actual strings, but just create a new pointer array, pointing
|
||||
* to the same memory as the input strings.
|
||||
*
|
||||
* Constraints: Keys up to a maximum length of 255 characters are supported.
|
||||
*
|
||||
* \oaram[out] out caller-allocated array of pointers for the generated output
|
||||
* \param[in] out_len size of out (number of pointers)
|
||||
* \param[in] in input environment (NULL-terminated list of pointers like **environ)
|
||||
* \param[in] whitelist whitelist of permitted keys in environment (like **environ)
|
||||
* \returns number of entries filled in 'out'; negtive on error */
|
||||
int osmo_environment_filter(char **out, size_t out_len, char **in, const char **whitelist)
|
||||
{
|
||||
char tmp[256];
|
||||
char **ent;
|
||||
size_t out_used = 0;
|
||||
|
||||
/* invalid calls */
|
||||
if (!out || out_len == 0 || !whitelist)
|
||||
return -EINVAL;
|
||||
|
||||
/* legal, but unusual: no input to filter should generate empty, terminated out */
|
||||
if (!in) {
|
||||
out[0] = NULL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* iterate over input entries */
|
||||
for (ent = in; *ent; ent++) {
|
||||
char *eq = strchr(*ent, '=');
|
||||
unsigned long eq_pos;
|
||||
if (!eq) {
|
||||
/* no '=' in string, skip it */
|
||||
continue;
|
||||
}
|
||||
eq_pos = eq - *ent;
|
||||
if (eq_pos >= ARRAY_SIZE(tmp))
|
||||
continue;
|
||||
strncpy(tmp, *ent, eq_pos);
|
||||
tmp[eq_pos] = '\0';
|
||||
if (str_in_list(whitelist, tmp)) {
|
||||
if (out_used == out_len-1)
|
||||
break;
|
||||
/* append to output */
|
||||
out[out_used++] = *ent;
|
||||
}
|
||||
}
|
||||
OSMO_ASSERT(out_used < out_len);
|
||||
out[out_used++] = NULL;
|
||||
return out_used;
|
||||
}
|
||||
|
||||
/*! append one environment to another; only copying pointers, not actual strings.
|
||||
*
|
||||
* This function is useful if you'd like to append soem entries to an environment
|
||||
* befoer passing it to exec*e() functions.
|
||||
*
|
||||
* It will append all entries from 'in' to the environment in 'out', as long as
|
||||
* 'out' has space (determined by 'out_len').
|
||||
*
|
||||
* Constraints: If the same key exists in 'out' and 'in', duplicate keys are
|
||||
* generated. It is a simple append, without any duplicate checks.
|
||||
*
|
||||
* \oaram[out] out caller-allocated array of pointers for the generated output
|
||||
* \param[in] out_len size of out (number of pointers)
|
||||
* \param[in] in input environment (NULL-terminated list of pointers like **environ)
|
||||
* \returns number of entries filled in 'out'; negative on error */
|
||||
int osmo_environment_append(char **out, size_t out_len, char **in)
|
||||
{
|
||||
size_t out_used = 0;
|
||||
|
||||
if (!out || out_len == 0)
|
||||
return -EINVAL;
|
||||
|
||||
/* seek to end of existing output */
|
||||
for (out_used = 0; out[out_used]; out_used++) {}
|
||||
|
||||
if (!in) {
|
||||
if (out_used == 0)
|
||||
out[out_used++] = NULL;
|
||||
return out_used;
|
||||
}
|
||||
|
||||
for (; *in && out_used < out_len-1; in++)
|
||||
out[out_used++] = *in;
|
||||
|
||||
OSMO_ASSERT(out_used < out_len);
|
||||
out[out_used++] = NULL;
|
||||
|
||||
return out_used;
|
||||
}
|
||||
|
||||
/* Iterate over files in /proc/self/fd and close all above lst_fd_to_keep */
|
||||
int osmo_close_all_fds_above(int last_fd_to_keep)
|
||||
{
|
||||
struct dirent *ent;
|
||||
DIR *dir;
|
||||
int rc;
|
||||
|
||||
dir = opendir("/proc/self/fd");
|
||||
if (!dir) {
|
||||
LOGP(DLGLOBAL, LOGL_ERROR, "Cannot open /proc/self/fd: %s\n", strerror(errno));
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
while ((ent = readdir(dir))) {
|
||||
int fd = atoi(ent->d_name);
|
||||
if (fd <= last_fd_to_keep)
|
||||
continue;
|
||||
if (fd == dirfd(dir))
|
||||
continue;
|
||||
rc = close(fd);
|
||||
if (rc)
|
||||
LOGP(DLGLOBAL, LOGL_ERROR, "Error closing fd=%d: %s\n", fd, strerror(errno));
|
||||
}
|
||||
closedir(dir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Seems like POSIX has no header file for this, and even glibc + __USE_GNU doesn't help */
|
||||
extern char **environ;
|
||||
|
||||
/*! call an external shell command without waiting for it.
|
||||
*
|
||||
* This mimics the behavior of system(3), with the following differences:
|
||||
* - it doesn't wait for completion of the child process
|
||||
* - it closes all non-stdio file descriptors by iterating /proc/self/fd
|
||||
* - it constructs a reduced environment where only whitelisted keys survive
|
||||
* - it (optionally) appends additional variables to the environment
|
||||
*
|
||||
* \param[in] command the shell command to be executed, see system(3)
|
||||
* \param[in] env_whitelist A white-list of keys for environment variables
|
||||
* \param[in] addl_env any additional environment variables to be appended
|
||||
* \returns PID of generated child process; negative on error
|
||||
*/
|
||||
int osmo_system_nowait(const char *command, const char **env_whitelist, char **addl_env)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = fork();
|
||||
if (rc == 0) {
|
||||
/* we are in the child */
|
||||
char *new_env[1024];
|
||||
|
||||
/* close all file descriptors above stdio */
|
||||
osmo_close_all_fds_above(2);
|
||||
|
||||
/* build the new environment */
|
||||
if (env_whitelist)
|
||||
osmo_environment_filter(new_env, ARRAY_SIZE(new_env), environ, env_whitelist);
|
||||
if (addl_env)
|
||||
osmo_environment_append(new_env, ARRAY_SIZE(new_env), addl_env);
|
||||
|
||||
/* if we want to behave like system(3), we must go via the shell */
|
||||
execle("/bin/sh", "sh", "-c", command, (char *) NULL, new_env);
|
||||
/* only reached in case of error */
|
||||
LOGP(DLGLOBAL, LOGL_ERROR, "Error executing command '%s' after fork: %s\n",
|
||||
command, strerror(errno));
|
||||
return -EIO;
|
||||
} else {
|
||||
/* we are in the parent */
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* EMBEDDED */
|
|
@ -60,8 +60,10 @@ check_PROGRAMS += \
|
|||
$(NULL)
|
||||
endif
|
||||
|
||||
if ENABLE_STATS_TEST
|
||||
check_PROGRAMS += stats/stats_test
|
||||
if !EMBEDDED
|
||||
check_PROGRAMS += \
|
||||
stats/stats_test \
|
||||
exec/exec_test
|
||||
endif
|
||||
|
||||
if ENABLE_GB
|
||||
|
@ -259,6 +261,9 @@ use_count_use_count_test_LDADD = $(LDADD)
|
|||
context_context_test_SOURCES = context/context_test.c
|
||||
context_context_test_LDADD = $(LDADD)
|
||||
|
||||
exec_exec_test_SOURCES = exec/exec_test.c
|
||||
exec_exec_test_LDADD = $(LDADD)
|
||||
|
||||
# The `:;' works around a Bash 3.2 bug when the output is not writeable.
|
||||
$(srcdir)/package.m4: $(top_srcdir)/configure.ac
|
||||
:;{ \
|
||||
|
@ -334,6 +339,7 @@ EXTRA_DIST = testsuite.at $(srcdir)/package.m4 $(TESTSUITE) \
|
|||
use_count/use_count_test.ok use_count/use_count_test.err \
|
||||
context/context_test.ok \
|
||||
gsm0502/gsm0502_test.ok \
|
||||
exec/exec_test.ok exec/exec_test.err \
|
||||
$(NULL)
|
||||
|
||||
DISTCLEANFILES = atconfig atlocal conv/gsm0503_test_vectors.c
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
#include <osmocom/core/utils.h>
|
||||
#include <osmocom/core/exec.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
static void env_dump(char **env)
|
||||
{
|
||||
char **ent;
|
||||
|
||||
for (ent = env; *ent; ent++)
|
||||
printf("\t%s\n", *ent);
|
||||
}
|
||||
|
||||
static void test_env_filter(void)
|
||||
{
|
||||
char *out[256];
|
||||
char *env_in[] = {
|
||||
"FOO=1",
|
||||
"BAR=2",
|
||||
"USER=mahlzeit",
|
||||
"BAZ=3",
|
||||
"SHELL=/bin/sh",
|
||||
NULL
|
||||
};
|
||||
const char *filter[] = {
|
||||
"SHELL",
|
||||
"USER",
|
||||
NULL
|
||||
};
|
||||
int rc;
|
||||
|
||||
printf("\n==== osmo_environment_filter ====\n");
|
||||
|
||||
printf("Input Environment:\n");
|
||||
env_dump(env_in);
|
||||
printf("Input Whitelist:\n");
|
||||
env_dump((char **) filter);
|
||||
rc = osmo_environment_filter(out, ARRAY_SIZE(out), env_in, filter);
|
||||
printf("Output Environment (%d):\n", rc);
|
||||
env_dump(out);
|
||||
OSMO_ASSERT(rc == 3);
|
||||
|
||||
printf("Testing for NULL out\n");
|
||||
rc = osmo_environment_filter(NULL, 123, env_in, filter);
|
||||
OSMO_ASSERT(rc < 0);
|
||||
|
||||
printf("Testing for zero-length out\n");
|
||||
rc = osmo_environment_filter(out, 0, env_in, filter);
|
||||
OSMO_ASSERT(rc < 0);
|
||||
|
||||
printf("Testing for one-length out\n");
|
||||
rc = osmo_environment_filter(out, 1, env_in, filter);
|
||||
OSMO_ASSERT(rc == 1 && out[0] == NULL);
|
||||
|
||||
printf("Testing for no filter\n");
|
||||
rc = osmo_environment_filter(out, ARRAY_SIZE(out), env_in, NULL);
|
||||
OSMO_ASSERT(rc < 0);
|
||||
|
||||
printf("Testing for no input\n");
|
||||
rc = osmo_environment_filter(out, ARRAY_SIZE(out), NULL, filter);
|
||||
OSMO_ASSERT(rc == 1 && out[0] == NULL);
|
||||
printf("Success!\n");
|
||||
}
|
||||
|
||||
static void test_env_append(void)
|
||||
{
|
||||
char *out[256] = {
|
||||
"FOO=a",
|
||||
"BAR=b",
|
||||
"BAZ=c",
|
||||
NULL,
|
||||
};
|
||||
char *add[] = {
|
||||
"MAHL=zeit",
|
||||
"GSM=global",
|
||||
"UMTS=universal",
|
||||
"LTE=evolved",
|
||||
NULL,
|
||||
};
|
||||
int rc;
|
||||
|
||||
printf("\n==== osmo_environment_append ====\n");
|
||||
|
||||
printf("Input Environment:\n");
|
||||
env_dump(out);
|
||||
printf("Input Addition:\n");
|
||||
env_dump(add);
|
||||
rc = osmo_environment_append(out, ARRAY_SIZE(out), add);
|
||||
printf("Output Environment (%d)\n", rc);
|
||||
env_dump(out);
|
||||
OSMO_ASSERT(rc == 8);
|
||||
printf("Success!\n");
|
||||
}
|
||||
|
||||
static void test_close_fd(void)
|
||||
{
|
||||
struct stat st;
|
||||
int fds[2];
|
||||
int rc;
|
||||
|
||||
printf("\n==== osmo_close_all_fds_above ====\n");
|
||||
|
||||
/* create some extra fds */
|
||||
rc = socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
|
||||
OSMO_ASSERT(rc == 0);
|
||||
|
||||
rc = fstat(fds[0], &st);
|
||||
OSMO_ASSERT(rc == 0);
|
||||
|
||||
osmo_close_all_fds_above(2);
|
||||
|
||||
rc = fstat(fds[0], &st);
|
||||
OSMO_ASSERT(rc == -1 && errno == EBADF);
|
||||
rc = fstat(fds[1], &st);
|
||||
OSMO_ASSERT(rc == -1 && errno == EBADF);
|
||||
printf("Success!\n");
|
||||
}
|
||||
|
||||
static void test_system_nowait(void)
|
||||
{
|
||||
char *addl_env[] = {
|
||||
"MAHLZEIT=spaet",
|
||||
NULL
|
||||
};
|
||||
int rc, pid, i;
|
||||
|
||||
printf("\n==== osmo_system_nowait ====\n");
|
||||
|
||||
pid = osmo_system_nowait("env | grep MAHLZEIT 1>&2", osmo_environment_whitelist, addl_env);
|
||||
OSMO_ASSERT(pid > 0);
|
||||
for (i = 0; i < 10; i++) {
|
||||
sleep(1);
|
||||
rc = waitpid(pid, NULL, WNOHANG);
|
||||
if (rc == pid) {
|
||||
printf("Success!\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
printf("ERROR: child didn't terminate within 10s\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
test_env_filter();
|
||||
test_env_append();
|
||||
test_close_fd();
|
||||
test_system_nowait();
|
||||
|
||||
exit(0);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
MAHLZEIT=spaet
|
|
@ -0,0 +1,46 @@
|
|||
|
||||
==== osmo_environment_filter ====
|
||||
Input Environment:
|
||||
FOO=1
|
||||
BAR=2
|
||||
USER=mahlzeit
|
||||
BAZ=3
|
||||
SHELL=/bin/sh
|
||||
Input Whitelist:
|
||||
SHELL
|
||||
USER
|
||||
Output Environment (3):
|
||||
USER=mahlzeit
|
||||
SHELL=/bin/sh
|
||||
Testing for NULL out
|
||||
Testing for zero-length out
|
||||
Testing for one-length out
|
||||
Testing for no filter
|
||||
Testing for no input
|
||||
Success!
|
||||
|
||||
==== osmo_environment_append ====
|
||||
Input Environment:
|
||||
FOO=a
|
||||
BAR=b
|
||||
BAZ=c
|
||||
Input Addition:
|
||||
MAHL=zeit
|
||||
GSM=global
|
||||
UMTS=universal
|
||||
LTE=evolved
|
||||
Output Environment (8)
|
||||
FOO=a
|
||||
BAR=b
|
||||
BAZ=c
|
||||
MAHL=zeit
|
||||
GSM=global
|
||||
UMTS=universal
|
||||
LTE=evolved
|
||||
Success!
|
||||
|
||||
==== osmo_close_all_fds_above ====
|
||||
Success!
|
||||
|
||||
==== osmo_system_nowait ====
|
||||
Success!
|
|
@ -362,3 +362,10 @@ AT_KEYWORDS([context])
|
|||
cat $abs_srcdir/context/context_test.ok > expout
|
||||
AT_CHECK([$abs_top_builddir/tests/context/context_test], [0], [expout], [ignore])
|
||||
AT_CLEANUP
|
||||
|
||||
AT_SETUP([exec])
|
||||
AT_KEYWORDS([exec])
|
||||
cat $abs_srcdir/exec/exec_test.ok > expout
|
||||
cat $abs_srcdir/exec/exec_test.err > experr
|
||||
AT_CHECK([$abs_top_builddir/tests/exec/exec_test], [0], [expout], [experr])
|
||||
AT_CLEANUP
|
||||
|
|
Loading…
Reference in New Issue