libosmocore/src/core/exec.c

291 lines
8.5 KiB
C

/* (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.
*
*/
#include "config.h"
#ifndef EMBEDDED
#define _GNU_SOURCE
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h>
#include <pwd.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.
*
* \param[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.
*
* \param[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 as 'user' 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
* - it (optionally) changes the user ID to that of 'user' (requires execution as root)
*
* \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
* \param[in] user name of the user to which we should switch before executing the command
* \returns PID of generated child process; negative on error
*/
int osmo_system_nowait2(const char *command, const char **env_whitelist, char **addl_env, const char *user)
{
struct passwd _pw;
struct passwd *pw = NULL;
int getpw_buflen = sysconf(_SC_GETPW_R_SIZE_MAX);
int rc;
if (user) {
char buf[getpw_buflen];
getpwnam_r(user, &_pw, buf, sizeof(buf), &pw);
if (!pw)
return -EINVAL;
}
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);
/* man execle: "an array of pointers *must* be terminated by a null pointer" */
new_env[0] = NULL;
/* build the new environment */
if (env_whitelist) {
rc = osmo_environment_filter(new_env, ARRAY_SIZE(new_env), environ, env_whitelist);
if (rc < 0)
return rc;
}
if (addl_env) {
rc = osmo_environment_append(new_env, ARRAY_SIZE(new_env), addl_env);
if (rc < 0)
return rc;
}
/* drop privileges */
if (pw) {
if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) < 0) {
perror("setresgid() during privilege drop");
exit(1);
}
if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0) {
perror("setresuid() during privilege drop");
exit(1);
}
}
/* 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;
}
}
/*! 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)
{
return osmo_system_nowait2(command, env_whitelist, addl_env, NULL);
}
#endif /* EMBEDDED */