From a5be62c68fca656beab7e68590d817154cbf6a4c Mon Sep 17 00:00:00 2001 From: "Todd C. Miller" Date: Wed, 6 Jan 2021 13:01:09 -0700 Subject: [PATCH] Move safe open code out of sudo_edit.c and into edit_open.c. --- MANIFEST | 2 + src/Makefile.in | 42 +++-- src/edit_open.c | 404 ++++++++++++++++++++++++++++++++++++++++++++++++ src/sesh.c | 1 + src/sudo_edit.c | 398 +---------------------------------------------- src/sudo_edit.h | 53 +++++++ src/sudo_exec.h | 4 - 7 files changed, 497 insertions(+), 407 deletions(-) create mode 100644 src/edit_open.c create mode 100644 src/sudo_edit.h diff --git a/MANIFEST b/MANIFEST index 623e9c452..2d7746da8 100644 --- a/MANIFEST +++ b/MANIFEST @@ -995,6 +995,7 @@ scripts/unanon src/Makefile.in src/conversation.c src/copy_file.c +src/edit_open.c src/env_hooks.c src/exec.c src/exec_common.c @@ -1019,6 +1020,7 @@ src/solaris.c src/sudo.c src/sudo.h src/sudo_edit.c +src/sudo_edit.h src/sudo_exec.h src/sudo_noexec.c src/sudo_plugin_int.h diff --git a/src/Makefile.in b/src/Makefile.in index 82c9f0efa..06b75ec12 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -1,7 +1,7 @@ # # SPDX-License-Identifier: ISC # -# Copyright (c) 2010-2018 Todd C. Miller +# Copyright (c) 2010-2021 # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -123,8 +123,8 @@ SHELL = @SHELL@ PROGS = @PROGS@ -OBJS = conversation.o copy_file.o env_hooks.o exec.o exec_common.o \ - exec_monitor.o exec_nopty.o exec_pty.o get_pty.o hooks.o \ +OBJS = conversation.o copy_file.o edit_open.o env_hooks.o exec.o \ + exec_common.o exec_monitor.o exec_nopty.o exec_pty.o get_pty.o hooks.o \ limits.o load_plugins.o net_ifs.o parse_args.o preserve_fds.o \ signal.o sudo.o sudo_edit.o tcsetpgrp_nobg.o tgetpass.o \ ttyname.o utmp.o @SUDO_OBJS@ @@ -362,6 +362,24 @@ copy_file.i: $(srcdir)/copy_file.c $(incdir)/compat/stdbool.h \ $(CC) -E -o $@ $(CPPFLAGS) $< copy_file.plog: copy_file.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/copy_file.c --i-file $< --output-file $@ +edit_open.o: $(srcdir)/edit_open.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_edit.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/edit_open.c +edit_open.i: $(srcdir)/edit_open.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_edit.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +edit_open.plog: edit_open.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/edit_open.c --i-file $< --output-file $@ env_hooks.o: $(srcdir)/env_hooks.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ $(incdir)/sudo_debug.h $(incdir)/sudo_dso.h \ @@ -643,14 +661,14 @@ selinux.plog: selinux.i sesh.o: $(srcdir)/sesh.c $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ - $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/sudo_exec.h \ - $(top_builddir)/config.h + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/sudo_edit.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sesh.c sesh.i: $(srcdir)/sesh.c $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ - $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/sudo_exec.h \ - $(top_builddir)/config.h + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/sudo_edit.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h $(CC) -E -o $@ $(CPPFLAGS) $< sesh.plog: sesh.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sesh.c --i-file $< --output-file $@ @@ -711,16 +729,18 @@ sudo_edit.o: $(srcdir)/sudo_edit.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ - $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \ - $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_edit.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sudo_edit.c sudo_edit.i: $(srcdir)/sudo_edit.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ - $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \ - $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_edit.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h $(CC) -E -o $@ $(CPPFLAGS) $< sudo_edit.plog: sudo_edit.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sudo_edit.c --i-file $< --output-file $@ diff --git a/src/edit_open.c b/src/edit_open.c new file mode 100644 index 000000000..287bee381 --- /dev/null +++ b/src/edit_open.c @@ -0,0 +1,404 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2015-2021 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sudo.h" +#include "sudo_edit.h" + +#if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID) + +void +switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups) +{ + int serrno = errno; + debug_decl(switch_user, SUDO_DEBUG_EDIT); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "set uid:gid to %u:%u(%u)", (unsigned int)euid, (unsigned int)egid, + ngroups ? (unsigned int)groups[0] : (unsigned int)egid); + + /* When restoring root, change euid first; otherwise change it last. */ + if (euid == ROOT_UID) { + if (seteuid(ROOT_UID) != 0) + sudo_fatal("seteuid(ROOT_UID)"); + } + if (setegid(egid) != 0) + sudo_fatal("setegid(%d)", (int)egid); + if (ngroups != -1) { + if (sudo_setgroups(ngroups, groups) != 0) + sudo_fatal("setgroups"); + } + if (euid != ROOT_UID) { + if (seteuid(euid) != 0) + sudo_fatal("seteuid(%u)", (unsigned int)euid); + } + errno = serrno; + + debug_return; +} + +#if defined(HAVE_FACCESSAT) && defined(AT_EACCESS) +/* + * Returns true if the open directory fd is owned or writable by the user. + */ +int +dir_is_writable(int dfd, struct user_details *ud, struct command_details *cd) +{ + struct stat sb; + int rc; + debug_decl(dir_is_writable, SUDO_DEBUG_EDIT); + + if (fstat(dfd, &sb) == -1) + debug_return_int(-1); + + /* If the user owns the dir we always consider it writable. */ + if (sb.st_uid == ud->uid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user uid %u matches directory uid %u", (unsigned int)ud->uid, + (unsigned int)sb.st_uid); + debug_return_int(true); + } + + /* Change uid/gid/groups to invoking user, usually needs root perms. */ + if (cd->euid != ROOT_UID) { + if (seteuid(ROOT_UID) != 0) + sudo_fatal("seteuid(ROOT_UID)"); + } + switch_user(ud->uid, ud->gid, ud->ngroups, ud->groups); + + /* Access checks are done using the euid/egid and group vector. */ + rc = faccessat(dfd, ".", W_OK, AT_EACCESS); + + /* Change uid/gid/groups back to target user, may need root perms. */ + if (ud->uid != ROOT_UID) { + if (seteuid(ROOT_UID) != 0) + sudo_fatal("seteuid(ROOT_UID)"); + } + switch_user(cd->euid, cd->egid, cd->ngroups, cd->groups); + + if (rc == 0) + debug_return_int(true); + if (errno == EACCES || errno == EROFS) + debug_return_int(false); + debug_return_int(-1); +} +#else +static bool +group_matches(gid_t target, gid_t gid, int ngroups, GETGROUPS_T *groups) +{ + int i; + debug_decl(group_matches, SUDO_DEBUG_EDIT); + + if (target == gid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user gid %u matches directory gid %u", (unsigned int)gid, + (unsigned int)target); + debug_return_bool(true); + } + for (i = 0; i < ngroups; i++) { + if (target == groups[i]) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user gid %u matches directory gid %u", (unsigned int)gid, + (unsigned int)target); + debug_return_bool(true); + } + } + debug_return_bool(false); +} + +/* + * Returns true if the open directory fd is owned or writable by the user. + */ +int +dir_is_writable(int dfd, struct user_details *ud, struct command_details *cd) +{ + struct stat sb; + debug_decl(dir_is_writable, SUDO_DEBUG_EDIT); + + if (fstat(dfd, &sb) == -1) + debug_return_int(-1); + + /* If the user owns the dir we always consider it writable. */ + if (sb.st_uid == ud->uid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user uid %u matches directory uid %u", (unsigned int)ud->uid, + (unsigned int)sb.st_uid); + debug_return_int(true); + } + + /* Other writable? */ + if (ISSET(sb.st_mode, S_IWOTH)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "directory is writable by other"); + debug_return_int(true); + } + + /* Group writable? */ + if (ISSET(sb.st_mode, S_IWGRP)) { + if (group_matches(sb.st_gid, ud->gid, ud->ngroups, ud->groups)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "directory is writable by one of the user's groups"); + debug_return_int(true); + } + } + + errno = EACCES; + debug_return_int(false); +} +#endif /* HAVE_FACCESSAT && AT_EACCESS */ + +#ifdef O_NOFOLLOW +static int +sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode) +{ + debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT); + + debug_return_int(openat(dfd, path, oflags|O_NOFOLLOW, mode)); +} +#else +/* + * Returns true if fd and path don't match or path is a symlink. + * Used on older systems without O_NOFOLLOW. + */ +static bool +sudo_edit_is_symlink(int fd, char *path) +{ + struct stat sb1, sb2; + debug_decl(sudo_edit_is_symlink, SUDO_DEBUG_EDIT); + + /* + * Treat [fl]stat() failure like there was a symlink. + */ + if (fstat(fd, &sb1) == -1 || lstat(path, &sb2) == -1) + debug_return_bool(true); + + /* + * Make sure we did not open a link and that what we opened + * matches what is currently on the file system. + */ + if (S_ISLNK(sb2.st_mode) || + sb1.st_dev != sb2.st_dev || sb1.st_ino != sb2.st_ino) { + debug_return_bool(true); + } + + debug_return_bool(false); +} + +static int +sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode) +{ + int fd = -1, odfd = -1; + struct stat sb; + debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT); + + /* Save cwd and chdir to dfd */ + if ((odfd = open(".", O_RDONLY)) == -1) + debug_return_int(-1); + if (fchdir(dfd) == -1) { + close(odfd); + debug_return_int(-1); + } + + /* + * Check if path is a symlink. This is racey but we detect whether + * we lost the race in sudo_edit_is_symlink() after the open. + */ + if (lstat(path, &sb) == -1 && errno != ENOENT) + goto done; + if (S_ISLNK(sb.st_mode)) { + errno = ELOOP; + goto done; + } + + fd = open(path, oflags, mode); + if (fd == -1) + goto done; + + /* + * Post-open symlink check. This will leave a zero-length file if + * O_CREAT was specified but it is too dangerous to try and remove it. + */ + if (sudo_edit_is_symlink(fd, path)) { + close(fd); + fd = -1; + errno = ELOOP; + } + +done: + /* Restore cwd */ + if (odfd != -1) { + if (fchdir(odfd) == -1) + sudo_fatal("%s", U_("unable to restore current working directory")); + close(odfd); + } + + debug_return_int(fd); +} +#endif /* O_NOFOLLOW */ + +static int +sudo_edit_open_nonwritable(char *path, int oflags, mode_t mode, + struct user_details *ud, struct command_details *cd) +{ + const int dflags = DIR_OPEN_FLAGS; + int dfd, fd, is_writable; + debug_decl(sudo_edit_open_nonwritable, SUDO_DEBUG_EDIT); + + if (path[0] == '/') { + dfd = open("/", dflags); + path++; + } else { + dfd = open(".", dflags); + if (path[0] == '.' && path[1] == '/') + path += 2; + } + if (dfd == -1) + debug_return_int(-1); + + for (;;) { + char *slash; + int subdfd; + + /* + * Look up one component at a time, avoiding symbolic links in + * writable directories. + */ + is_writable = dir_is_writable(dfd, ud, cd); + if (is_writable == -1) { + close(dfd); + debug_return_int(-1); + } + + path += strspn(path, "/"); + slash = strchr(path, '/'); + if (slash == NULL) + break; + *slash = '\0'; + if (is_writable) + subdfd = sudo_edit_openat_nofollow(dfd, path, dflags, 0); + else + subdfd = openat(dfd, path, dflags, 0); + *slash = '/'; /* restore path */ + close(dfd); + if (subdfd == -1) + debug_return_int(-1); + path = slash + 1; + dfd = subdfd; + } + + if (is_writable) { + close(dfd); + errno = EISDIR; + debug_return_int(-1); + } + + /* + * For "sudoedit /" we will receive ENOENT from openat() and sudoedit + * will try to create a file with an empty name. We treat an empty + * path as the cwd so sudoedit can give a sensible error message. + */ + fd = openat(dfd, *path ? path : ".", oflags, mode); + close(dfd); + debug_return_int(fd); +} + +#ifdef O_NOFOLLOW +int +sudo_edit_open(char *path, int oflags, mode_t mode, struct user_details *ud, + struct command_details *cd) +{ + const int sflags = cd ? cd->flags : 0; + int fd; + debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT); + + if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) + oflags |= O_NOFOLLOW; + if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && ud->uid != ROOT_UID) { + fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, ud, cd); + } else { + fd = open(path, oflags|O_NONBLOCK, mode); + } + if (fd != -1 && !ISSET(oflags, O_NONBLOCK)) + (void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); + debug_return_int(fd); +} +#else +int +sudo_edit_open(char *path, int oflags, mode_t mode, struct user_details *ud, + struct command_details *cd) +{ + const int sflags = cd ? cd->flags : 0; + struct stat sb; + int fd; + debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT); + + /* + * Check if path is a symlink. This is racey but we detect whether + * we lost the race in sudo_edit_is_symlink() after the file is opened. + */ + if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) { + if (lstat(path, &sb) == -1 && errno != ENOENT) + debug_return_int(-1); + if (S_ISLNK(sb.st_mode)) { + errno = ELOOP; + debug_return_int(-1); + } + } + + if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && ud->uid != ROOT_UID) { + fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, ud, cd); + } else { + fd = open(path, oflags|O_NONBLOCK, mode); + } + if (fd == -1) + debug_return_int(-1); + if (!ISSET(oflags, O_NONBLOCK)) + (void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); + + /* + * Post-open symlink check. This will leave a zero-length file if + * O_CREAT was specified but it is too dangerous to try and remove it. + */ + if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW) && sudo_edit_is_symlink(fd, path)) { + close(fd); + fd = -1; + errno = ELOOP; + } + + debug_return_int(fd); +} +#endif /* O_NOFOLLOW */ + +#endif /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */ diff --git a/src/sesh.c b/src/sesh.c index abbef2577..c2bf29ac3 100644 --- a/src/sesh.c +++ b/src/sesh.c @@ -42,6 +42,7 @@ #include "sudo_compat.h" #include "sudo_conf.h" #include "sudo_debug.h" +#include "sudo_edit.h" #include "sudo_exec.h" #include "sudo_fatal.h" #include "sudo_gettext.h" diff --git a/src/sudo_edit.c b/src/sudo_edit.c index ba8e99c12..4e0a38dc0 100644 --- a/src/sudo_edit.c +++ b/src/sudo_edit.c @@ -31,7 +31,6 @@ #include #include #include -#include #include #include #include @@ -39,6 +38,7 @@ #include #include "sudo.h" +#include "sudo_edit.h" #include "sudo_exec.h" #if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID) @@ -55,146 +55,6 @@ struct tempfile { static char edit_tmpdir[MAX(sizeof(_PATH_VARTMP), sizeof(_PATH_TMP))]; -static void -switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups) -{ - int serrno = errno; - debug_decl(switch_user, SUDO_DEBUG_EDIT); - - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "set uid:gid to %u:%u(%u)", (unsigned int)euid, (unsigned int)egid, - ngroups ? (unsigned int)groups[0] : (unsigned int)egid); - - /* When restoring root, change euid first; otherwise change it last. */ - if (euid == ROOT_UID) { - if (seteuid(ROOT_UID) != 0) - sudo_fatal("seteuid(ROOT_UID)"); - } - if (setegid(egid) != 0) - sudo_fatal("setegid(%d)", (int)egid); - if (ngroups != -1) { - if (sudo_setgroups(ngroups, groups) != 0) - sudo_fatal("setgroups"); - } - if (euid != ROOT_UID) { - if (seteuid(euid) != 0) - sudo_fatal("seteuid(%u)", (unsigned int)euid); - } - errno = serrno; - - debug_return; -} - -#if defined(HAVE_FACCESSAT) && defined(AT_EACCESS) -/* - * Returns true if the open directory fd is owned or writable by the user. - */ -static int -dir_is_writable(int dfd, struct user_details *ud, struct command_details *cd) -{ - struct stat sb; - int rc; - debug_decl(dir_is_writable, SUDO_DEBUG_EDIT); - - if (fstat(dfd, &sb) == -1) - debug_return_int(-1); - - /* If the user owns the dir we always consider it writable. */ - if (sb.st_uid == ud->uid) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "user uid %u matches directory uid %u", (unsigned int)ud->uid, - (unsigned int)sb.st_uid); - debug_return_int(true); - } - - /* Change uid/gid/groups to invoking user, usually needs root perms. */ - if (cd->euid != ROOT_UID) { - if (seteuid(ROOT_UID) != 0) - sudo_fatal("seteuid(ROOT_UID)"); - } - switch_user(ud->uid, ud->gid, ud->ngroups, ud->groups); - - /* Access checks are done using the euid/egid and group vector. */ - rc = faccessat(dfd, ".", W_OK, AT_EACCESS); - - /* Change uid/gid/groups back to target user, may need root perms. */ - if (ud->uid != ROOT_UID) { - if (seteuid(ROOT_UID) != 0) - sudo_fatal("seteuid(ROOT_UID)"); - } - switch_user(cd->euid, cd->egid, cd->ngroups, cd->groups); - - if (rc == 0) - debug_return_int(true); - if (errno == EACCES || errno == EROFS) - debug_return_int(false); - debug_return_int(-1); -} -#else -static bool -group_matches(gid_t target, gid_t gid, int ngroups, GETGROUPS_T *groups) -{ - int i; - debug_decl(group_matches, SUDO_DEBUG_EDIT); - - if (target == gid) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "user gid %u matches directory gid %u", (unsigned int)gid, - (unsigned int)target); - debug_return_bool(true); - } - for (i = 0; i < ngroups; i++) { - if (target == groups[i]) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "user gid %u matches directory gid %u", (unsigned int)gid, - (unsigned int)target); - debug_return_bool(true); - } - } - debug_return_bool(false); -} - -/* - * Returns true if the open directory fd is owned or writable by the user. - */ -static int -dir_is_writable(int dfd, struct user_details *ud, struct command_details *cd) -{ - struct stat sb; - debug_decl(dir_is_writable, SUDO_DEBUG_EDIT); - - if (fstat(dfd, &sb) == -1) - debug_return_int(-1); - - /* If the user owns the dir we always consider it writable. */ - if (sb.st_uid == ud->uid) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "user uid %u matches directory uid %u", (unsigned int)ud->uid, - (unsigned int)sb.st_uid); - debug_return_int(true); - } - - /* Other writable? */ - if (ISSET(sb.st_mode, S_IWOTH)) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "directory is writable by other"); - debug_return_int(true); - } - - /* Group writable? */ - if (ISSET(sb.st_mode, S_IWGRP)) { - if (group_matches(sb.st_gid, ud->gid, ud->ngroups, ud->groups)) { - sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, - "directory is writable by one of the user's groups"); - debug_return_int(true); - } - } - - errno = EACCES; - debug_return_int(false); -} -#endif /* HAVE_FACCESSAT && AT_EACCESS */ - /* * Find our temporary directory, one of /var/tmp, /usr/tmp, or /tmp * Returns true on success, else false; @@ -269,253 +129,6 @@ sudo_edit_mktemp(const char *ofile, char **tfile) debug_return_int(tfd); } -#ifdef O_NOFOLLOW -static int -sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode) -{ - debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT); - - debug_return_int(openat(dfd, path, oflags|O_NOFOLLOW, mode)); -} -#else -/* - * Returns true if fd and path don't match or path is a symlink. - * Used on older systems without O_NOFOLLOW. - */ -static bool -sudo_edit_is_symlink(int fd, char *path) -{ - struct stat sb1, sb2; - debug_decl(sudo_edit_is_symlink, SUDO_DEBUG_EDIT); - - /* - * Treat [fl]stat() failure like there was a symlink. - */ - if (fstat(fd, &sb1) == -1 || lstat(path, &sb2) == -1) - debug_return_bool(true); - - /* - * Make sure we did not open a link and that what we opened - * matches what is currently on the file system. - */ - if (S_ISLNK(sb2.st_mode) || - sb1.st_dev != sb2.st_dev || sb1.st_ino != sb2.st_ino) { - debug_return_bool(true); - } - - debug_return_bool(false); -} - -static int -sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode) -{ - int fd = -1, odfd = -1; - struct stat sb; - debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT); - - /* Save cwd and chdir to dfd */ - if ((odfd = open(".", O_RDONLY)) == -1) - debug_return_int(-1); - if (fchdir(dfd) == -1) { - close(odfd); - debug_return_int(-1); - } - - /* - * Check if path is a symlink. This is racey but we detect whether - * we lost the race in sudo_edit_is_symlink() after the open. - */ - if (lstat(path, &sb) == -1 && errno != ENOENT) - goto done; - if (S_ISLNK(sb.st_mode)) { - errno = ELOOP; - goto done; - } - - fd = open(path, oflags, mode); - if (fd == -1) - goto done; - - /* - * Post-open symlink check. This will leave a zero-length file if - * O_CREAT was specified but it is too dangerous to try and remove it. - */ - if (sudo_edit_is_symlink(fd, path)) { - close(fd); - fd = -1; - errno = ELOOP; - } - -done: - /* Restore cwd */ - if (odfd != -1) { - if (fchdir(odfd) == -1) - sudo_fatal("%s", U_("unable to restore current working directory")); - close(odfd); - } - - debug_return_int(fd); -} -#endif /* O_NOFOLLOW */ - -/* - * Directory open flags for use with openat(2). - * Use O_SEARCH/O_PATH and/or O_DIRECTORY where possible. - */ -#if defined(O_SEARCH) -# if defined(O_DIRECTORY) -# define DIR_OPEN_FLAGS (O_SEARCH|O_DIRECTORY) -# else -# define DIR_OPEN_FLAGS (O_SEARCH) -# endif -#elif defined(O_PATH) -# if defined(O_DIRECTORY) -# define DIR_OPEN_FLAGS (O_PATH|O_DIRECTORY) -# else -# define DIR_OPEN_FLAGS (O_PATH) -# endif -#elif defined(O_DIRECTORY) -# define DIR_OPEN_FLAGS (O_RDONLY|O_DIRECTORY) -#else -# define DIR_OPEN_FLAGS (O_RDONLY|O_NONBLOCK) -#endif - -static int -sudo_edit_open_nonwritable(char *path, int oflags, mode_t mode, - struct command_details *command_details) -{ - const int dflags = DIR_OPEN_FLAGS; - int dfd, fd, is_writable; - debug_decl(sudo_edit_open_nonwritable, SUDO_DEBUG_EDIT); - - if (path[0] == '/') { - dfd = open("/", dflags); - path++; - } else { - dfd = open(".", dflags); - if (path[0] == '.' && path[1] == '/') - path += 2; - } - if (dfd == -1) - debug_return_int(-1); - - for (;;) { - char *slash; - int subdfd; - - /* - * Look up one component at a time, avoiding symbolic links in - * writable directories. - */ - is_writable = dir_is_writable(dfd, &user_details, command_details); - if (is_writable == -1) { - close(dfd); - debug_return_int(-1); - } - - while (path[0] == '/') - path++; - slash = strchr(path, '/'); - if (slash == NULL) - break; - *slash = '\0'; - if (is_writable) - subdfd = sudo_edit_openat_nofollow(dfd, path, dflags, 0); - else - subdfd = openat(dfd, path, dflags, 0); - *slash = '/'; /* restore path */ - close(dfd); - if (subdfd == -1) - debug_return_int(-1); - path = slash + 1; - dfd = subdfd; - } - - if (is_writable) { - close(dfd); - errno = EISDIR; - debug_return_int(-1); - } - - /* - * For "sudoedit /" we will receive ENOENT from openat() and sudoedit - * will try to create a file with an empty name. We treat an empty - * path as the cwd so sudoedit can give a sensible error message. - */ - fd = openat(dfd, *path ? path : ".", oflags, mode); - close(dfd); - debug_return_int(fd); -} - -#ifdef O_NOFOLLOW -static int -sudo_edit_open(char *path, int oflags, mode_t mode, - struct command_details *command_details) -{ - const int sflags = command_details ? command_details->flags : 0; - int fd; - debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT); - - if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) - oflags |= O_NOFOLLOW; - if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_details.uid != ROOT_UID) { - fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, - command_details); - } else { - fd = open(path, oflags|O_NONBLOCK, mode); - } - if (fd != -1 && !ISSET(oflags, O_NONBLOCK)) - (void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); - debug_return_int(fd); -} -#else -static int -sudo_edit_open(char *path, int oflags, mode_t mode, - struct command_details *command_details) -{ - const int sflags = command_details ? command_details->flags : 0; - struct stat sb; - int fd; - debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT); - - /* - * Check if path is a symlink. This is racey but we detect whether - * we lost the race in sudo_edit_is_symlink() after the file is opened. - */ - if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) { - if (lstat(path, &sb) == -1 && errno != ENOENT) - debug_return_int(-1); - if (S_ISLNK(sb.st_mode)) { - errno = ELOOP; - debug_return_int(-1); - } - } - - if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_details.uid != ROOT_UID) { - fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, - command_details); - } else { - fd = open(path, oflags|O_NONBLOCK, mode); - } - if (fd == -1) - debug_return_int(-1); - if (!ISSET(oflags, O_NONBLOCK)) - (void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); - - /* - * Post-open symlink check. This will leave a zero-length file if - * O_CREAT was specified but it is too dangerous to try and remove it. - */ - if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW) && sudo_edit_is_symlink(fd, path)) { - close(fd); - fd = -1; - errno = ELOOP; - } - - debug_return_int(fd); -} -#endif /* O_NOFOLLOW */ - /* * Create temporary copies of files[] and store the temporary path name * along with the original name, size and mtime in tf. @@ -540,7 +153,7 @@ sudo_edit_create_tfiles(struct command_details *command_details, switch_user(command_details->euid, command_details->egid, command_details->ngroups, command_details->groups); ofd = sudo_edit_open(files[i], O_RDONLY, - S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, command_details); + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, &user_details, command_details); if (ofd != -1 || errno == ENOENT) { if (ofd == -1) { /* @@ -562,7 +175,8 @@ sudo_edit_create_tfiles(struct command_details *command_details, *slash = '\0'; SET(command_details->flags, CD_SUDOEDIT_FOLLOW); dfd = sudo_edit_open(files[i], DIR_OPEN_FLAGS, - S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, command_details); + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, &user_details, + command_details); command_details->flags = sflags; if (dfd != -1) { if (fstat(dfd, &sb) == 0 && S_ISDIR(sb.st_mode)) { @@ -672,7 +286,7 @@ sudo_edit_copy_tfiles(struct command_details *command_details, if (seteuid(user_details.uid) != 0) sudo_fatal("seteuid(%u)", (unsigned int)user_details.uid); tfd = sudo_edit_open(tf[i].tfile, O_RDONLY, - S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, NULL); + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, &user_details, NULL); if (seteuid(ROOT_UID) != 0) sudo_fatal("seteuid(ROOT_UID)"); sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, @@ -701,7 +315,7 @@ sudo_edit_copy_tfiles(struct command_details *command_details, command_details->ngroups, command_details->groups); oldmask = umask(command_details->umask); ofd = sudo_edit_open(tf[i].ofile, O_WRONLY|O_CREAT, - S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, command_details); + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, &user_details, command_details); umask(oldmask); switch_user(ROOT_UID, user_details.egid, user_details.ngroups, user_details.groups); diff --git a/src/sudo_edit.h b/src/sudo_edit.h new file mode 100644 index 000000000..9a81dfd13 --- /dev/null +++ b/src/sudo_edit.h @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SUDO_EDIT_H +#define SUDO_EDIT_H + +/* + * Directory open flags for use with openat(2). + * Use O_SEARCH/O_PATH and/or O_DIRECTORY where possible. + */ +#if defined(O_SEARCH) +# if defined(O_DIRECTORY) +# define DIR_OPEN_FLAGS (O_SEARCH|O_DIRECTORY) +# else +# define DIR_OPEN_FLAGS (O_SEARCH) +# endif +#elif defined(O_PATH) +# if defined(O_DIRECTORY) +# define DIR_OPEN_FLAGS (O_PATH|O_DIRECTORY) +# else +# define DIR_OPEN_FLAGS (O_PATH) +# endif +#elif defined(O_DIRECTORY) +# define DIR_OPEN_FLAGS (O_RDONLY|O_DIRECTORY) +#else +# define DIR_OPEN_FLAGS (O_RDONLY|O_NONBLOCK) +#endif + +/* copy_file.c */ +int sudo_copy_file(const char *src, int src_fd, off_t src_len, const char *dst, int dst_fd, off_t dst_len); +bool sudo_check_temp_file(int tfd, const char *tname, uid_t uid, struct stat *sb); + +/* edit_open.c */ +void switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups); +int sudo_edit_open(char *path, int oflags, mode_t mode, struct user_details *ud, struct command_details *cd); +int dir_is_writable(int dfd, struct user_details *ud, struct command_details *cd); + +#endif /* SUDO_EDIT_H */ diff --git a/src/sudo_exec.h b/src/sudo_exec.h index ec213a6f0..1e086d2cc 100644 --- a/src/sudo_exec.h +++ b/src/sudo_exec.h @@ -86,10 +86,6 @@ struct command_details; struct command_status; struct stat; -/* copy_file.c */ -int sudo_copy_file(const char *src, int src_fd, off_t src_len, const char *dst, int dst_fd, off_t dst_len); -bool sudo_check_temp_file(int tfd, const char *tname, uid_t uid, struct stat *sb); - /* exec.c */ void exec_cmnd(struct command_details *details, int errfd); void terminate_command(pid_t pid, bool use_pgrp);