blob: 2a03ba80c5b7adf01a935f84742ab20147e152a7 [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
Harald Weltef3cc7312020-04-17 18:23:52 +020026#define _GNU_SOURCE
Harald Weltec6a86972019-12-16 23:14:45 +010027#include <unistd.h>
28
29#include <errno.h>
30#include <string.h>
31
32#include <stdio.h>
33#include <dirent.h>
34#include <sys/types.h>
Harald Weltef3cc7312020-04-17 18:23:52 +020035#include <pwd.h>
Harald Weltec6a86972019-12-16 23:14:45 +010036
37#include <osmocom/core/logging.h>
38#include <osmocom/core/utils.h>
39#include <osmocom/core/exec.h>
40
41/*! suggested list of environment variables to pass (if they exist) to a sub-process/script */
42const char *osmo_environment_whitelist[] = {
43 "USER", "LOGNAME", "HOME",
44 "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
45 "PATH",
46 "PWD",
47 "SHELL",
48 "TERM",
49 "TMPDIR",
50 "LD_LIBRARY_PATH",
51 "LD_PRELOAD",
52 "POSIXLY_CORRECT",
53 "HOSTALIASES",
54 "TZ", "TZDIR",
55 "TERMCAP",
56 "COLUMNS", "LINES",
57 NULL
58};
59
60static bool str_in_list(const char **list, const char *key)
61{
62 const char **ent;
63
64 for (ent = list; *ent; ent++) {
65 if (!strcmp(*ent, key))
66 return true;
67 }
68 return false;
69}
70
71/*! filtered a process environment by whitelist; only copying pointers, no actual strings.
72 *
73 * This function is useful if you'd like to generate an environment to pass exec*e()
74 * functions. It will create a new environment containing only those entries whose
75 * keys (as per environment convention KEY=VALUE) are contained in the whitelist. The
76 * function will not copy the actual strings, but just create a new pointer array, pointing
77 * to the same memory as the input strings.
78 *
79 * Constraints: Keys up to a maximum length of 255 characters are supported.
80 *
81 * \oaram[out] out caller-allocated array of pointers for the generated output
82 * \param[in] out_len size of out (number of pointers)
83 * \param[in] in input environment (NULL-terminated list of pointers like **environ)
84 * \param[in] whitelist whitelist of permitted keys in environment (like **environ)
85 * \returns number of entries filled in 'out'; negtive on error */
86int osmo_environment_filter(char **out, size_t out_len, char **in, const char **whitelist)
87{
88 char tmp[256];
89 char **ent;
90 size_t out_used = 0;
91
92 /* invalid calls */
93 if (!out || out_len == 0 || !whitelist)
94 return -EINVAL;
95
96 /* legal, but unusual: no input to filter should generate empty, terminated out */
97 if (!in) {
98 out[0] = NULL;
99 return 1;
100 }
101
102 /* iterate over input entries */
103 for (ent = in; *ent; ent++) {
104 char *eq = strchr(*ent, '=');
105 unsigned long eq_pos;
106 if (!eq) {
107 /* no '=' in string, skip it */
108 continue;
109 }
110 eq_pos = eq - *ent;
111 if (eq_pos >= ARRAY_SIZE(tmp))
112 continue;
113 strncpy(tmp, *ent, eq_pos);
114 tmp[eq_pos] = '\0';
115 if (str_in_list(whitelist, tmp)) {
116 if (out_used == out_len-1)
117 break;
118 /* append to output */
119 out[out_used++] = *ent;
120 }
121 }
122 OSMO_ASSERT(out_used < out_len);
123 out[out_used++] = NULL;
124 return out_used;
125}
126
127/*! append one environment to another; only copying pointers, not actual strings.
128 *
129 * This function is useful if you'd like to append soem entries to an environment
130 * befoer passing it to exec*e() functions.
131 *
132 * It will append all entries from 'in' to the environment in 'out', as long as
133 * 'out' has space (determined by 'out_len').
134 *
135 * Constraints: If the same key exists in 'out' and 'in', duplicate keys are
136 * generated. It is a simple append, without any duplicate checks.
137 *
138 * \oaram[out] out caller-allocated array of pointers for the generated output
139 * \param[in] out_len size of out (number of pointers)
140 * \param[in] in input environment (NULL-terminated list of pointers like **environ)
141 * \returns number of entries filled in 'out'; negative on error */
142int osmo_environment_append(char **out, size_t out_len, char **in)
143{
144 size_t out_used = 0;
145
146 if (!out || out_len == 0)
147 return -EINVAL;
148
149 /* seek to end of existing output */
150 for (out_used = 0; out[out_used]; out_used++) {}
151
152 if (!in) {
153 if (out_used == 0)
154 out[out_used++] = NULL;
155 return out_used;
156 }
157
158 for (; *in && out_used < out_len-1; in++)
159 out[out_used++] = *in;
160
161 OSMO_ASSERT(out_used < out_len);
162 out[out_used++] = NULL;
163
164 return out_used;
165}
166
167/* Iterate over files in /proc/self/fd and close all above lst_fd_to_keep */
168int osmo_close_all_fds_above(int last_fd_to_keep)
169{
170 struct dirent *ent;
171 DIR *dir;
172 int rc;
173
174 dir = opendir("/proc/self/fd");
175 if (!dir) {
176 LOGP(DLGLOBAL, LOGL_ERROR, "Cannot open /proc/self/fd: %s\n", strerror(errno));
177 return -ENODEV;
178 }
179
180 while ((ent = readdir(dir))) {
181 int fd = atoi(ent->d_name);
182 if (fd <= last_fd_to_keep)
183 continue;
184 if (fd == dirfd(dir))
185 continue;
186 rc = close(fd);
187 if (rc)
188 LOGP(DLGLOBAL, LOGL_ERROR, "Error closing fd=%d: %s\n", fd, strerror(errno));
189 }
190 closedir(dir);
191 return 0;
192}
193
194/* Seems like POSIX has no header file for this, and even glibc + __USE_GNU doesn't help */
195extern char **environ;
196
Harald Weltef3cc7312020-04-17 18:23:52 +0200197/*! call an external shell command as 'user' without waiting for it.
Harald Weltec6a86972019-12-16 23:14:45 +0100198 *
199 * This mimics the behavior of system(3), with the following differences:
200 * - it doesn't wait for completion of the child process
201 * - it closes all non-stdio file descriptors by iterating /proc/self/fd
202 * - it constructs a reduced environment where only whitelisted keys survive
203 * - it (optionally) appends additional variables to the environment
Harald Weltef3cc7312020-04-17 18:23:52 +0200204 * - it (optionally) changes the user ID to that of 'user' (requires execution as root)
Harald Weltec6a86972019-12-16 23:14:45 +0100205 *
206 * \param[in] command the shell command to be executed, see system(3)
207 * \param[in] env_whitelist A white-list of keys for environment variables
208 * \param[in] addl_env any additional environment variables to be appended
Harald Weltef3cc7312020-04-17 18:23:52 +0200209 * \param[in] user name of the user to which we should switch before executing the command
Harald Weltec6a86972019-12-16 23:14:45 +0100210 * \returns PID of generated child process; negative on error
211 */
Harald Weltef3cc7312020-04-17 18:23:52 +0200212int osmo_system_nowait2(const char *command, const char **env_whitelist, char **addl_env, const char *user)
Harald Weltec6a86972019-12-16 23:14:45 +0100213{
Philipp Maier6a90c722020-04-20 11:08:29 +0200214 struct passwd _pw;
215 struct passwd *pw = NULL;
Harald Weltef3cc7312020-04-17 18:23:52 +0200216 int getpw_buflen = sysconf(_SC_GETPW_R_SIZE_MAX);
Harald Weltec6a86972019-12-16 23:14:45 +0100217 int rc;
218
Harald Weltef3cc7312020-04-17 18:23:52 +0200219 if (user) {
220 char buf[getpw_buflen];
221 getpwnam_r(user, &_pw, buf, sizeof(buf), &pw);
222 if (!pw)
223 return -EINVAL;
224 }
225
Harald Weltec6a86972019-12-16 23:14:45 +0100226 rc = fork();
227 if (rc == 0) {
228 /* we are in the child */
229 char *new_env[1024];
230
231 /* close all file descriptors above stdio */
232 osmo_close_all_fds_above(2);
233
Vadim Yanitskiyd419f652020-02-09 04:44:35 +0700234 /* man execle: "an array of pointers *must* be terminated by a null pointer" */
235 new_env[0] = NULL;
236
Harald Weltec6a86972019-12-16 23:14:45 +0100237 /* build the new environment */
Vadim Yanitskiy979c3b72020-02-09 05:06:12 +0700238 if (env_whitelist) {
239 rc = osmo_environment_filter(new_env, ARRAY_SIZE(new_env), environ, env_whitelist);
240 if (rc < 0)
241 return rc;
242 }
243 if (addl_env) {
244 rc = osmo_environment_append(new_env, ARRAY_SIZE(new_env), addl_env);
245 if (rc < 0)
246 return rc;
247 }
Harald Weltec6a86972019-12-16 23:14:45 +0100248
Harald Weltef3cc7312020-04-17 18:23:52 +0200249 /* drop privileges */
250 if (pw) {
251 if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) < 0) {
252 perror("setresgid() during privilege drop");
253 exit(1);
254 }
255
256 if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0) {
257 perror("setresuid() during privilege drop");
258 exit(1);
259 }
260
261 }
262
Harald Weltec6a86972019-12-16 23:14:45 +0100263 /* if we want to behave like system(3), we must go via the shell */
264 execle("/bin/sh", "sh", "-c", command, (char *) NULL, new_env);
265 /* only reached in case of error */
266 LOGP(DLGLOBAL, LOGL_ERROR, "Error executing command '%s' after fork: %s\n",
267 command, strerror(errno));
268 return -EIO;
269 } else {
270 /* we are in the parent */
271 return rc;
272 }
273}
274
Harald Weltef3cc7312020-04-17 18:23:52 +0200275/*! call an external shell command without waiting for it.
276 *
277 * This mimics the behavior of system(3), with the following differences:
278 * - it doesn't wait for completion of the child process
279 * - it closes all non-stdio file descriptors by iterating /proc/self/fd
280 * - it constructs a reduced environment where only whitelisted keys survive
281 * - it (optionally) appends additional variables to the environment
282 *
283 * \param[in] command the shell command to be executed, see system(3)
284 * \param[in] env_whitelist A white-list of keys for environment variables
285 * \param[in] addl_env any additional environment variables to be appended
286 * \returns PID of generated child process; negative on error
287 */
288int osmo_system_nowait(const char *command, const char **env_whitelist, char **addl_env)
289{
290 return osmo_system_nowait2(command, env_whitelist, addl_env, NULL);
291}
292
293
Harald Weltec6a86972019-12-16 23:14:45 +0100294#endif /* EMBEDDED */