1 /*
2 * SPDX-License-Identifier: ISC
3 *
4 * Copyright (c) 2008, 2010-2018, 2020-2021 Todd C. Miller <Todd.Miller@sudo.ws>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19 /*
20 * This is an open source non-commercial project. Dear PVS-Studio, please check it.
21 * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
22 */
23
24 #include <config.h>
25
26 #include <sys/stat.h>
27 #include <errno.h>
28 #include <fcntl.h>
29 #include <limits.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <signal.h>
34 #include <time.h>
35 #include <unistd.h>
36 #ifdef HAVE_STDBOOL_H
37 # include <stdbool.h>
38 #else
39 # include "compat/stdbool.h"
40 #endif /* HAVE_STDBOOL_H */
41
42 #include "sudo.h"
43 #include "sudo_exec.h"
44 #include "sudo_edit.h"
45
46 sudo_dso_public int main(int argc, char *argv[], char *envp[]);
47
48 static int sesh_sudoedit(int argc, char *argv[]);
49
50 /*
51 * Exit codes defined in sudo_exec.h:
52 * SESH_SUCCESS (0) ... successful operation
53 * SESH_ERR_FAILURE (1) ... unspecified error
54 * SESH_ERR_INVALID (30) ... invalid -e arg value
55 * SESH_ERR_BAD_PATHS (31) ... odd number of paths
56 * SESH_ERR_NO_FILES (32) ... copy error, no files copied
57 * SESH_ERR_SOME_FILES (33) ... copy error, no files copied
58 */
59 int
main(int argc,char * argv[],char * envp[])60 main(int argc, char *argv[], char *envp[])
61 {
62 int ret;
63 debug_decl(main, SUDO_DEBUG_MAIN);
64
65 initprogname(argc > 0 ? argv[0] : "sesh");
66
67 setlocale(LC_ALL, "");
68 bindtextdomain(PACKAGE_NAME, LOCALEDIR);
69 textdomain(PACKAGE_NAME);
70
71 if (argc < 2)
72 sudo_fatalx("%s", U_("requires at least one argument"));
73
74 /* Read sudo.conf and initialize the debug subsystem. */
75 if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)
76 exit(EXIT_FAILURE);
77 sudo_debug_register(getprogname(), NULL, NULL,
78 sudo_conf_debug_files(getprogname()), -1);
79
80 if (strcmp(argv[1], "-e") == 0) {
81 ret = sesh_sudoedit(argc, argv);
82 } else {
83 bool login_shell;
84 char *cp, *cmnd;
85 int flags = 0;
86 int fd = -1;
87
88 /* If the first char of argv[0] is '-', we are running a login shell. */
89 login_shell = argv[0][0] == '-';
90
91 /* If argv[0] ends in -noexec, pass the flag to sudo_execve() */
92 if ((cp = strrchr(argv[0], '-')) != NULL && cp != argv[0]) {
93 if (strcmp(cp, "-noexec") == 0)
94 SET(flags, CD_NOEXEC);
95 }
96
97 /* If argv[1] is --execfd=%d, extract the fd to exec with. */
98 if (strncmp(argv[1], "--execfd=", 9) == 0) {
99 const char *errstr;
100
101 cp = argv[1] + 9;
102 fd = sudo_strtonum(cp, 0, INT_MAX, &errstr);
103 if (errstr != NULL)
104 sudo_fatalx(U_("invalid file descriptor number: %s"), cp);
105 argv++;
106 argc--;
107 }
108
109 /* Shift argv and make a copy of the command to execute. */
110 argv++;
111 argc--;
112 if ((cmnd = strdup(argv[0])) == NULL)
113 sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
114
115 /* If invoked as a login shell, modify argv[0] accordingly. */
116 if (login_shell) {
117 if ((cp = strrchr(argv[0], '/')) == NULL)
118 sudo_fatal(U_("unable to run %s as a login shell"), argv[0]);
119 *cp = '-';
120 argv[0] = cp;
121 }
122 sudo_execve(fd, cmnd, argv, envp, -1, flags);
123 sudo_warn(U_("unable to execute %s"), cmnd);
124 ret = SESH_ERR_FAILURE;
125 }
126 sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, ret);
127 _exit(ret);
128 }
129
130 /*
131 * Destructively parse a string in the format:
132 * uid:gid:groups,...
133 *
134 * On success, fills in ud and returns true, else false.
135 */
136 static bool
parse_user(char * userstr,struct sudo_cred * cred)137 parse_user(char *userstr, struct sudo_cred *cred)
138 {
139 char *cp, *ep;
140 const char *errstr;
141 debug_decl(parse_user, SUDO_DEBUG_EDIT);
142
143 /* UID */
144 cp = userstr;
145 if ((ep = strchr(cp, ':')) == NULL) {
146 sudo_warnx(U_("%s: %s"), cp, U_("invalid value"));
147 debug_return_bool(false);
148 }
149 *ep++ = '\0';
150 cred->uid = cred->euid = sudo_strtoid(cp, &errstr);
151 if (errstr != NULL) {
152 sudo_warnx(U_("%s: %s"), cp, errstr);
153 debug_return_bool(false);
154 }
155
156 /* GID */
157 cp = ep;
158 if ((ep = strchr(cp, ':')) == NULL) {
159 sudo_warnx(U_("%s: %s"), cp, U_("invalid value"));
160 debug_return_bool(false);
161 }
162 *ep++ = '\0';
163 cred->gid = cred->egid = sudo_strtoid(cp, &errstr);
164 if (errstr != NULL) {
165 sudo_warnx(U_("%s: %s"), cp, errstr);
166 debug_return_bool(false);
167 }
168
169 /* group vector */
170 cp = ep;
171 cred->ngroups = sudo_parse_gids(cp, NULL, &cred->groups);
172 if (cred->ngroups == -1)
173 debug_return_bool(false);
174
175 debug_return_bool(true);
176 }
177
178 static int
sesh_edit_create_tfiles(int edit_flags,struct sudo_cred * user_cred,struct sudo_cred * run_cred,int argc,char * argv[])179 sesh_edit_create_tfiles(int edit_flags, struct sudo_cred *user_cred,
180 struct sudo_cred *run_cred, int argc, char *argv[])
181 {
182 int i, fd_src = -1, fd_dst = -1;
183 struct timespec times[2];
184 struct stat sb;
185 debug_decl(sesh_edit_create_tfiles, SUDO_DEBUG_EDIT);
186
187 for (i = 0; i < argc - 1; i += 2) {
188 char *path_src = argv[i];
189 const char *path_dst = argv[i + 1];
190
191 /*
192 * Try to open the source file for reading.
193 * If it doesn't exist, we'll create an empty destination file.
194 */
195 fd_src = sudo_edit_open(path_src, O_RDONLY,
196 S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, edit_flags, user_cred, run_cred);
197 if (fd_src == -1) {
198 if (errno != ENOENT) {
199 if (errno == ELOOP) {
200 sudo_warnx(U_("%s: editing symbolic links is not "
201 "permitted"), path_src);
202 } else if (errno == EISDIR) {
203 sudo_warnx(U_("%s: editing files in a writable directory "
204 "is not permitted"), path_src);
205 } else {
206 sudo_warn("%s", path_src);
207 }
208 goto cleanup;
209 }
210 /* New file, verify parent dir exists and is not writable. */
211 if (!sudo_edit_parent_valid(path_src, edit_flags, user_cred, run_cred))
212 goto cleanup;
213 }
214 if (fd_src == -1) {
215 /* New file. */
216 memset(&sb, 0, sizeof(sb));
217 } else if (fstat(fd_src, &sb) == -1 || !S_ISREG(sb.st_mode)) {
218 sudo_warnx(U_("%s: not a regular file"), path_src);
219 goto cleanup;
220 }
221
222 /*
223 * Create temporary file using O_EXCL to ensure that temporary
224 * files are created by us and that we do not open any symlinks.
225 */
226 fd_dst = open(path_dst, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
227 if (fd_dst == -1) {
228 sudo_warn("%s", path_dst);
229 goto cleanup;
230 }
231
232 if (fd_src != -1) {
233 if (sudo_copy_file(path_src, fd_src, -1, path_dst, fd_dst, -1) == -1)
234 goto cleanup;
235 close(fd_src);
236 }
237
238 /* Make mtime on temp file match src (sb filled in above). */
239 mtim_get(&sb, times[0]);
240 times[1].tv_sec = times[0].tv_sec;
241 times[1].tv_nsec = times[0].tv_nsec;
242 if (futimens(fd_dst, times) == -1) {
243 if (utimensat(AT_FDCWD, path_dst, times, 0) == -1)
244 sudo_warn("%s", path_dst);
245 }
246 close(fd_dst);
247 fd_dst = -1;
248 }
249 debug_return_int(SESH_SUCCESS);
250
251 cleanup:
252 /* Remove temporary files. */
253 for (i = 0; i < argc - 1; i += 2)
254 unlink(argv[i + 1]);
255 if (fd_src != -1)
256 close(fd_src);
257 if (fd_dst != -1)
258 close(fd_dst);
259 debug_return_int(SESH_ERR_NO_FILES);
260 }
261
262 static int
sesh_edit_copy_tfiles(int edit_flags,struct sudo_cred * user_cred,struct sudo_cred * run_cred,int argc,char * argv[])263 sesh_edit_copy_tfiles(int edit_flags, struct sudo_cred *user_cred,
264 struct sudo_cred *run_cred, int argc, char *argv[])
265 {
266 int i, ret = SESH_SUCCESS;
267 int fd_src = -1, fd_dst = -1;
268 debug_decl(sesh_edit_copy_tfiles, SUDO_DEBUG_EDIT);
269
270 for (i = 0; i < argc - 1; i += 2) {
271 const char *path_src = argv[i];
272 char *path_dst = argv[i + 1];
273 off_t len_src, len_dst;
274 struct stat sb;
275
276 /* Open temporary file for reading. */
277 if (fd_src != -1)
278 close(fd_src);
279 fd_src = open(path_src, O_RDONLY|O_NONBLOCK|O_NOFOLLOW);
280 if (fd_src == -1) {
281 sudo_warn("%s", path_src);
282 ret = SESH_ERR_SOME_FILES;
283 continue;
284 }
285 /* Make sure the temporary file is safe and has the proper owner. */
286 if (!sudo_check_temp_file(fd_src, path_src, run_cred->uid, &sb)) {
287 sudo_warnx(U_("contents of edit session left in %s"), path_src);
288 ret = SESH_ERR_SOME_FILES;
289 continue;
290 }
291 (void) fcntl(fd_src, F_SETFL, fcntl(fd_src, F_GETFL, 0) & ~O_NONBLOCK);
292
293 /* Create destination file. */
294 if (fd_dst != -1)
295 close(fd_dst);
296 fd_dst = sudo_edit_open(path_dst, O_WRONLY|O_CREAT,
297 S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, edit_flags, user_cred, run_cred);
298 if (fd_dst == -1) {
299 if (errno == ELOOP) {
300 sudo_warnx(U_("%s: editing symbolic links is not "
301 "permitted"), path_dst);
302 } else if (errno == EISDIR) {
303 sudo_warnx(U_("%s: editing files in a writable directory "
304 "is not permitted"), path_dst);
305 } else {
306 sudo_warn("%s", path_dst);
307 }
308 sudo_warnx(U_("contents of edit session left in %s"), path_src);
309 ret = SESH_ERR_SOME_FILES;
310 continue;
311 }
312
313 /* sudo_check_temp_file() filled in sb for us. */
314 len_src = sb.st_size;
315 if (fstat(fd_dst, &sb) != 0) {
316 sudo_warn("%s", path_dst);
317 sudo_warnx(U_("contents of edit session left in %s"), path_src);
318 ret = SESH_ERR_SOME_FILES;
319 continue;
320 }
321 len_dst = sb.st_size;
322
323 if (sudo_copy_file(path_src, fd_src, len_src, path_dst, fd_dst,
324 len_dst) == -1) {
325 sudo_warnx(U_("contents of edit session left in %s"), path_src);
326 ret = SESH_ERR_SOME_FILES;
327 continue;
328 }
329 unlink(path_src);
330 }
331 if (fd_src != -1)
332 close(fd_src);
333 if (fd_dst != -1)
334 close(fd_dst);
335
336 debug_return_int(ret);
337 }
338
339 static int
sesh_sudoedit(int argc,char * argv[])340 sesh_sudoedit(int argc, char *argv[])
341 {
342 int edit_flags, post, ret;
343 struct sudo_cred user_cred, run_cred;
344 debug_decl(sesh_sudoedit, SUDO_DEBUG_EDIT);
345
346 memset(&user_cred, 0, sizeof(user_cred));
347 memset(&run_cred, 0, sizeof(run_cred));
348 edit_flags = CD_SUDOEDIT_FOLLOW;
349
350 /* Check for -h flag (don't follow links). */
351 if (argc > 2 && strcmp(argv[2], "-h") == 0) {
352 argv++;
353 argc--;
354 CLR(edit_flags, CD_SUDOEDIT_FOLLOW); // -V753
355 }
356
357 /* Check for -w flag (disallow directories writable by the user). */
358 if (argc > 2 && strcmp(argv[2], "-w") == 0) {
359 SET(edit_flags, CD_SUDOEDIT_CHECKDIR);
360
361 /* Parse uid:gid:gid1,gid2,... */
362 if (argv[3] == NULL || !parse_user(argv[3], &user_cred))
363 debug_return_int(SESH_ERR_FAILURE);
364 argv += 2;
365 argc -= 2;
366 }
367
368 if (argc < 3)
369 debug_return_int(SESH_ERR_FAILURE);
370
371 /*
372 * We need to know whether we are performing the copy operation
373 * before or after the editing. Without this we would not know
374 * which files are temporary and which are the originals.
375 * post = 0 ... before
376 * post = 1 ... after
377 */
378 if (strcmp(argv[2], "0") == 0)
379 post = 0;
380 else if (strcmp(argv[2], "1") == 0)
381 post = 1;
382 else /* invalid value */
383 debug_return_int(SESH_ERR_INVALID);
384
385 /* Align argv & argc to the beginning of the file list. */
386 argv += 3;
387 argc -= 3;
388
389 /* no files specified, nothing to do */
390 if (argc == 0)
391 debug_return_int(SESH_SUCCESS);
392 /* odd number of paths specified */
393 if (argc & 1)
394 debug_return_int(SESH_ERR_BAD_PATHS);
395
396 /* Masquerade as sudoedit so the user gets consistent error messages. */
397 setprogname("sudoedit");
398
399 /*
400 * sudoedit runs us with the effective user-ID and group-ID of
401 * the target user as well as with the target user's group list.
402 */
403 run_cred.uid = run_cred.euid = geteuid();
404 run_cred.gid = run_cred.egid = getegid();
405 run_cred.ngroups = getgroups(0, NULL); // -V575
406 if (run_cred.ngroups > 0) {
407 run_cred.groups = reallocarray(NULL, run_cred.ngroups,
408 sizeof(GETGROUPS_T));
409 if (run_cred.groups == NULL) {
410 sudo_warnx(U_("%s: %s"), __func__,
411 U_("unable to allocate memory"));
412 debug_return_int(SESH_ERR_FAILURE);
413 }
414 run_cred.ngroups = getgroups(run_cred.ngroups, run_cred.groups);
415 if (run_cred.ngroups < 0) {
416 sudo_warn("%s", U_("unable to get group list"));
417 free(run_cred.groups);
418 debug_return_int(SESH_ERR_FAILURE);
419 }
420 } else {
421 run_cred.ngroups = 0;
422 run_cred.groups = NULL;
423 }
424
425 ret = post ?
426 sesh_edit_copy_tfiles(edit_flags, &user_cred, &run_cred, argc, argv) :
427 sesh_edit_create_tfiles(edit_flags, &user_cred, &run_cred, argc, argv);
428 debug_return_int(ret);
429 }
430