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