blob: a9d8ce0ff35222b10e29345a085bbf0775fce318 [file] [log] [blame]
Harald Weltec6a86972019-12-16 23:14:45 +01001/* (C) 2019 by Harald Welte <laforge@gnumonks.org>
2 *
3 * All Rights Reserved
4 *
5 * SPDX-License-Identifier: GPL-2.0+
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 */
22
23#include "config.h"
24#ifndef EMBEDDED
25
26#include <unistd.h>
27
28#include <errno.h>
29#include <string.h>
30
31#include <stdio.h>
32#include <dirent.h>
33#include <sys/types.h>
34
35#include <osmocom/core/logging.h>
36#include <osmocom/core/utils.h>
37#include <osmocom/core/exec.h>
38
39/*! suggested list of environment variables to pass (if they exist) to a sub-process/script */
40const char *osmo_environment_whitelist[] = {
41 "USER", "LOGNAME", "HOME",
42 "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
43 "PATH",
44 "PWD",
45 "SHELL",
46 "TERM",
47 "TMPDIR",
48 "LD_LIBRARY_PATH",
49 "LD_PRELOAD",
50 "POSIXLY_CORRECT",
51 "HOSTALIASES",
52 "TZ", "TZDIR",
53 "TERMCAP",
54 "COLUMNS", "LINES",
55 NULL
56};
57
58static bool str_in_list(const char **list, const char *key)
59{
60 const char **ent;
61
62 for (ent = list; *ent; ent++) {
63 if (!strcmp(*ent, key))
64 return true;
65 }
66 return false;
67}
68
69/*! filtered a process environment by whitelist; only copying pointers, no actual strings.
70 *
71 * This function is useful if you'd like to generate an environment to pass exec*e()
72 * functions. It will create a new environment containing only those entries whose
73 * keys (as per environment convention KEY=VALUE) are contained in the whitelist. The
74 * function will not copy the actual strings, but just create a new pointer array, pointing
75 * to the same memory as the input strings.
76 *
77 * Constraints: Keys up to a maximum length of 255 characters are supported.
78 *
79 * \oaram[out] out caller-allocated array of pointers for the generated output
80 * \param[in] out_len size of out (number of pointers)
81 * \param[in] in input environment (NULL-terminated list of pointers like **environ)
82 * \param[in] whitelist whitelist of permitted keys in environment (like **environ)
83 * \returns number of entries filled in 'out'; negtive on error */
84int osmo_environment_filter(char **out, size_t out_len, char **in, const char **whitelist)
85{
86 char tmp[256];
87 char **ent;
88 size_t out_used = 0;
89
90 /* invalid calls */
91 if (!out || out_len == 0 || !whitelist)
92 return -EINVAL;
93
94 /* legal, but unusual: no input to filter should generate empty, terminated out */
95 if (!in) {
96 out[0] = NULL;
97 return 1;
98 }
99
100 /* iterate over input entries */
101 for (ent = in; *ent; ent++) {
102 char *eq = strchr(*ent, '=');
103 unsigned long eq_pos;
104 if (!eq) {
105 /* no '=' in string, skip it */
106 continue;
107 }
108 eq_pos = eq - *ent;
109 if (eq_pos >= ARRAY_SIZE(tmp))
110 continue;
111 strncpy(tmp, *ent, eq_pos);
112 tmp[eq_pos] = '\0';
113 if (str_in_list(whitelist, tmp)) {
114 if (out_used == out_len-1)
115 break;
116 /* append to output */
117 out[out_used++] = *ent;
118 }
119 }
120 OSMO_ASSERT(out_used < out_len);
121 out[out_used++] = NULL;
122 return out_used;
123}
124
125/*! append one environment to another; only copying pointers, not actual strings.
126 *
127 * This function is useful if you'd like to append soem entries to an environment
128 * befoer passing it to exec*e() functions.
129 *
130 * It will append all entries from 'in' to the environment in 'out', as long as
131 * 'out' has space (determined by 'out_len').
132 *
133 * Constraints: If the same key exists in 'out' and 'in', duplicate keys are
134 * generated. It is a simple append, without any duplicate checks.
135 *
136 * \oaram[out] out caller-allocated array of pointers for the generated output
137 * \param[in] out_len size of out (number of pointers)
138 * \param[in] in input environment (NULL-terminated list of pointers like **environ)
139 * \returns number of entries filled in 'out'; negative on error */
140int osmo_environment_append(char **out, size_t out_len, char **in)
141{
142 size_t out_used = 0;
143
144 if (!out || out_len == 0)
145 return -EINVAL;
146
147 /* seek to end of existing output */
148 for (out_used = 0; out[out_used]; out_used++) {}
149
150 if (!in) {
151 if (out_used == 0)
152 out[out_used++] = NULL;
153 return out_used;
154 }
155
156 for (; *in && out_used < out_len-1; in++)
157 out[out_used++] = *in;
158
159 OSMO_ASSERT(out_used < out_len);
160 out[out_used++] = NULL;
161
162 return out_used;
163}
164
165/* Iterate over files in /proc/self/fd and close all above lst_fd_to_keep */
166int osmo_close_all_fds_above(int last_fd_to_keep)
167{
168 struct dirent *ent;
169 DIR *dir;
170 int rc;
171
172 dir = opendir("/proc/self/fd");
173 if (!dir) {
174 LOGP(DLGLOBAL, LOGL_ERROR, "Cannot open /proc/self/fd: %s\n", strerror(errno));
175 return -ENODEV;
176 }
177
178 while ((ent = readdir(dir))) {
179 int fd = atoi(ent->d_name);
180 if (fd <= last_fd_to_keep)
181 continue;
182 if (fd == dirfd(dir))
183 continue;
184 rc = close(fd);
185 if (rc)
186 LOGP(DLGLOBAL, LOGL_ERROR, "Error closing fd=%d: %s\n", fd, strerror(errno));
187 }
188 closedir(dir);
189 return 0;
190}
191
192/* Seems like POSIX has no header file for this, and even glibc + __USE_GNU doesn't help */
193extern char **environ;
194
195/*! call an external shell command without waiting for it.
196 *
197 * This mimics the behavior of system(3), with the following differences:
198 * - it doesn't wait for completion of the child process
199 * - it closes all non-stdio file descriptors by iterating /proc/self/fd
200 * - it constructs a reduced environment where only whitelisted keys survive
201 * - it (optionally) appends additional variables to the environment
202 *
203 * \param[in] command the shell command to be executed, see system(3)
204 * \param[in] env_whitelist A white-list of keys for environment variables
205 * \param[in] addl_env any additional environment variables to be appended
206 * \returns PID of generated child process; negative on error
207 */
208int osmo_system_nowait(const char *command, const char **env_whitelist, char **addl_env)
209{
210 int rc;
211
212 rc = fork();
213 if (rc == 0) {
214 /* we are in the child */
215 char *new_env[1024];
216
217 /* close all file descriptors above stdio */
218 osmo_close_all_fds_above(2);
219
220 /* build the new environment */
221 if (env_whitelist)
222 osmo_environment_filter(new_env, ARRAY_SIZE(new_env), environ, env_whitelist);
223 if (addl_env)
224 osmo_environment_append(new_env, ARRAY_SIZE(new_env), addl_env);
225
226 /* if we want to behave like system(3), we must go via the shell */
227 execle("/bin/sh", "sh", "-c", command, (char *) NULL, new_env);
228 /* only reached in case of error */
229 LOGP(DLGLOBAL, LOGL_ERROR, "Error executing command '%s' after fork: %s\n",
230 command, strerror(errno));
231 return -EIO;
232 } else {
233 /* we are in the parent */
234 return rc;
235 }
236}
237
238#endif /* EMBEDDED */