diff options
Diffstat (limited to '')
37 files changed, 15385 insertions, 0 deletions
diff --git a/src/Makefile.in b/src/Makefile.in new file mode 100644 index 0000000..fb49d67 --- /dev/null +++ b/src/Makefile.in @@ -0,0 +1,818 @@ +# +# SPDX-License-Identifier: ISC +# +# 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 +# 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. +# +# @configure_input@ +# + +#### Start of system configuration section. #### + +srcdir = @srcdir@ +abs_srcdir = @abs_srcdir@ +top_srcdir = @top_srcdir@ +abs_top_srcdir = @abs_top_srcdir@ +top_builddir = @top_builddir@ +abs_top_builddir = @abs_top_builddir@ +devdir = @devdir@ +scriptdir = $(top_srcdir)/scripts +incdir = $(top_srcdir)/include +rundir = @rundir@ +cross_compiling = @CROSS_COMPILING@ + +# Compiler & tools to use +CC = @CC@ +LIBTOOL = @LIBTOOL@ +SED = @SED@ + +# Our install program supports extra flags... +INSTALL = $(SHELL) $(top_srcdir)/install-sh -c +INSTALL_OWNER = -o $(install_uid) -g $(install_gid) +INSTALL_BACKUP = @INSTALL_BACKUP@ + +# Libraries +LT_LIBS = $(top_builddir)/lib/util/libsudo_util.la +LIBS = @LIBS@ @SUDO_LIBS@ @GETGROUPS_LIB@ @NET_LIBS@ $(LT_LIBS) + +# C preprocessor defines +CPPDEFS = -D_PATH_SUDO_CONF=\"$(sysconfdir)/sudo.conf\" \ + -DLOCALEDIR=\"$(localedir)\" + +# C preprocessor flags +CPPFLAGS = -I$(incdir) -I$(top_builddir) -I. -I$(srcdir) $(CPPDEFS) @CPPFLAGS@ + +# Usually -O and/or -g +CFLAGS = @CFLAGS@ + +# Flags to pass to the link stage +LDFLAGS = @LDFLAGS@ +SUDO_LDFLAGS = $(LDFLAGS) @SUDO_LDFLAGS@ +LT_LDFLAGS = @LT_LDFLAGS@ + +# Flags to pass to libtool +LTFLAGS = --tag=disable-static + +# Address sanitizer flags +ASAN_CFLAGS = @ASAN_CFLAGS@ +ASAN_LDFLAGS = @ASAN_LDFLAGS@ + +# PIE flags +PIE_CFLAGS = @PIE_CFLAGS@ +PIE_LDFLAGS = @PIE_LDFLAGS@ + +# Stack smashing protection flags +SSP_CFLAGS = @SSP_CFLAGS@ +SSP_LDFLAGS = @SSP_LDFLAGS@ + +# cppcheck options, usually set in the top-level Makefile +CPPCHECK_OPTS = -q --enable=warning,performance,portability --suppress=constStatement --suppress=compareBoolExpressionWithInt --error-exitcode=1 --inline-suppr -Dva_copy=va_copy -U__cplusplus -UQUAD_MAX -UQUAD_MIN -UUQUAD_MAX -U_POSIX_HOST_NAME_MAX -U_POSIX_PATH_MAX -U__NBBY -DNSIG=64 + +# splint options, usually set in the top-level Makefile +SPLINT_OPTS = -D__restrict= -checks + +# PVS-studio options +PVS_CFG = $(top_srcdir)/PVS-Studio.cfg +PVS_IGNORE = 'V707,V011,V002,V536' +PVS_LOG_OPTS = -a 'GA:1,2' -e -t errorfile -d $(PVS_IGNORE) + +# Where to install things... +prefix = @prefix@ +exec_prefix = @exec_prefix@ +bindir = @bindir@ +sbindir = @sbindir@ +sysconfdir = @sysconfdir@ +libexecdir = @libexecdir@ +datarootdir = @datarootdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +noexecfile = @NOEXECFILE@ +noexecdir = @NOEXECDIR@ +tmpfiles_d = @TMPFILES_D@ + +# User and group ids the installed files should be "owned" by +install_uid = 0 +install_gid = 0 + +# File mode to use for shared libraries +shlib_mode = @SHLIB_MODE@ + +# Optional init script and rc.d link +INIT_DIR=@INIT_DIR@ +INIT_SCRIPT=@INIT_SCRIPT@ +RC_LINK=@RC_LINK@ + +TEST_PROGS = check_ttyname @CHECK_NOEXEC@ +TEST_LIBS = @LIBS@ $(LT_LIBS) +TEST_LDFLAGS = @LDFLAGS@ + +#### End of system configuration section. #### + +SHELL = @SHELL@ + +PROGS = @PROGS@ + +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@ + +IOBJS = $(OBJS:.o=.i) sesh.i + +POBJS = $(IOBJS:.i=.plog) + +SESH_OBJS = copy_file.o edit_open.o exec_common.o sesh.o + +CHECK_NOEXEC_OBJS = check_noexec.o exec_common.o + +CHECK_TTYNAME_OBJS = check_ttyname.o ttyname.o + +LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/ + +VERSION = @PACKAGE_VERSION@ + +all: $(PROGS) + +depend: + $(scriptdir)/mkdep.pl --srcdir=$(abs_top_srcdir) \ + --builddir=$(abs_top_builddir) src/Makefile.in + cd $(top_builddir) && ./config.status --file src/Makefile + +Makefile: $(srcdir)/Makefile.in + cd $(top_builddir) && ./config.status --file src/Makefile + +./sudo_usage.h: $(srcdir)/sudo_usage.h.in + cd $(top_builddir) && ./config.status --file src/sudo_usage.h + +.SUFFIXES: .c .h .i .lo .o .plog + +.c.o: + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $< + +.c.lo: + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $< + +.c.i: + $(CC) -E -o $@ $(CPPFLAGS) $< + +.i.plog: + ifile=$<; rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $${ifile%i}c --i-file $< --output-file $@ + +sudo: $(OBJS) $(LT_LIBS) @STATIC_SUDOERS@ + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(OBJS) $(SUDO_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS) @STATIC_SUDOERS@ + +# We can't use -module here since you cannot preload a module on Darwin +libsudo_noexec.la: sudo_noexec.lo + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) $(LDFLAGS) $(LT_LDFLAGS) $(SSP_LDFLAGS) @LIBDL@ -o $@ sudo_noexec.lo -avoid-version -rpath $(noexecdir) -shrext .so + +# Some hackery is required to install this as sudo_noexec, not libsudo_noexec +sudo_noexec.la: libsudo_noexec.la + sed 's/libsudo_noexec/sudo_noexec/g' libsudo_noexec.la > sudo_noexec.la + if test -f .libs/libsudo_noexec.lai; then sed 's/libsudo_noexec/sudo_noexec/g' .libs/libsudo_noexec.lai > .libs/sudo_noexec.lai; fi + cp -p .libs/libsudo_noexec.so .libs/sudo_noexec.so + +sesh: $(SESH_OBJS) $(LT_LIBS) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(SESH_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS) + +check_noexec: $(CHECK_NOEXEC_OBJS) $(top_builddir)/lib/util/libsudo_util.la sudo_noexec.la + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_NOEXEC_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(TEST_LIBS) + +check_ttyname: $(CHECK_TTYNAME_OBJS) $(top_builddir)/lib/util/libsudo_util.la + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_TTYNAME_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(TEST_LIBS) + +pre-install: + +install: install-binaries install-rc @INSTALL_NOEXEC@ + +install-dirs: + # We only create the rc.d dir when installing to the actual system dir + $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(bindir) \ + $(DESTDIR)$(libexecdir)/sudo $(DESTDIR)$(noexecdir) + if test -n "$(INIT_SCRIPT)"; then \ + $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(INIT_DIR); \ + if test -z "$(DESTDIR)"; then \ + $(SHELL) $(scriptdir)/mkinstalldirs \ + `echo $(RC_LINK) | $(SED) 's,/[^/]*$$,,'`; \ + fi; \ + elif test -n "$(tmpfiles_d)"; then \ + $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(tmpfiles_d); \ + fi + +install-rc: install-dirs + # We only create the rc.d link when installing to the actual system dir + if [ -n "$(INIT_SCRIPT)" ]; then \ + $(INSTALL) $(INSTALL_OWNER) -m 0755 $(top_builddir)/etc/init.d/$(INIT_SCRIPT) $(DESTDIR)$(INIT_DIR)/sudo; \ + if test -z "$(DESTDIR)"; then \ + rm -f $(RC_LINK); \ + ln -s $(INIT_DIR)/sudo $(RC_LINK); \ + fi; \ + elif test -n "$(tmpfiles_d)"; then \ + $(INSTALL) $(INSTALL_OWNER) -m 0644 $(top_builddir)/etc/init.d/sudo.conf $(DESTDIR)$(tmpfiles_d)/sudo.conf; \ + fi + +install-binaries: install-dirs $(PROGS) + INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m 04755 sudo $(DESTDIR)$(bindir)/sudo + rm -f $(DESTDIR)$(bindir)/sudoedit + ln -s sudo $(DESTDIR)$(bindir)/sudoedit + if [ -f sesh ]; then \ + INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m 0755 sesh $(DESTDIR)$(libexecdir)/sudo/sesh; \ + fi + +install-doc: + +install-includes: + +install-noexec: install-dirs sudo_noexec.la + INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m $(shlib_mode) sudo_noexec.la $(DESTDIR)$(noexecdir) + +install-plugin: + +uninstall: + -$(LIBTOOL) $(LTFLAGS) --mode=uninstall rm -f $(DESTDIR)$(noexecdir)/sudo_noexec.la + -rm -f $(DESTDIR)$(bindir)/sudo \ + $(DESTDIR)$(bindir)/sudoedit \ + $(DESTDIR)$(libexecdir)/sudo/sesh \ + $(DESTDIR)/usr/lib/tmpfiles.d/sudo.conf + -test -z "$(INSTALL_BACKUP)" || \ + rm -f $(DESTDIR)$(bindir)/sudo$(INSTALL_BACKUP) \ + $(DESTDIR)$(libexecdir)/sudo/sesh$(INSTALL_BACKUP) \ + $(DESTDIR)$(noexecdir)/sudo_noexec.so$(INSTALL_BACKUP) + -test -z "$(INIT_SCRIPT)" || \ + rm -f $(DESTDIR)$(RC_LINK) $(DESTDIR)$(INIT_DIR)/sudo + +splint: + splint $(SPLINT_OPTS) -I$(incdir) -I$(top_builddir) -I. -I$(srcdir) $(srcdir)/*.c + +cppcheck: + cppcheck $(CPPCHECK_OPTS) -I$(incdir) -I$(top_builddir) -I. -I$(srcdir) $(srcdir)/*.c + +pvs-log-files: $(POBJS) + +pvs-studio: $(POBJS) + plog-converter $(PVS_LOG_OPTS) $(POBJS) + +check: $(TEST_PROGS) + @if test X"$(cross_compiling)" != X"yes"; then \ + MALLOC_OPTIONS=S; export MALLOC_OPTIONS; \ + MALLOC_CONF="abort:true,junk:true"; export MALLOC_CONF; \ + ./check_ttyname; \ + if test X"@CHECK_NOEXEC@" != X""; then \ + ./check_noexec .libs/$(noexecfile); \ + fi; \ + fi + +clean: + -$(LIBTOOL) $(LTFLAGS) --mode=clean rm -f $(PROGS) $(TEST_PROGS) \ + *.lo *.o *.la + -rm -f *.i *.plog stamp-* core *.core core.* + +mostlyclean: clean + +distclean: clean + -rm -rf Makefile .libs sudo_usage.h + +clobber: distclean + +realclean: distclean + rm -f TAGS tags + +cleandir: realclean + +# *Not* auto-generated to avoid building with ASAN +sudo_noexec.lo: $(srcdir)/sudo_noexec.c $(incdir)/sudo_compat.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sudo_noexec.c + +# Autogenerated dependencies, do not modify +check_noexec.o: $(srcdir)/regress/noexec/check_noexec.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/noexec/check_noexec.c +check_noexec.i: $(srcdir)/regress/noexec/check_noexec.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +check_noexec.plog: check_noexec.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/noexec/check_noexec.c --i-file $< --output-file $@ +check_ttyname.o: $(srcdir)/regress/ttyname/check_ttyname.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/ttyname/check_ttyname.c +check_ttyname.i: $(srcdir)/regress/ttyname/check_ttyname.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +check_ttyname.plog: check_ttyname.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/ttyname/check_ttyname.c --i-file $< --output-file $@ +conversation.o: $(srcdir)/conversation.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/conversation.c +conversation.i: $(srcdir)/conversation.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +conversation.plog: conversation.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/conversation.c --i-file $< --output-file $@ +copy_file.o: $(srcdir)/copy_file.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/copy_file.c +copy_file.i: $(srcdir)/copy_file.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.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 \ + $(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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/env_hooks.c +env_hooks.i: $(srcdir)/env_hooks.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_dso.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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +env_hooks.plog: env_hooks.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/env_hooks.c --i-file $< --output-file $@ +exec.o: $(srcdir)/exec.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 $(srcdir)/sudo_plugin_int.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec.c +exec.i: $(srcdir)/exec.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 $(srcdir)/sudo_plugin_int.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +exec.plog: exec.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec.c --i-file $< --output-file $@ +exec_common.o: $(srcdir)/exec_common.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 + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec_common.c +exec_common.i: $(srcdir)/exec_common.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 + $(CC) -E -o $@ $(CPPFLAGS) $< +exec_common.plog: exec_common.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_common.c --i-file $< --output-file $@ +exec_monitor.o: $(srcdir)/exec_monitor.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 \ + $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec_monitor.c +exec_monitor.i: $(srcdir)/exec_monitor.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 \ + $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +exec_monitor.plog: exec_monitor.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_monitor.c --i-file $< --output-file $@ +exec_nopty.o: $(srcdir)/exec_nopty.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 \ + $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec_nopty.c +exec_nopty.i: $(srcdir)/exec_nopty.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 \ + $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +exec_nopty.plog: exec_nopty.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_nopty.c --i-file $< --output-file $@ +exec_pty.o: $(srcdir)/exec_pty.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 \ + $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec_pty.c +exec_pty.i: $(srcdir)/exec_pty.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 \ + $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +exec_pty.plog: exec_pty.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_pty.c --i-file $< --output-file $@ +get_pty.o: $(srcdir)/get_pty.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/get_pty.c +get_pty.i: $(srcdir)/get_pty.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +get_pty.plog: get_pty.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/get_pty.c --i-file $< --output-file $@ +hooks.o: $(srcdir)/hooks.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/hooks.c +hooks.i: $(srcdir)/hooks.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +hooks.plog: hooks.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/hooks.c --i-file $< --output-file $@ +limits.o: $(srcdir)/limits.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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/limits.c +limits.i: $(srcdir)/limits.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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +limits.plog: limits.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/limits.c --i-file $< --output-file $@ +load_plugins.o: $(srcdir)/load_plugins.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_dso.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/load_plugins.c +load_plugins.i: $(srcdir)/load_plugins.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_dso.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +load_plugins.plog: load_plugins.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/load_plugins.c --i-file $< --output-file $@ +net_ifs.o: $(srcdir)/net_ifs.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 $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/net_ifs.c +net_ifs.i: $(srcdir)/net_ifs.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 $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +net_ifs.plog: net_ifs.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/net_ifs.c --i-file $< --output-file $@ +openbsd.o: $(srcdir)/openbsd.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/openbsd.c +openbsd.i: $(srcdir)/openbsd.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +openbsd.plog: openbsd.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/openbsd.c --i-file $< --output-file $@ +parse_args.o: $(srcdir)/parse_args.c $(incdir)/compat/getopt.h \ + $(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_lbuf.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h ./sudo_usage.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/parse_args.c +parse_args.i: $(srcdir)/parse_args.c $(incdir)/compat/getopt.h \ + $(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_lbuf.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h ./sudo_usage.h + $(CC) -E -o $@ $(CPPFLAGS) $< +parse_args.plog: parse_args.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/parse_args.c --i-file $< --output-file $@ +preload.o: $(srcdir)/preload.c $(incdir)/sudo_compat.h $(incdir)/sudo_dso.h \ + $(incdir)/sudo_plugin.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/preload.c +preload.i: $(srcdir)/preload.c $(incdir)/sudo_compat.h $(incdir)/sudo_dso.h \ + $(incdir)/sudo_plugin.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +preload.plog: preload.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/preload.c --i-file $< --output-file $@ +preserve_fds.o: $(srcdir)/preserve_fds.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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/preserve_fds.c +preserve_fds.i: $(srcdir)/preserve_fds.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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +preserve_fds.plog: preserve_fds.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/preserve_fds.c --i-file $< --output-file $@ +selinux.o: $(srcdir)/selinux.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 + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/selinux.c +selinux.i: $(srcdir)/selinux.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 + $(CC) -E -o $@ $(CPPFLAGS) $< +selinux.plog: selinux.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/selinux.c --i-file $< --output-file $@ +sesh.o: $(srcdir)/sesh.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 $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.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_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 $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.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 $@ +signal.o: $(srcdir)/signal.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 + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/signal.c +signal.i: $(srcdir)/signal.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 + $(CC) -E -o $@ $(CPPFLAGS) $< +signal.plog: signal.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/signal.c --i-file $< --output-file $@ +solaris.o: $(srcdir)/solaris.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_dso.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/solaris.c +solaris.i: $(srcdir)/solaris.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_dso.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +solaris.plog: solaris.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/solaris.c --i-file $< --output-file $@ +sudo.o: $(srcdir)/sudo.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h ./sudo_usage.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sudo.c +sudo.i: $(srcdir)/sudo.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_plugin_int.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h ./sudo_usage.h + $(CC) -E -o $@ $(CPPFLAGS) $< +sudo.plog: sudo.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sudo.c --i-file $< --output-file $@ +sudo_edit.o: $(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_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_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 $@ +tcsetpgrp_nobg.o: $(srcdir)/tcsetpgrp_nobg.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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/tcsetpgrp_nobg.c +tcsetpgrp_nobg.i: $(srcdir)/tcsetpgrp_nobg.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 \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +tcsetpgrp_nobg.plog: tcsetpgrp_nobg.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/tcsetpgrp_nobg.c --i-file $< --output-file $@ +tgetpass.o: $(srcdir)/tgetpass.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/tgetpass.c +tgetpass.i: $(srcdir)/tgetpass.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +tgetpass.plog: tgetpass.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/tgetpass.c --i-file $< --output-file $@ +ttyname.o: $(srcdir)/ttyname.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/ttyname.c +ttyname.i: $(srcdir)/ttyname.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 $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +ttyname.plog: ttyname.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/ttyname.c --i-file $< --output-file $@ +utmp.o: $(srcdir)/utmp.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 + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/utmp.c +utmp.i: $(srcdir)/utmp.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 + $(CC) -E -o $@ $(CPPFLAGS) $< +utmp.plog: utmp.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/utmp.c --i-file $< --output-file $@ diff --git a/src/conversation.c b/src/conversation.c new file mode 100644 index 0000000..95ecadd --- /dev/null +++ b/src/conversation.c @@ -0,0 +1,191 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1999-2005, 2007-2012 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +/* + * 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 <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> + +#include "sudo.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" + +extern int tgetpass_flags; /* XXX */ + +/* + * Sudo conversation function. + */ +int +sudo_conversation(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + char *pass; + int n; + const int conv_debug_instance = sudo_debug_get_active_instance(); + + sudo_debug_set_active_instance(sudo_debug_instance); + + for (n = 0; n < num_msgs; n++) { + const struct sudo_conv_message *msg = &msgs[n]; + int flags = tgetpass_flags; + FILE *fp = stdout; + + switch (msg->msg_type & 0xff) { + case SUDO_CONV_PROMPT_ECHO_ON: + SET(flags, TGP_ECHO); + goto read_pass; + case SUDO_CONV_PROMPT_MASK: + SET(flags, TGP_MASK); + FALLTHROUGH; + case SUDO_CONV_PROMPT_ECHO_OFF: + if (ISSET(msg->msg_type, SUDO_CONV_PROMPT_ECHO_OK)) + SET(flags, TGP_NOECHO_TRY); + read_pass: + /* Read the password unless interrupted. */ + if (replies == NULL) + goto err; + pass = tgetpass(msg->msg, msg->timeout, flags, callback); + if (pass == NULL) + goto err; + replies[n].reply = strdup(pass); + if (replies[n].reply == NULL) { + sudo_fatalx_nodebug(U_("%s: %s"), "sudo_conversation", + U_("unable to allocate memory")); + } + explicit_bzero(pass, strlen(pass)); + break; + case SUDO_CONV_ERROR_MSG: + fp = stderr; + FALLTHROUGH; + case SUDO_CONV_INFO_MSG: + if (msg->msg != NULL) { + size_t len = strlen(msg->msg); + const char *crnl = NULL; + bool written = false; + int ttyfd = -1; + + if (ISSET(msg->msg_type, SUDO_CONV_PREFER_TTY) && + !ISSET(tgetpass_flags, TGP_STDIN)) + ttyfd = open(_PATH_TTY, O_WRONLY); + if (len != 0 && (ttyfd != -1 || isatty(fileno(fp)))) { + /* Convert nl -> cr nl in case tty is in raw mode. */ + if (msg->msg[len - 1] == '\n') { + if (len == 1 || msg->msg[len - 2] != '\r') { + len--; + crnl = "\r\n"; + } + } + } + if (ttyfd != -1) { + /* Try writing to tty but fall back to fp on error. */ + if ((len == 0 || write(ttyfd, msg->msg, len) != -1) && + (crnl == NULL || write(ttyfd, crnl, 2) != -1)) { + written = true; + } + close(ttyfd); + } + if (!written) { + if (len != 0 && fwrite(msg->msg, 1, len, fp) == 0) + goto err; + if (crnl != NULL && fwrite(crnl, 1, 2, fp) == 0) + goto err; + } + } + break; + default: + goto err; + } + } + + sudo_debug_set_active_instance(conv_debug_instance); + return 0; + +err: + /* Zero and free allocated memory and return an error. */ + if (replies != NULL) { + do { + struct sudo_conv_reply *repl = &replies[n]; + if (repl->reply == NULL) + continue; + freezero(repl->reply, strlen(repl->reply)); + repl->reply = NULL; + } while (n--); + } + + sudo_debug_set_active_instance(conv_debug_instance); + return -1; +} + +int +sudo_conversation_1_7(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[]) +{ + return sudo_conversation(num_msgs, msgs, replies, NULL); +} + +int +sudo_conversation_printf(int msg_type, const char *fmt, ...) +{ + FILE *fp = stdout; + FILE *ttyfp = NULL; + va_list ap; + int len; + const int conv_debug_instance = sudo_debug_get_active_instance(); + + sudo_debug_set_active_instance(sudo_debug_instance); + + if (ISSET(msg_type, SUDO_CONV_PREFER_TTY) && + !ISSET(tgetpass_flags, TGP_STDIN)) { + /* Try writing to /dev/tty first. */ + ttyfp = fopen(_PATH_TTY, "w"); + } + + switch (msg_type & 0xff) { + case SUDO_CONV_ERROR_MSG: + fp = stderr; + FALLTHROUGH; + case SUDO_CONV_INFO_MSG: + va_start(ap, fmt); + len = vfprintf(ttyfp ? ttyfp : fp, fmt, ap); + va_end(ap); + break; + default: + len = -1; + errno = EINVAL; + break; + } + + if (ttyfp != NULL) + fclose(ttyfp); + + sudo_debug_set_active_instance(conv_debug_instance); + return len; +} diff --git a/src/copy_file.c b/src/copy_file.c new file mode 100644 index 0000000..9404408 --- /dev/null +++ b/src/copy_file.c @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2020-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/stat.h> + +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> + +#include "sudo.h" + +/* + * Extend the given fd to the specified size in bytes. + * We do this to allocate disk space up-front before overwriting + * the original file with the temporary. Otherwise, we could + * we run out of disk space after truncating the original file. + */ +static int +sudo_extend_file(int fd, const char *name, off_t new_size) +{ + off_t old_size, size; + ssize_t nwritten; + char zeroes[BUFSIZ] = { '\0' }; + debug_decl(sudo_extend_file, SUDO_DEBUG_UTIL); + + if ((old_size = lseek(fd, 0, SEEK_END)) == -1) { + sudo_warn("lseek"); + debug_return_int(-1); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: extending %s from %lld to %lld", + __func__, name, (long long)old_size, (long long)new_size); + + for (size = old_size; size < new_size; size += nwritten) { + size_t len = new_size - size; + if (len > sizeof(zeroes)) + len = sizeof(zeroes); + nwritten = write(fd, zeroes, len); + if (nwritten == -1) { + int serrno = errno; + if (ftruncate(fd, old_size) == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to truncate %s to %lld", name, (long long)old_size); + } + errno = serrno; + debug_return_int(-1); + } + } + if (lseek(fd, 0, SEEK_SET) == -1) { + sudo_warn("lseek"); + debug_return_int(-1); + } + + debug_return_int(0); +} + +/* + * Copy the contents of src_fd into dst_fd. + * Returns 0 on success or -1 on error. + */ +int +sudo_copy_file(const char *src, int src_fd, off_t src_len, const char *dst, + int dst_fd, off_t dst_len) +{ + char buf[BUFSIZ]; + ssize_t nwritten, nread; + debug_decl(sudo_copy_file, SUDO_DEBUG_UTIL); + + /* Prompt the user before zeroing out an existing file. */ + if (dst_len > 0 && src_len == 0) { + fprintf(stderr, U_("%s: truncate %s to zero bytes? (y/n) [n] "), + getprogname(), dst); + if (fgets(buf, sizeof(buf), stdin) == NULL || + (buf[0] != 'y' && buf[0] != 'Y')) { + sudo_warnx(U_("not overwriting %s"), dst); + debug_return_int(0); + } + } + + /* Extend the file to the new size if larger before copying. */ + if (dst_len > 0 && src_len > dst_len) { + if (sudo_extend_file(dst_fd, dst, src_len) == -1) + goto write_error; + } + + /* Overwrite the old file with the new contents. */ + while ((nread = read(src_fd, buf, sizeof(buf))) > 0) { + ssize_t off = 0; + do { + nwritten = write(dst_fd, buf + off, nread - off); + if (nwritten == -1) + goto write_error; + off += nwritten; + } while (nread > off); + } + if (nread == -1) { + sudo_warn(U_("unable to read from %s"), src); + debug_return_int(-1); + } + + /* Did the file shrink? */ + if (src_len < dst_len) { + /* We don't open with O_TRUNC so must truncate manually. */ + if (ftruncate(dst_fd, src_len) == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to truncate %s to %lld", dst, (long long)src_len); + goto write_error; + } + } + + debug_return_int(0); +write_error: + sudo_warn(U_("unable to write to %s"), dst); + debug_return_int(-1); +} + +bool +sudo_check_temp_file(int tfd, const char *tfile, uid_t uid, struct stat *sb) +{ + struct stat sbuf; + debug_decl(sudo_check_temp_file, SUDO_DEBUG_UTIL); + + if (sb == NULL) + sb = &sbuf; + + if (fstat(tfd, sb) == -1) { + sudo_warn(U_("unable to stat %s"), tfile); + debug_return_bool(false); + } + if (!S_ISREG(sb->st_mode)) { + sudo_warnx(U_("%s: not a regular file"), tfile); + debug_return_bool(false); + } + if ((sb->st_mode & ALLPERMS) != (S_IRUSR|S_IWUSR)) { + sudo_warnx(U_("%s: bad file mode: 0%o"), tfile, + (unsigned int)(sb->st_mode & ALLPERMS)); + debug_return_bool(false); + } + if (sb->st_uid != uid) { + sudo_warnx(U_("%s is owned by uid %u, should be %u"), + tfile, (unsigned int)sb->st_uid, (unsigned int)uid); + debug_return_bool(false); + } + debug_return_bool(true); +} diff --git a/src/edit_open.c b/src/edit_open.c new file mode 100644 index 0000000..864dcea --- /dev/null +++ b/src/edit_open.c @@ -0,0 +1,456 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2015-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <fcntl.h> + +#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 > 0 ? (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 sudo_cred *user_cred, struct sudo_cred *cur_cred) +{ + 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 == user_cred->uid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user uid %u matches directory uid %u", + (unsigned int)user_cred->uid, (unsigned int)sb.st_uid); + debug_return_int(true); + } + + /* Change uid/gid/groups to invoking user, usually needs root perms. */ + if (cur_cred->euid != ROOT_UID) { + if (seteuid(ROOT_UID) != 0) + sudo_fatal("seteuid(ROOT_UID)"); + } + switch_user(user_cred->uid, user_cred->gid, user_cred->ngroups, + user_cred->groups); + + /* Access checks are done using the euid/egid and group vector. */ + rc = faccessat(dfd, ".", W_OK, AT_EACCESS); + + /* Restore uid/gid/groups, may need root perms. */ + if (user_cred->uid != ROOT_UID) { + if (seteuid(ROOT_UID) != 0) + sudo_fatal("seteuid(ROOT_UID)"); + } + switch_user(cur_cred->euid, cur_cred->egid, cur_cred->ngroups, + cur_cred->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, struct sudo_cred *cred) +{ + int i; + debug_decl(group_matches, SUDO_DEBUG_EDIT); + + if (target == cred->gid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user gid %u matches directory gid %u", (unsigned int)cred->gid, + (unsigned int)target); + debug_return_bool(true); + } + for (i = 0; i < cred->ngroups; i++) { + if (target == cred->groups[i]) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user gid %u matches directory gid %u", + (unsigned int)cred->groups[i], (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 sudo_cred *user_cred, struct sudo_cred *cur_cred) +{ + 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 == user_cred->uid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user uid %u matches directory uid %u", + (unsigned int)user_cred->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, user_cred)) { + 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 sudo_cred *user_cred, struct sudo_cred *cur_cred) +{ + 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_cred, cur_cred); + 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, int sflags, + struct sudo_cred *user_cred, struct sudo_cred *cur_cred) +{ + 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_cred->uid != ROOT_UID) { + fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, + user_cred, cur_cred); + } 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, int sflags, + struct sudo_cred *user_cred, struct sudo_cred *cur_cred) +{ + 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_cred->uid != ROOT_UID) { + fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, + user_cred, cur_cred); + } 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 */ + +/* + * Verify that the parent dir of a new file exists and is not writable + * by the user. This fails early so the user knows ahead of time if the + * edit won't succeed. Additional checks are performed when copying the + * temporary file back to the origin so there are no TOCTOU issues. + * Does not modify the value of errno. + */ +bool +sudo_edit_parent_valid(char *path, int sflags, struct sudo_cred *user_cred, + struct sudo_cred *cur_cred) +{ + const int serrno = errno; + struct stat sb; + bool ret = false; + char *slash; + int dfd; + debug_decl(sudo_edit_parent_valid, SUDO_DEBUG_EDIT); + + /* Get dirname of path (the slash is restored later). */ + slash = strrchr(path, '/'); + if (slash == NULL) { + /* cwd */ + path = "."; + } else if (slash == path) { + path = "/"; + slash = NULL; + } else { + *slash = '\0'; + } + + /* + * The parent directory is allowed to be a symbolic link unless + * *its* parent is writable and CD_SUDOEDIT_CHECK is set. + */ + dfd = sudo_edit_open(path, DIR_OPEN_FLAGS, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, + sflags|CD_SUDOEDIT_FOLLOW, user_cred, cur_cred); + if (dfd != -1) { + if (fstat(dfd, &sb) == 0 && S_ISDIR(sb.st_mode)) + ret = true; + close(dfd); + } + if (slash != NULL) + *slash = '/'; + + /* Restore errno. */ + errno = serrno; + + debug_return_bool(ret); +} + +#endif /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */ diff --git a/src/env_hooks.c b/src/env_hooks.c new file mode 100644 index 0000000..5f9a61c --- /dev/null +++ b/src/env_hooks.c @@ -0,0 +1,291 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2010, 2012-2016 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +#include "sudo.h" +#include "sudo_plugin.h" +#include "sudo_dso.h" + +extern char **environ; /* global environment pointer */ +static char **priv_environ; /* private environment pointer */ + +/* + * NOTE: we don't use dlsym() to find the libc getenv() + * since this may allocate memory on some systems (glibc) + * which leads to a hang if malloc() calls getenv (jemalloc). + */ +char * +getenv_unhooked(const char *name) +{ + char **ep, *val = NULL; + size_t namelen = 0; + + /* For BSD compatibility, treat '=' in name like end of string. */ + while (name[namelen] != '\0' && name[namelen] != '=') + namelen++; + for (ep = environ; *ep != NULL; ep++) { + if (strncmp(*ep, name, namelen) == 0 && (*ep)[namelen] == '=') { + val = *ep + namelen + 1; + break; + } + } + return val; +} + +sudo_dso_public char * +getenv(const char *name) +{ + char *val = NULL; + + switch (process_hooks_getenv(name, &val)) { + case SUDO_HOOK_RET_STOP: + return val; + case SUDO_HOOK_RET_ERROR: + return NULL; + default: + return getenv_unhooked(name); + } +} + +static int +rpl_putenv(PUTENV_CONST char *string) +{ + char **ep; + size_t len; + bool found = false; + + /* Look for existing entry. */ + len = (strchr(string, '=') - string) + 1; + for (ep = environ; *ep != NULL; ep++) { + if (strncmp(string, *ep, len) == 0) { + *ep = (char *)string; + found = true; + break; + } + } + /* Prune out duplicate variables. */ + if (found) { + while (*ep != NULL) { + if (strncmp(string, *ep, len) == 0) { + char **cur = ep; + while ((*cur = *(cur + 1)) != NULL) + cur++; + } else { + ep++; + } + } + } + + /* Append at the end if not already found. */ + if (!found) { + size_t env_len = (size_t)(ep - environ); + char **envp = reallocarray(priv_environ, env_len + 2, sizeof(char *)); + if (envp == NULL) + return -1; + if (environ != priv_environ) + memcpy(envp, environ, env_len * sizeof(char *)); + envp[env_len++] = (char *)string; + envp[env_len] = NULL; + priv_environ = environ = envp; + } + return 0; +} + +typedef int (*sudo_fn_putenv_t)(PUTENV_CONST char *); + +static int +putenv_unhooked(PUTENV_CONST char *string) +{ + sudo_fn_putenv_t fn; + + fn = (sudo_fn_putenv_t)sudo_dso_findsym(SUDO_DSO_NEXT, "putenv"); + if (fn != NULL) + return fn(string); + return rpl_putenv(string); +} + +sudo_dso_public int +putenv(PUTENV_CONST char *string) +{ + switch (process_hooks_putenv((char *)string)) { + case SUDO_HOOK_RET_STOP: + return 0; + case SUDO_HOOK_RET_ERROR: + return -1; + default: + return putenv_unhooked(string); + } +} + +static int +rpl_setenv(const char *var, const char *val, int overwrite) +{ + char *envstr, *dst; + const char *src; + size_t esize; + + if (!var || *var == '\0') { + errno = EINVAL; + return -1; + } + + /* + * POSIX says a var name with '=' is an error but BSD + * just ignores the '=' and anything after it. + */ + for (src = var; *src != '\0' && *src != '='; src++) + continue; + esize = (size_t)(src - var) + 2; + if (val) { + esize += strlen(val); /* glibc treats a NULL val as "" */ + } + + /* Allocate and fill in envstr. */ + if ((envstr = malloc(esize)) == NULL) + return -1; + for (src = var, dst = envstr; *src != '\0' && *src != '=';) + *dst++ = *src++; + *dst++ = '='; + if (val) { + for (src = val; *src != '\0';) + *dst++ = *src++; + } + *dst = '\0'; + + if (!overwrite && getenv(var) != NULL) { + free(envstr); + return 0; + } + if (rpl_putenv(envstr) == -1) { + free(envstr); + return -1; + } + return 0; +} + +typedef int (*sudo_fn_setenv_t)(const char *, const char *, int); + +static int +setenv_unhooked(const char *var, const char *val, int overwrite) +{ + sudo_fn_setenv_t fn; + + fn = (sudo_fn_setenv_t)sudo_dso_findsym(SUDO_DSO_NEXT, "setenv"); + if (fn != NULL) + return fn(var, val, overwrite); + return rpl_setenv(var, val, overwrite); +} + +sudo_dso_public int +setenv(const char *var, const char *val, int overwrite) +{ + switch (process_hooks_setenv(var, val, overwrite)) { + case SUDO_HOOK_RET_STOP: + return 0; + case SUDO_HOOK_RET_ERROR: + return -1; + default: + return setenv_unhooked(var, val, overwrite); + } +} + +static int +rpl_unsetenv(const char *var) +{ + char **ep = environ; + size_t len; + + if (var == NULL || *var == '\0' || strchr(var, '=') != NULL) { + errno = EINVAL; + return -1; + } + + len = strlen(var); + while (*ep != NULL) { + if (strncmp(var, *ep, len) == 0 && (*ep)[len] == '=') { + /* Found it; shift remainder + NULL over by one. */ + char **cur = ep; + while ((*cur = *(cur + 1)) != NULL) + cur++; + /* Keep going, could be multiple instances of the var. */ + } else { + ep++; + } + } + return 0; +} + +#ifdef UNSETENV_VOID +typedef void (*sudo_fn_unsetenv_t)(const char *); +#else +typedef int (*sudo_fn_unsetenv_t)(const char *); +#endif + +static int +unsetenv_unhooked(const char *var) +{ + int ret = 0; + sudo_fn_unsetenv_t fn; + + fn = (sudo_fn_unsetenv_t)sudo_dso_findsym(SUDO_DSO_NEXT, "unsetenv"); + if (fn != NULL) { +# ifdef UNSETENV_VOID + fn(var); +# else + ret = fn(var); +# endif + } else { + ret = rpl_unsetenv(var); + } + return ret; +} + +#ifdef UNSETENV_VOID +sudo_dso_public void +#else +sudo_dso_public int +#endif +unsetenv(const char *var) +{ + int ret; + + switch (process_hooks_unsetenv(var)) { + case SUDO_HOOK_RET_STOP: + ret = 0; + break; + case SUDO_HOOK_RET_ERROR: + ret = -1; + break; + default: + ret = unsetenv_unhooked(var); + break; + } +#ifndef UNSETENV_VOID + return ret; +#endif +} diff --git a/src/exec.c b/src/exec.c new file mode 100644 index 0000000..ffee016 --- /dev/null +++ b/src/exec.c @@ -0,0 +1,463 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/resource.h> +#include <sys/stat.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#ifdef HAVE_LOGIN_CAP_H +# include <login_cap.h> +# ifndef LOGIN_SETENV +# define LOGIN_SETENV 0 +# endif +#endif +#ifdef HAVE_PROJECT_H +# include <project.h> +# include <sys/task.h> +#endif + +#include "sudo.h" +#include "sudo_exec.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" + +static void +close_fds(struct command_details *details, int errfd) +{ + int fd, maxfd; + unsigned char *debug_fds; + debug_decl(close_fds, SUDO_DEBUG_EXEC); + + if (details->closefrom < 0) + debug_return; + + /* Preserve debug fds and error pipe as needed. */ + maxfd = sudo_debug_get_fds(&debug_fds); + for (fd = 0; fd <= maxfd; fd++) { + if (sudo_isset(debug_fds, fd)) + add_preserved_fd(&details->preserved_fds, fd); + } + if (errfd != -1) + add_preserved_fd(&details->preserved_fds, errfd); + + /* Close all fds except those explicitly preserved. */ + closefrom_except(details->closefrom, &details->preserved_fds); + + debug_return; +} + +/* + * Setup the execution environment immediately prior to the call to execve(). + * Group setup is performed by policy_init_session(), called earlier. + * Returns true on success and false on failure. + */ +static bool +exec_setup(struct command_details *details, int errfd) +{ + bool ret = false; + debug_decl(exec_setup, SUDO_DEBUG_EXEC); + + if (details->pw != NULL) { +#ifdef HAVE_PROJECT_H + set_project(details->pw); +#endif +#ifdef HAVE_PRIV_SET + if (details->privs != NULL) { + if (setppriv(PRIV_SET, PRIV_INHERITABLE, details->privs) != 0) { + sudo_warn("unable to set privileges"); + goto done; + } + } + if (details->limitprivs != NULL) { + if (setppriv(PRIV_SET, PRIV_LIMIT, details->limitprivs) != 0) { + sudo_warn("unable to set limit privileges"); + goto done; + } + } else if (details->privs != NULL) { + if (setppriv(PRIV_SET, PRIV_LIMIT, details->privs) != 0) { + sudo_warn("unable to set limit privileges"); + goto done; + } + } +#endif /* HAVE_PRIV_SET */ + +#ifdef HAVE_GETUSERATTR + if (aix_prep_user(details->pw->pw_name, details->tty) != 0) { + /* error message displayed by aix_prep_user */ + goto done; + } +#endif +#ifdef HAVE_LOGIN_CAP_H + if (details->login_class) { + int flags; + login_cap_t *lc; + + /* + * We only use setusercontext() to set the nice value, rlimits + * and umask unless this is a login shell (sudo -i). + */ + lc = login_getclass((char *)details->login_class); + if (!lc) { + sudo_warnx(U_("unknown login class %s"), details->login_class); + errno = ENOENT; + goto done; + } + if (ISSET(details->flags, CD_LOGIN_SHELL)) { + /* Set everything except user, group and login name. */ + flags = LOGIN_SETALL; + CLR(flags, LOGIN_SETGROUP|LOGIN_SETLOGIN|LOGIN_SETUSER|LOGIN_SETENV|LOGIN_SETPATH); + } else { + flags = LOGIN_SETRESOURCES|LOGIN_SETPRIORITY|LOGIN_SETUMASK; + } + if (setusercontext(lc, details->pw, details->pw->pw_uid, flags)) { + sudo_warn("%s", U_("unable to set user context")); + if (details->pw->pw_uid != ROOT_UID) + goto done; + } + } +#endif /* HAVE_LOGIN_CAP_H */ + } + + if (ISSET(details->flags, CD_SET_GROUPS)) { + /* set_user_groups() prints error message on failure. */ + if (!set_user_groups(details)) + goto done; + } + + if (ISSET(details->flags, CD_SET_PRIORITY)) { + if (setpriority(PRIO_PROCESS, 0, details->priority) != 0) { + sudo_warn("%s", U_("unable to set process priority")); + goto done; + } + } + + /* Policy may override umask in PAM or login.conf. */ + if (ISSET(details->flags, CD_OVERRIDE_UMASK)) + (void) umask(details->umask); + + /* Close fds before chroot (need /dev) or uid change (prlimit on Linux). */ + close_fds(details, errfd); + + if (details->chroot) { + if (chroot(details->chroot) != 0 || chdir("/") != 0) { + sudo_warn(U_("unable to change root to %s"), details->chroot); + goto done; + } + } + + /* + * Unlimit the number of processes since Linux's setuid() will + * return EAGAIN if RLIMIT_NPROC would be exceeded by the uid switch. + */ + unlimit_nproc(); + +#if defined(HAVE_SETRESUID) + if (setresuid(details->cred.uid, details->cred.euid, details->cred.euid) != 0) { + sudo_warn(U_("unable to change to runas uid (%u, %u)"), + (unsigned int)details->cred.uid, (unsigned int)details->cred.euid); + goto done; + } +#elif defined(HAVE_SETREUID) + if (setreuid(details->cred.uid, details->cred.euid) != 0) { + sudo_warn(U_("unable to change to runas uid (%u, %u)"), + (unsigned int)details->cred.uid, (unsigned int)details->cred.euid); + goto done; + } +#else + /* Cannot support real user-ID that is different from effective user-ID. */ + if (setuid(details->cred.euid) != 0) { + sudo_warn(U_("unable to change to runas uid (%u, %u)"), + (unsigned int)details->cred.euid, (unsigned int)details->cred.euid); + goto done; + } +#endif /* !HAVE_SETRESUID && !HAVE_SETREUID */ + + /* Restore previous value of RLIMIT_NPROC. */ + restore_nproc(); + + /* + * Only change cwd if we have chroot()ed or the policy modules + * specifies a different cwd. Must be done after uid change. + */ + if (details->cwd != NULL) { + if (details->chroot != NULL || user_details.cwd == NULL || + strcmp(details->cwd, user_details.cwd) != 0) { + /* Note: cwd is relative to the new root, if any. */ + if (chdir(details->cwd) == -1) { + sudo_warn(U_("unable to change directory to %s"), details->cwd); + if (!details->cwd_optional) + goto done; + if (details->chroot != NULL) + sudo_warnx(U_("starting from %s"), "/"); + } + } + } + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Setup the execution environment and execute the command. + * If SELinux is enabled, run the command via sesh, otherwise + * execute it directly. + * If the exec fails, cstat is filled in with the value of errno. + */ +void +exec_cmnd(struct command_details *details, int errfd) +{ + debug_decl(exec_cmnd, SUDO_DEBUG_EXEC); + + restore_signals(); + if (exec_setup(details, errfd) == true) { + /* headed for execve() */ +#ifdef HAVE_SELINUX + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + selinux_execve(details->execfd, details->command, details->argv, + details->envp, ISSET(details->flags, CD_NOEXEC)); + } else +#endif + { + sudo_execve(details->execfd, details->command, details->argv, + details->envp, ISSET(details->flags, CD_NOEXEC)); + } + } + sudo_debug_printf(SUDO_DEBUG_ERROR, "unable to exec %s: %s", + details->command, strerror(errno)); + debug_return; +} + +/* + * Check for caught signals sent to sudo before command execution. + * Also suspends the process if SIGTSTP was caught. + * Returns true if we should terminate, else false. + */ +bool +sudo_terminated(struct command_status *cstat) +{ + int signo; + bool sigtstp = false; + debug_decl(sudo_terminated, SUDO_DEBUG_EXEC); + + for (signo = 0; signo < NSIG; signo++) { + if (signal_pending(signo)) { + switch (signo) { + case SIGCHLD: + /* Ignore. */ + break; + case SIGTSTP: + /* Suspend below if not terminated. */ + sigtstp = true; + break; + default: + /* Terminal signal, do not exec command. */ + cstat->type = CMD_WSTATUS; + cstat->val = signo + 128; + debug_return_bool(true); + break; + } + } + } + if (sigtstp) { + struct sigaction sa; + sigset_t set, oset; + + /* Send SIGTSTP to ourselves, unblocking it if needed. */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_DFL; + if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0) + sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP); + sigemptyset(&set); + sigaddset(&set, SIGTSTP); + sigprocmask(SIG_UNBLOCK, &set, &oset); + if (kill(getpid(), SIGTSTP) != 0) + sudo_warn("kill(%d, SIGTSTP)", (int)getpid()); + sigprocmask(SIG_SETMASK, &oset, NULL); + /* No need to restore old SIGTSTP handler. */ + } + debug_return_bool(false); +} + +#if SUDO_API_VERSION != SUDO_API_MKVERSION(1, 17) +# error "Update sudo_needs_pty() after changing the plugin API" +#endif +static bool +sudo_needs_pty(struct command_details *details) +{ + struct plugin_container *plugin; + + if (ISSET(details->flags, CD_USE_PTY)) + return true; + + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_ttyin != NULL || + plugin->u.io->log_ttyout != NULL || + plugin->u.io->log_stdin != NULL || + plugin->u.io->log_stdout != NULL || + plugin->u.io->log_stderr != NULL || + plugin->u.io->change_winsize != NULL || + plugin->u.io->log_suspend != NULL) + return true; + } + return false; +} + +/* + * If we are not running the command in a pty, we were not invoked as + * sudoedit, there is no command timeout and there is no close function, + * sudo can exec the command directly (and not wait). + */ +static bool +direct_exec_allowed(struct command_details *details) +{ + struct plugin_container *plugin; + debug_decl(direct_exec_allowed, SUDO_DEBUG_EXEC); + + /* Assumes sudo_needs_pty() was already checked. */ + if (ISSET(details->flags, CD_RBAC_ENABLED|CD_SET_TIMEOUT|CD_SUDOEDIT) || + policy_plugin.u.policy->close != NULL) + debug_return_bool(false); + + TAILQ_FOREACH(plugin, &audit_plugins, entries) { + if (plugin->u.audit->close != NULL) + debug_return_bool(false); + } + + debug_return_bool(true); +} + +/* + * Execute a command, potentially in a pty with I/O logging, and + * wait for it to finish. + * This is a little bit tricky due to how POSIX job control works and + * we fact that we have two different controlling terminals to deal with. + */ +int +sudo_execute(struct command_details *details, struct command_status *cstat) +{ + debug_decl(sudo_execute, SUDO_DEBUG_EXEC); + + /* If running in background mode, fork and exit. */ + if (ISSET(details->flags, CD_BACKGROUND)) { + switch (sudo_debug_fork()) { + case -1: + cstat->type = CMD_ERRNO; + cstat->val = errno; + debug_return_int(-1); + case 0: + /* child continues without controlling terminal */ + (void)setpgid(0, 0); + break; + default: + /* parent exits (but does not flush buffers) */ + sudo_debug_exit_int(__func__, __FILE__, __LINE__, + sudo_debug_subsys, 0); + _exit(EXIT_SUCCESS); + } + } + + /* + * Restore resource limits before running. + * We must do this *before* calling the PAM session module. + */ + restore_limits(); + + /* + * Run the command in a new pty if there is an I/O plugin or the policy + * has requested a pty. If /dev/tty is unavailable and no I/O plugin + * is configured, this returns false and we run the command without a pty. + */ + if (sudo_needs_pty(details)) { + if (exec_pty(details, cstat)) + goto done; + } + + /* + * If we are not running the command in a pty, we may be able to + * exec directly, depending on the plugins used. + */ + if (direct_exec_allowed(details)) { + if (!sudo_terminated(cstat)) { + exec_cmnd(details, -1); + cstat->type = CMD_ERRNO; + cstat->val = errno; + } + goto done; + } + + /* + * Run the command in the existing tty (if any) and wait for it to finish. + */ + exec_nopty(details, cstat); + +done: + /* The caller will run any plugin close functions. */ + debug_return_int(cstat->type == CMD_ERRNO ? -1 : 0); +} + +/* + * Kill command with increasing urgency. + */ +void +terminate_command(pid_t pid, bool use_pgrp) +{ + debug_decl(terminate_command, SUDO_DEBUG_EXEC); + + /* Avoid killing more than a single process or process group. */ + if (pid <= 0) + debug_return; + + /* + * Note that SIGCHLD will interrupt the sleep() + */ + if (use_pgrp) { + sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGHUP", (int)pid); + killpg(pid, SIGHUP); + sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGTERM", (int)pid); + killpg(pid, SIGTERM); + sleep(2); + sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGKILL", (int)pid); + killpg(pid, SIGKILL); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGHUP", (int)pid); + kill(pid, SIGHUP); + sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGTERM", (int)pid); + kill(pid, SIGTERM); + sleep(2); + sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGKILL", (int)pid); + kill(pid, SIGKILL); + } + + debug_return; +} diff --git a/src/exec_common.c b/src/exec_common.c new file mode 100644 index 0000000..32834de --- /dev/null +++ b/src/exec_common.c @@ -0,0 +1,213 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2016 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#ifdef HAVE_PRIV_SET +# include <priv.h> +#endif +#include <errno.h> + +#include "sudo.h" +#include "sudo_exec.h" + +#ifdef RTLD_PRELOAD_VAR +/* + * Add a DSO file to LD_PRELOAD or the system equivalent. + */ +static char ** +preload_dso(char *envp[], const char *dso_file) +{ + char *preload = NULL; + int env_len; + int preload_idx = -1; + bool present = false; +# ifdef RTLD_PRELOAD_ENABLE_VAR + bool enabled = false; +# else + const bool enabled = true; +# endif + debug_decl(preload_dso, SUDO_DEBUG_UTIL); + + /* + * Preload a DSO file. For a list of LD_PRELOAD-alikes, see + * http://www.fortran-2000.com/ArnaudRecipes/sharedlib.html + * XXX - need to support 32-bit and 64-bit variants + */ + + /* Count entries in envp, looking for LD_PRELOAD as we go. */ + for (env_len = 0; envp[env_len] != NULL; env_len++) { + if (preload_idx == -1 && strncmp(envp[env_len], RTLD_PRELOAD_VAR "=", + sizeof(RTLD_PRELOAD_VAR)) == 0) { + const char *cp = envp[env_len] + sizeof(RTLD_PRELOAD_VAR); + const char *end = cp + strlen(cp); + const char *ep; + const size_t dso_len = strlen(dso_file); + + /* Check to see if dso_file is already present. */ + for (cp = sudo_strsplit(cp, end, RTLD_PRELOAD_DELIM, &ep); + cp != NULL; cp = sudo_strsplit(NULL, end, RTLD_PRELOAD_DELIM, + &ep)) { + if ((size_t)(ep - cp) == dso_len) { + if (memcmp(cp, dso_file, dso_len) == 0) { + /* already present */ + present = true; + break; + } + } + } + + /* Save index of existing LD_PRELOAD variable. */ + preload_idx = env_len; + continue; + } +# ifdef RTLD_PRELOAD_ENABLE_VAR + if (strncmp(envp[env_len], RTLD_PRELOAD_ENABLE_VAR "=", sizeof(RTLD_PRELOAD_ENABLE_VAR)) == 0) { + enabled = true; + continue; + } +# endif + } + + /* + * Make a new copy of envp as needed. + * It would be nice to realloc the old envp[] but we don't know + * whether it was dynamically allocated. [TODO: plugin API] + */ + if (preload_idx == -1 || !enabled) { + const int env_size = env_len + 1 + (preload_idx == -1) + enabled; // -V547 + + char **nenvp = reallocarray(NULL, env_size, sizeof(*envp)); + if (nenvp == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + memcpy(nenvp, envp, env_len * sizeof(*envp)); + nenvp[env_len] = NULL; + envp = nenvp; + } + + /* Prepend our LD_PRELOAD to existing value or add new entry at the end. */ + if (!present) { + if (preload_idx == -1) { +# ifdef RTLD_PRELOAD_DEFAULT + asprintf(&preload, "%s=%s%s%s", RTLD_PRELOAD_VAR, dso_file, + RTLD_PRELOAD_DELIM, RTLD_PRELOAD_DEFAULT); +# else + preload = sudo_new_key_val(RTLD_PRELOAD_VAR, dso_file); +# endif + if (preload == NULL) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + envp[env_len++] = preload; + envp[env_len] = NULL; + } else { + int len = asprintf(&preload, "%s=%s%s%s", RTLD_PRELOAD_VAR, + dso_file, RTLD_PRELOAD_DELIM, envp[preload_idx]); + if (len == -1) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + envp[preload_idx] = preload; + } + } +# ifdef RTLD_PRELOAD_ENABLE_VAR + if (!enabled) { + envp[env_len++] = RTLD_PRELOAD_ENABLE_VAR "="; + envp[env_len] = NULL; + } +# endif + + debug_return_ptr(envp); +} +#endif /* RTLD_PRELOAD_VAR */ + +/* + * Disable execution of child processes in the command we are about + * to run. On systems with privilege sets, we can remove the exec + * privilege. On other systems we use LD_PRELOAD and the like. + */ +char ** +disable_execute(char *envp[], const char *dso) +{ + debug_decl(disable_execute, SUDO_DEBUG_UTIL); + +#ifdef HAVE_PRIV_SET + /* Solaris privileges, remove PRIV_PROC_EXEC post-execve. */ + (void)priv_set(PRIV_ON, PRIV_INHERITABLE, "PRIV_FILE_DAC_READ", NULL); + (void)priv_set(PRIV_ON, PRIV_INHERITABLE, "PRIV_FILE_DAC_WRITE", NULL); + (void)priv_set(PRIV_ON, PRIV_INHERITABLE, "PRIV_FILE_DAC_SEARCH", NULL); + if (priv_set(PRIV_OFF, PRIV_LIMIT, "PRIV_PROC_EXEC", NULL) == 0) + debug_return_ptr(envp); + sudo_warn("%s", U_("unable to remove PRIV_PROC_EXEC from PRIV_LIMIT")); +#endif /* HAVE_PRIV_SET */ + +#ifdef RTLD_PRELOAD_VAR + if (dso != NULL) + envp = preload_dso(envp, dso); +#endif /* RTLD_PRELOAD_VAR */ + + debug_return_ptr(envp); +} + +/* + * Like execve(2) but falls back to running through /bin/sh + * ala execvp(3) if we get ENOEXEC. + */ +int +sudo_execve(int fd, const char *path, char *const argv[], char *envp[], bool noexec) +{ + debug_decl(sudo_execve, SUDO_DEBUG_UTIL); + + sudo_debug_execve(SUDO_DEBUG_INFO, path, argv, envp); + + /* Modify the environment as needed to disable further execve(). */ + if (noexec) + envp = disable_execute(envp, sudo_conf_noexec_path()); + +#ifdef HAVE_FEXECVE + if (fd != -1) + fexecve(fd, argv, envp); + else +#endif + execve(path, argv, envp); + if (fd == -1 && errno == ENOEXEC) { + int argc; + char **nargv; + + for (argc = 0; argv[argc] != NULL; argc++) + continue; + nargv = reallocarray(NULL, argc + 2, sizeof(char *)); + if (nargv != NULL) { + nargv[0] = "sh"; + nargv[1] = (char *)path; + memcpy(nargv + 2, argv + 1, argc * sizeof(char *)); + execve(_PATH_SUDO_BSHELL, nargv, envp); + free(nargv); + } + } + debug_return_int(-1); +} diff --git a/src/exec_monitor.c b/src/exec_monitor.c new file mode 100644 index 0000000..bd8eee1 --- /dev/null +++ b/src/exec_monitor.c @@ -0,0 +1,723 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <time.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <termios.h> + +#include "sudo.h" +#include "sudo_exec.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" + +struct monitor_closure { + pid_t cmnd_pid; + pid_t cmnd_pgrp; + pid_t mon_pgrp; + int backchannel; + struct command_status *cstat; + struct sudo_event_base *evbase; + struct sudo_event *errpipe_event; + struct sudo_event *backchannel_event; + struct sudo_event *sigint_event; + struct sudo_event *sigquit_event; + struct sudo_event *sigtstp_event; + struct sudo_event *sigterm_event; + struct sudo_event *sighup_event; + struct sudo_event *sigusr1_event; + struct sudo_event *sigusr2_event; + struct sudo_event *sigchld_event; +}; + +static bool tty_initialized; + +/* + * Deliver a signal to the running command. + * The signal was either forwarded to us by the parent sudo process + * or was received by the monitor itself. + * + * There are two "special" signals, SIGCONT_FG and SIGCONT_BG that + * also specify whether the command should have the controlling tty. + */ +static void +deliver_signal(struct monitor_closure *mc, int signo, bool from_parent) +{ + char signame[SIG2STR_MAX]; + debug_decl(deliver_signal, SUDO_DEBUG_EXEC); + + /* Avoid killing more than a single process or process group. */ + if (mc->cmnd_pid <= 0) + debug_return; + + if (signo == SIGCONT_FG) + (void)strlcpy(signame, "CONT_FG", sizeof(signame)); + else if (signo == SIGCONT_BG) + (void)strlcpy(signame, "CONT_BG", sizeof(signame)); + else if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + + /* Handle signal from parent or monitor. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "received SIG%s%s", + signame, from_parent ? " from parent" : ""); + switch (signo) { + case SIGALRM: + terminate_command(mc->cmnd_pid, true); + break; + case SIGCONT_FG: + /* Continue in foreground, grant it controlling tty. */ + if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc->cmnd_pgrp) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to set foreground pgrp to %d (command)", + __func__, (int)mc->cmnd_pgrp); + } + /* Lazily initialize the pty if needed. */ + if (!tty_initialized) { + if (sudo_term_copy(io_fds[SFD_USERTTY], io_fds[SFD_FOLLOWER])) + tty_initialized = true; + } + killpg(mc->cmnd_pid, SIGCONT); + break; + case SIGCONT_BG: + /* Continue in background, I take controlling tty. */ + if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc->mon_pgrp) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to set foreground pgrp to %d (monitor)", + __func__, (int)mc->mon_pgrp); + } + killpg(mc->cmnd_pid, SIGCONT); + break; + case SIGKILL: + _exit(EXIT_FAILURE); /* XXX */ + /* NOTREACHED */ + default: + /* Relay signal to command. */ + killpg(mc->cmnd_pid, signo); + break; + } + debug_return; +} + +/* + * Unpack rows and cols from a CMD_TTYWINCH value, set the new window + * size on the pty follower and inform the command of the change. + */ +static void +handle_winch(struct monitor_closure *mc, unsigned int wsize_packed) +{ + struct winsize wsize, owsize; + debug_decl(handle_winch, SUDO_DEBUG_EXEC); + + /* Rows and columns are stored as two shorts packed into a single int. */ + wsize.ws_row = wsize_packed & 0xffff; + wsize.ws_col = (wsize_packed >> 16) & 0xffff; + + if (ioctl(io_fds[SFD_FOLLOWER], TIOCGWINSZ, &owsize) == 0 && + (wsize.ws_row != owsize.ws_row || wsize.ws_col != owsize.ws_col)) { + + sudo_debug_printf(SUDO_DEBUG_INFO, + "window size change %dx%d -> %dx%d", + owsize.ws_col, owsize.ws_row, wsize.ws_col, wsize.ws_row); + + (void)ioctl(io_fds[SFD_FOLLOWER], TIOCSWINSZ, &wsize); + deliver_signal(mc, SIGWINCH, true); + } + + debug_return; +} + +/* + * Send status to parent over socketpair. + * Return value is the same as send(2). + */ +static int +send_status(int fd, struct command_status *cstat) +{ + int n = -1; + debug_decl(send_status, SUDO_DEBUG_EXEC); + + if (cstat->type != CMD_INVALID) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "sending status message to parent: [%d, %d]", + cstat->type, cstat->val); + n = send(fd, cstat, sizeof(*cstat), 0); + if (n != sizeof(*cstat)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to send status to parent", __func__); + } + cstat->type = CMD_INVALID; /* prevent re-sending */ + } + debug_return_int(n); +} + +/* + * Wait for command status after receiving SIGCHLD. + * If the command was stopped, the status is send back to the parent. + * Otherwise, cstat is filled in but not sent. + */ +static void +mon_handle_sigchld(struct monitor_closure *mc) +{ + char signame[SIG2STR_MAX]; + int status; + pid_t pid; + debug_decl(mon_handle_sigchld, SUDO_DEBUG_EXEC); + + /* Read command status. */ + do { + pid = waitpid(mc->cmnd_pid, &status, WUNTRACED|WCONTINUED|WNOHANG); + } while (pid == -1 && errno == EINTR); + switch (pid) { + case 0: + errno = ECHILD; + FALLTHROUGH; + case -1: + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + debug_return; + } + + if (WIFCONTINUED(status)) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) resumed", + __func__, (int)mc->cmnd_pid); + } else if (WIFSTOPPED(status)) { + if (sig2str(WSTOPSIG(status), signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", WSTOPSIG(status)); + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) stopped, SIG%s", + __func__, (int)mc->cmnd_pid, signame); + } else if (WIFSIGNALED(status)) { + if (sig2str(WTERMSIG(status), signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", WTERMSIG(status)); + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) killed, SIG%s", + __func__, (int)mc->cmnd_pid, signame); + mc->cmnd_pid = -1; + } else if (WIFEXITED(status)) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) exited: %d", + __func__, (int)mc->cmnd_pid, WEXITSTATUS(status)); + mc->cmnd_pid = -1; + } else { + sudo_debug_printf(SUDO_DEBUG_WARN, + "%s: unexpected wait status %d for command (%d)", + __func__, status, (int)mc->cmnd_pid); + } + + /* Don't overwrite execve() failure with child exit status. */ + if (mc->cstat->type == CMD_INVALID) { + /* + * Store wait status in cstat and forward to parent if stopped. + * Parent does not expect SIGCONT so don't bother sending it. + */ + if (!WIFCONTINUED(status)) { + mc->cstat->type = CMD_WSTATUS; + mc->cstat->val = status; + if (WIFSTOPPED(status)) { + /* Save the foreground pgid so we can restore it later. */ + pid = tcgetpgrp(io_fds[SFD_FOLLOWER]); + if (pid != mc->mon_pgrp) + mc->cmnd_pgrp = pid; + send_status(mc->backchannel, mc->cstat); + } + } + } else { + sudo_debug_printf(SUDO_DEBUG_WARN, + "%s: not overwriting command status %d,%d with %d,%d", + __func__, mc->cstat->type, mc->cstat->val, CMD_WSTATUS, status); + } + + debug_return; +} + +static void +mon_signal_cb(int signo, int what, void *v) +{ + struct sudo_ev_siginfo_container *sc = v; + struct monitor_closure *mc = sc->closure; + debug_decl(mon_signal_cb, SUDO_DEBUG_EXEC); + + /* + * Handle SIGCHLD specially and deliver other signals + * directly to the command. + */ + if (signo == SIGCHLD) { + mon_handle_sigchld(mc); + if (mc->cmnd_pid == -1) { + /* Command exited or was killed, exit event loop. */ + sudo_ev_loopexit(mc->evbase); + } + } else { + /* + * If the signal came from the process group of the command we ran, + * do not forward it as we don't want the child to indirectly kill + * itself. This can happen with, e.g., BSD-derived versions of + * reboot that call kill(-1, SIGTERM) to kill all other processes. + */ + if (USER_SIGNALED(sc->siginfo) && sc->siginfo->si_pid != 0) { + pid_t si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == mc->cmnd_pgrp) + debug_return; + } else if (sc->siginfo->si_pid == mc->cmnd_pid) { + debug_return; + } + } + deliver_signal(mc, signo, false); + } + debug_return; +} + +/* Note: this is basically the same as errpipe_cb() in exec_nopty.c */ +static void +mon_errpipe_cb(int fd, int what, void *v) +{ + struct monitor_closure *mc = v; + ssize_t nread; + int errval; + debug_decl(mon_errpipe_cb, SUDO_DEBUG_EXEC); + + /* + * Read errno from child or EOF when command is executed. + * Note that the error pipe is *blocking*. + */ + nread = read(fd, &errval, sizeof(errval)); + switch (nread) { + case -1: + if (errno != EAGAIN && errno != EINTR) { + if (mc->cstat->val == CMD_INVALID) { + /* XXX - need a way to distinguish non-exec error. */ + mc->cstat->type = CMD_ERRNO; + mc->cstat->val = errno; + } + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: failed to read error pipe", __func__); + sudo_ev_loopbreak(mc->evbase); + } + break; + default: + if (nread == 0) { + /* The error pipe closes when the command is executed. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "EOF on error pipe"); + } else { + /* Errno value when child is unable to execute command. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "errno from child: %s", + strerror(errval)); + mc->cstat->type = CMD_ERRNO; + mc->cstat->val = errval; + } + sudo_ev_del(mc->evbase, mc->errpipe_event); + close(fd); + break; + } + debug_return; +} + +static void +mon_backchannel_cb(int fd, int what, void *v) +{ + struct monitor_closure *mc = v; + struct command_status cstmp; + ssize_t n; + debug_decl(mon_backchannel_cb, SUDO_DEBUG_EXEC); + + /* + * Read command from backchannel, should be a signal. + * Note that the backchannel is a *blocking* socket. + */ + n = recv(fd, &cstmp, sizeof(cstmp), MSG_WAITALL); + if (n != sizeof(cstmp)) { + if (n == -1) { + if (errno == EINTR || errno == EAGAIN) + debug_return; + sudo_warn("%s", U_("error reading from socketpair")); + } else { + /* short read or EOF, parent process died? */ + } + /* XXX - need a way to distinguish non-exec error. */ + mc->cstat->type = CMD_ERRNO; + mc->cstat->val = n ? EIO : ECONNRESET; + sudo_ev_loopbreak(mc->evbase); + } else { + switch (cstmp.type) { + case CMD_TTYWINCH: + handle_winch(mc, cstmp.val); + break; + case CMD_SIGNO: + deliver_signal(mc, cstmp.val, true); + break; + default: + sudo_warnx(U_("unexpected reply type on backchannel: %d"), cstmp.type); + break; + } + } + debug_return; +} + +/* + * Sets up std{in,out,err} and executes the actual command. + * Returns only if execve() fails. + */ +static void +exec_cmnd_pty(struct command_details *details, bool foreground, int errfd) +{ + volatile pid_t self = getpid(); + debug_decl(exec_cmnd_pty, SUDO_DEBUG_EXEC); + + /* Register cleanup function */ + sudo_fatal_callback_register(pty_cleanup); + + /* Set command process group here too to avoid a race. */ + setpgid(0, self); + + /* Wire up standard fds, note that stdout/stderr may be pipes. */ + if (dup3(io_fds[SFD_STDIN], STDIN_FILENO, 0) == -1) + sudo_fatal("dup3"); + if (io_fds[SFD_STDIN] != io_fds[SFD_FOLLOWER]) + close(io_fds[SFD_STDIN]); + if (dup3(io_fds[SFD_STDOUT], STDOUT_FILENO, 0) == -1) + sudo_fatal("dup3"); + if (io_fds[SFD_STDOUT] != io_fds[SFD_FOLLOWER]) + close(io_fds[SFD_STDOUT]); + if (dup3(io_fds[SFD_STDERR], STDERR_FILENO, 0) == -1) + sudo_fatal("dup3"); + if (io_fds[SFD_STDERR] != io_fds[SFD_FOLLOWER]) + close(io_fds[SFD_STDERR]); + + /* Wait for parent to grant us the tty if we are foreground. */ + if (foreground && !ISSET(details->flags, CD_EXEC_BG)) { + struct timespec ts = { 0, 1000 }; /* 1us */ + sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: waiting for controlling tty", + __func__); + while (tcgetpgrp(io_fds[SFD_FOLLOWER]) != self) + nanosleep(&ts, NULL); + sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: got controlling tty", + __func__); + } + + /* Done with the pty follower, don't leak it. */ + if (io_fds[SFD_FOLLOWER] != -1) + close(io_fds[SFD_FOLLOWER]); + + /* Execute command; only returns on error. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "executing %s in the %s", + details->command, foreground ? "foreground" : "background"); + exec_cmnd(details, errfd); + + debug_return; +} + +/* + * Fill in the monitor closure and setup initial events. + * Allocates read events for the signal pipe, error pipe and backchannel. + */ +static void +fill_exec_closure_monitor(struct monitor_closure *mc, + struct command_status *cstat, int errfd, int backchannel) +{ + debug_decl(fill_exec_closure_monitor, SUDO_DEBUG_EXEC); + + /* Fill in the non-event part of the closure. */ + mc->cstat = cstat; + mc->backchannel = backchannel; + mc->mon_pgrp = getpgrp(); + + /* Setup event base and events. */ + mc->evbase = sudo_ev_base_alloc(); + if (mc->evbase == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* Event for command status via errfd. */ + mc->errpipe_event = sudo_ev_alloc(errfd, + SUDO_EV_READ|SUDO_EV_PERSIST, mon_errpipe_cb, mc); + if (mc->errpipe_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->errpipe_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + /* Event for forwarded signals via backchannel. */ + mc->backchannel_event = sudo_ev_alloc(backchannel, + SUDO_EV_READ|SUDO_EV_PERSIST, mon_backchannel_cb, mc); + if (mc->backchannel_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->backchannel_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + /* Events for local signals. */ + mc->sigint_event = sudo_ev_alloc(SIGINT, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sigint_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sigint_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + mc->sigquit_event = sudo_ev_alloc(SIGQUIT, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sigquit_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sigquit_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + mc->sigtstp_event = sudo_ev_alloc(SIGTSTP, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sigtstp_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sigtstp_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + mc->sigterm_event = sudo_ev_alloc(SIGTERM, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sigterm_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sigterm_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + mc->sighup_event = sudo_ev_alloc(SIGHUP, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sighup_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sighup_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + mc->sigusr1_event = sudo_ev_alloc(SIGUSR1, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sigusr1_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sigusr1_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + mc->sigusr2_event = sudo_ev_alloc(SIGUSR2, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sigusr2_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sigusr2_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + mc->sigchld_event = sudo_ev_alloc(SIGCHLD, + SUDO_EV_SIGINFO, mon_signal_cb, mc); + if (mc->sigchld_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->sigchld_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + /* Clear the default event base. */ + sudo_ev_base_setdef(NULL); + + debug_return; +} + +/* + * Monitor process that creates a new session with the controlling tty, + * resets signal handlers and forks a child to call exec_cmnd_pty(). + * Waits for status changes from the command and relays them to the + * parent and relays signals from the parent to the command. + * Must be called with signals blocked and the old signal mask in oset. + * Returns an error if fork(2) fails, else calls _exit(2). + */ +int +exec_monitor(struct command_details *details, sigset_t *oset, + bool foreground, int backchannel) +{ + struct monitor_closure mc = { 0 }; + struct command_status cstat; + struct sigaction sa; + int errpipe[2]; + debug_decl(exec_monitor, SUDO_DEBUG_EXEC); + + /* The pty leader is not used by the monitor. */ + if (io_fds[SFD_LEADER] != -1) + close(io_fds[SFD_LEADER]); + + /* Ignore any SIGTTIN or SIGTTOU we receive (shouldn't be possible). */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_IGN; + if (sudo_sigaction(SIGTTIN, &sa, NULL) != 0) + sudo_warn(U_("unable to set handler for signal %d"), SIGTTIN); + if (sudo_sigaction(SIGTTOU, &sa, NULL) != 0) + sudo_warn(U_("unable to set handler for signal %d"), SIGTTOU); + + /* If we are starting in the foreground, the pty was already initialized. */ + if (foreground) + tty_initialized = true; + + /* + * Start a new session with the parent as the session leader + * and the follower device as the controlling terminal. + * This allows us to be notified when the command has been suspended. + */ + if (setsid() == -1) { + sudo_warn("setsid"); + goto bad; + } + if (pty_make_controlling() == -1) { + sudo_warn("%s", U_("unable to set controlling tty")); + goto bad; + } + + /* + * We use a pipe to get errno if execve(2) fails in the child. + */ + if (pipe2(errpipe, O_CLOEXEC) != 0) + sudo_fatal("%s", U_("unable to create pipe")); + + /* + * Before forking, wait for the main sudo process to tell us to go. + * Avoids race conditions when the command exits quickly. + */ + while (recv(backchannel, &cstat, sizeof(cstat), MSG_WAITALL) == -1) { + if (errno != EINTR && errno != EAGAIN) + sudo_fatal("%s", U_("unable to receive message from parent")); + } + +#ifdef HAVE_SELINUX + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + if (selinux_setup(details->selinux_role, details->selinux_type, + details->tty, io_fds[SFD_FOLLOWER], true) == -1) + goto bad; + } +#endif + + mc.cmnd_pid = sudo_debug_fork(); + switch (mc.cmnd_pid) { + case -1: + sudo_warn("%s", U_("unable to fork")); +#ifdef HAVE_SELINUX + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + if (selinux_restore_tty() != 0) + sudo_warnx("%s", U_("unable to restore tty label")); + } +#endif + goto bad; + case 0: + /* child */ + sigprocmask(SIG_SETMASK, oset, NULL); + close(backchannel); + close(errpipe[0]); + if (io_fds[SFD_USERTTY] != -1) + close(io_fds[SFD_USERTTY]); + restore_signals(); + + /* setup tty and exec command */ + exec_cmnd_pty(details, foreground, errpipe[1]); + if (write(errpipe[1], &errno, sizeof(int)) == -1) + sudo_warn(U_("unable to execute %s"), details->command); + _exit(EXIT_FAILURE); + } + close(errpipe[1]); + + /* No longer need execfd. */ + if (details->execfd != -1) { + close(details->execfd); + details->execfd = -1; + } + + /* Send the command's pid to main sudo process. */ + cstat.type = CMD_PID; + cstat.val = mc.cmnd_pid; + send_status(backchannel, &cstat); + + /* + * Create new event base and register read events for the + * signal pipe, error pipe, and backchannel. + */ + fill_exec_closure_monitor(&mc, &cstat, errpipe[0], backchannel); + + /* Restore signal mask now that signal handlers are setup. */ + sigprocmask(SIG_SETMASK, oset, NULL); + + /* If any of stdin/stdout/stderr are pipes, close them in parent. */ + if (io_fds[SFD_STDIN] != io_fds[SFD_FOLLOWER]) + close(io_fds[SFD_STDIN]); + if (io_fds[SFD_STDOUT] != io_fds[SFD_FOLLOWER]) + close(io_fds[SFD_STDOUT]); + if (io_fds[SFD_STDERR] != io_fds[SFD_FOLLOWER]) + close(io_fds[SFD_STDERR]); + + /* Put command in its own process group. */ + mc.cmnd_pgrp = mc.cmnd_pid; + setpgid(mc.cmnd_pid, mc.cmnd_pgrp); + + /* Make the command the foreground process for the pty follower. */ + if (foreground && !ISSET(details->flags, CD_EXEC_BG)) { + if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc.cmnd_pgrp) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to set foreground pgrp to %d (command)", + __func__, (int)mc.cmnd_pgrp); + } + } + + /* + * Wait for errno on pipe, signal on backchannel or for SIGCHLD. + * The event loop ends when the child is no longer running and + * the error pipe is closed. + */ + cstat.type = CMD_INVALID; + cstat.val = 0; + (void) sudo_ev_dispatch(mc.evbase); + if (mc.cmnd_pid != -1) { + pid_t pid; + + /* Command still running, did the parent die? */ + sudo_debug_printf(SUDO_DEBUG_ERROR, + "Command still running after event loop exit, terminating"); + terminate_command(mc.cmnd_pid, true); + do { + pid = waitpid(mc.cmnd_pid, NULL, 0); + } while (pid == -1 && errno == EINTR); + /* XXX - update cstat with wait status? */ + } + + /* + * Take the controlling tty. This prevents processes spawned by the + * command from receiving SIGHUP when the session leader (us) exits. + */ + if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc.mon_pgrp) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to set foreground pgrp to %d (monitor)", + __func__, (int)mc.mon_pgrp); + } + + /* Send parent status. */ + send_status(backchannel, &cstat); + +#ifdef HAVE_SELINUX + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + if (selinux_restore_tty() != 0) + sudo_warnx("%s", U_("unable to restore tty label")); + } +#endif + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1); + _exit(EXIT_FAILURE); + +bad: + debug_return_int(-1); +} diff --git a/src/exec_nopty.c b/src/exec_nopty.c new file mode 100644 index 0000000..7281531 --- /dev/null +++ b/src/exec_nopty.c @@ -0,0 +1,579 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> + +#include "sudo.h" +#include "sudo_exec.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" + +struct exec_closure_nopty { + pid_t cmnd_pid; + pid_t ppgrp; + struct command_status *cstat; + struct command_details *details; + struct sudo_event_base *evbase; + struct sudo_event *errpipe_event; + struct sudo_event *sigint_event; + struct sudo_event *sigquit_event; + struct sudo_event *sigtstp_event; + struct sudo_event *sigterm_event; + struct sudo_event *sighup_event; + struct sudo_event *sigalrm_event; + struct sudo_event *sigpipe_event; + struct sudo_event *sigusr1_event; + struct sudo_event *sigusr2_event; + struct sudo_event *sigchld_event; + struct sudo_event *sigcont_event; + struct sudo_event *siginfo_event; +}; + +static void handle_sigchld_nopty(struct exec_closure_nopty *ec); + +/* Note: this is basically the same as mon_errpipe_cb() in exec_monitor.c */ +static void +errpipe_cb(int fd, int what, void *v) +{ + struct exec_closure_nopty *ec = v; + ssize_t nread; + int errval; + debug_decl(errpipe_cb, SUDO_DEBUG_EXEC); + + /* + * Read errno from child or EOF when command is executed. + * Note that the error pipe is *blocking*. + */ + nread = read(fd, &errval, sizeof(errval)); + switch (nread) { + case -1: + if (errno != EAGAIN && errno != EINTR) { + if (ec->cstat->val == CMD_INVALID) { + /* XXX - need a way to distinguish non-exec error. */ + ec->cstat->type = CMD_ERRNO; + ec->cstat->val = errno; + } + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: failed to read error pipe", __func__); + sudo_ev_loopbreak(ec->evbase); + } + break; + default: + if (nread == 0) { + /* The error pipe closes when the command is executed. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "EOF on error pipe"); + } else { + /* Errno value when child is unable to execute command. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "errno from child: %s", + strerror(errval)); + ec->cstat->type = CMD_ERRNO; + ec->cstat->val = errval; + } + sudo_ev_del(ec->evbase, ec->errpipe_event); + close(fd); + break; + } + debug_return; +} + +/* Signal callback */ +static void +signal_cb_nopty(int signo, int what, void *v) +{ + struct sudo_ev_siginfo_container *sc = v; + struct exec_closure_nopty *ec = sc->closure; + char signame[SIG2STR_MAX]; + debug_decl(signal_cb_nopty, SUDO_DEBUG_EXEC); + + if (ec->cmnd_pid == -1) + debug_return; + + if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + sudo_debug_printf(SUDO_DEBUG_DIAG, + "%s: evbase %p, command: %d, signo %s(%d), cstat %p", + __func__, ec->evbase, (int)ec->cmnd_pid, signame, signo, ec->cstat); + + switch (signo) { + case SIGCHLD: + handle_sigchld_nopty(ec); + if (ec->cmnd_pid == -1) { + /* Command exited or was killed, exit event loop. */ + sudo_ev_loopexit(ec->evbase); + } + debug_return; +#ifdef SIGINFO + case SIGINFO: +#endif + case SIGINT: + case SIGQUIT: + case SIGTSTP: + /* + * Only forward user-generated signals not sent by a process in + * the command's own process group. Signals sent by the kernel + * may include SIGTSTP when the user presses ^Z. Curses programs + * often trap ^Z and send SIGTSTP to their own pgrp, so we don't + * want to send an extra SIGTSTP. + */ + if (!USER_SIGNALED(sc->siginfo)) + debug_return; + if (sc->siginfo->si_pid != 0) { + pid_t si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == ec->ppgrp || si_pgrp == ec->cmnd_pid) + debug_return; + } else if (sc->siginfo->si_pid == ec->cmnd_pid) { + debug_return; + } + } + break; + default: + /* + * Do not forward signals sent by a process in the command's process + * group, as we don't want the command to indirectly kill itself. + * For example, this can happen with some versions of reboot that + * call kill(-1, SIGTERM) to kill all other processes. + */ + if (USER_SIGNALED(sc->siginfo) && sc->siginfo->si_pid != 0) { + pid_t si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == ec->ppgrp || si_pgrp == ec->cmnd_pid) + debug_return; + } else if (sc->siginfo->si_pid == ec->cmnd_pid) { + debug_return; + } + } + break; + } + + /* Send signal to command. */ + if (signo == SIGALRM) { + terminate_command(ec->cmnd_pid, false); + } else if (kill(ec->cmnd_pid, signo) != 0) { + sudo_warn("kill(%d, SIG%s)", (int)ec->cmnd_pid, signame); + } + + debug_return; +} + + +/* + * Fill in the exec closure and setup initial exec events. + * Allocates events for the signal pipe and error pipe. + */ +static void +fill_exec_closure_nopty(struct exec_closure_nopty *ec, + struct command_status *cstat, struct command_details *details, int errfd) +{ + debug_decl(fill_exec_closure_nopty, SUDO_DEBUG_EXEC); + + /* Fill in the non-event part of the closure. */ + ec->ppgrp = getpgrp(); + ec->cstat = cstat; + ec->details = details; + + /* Setup event base and events. */ + ec->evbase = details->evbase; + details->evbase = NULL; + + /* Event for command status via errfd. */ + ec->errpipe_event = sudo_ev_alloc(errfd, + SUDO_EV_READ|SUDO_EV_PERSIST, errpipe_cb, ec); + if (ec->errpipe_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->errpipe_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + sudo_debug_printf(SUDO_DEBUG_INFO, "error pipe fd %d\n", errfd); + + /* Events for local signals. */ + ec->sigint_event = sudo_ev_alloc(SIGINT, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigint_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigint_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigquit_event = sudo_ev_alloc(SIGQUIT, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigquit_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigquit_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigtstp_event = sudo_ev_alloc(SIGTSTP, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigtstp_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigtstp_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigterm_event = sudo_ev_alloc(SIGTERM, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigterm_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigterm_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sighup_event = sudo_ev_alloc(SIGHUP, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sighup_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sighup_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigalrm_event = sudo_ev_alloc(SIGALRM, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigalrm_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigalrm_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigpipe_event = sudo_ev_alloc(SIGPIPE, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigpipe_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigpipe_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigusr1_event = sudo_ev_alloc(SIGUSR1, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigusr1_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigusr1_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigusr2_event = sudo_ev_alloc(SIGUSR2, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigusr2_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigusr2_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigchld_event = sudo_ev_alloc(SIGCHLD, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigchld_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigchld_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigcont_event = sudo_ev_alloc(SIGCONT, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->sigcont_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigcont_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + +#ifdef SIGINFO + ec->siginfo_event = sudo_ev_alloc(SIGINFO, + SUDO_EV_SIGINFO, signal_cb_nopty, ec); + if (ec->siginfo_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->siginfo_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); +#endif + + /* Set the default event base. */ + sudo_ev_base_setdef(ec->evbase); + + debug_return; +} + +/* + * Free the dynamically-allocated contents of the exec closure. + */ +static void +free_exec_closure_nopty(struct exec_closure_nopty *ec) +{ + debug_decl(free_exec_closure_nopty, SUDO_DEBUG_EXEC); + + sudo_ev_base_free(ec->evbase); + sudo_ev_free(ec->errpipe_event); + sudo_ev_free(ec->sigint_event); + sudo_ev_free(ec->sigquit_event); + sudo_ev_free(ec->sigtstp_event); + sudo_ev_free(ec->sigterm_event); + sudo_ev_free(ec->sighup_event); + sudo_ev_free(ec->sigalrm_event); + sudo_ev_free(ec->sigpipe_event); + sudo_ev_free(ec->sigusr1_event); + sudo_ev_free(ec->sigusr2_event); + sudo_ev_free(ec->sigchld_event); + sudo_ev_free(ec->sigcont_event); + sudo_ev_free(ec->siginfo_event); + + debug_return; +} + +/* + * Execute a command and wait for it to finish. + */ +void +exec_nopty(struct command_details *details, struct command_status *cstat) +{ + struct exec_closure_nopty ec = { 0 }; + sigset_t set, oset; + int errpipe[2]; + debug_decl(exec_nopty, SUDO_DEBUG_EXEC); + + /* + * The policy plugin's session init must be run before we fork + * or certain pam modules won't be able to track their state. + */ + if (policy_init_session(details) != true) + sudo_fatalx("%s", U_("policy plugin failed session initialization")); + + /* + * We use a pipe to get errno if execve(2) fails in the child. + */ + if (pipe2(errpipe, O_CLOEXEC) != 0) + sudo_fatal("%s", U_("unable to create pipe")); + + /* + * Block signals until we have our handlers setup in the parent so + * we don't miss SIGCHLD if the command exits immediately. + */ + sigfillset(&set); + sigprocmask(SIG_BLOCK, &set, &oset); + + /* Check for early termination or suspend signals before we fork. */ + if (sudo_terminated(cstat)) { + sigprocmask(SIG_SETMASK, &oset, NULL); + debug_return; + } + +#ifdef HAVE_SELINUX + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + if (selinux_setup(details->selinux_role, details->selinux_type, + details->tty, -1, true) == -1) { + cstat->type = CMD_ERRNO; + cstat->val = errno; + debug_return; + } + } +#endif + + ec.cmnd_pid = sudo_debug_fork(); + switch (ec.cmnd_pid) { + case -1: + sudo_fatal("%s", U_("unable to fork")); + break; + case 0: + /* child */ + sigprocmask(SIG_SETMASK, &oset, NULL); + close(errpipe[0]); + exec_cmnd(details, errpipe[1]); + while (write(errpipe[1], &errno, sizeof(int)) == -1) { + if (errno != EINTR) + break; + } + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1); + _exit(EXIT_FAILURE); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", details->command, + (int)ec.cmnd_pid); + close(errpipe[1]); + + /* No longer need execfd. */ + if (details->execfd != -1) { + close(details->execfd); + details->execfd = -1; + } + + /* Set command timeout if specified. */ + if (ISSET(details->flags, CD_SET_TIMEOUT)) + alarm(details->timeout); + + /* + * Fill in exec closure, allocate event base, signal events and + * the error pipe event. + */ + fill_exec_closure_nopty(&ec, cstat, details, errpipe[0]); + + /* Restore signal mask now that signal handlers are setup. */ + sigprocmask(SIG_SETMASK, &oset, NULL); + + /* + * Non-pty event loop. + * Wait for command to exit, handles signals and the error pipe. + */ + if (sudo_ev_dispatch(ec.evbase) == -1) + sudo_warn("%s", U_("error in event loop")); + if (sudo_ev_got_break(ec.evbase)) { + /* error from callback */ + sudo_debug_printf(SUDO_DEBUG_ERROR, "event loop exited prematurely"); + /* kill command */ + terminate_command(ec.cmnd_pid, true); + ec.cmnd_pid = -1; + } + +#ifdef HAVE_SELINUX + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + if (selinux_restore_tty() != 0) + sudo_warnx("%s", U_("unable to restore tty label")); + } +#endif + + /* Free things up. */ + free_exec_closure_nopty(&ec); + debug_return; +} + +/* + * Wait for command status after receiving SIGCHLD. + * If the command exits, fill in cstat and stop the event loop. + * If the command stops, save the tty pgrp, suspend sudo, then restore + * the tty pgrp when sudo resumes. + */ +static void +handle_sigchld_nopty(struct exec_closure_nopty *ec) +{ + pid_t pid; + int status; + char signame[SIG2STR_MAX]; + debug_decl(handle_sigchld_nopty, SUDO_DEBUG_EXEC); + + /* Read command status. */ + do { + pid = waitpid(ec->cmnd_pid, &status, WUNTRACED|WNOHANG); + } while (pid == -1 && errno == EINTR); + switch (pid) { + case 0: + /* waitpid() will return 0 for SIGCONT, which we don't care about */ + debug_return; + case -1: + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + debug_return; + } + + if (WIFSTOPPED(status)) { + /* + * Save the controlling terminal's process group so we can restore it + * after we resume, if needed. Most well-behaved shells change the + * pgrp back to its original value before suspending so we must + * not try to restore in that case, lest we race with the command upon + * resume, potentially stopping sudo with SIGTTOU while the command + * continues to run. + */ + struct sigaction sa, osa; + pid_t saved_pgrp = -1; + int fd, signo = WSTOPSIG(status); + + if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) stopped, SIG%s", + __func__, (int)ec->cmnd_pid, signame); + + fd = open(_PATH_TTY, O_RDWR); + if (fd != -1) { + saved_pgrp = tcgetpgrp(fd); + if (saved_pgrp == -1) { + close(fd); + fd = -1; + } + } + if (saved_pgrp != -1) { + /* + * Command was stopped trying to access the controlling terminal. + * If the command has a different pgrp and we own the controlling + * terminal, give it to the command's pgrp and let it continue. + */ + if (signo == SIGTTOU || signo == SIGTTIN) { + if (saved_pgrp == ec->ppgrp) { + pid_t cmnd_pgrp = getpgid(ec->cmnd_pid); + if (cmnd_pgrp != ec->ppgrp) { + if (tcsetpgrp_nobg(fd, cmnd_pgrp) == 0) { + if (killpg(cmnd_pgrp, SIGCONT) != 0) { + sudo_warn("kill(%d, SIGCONT)", + (int)cmnd_pgrp); + } + close(fd); + goto done; + } + } + } + } + } + if (signo == SIGTSTP) { + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_DFL; + if (sudo_sigaction(SIGTSTP, &sa, &osa) != 0) { + sudo_warn(U_("unable to set handler for signal %d"), + SIGTSTP); + } + } + if (kill(getpid(), signo) != 0) + sudo_warn("kill(%d, SIG%s)", (int)getpid(), signame); + if (signo == SIGTSTP) { + if (sudo_sigaction(SIGTSTP, &osa, NULL) != 0) { + sudo_warn(U_("unable to restore handler for signal %d"), + SIGTSTP); + } + } + if (saved_pgrp != -1) { + /* + * On resume, restore foreground process group, if different. + * Otherwise, we cannot resume some shells (pdksh). + * + * It is possible that we are no longer the foreground process so + * use tcsetpgrp_nobg() to prevent sudo from receiving SIGTTOU. + */ + if (saved_pgrp != ec->ppgrp) + tcsetpgrp_nobg(fd, saved_pgrp); + close(fd); + } + } else { + /* Command has exited or been killed, we are done. */ + if (WIFSIGNALED(status)) { + if (sig2str(WTERMSIG(status), signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", WTERMSIG(status)); + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) killed, SIG%s", + __func__, (int)ec->cmnd_pid, signame); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) exited: %d", + __func__, (int)ec->cmnd_pid, WEXITSTATUS(status)); + } + /* Don't overwrite execve() failure with command exit status. */ + if (ec->cstat->type == CMD_INVALID) { + ec->cstat->type = CMD_WSTATUS; + ec->cstat->val = status; + } else { + sudo_debug_printf(SUDO_DEBUG_WARN, + "%s: not overwriting command status %d,%d with %d,%d", + __func__, ec->cstat->type, ec->cstat->val, CMD_WSTATUS, status); + } + ec->cmnd_pid = -1; + } +done: + debug_return; +} diff --git a/src/exec_pty.c b/src/exec_pty.c new file mode 100644 index 0000000..cfe4929 --- /dev/null +++ b/src/exec_pty.c @@ -0,0 +1,1897 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <sys/ioctl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <termios.h> /* for struct winsize on HP-UX */ + +#include "sudo.h" +#include "sudo_exec.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" + +/* Evaluates to true if the event has /dev/tty as its fd. */ +#define USERTTY_EVENT(_ev) (sudo_ev_get_fd((_ev)) == io_fds[SFD_USERTTY]) + +#define TERM_COOKED 0 +#define TERM_RAW 1 + +/* Tail queue of messages to send to the monitor. */ +struct monitor_message { + TAILQ_ENTRY(monitor_message) entries; + struct command_status cstat; +}; +TAILQ_HEAD(monitor_message_list, monitor_message); + +struct exec_closure_pty { + pid_t monitor_pid; + pid_t cmnd_pid; + pid_t ppgrp; + short rows; + short cols; + struct command_status *cstat; + struct command_details *details; + struct sudo_event_base *evbase; + struct sudo_event *backchannel_event; + struct sudo_event *fwdchannel_event; + struct sudo_event *sigint_event; + struct sudo_event *sigquit_event; + struct sudo_event *sigtstp_event; + struct sudo_event *sigterm_event; + struct sudo_event *sighup_event; + struct sudo_event *sigalrm_event; + struct sudo_event *sigusr1_event; + struct sudo_event *sigusr2_event; + struct sudo_event *sigchld_event; + struct sudo_event *sigwinch_event; + struct monitor_message_list monitor_messages; +}; + +/* + * I/O buffer with associated read/write events and a logging action. + * Used to, e.g. pass data from the pty to the user's terminal + * and any I/O logging plugins. + */ +struct io_buffer; +typedef bool (*sudo_io_action_t)(const char *, unsigned int, struct io_buffer *); +struct io_buffer { + SLIST_ENTRY(io_buffer) entries; + struct exec_closure_pty *ec; + struct sudo_event *revent; + struct sudo_event *wevent; + sudo_io_action_t action; + int len; /* buffer length (how much produced) */ + int off; /* write position (how much already consumed) */ + char buf[64 * 1024]; +}; +SLIST_HEAD(io_buffer_list, io_buffer); + +static char ptyname[PATH_MAX]; +int io_fds[6] = { -1, -1, -1, -1, -1, -1}; +static bool foreground, pipeline; +static int ttymode = TERM_COOKED; +static sigset_t ttyblock; +static struct io_buffer_list iobufs; +static const char *utmp_user; + +static void del_io_events(bool nonblocking); +static void sync_ttysize(struct exec_closure_pty *ec); +static int safe_close(int fd); +static void ev_free_by_fd(struct sudo_event_base *evbase, int fd); +static pid_t check_foreground(struct exec_closure_pty *ec); +static void add_io_events(struct sudo_event_base *evbase); +static void schedule_signal(struct exec_closure_pty *ec, int signo); + +/* + * Cleanup hook for sudo_fatal()/sudo_fatalx() + */ +void +pty_cleanup(void) +{ + debug_decl(cleanup, SUDO_DEBUG_EXEC); + + if (io_fds[SFD_USERTTY] != -1) + sudo_term_restore(io_fds[SFD_USERTTY], false); + if (utmp_user != NULL) + utmp_logout(ptyname, 0); + + debug_return; +} + +/* + * Allocate a pty if /dev/tty is a tty. + * Fills in io_fds[SFD_USERTTY], io_fds[SFD_LEADER], io_fds[SFD_FOLLOWER] + * and ptyname globals. + */ +static bool +pty_setup(struct command_details *details, const char *tty) +{ + debug_decl(pty_setup, SUDO_DEBUG_EXEC); + + io_fds[SFD_USERTTY] = open(_PATH_TTY, O_RDWR); + if (io_fds[SFD_USERTTY] == -1) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: no %s, not allocating a pty", + __func__, _PATH_TTY); + debug_return_bool(false); + } + + if (!get_pty(&io_fds[SFD_LEADER], &io_fds[SFD_FOLLOWER], + ptyname, sizeof(ptyname), details->cred.euid)) + sudo_fatal("%s", U_("unable to allocate pty")); + + /* Update tty name in command details (used by SELinux and AIX). */ + details->tty = ptyname; + + /* Add entry to utmp/utmpx? */ + if (ISSET(details->flags, CD_SET_UTMP)) { + utmp_user = + details->utmp_user ? details->utmp_user : user_details.username; + utmp_login(tty, ptyname, io_fds[SFD_FOLLOWER], utmp_user); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %s fd %d, pty leader fd %d, pty follower fd %d", + __func__, _PATH_TTY, io_fds[SFD_USERTTY], io_fds[SFD_LEADER], + io_fds[SFD_FOLLOWER]); + + debug_return_bool(true); +} + +/* + * Make the tty follower the controlling tty. + * This is only used by the monitor but ptyname[] is static. + */ +int +pty_make_controlling(void) +{ + if (io_fds[SFD_FOLLOWER] != -1) { +#ifdef TIOCSCTTY + if (ioctl(io_fds[SFD_FOLLOWER], TIOCSCTTY, NULL) != 0) + return -1; +#else + /* Set controlling tty by reopening pty follower. */ + int fd = open(ptyname, O_RDWR); + if (fd == -1) + return -1; + close(fd); +#endif + } + return 0; +} + +/* Call I/O plugin tty input log method. */ +static bool +log_ttyin(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_ttyin, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_ttyin) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_ttyin(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_ttyin = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin stdin log method. */ +static bool +log_stdin(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_stdin, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_stdin) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_stdin(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_stdin = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin tty output log method. */ +static bool +log_ttyout(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_ttyout, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_ttyout) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_ttyout(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_ttyout = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + if (!ret) { + /* + * I/O plugin rejected the output, delete the write event + * (user's tty) so we do not display the rejected output. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing devtty wevent %p", __func__, iob->wevent); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + iob->off = iob->len = 0; + } + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin stdout log method. */ +static bool +log_stdout(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_stdout, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_stdout) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_stdout(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_stdout = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + if (!ret) { + /* + * I/O plugin rejected the output, delete the write event + * (user's stdout) so we do not display the rejected output. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing stdout wevent %p", __func__, iob->wevent); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + iob->off = iob->len = 0; + } + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin stderr log method. */ +static bool +log_stderr(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_stderr, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_stderr) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_stderr(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_stderr = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + if (!ret) { + /* + * I/O plugin rejected the output, delete the write event + * (user's stderr) so we do not display the rejected output. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing stderr wevent %p", __func__, iob->wevent); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + iob->off = iob->len = 0; + } + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin suspend log method. */ +static void +log_suspend(struct exec_closure_pty *ec, int signo) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + debug_decl(log_suspend, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->version < SUDO_API_MKVERSION(1, 13)) + continue; + if (plugin->u.io->log_suspend) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_suspend(signo, &errstr); + if (rc <= 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_suspend = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("error logging suspend"), + ec->details->info); + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return; +} + +/* Call I/O plugin window change log method. */ +static void +log_winchange(struct exec_closure_pty *ec, unsigned int rows, unsigned int cols) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + debug_decl(log_winchange, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->version < SUDO_API_MKVERSION(1, 12)) + continue; + if (plugin->u.io->change_winsize) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->change_winsize(rows, cols, &errstr); + if (rc <= 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->change_winsize = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("error changing window size"), + ec->details->info); + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return; +} + +/* + * Check whether we are running in the foregroup. + * Updates the foreground global and updates the window size. + * Returns 0 if there is no tty, the foreground process group ID + * on success, or -1 on failure (tty revoked). + */ +static pid_t +check_foreground(struct exec_closure_pty *ec) +{ + int ret = 0; + debug_decl(check_foreground, SUDO_DEBUG_EXEC); + + if (io_fds[SFD_USERTTY] != -1) { + if ((ret = tcgetpgrp(io_fds[SFD_USERTTY])) != -1) { + foreground = ret == ec->ppgrp; + + /* Also check for window size changes. */ + sync_ttysize(ec); + } + } + debug_return_int(ret); +} + +/* + * Suspend sudo if the underlying command is suspended. + * Returns SIGCONT_FG if the command should be resumed in the + * foreground or SIGCONT_BG if it is a background process. + */ +static int +suspend_sudo(struct exec_closure_pty *ec, int signo) +{ + char signame[SIG2STR_MAX]; + struct sigaction sa, osa; + int ret = 0; + debug_decl(suspend_sudo, SUDO_DEBUG_EXEC); + + switch (signo) { + case SIGTTOU: + case SIGTTIN: + /* + * If sudo is already the foreground process, just resume the command + * in the foreground. If not, we'll suspend sudo and resume later. + */ + if (!foreground) { + if (check_foreground(ec) == -1) { + /* User's tty was revoked. */ + break; + } + } + if (foreground) { + if (ttymode != TERM_RAW) { + if (sudo_term_raw(io_fds[SFD_USERTTY], 0)) + ttymode = TERM_RAW; + } + ret = SIGCONT_FG; /* resume command in foreground */ + break; + } + FALLTHROUGH; + case SIGSTOP: + case SIGTSTP: + /* Flush any remaining output and deschedule I/O events. */ + del_io_events(true); + + /* Restore original tty mode before suspending. */ + if (ttymode != TERM_COOKED) + sudo_term_restore(io_fds[SFD_USERTTY], false); + + /* Log the suspend event. */ + log_suspend(ec, signo); + + if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + + /* Suspend self and continue command when we resume. */ + if (signo != SIGSTOP) { + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_DFL; + if (sudo_sigaction(signo, &sa, &osa) != 0) + sudo_warn(U_("unable to set handler for signal %d"), signo); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "kill parent SIG%s", signame); + if (killpg(ec->ppgrp, signo) != 0) + sudo_warn("killpg(%d, SIG%s)", (int)ec->ppgrp, signame); + + /* Log the resume event. */ + log_suspend(ec, SIGCONT); + + /* Check foreground/background status on resume. */ + if (check_foreground(ec) == -1) { + /* User's tty was revoked. */ + break; + } + + /* + * We always resume the command in the foreground if sudo itself + * is the foreground process. This helps work around poorly behaved + * programs that catch SIGTTOU/SIGTTIN but suspend themselves with + * SIGSTOP. At worst, sudo will go into the background but upon + * resume the command will be runnable. Otherwise, we can get into + * a situation where the command will immediately suspend itself. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, "parent is in %s, ttymode %d -> %d", + foreground ? "foreground" : "background", ttymode, + foreground ? TERM_RAW : TERM_COOKED); + + if (foreground) { + /* Foreground process, set tty to raw mode. */ + if (sudo_term_raw(io_fds[SFD_USERTTY], 0)) + ttymode = TERM_RAW; + } else { + /* Background process, no access to tty. */ + ttymode = TERM_COOKED; + } + + if (signo != SIGSTOP) { + if (sudo_sigaction(signo, &osa, NULL) != 0) + sudo_warn(U_("unable to restore handler for signal %d"), signo); + } + + ret = ttymode == TERM_RAW ? SIGCONT_FG : SIGCONT_BG; + break; + } + + debug_return_int(ret); +} + +/* + * SIGTTIN signal handler for read_callback that just sets a flag. + */ +static volatile sig_atomic_t got_sigttin; + +static void +sigttin(int signo) +{ + got_sigttin = 1; +} + +/* + * Read an iobuf that is ready. + */ +static void +read_callback(int fd, int what, void *v) +{ + struct io_buffer *iob = v; + struct sudo_event_base *evbase = sudo_ev_get_base(iob->revent); + struct sigaction sa, osa; + int saved_errno; + ssize_t n; + debug_decl(read_callback, SUDO_DEBUG_EXEC); + + /* + * We ignore SIGTTIN by default but we need to handle it when reading + * from the terminal. A signal event won't work here because the + * read() would be restarted, preventing the callback from running. + */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_handler = sigttin; + got_sigttin = 0; + sigaction(SIGTTIN, &sa, &osa); + n = read(fd, iob->buf + iob->len, sizeof(iob->buf) - iob->len); + saved_errno = errno; + sigaction(SIGTTIN, &osa, NULL); + errno = saved_errno; + + switch (n) { + case -1: + if (got_sigttin) { + /* Schedule SIGTTIN to be forwarded to the command. */ + schedule_signal(iob->ec, SIGTTIN); + } + if (errno == EAGAIN || errno == EINTR) + break; + /* treat read error as fatal and close the fd */ + sudo_debug_printf(SUDO_DEBUG_ERROR, + "error reading fd %d: %s", fd, strerror(errno)); + FALLTHROUGH; + case 0: + /* got EOF or pty has gone away */ + if (n == 0) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "read EOF from fd %d", fd); + } + safe_close(fd); + ev_free_by_fd(evbase, fd); + /* If writer already consumed the buffer, close it too. */ + if (iob->wevent != NULL && iob->off == iob->len) { + safe_close(sudo_ev_get_fd(iob->wevent)); + ev_free_by_fd(evbase, sudo_ev_get_fd(iob->wevent)); + iob->off = iob->len = 0; + } + break; + default: + sudo_debug_printf(SUDO_DEBUG_INFO, + "read %zd bytes from fd %d", n, fd); + if (!iob->action(iob->buf + iob->len, n, iob)) { + terminate_command(iob->ec->cmnd_pid, true); + iob->ec->cmnd_pid = -1; + } + iob->len += n; + /* Enable writer now that there is data in the buffer. */ + if (iob->wevent != NULL) { + if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + /* Re-enable reader if buffer is not full. */ + if (iob->len != sizeof(iob->buf)) { + if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + break; + } +} + +/* + * SIGTTOU signal handler for write_callback that just sets a flag. + */ +static volatile sig_atomic_t got_sigttou; + +static void +sigttou(int signo) +{ + got_sigttou = 1; +} + +/* + * Write an iobuf that is ready. + */ +static void +write_callback(int fd, int what, void *v) +{ + struct io_buffer *iob = v; + struct sudo_event_base *evbase = sudo_ev_get_base(iob->wevent); + struct sigaction sa, osa; + int saved_errno; + ssize_t n; + debug_decl(write_callback, SUDO_DEBUG_EXEC); + + /* + * We ignore SIGTTOU by default but we need to handle it when writing + * to the terminal. A signal event won't work here because the + * write() would be restarted, preventing the callback from running. + */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_handler = sigttou; + got_sigttou = 0; + sigaction(SIGTTOU, &sa, &osa); + n = write(fd, iob->buf + iob->off, iob->len - iob->off); + saved_errno = errno; + sigaction(SIGTTOU, &osa, NULL); + errno = saved_errno; + + if (n == -1) { + switch (errno) { + case EPIPE: + case ENXIO: + case EIO: + case EBADF: + /* other end of pipe closed or pty revoked */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "unable to write %d bytes to fd %d", + iob->len - iob->off, fd); + /* Close reader if there is one. */ + if (iob->revent != NULL) { + safe_close(sudo_ev_get_fd(iob->revent)); + ev_free_by_fd(evbase, sudo_ev_get_fd(iob->revent)); + } + safe_close(fd); + ev_free_by_fd(evbase, fd); + break; + case EINTR: + if (got_sigttou) { + /* Schedule SIGTTOU to be forwarded to the command. */ + schedule_signal(iob->ec, SIGTTOU); + } + FALLTHROUGH; + case EAGAIN: + /* not an error */ + break; + default: + /* XXX - need a way to distinguish non-exec error. */ + iob->ec->cstat->type = CMD_ERRNO; + iob->ec->cstat->val = errno; + sudo_debug_printf(SUDO_DEBUG_ERROR, + "error writing fd %d: %s", fd, strerror(errno)); + sudo_ev_loopbreak(evbase); + break; + } + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "wrote %zd bytes to fd %d", n, fd); + iob->off += n; + /* Reset buffer if fully consumed. */ + if (iob->off == iob->len) { + iob->off = iob->len = 0; + /* Forward the EOF from reader to writer. */ + if (iob->revent == NULL) { + safe_close(fd); + ev_free_by_fd(evbase, fd); + } + } + /* Re-enable writer if buffer is not empty. */ + if (iob->len > iob->off) { + if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + /* Enable reader if buffer is not full. */ + if (iob->revent != NULL && + (ttymode == TERM_RAW || !USERTTY_EVENT(iob->revent))) { + if (iob->len != sizeof(iob->buf)) { + if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + } +} + +static void +io_buf_new(int rfd, int wfd, + bool (*action)(const char *, unsigned int, struct io_buffer *), + struct exec_closure_pty *ec, struct io_buffer_list *head) +{ + int n; + struct io_buffer *iob; + debug_decl(io_buf_new, SUDO_DEBUG_EXEC); + + /* Set non-blocking mode. */ + n = fcntl(rfd, F_GETFL, 0); + if (n != -1 && !ISSET(n, O_NONBLOCK)) + (void) fcntl(rfd, F_SETFL, n | O_NONBLOCK); + n = fcntl(wfd, F_GETFL, 0); + if (n != -1 && !ISSET(n, O_NONBLOCK)) + (void) fcntl(wfd, F_SETFL, n | O_NONBLOCK); + + /* Allocate and add to head of list. */ + if ((iob = malloc(sizeof(*iob))) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + iob->ec = ec; + iob->revent = sudo_ev_alloc(rfd, SUDO_EV_READ, read_callback, iob); + iob->wevent = sudo_ev_alloc(wfd, SUDO_EV_WRITE, write_callback, iob); + iob->len = 0; + iob->off = 0; + iob->action = action; + iob->buf[0] = '\0'; + if (iob->revent == NULL || iob->wevent == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + SLIST_INSERT_HEAD(head, iob, entries); + + debug_return; +} + +/* + * We already closed the follower so reads from the leader will not block. + */ +static void +pty_finish(struct command_status *cstat) +{ + struct io_buffer *iob; + int n; + debug_decl(pty_finish, SUDO_DEBUG_EXEC); + + /* Flush any remaining output (the plugin already got it). */ + if (io_fds[SFD_USERTTY] != -1) { + n = fcntl(io_fds[SFD_USERTTY], F_GETFL, 0); + if (n != -1 && ISSET(n, O_NONBLOCK)) { + CLR(n, O_NONBLOCK); + (void) fcntl(io_fds[SFD_USERTTY], F_SETFL, n); + } + } + del_io_events(false); + + /* Free I/O buffers. */ + while ((iob = SLIST_FIRST(&iobufs)) != NULL) { + SLIST_REMOVE_HEAD(&iobufs, entries); + if (iob->revent != NULL) + sudo_ev_free(iob->revent); + if (iob->wevent != NULL) + sudo_ev_free(iob->wevent); + free(iob); + } + + /* Restore terminal settings. */ + if (io_fds[SFD_USERTTY] != -1) + sudo_term_restore(io_fds[SFD_USERTTY], false); + + /* Update utmp */ + if (utmp_user != NULL) + utmp_logout(ptyname, cstat->type == CMD_WSTATUS ? cstat->val : 0); + + debug_return; +} + +/* + * Send command status to the monitor (signal or window size change). + */ +static void +send_command_status(struct exec_closure_pty *ec, int type, int val) +{ + struct monitor_message *msg; + debug_decl(send_command, SUDO_DEBUG_EXEC); + + if ((msg = calloc(1, sizeof(*msg))) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + msg->cstat.type = type; + msg->cstat.val = val; + TAILQ_INSERT_TAIL(&ec->monitor_messages, msg, entries); + + if (sudo_ev_add(ec->evbase, ec->fwdchannel_event, NULL, true) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + /* Restart event loop to send the command immediately. */ + sudo_ev_loopcontinue(ec->evbase); + + debug_return; +} + +/* + * Schedule a signal to be forwarded. + */ +static void +schedule_signal(struct exec_closure_pty *ec, int signo) +{ + char signame[SIG2STR_MAX]; + debug_decl(schedule_signal, SUDO_DEBUG_EXEC); + + if (signo == 0) + debug_return; + + if (signo == SIGCONT_FG) + strlcpy(signame, "CONT_FG", sizeof(signame)); + else if (signo == SIGCONT_BG) + strlcpy(signame, "CONT_BG", sizeof(signame)); + else if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + sudo_debug_printf(SUDO_DEBUG_DIAG, "scheduled SIG%s for command", signame); + + send_command_status(ec, CMD_SIGNO, signo); + + debug_return; +} + +static void +backchannel_cb(int fd, int what, void *v) +{ + struct exec_closure_pty *ec = v; + struct command_status cstat; + ssize_t nread; + debug_decl(backchannel_cb, SUDO_DEBUG_EXEC); + + /* + * Read command status from the monitor. + * Note that the backchannel is a *blocking* socket. + */ + nread = recv(fd, &cstat, sizeof(cstat), MSG_WAITALL); + switch (nread) { + case -1: + switch (errno) { + case EINTR: + case EAGAIN: + /* Nothing ready. */ + break; + default: + if (ec->cstat->val == CMD_INVALID) { + ec->cstat->type = CMD_ERRNO; + ec->cstat->val = errno; + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: failed to read command status: %s", + __func__, strerror(errno)); + sudo_ev_loopbreak(ec->evbase); + } + break; + } + break; + case 0: + /* EOF, monitor exited or was killed. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "EOF on backchannel, monitor dead?"); + if (ec->cstat->type == CMD_INVALID) { + /* XXX - need new CMD_ type for monitor errors. */ + ec->cstat->type = CMD_ERRNO; + ec->cstat->val = ECONNRESET; + } + sudo_ev_loopexit(ec->evbase); + break; + case sizeof(cstat): + /* Check command status. */ + switch (cstat.type) { + case CMD_PID: + ec->cmnd_pid = cstat.val; + sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", + ec->details->command, (int)ec->cmnd_pid); + break; + case CMD_WSTATUS: + if (WIFSTOPPED(cstat.val)) { + int signo; + + /* Suspend parent and tell monitor how to resume on return. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "command stopped, suspending parent"); + signo = suspend_sudo(ec, WSTOPSIG(cstat.val)); + schedule_signal(ec, signo); + /* Re-enable I/O events */ + add_io_events(ec->evbase); + } else { + /* Command exited or was killed, either way we are done. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "command exited or was killed"); + sudo_ev_loopexit(ec->evbase); + } + *ec->cstat = cstat; + break; + case CMD_ERRNO: + /* Monitor was unable to execute command or broken pipe. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "errno from monitor: %s", + strerror(cstat.val)); + sudo_ev_loopbreak(ec->evbase); + *ec->cstat = cstat; + break; + } + /* Keep reading command status messages until EAGAIN or EOF. */ + break; + default: + /* Short read, should not happen. */ + if (ec->cstat->val == CMD_INVALID) { + ec->cstat->type = CMD_ERRNO; + ec->cstat->val = EIO; + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: failed to read command status: short read", __func__); + sudo_ev_loopbreak(ec->evbase); + } + break; + } + debug_return; +} + +/* + * Handle changes to the monitors's status (SIGCHLD). + */ +static void +handle_sigchld_pty(struct exec_closure_pty *ec) +{ + int n, status; + pid_t pid; + debug_decl(handle_sigchld_pty, SUDO_DEBUG_EXEC); + + /* + * Monitor process was signaled; wait for it as needed. + */ + do { + pid = waitpid(ec->monitor_pid, &status, WUNTRACED|WNOHANG); + } while (pid == -1 && errno == EINTR); + switch (pid) { + case 0: + errno = ECHILD; + FALLTHROUGH; + case -1: + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + debug_return; + } + + /* + * If the monitor dies we get notified via backchannel_cb(). + * If it was stopped, we should stop too (the command keeps + * running in its pty) and continue it when we come back. + */ + if (WIFSTOPPED(status)) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "monitor stopped, suspending sudo"); + n = suspend_sudo(ec, WSTOPSIG(status)); + kill(pid, SIGCONT); + schedule_signal(ec, n); + /* Re-enable I/O events */ + add_io_events(ec->evbase); + } else if (WIFSIGNALED(status)) { + char signame[SIG2STR_MAX]; + if (sig2str(WTERMSIG(status), signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", WTERMSIG(status)); + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: monitor (%d) killed, SIG%s", + __func__, (int)ec->monitor_pid, signame); + ec->monitor_pid = -1; + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: monitor exited, status %d", __func__, WEXITSTATUS(status)); + ec->monitor_pid = -1; + } + debug_return; +} + +/* Signal callback */ +static void +signal_cb_pty(int signo, int what, void *v) +{ + struct sudo_ev_siginfo_container *sc = v; + struct exec_closure_pty *ec = sc->closure; + char signame[SIG2STR_MAX]; + debug_decl(signal_cb_pty, SUDO_DEBUG_EXEC); + + if (ec->monitor_pid == -1) + debug_return; + + if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + sudo_debug_printf(SUDO_DEBUG_DIAG, + "%s: evbase %p, monitor: %d, signo %s(%d), cstat %p", __func__, + ec->evbase, (int)ec->monitor_pid, signame, signo, ec->cstat); + + switch (signo) { + case SIGCHLD: + handle_sigchld_pty(ec); + break; + case SIGWINCH: + sync_ttysize(ec); + break; + default: + /* + * Do not forward signals sent by a process in the command's process + * group, as we don't want the command to indirectly kill itself. + * For example, this can happen with some versions of reboot that + * call kill(-1, SIGTERM) to kill all other processes. + */ + if (USER_SIGNALED(sc->siginfo) && sc->siginfo->si_pid != 0) { + pid_t si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == ec->ppgrp || si_pgrp == ec->cmnd_pid) + debug_return; + } else if (sc->siginfo->si_pid == ec->cmnd_pid) { + debug_return; + } + } + /* Schedule signal to be forwarded to the command. */ + schedule_signal(ec, signo); + break; + } + + debug_return; +} + +/* + * Forward signals in monitor_messages to the monitor so it can + * deliver them to the command. + */ +static void +fwdchannel_cb(int sock, int what, void *v) +{ + struct exec_closure_pty *ec = v; + char signame[SIG2STR_MAX]; + struct monitor_message *msg; + ssize_t nsent; + debug_decl(fwdchannel_cb, SUDO_DEBUG_EXEC); + + while ((msg = TAILQ_FIRST(&ec->monitor_messages)) != NULL) { + switch (msg->cstat.type) { + case CMD_SIGNO: + if (msg->cstat.val == SIGCONT_FG) + strlcpy(signame, "CONT_FG", sizeof(signame)); + else if (msg->cstat.val == SIGCONT_BG) + strlcpy(signame, "CONT_BG", sizeof(signame)); + else if (sig2str(msg->cstat.val, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", msg->cstat.val); + sudo_debug_printf(SUDO_DEBUG_INFO, + "sending SIG%s to monitor over backchannel", signame); + break; + case CMD_TTYWINCH: + sudo_debug_printf(SUDO_DEBUG_INFO, "sending window size change " + "to monitor over backchannelL %d x %d", + msg->cstat.val & 0xffff, (msg->cstat.val >> 16) & 0xffff); + break; + default: + sudo_debug_printf(SUDO_DEBUG_INFO, + "sending cstat type %d, value %d to monitor over backchannel", + msg->cstat.type, msg->cstat.val); + break; + } + TAILQ_REMOVE(&ec->monitor_messages, msg, entries); + nsent = send(sock, &msg->cstat, sizeof(msg->cstat), 0); + if (nsent != sizeof(msg->cstat)) { + if (errno == EPIPE) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "broken pipe writing to monitor over backchannel"); + /* Other end of socket gone, empty out monitor_messages. */ + free(msg); + while ((msg = TAILQ_FIRST(&ec->monitor_messages)) != NULL) { + TAILQ_REMOVE(&ec->monitor_messages, msg, entries); + free(msg); + } + /* XXX - need new CMD_ type for monitor errors. */ + ec->cstat->type = CMD_ERRNO; + ec->cstat->val = errno; + sudo_ev_loopbreak(ec->evbase); + } + break; + } + free(msg); + } +} + +/* + * Fill in the exec closure and setup initial exec events. + * Allocates events for the signal pipe and backchannel. + * Forwarded signals on the backchannel are enabled on demand. + */ +static void +fill_exec_closure_pty(struct exec_closure_pty *ec, struct command_status *cstat, + struct command_details *details, pid_t ppgrp, int backchannel) +{ + debug_decl(fill_exec_closure_pty, SUDO_DEBUG_EXEC); + + /* Fill in the non-event part of the closure. */ + ec->cmnd_pid = -1; + ec->ppgrp = ppgrp; + ec->cstat = cstat; + ec->details = details; + ec->rows = user_details.ts_rows; + ec->cols = user_details.ts_cols; + TAILQ_INIT(&ec->monitor_messages); + + /* Reset cstat for running the command. */ + cstat->type = CMD_INVALID; + cstat->val = 0; + + /* Setup event base and events. */ + ec->evbase = details->evbase; + details->evbase = NULL; + + /* Event for command status via backchannel. */ + ec->backchannel_event = sudo_ev_alloc(backchannel, + SUDO_EV_READ|SUDO_EV_PERSIST, backchannel_cb, ec); + if (ec->backchannel_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->backchannel_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + sudo_debug_printf(SUDO_DEBUG_INFO, "backchannel fd %d\n", backchannel); + + /* Events for local signals. */ + ec->sigint_event = sudo_ev_alloc(SIGINT, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigint_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigint_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigquit_event = sudo_ev_alloc(SIGQUIT, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigquit_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigquit_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigtstp_event = sudo_ev_alloc(SIGTSTP, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigtstp_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigtstp_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigterm_event = sudo_ev_alloc(SIGTERM, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigterm_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigterm_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sighup_event = sudo_ev_alloc(SIGHUP, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sighup_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sighup_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigalrm_event = sudo_ev_alloc(SIGALRM, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigalrm_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigalrm_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigusr1_event = sudo_ev_alloc(SIGUSR1, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigusr1_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigusr1_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigusr2_event = sudo_ev_alloc(SIGUSR2, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigusr2_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigusr2_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigchld_event = sudo_ev_alloc(SIGCHLD, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigchld_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigchld_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + ec->sigwinch_event = sudo_ev_alloc(SIGWINCH, + SUDO_EV_SIGINFO, signal_cb_pty, ec); + if (ec->sigwinch_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(ec->evbase, ec->sigwinch_event, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + + /* The signal forwarding event gets added on demand. */ + ec->fwdchannel_event = sudo_ev_alloc(backchannel, + SUDO_EV_WRITE, fwdchannel_cb, ec); + if (ec->fwdchannel_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* Set the default event base. */ + sudo_ev_base_setdef(ec->evbase); + + debug_return; +} + +/* + * Free the dynamically-allocated contents of the exec closure. + */ +static void +free_exec_closure_pty(struct exec_closure_pty *ec) +{ + struct monitor_message *msg; + debug_decl(free_exec_closure_pty, SUDO_DEBUG_EXEC); + + sudo_ev_base_free(ec->evbase); + sudo_ev_free(ec->backchannel_event); + sudo_ev_free(ec->fwdchannel_event); + sudo_ev_free(ec->sigint_event); + sudo_ev_free(ec->sigquit_event); + sudo_ev_free(ec->sigtstp_event); + sudo_ev_free(ec->sigterm_event); + sudo_ev_free(ec->sighup_event); + sudo_ev_free(ec->sigalrm_event); + sudo_ev_free(ec->sigusr1_event); + sudo_ev_free(ec->sigusr2_event); + sudo_ev_free(ec->sigchld_event); + sudo_ev_free(ec->sigwinch_event); + + while ((msg = TAILQ_FIRST(&ec->monitor_messages)) != NULL) { + TAILQ_REMOVE(&ec->monitor_messages, msg, entries); + free(msg); + } + + debug_return; +} + +/* + * Execute a command in a pty, potentially with I/O logging, and + * wait for it to finish. + * This is a little bit tricky due to how POSIX job control works and + * we fact that we have two different controlling terminals to deal with. + */ +bool +exec_pty(struct command_details *details, struct command_status *cstat) +{ + int io_pipe[3][2] = { { -1, -1 }, { -1, -1 }, { -1, -1 } }; + bool interpose[3] = { false, false, false }; + struct exec_closure_pty ec = { 0 }; + struct plugin_container *plugin; + int evloop_retries = -1; + sigset_t set, oset; + struct sigaction sa; + struct stat sb; + pid_t ppgrp; + int sv[2]; + debug_decl(exec_pty, SUDO_DEBUG_EXEC); + + /* + * Allocate a pty. + */ + if (!pty_setup(details, user_details.tty)) { + if (TAILQ_EMPTY(&io_plugins)) { + /* Not logging I/O and didn't allocate a pty. */ + debug_return_bool(false); + } + } + + /* + * We communicate with the monitor over a bi-directional pair of sockets. + * Parent sends signal info to monitor and monitor sends back wait status. + */ + if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1 || + fcntl(sv[0], F_SETFD, FD_CLOEXEC) == -1 || + fcntl(sv[1], F_SETFD, FD_CLOEXEC) == -1) + sudo_fatal("%s", U_("unable to create sockets")); + + /* + * We don't want to receive SIGTTIN/SIGTTOU. + * XXX - this affects tcsetattr() and tcsetpgrp() too. + */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_IGN; + if (sudo_sigaction(SIGTTIN, &sa, NULL) != 0) + sudo_warn(U_("unable to set handler for signal %d"), SIGTTIN); + if (sudo_sigaction(SIGTTOU, &sa, NULL) != 0) + sudo_warn(U_("unable to set handler for signal %d"), SIGTTOU); + + /* + * The policy plugin's session init must be run before we fork + * or certain pam modules won't be able to track their state. + */ + if (policy_init_session(details) != true) + sudo_fatalx("%s", U_("policy plugin failed session initialization")); + + /* + * Child will run the command in the pty, parent will pass data + * to and from pty. + */ + + /* So we can block tty-generated signals */ + sigemptyset(&ttyblock); + sigaddset(&ttyblock, SIGINT); + sigaddset(&ttyblock, SIGQUIT); + sigaddset(&ttyblock, SIGTSTP); + sigaddset(&ttyblock, SIGTTIN); + sigaddset(&ttyblock, SIGTTOU); + + ppgrp = getpgrp(); /* parent's pgrp, so child can signal us */ + + /* Determine whether any of std{in,out,err} should be logged. */ + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_stdin) + interpose[STDIN_FILENO] = true; + if (plugin->u.io->log_stdout) + interpose[STDOUT_FILENO] = true; + if (plugin->u.io->log_stderr) + interpose[STDERR_FILENO] = true; + } + + /* + * Setup stdin/stdout/stderr for command, to be duped after forking. + * In background mode there is no stdin. + */ + if (!ISSET(details->flags, CD_BACKGROUND)) + io_fds[SFD_STDIN] = io_fds[SFD_FOLLOWER]; + io_fds[SFD_STDOUT] = io_fds[SFD_FOLLOWER]; + io_fds[SFD_STDERR] = io_fds[SFD_FOLLOWER]; + + if (io_fds[SFD_USERTTY] != -1) { + /* Read from /dev/tty, write to pty leader */ + if (!ISSET(details->flags, CD_BACKGROUND)) { + io_buf_new(io_fds[SFD_USERTTY], io_fds[SFD_LEADER], + log_ttyin, &ec, &iobufs); + } + + /* Read from pty leader, write to /dev/tty */ + io_buf_new(io_fds[SFD_LEADER], io_fds[SFD_USERTTY], + log_ttyout, &ec, &iobufs); + + /* Are we the foreground process? */ + foreground = tcgetpgrp(io_fds[SFD_USERTTY]) == ppgrp; + } + + /* + * If stdin, stdout or stderr is not a tty and logging is enabled, + * use a pipe to interpose ourselves instead of using the pty fd. + */ + if (io_fds[SFD_STDIN] == -1 || !isatty(STDIN_FILENO)) { + if (!interpose[STDIN_FILENO]) { + /* Not logging stdin, do not interpose. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdin not a tty, not logging"); + if (fstat(STDIN_FILENO, &sb) == 0 && S_ISFIFO(sb.st_mode)) + pipeline = true; + io_fds[SFD_STDIN] = dup(STDIN_FILENO); + if (io_fds[SFD_STDIN] == -1) + sudo_fatal("dup"); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdin not a tty, creating a pipe"); + pipeline = true; + if (pipe2(io_pipe[STDIN_FILENO], O_CLOEXEC) != 0) + sudo_fatal("%s", U_("unable to create pipe")); + io_buf_new(STDIN_FILENO, io_pipe[STDIN_FILENO][1], + log_stdin, &ec, &iobufs); + io_fds[SFD_STDIN] = io_pipe[STDIN_FILENO][0]; + } + } + if (io_fds[SFD_STDOUT] == -1 || !isatty(STDOUT_FILENO)) { + if (!interpose[STDOUT_FILENO]) { + /* Not logging stdout, do not interpose. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdout not a tty, not logging"); + if (fstat(STDOUT_FILENO, &sb) == 0 && S_ISFIFO(sb.st_mode)) + pipeline = true; + io_fds[SFD_STDOUT] = dup(STDOUT_FILENO); + if (io_fds[SFD_STDOUT] == -1) + sudo_fatal("dup"); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdout not a tty, creating a pipe"); + pipeline = true; + if (pipe2(io_pipe[STDOUT_FILENO], O_CLOEXEC) != 0) + sudo_fatal("%s", U_("unable to create pipe")); + io_buf_new(io_pipe[STDOUT_FILENO][0], STDOUT_FILENO, + log_stdout, &ec, &iobufs); + io_fds[SFD_STDOUT] = io_pipe[STDOUT_FILENO][1]; + } + } + if (io_fds[SFD_STDERR] == -1 || !isatty(STDERR_FILENO)) { + if (!interpose[STDERR_FILENO]) { + /* Not logging stderr, do not interpose. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "stderr not a tty, not logging"); + if (fstat(STDERR_FILENO, &sb) == 0 && S_ISFIFO(sb.st_mode)) + pipeline = true; + io_fds[SFD_STDERR] = dup(STDERR_FILENO); + if (io_fds[SFD_STDERR] == -1) + sudo_fatal("dup"); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "stderr not a tty, creating a pipe"); + if (pipe2(io_pipe[STDERR_FILENO], O_CLOEXEC) != 0) + sudo_fatal("%s", U_("unable to create pipe")); + io_buf_new(io_pipe[STDERR_FILENO][0], STDERR_FILENO, + log_stderr, &ec, &iobufs); + io_fds[SFD_STDERR] = io_pipe[STDERR_FILENO][1]; + } + } + + if (foreground) { + /* Copy terminal attrs from user tty -> pty follower. */ + if (!sudo_term_copy(io_fds[SFD_USERTTY], io_fds[SFD_FOLLOWER])) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to copy terminal settings to pty", __func__); + foreground = false; + } else { + /* Start in raw mode unless part of a pipeline or backgrounded. */ + if (!pipeline && !ISSET(details->flags, CD_EXEC_BG)) { + if (sudo_term_raw(io_fds[SFD_USERTTY], 0)) + ttymode = TERM_RAW; + } + } + } + + /* + * Block signals until we have our handlers setup in the parent so + * we don't miss SIGCHLD if the command exits immediately. + */ + sigfillset(&set); + sigprocmask(SIG_BLOCK, &set, &oset); + + /* Check for early termination or suspend signals before we fork. */ + if (sudo_terminated(cstat)) { + sigprocmask(SIG_SETMASK, &oset, NULL); + debug_return_int(true); + } + + ec.monitor_pid = sudo_debug_fork(); + switch (ec.monitor_pid) { + case -1: + sudo_fatal("%s", U_("unable to fork")); + break; + case 0: + /* child */ + close(sv[0]); + /* Close the other end of the stdin/stdout/stderr pipes and exec. */ + if (io_pipe[STDIN_FILENO][1] != -1) + close(io_pipe[STDIN_FILENO][1]); + if (io_pipe[STDOUT_FILENO][0] != -1) + close(io_pipe[STDOUT_FILENO][0]); + if (io_pipe[STDERR_FILENO][0] != -1) + close(io_pipe[STDERR_FILENO][0]); + /* + * If stdin/stdout is not a tty, start command in the background + * since it might be part of a pipeline that reads from /dev/tty. + * In this case, we rely on the command receiving SIGTTOU or SIGTTIN + * when it needs access to the controlling tty. + */ + exec_monitor(details, &oset, foreground && !pipeline, sv[1]); + cstat->type = CMD_ERRNO; + cstat->val = errno; + if (send(sv[1], cstat, sizeof(*cstat), 0) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to send status to parent", __func__); + } + _exit(EXIT_FAILURE); + } + + /* + * We close the pty follower so only the monitor and command have a + * reference to it. This ensures that we can don't block reading + * from the leader when the command and monitor have exited. + */ + if (io_fds[SFD_FOLLOWER] != -1) { + close(io_fds[SFD_FOLLOWER]); + io_fds[SFD_FOLLOWER] = -1; + } + + /* Tell the monitor to continue now that the follower is closed. */ + cstat->type = CMD_SIGNO; + cstat->val = 0; + while (send(sv[0], cstat, sizeof(*cstat), 0) == -1) { + if (errno != EINTR && errno != EAGAIN) + sudo_fatal("%s", U_("unable to send message to monitor process")); + } + + /* Close the other end of the stdin/stdout/stderr pipes and socketpair. */ + if (io_pipe[STDIN_FILENO][0] != -1) + close(io_pipe[STDIN_FILENO][0]); + if (io_pipe[STDOUT_FILENO][1] != -1) + close(io_pipe[STDOUT_FILENO][1]); + if (io_pipe[STDERR_FILENO][1] != -1) + close(io_pipe[STDERR_FILENO][1]); + close(sv[1]); + + /* No longer need execfd. */ + if (details->execfd != -1) { + close(details->execfd); + details->execfd = -1; + } + + /* Set command timeout if specified. */ + if (ISSET(details->flags, CD_SET_TIMEOUT)) + alarm(details->timeout); + + /* + * Fill in exec closure, allocate event base, signal events and + * the backchannel event. + */ + fill_exec_closure_pty(&ec, cstat, details, ppgrp, sv[0]); + + /* Restore signal mask now that signal handlers are setup. */ + sigprocmask(SIG_SETMASK, &oset, NULL); + + /* + * I/O logging must be in the C locale for floating point numbers + * to be logged consistently. + */ + setlocale(LC_ALL, "C"); + + /* + * In the event loop we pass input from user tty to leader + * and pass output from leader to stdout and IO plugin. + * Try to recover on ENXIO, it means the tty was revoked. + */ + add_io_events(ec.evbase); + do { + if (sudo_ev_dispatch(ec.evbase) == -1) + sudo_warn("%s", U_("error in event loop")); + if (sudo_ev_got_break(ec.evbase)) { + /* error from callback or monitor died */ + sudo_debug_printf(SUDO_DEBUG_ERROR, "event loop exited prematurely"); + /* XXX: no good way to know if we should terminate the command. */ + if (cstat->val == CMD_INVALID && ec.cmnd_pid != -1) { + /* no status message, kill command */ + terminate_command(ec.cmnd_pid, true); + ec.cmnd_pid = -1; + /* TODO: need way to pass an error to the sudo front end */ + cstat->type = CMD_WSTATUS; + cstat->val = W_EXITCODE(1, SIGKILL); + } + } else if (!sudo_ev_got_exit(ec.evbase)) { + switch (errno) { + case ENXIO: + case EIO: + case EBADF: + /* /dev/tty was revoked, remove tty events and retry (once) */ + if (evloop_retries == -1 && io_fds[SFD_USERTTY] != -1) { + ev_free_by_fd(ec.evbase, io_fds[SFD_USERTTY]); + evloop_retries = 1; + } + break; + } + } + } while (evloop_retries-- > 0); + + /* Flush any remaining output, free I/O bufs and events, do logout. */ + pty_finish(cstat); + + /* Free things up. */ + free_exec_closure_pty(&ec); + + debug_return_bool(true); +} + +/* + * Schedule I/O events before starting the main event loop or + * resuming from suspend. + */ +static void +add_io_events(struct sudo_event_base *evbase) +{ + struct io_buffer *iob; + debug_decl(add_io_events, SUDO_DEBUG_EXEC); + + /* + * Schedule all readers as long as the buffer is not full. + * Schedule writers that contain buffered data. + * Normally, write buffers are added on demand when data is read. + */ + SLIST_FOREACH(iob, &iobufs, entries) { + /* Don't read from /dev/tty if we are not in the foreground. */ + if (iob->revent != NULL && + (ttymode == TERM_RAW || !USERTTY_EVENT(iob->revent))) { + if (iob->len != sizeof(iob->buf)) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "added I/O revent %p, fd %d, events %d", + iob->revent, iob->revent->fd, iob->revent->events); + if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + if (iob->wevent != NULL) { + /* Enable writer if buffer is not empty. */ + if (iob->len > iob->off) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "added I/O wevent %p, fd %d, events %d", + iob->wevent, iob->wevent->fd, iob->wevent->events); + if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + } + debug_return; +} + +/* + * Flush any output buffered in iobufs or readable from fds other + * than /dev/tty. Removes I/O events from the event base when done. + */ +static void +del_io_events(bool nonblocking) +{ + struct io_buffer *iob; + struct sudo_event_base *evbase; + debug_decl(del_io_events, SUDO_DEBUG_EXEC); + + /* Remove iobufs from existing event base. */ + SLIST_FOREACH(iob, &iobufs, entries) { + if (iob->revent != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "deleted I/O revent %p, fd %d, events %d", + iob->revent, iob->revent->fd, iob->revent->events); + sudo_ev_del(NULL, iob->revent); + } + if (iob->wevent != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "deleted I/O wevent %p, fd %d, events %d", + iob->wevent, iob->wevent->fd, iob->wevent->events); + sudo_ev_del(NULL, iob->wevent); + } + } + + /* Create temporary event base for flushing. */ + evbase = sudo_ev_base_alloc(); + if (evbase == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* Avoid reading from /dev/tty, just flush existing data. */ + SLIST_FOREACH(iob, &iobufs, entries) { + /* Don't read from /dev/tty while flushing. */ + if (iob->revent != NULL && !USERTTY_EVENT(iob->revent)) { + if (iob->len != sizeof(iob->buf)) { + if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + /* Flush any write buffers with data in them. */ + if (iob->wevent != NULL) { + if (iob->len > iob->off) { + if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + } + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: flushing remaining I/O buffers (nonblocking)", __func__); + (void) sudo_ev_loop(evbase, SUDO_EVLOOP_NONBLOCK); + + /* + * If not in non-blocking mode, make sure we flush write buffers. + * We don't want to read from the pty or stdin since that might block + * and the command is no longer running anyway. + */ + if (!nonblocking) { + /* Clear out iobufs from event base. */ + SLIST_FOREACH(iob, &iobufs, entries) { + if (iob->revent != NULL && !USERTTY_EVENT(iob->revent)) + sudo_ev_del(evbase, iob->revent); + if (iob->wevent != NULL) + sudo_ev_del(evbase, iob->wevent); + } + + SLIST_FOREACH(iob, &iobufs, entries) { + /* Flush any write buffers with data in them. */ + if (iob->wevent != NULL) { + if (iob->len > iob->off) { + if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + } + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: flushing remaining write buffers (blocking)", __func__); + (void) sudo_ev_dispatch(evbase); + + /* We should now have flushed all write buffers. */ + SLIST_FOREACH(iob, &iobufs, entries) { + if (iob->wevent != NULL) { + if (iob->len > iob->off) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "unflushed data: wevent %p, fd %d, events %d", + iob->wevent, iob->wevent->fd, iob->wevent->events); + } + } + } + } + + /* Free temporary event base, removing its events. */ + sudo_ev_base_free(evbase); + + debug_return; +} + +/* + * Check for tty size changes. + * Passes the new window size to the I/O plugin and to the monitor. + */ +static void +sync_ttysize(struct exec_closure_pty *ec) +{ + struct winsize wsize; + debug_decl(sync_ttysize, SUDO_DEBUG_EXEC); + + if (ioctl(io_fds[SFD_USERTTY], TIOCGWINSZ, &wsize) == 0) { + if (wsize.ws_row != ec->rows || wsize.ws_col != ec->cols) { + const unsigned int wsize_packed = (wsize.ws_row & 0xffff) | + ((wsize.ws_col & 0xffff) << 16); + + /* Log window change event. */ + log_winchange(ec, wsize.ws_row, wsize.ws_col); + + /* Send window change event to monitor process. */ + send_command_status(ec, CMD_TTYWINCH, wsize_packed); + + /* Update rows/cols. */ + ec->rows = wsize.ws_row; + ec->cols = wsize.ws_col; + } + } + + debug_return; +} + +/* + * Remove and free any events associated with the specified + * file descriptor present in the I/O buffers list. + */ +static void +ev_free_by_fd(struct sudo_event_base *evbase, int fd) +{ + struct io_buffer *iob; + debug_decl(ev_free_by_fd, SUDO_DEBUG_EXEC); + + /* Deschedule any users of the fd and free up the events. */ + SLIST_FOREACH(iob, &iobufs, entries) { + if (iob->revent != NULL) { + if (sudo_ev_get_fd(iob->revent) == fd) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing revent %p with fd %d", + __func__, iob->revent, fd); + sudo_ev_free(iob->revent); + iob->revent = NULL; + } + } + if (iob->wevent != NULL) { + if (sudo_ev_get_fd(iob->wevent) == fd) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing wevent %p with fd %d", + __func__, iob->wevent, fd); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + } + } + } + debug_return; +} + +/* + * Only close the fd if it is not /dev/tty or std{in,out,err}. + * Return value is the same as close(2). + */ +static int +safe_close(int fd) +{ + debug_decl(safe_close, SUDO_DEBUG_EXEC); + + /* Avoid closing /dev/tty or std{in,out,err}. */ + if (fd < 3 || fd == io_fds[SFD_USERTTY]) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: not closing fd %d (%s)", __func__, fd, _PATH_TTY); + errno = EINVAL; + debug_return_int(-1); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: closing fd %d", __func__, fd); + debug_return_int(close(fd)); +} diff --git a/src/get_pty.c b/src/get_pty.c new file mode 100644 index 0000000..e50a9a6 --- /dev/null +++ b/src/get_pty.c @@ -0,0 +1,192 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2012, 2014-2016 + * Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/stat.h> +#include <sys/ioctl.h> +#ifdef HAVE_SYS_STROPTS_H +#include <sys/stropts.h> +#endif /* HAVE_SYS_STROPTS_H */ +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <grp.h> + +#if defined(HAVE_OPENPTY) +# if defined(HAVE_LIBUTIL_H) +# include <libutil.h> /* *BSD */ +# elif defined(HAVE_UTIL_H) +# include <util.h> /* macOS */ +# elif defined(HAVE_PTY_H) +# include <pty.h> /* Linux */ +# else +# include <termios.h> /* Solaris */ +# endif +#endif + +#include "sudo.h" + +#if defined(HAVE_OPENPTY) +bool +get_pty(int *leader, int *follower, char *name, size_t namesz, uid_t ttyuid) +{ + struct group *gr; + gid_t ttygid = -1; + bool ret = false; + debug_decl(get_pty, SUDO_DEBUG_PTY); + + if ((gr = getgrnam("tty")) != NULL) + ttygid = gr->gr_gid; + + if (openpty(leader, follower, name, NULL, NULL) == 0) { + if (chown(name, ttyuid, ttygid) == 0) + ret = true; + } + + debug_return_bool(ret); +} + +#elif defined(HAVE__GETPTY) +bool +get_pty(int *leader, int *follower, char *name, size_t namesz, uid_t ttyuid) +{ + char *line; + bool ret = false; + debug_decl(get_pty, SUDO_DEBUG_PTY); + + /* IRIX-style dynamic ptys (may fork) */ + line = _getpty(leader, O_RDWR, S_IRUSR|S_IWUSR|S_IWGRP, 0); + if (line != NULL) { + *follower = open(line, O_RDWR|O_NOCTTY, 0); + if (*follower != -1) { + (void) chown(line, ttyuid, -1); + strlcpy(name, line, namesz); + ret = true; + } else { + close(*leader); + *leader = -1; + } + } + debug_return_bool(ret); +} +#elif defined(HAVE_GRANTPT) +# ifndef HAVE_POSIX_OPENPT +static int +posix_openpt(int oflag) +{ + int fd; + +# ifdef _AIX + fd = open(_PATH_DEV "ptc", oflag); +# else + fd = open(_PATH_DEV "ptmx", oflag); +# endif + return fd; +} +# endif /* HAVE_POSIX_OPENPT */ + +bool +get_pty(int *leader, int *follower, char *name, size_t namesz, uid_t ttyuid) +{ + char *line; + bool ret = false; + debug_decl(get_pty, SUDO_DEBUG_PTY); + + *leader = posix_openpt(O_RDWR|O_NOCTTY); + if (*leader != -1) { + (void) grantpt(*leader); /* may fork */ + if (unlockpt(*leader) != 0) { + close(*leader); + goto done; + } + line = ptsname(*leader); + if (line == NULL) { + close(*leader); + goto done; + } + *follower = open(line, O_RDWR|O_NOCTTY, 0); + if (*follower == -1) { + close(*leader); + goto done; + } +# if defined(I_PUSH) && !defined(_AIX) + ioctl(*follower, I_PUSH, "ptem"); /* pseudo tty emulation module */ + ioctl(*follower, I_PUSH, "ldterm"); /* line discipline module */ +# endif + (void) chown(line, ttyuid, -1); + strlcpy(name, line, namesz); + ret = true; + } +done: + debug_return_bool(ret); +} + +#else /* Old-style BSD ptys */ + +static char line[] = _PATH_DEV "ptyXX"; + +bool +get_pty(int *leader, int *follower, char *name, size_t namesz, uid_t ttyuid) +{ + char *bank, *cp; + struct group *gr; + gid_t ttygid = -1; + bool ret = false; + debug_decl(get_pty, SUDO_DEBUG_PTY); + + if ((gr = getgrnam("tty")) != NULL) + ttygid = gr->gr_gid; + + for (bank = "pqrs"; *bank != '\0'; bank++) { + line[sizeof(_PATH_DEV "ptyX") - 2] = *bank; + for (cp = "0123456789abcdef"; *cp != '\0'; cp++) { + line[sizeof(_PATH_DEV "ptyXX") - 2] = *cp; + *leader = open(line, O_RDWR|O_NOCTTY, 0); + if (*leader == -1) { + if (errno == ENOENT) + goto done; /* out of ptys */ + continue; /* already in use */ + } + line[sizeof(_PATH_DEV "p") - 2] = 't'; + (void) chown(line, ttyuid, ttygid); + (void) chmod(line, S_IRUSR|S_IWUSR|S_IWGRP); +# ifdef HAVE_REVOKE + (void) revoke(line); +# endif + *follower = open(line, O_RDWR|O_NOCTTY, 0); + if (*follower != -1) { + strlcpy(name, line, namesz); + ret = true; /* success */ + goto done; + } + (void) close(*leader); + } + } +done: + debug_return_bool(ret); +} +#endif /* HAVE_OPENPTY */ diff --git a/src/hooks.c b/src/hooks.c new file mode 100644 index 0000000..70ff02e --- /dev/null +++ b/src/hooks.c @@ -0,0 +1,245 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2012-2016 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +#include "sudo.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" + +/* Singly linked hook list. */ +struct sudo_hook_entry { + SLIST_ENTRY(sudo_hook_entry) entries; + union { + sudo_hook_fn_t generic_fn; + sudo_hook_fn_setenv_t setenv_fn; + sudo_hook_fn_unsetenv_t unsetenv_fn; + sudo_hook_fn_getenv_t getenv_fn; + sudo_hook_fn_putenv_t putenv_fn; + } u; + void *closure; +}; +SLIST_HEAD(sudo_hook_list, sudo_hook_entry); + +/* Each hook type gets own hook list. */ +static struct sudo_hook_list sudo_hook_setenv_list = + SLIST_HEAD_INITIALIZER(sudo_hook_setenv_list); +static struct sudo_hook_list sudo_hook_unsetenv_list = + SLIST_HEAD_INITIALIZER(sudo_hook_unsetenv_list); +static struct sudo_hook_list sudo_hook_getenv_list = + SLIST_HEAD_INITIALIZER(sudo_hook_getenv_list); +static struct sudo_hook_list sudo_hook_putenv_list = + SLIST_HEAD_INITIALIZER(sudo_hook_putenv_list); + +/* NOTE: must not anything that might call setenv() */ +int +process_hooks_setenv(const char *name, const char *value, int overwrite) +{ + struct sudo_hook_entry *hook; + int rc = SUDO_HOOK_RET_NEXT; + + /* First process the hooks. */ + SLIST_FOREACH(hook, &sudo_hook_setenv_list, entries) { + rc = hook->u.setenv_fn(name, value, overwrite, hook->closure); + if (rc == SUDO_HOOK_RET_STOP || rc == SUDO_HOOK_RET_ERROR) + break; + } + return rc; +} + +/* NOTE: must not anything that might call putenv() */ +int +process_hooks_putenv(char *string) +{ + struct sudo_hook_entry *hook; + int rc = SUDO_HOOK_RET_NEXT; + + /* First process the hooks. */ + SLIST_FOREACH(hook, &sudo_hook_putenv_list, entries) { + rc = hook->u.putenv_fn(string, hook->closure); + if (rc == SUDO_HOOK_RET_STOP || rc == SUDO_HOOK_RET_ERROR) + break; + } + return rc; +} + +/* NOTE: must not anything that might call getenv() */ +int +process_hooks_getenv(const char *name, char **value) +{ + struct sudo_hook_entry *hook; + char *val = NULL; + int rc = SUDO_HOOK_RET_NEXT; + + /* First process the hooks. */ + SLIST_FOREACH(hook, &sudo_hook_getenv_list, entries) { + rc = hook->u.getenv_fn(name, &val, hook->closure); + if (rc == SUDO_HOOK_RET_STOP || rc == SUDO_HOOK_RET_ERROR) + break; + } + if (val != NULL) + *value = val; + return rc; +} + +/* NOTE: must not anything that might call unsetenv() */ +int +process_hooks_unsetenv(const char *name) +{ + struct sudo_hook_entry *hook; + int rc = SUDO_HOOK_RET_NEXT; + + /* First process the hooks. */ + SLIST_FOREACH(hook, &sudo_hook_unsetenv_list, entries) { + rc = hook->u.unsetenv_fn(name, hook->closure); + if (rc == SUDO_HOOK_RET_STOP || rc == SUDO_HOOK_RET_ERROR) + break; + } + return rc; +} + +/* Hook registration internals. */ +static int +register_hook_internal(struct sudo_hook_list *head, + int (*hook_fn)(), void *closure) +{ + struct sudo_hook_entry *hook; + debug_decl(register_hook_internal, SUDO_DEBUG_HOOKS); + + if ((hook = calloc(1, sizeof(*hook))) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + debug_return_int(-1); + } + hook->u.generic_fn = hook_fn; + hook->closure = closure; + SLIST_INSERT_HEAD(head, hook, entries); + + debug_return_int(0); +} + +/* Register the specified hook. */ +int +register_hook(struct sudo_hook *hook) +{ + int ret; + debug_decl(register_hook, SUDO_DEBUG_HOOKS); + + if (SUDO_API_VERSION_GET_MAJOR(hook->hook_version) != SUDO_HOOK_VERSION_MAJOR) { + /* Major versions must match. */ + errno = EINVAL; + ret = -1; + } else { + switch (hook->hook_type) { + case SUDO_HOOK_GETENV: + ret = register_hook_internal(&sudo_hook_getenv_list, + hook->hook_fn, hook->closure); + break; + case SUDO_HOOK_PUTENV: + ret = register_hook_internal(&sudo_hook_putenv_list, + hook->hook_fn, hook->closure); + break; + case SUDO_HOOK_SETENV: + ret = register_hook_internal(&sudo_hook_setenv_list, + hook->hook_fn, hook->closure); + break; + case SUDO_HOOK_UNSETENV: + ret = register_hook_internal(&sudo_hook_unsetenv_list, + hook->hook_fn, hook->closure); + break; + default: + /* XXX - use define for unknown value */ + errno = ENOTSUP; + ret = 1; + break; + } + } + + debug_return_int(ret); +} + +/* Hook deregistration internals. */ +static void +deregister_hook_internal(struct sudo_hook_list *head, + int (*hook_fn)(), void *closure) +{ + struct sudo_hook_entry *hook, *prev = NULL; + debug_decl(deregister_hook_internal, SUDO_DEBUG_HOOKS); + + SLIST_FOREACH(hook, head, entries) { + if (hook->u.generic_fn == hook_fn && hook->closure == closure) { + /* Remove from list and free. */ + if (prev == NULL) + SLIST_REMOVE_HEAD(head, entries); + else + SLIST_REMOVE_AFTER(prev, entries); + free(hook); + break; + } + prev = hook; + } + + debug_return; +} + +/* Deregister the specified hook. */ +int +deregister_hook(struct sudo_hook *hook) +{ + int ret = 0; + debug_decl(deregister_hook, SUDO_DEBUG_HOOKS); + + if (SUDO_API_VERSION_GET_MAJOR(hook->hook_version) != SUDO_HOOK_VERSION_MAJOR) { + /* Major versions must match. */ + ret = -1; + } else { + switch (hook->hook_type) { + case SUDO_HOOK_GETENV: + deregister_hook_internal(&sudo_hook_getenv_list, hook->hook_fn, + hook->closure); + break; + case SUDO_HOOK_PUTENV: + deregister_hook_internal(&sudo_hook_putenv_list, hook->hook_fn, + hook->closure); + break; + case SUDO_HOOK_SETENV: + deregister_hook_internal(&sudo_hook_setenv_list, hook->hook_fn, + hook->closure); + break; + case SUDO_HOOK_UNSETENV: + deregister_hook_internal(&sudo_hook_unsetenv_list, hook->hook_fn, + hook->closure); + break; + default: + /* XXX - use define for unknown value */ + ret = 1; + break; + } + } + + debug_return_int(ret); +} diff --git a/src/limits.c b/src/limits.c new file mode 100644 index 0000000..9ce475a --- /dev/null +++ b/src/limits.c @@ -0,0 +1,385 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1999-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/resource.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#ifdef __linux__ +# include <sys/prctl.h> +#endif +#include <errno.h> +#include <limits.h> + +#include "sudo.h" + +#if defined(OPEN_MAX) && OPEN_MAX > 256 +# define SUDO_OPEN_MAX OPEN_MAX +#else +# define SUDO_OPEN_MAX 256 +#endif + +#ifdef __LP64__ +# define SUDO_STACK_MIN (4 * 1024 * 1024) +#else +# define SUDO_STACK_MIN (2 * 1024 * 1024) +#endif + +#ifdef HAVE_SETRLIMIT64 +# define getrlimit(a, b) getrlimit64((a), (b)) +# define setrlimit(a, b) setrlimit64((a), (b)) +# define rlimit rlimit64 +# define rlim_t rlim64_t +# undef RLIM_INFINITY +# define RLIM_INFINITY RLIM64_INFINITY +#endif /* HAVE_SETRLIMIT64 */ + +/* Older BSD systems have RLIMIT_VMEM, not RLIMIT_AS. */ +#if !defined(RLIMIT_AS) && defined(RLIMIT_VMEM) +# define RLIMIT_AS RLIMIT_VMEM +#endif + +/* + * macOS doesn't allow nofile soft limit to be infinite or + * the stack hard limit to be infinite. + * Linux containers have a problem with an infinite stack soft limit. + */ +static struct rlimit nofile_fallback = { SUDO_OPEN_MAX, RLIM_INFINITY }; +static struct rlimit stack_fallback = { SUDO_STACK_MIN, 65532 * 1024 }; + +static struct saved_limit { + const char *name; /* rlimit_foo in lower case */ + int resource; /* RLIMIT_FOO definition */ + bool override; /* override limit while sudo executes? */ + bool saved; /* true if we were able to get the value */ + struct rlimit *fallback; /* fallback if we fail to set to newlimit */ + struct rlimit newlimit; /* new limit to use if override is true */ + struct rlimit oldlimit; /* original limit, valid if saved is true */ +} saved_limits[] = { +#ifdef RLIMIT_AS + { "rlimit_as", RLIMIT_AS, true, false, NULL, { RLIM_INFINITY, RLIM_INFINITY } }, +#endif + { "rlimit_core", RLIMIT_CORE, false }, + { "rlimit_cpu", RLIMIT_CPU, true, false, NULL, { RLIM_INFINITY, RLIM_INFINITY } }, + { "rlimit_data", RLIMIT_DATA, true, false, NULL, { RLIM_INFINITY, RLIM_INFINITY } }, + { "rlimit_fsize", RLIMIT_FSIZE, true, false, NULL, { RLIM_INFINITY, RLIM_INFINITY } }, +#ifdef RLIMIT_LOCKS + { "rlimit_locks", RLIMIT_LOCKS, false }, +#endif +#ifdef RLIMIT_MEMLOCK + { "rlimit_memlock", RLIMIT_MEMLOCK, false }, +#endif + { "rlimit_nofile", RLIMIT_NOFILE, true, false, &nofile_fallback, { RLIM_INFINITY, RLIM_INFINITY } }, +#ifdef RLIMIT_NPROC + { "rlimit_nproc", RLIMIT_NPROC, true, false, NULL, { RLIM_INFINITY, RLIM_INFINITY } }, +#endif +#ifdef RLIMIT_RSS + { "rlimit_rss", RLIMIT_RSS, true, false, NULL, { RLIM_INFINITY, RLIM_INFINITY } }, +#endif + { "rlimit_stack", RLIMIT_STACK, true, false, &stack_fallback, { SUDO_STACK_MIN, RLIM_INFINITY } } +}; + +static struct rlimit corelimit; +static bool coredump_disabled; +#ifdef __linux__ +static struct rlimit nproclimit; +static int dumpflag; +#endif + +/* + * Disable core dumps to avoid dropping a core with user password in it. + * Not all operating systems disable core dumps for setuid processes. + */ +void +disable_coredump(void) +{ + struct rlimit rl = { 0, 0 }; + debug_decl(disable_coredump, SUDO_DEBUG_UTIL); + + if (getrlimit(RLIMIT_CORE, &corelimit) == -1) + sudo_warn("getrlimit(RLIMIT_CORE)"); + sudo_debug_printf(SUDO_DEBUG_INFO, "RLIMIT_CORE [%lld, %lld] -> [0, 0]", + (long long)corelimit.rlim_cur, (long long)corelimit.rlim_max); + if (setrlimit(RLIMIT_CORE, &rl) == -1) + sudo_warn("setrlimit(RLIMIT_CORE)"); +#ifdef __linux__ + /* On Linux, also set PR_SET_DUMPABLE to zero (reset by execve). */ + if ((dumpflag = prctl(PR_GET_DUMPABLE, 0, 0, 0, 0)) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "prctl(PR_GET_DUMPABLE, 0, 0, 0, 0)"); + dumpflag = 0; + } + if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "prctl(PR_SET_DUMPABLE, %d, 0, 0, 0)", dumpflag); + } +#endif /* __linux__ */ + coredump_disabled = true; + + debug_return; +} + +/* + * Restore core resource limit before executing the command. + */ +static void +restore_coredump(void) +{ + debug_decl(restore_coredump, SUDO_DEBUG_UTIL); + + if (coredump_disabled) { + /* + * Linux containers don't allow RLIMIT_CORE to be set back to + * RLIM_INFINITY if we set the limit to zero, even for root. + */ + if (setrlimit(RLIMIT_CORE, &corelimit) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "setrlimit(RLIMIT_CORE, [%lld, %lld])", + (long long)corelimit.rlim_cur, (long long)corelimit.rlim_max); + } +#ifdef __linux__ + if (prctl(PR_SET_DUMPABLE, dumpflag, 0, 0, 0) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "prctl(PR_SET_DUMPABLE, %d, 0, 0, 0)", dumpflag); + } +#endif /* __linux__ */ + } + debug_return; +} + +/* + * Unlimit the number of processes since Linux's setuid() will + * apply resource limits when changing uid and return EAGAIN if + * nproc would be exceeded by the uid switch. + * + * This function is called *after* session setup and before the + * final setuid() call. + */ +void +unlimit_nproc(void) +{ +#ifdef __linux__ + struct rlimit rl = { RLIM_INFINITY, RLIM_INFINITY }; + debug_decl(unlimit_nproc, SUDO_DEBUG_UTIL); + + if (getrlimit(RLIMIT_NPROC, &nproclimit) != 0) + sudo_warn("getrlimit(RLIMIT_NPROC)"); + sudo_debug_printf(SUDO_DEBUG_INFO, "RLIMIT_NPROC [%lld, %lld] -> [inf, inf]", + (long long)nproclimit.rlim_cur, (long long)nproclimit.rlim_max); + if (setrlimit(RLIMIT_NPROC, &rl) == -1) { + rl.rlim_cur = rl.rlim_max = nproclimit.rlim_max; + sudo_debug_printf(SUDO_DEBUG_INFO, + "RLIMIT_NPROC [%lld, %lld] -> [%lld, %lld]", + (long long)nproclimit.rlim_cur, (long long)nproclimit.rlim_max, + (long long)rl.rlim_cur, (long long)rl.rlim_max); + if (setrlimit(RLIMIT_NPROC, &rl) != 0) + sudo_warn("setrlimit(RLIMIT_NPROC)"); + } + debug_return; +#endif /* __linux__ */ +} + +/* + * Restore saved value of RLIMIT_NPROC before execve(). + */ +void +restore_nproc(void) +{ +#ifdef __linux__ + debug_decl(restore_nproc, SUDO_DEBUG_UTIL); + + if (setrlimit(RLIMIT_NPROC, &nproclimit) != 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "setrlimit(RLIMIT_NPROC, [%lld, %lld])", + (long long)nproclimit.rlim_cur, (long long)nproclimit.rlim_max); + } + + debug_return; +#endif /* __linux__ */ +} + +/* + * Unlimit resource limits so sudo is not limited by, e.g. + * stack, data or file table sizes. + */ +void +unlimit_sudo(void) +{ + unsigned int idx; + int rc; + debug_decl(unlimit_sudo, SUDO_DEBUG_UTIL); + + /* Set resource limits to unlimited and stash the old values. */ + for (idx = 0; idx < nitems(saved_limits); idx++) { + struct saved_limit *lim = &saved_limits[idx]; + if (getrlimit(lim->resource, &lim->oldlimit) == -1) + continue; + sudo_debug_printf(SUDO_DEBUG_INFO, + "getrlimit(lim->name) -> [%lld, %lld]", + (long long)lim->oldlimit.rlim_cur, + (long long)lim->oldlimit.rlim_max); + + lim->saved = true; + if (!lim->override) + continue; + + if (lim->newlimit.rlim_cur != RLIM_INFINITY) { + /* Don't reduce the soft resource limit. */ + if (lim->oldlimit.rlim_cur == RLIM_INFINITY || + lim->oldlimit.rlim_cur > lim->newlimit.rlim_cur) + lim->newlimit.rlim_cur = lim->oldlimit.rlim_cur; + } + if (lim->newlimit.rlim_max != RLIM_INFINITY) { + /* Don't reduce the hard resource limit. */ + if (lim->oldlimit.rlim_max == RLIM_INFINITY || + lim->oldlimit.rlim_max > lim->newlimit.rlim_max) + lim->newlimit.rlim_max = lim->oldlimit.rlim_max; + } + if ((rc = setrlimit(lim->resource, &lim->newlimit)) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "setrlimit(%s, [%lld, %lld])", lim->name, + (long long)lim->newlimit.rlim_cur, + (long long)lim->newlimit.rlim_max); + if (lim->fallback != NULL) { + if ((rc = setrlimit(lim->resource, lim->fallback)) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "setrlimit(%s, [%lld, %lld])", lim->name, + (long long)lim->fallback->rlim_cur, + (long long)lim->fallback->rlim_max); + } + } + if (rc == -1) { + /* Try setting new rlim_cur to old rlim_max. */ + lim->newlimit.rlim_cur = lim->oldlimit.rlim_max; + lim->newlimit.rlim_max = lim->oldlimit.rlim_max; + if ((rc = setrlimit(lim->resource, &lim->newlimit)) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "setrlimit(%s, [%lld, %lld])", lim->name, + (long long)lim->newlimit.rlim_cur, + (long long)lim->newlimit.rlim_max); + } + } + if (rc == -1) + sudo_warn("setrlimit(%s)", lim->name); + } + } + + debug_return; +} + +/* + * Restore resource limits modified by unlimit_sudo() and disable_coredump(). + */ +void +restore_limits(void) +{ + unsigned int idx; + debug_decl(restore_limits, SUDO_DEBUG_UTIL); + + /* Restore resource limits to saved values. */ + for (idx = 0; idx < nitems(saved_limits); idx++) { + struct saved_limit *lim = &saved_limits[idx]; + if (lim->override && lim->saved) { + struct rlimit rl = lim->oldlimit; + int i, rc; + + for (i = 0; i < 10; i++) { + rc = setrlimit(lim->resource, &rl); + if (rc != -1 || errno != EINVAL) + break; + + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "setrlimit(%s, [%lld, %lld])", lim->name, + (long long)rl.rlim_cur, (long long)rl.rlim_max); + + /* + * Soft limit could be lower than current resource usage. + * This can be an issue on NetBSD with RLIMIT_STACK and ASLR. + */ + if (rl.rlim_cur > LLONG_MAX / 2) + break; + rl.rlim_cur *= 2; + if (lim->newlimit.rlim_cur != RLIM_INFINITY && + rl.rlim_cur > lim->newlimit.rlim_cur) { + rl.rlim_cur = lim->newlimit.rlim_cur; + } + if (rl.rlim_max != RLIM_INFINITY && + rl.rlim_cur > rl.rlim_max) { + rl.rlim_max = rl.rlim_cur; + } + rc = setrlimit(lim->resource, &rl); + if (rc != -1 || errno != EINVAL) + break; + } + if (rc == -1) + sudo_warn("setrlimit(%s)", lim->name); + } + } + restore_coredump(); + + debug_return; +} + +int +serialize_limits(char **info, size_t info_max) +{ + char *str; + unsigned int idx, nstored = 0; + debug_decl(serialize_limits, SUDO_DEBUG_UTIL); + + for (idx = 0; idx < nitems(saved_limits); idx++) { + const struct saved_limit *lim = &saved_limits[idx]; + const struct rlimit *rl = &lim->oldlimit; + char curlim[(((sizeof(long long) * 8) + 2) / 3) + 2]; + char maxlim[(((sizeof(long long) * 8) + 2) / 3) + 2]; + + if (!lim->saved) + continue; + + if (nstored == info_max) + goto oom; + + if (rl->rlim_cur == RLIM_INFINITY) { + strlcpy(curlim, "infinity", sizeof(curlim)); + } else { + snprintf(curlim, sizeof(curlim), "%llu", + (unsigned long long)rl->rlim_cur); + } + if (rl->rlim_max == RLIM_INFINITY) { + strlcpy(maxlim, "infinity", sizeof(maxlim)); + } else { + snprintf(maxlim, sizeof(maxlim), "%llu", + (unsigned long long)rl->rlim_max); + } + if (asprintf(&str, "%s=%s,%s", lim->name, curlim, maxlim) == -1) + goto oom; + info[nstored++] = str; + } + debug_return_int(nstored); +oom: + while (nstored--) + free(info[nstored]); + debug_return_int(-1); +} diff --git a/src/load_plugins.c b/src/load_plugins.c new file mode 100644 index 0000000..3c2973e --- /dev/null +++ b/src/load_plugins.c @@ -0,0 +1,549 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2018 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/stat.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +#include "sudo.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" +#include "sudo_dso.h" + +/* We always use the same name for the sudoers plugin, regardless of the OS */ +#define SUDOERS_PLUGIN "sudoers.so" + +#ifdef ENABLE_SUDO_PLUGIN_API +static int +sudo_stat_plugin(struct plugin_info *info, char *fullpath, + size_t pathsize, struct stat *sb) +{ + int status = -1; + debug_decl(sudo_stat_plugin, SUDO_DEBUG_PLUGIN); + + if (info->path[0] == '/') { + if (strlcpy(fullpath, info->path, pathsize) >= pathsize) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("%s: %s"), info->path, strerror(ENAMETOOLONG)); + goto done; + } + status = stat(fullpath, sb); + } else { + int len; + +#ifdef STATIC_SUDOERS_PLUGIN + /* Check static symbols. */ + if (strcmp(info->path, SUDOERS_PLUGIN) == 0) { + if (strlcpy(fullpath, info->path, pathsize) >= pathsize) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("%s: %s"), info->path, strerror(ENAMETOOLONG)); + goto done; + } + /* Plugin is static, fake up struct stat. */ + memset(sb, 0, sizeof(*sb)); + sb->st_uid = ROOT_UID; + sb->st_mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH; + status = 0; + goto done; + } +#endif /* STATIC_SUDOERS_PLUGIN */ + + if (sudo_conf_plugin_dir_path() == NULL) { + errno = ENOENT; + goto done; + } + + len = snprintf(fullpath, pathsize, "%s%s", sudo_conf_plugin_dir_path(), + info->path); + if (len < 0 || (size_t)len >= pathsize) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("%s%s: %s"), sudo_conf_plugin_dir_path(), info->path, + strerror(ENAMETOOLONG)); + goto done; + } + /* Try parent dir for compatibility with old plugindir default. */ + if ((status = stat(fullpath, sb)) != 0) { + char *cp = strrchr(fullpath, '/'); + if (cp > fullpath + 4 && cp[-5] == '/' && cp[-4] == 's' && + cp[-3] == 'u' && cp[-2] == 'd' && cp[-1] == 'o') { + int serrno = errno; + strlcpy(cp - 4, info->path, pathsize - (cp - 4 - fullpath)); + if ((status = stat(fullpath, sb)) != 0) + errno = serrno; + } + } + } +done: + debug_return_int(status); +} + +static bool +sudo_check_plugin(struct plugin_info *info, char *fullpath, size_t pathsize) +{ + struct stat sb; + bool ret = false; + debug_decl(sudo_check_plugin, SUDO_DEBUG_PLUGIN); + + if (sudo_stat_plugin(info, fullpath, pathsize, &sb) != 0) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + if (info->path[0] == '/') { + sudo_warn("%s", info->path); + } else { + sudo_warn("%s%s", + sudo_conf_plugin_dir_path() ? sudo_conf_plugin_dir_path() : "", + info->path); + } + goto done; + } + + if (!sudo_conf_developer_mode()) { + if (sb.st_uid != ROOT_UID) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("%s must be owned by uid %d"), fullpath, ROOT_UID); + goto done; + } + if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("%s must be only be writable by owner"), fullpath); + goto done; + } + } + ret = true; + +done: + debug_return_bool(ret); +} +#else +static bool +sudo_check_plugin(struct plugin_info *info, char *fullpath, size_t pathsize) +{ + debug_decl(sudo_check_plugin, SUDO_DEBUG_PLUGIN); + (void)strlcpy(fullpath, info->path, pathsize); + debug_return_bool(true); +} +#endif /* ENABLE_SUDO_PLUGIN_API */ + +static bool +fill_container(struct plugin_container *container, void *handle, + const char *path, struct generic_plugin *plugin, struct plugin_info *info) +{ + debug_decl(fill_container, SUDO_DEBUG_PLUGIN); + + if ((container->path = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + container->handle = handle; + container->name = info->symbol_name; + container->options = info->options; + container->debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; + container->u.generic = plugin; + container->debug_files = sudo_conf_debug_files(path); + + /* Zero out info strings that the container now owns. */ + info->symbol_name = NULL; + info->options = NULL; + + debug_return_bool(true); +} + +static struct plugin_container * +new_container(void *handle, const char *path, struct generic_plugin *plugin, + struct plugin_info *info) +{ + struct plugin_container *container = NULL; + debug_decl(new_container, SUDO_DEBUG_PLUGIN); + + if ((container = calloc(1, sizeof(*container))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + if (!fill_container(container, handle, path, plugin, info)) + goto bad; + + debug_return_ptr(container); +bad: + free(container); + debug_return_ptr(NULL); +} + +static bool +plugin_exists(struct plugin_container_list *plugins, const char *symbol_name) +{ + struct plugin_container *container; + debug_decl(find_plugin, SUDO_DEBUG_PLUGIN); + + TAILQ_FOREACH(container, plugins, entries) { + if (strcmp(container->name, symbol_name) == 0) + debug_return_bool(true); + } + debug_return_bool(false); +} + +typedef struct generic_plugin * (plugin_clone_func)(void); + +struct generic_plugin * +sudo_plugin_try_to_clone(void *so_handle, const char *symbol_name) +{ + debug_decl(sudo_plugin_clone, SUDO_DEBUG_PLUGIN); + struct generic_plugin * plugin = NULL; + char *clone_func_name = NULL; + + if (asprintf(&clone_func_name, "%s_clone", symbol_name) < 0) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto cleanup; + } + + plugin_clone_func *clone_func = (plugin_clone_func *)sudo_dso_findsym(so_handle, clone_func_name); + if (clone_func) { + plugin = (*clone_func)(); + } + +cleanup: + free(clone_func_name); + debug_return_ptr(plugin); +} + +static bool +sudo_insert_plugin(struct plugin_container_list *plugin_list, void *handle, + const char *path, struct generic_plugin *plugin, struct plugin_info *info) +{ + struct plugin_container *container; + debug_decl(sudo_insert_plugin, SUDO_DEBUG_PLUGIN); + + if (plugin_exists(plugin_list, info->symbol_name)) { + plugin = sudo_plugin_try_to_clone(handle, info->symbol_name); + if (plugin == NULL) { + sudo_warnx(U_("ignoring duplicate plugin \"%s\" in %s, line %d"), + info->symbol_name, _PATH_SUDO_CONF, info->lineno); + sudo_dso_unload(handle); + goto done; + } + } + + if ((container = new_container(handle, path, plugin, info)) == NULL) + debug_return_bool(false); + TAILQ_INSERT_TAIL(plugin_list, container, entries); + +done: + debug_return_bool(true); +} + +/* + * Load the plugin specified by "info". + */ +static bool +sudo_load_plugin(struct plugin_info *info, bool quiet) +{ + struct generic_plugin *plugin; + char path[PATH_MAX]; + void *handle = NULL; + bool ret = false; + debug_decl(sudo_load_plugin, SUDO_DEBUG_PLUGIN); + + /* Check plugin owner/mode and fill in path */ + if (!sudo_check_plugin(info, path, sizeof(path))) + goto done; + + /* Open plugin and map in symbol */ + handle = sudo_dso_load(path, SUDO_DSO_LAZY|SUDO_DSO_GLOBAL); + if (!handle) { + if (!quiet) { + const char *errstr = sudo_dso_strerror(); + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("unable to load %s: %s"), path, + errstr ? errstr : "unknown error"); + } + goto done; + } + plugin = sudo_dso_findsym(handle, info->symbol_name); + if (!plugin) { + if (!quiet) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("unable to find symbol \"%s\" in %s"), + info->symbol_name, path); + } + goto done; + } + + if (SUDO_API_VERSION_GET_MAJOR(plugin->version) != SUDO_API_VERSION_MAJOR) { + if (!quiet) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("incompatible plugin major version %d (expected %d) found in %s"), + SUDO_API_VERSION_GET_MAJOR(plugin->version), + SUDO_API_VERSION_MAJOR, path); + } + goto done; + } + + switch (plugin->type) { + case SUDO_POLICY_PLUGIN: + if (policy_plugin.handle != NULL) { + /* Ignore duplicate entries. */ + if (strcmp(policy_plugin.name, info->symbol_name) == 0) { + if (!quiet) { + sudo_warnx(U_("ignoring duplicate plugin \"%s\" in %s, line %d"), + info->symbol_name, _PATH_SUDO_CONF, info->lineno); + } + } else { + if (!quiet) { + sudo_warnx(U_("ignoring policy plugin \"%s\" in %s, line %d"), + info->symbol_name, _PATH_SUDO_CONF, info->lineno); + sudo_warnx("%s", + U_("only a single policy plugin may be specified")); + } + goto done; + } + ret = true; + goto done; + } + if (!fill_container(&policy_plugin, handle, path, plugin, info)) + goto done; + break; + case SUDO_IO_PLUGIN: + if (!sudo_insert_plugin(&io_plugins, handle, path, plugin, info)) + goto done; + break; + case SUDO_AUDIT_PLUGIN: + if (!sudo_insert_plugin(&audit_plugins, handle, path, plugin, info)) + goto done; + break; + case SUDO_APPROVAL_PLUGIN: + if (!sudo_insert_plugin(&approval_plugins, handle, path, plugin, info)) + goto done; + break; + default: + if (!quiet) { + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + sudo_warnx(U_("unknown plugin type %d found in %s"), plugin->type, path); + } + goto done; + } + + /* Handle is either in use or has been closed. */ + handle = NULL; + + ret = true; + +done: + if (handle != NULL) + sudo_dso_unload(handle); + debug_return_bool(ret); +} + +static void +free_plugin_info(struct plugin_info *info) +{ + free(info->path); + free(info->symbol_name); + if (info->options != NULL) { + int i = 0; + while (info->options[i] != NULL) + free(info->options[i++]); + free(info->options); + } + free(info); +} + +static void +sudo_register_hooks(void) +{ + struct plugin_container *container; + debug_decl(sudo_register_hooks, SUDO_DEBUG_PLUGIN); + + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 2)) { + if (policy_plugin.u.policy->register_hooks != NULL) { + sudo_debug_set_active_instance(policy_plugin.debug_instance); + policy_plugin.u.policy->register_hooks(SUDO_HOOK_VERSION, + register_hook); + sudo_debug_set_active_instance(sudo_debug_instance); + } + } + + TAILQ_FOREACH(container, &io_plugins, entries) { + if (container->u.io->version >= SUDO_API_MKVERSION(1, 2)) { + if (container->u.io->register_hooks != NULL) { + sudo_debug_set_active_instance(container->debug_instance); + container->u.io->register_hooks(SUDO_HOOK_VERSION, + register_hook); + sudo_debug_set_active_instance(sudo_debug_instance); + } + } + } + + TAILQ_FOREACH(container, &audit_plugins, entries) { + if (container->u.audit->register_hooks != NULL) { + sudo_debug_set_active_instance(container->debug_instance); + container->u.audit->register_hooks(SUDO_HOOK_VERSION, + register_hook); + sudo_debug_set_active_instance(sudo_debug_instance); + } + } + + debug_return; +} + +static void +sudo_init_event_alloc(void) +{ + struct plugin_container *container; + debug_decl(sudo_init_event_alloc, SUDO_DEBUG_PLUGIN); + + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) + policy_plugin.u.policy->event_alloc = sudo_plugin_event_alloc; + + TAILQ_FOREACH(container, &io_plugins, entries) { + if (container->u.io->version >= SUDO_API_MKVERSION(1, 15)) + container->u.io->event_alloc = sudo_plugin_event_alloc; + } + TAILQ_FOREACH(container, &audit_plugins, entries) { + if (container->u.audit->version >= SUDO_API_MKVERSION(1, 17)) + container->u.audit->event_alloc = sudo_plugin_event_alloc; + } + + debug_return; +} + +/* + * Load the specified symbol from the sudoers plugin. + * Used to provide a default plugin when none are specified in sudo.conf. + */ +bool +sudo_load_sudoers_plugin(const char *symbol_name, bool optional) +{ + struct plugin_info *info; + bool ret = false; + debug_decl(sudo_load_sudoers_plugin, SUDO_DEBUG_PLUGIN); + + /* Default policy plugin */ + info = calloc(1, sizeof(*info)); + if (info == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + info->symbol_name = strdup(symbol_name); + info->path = strdup(SUDOERS_PLUGIN); + if (info->symbol_name == NULL || info->path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free_plugin_info(info); + goto done; + } + /* info->options = NULL; */ + ret = sudo_load_plugin(info, optional); + free_plugin_info(info); + +done: + debug_return_bool(ret); +} + +/* + * Load the plugins listed in sudo.conf. + */ +bool +sudo_load_plugins(void) +{ + struct plugin_info_list *plugins; + struct plugin_info *info, *next; + bool ret = false; + debug_decl(sudo_load_plugins, SUDO_DEBUG_PLUGIN); + + /* Walk the plugin list from sudo.conf, if any and free it. */ + plugins = sudo_conf_plugins(); + TAILQ_FOREACH_SAFE(info, plugins, entries, next) { + ret = sudo_load_plugin(info, false); + if (!ret) + goto done; + free_plugin_info(info); + } + TAILQ_INIT(plugins); + + /* + * If no policy plugin, fall back to the default (sudoers). + * If there is also no I/O log plugin, use sudoers for that too. + */ + if (policy_plugin.handle == NULL) { + /* Default policy plugin */ + ret = sudo_load_sudoers_plugin("sudoers_policy", false); + if (!ret) + goto done; + + /* Default audit plugin, optional (sudoers < 1.9.1 lack this) */ + (void)sudo_load_sudoers_plugin("sudoers_audit", true); + + /* Default I/O plugin */ + if (TAILQ_EMPTY(&io_plugins)) { + ret = sudo_load_sudoers_plugin("sudoers_io", false); + if (!ret) + goto done; + } + } else if (strcmp(policy_plugin.name, "sudoers_policy") == 0) { + /* + * If policy plugin is sudoers_policy but there is no sudoers_audit + * loaded, load it too, if possible. + */ + if (!plugin_exists(&audit_plugins, "sudoers_audit")) { + if (sudo_load_sudoers_plugin("sudoers_audit", true)) { + /* + * Move the plugin options from sudoers_policy to sudoers_audit + * since the audit module is now what actually opens sudoers. + */ + if (policy_plugin.options != NULL) { + TAILQ_LAST(&audit_plugins, plugin_container_list)->options = + policy_plugin.options; + policy_plugin.options = NULL; + } + } + } + } + + /* TODO: check all plugins for open function too */ + if (policy_plugin.u.policy->check_policy == NULL) { + sudo_warnx(U_("policy plugin %s does not include a check_policy method"), + policy_plugin.name); + ret = false; + goto done; + } + + /* Set event_alloc() in plugins. */ + sudo_init_event_alloc(); + + /* Install hooks (XXX - later, after open). */ + sudo_register_hooks(); + +done: + debug_return_bool(ret); +} diff --git a/src/net_ifs.c b/src/net_ifs.c new file mode 100644 index 0000000..273ca9a --- /dev/null +++ b/src/net_ifs.c @@ -0,0 +1,373 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1996, 1998-2005, 2007-2015, 2018 + * Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +/* + * 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 + */ + +/* + * Suppress a warning w/ gcc on Digital UN*X. + * The system headers should really do this.... + */ +#if defined(__osf__) && !defined(__cplusplus) +struct mbuf; +struct rtentry; +#endif + +/* Avoid a compilation problem with gcc and machine/sys/getppdp.h */ +#define _MACHINE_SYS_GETPPDP_INCLUDED + +#include <config.h> + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#if defined(HAVE_SYS_SOCKIO_H) && !defined(SIOCGIFCONF) +# include <sys/sockio.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#include <unistd.h> +#include <netdb.h> +#include <errno.h> +#ifdef _ISC +# include <sys/stream.h> +# include <sys/sioctl.h> +# include <sys/stropts.h> +# define STRSET(cmd, param, len) {strioctl.ic_cmd=(cmd);\ + strioctl.ic_dp=(param);\ + strioctl.ic_timout=0;\ + strioctl.ic_len=(len);} +#endif /* _ISC */ +#ifdef _MIPS +# include <net/soioctl.h> +#endif /* _MIPS */ +#include <netinet/in.h> +#include <arpa/inet.h> +#ifdef NEED_RESOLV_H +# include <arpa/nameser.h> +# include <resolv.h> +#endif /* NEED_RESOLV_H */ +#include <net/if.h> +#ifdef HAVE_GETIFADDRS +# include <ifaddrs.h> +#endif + +#define NEED_INET_NTOP /* to expose sudo_inet_ntop in sudo_compat.h */ + +#define DEFAULT_TEXT_DOMAIN "sudo" + +#include "sudo_compat.h" +#include "sudo_conf.h" +#include "sudo_debug.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" + +/* Minix apparently lacks IFF_LOOPBACK */ +#ifndef IFF_LOOPBACK +# define IFF_LOOPBACK 0 +#endif + +#ifndef INET_ADDRSTRLEN +# define INET_ADDRSTRLEN 16 +#endif +#ifndef INET6_ADDRSTRLEN +# define INET6_ADDRSTRLEN 46 +#endif + +#ifdef HAVE_GETIFADDRS + +/* + * Fill in the interfaces string with the machine's ip addresses and netmasks + * and return the number of interfaces found. Returns -1 on error. + */ +int +get_net_ifs(char **addrinfo) +{ + struct ifaddrs *ifa, *ifaddrs; + struct sockaddr_in *sin; +#ifdef HAVE_STRUCT_IN6_ADDR + struct sockaddr_in6 *sin6; + char addrstr[INET6_ADDRSTRLEN], maskstr[INET6_ADDRSTRLEN]; +#else + char addrstr[INET_ADDRSTRLEN], maskstr[INET_ADDRSTRLEN]; +#endif + int len, num_interfaces = 0; + size_t ailen; + char *cp; + debug_decl(get_net_ifs, SUDO_DEBUG_NETIF); + + if (!sudo_conf_probe_interfaces()) + debug_return_int(0); + + if (getifaddrs(&ifaddrs) == -1) + debug_return_int(-1); + + /* Allocate space for the interfaces info string. */ + for (ifa = ifaddrs; ifa != NULL; ifa = ifa -> ifa_next) { + /* Skip interfaces marked "down" and "loopback". */ + if (ifa->ifa_addr == NULL || ifa->ifa_netmask == NULL || + !ISSET(ifa->ifa_flags, IFF_UP) || ISSET(ifa->ifa_flags, IFF_LOOPBACK)) + continue; + + switch (ifa->ifa_addr->sa_family) { + case AF_INET: +#ifdef HAVE_STRUCT_IN6_ADDR + case AF_INET6: +#endif + num_interfaces++; + break; + } + } + if (num_interfaces == 0) + goto done; + ailen = num_interfaces * 2 * INET6_ADDRSTRLEN; + if ((cp = malloc(ailen)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + num_interfaces = -1; + goto done; + } + *addrinfo = cp; + + /* Store the IP addr/netmask pairs. */ + for (ifa = ifaddrs; ifa != NULL; ifa = ifa -> ifa_next) { + /* Skip interfaces marked "down" and "loopback". */ + if (ifa->ifa_addr == NULL || ifa->ifa_netmask == NULL || + !ISSET(ifa->ifa_flags, IFF_UP) || ISSET(ifa->ifa_flags, IFF_LOOPBACK)) + continue; + + switch (ifa->ifa_addr->sa_family) { + case AF_INET: + sin = (struct sockaddr_in *)ifa->ifa_addr; + if (inet_ntop(AF_INET, &sin->sin_addr, addrstr, sizeof(addrstr)) == NULL) + continue; + sin = (struct sockaddr_in *)ifa->ifa_netmask; + if (inet_ntop(AF_INET, &sin->sin_addr, maskstr, sizeof(maskstr)) == NULL) + continue; + + len = snprintf(cp, ailen, "%s%s/%s", + cp == *addrinfo ? "" : " ", addrstr, maskstr); + if (len < 0 || (size_t)len >= ailen) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto done; + } + cp += len; + ailen -= len; + break; +#ifdef HAVE_STRUCT_IN6_ADDR + case AF_INET6: + sin6 = (struct sockaddr_in6 *)ifa->ifa_addr; + if (inet_ntop(AF_INET6, &sin6->sin6_addr, addrstr, sizeof(addrstr)) == NULL) + continue; + sin6 = (struct sockaddr_in6 *)ifa->ifa_netmask; + if (inet_ntop(AF_INET6, &sin6->sin6_addr, maskstr, sizeof(maskstr)) == NULL) + continue; + + len = snprintf(cp, ailen, "%s%s/%s", + cp == *addrinfo ? "" : " ", addrstr, maskstr); + if (len < 0 || (size_t)len >= ailen) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto done; + } + cp += len; + ailen -= len; + break; +#endif /* HAVE_STRUCT_IN6_ADDR */ + } + } + +done: +#ifdef HAVE_FREEIFADDRS + freeifaddrs(ifaddrs); +#else + free(ifaddrs); +#endif + debug_return_int(num_interfaces); +} + +#elif defined(SIOCGIFCONF) && !defined(STUB_LOAD_INTERFACES) + +/* + * Fill in the interfaces string with the machine's ip addresses and netmasks + * and return the number of interfaces found. Returns -1 on error. + */ +int +get_net_ifs(char **addrinfo) +{ + char ifr_tmpbuf[sizeof(struct ifreq)]; + struct ifreq *ifr, *ifr_tmp = (struct ifreq *)ifr_tmpbuf; + struct ifconf *ifconf; + struct sockaddr_in *sin; + int i, len, n, sock, num_interfaces = 0; + size_t ailen, buflen = sizeof(struct ifconf) + BUFSIZ; + char *cp, *previfname = "", *ifconf_buf = NULL; + char addrstr[INET_ADDRSTRLEN], maskstr[INET_ADDRSTRLEN]; +#ifdef _ISC + struct strioctl strioctl; +#endif /* _ISC */ + debug_decl(get_net_ifs, SUDO_DEBUG_NETIF); + + if (!sudo_conf_probe_interfaces()) + debug_return_int(0); + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) + debug_return_int(-1); + + /* + * Get interface configuration or return. + */ + for (;;) { + if ((ifconf_buf = malloc(buflen)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + num_interfaces = -1; + goto done; + } + ifconf = (struct ifconf *) ifconf_buf; + ifconf->ifc_len = buflen - sizeof(struct ifconf); + ifconf->ifc_buf = (caddr_t) (ifconf_buf + sizeof(struct ifconf)); + +#ifdef _ISC + STRSET(SIOCGIFCONF, (caddr_t) ifconf, buflen); + if (ioctl(sock, I_STR, (caddr_t) &strioctl) < 0) +#else + /* Note that some kernels return EINVAL if the buffer is too small */ + if (ioctl(sock, SIOCGIFCONF, (caddr_t) ifconf) < 0 && errno != EINVAL) +#endif /* _ISC */ + goto done; + + /* Break out of loop if we have a big enough buffer. */ + if (ifconf->ifc_len + sizeof(struct ifreq) < buflen) + break; + buflen += BUFSIZ; + free(ifconf_buf); + } + + /* Allocate space for the maximum number of interfaces that could exist. */ + if ((n = ifconf->ifc_len / sizeof(struct ifreq)) == 0) + goto done; + ailen = n * 2 * INET6_ADDRSTRLEN; + if ((cp = malloc(ailen)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + num_interfaces = -1; + goto done; + } + *addrinfo = cp; + + /* For each interface, store the ip address and netmask. */ + for (i = 0; i < ifconf->ifc_len; ) { + /* Get a pointer to the current interface. */ + ifr = (struct ifreq *) &ifconf->ifc_buf[i]; + + /* Set i to the subscript of the next interface. */ + i += sizeof(struct ifreq); +#ifdef HAVE_STRUCT_SOCKADDR_SA_LEN + if (ifr->ifr_addr.sa_len > sizeof(ifr->ifr_addr)) + i += ifr->ifr_addr.sa_len - sizeof(struct sockaddr); +#endif /* HAVE_STRUCT_SOCKADDR_SA_LEN */ + + /* Skip duplicates and interfaces with NULL addresses. */ + sin = (struct sockaddr_in *) &ifr->ifr_addr; + if (sin->sin_addr.s_addr == 0 || + strncmp(previfname, ifr->ifr_name, sizeof(ifr->ifr_name) - 1) == 0) + continue; + + if (ifr->ifr_addr.sa_family != AF_INET) + continue; + +#ifdef SIOCGIFFLAGS + memset(ifr_tmp, 0, sizeof(*ifr_tmp)); + memcpy(ifr_tmp->ifr_name, ifr->ifr_name, sizeof(ifr_tmp->ifr_name)); + if (ioctl(sock, SIOCGIFFLAGS, (caddr_t) ifr_tmp) < 0) +#endif + memcpy(ifr_tmp, ifr, sizeof(*ifr_tmp)); + + /* Skip interfaces marked "down" and "loopback". */ + if (!ISSET(ifr_tmp->ifr_flags, IFF_UP) || + ISSET(ifr_tmp->ifr_flags, IFF_LOOPBACK)) + continue; + + /* Get the netmask. */ + memset(ifr_tmp, 0, sizeof(*ifr_tmp)); + memcpy(ifr_tmp->ifr_name, ifr->ifr_name, sizeof(ifr_tmp->ifr_name)); + sin = (struct sockaddr_in *) &ifr_tmp->ifr_addr; +#ifdef _ISC + STRSET(SIOCGIFNETMASK, (caddr_t) ifr_tmp, sizeof(*ifr_tmp)); + if (ioctl(sock, I_STR, (caddr_t) &strioctl) < 0) +#else + if (ioctl(sock, SIOCGIFNETMASK, (caddr_t) ifr_tmp) < 0) +#endif /* _ISC */ + sin->sin_addr.s_addr = htonl(IN_CLASSC_NET); + + /* Convert the addr and mask to string form. */ + sin = (struct sockaddr_in *) &ifr->ifr_addr; + if (inet_ntop(AF_INET, &sin->sin_addr, addrstr, sizeof(addrstr)) == NULL) + continue; + sin = (struct sockaddr_in *) &ifr_tmp->ifr_addr; + if (inet_ntop(AF_INET, &sin->sin_addr, maskstr, sizeof(maskstr)) == NULL) + continue; + + len = snprintf(cp, ailen, "%s%s/%s", + cp == *addrinfo ? "" : " ", addrstr, maskstr); + if (len < 0 || (size_t)len >= ailen) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto done; + } + cp += len; + ailen -= len; + + /* Stash the name of the interface we saved. */ + previfname = ifr->ifr_name; + num_interfaces++; + } + +done: + free(ifconf_buf); + (void) close(sock); + + debug_return_int(num_interfaces); +} + +#else /* !SIOCGIFCONF || STUB_LOAD_INTERFACES */ + +/* + * Stub function for those without SIOCGIFCONF or getifaddrs() + */ +int +get_net_ifs(char **addrinfo) +{ + debug_decl(get_net_ifs, SUDO_DEBUG_NETIF); + debug_return_int(0); +} + +#endif /* SIOCGIFCONF && !STUB_LOAD_INTERFACES */ diff --git a/src/openbsd.c b/src/openbsd.c new file mode 100644 index 0000000..180a791 --- /dev/null +++ b/src/openbsd.c @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2012 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include "sudo.h" + +int +os_init(int argc, char *argv[], char *envp[]) +{ +#ifdef SUDO_DEVEL + extern char *malloc_options; + malloc_options = "S"; +#endif + return os_init_common(argc, argv, envp); +} diff --git a/src/parse_args.c b/src/parse_args.c new file mode 100644 index 0000000..2d326cc --- /dev/null +++ b/src/parse_args.c @@ -0,0 +1,863 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1993-1996, 1998-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +/* + * 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 <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <assert.h> +#ifdef HAVE_GETOPT_LONG +# include <getopt.h> +# else +# include "compat/getopt.h" +#endif /* HAVE_GETOPT_LONG */ + +#include <sudo_usage.h> +#include "sudo.h" +#include "sudo_lbuf.h" + +int tgetpass_flags; + +/* + * Local functions. + */ +static void help(void) __attribute__((__noreturn__)); +static void usage_excl(void) __attribute__((__noreturn__)); + +/* + * Mapping of command line flags to name/value settings. + */ +static struct sudo_settings sudo_settings[] = { +#define ARG_BSDAUTH_TYPE 0 + { "bsdauth_type" }, +#define ARG_LOGIN_CLASS 1 + { "login_class" }, +#define ARG_PRESERVE_ENVIRONMENT 2 + { "preserve_environment" }, +#define ARG_RUNAS_GROUP 3 + { "runas_group" }, +#define ARG_SET_HOME 4 + { "set_home" }, +#define ARG_USER_SHELL 5 + { "run_shell" }, +#define ARG_LOGIN_SHELL 6 + { "login_shell" }, +#define ARG_IGNORE_TICKET 7 + { "ignore_ticket" }, +#define ARG_PROMPT 8 + { "prompt" }, +#define ARG_SELINUX_ROLE 9 + { "selinux_role" }, +#define ARG_SELINUX_TYPE 10 + { "selinux_type" }, +#define ARG_RUNAS_USER 11 + { "runas_user" }, +#define ARG_PROGNAME 12 + { "progname" }, +#define ARG_IMPLIED_SHELL 13 + { "implied_shell" }, +#define ARG_PRESERVE_GROUPS 14 + { "preserve_groups" }, +#define ARG_NONINTERACTIVE 15 + { "noninteractive" }, +#define ARG_SUDOEDIT 16 + { "sudoedit" }, +#define ARG_CLOSEFROM 17 + { "closefrom" }, +#define ARG_NET_ADDRS 18 + { "network_addrs" }, +#define ARG_MAX_GROUPS 19 + { "max_groups" }, +#define ARG_PLUGIN_DIR 20 + { "plugin_dir" }, +#define ARG_REMOTE_HOST 21 + { "remote_host" }, +#define ARG_TIMEOUT 22 + { "timeout" }, +#define ARG_CHROOT 23 + { "cmnd_chroot" }, +#define ARG_CWD 24 + { "cmnd_cwd" }, +#define NUM_SETTINGS 25 + { NULL } +}; + +struct environment { + char **envp; /* pointer to the new environment */ + size_t env_size; /* size of new_environ in char **'s */ + size_t env_len; /* number of slots used, not counting NULL */ +}; + +/* + * Default flags allowed when running a command. + */ +#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_PRESERVE_GROUPS|MODE_SHELL) +#define EDIT_VALID_FLAGS MODE_NONINTERACTIVE +#define LIST_VALID_FLAGS (MODE_NONINTERACTIVE|MODE_LONG_LIST) +#define VALIDATE_VALID_FLAGS MODE_NONINTERACTIVE + +/* Option number for the --host long option due to ambiguity of the -h flag. */ +#define OPT_HOSTNAME 256 + +/* + * Available command line options, both short and long. + * Note that we must disable arg permutation to support setting environment + * variables and to better support the optional arg of the -h flag. + */ +static const char short_opts[] = "+Aa:BbC:c:D:Eeg:Hh::iKklnPp:R:r:SsT:t:U:u:Vv"; +static struct option long_opts[] = { + { "askpass", no_argument, NULL, 'A' }, + { "auth-type", required_argument, NULL, 'a' }, + { "background", no_argument, NULL, 'b' }, + { "bell", no_argument, NULL, 'B' }, + { "close-from", required_argument, NULL, 'C' }, + { "login-class", required_argument, NULL, 'c' }, + { "chdir", required_argument, NULL, 'D' }, + { "preserve-env", optional_argument, NULL, 'E' }, + { "edit", no_argument, NULL, 'e' }, + { "group", required_argument, NULL, 'g' }, + { "set-home", no_argument, NULL, 'H' }, + { "help", no_argument, NULL, 'h' }, + { "host", required_argument, NULL, OPT_HOSTNAME }, + { "login", no_argument, NULL, 'i' }, + { "remove-timestamp", no_argument, NULL, 'K' }, + { "reset-timestamp", no_argument, NULL, 'k' }, + { "list", no_argument, NULL, 'l' }, + { "non-interactive", no_argument, NULL, 'n' }, + { "preserve-groups", no_argument, NULL, 'P' }, + { "prompt", required_argument, NULL, 'p' }, + { "chroot", required_argument, NULL, 'R' }, + { "role", required_argument, NULL, 'r' }, + { "stdin", no_argument, NULL, 'S' }, + { "shell", no_argument, NULL, 's' }, + { "type", required_argument, NULL, 't' }, + { "command-timeout",required_argument, NULL, 'T' }, + { "other-user", required_argument, NULL, 'U' }, + { "user", required_argument, NULL, 'u' }, + { "version", no_argument, NULL, 'V' }, + { "validate", no_argument, NULL, 'v' }, + { NULL, no_argument, NULL, '\0' }, +}; + +/* + * Insert a key=value pair into the specified environment. + */ +static void +env_insert(struct environment *e, char *pair) +{ + debug_decl(env_insert, SUDO_DEBUG_ARGS); + + /* Make sure we have at least two slots free (one for NULL). */ + if (e->env_len + 1 >= e->env_size) { + char **tmp; + + if (e->env_size == 0) + e->env_size = 16; + tmp = reallocarray(e->envp, e->env_size, 2 * sizeof(char *)); + if (tmp == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + e->envp = tmp; + e->env_size *= 2; + } + e->envp[e->env_len++] = pair; + e->envp[e->env_len] = NULL; + + debug_return; +} + +/* + * Format as var=val and insert into the specified environment. + */ +static void +env_set(struct environment *e, char *var, char *val) +{ + char *pair; + debug_decl(env_set, SUDO_DEBUG_ARGS); + + pair = sudo_new_key_val(var, val); + if (pair == NULL) { + sudo_fatalx(U_("%s: %s"), + __func__, U_("unable to allocate memory")); + } + env_insert(e, pair); + + debug_return; +} + +/* + * Parse a comma-separated list of env vars and add to the + * specified environment. + */ +static void +parse_env_list(struct environment *e, char *list) +{ + char *cp, *last, *val; + debug_decl(parse_env_list, SUDO_DEBUG_ARGS); + + for ((cp = strtok_r(list, ",", &last)); cp != NULL; + (cp = strtok_r(NULL, ",", &last))) { + if (strchr(cp, '=') != NULL) { + sudo_warnx(U_("invalid environment variable name: %s"), cp); + usage(); + } + if ((val = getenv(cp)) != NULL) + env_set(e, cp, val); + } + debug_return; +} + +/* + * Command line argument parsing. + * Sets nargc and nargv which corresponds to the argc/argv we'll use + * for the command to be run (if we are running one). + */ +int +parse_args(int argc, char **argv, int *old_optind, int *nargc, char ***nargv, + struct sudo_settings **settingsp, char ***env_addp) +{ + struct environment extra_env; + int mode = 0; /* what mode is sudo to be run in? */ + int flags = 0; /* mode flags */ + int valid_flags = DEFAULT_VALID_FLAGS; + int ch, i; + char *cp; + const char *progname; + int proglen; + debug_decl(parse_args, SUDO_DEBUG_ARGS); + + /* Is someone trying something funny? */ + if (argc <= 0) + usage(); + + /* Pass progname to plugin so it can call initprogname() */ + progname = getprogname(); + sudo_settings[ARG_PROGNAME].value = progname; + + /* First, check to see if we were invoked as "sudoedit". */ + proglen = strlen(progname); + if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { + progname = "sudoedit"; + mode = MODE_EDIT; + sudo_settings[ARG_SUDOEDIT].value = "true"; + valid_flags = EDIT_VALID_FLAGS; + } + + /* Load local IP addresses and masks. */ + if (get_net_ifs(&cp) > 0) + sudo_settings[ARG_NET_ADDRS].value = cp; + + /* Set max_groups from sudo.conf. */ + i = sudo_conf_max_groups(); + if (i != -1) { + if (asprintf(&cp, "%d", i) == -1) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + sudo_settings[ARG_MAX_GROUPS].value = cp; + } + + /* Returns true if the last option string was "-h" */ +#define got_host_flag (optind > 1 && argv[optind - 1][0] == '-' && \ + argv[optind - 1][1] == 'h' && argv[optind - 1][2] == '\0') + + /* Returns true if the last option string was "--" */ +#define got_end_of_args (optind > 1 && argv[optind - 1][0] == '-' && \ + argv[optind - 1][1] == '-' && argv[optind - 1][2] == '\0') + + /* Returns true if next option is an environment variable */ +#define is_envar (optind < argc && argv[optind][0] != '/' && \ + strchr(argv[optind], '=') != NULL) + + /* Space for environment variables is lazy allocated. */ + memset(&extra_env, 0, sizeof(extra_env)); + + /* XXX - should fill in settings at the end to avoid dupes */ + for (;;) { + /* + * Some trickiness is required to allow environment variables + * to be interspersed with command line options. + */ + if ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { + switch (ch) { + case 'A': + SET(tgetpass_flags, TGP_ASKPASS); + break; +#ifdef HAVE_BSD_AUTH_H + case 'a': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_BSDAUTH_TYPE].value != NULL) + usage(); + sudo_settings[ARG_BSDAUTH_TYPE].value = optarg; + break; +#endif + case 'b': + SET(flags, MODE_BACKGROUND); + break; + case 'B': + SET(tgetpass_flags, TGP_BELL); + break; + case 'C': + assert(optarg != NULL); + if (sudo_strtonum(optarg, 3, INT_MAX, NULL) == 0) { + sudo_warnx("%s", + U_("the argument to -C must be a number greater than or equal to 3")); + usage(); + } + if (sudo_settings[ARG_CLOSEFROM].value != NULL) + usage(); + sudo_settings[ARG_CLOSEFROM].value = optarg; + break; +#ifdef HAVE_LOGIN_CAP_H + case 'c': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_LOGIN_CLASS].value != NULL) + usage(); + sudo_settings[ARG_LOGIN_CLASS].value = optarg; + break; +#endif + case 'D': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_CWD].value != NULL) + usage(); + sudo_settings[ARG_CWD].value = optarg; + break; + case 'E': + /* + * Optional argument is a comma-separated list of + * environment variables to preserve. + * If not present, preserve everything. + */ + if (optarg == NULL) { + sudo_settings[ARG_PRESERVE_ENVIRONMENT].value = "true"; + SET(flags, MODE_PRESERVE_ENV); + } else { + parse_env_list(&extra_env, optarg); + } + break; + case 'e': + if (mode && mode != MODE_EDIT) + usage_excl(); + mode = MODE_EDIT; + sudo_settings[ARG_SUDOEDIT].value = "true"; + valid_flags = EDIT_VALID_FLAGS; + break; + case 'g': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_RUNAS_GROUP].value != NULL) + usage(); + sudo_settings[ARG_RUNAS_GROUP].value = optarg; + break; + case 'H': + sudo_settings[ARG_SET_HOME].value = "true"; + SET(flags, MODE_RESET_HOME); + break; + case 'h': + if (optarg == NULL) { + /* + * Optional args support -hhostname, not -h hostname. + * If we see a non-option after the -h flag, treat as + * remote host and bump optind to skip over it. + */ + if (got_host_flag && argv[optind] != NULL && + argv[optind][0] != '-' && !is_envar) { + if (sudo_settings[ARG_REMOTE_HOST].value != NULL) + usage(); + sudo_settings[ARG_REMOTE_HOST].value = argv[optind++]; + continue; + } + if (mode && mode != MODE_HELP) { + if (strcmp(progname, "sudoedit") != 0) + usage_excl(); + } + mode = MODE_HELP; + valid_flags = 0; + break; + } + FALLTHROUGH; + case OPT_HOSTNAME: + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_REMOTE_HOST].value != NULL) + usage(); + sudo_settings[ARG_REMOTE_HOST].value = optarg; + break; + case 'i': + sudo_settings[ARG_LOGIN_SHELL].value = "true"; + SET(flags, MODE_LOGIN_SHELL); + break; + case 'k': + sudo_settings[ARG_IGNORE_TICKET].value = "true"; + break; + case 'K': + sudo_settings[ARG_IGNORE_TICKET].value = "true"; + if (mode && mode != MODE_KILL) + usage_excl(); + mode = MODE_KILL; + valid_flags = 0; + break; + case 'l': + if (mode) { + if (mode == MODE_LIST) + SET(flags, MODE_LONG_LIST); + else + usage_excl(); + } + mode = MODE_LIST; + valid_flags = LIST_VALID_FLAGS; + break; + case 'n': + SET(flags, MODE_NONINTERACTIVE); + sudo_settings[ARG_NONINTERACTIVE].value = "true"; + break; + case 'P': + sudo_settings[ARG_PRESERVE_GROUPS].value = "true"; + SET(flags, MODE_PRESERVE_GROUPS); + break; + case 'p': + /* An empty prompt is allowed. */ + assert(optarg != NULL); + if (sudo_settings[ARG_PROMPT].value != NULL) + usage(); + sudo_settings[ARG_PROMPT].value = optarg; + break; + case 'R': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_CHROOT].value != NULL) + usage(); + sudo_settings[ARG_CHROOT].value = optarg; + break; +#ifdef HAVE_SELINUX + case 'r': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_SELINUX_ROLE].value != NULL) + usage(); + sudo_settings[ARG_SELINUX_ROLE].value = optarg; + break; + case 't': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_SELINUX_TYPE].value != NULL) + usage(); + sudo_settings[ARG_SELINUX_TYPE].value = optarg; + break; +#endif + case 'T': + /* Plugin determines whether empty timeout is allowed. */ + assert(optarg != NULL); + if (sudo_settings[ARG_TIMEOUT].value != NULL) + usage(); + sudo_settings[ARG_TIMEOUT].value = optarg; + break; + case 'S': + SET(tgetpass_flags, TGP_STDIN); + break; + case 's': + sudo_settings[ARG_USER_SHELL].value = "true"; + SET(flags, MODE_SHELL); + break; + case 'U': + assert(optarg != NULL); + if (list_user != NULL || *optarg == '\0') + usage(); + list_user = optarg; + break; + case 'u': + assert(optarg != NULL); + if (*optarg == '\0') + usage(); + if (sudo_settings[ARG_RUNAS_USER].value != NULL) + usage(); + sudo_settings[ARG_RUNAS_USER].value = optarg; + break; + case 'v': + if (mode && mode != MODE_VALIDATE) + usage_excl(); + mode = MODE_VALIDATE; + valid_flags = VALIDATE_VALID_FLAGS; + break; + case 'V': + if (mode && mode != MODE_VERSION) + usage_excl(); + mode = MODE_VERSION; + valid_flags = 0; + break; + default: + usage(); + } + } else if (!got_end_of_args && is_envar) { + /* Insert key=value pair, crank optind and resume getopt. */ + env_insert(&extra_env, argv[optind]); + optind++; + } else { + /* Not an option or an environment variable -- we're done. */ + break; + } + } + + argc -= optind; + argv += optind; + *old_optind = optind; + + if (!mode) { + /* Defer -k mode setting until we know whether it is a flag or not */ + if (sudo_settings[ARG_IGNORE_TICKET].value != NULL) { + if (argc == 0 && !ISSET(flags, MODE_SHELL|MODE_LOGIN_SHELL)) { + mode = MODE_INVALIDATE; /* -k by itself */ + sudo_settings[ARG_IGNORE_TICKET].value = NULL; + valid_flags = 0; + } + } + if (!mode) + mode = MODE_RUN; /* running a command */ + } + + if (argc > 0 && mode == MODE_LIST) + mode = MODE_CHECK; + + if (ISSET(flags, MODE_LOGIN_SHELL)) { + if (ISSET(flags, MODE_SHELL)) { + sudo_warnx("%s", + U_("you may not specify both the -i and -s options")); + usage(); + } + if (ISSET(flags, MODE_PRESERVE_ENV)) { + sudo_warnx("%s", + U_("you may not specify both the -i and -E options")); + usage(); + } + SET(flags, MODE_SHELL); + } + if ((flags & valid_flags) != flags) + usage(); + if (mode == MODE_EDIT && + (ISSET(flags, MODE_PRESERVE_ENV) || extra_env.env_len != 0)) { + if (ISSET(mode, MODE_PRESERVE_ENV)) + sudo_warnx("%s", U_("the -E option is not valid in edit mode")); + if (extra_env.env_len != 0) + sudo_warnx("%s", + U_("you may not specify environment variables in edit mode")); + usage(); + } + if ((sudo_settings[ARG_RUNAS_USER].value != NULL || + sudo_settings[ARG_RUNAS_GROUP].value != NULL) && + !ISSET(mode, MODE_EDIT | MODE_RUN | MODE_CHECK | MODE_VALIDATE)) { + usage(); + } + if (list_user != NULL && mode != MODE_LIST && mode != MODE_CHECK) { + sudo_warnx("%s", + U_("the -U option may only be used with the -l option")); + usage(); + } + if (ISSET(tgetpass_flags, TGP_STDIN) && ISSET(tgetpass_flags, TGP_ASKPASS)) { + sudo_warnx("%s", U_("the -A and -S options may not be used together")); + usage(); + } + if ((argc == 0 && mode == MODE_EDIT) || + (argc > 0 && !ISSET(mode, MODE_RUN | MODE_EDIT | MODE_CHECK))) + usage(); + if (argc == 0 && mode == MODE_RUN && !ISSET(flags, MODE_SHELL)) { + SET(flags, (MODE_IMPLIED_SHELL | MODE_SHELL)); + sudo_settings[ARG_IMPLIED_SHELL].value = "true"; + } +#ifdef ENABLE_SUDO_PLUGIN_API + sudo_settings[ARG_PLUGIN_DIR].value = sudo_conf_plugin_dir_path(); +#endif + + if (mode == MODE_HELP) + help(); + + /* + * For shell mode we need to rewrite argv + */ + if (ISSET(flags, MODE_SHELL|MODE_LOGIN_SHELL) && ISSET(mode, MODE_RUN)) { + char **av, *cmnd = NULL; + int ac = 1; + + if (argc != 0) { + /* shell -c "command" */ + char *src, *dst; + size_t size = 0; + + for (av = argv; *av != NULL; av++) + size += strlen(*av) + 1; + if (size == 0 || (cmnd = reallocarray(NULL, size, 2)) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (!gc_add(GC_PTR, cmnd)) + exit(EXIT_FAILURE); + + for (dst = cmnd, av = argv; *av != NULL; av++) { + for (src = *av; *src != '\0'; src++) { + /* quote potential meta characters */ + if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$') + *dst++ = '\\'; + *dst++ = *src; + } + *dst++ = ' '; + } + if (cmnd != dst) + dst--; /* replace last space with a NUL */ + *dst = '\0'; + + ac += 2; /* -c cmnd */ + } + + av = reallocarray(NULL, ac + 1, sizeof(char *)); + if (av == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (!gc_add(GC_PTR, av)) + exit(EXIT_FAILURE); + + av[0] = (char *)user_details.shell; /* plugin may override shell */ + if (cmnd != NULL) { + av[1] = "-c"; + av[2] = cmnd; + } + av[ac] = NULL; + + argv = av; + argc = ac; + } + + /* + * For sudoedit we need to rewrite argv + */ + if (mode == MODE_EDIT) { +#if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID) + char **av; + int ac; + + av = reallocarray(NULL, argc + 2, sizeof(char *)); + if (av == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (!gc_add(GC_PTR, av)) + exit(EXIT_FAILURE); + + /* Must have the command in argv[0]. */ + av[0] = "sudoedit"; + for (ac = 0; argv[ac] != NULL; ac++) { + av[ac + 1] = argv[ac]; + } + av[++ac] = NULL; + + argv = av; + argc = ac; +#else + sudo_fatalx("%s", U_("sudoedit is not supported on this platform")); +#endif + } + + *settingsp = sudo_settings; + *env_addp = extra_env.envp; + *nargc = argc; + *nargv = argv; + debug_return_int(mode | flags); +} + +static int +usage_err(const char *buf) +{ + return fputs(buf, stderr); +} + +static int +usage_out(const char *buf) +{ + return fputs(buf, stdout); +} + +/* + * Display usage message. + * The actual usage strings are in sudo_usage.h for configure substitution. + */ +static void +display_usage(int (*output)(const char *)) +{ + struct sudo_lbuf lbuf; + char *uvec[6]; + int i, ulen; + + /* + * Use usage vectors appropriate to the progname. + */ + if (strcmp(getprogname(), "sudoedit") == 0) { + uvec[0] = &SUDO_USAGE5[3]; + uvec[1] = NULL; + } else { + uvec[0] = SUDO_USAGE1; + uvec[1] = SUDO_USAGE2; + uvec[2] = SUDO_USAGE3; + uvec[3] = SUDO_USAGE4; + uvec[4] = SUDO_USAGE5; + uvec[5] = NULL; + } + + /* + * Print usage and wrap lines as needed, depending on the + * tty width. + */ + ulen = (int)strlen(getprogname()) + 8; + sudo_lbuf_init(&lbuf, output, ulen, NULL, + user_details.ts_cols); + for (i = 0; uvec[i] != NULL; i++) { + sudo_lbuf_append(&lbuf, "usage: %s%s", getprogname(), uvec[i]); + sudo_lbuf_print(&lbuf); + } + sudo_lbuf_destroy(&lbuf); +} + +/* + * Display usage message and exit. + */ +void +usage(void) +{ + display_usage(usage_err); + exit(EXIT_FAILURE); +} + +/* + * Tell which options are mutually exclusive and exit. + */ +static void +usage_excl(void) +{ + debug_decl(usage_excl, SUDO_DEBUG_ARGS); + + sudo_warnx("%s", + U_("Only one of the -e, -h, -i, -K, -l, -s, -v or -V options may be specified")); + usage(); +} + +static void +help(void) +{ + struct sudo_lbuf lbuf; + const int indent = 32; + const char *pname = getprogname(); + debug_decl(help, SUDO_DEBUG_ARGS); + + sudo_lbuf_init(&lbuf, usage_out, indent, NULL, user_details.ts_cols); + if (strcmp(pname, "sudoedit") == 0) + sudo_lbuf_append(&lbuf, _("%s - edit files as another user\n\n"), pname); + else + sudo_lbuf_append(&lbuf, _("%s - execute a command as another user\n\n"), pname); + sudo_lbuf_print(&lbuf); + + display_usage(usage_out); + + sudo_lbuf_append(&lbuf, "%s", _("\nOptions:\n")); + sudo_lbuf_append(&lbuf, " -A, --askpass %s\n", + _("use a helper program for password prompting")); +#ifdef HAVE_BSD_AUTH_H + sudo_lbuf_append(&lbuf, " -a, --auth-type=type %s\n", + _("use specified BSD authentication type")); +#endif + sudo_lbuf_append(&lbuf, " -b, --background %s\n", + _("run command in the background")); + sudo_lbuf_append(&lbuf, " -B, --bell %s\n", + _("ring bell when prompting")); + sudo_lbuf_append(&lbuf, " -C, --close-from=num %s\n", + _("close all file descriptors >= num")); +#ifdef HAVE_LOGIN_CAP_H + sudo_lbuf_append(&lbuf, " -c, --login-class=class %s\n", + _("run command with the specified BSD login class")); +#endif + sudo_lbuf_append(&lbuf, " -D, --chdir=directory %s\n", + _("change the working directory before running command")); + sudo_lbuf_append(&lbuf, " -E, --preserve-env %s\n", + _("preserve user environment when running command")); + sudo_lbuf_append(&lbuf, " --preserve-env=list %s\n", + _("preserve specific environment variables")); + sudo_lbuf_append(&lbuf, " -e, --edit %s\n", + _("edit files instead of running a command")); + sudo_lbuf_append(&lbuf, " -g, --group=group %s\n", + _("run command as the specified group name or ID")); + sudo_lbuf_append(&lbuf, " -H, --set-home %s\n", + _("set HOME variable to target user's home dir")); + sudo_lbuf_append(&lbuf, " -h, --help %s\n", + _("display help message and exit")); + sudo_lbuf_append(&lbuf, " -h, --host=host %s\n", + _("run command on host (if supported by plugin)")); + sudo_lbuf_append(&lbuf, " -i, --login %s\n", + _("run login shell as the target user; a command may also be specified")); + sudo_lbuf_append(&lbuf, " -K, --remove-timestamp %s\n", + _("remove timestamp file completely")); + sudo_lbuf_append(&lbuf, " -k, --reset-timestamp %s\n", + _("invalidate timestamp file")); + sudo_lbuf_append(&lbuf, " -l, --list %s\n", + _("list user's privileges or check a specific command; use twice for longer format")); + sudo_lbuf_append(&lbuf, " -n, --non-interactive %s\n", + _("non-interactive mode, no prompts are used")); + sudo_lbuf_append(&lbuf, " -P, --preserve-groups %s\n", + _("preserve group vector instead of setting to target's")); + sudo_lbuf_append(&lbuf, " -p, --prompt=prompt %s\n", + _("use the specified password prompt")); + sudo_lbuf_append(&lbuf, " -R, --chroot=directory %s\n", + _("change the root directory before running command")); +#ifdef HAVE_SELINUX + sudo_lbuf_append(&lbuf, " -r, --role=role %s\n", + _("create SELinux security context with specified role")); +#endif + sudo_lbuf_append(&lbuf, " -S, --stdin %s\n", + _("read password from standard input")); + sudo_lbuf_append(&lbuf, " -s, --shell %s\n", + _("run shell as the target user; a command may also be specified")); +#ifdef HAVE_SELINUX + sudo_lbuf_append(&lbuf, " -t, --type=type %s\n", + _("create SELinux security context with specified type")); +#endif + sudo_lbuf_append(&lbuf, " -T, --command-timeout=timeout %s\n", + _("terminate command after the specified time limit")); + sudo_lbuf_append(&lbuf, " -U, --other-user=user %s\n", + _("in list mode, display privileges for user")); + sudo_lbuf_append(&lbuf, " -u, --user=user %s\n", + _("run command (or edit file) as specified user name or ID")); + sudo_lbuf_append(&lbuf, " -V, --version %s\n", + _("display version information and exit")); + sudo_lbuf_append(&lbuf, " -v, --validate %s\n", + _("update user's timestamp without running a command")); + sudo_lbuf_append(&lbuf, " -- %s\n", + _("stop processing command line arguments")); + sudo_lbuf_print(&lbuf); + sudo_lbuf_destroy(&lbuf); + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 0); + exit(EXIT_SUCCESS); +} diff --git a/src/preload.c b/src/preload.c new file mode 100644 index 0000000..250ee7e --- /dev/null +++ b/src/preload.c @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2010, 2011, 2013 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#ifdef HAVE_GSS_KRB5_CCACHE_NAME +# if defined(HAVE_GSSAPI_GSSAPI_KRB5_H) +# include <gssapi/gssapi.h> +# include <gssapi/gssapi_krb5.h> +# elif defined(HAVE_GSSAPI_GSSAPI_H) +# include <gssapi/gssapi.h> +# else +# include <gssapi.h> +# endif +#endif + +#include "sudo_plugin.h" +#include "sudo_compat.h" +#include "sudo_dso.h" + +#ifdef STATIC_SUDOERS_PLUGIN + +extern struct policy_plugin sudoers_policy; +extern struct io_plugin sudoers_io; +extern struct io_plugin sudoers_audit; + +static struct sudo_preload_symbol sudo_rtld_default_symbols[] = { +# ifdef HAVE_GSS_KRB5_CCACHE_NAME + { "gss_krb5_ccache_name", (void *)&gss_krb5_ccache_name}, +# endif + { (const char *)0, (void *)0 } +}; + +/* XXX - can we autogenerate these? */ +static struct sudo_preload_symbol sudo_sudoers_plugin_symbols[] = { + { "sudoers_policy", (void *)&sudoers_policy }, + { "sudoers_io", (void *)&sudoers_io }, + { "sudoers_audit", (void *)&sudoers_audit }, + { (const char *)0, (void *)0 } +}; + +/* + * Statically compiled symbols indexed by handle. + */ +static struct sudo_preload_table sudo_preload_table[] = { + { (char *)0, SUDO_DSO_DEFAULT, sudo_rtld_default_symbols }, + { "sudoers.so", &sudo_sudoers_plugin_symbols, sudo_sudoers_plugin_symbols }, + { (char *)0, (void *)0, (struct sudo_preload_symbol *)0 } +}; + +void +preload_static_symbols(void) +{ + sudo_dso_preload_table(sudo_preload_table); +} + +#endif /* STATIC_SUDOERS_PLUGIN */ diff --git a/src/preserve_fds.c b/src/preserve_fds.c new file mode 100644 index 0000000..3dd65db --- /dev/null +++ b/src/preserve_fds.c @@ -0,0 +1,216 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2013-2015 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> + +#include "sudo.h" + +/* + * Add an fd to preserve. + */ +int +add_preserved_fd(struct preserved_fd_list *pfds, int fd) +{ + struct preserved_fd *pfd, *pfd_new; + debug_decl(add_preserved_fd, SUDO_DEBUG_UTIL); + + pfd_new = malloc(sizeof(*pfd)); + if (pfd_new == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + pfd_new->lowfd = fd; + pfd_new->highfd = fd; + pfd_new->flags = fcntl(fd, F_GETFD); + if (pfd_new->flags == -1) { + free(pfd_new); + debug_return_int(-1); + } + + TAILQ_FOREACH(pfd, pfds, entries) { + if (fd == pfd->highfd) { + /* already preserved */ + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "fd %d already preserved", fd); + free(pfd_new); + pfd_new = NULL; + break; + } + if (fd < pfd->highfd) { + TAILQ_INSERT_BEFORE(pfd, pfd_new, entries); + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "preserving fd %d", fd); + pfd_new = NULL; + break; + } + } + if (pfd_new != NULL) { + TAILQ_INSERT_TAIL(pfds, pfd_new, entries); + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "preserving fd %d", fd); + } + + debug_return_int(0); +} + +/* + * Close all descriptors, startfd and higher except those listed + * in pfds. + */ +void +closefrom_except(int startfd, struct preserved_fd_list *pfds) +{ + int fd, lastfd = -1; + struct preserved_fd *pfd, *pfd_next; + unsigned char *fdbits; + debug_decl(closefrom_except, SUDO_DEBUG_UTIL); + + /* First, relocate preserved fds to be as contiguous as possible. */ + TAILQ_FOREACH_REVERSE_SAFE(pfd, pfds, preserved_fd_list, entries, pfd_next) { + if (pfd->highfd < startfd) + continue; + fd = dup(pfd->highfd); + if (fd == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "dup %d", pfd->highfd); + if (errno == EBADF) { + TAILQ_REMOVE(pfds, pfd, entries); + continue; + } + /* NOTE: still need to adjust lastfd below with unchanged lowfd. */ + } else if (fd < pfd->highfd) { + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "dup %d -> %d", pfd->highfd, pfd->lowfd); + sudo_debug_update_fd(pfd->highfd, pfd->lowfd); + pfd->lowfd = fd; + fd = pfd->highfd; + } + if (fd != -1) + (void) close(fd); + + if (pfd->lowfd > lastfd) + lastfd = pfd->lowfd; /* highest (relocated) preserved fd */ + } + + if (lastfd == -1) { + /* No fds to preserve. */ + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "closefrom(%d)", startfd); + closefrom(startfd); + debug_return; + } + + /* Create bitmap of preserved (relocated) fds. */ + fdbits = calloc((lastfd + NBBY) / NBBY, 1); + if (fdbits == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + TAILQ_FOREACH(pfd, pfds, entries) { + sudo_setbit(fdbits, pfd->lowfd); + } + + /* + * Close any unpreserved fds [startfd,lastfd] + */ + for (fd = startfd; fd <= lastfd; fd++) { + if (!sudo_isset(fdbits, fd)) { + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "closing fd %d", fd); +#ifdef __APPLE__ + /* Avoid potential libdispatch crash when we close its fds. */ + (void) fcntl(fd, F_SETFD, FD_CLOEXEC); +#else + (void) close(fd); +#endif + } + } + free(fdbits); + + /* Let closefrom() do the rest for us. */ + if (lastfd + 1 > startfd) + startfd = lastfd + 1; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "closefrom(%d)", startfd); + closefrom(startfd); + + /* Restore preserved fds and set flags. */ + TAILQ_FOREACH_REVERSE(pfd, pfds, preserved_fd_list, entries) { + if (pfd->lowfd != pfd->highfd) { + if (dup2(pfd->lowfd, pfd->highfd) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "dup2(%d, %d): %s", pfd->lowfd, pfd->highfd, + strerror(errno)); + } else { + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "dup2(%d, %d)", pfd->lowfd, pfd->highfd); + } + if (fcntl(pfd->highfd, F_SETFD, pfd->flags) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "fcntl(%d, F_SETFD, %d): %s", pfd->highfd, + pfd->flags, strerror(errno)); + } else { + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "fcntl(%d, F_SETFD, %d)", pfd->highfd, pfd->flags); + } + sudo_debug_update_fd(pfd->lowfd, pfd->highfd); + (void) close(pfd->lowfd); + pfd->lowfd = pfd->highfd; + } + } + debug_return; +} + +/* + * Parse a comma-separated list of fds and add them to preserved_fds. + */ +void +parse_preserved_fds(struct preserved_fd_list *pfds, const char *fdstr) +{ + const char *cp = fdstr; + long lval; + char *ep; + debug_decl(parse_preserved_fds, SUDO_DEBUG_UTIL); + + do { + errno = 0; + lval = strtol(cp, &ep, 10); + if (ep == cp || (*ep != ',' && *ep != '\0')) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to parse fd string %s", cp); + break; + } + if ((errno == ERANGE && lval == LONG_MAX) || lval < 0 || lval > INT_MAX) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "range error parsing fd string %s", cp); + } else { + add_preserved_fd(pfds, (int)lval); + } + cp = ep + 1; + } while (*ep != '\0'); + + debug_return; +} diff --git a/src/regress/noexec/check_noexec.c b/src/regress/noexec/check_noexec.c new file mode 100644 index 0000000..1e4eb59 --- /dev/null +++ b/src/regress/noexec/check_noexec.c @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2016 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + */ + +#include <config.h> + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#include <string.h> +#ifdef HAVE_WORDEXP_H +# include <wordexp.h> +#endif +#include <signal.h> +#include <unistd.h> +#include <limits.h> +#include <errno.h> + +#include "sudo_compat.h" +#include "sudo_fatal.h" +#include "sudo_util.h" +#include "sudo_exec.h" + +sudo_dso_public int main(int argc, char *argv[], char *envp[]); + +static bool +report_status(int status, const char *what) +{ + bool ret = false; + + /* system() returns -1 for exec failure. */ + if (status == -1) { + printf("%s: OK (%s)\n", getprogname(), what); + return true; + } + + /* check exit value, expecting 127 for failure */ + if (WIFEXITED(status)) { + int exitval = WEXITSTATUS(status); + if (exitval == 127) { + printf("%s: OK (%s)\n", getprogname(), what); + ret = true; + } else { + printf("%s: FAIL (%s) [%d]\n", getprogname(), what, exitval); + } + } else if (WIFSIGNALED(status)) { + printf("%s: FAIL (%s) [signal %d]\n", getprogname(), what, + WTERMSIG(status)); + } else { + /* should not happen */ + printf("%s: FAIL (%s) [status %d]\n", getprogname(), what, status); + } + + return ret; +} + +static int +try_execl(void) +{ + pid_t child, pid; + int status; + + child = fork(); + switch (child) { + case -1: + sudo_fatal_nodebug("fork"); + case 0: + /* child */ + /* Try to exec /bin/true, else exit with value 127. */ + execl("/bin/true", "true", (char *)0); + _exit(127); + default: + /* parent */ + do { + pid = waitpid(child, &status, 0); + } while (pid == -1 && errno == EINTR); + if (pid == -1) + sudo_fatal_nodebug("waitpid"); + + if (report_status(status, "execl")) + return 0; + return 1; + } +} + +static int +try_system(void) +{ + int status; + + /* Try to run /bin/true, system() returns 127 on exec failure. */ + status = system("/bin/true > /dev/null 2>&1"); + + if (report_status(status, "system")) + return 0; + return 1; +} + +#ifdef HAVE_WORDEXP_H +static int +try_wordexp(void) +{ + wordexp_t we; + int rc, ret = 1; + + /* + * sudo_noexec.so prevents command substitution via the WRDE_NOCMD flag + * where possible. + */ + rc = wordexp("$(/bin/echo foo)", &we, 0); + switch (rc) { + case -1: + /* sudo's wordexp() wrapper returns -1 if RTLD_NEXT is not supported. */ + case 127: + /* Solaris 10 wordexp() returns 127 for execve() failure. */ +#ifdef WRDE_ERRNO + case WRDE_ERRNO: + /* Solaris 11 wordexp() returns WRDE_ERRNO for execve() failure. */ +#endif + printf("%s: OK (wordexp) [%d]\n", getprogname(), rc); + ret = 0; + break; + case WRDE_SYNTAX: + /* FreeBSD returns WRDE_SYNTAX if it can't write to the shell process */ + printf("%s: OK (wordexp) [WRDE_SYNTAX]\n", getprogname()); + ret = 0; + break; + case WRDE_CMDSUB: + printf("%s: OK (wordexp) [WRDE_CMDSUB]\n", getprogname()); + ret = 0; + break; + case 0: + /* + * On HP-UX 11.00 we don't seem to be able to add WRDE_NOCMD + * but the execve() wrapper prevents the command substitution. + */ + if (we.we_wordc == 0) { + printf("%s: OK (wordexp) [%d]\n", getprogname(), rc); + wordfree(&we); + ret = 0; + break; + } + wordfree(&we); + FALLTHROUGH; + default: + printf("%s: FAIL (wordexp) [%d]\n", getprogname(), rc); + break; + } + return ret; +} +#endif + +int +main(int argc, char *argv[], char *envp[]) +{ + int errors = 0; + + initprogname(argc > 0 ? argv[0] : "check_noexec"); + + if (argc != 2) { + fprintf(stderr, "usage: %s regress | /path/to/sudo_noexec.so\n", getprogname()); + exit(EXIT_FAILURE); + } + + /* Disable execution for post-exec and re-exec ourself. */ + if (strcmp(argv[1], "rexec") != 0) { + const char *noexec = argv[1]; + argv[1] = "rexec"; + execve(argv[0], argv, disable_execute(envp, noexec)); + sudo_fatalx_nodebug("execve"); + } + + errors += try_execl(); + errors += try_system(); +#ifdef HAVE_WORDEXP_H + errors += try_wordexp(); +#endif + + return errors; +} diff --git a/src/regress/ttyname/check_ttyname.c b/src/regress/ttyname/check_ttyname.c new file mode 100644 index 0000000..5914f71 --- /dev/null +++ b/src/regress/ttyname/check_ttyname.c @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2013-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + */ + +#include <config.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> +#include <errno.h> + +#include "sudo_compat.h" +#include "sudo_fatal.h" +#include "sudo_util.h" +#include "sudo_debug.h" + +sudo_dso_public int main(int argc, char *argv[]); + +int sudo_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; +extern char *get_process_ttyname(char *name, size_t namelen); + +static int +match_ttys(const char *tty1, const char *tty2) +{ + struct stat sb1, sb2; + + if (tty1 != NULL && tty2 != NULL) { + if (strcmp(tty1, tty2) == 0) + return 0; + /* Could be the same device with a different name. */ + if (stat(tty1, &sb1) == 0 && S_ISCHR(sb1.st_mode) && + stat(tty2, &sb2) == 0 && S_ISCHR(sb2.st_mode)) { + if (sb1.st_rdev == sb2.st_rdev) + return 0; + } + } else if (tty1 == NULL && tty2 == NULL) { + return 0; + } + + return 1; +} + + +int +main(int argc, char *argv[]) +{ + char *tty_libc = NULL, *tty_sudo = NULL; + char pathbuf[PATH_MAX]; + int ret = 1; + + initprogname(argc > 0 ? argv[0] : "check_ttyname"); + + /* Lookup tty name using kernel info if possible. */ + if (get_process_ttyname(pathbuf, sizeof(pathbuf)) != NULL) + tty_sudo = pathbuf; + +#if defined(HAVE_KINFO_PROC2_NETBSD) || \ + defined(HAVE_KINFO_PROC_OPENBSD) || \ + defined(HAVE_KINFO_PROC_FREEBSD) || \ + defined(HAVE_KINFO_PROC_44BSD) || \ + defined(HAVE__TTYNAME_DEV) || defined(HAVE_STRUCT_PSINFO_PR_TTYDEV) || \ + defined(HAVE_PSTAT_GETPROC) || defined(__linux__) + + /* Lookup tty name attached to stdin via libc. */ + tty_libc = ttyname(STDIN_FILENO); +#endif + + /* Compare libc and kernel ttys. */ + ret = match_ttys(tty_libc, tty_sudo); + if (ret == 0) { + printf("%s: OK (%s)\n", getprogname(), tty_sudo ? tty_sudo : "none"); + } else if (tty_libc == NULL) { + printf("%s: SKIP (%s)\n", getprogname(), tty_sudo ? tty_sudo : "none"); + ret = 0; + } else { + printf("%s: FAIL %s (sudo) vs. %s (libc)\n", getprogname(), + tty_sudo ? tty_sudo : "none", tty_libc ? tty_libc : "none"); + } + + return ret; +} diff --git a/src/selinux.c b/src/selinux.c new file mode 100644 index 0000000..a2f73f8 --- /dev/null +++ b/src/selinux.c @@ -0,0 +1,512 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2016 Todd C. Miller <Todd.Miller@sudo.ws> + * Copyright (c) 2008 Dan Walsh <dwalsh@redhat.com> + * + * Borrowed heavily from newrole source code + * Authors: + * Anthony Colatrella + * Tim Fraser + * Steve Grubb <sgrubb@redhat.com> + * Darrel Goeddel <DGoeddel@trustedcs.com> + * Michael Thompson <mcthomps@us.ibm.com> + * Dan Walsh <dwalsh@redhat.com> + * + * 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 <config.h> + +#ifdef HAVE_SELINUX + +#include <sys/stat.h> +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> + +#include <selinux/selinux.h> /* for is_selinux_enabled() */ +#include <selinux/context.h> /* for context-mangling functions */ +#include <selinux/get_default_type.h> +#include <selinux/get_context_list.h> + +#ifdef HAVE_LINUX_AUDIT +# include <libaudit.h> +#endif + +#include "sudo.h" +#include "sudo_exec.h" + +static struct selinux_state { + security_context_t old_context; + security_context_t new_context; + security_context_t tty_con_raw; + security_context_t new_tty_con_raw; + const char *ttyn; + int ttyfd; + int enforcing; +} se_state; + +#ifdef HAVE_LINUX_AUDIT +static int +audit_role_change(const security_context_t old_context, + const security_context_t new_context, const char *ttyn, int result) +{ + int au_fd, rc = -1; + char *message; + debug_decl(audit_role_change, SUDO_DEBUG_SELINUX); + + au_fd = audit_open(); + if (au_fd == -1) { + /* Kernel may not have audit support. */ + if (errno != EINVAL && errno != EPROTONOSUPPORT && errno != EAFNOSUPPORT +) + sudo_fatal("%s", U_("unable to open audit system")); + } else { + /* audit role change using the same format as newrole(1) */ + rc = asprintf(&message, "newrole: old-context=%s new-context=%s", + old_context, new_context); + if (rc == -1) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + rc = audit_log_user_message(au_fd, AUDIT_USER_ROLE_CHANGE, + message, NULL, NULL, ttyn, result); + if (rc <= 0) + sudo_warn("%s", U_("unable to send audit message")); + free(message); + close(au_fd); + } + + debug_return_int(rc); +} +#endif + +/* + * This function attempts to revert the relabeling done to the tty. + * fd - referencing the opened ttyn + * ttyn - name of tty to restore + * + * Returns 0 on success and -1 on failure. + */ +int +selinux_restore_tty(void) +{ + int ret = -1; + security_context_t chk_tty_con_raw = NULL; + debug_decl(selinux_restore_tty, SUDO_DEBUG_SELINUX); + + if (se_state.ttyfd == -1 || se_state.new_tty_con_raw == NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: no tty, skip relabel", + __func__); + debug_return_int(0); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %s -> %s", + __func__, se_state.new_tty_con_raw, se_state.tty_con_raw); + + /* Verify that the tty still has the context set by sudo. */ + if (fgetfilecon_raw(se_state.ttyfd, &chk_tty_con_raw) == -1) { + sudo_warn(U_("unable to fgetfilecon %s"), se_state.ttyn); + goto skip_relabel; + } + + if (strcmp(chk_tty_con_raw, se_state.new_tty_con_raw) != 0) { + sudo_warnx(U_("%s changed labels"), se_state.ttyn); + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: not restoring tty label, expected %s, have %s", + __func__, se_state.new_tty_con_raw, chk_tty_con_raw); + goto skip_relabel; + } + + if (fsetfilecon_raw(se_state.ttyfd, se_state.tty_con_raw) == -1) { + sudo_warn(U_("unable to restore context for %s"), se_state.ttyn); + goto skip_relabel; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: successfully set tty label to %s", + __func__, se_state.tty_con_raw); + ret = 0; + +skip_relabel: + if (se_state.ttyfd != -1) { + close(se_state.ttyfd); + se_state.ttyfd = -1; + } + freecon(chk_tty_con_raw); + debug_return_int(ret); +} + +/* + * This function attempts to relabel the tty. If this function fails, then + * the contexts are free'd and -1 is returned. On success, 0 is returned + * and tty_con_raw and new_tty_con_raw are set. + * + * This function will not fail if it can not relabel the tty when selinux is + * in permissive mode. + */ +static int +relabel_tty(const char *ttyn, int ptyfd) +{ + security_context_t tty_con = NULL; + security_context_t new_tty_con = NULL; + struct stat sb; + int fd; + debug_decl(relabel_tty, SUDO_DEBUG_SELINUX); + + se_state.ttyfd = ptyfd; + + /* It is perfectly legal to have no tty. */ + if (ptyfd == -1 && ttyn == NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: no tty, skip relabel", + __func__); + debug_return_int(0); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: relabeling tty %s", __func__, ttyn); + + /* If sudo is not allocating a pty for the command, open current tty. */ + if (ptyfd == -1) { + se_state.ttyfd = open(ttyn, O_RDWR|O_NOCTTY|O_NONBLOCK); + if (se_state.ttyfd == -1 || fstat(se_state.ttyfd, &sb) == -1) { + sudo_warn(U_("unable to open %s, not relabeling tty"), ttyn); + goto bad; + } + if (!S_ISCHR(sb.st_mode)) { + sudo_warn(U_("%s is not a character device, not relabeling tty"), + ttyn); + goto bad; + } + (void)fcntl(se_state.ttyfd, F_SETFL, + fcntl(se_state.ttyfd, F_GETFL, 0) & ~O_NONBLOCK); + } + + if (fgetfilecon(se_state.ttyfd, &tty_con) == -1) { + sudo_warn("%s", U_("unable to get current tty context, not relabeling tty")); + goto bad; + } + + if (tty_con != NULL) { + security_class_t tclass = string_to_security_class("chr_file"); + if (tclass == 0) { + sudo_warn("%s", U_("unknown security class \"chr_file\", not relabeling tty")); + goto bad; + } + if (security_compute_relabel(se_state.new_context, tty_con, + tclass, &new_tty_con) == -1) { + sudo_warn("%s", U_("unable to get new tty context, not relabeling tty")); + goto bad; + } + } + + if (new_tty_con != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: tty context %s -> %s", + __func__, tty_con, new_tty_con); + if (fsetfilecon(se_state.ttyfd, new_tty_con) == -1) { + sudo_warn("%s", U_("unable to set new tty context")); + goto bad; + } + } + + if (ptyfd != -1) { + int oflags, flags = 0; + + /* Reopen pty that was relabeled, std{in,out,err} are reset later. */ + se_state.ttyfd = open(ttyn, O_RDWR|O_NOCTTY, 0); + if (se_state.ttyfd == -1 || fstat(se_state.ttyfd, &sb) == -1) { + sudo_warn(U_("unable to open %s"), ttyn); + goto bad; + } + if (!S_ISCHR(sb.st_mode)) { + sudo_warn(U_("%s is not a character device, not relabeling tty"), + ttyn); + goto bad; + } + /* Preserve O_NONBLOCK and the close-on-exec flags. */ + if ((oflags = fcntl(ptyfd, F_GETFL)) == -1) { + sudo_warn("F_GETFL"); + goto bad; + } + if (ISSET(oflags, O_NONBLOCK)) + flags |= O_NONBLOCK; + if ((oflags = fcntl(ptyfd, F_GETFD)) == -1) { + sudo_warn("F_GETFD"); + goto bad; + } + if (ISSET(oflags, FD_CLOEXEC)) + flags |= O_CLOEXEC; + if (dup3(se_state.ttyfd, ptyfd, flags) == -1) { + sudo_warn("dup3"); + goto bad; + } + } else { + /* Re-open tty to get new label and reset std{in,out,err} */ + close(se_state.ttyfd); + se_state.ttyfd = open(ttyn, O_RDWR|O_NOCTTY|O_NONBLOCK); + if (se_state.ttyfd == -1 || fstat(se_state.ttyfd, &sb) == -1) { + sudo_warn(U_("unable to open %s"), ttyn); + goto bad; + } + if (!S_ISCHR(sb.st_mode)) { + sudo_warn(U_("%s is not a character device, not relabeling tty"), + ttyn); + goto bad; + } + (void)fcntl(se_state.ttyfd, F_SETFL, + fcntl(se_state.ttyfd, F_GETFL, 0) & ~O_NONBLOCK); + for (fd = STDIN_FILENO; fd <= STDERR_FILENO; fd++) { + if (isatty(fd) && dup2(se_state.ttyfd, fd) == -1) { + sudo_warn("dup2"); + goto bad; + } + } + } + /* Retain se_state.ttyfd so we can restore label when command finishes. */ + (void)fcntl(se_state.ttyfd, F_SETFD, FD_CLOEXEC); + + se_state.ttyn = ttyn; + if (selinux_trans_to_raw_context(tty_con, &se_state.tty_con_raw) == -1) + goto bad; + if (selinux_trans_to_raw_context(new_tty_con, &se_state.new_tty_con_raw) == -1) + goto bad; + freecon(tty_con); + freecon(new_tty_con); + debug_return_int(0); + +bad: + if (se_state.ttyfd != -1 && se_state.ttyfd != ptyfd) { + close(se_state.ttyfd); + se_state.ttyfd = -1; + } + freecon(se_state.tty_con_raw); + se_state.tty_con_raw = NULL; + freecon(se_state.new_tty_con_raw); + se_state.new_tty_con_raw = NULL; + freecon(tty_con); + freecon(new_tty_con); + debug_return_int(se_state.enforcing ? -1 : 0); +} + +/* + * Returns a new security context based on the old context and the + * specified role and type. + */ +security_context_t +get_exec_context(security_context_t old_context, const char *role, const char *type) +{ + security_context_t new_context = NULL; + context_t context = NULL; + char *typebuf = NULL; + debug_decl(get_exec_context, SUDO_DEBUG_SELINUX); + + /* We must have a role, the type is optional (we can use the default). */ + if (role == NULL) { + sudo_warnx(U_("you must specify a role for type %s"), type); + errno = EINVAL; + goto bad; + } + if (type == NULL) { + if (get_default_type(role, &typebuf)) { + sudo_warnx(U_("unable to get default type for role %s"), role); + errno = EINVAL; + goto bad; + } + type = typebuf; + } + + /* + * Expand old_context into a context_t so that we can extract and modify + * its components easily. + */ + if ((context = context_new(old_context)) == NULL) { + sudo_warn("%s", U_("failed to get new context")); + goto bad; + } + + /* + * Replace the role and type in "context" with the role and + * type we will be running the command as. + */ + if (context_role_set(context, role)) { + sudo_warn(U_("failed to set new role %s"), role); + goto bad; + } + if (context_type_set(context, type)) { + sudo_warn(U_("failed to set new type %s"), type); + goto bad; + } + + /* + * Convert "context" back into a string and verify it. + */ + if ((new_context = strdup(context_str(context))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + if (security_check_context(new_context) == -1) { + sudo_warnx(U_("%s is not a valid context"), new_context); + errno = EINVAL; + goto bad; + } + + context_free(context); + debug_return_str(new_context); + +bad: + free(typebuf); + context_free(context); + freecon(new_context); + debug_return_str(NULL); +} + +/* + * Determine the exec and tty contexts in preparation for fork/exec. + * Must run as root, before forking the child process. + * Sets the tty context but not the exec context (which happens later). + * If ptyfd is not -1, it indicates we are running + * in a pty and do not need to reset std{in,out,err}. + * Returns 0 on success and -1 on failure. + */ +int +selinux_setup(const char *role, const char *type, const char *ttyn, + int ptyfd, bool label_tty) +{ + int ret = -1; + debug_decl(selinux_setup, SUDO_DEBUG_SELINUX); + + /* Store the caller's SID in old_context. */ + if (getprevcon(&se_state.old_context)) { + sudo_warn("%s", U_("failed to get old context")); + goto done; + } + + se_state.enforcing = security_getenforce(); + if (se_state.enforcing == -1) { + sudo_warn("%s", U_("unable to determine enforcing mode.")); + goto done; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: old context %s", __func__, + se_state.old_context); + se_state.new_context = get_exec_context(se_state.old_context, role, type); + if (se_state.new_context == NULL) { +#ifdef HAVE_LINUX_AUDIT + audit_role_change(se_state.old_context, "?", se_state.ttyn, 0); +#endif + goto done; + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: new context %s", __func__, + se_state.new_context); + + if (label_tty && relabel_tty(ttyn, ptyfd) == -1) { + sudo_warn(U_("unable to set tty context to %s"), se_state.new_context); + goto done; + } + +#ifdef HAVE_LINUX_AUDIT + audit_role_change(se_state.old_context, se_state.new_context, + se_state.ttyn, 1); +#endif + + ret = 0; + +done: + debug_return_int(ret); +} + +int +selinux_setcon(void) +{ + debug_decl(selinux_setcon, SUDO_DEBUG_SELINUX); + + if (setexeccon(se_state.new_context)) { + sudo_warn(U_("unable to set exec context to %s"), se_state.new_context); + if (se_state.enforcing) + debug_return_int(-1); + } + +#ifdef HAVE_SETKEYCREATECON + if (setkeycreatecon(se_state.new_context)) { + sudo_warn(U_("unable to set key creation context to %s"), se_state.new_context); + if (se_state.enforcing) + debug_return_int(-1); + } +#endif /* HAVE_SETKEYCREATECON */ + + debug_return_int(0); +} + +void +selinux_execve(int fd, const char *path, char *const argv[], char *envp[], + bool noexec) +{ + char **nargv; + const char *sesh; + int argc, nargc, serrno; + debug_decl(selinux_execve, SUDO_DEBUG_SELINUX); + + sesh = sudo_conf_sesh_path(); + if (sesh == NULL) { + sudo_warnx("internal error: sesh path not set"); + errno = EINVAL; + debug_return; + } + + /* Set SELinux exec and keycreate contexts. */ + if (selinux_setcon() == -1) + debug_return; + + /* + * Build new argv with sesh as argv[0]. + * If argv[0] ends in -noexec, sesh will disable execute + * for the command it runs. + */ + for (argc = 0; argv[argc] != NULL; argc++) + continue; + if (argc == 0) { + errno = EINVAL; + debug_return; + } + nargv = reallocarray(NULL, argc + 3, sizeof(char *)); + if (nargv == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return; + } + if (noexec) + nargv[0] = *argv[0] == '-' ? "-sesh-noexec" : "sesh-noexec"; + else + nargv[0] = *argv[0] == '-' ? "-sesh" : "sesh"; + nargc = 1; + if (fd != -1 && asprintf(&nargv[nargc++], "--execfd=%d", fd) == -1) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return; + } + nargv[nargc++] = (char *)path; + memcpy(&nargv[nargc], &argv[1], argc * sizeof(char *)); /* copies NULL */ + + /* sesh will handle noexec for us. */ + sudo_execve(-1, sesh, nargv, envp, false); + serrno = errno; + free(nargv); + errno = serrno; + debug_return; +} + +#endif /* HAVE_SELINUX */ diff --git a/src/sesh.c b/src/sesh.c new file mode 100644 index 0000000..f8f4654 --- /dev/null +++ b/src/sesh.c @@ -0,0 +1,424 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2008, 2010-2018, 2020-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/stat.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <signal.h> +#include <time.h> +#include <unistd.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ + +#include "sudo.h" +#include "sudo_exec.h" +#include "sudo_edit.h" + +sudo_dso_public int main(int argc, char *argv[], char *envp[]); + +static int sesh_sudoedit(int argc, char *argv[]); + +/* + * Exit codes defined in sudo_exec.h: + * SESH_SUCCESS (0) ... successful operation + * SESH_ERR_FAILURE (1) ... unspecified error + * SESH_ERR_INVALID (30) ... invalid -e arg value + * SESH_ERR_BAD_PATHS (31) ... odd number of paths + * SESH_ERR_NO_FILES (32) ... copy error, no files copied + * SESH_ERR_SOME_FILES (33) ... copy error, no files copied + */ +int +main(int argc, char *argv[], char *envp[]) +{ + int ret; + debug_decl(main, SUDO_DEBUG_MAIN); + + initprogname(argc > 0 ? argv[0] : "sesh"); + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE_NAME, LOCALEDIR); + textdomain(PACKAGE_NAME); + + if (argc < 2) + sudo_fatalx("%s", U_("requires at least one argument")); + + /* Read sudo.conf and initialize the debug subsystem. */ + if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1) + exit(EXIT_FAILURE); + sudo_debug_register(getprogname(), NULL, NULL, + sudo_conf_debug_files(getprogname())); + + if (strcmp(argv[1], "-e") == 0) { + ret = sesh_sudoedit(argc, argv); + } else { + bool login_shell, noexec = false; + char *cp, *cmnd; + int fd = -1; + + /* If the first char of argv[0] is '-', we are running a login shell. */ + login_shell = argv[0][0] == '-'; + + /* If argv[0] ends in -noexec, pass the flag to sudo_execve() */ + if ((cp = strrchr(argv[0], '-')) != NULL && cp != argv[0]) + noexec = strcmp(cp, "-noexec") == 0; + + /* If argv[1] is --execfd=%d, extract the fd to exec with. */ + if (strncmp(argv[1], "--execfd=", 9) == 0) { + const char *errstr; + + cp = argv[1] + 9; + fd = sudo_strtonum(cp, 0, INT_MAX, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("invalid file descriptor number: %s"), cp); + argv++; + argc--; + } + + /* Shift argv and make a copy of the command to execute. */ + argv++; + argc--; + if ((cmnd = strdup(argv[0])) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* If invoked as a login shell, modify argv[0] accordingly. */ + if (login_shell) { + if ((cp = strrchr(argv[0], '/')) == NULL) + sudo_fatal(U_("unable to run %s as a login shell"), argv[0]); + *cp = '-'; + argv[0] = cp; + } + sudo_execve(fd, cmnd, argv, envp, noexec); + sudo_warn(U_("unable to execute %s"), cmnd); + ret = SESH_ERR_FAILURE; + } + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, ret); + _exit(ret); +} + +/* + * Destructively parse a string in the format: + * uid:gid:groups,... + * + * On success, fills in ud and returns true, else false. + */ +static bool +parse_user(char *userstr, struct sudo_cred *cred) +{ + char *cp, *ep; + const char *errstr; + debug_decl(parse_user, SUDO_DEBUG_EDIT); + + /* UID */ + cp = userstr; + if ((ep = strchr(cp, ':')) == NULL) { + sudo_warnx(U_("%s: %s"), cp, U_("invalid value")); + debug_return_bool(false); + } + *ep++ = '\0'; + cred->uid = cred->euid = sudo_strtoid(cp, &errstr); + if (errstr != NULL) { + sudo_warnx(U_("%s: %s"), cp, errstr); + debug_return_bool(false); + } + + /* GID */ + cp = ep; + if ((ep = strchr(cp, ':')) == NULL) { + sudo_warnx(U_("%s: %s"), cp, U_("invalid value")); + debug_return_bool(false); + } + *ep++ = '\0'; + cred->gid = cred->egid = sudo_strtoid(cp, &errstr); + if (errstr != NULL) { + sudo_warnx(U_("%s: %s"), cp, errstr); + debug_return_bool(false); + } + + /* group vector */ + cp = ep; + cred->ngroups = sudo_parse_gids(cp, NULL, &cred->groups); + if (cred->ngroups == -1) + debug_return_bool(false); + + debug_return_bool(true); +} + +static int +sesh_edit_create_tfiles(int edit_flags, struct sudo_cred *user_cred, + struct sudo_cred *run_cred, int argc, char *argv[]) +{ + int i, fd_src = -1, fd_dst = -1; + struct timespec times[2]; + struct stat sb; + debug_decl(sesh_edit_create_tfiles, SUDO_DEBUG_EDIT); + + for (i = 0; i < argc - 1; i += 2) { + char *path_src = argv[i]; + const char *path_dst = argv[i + 1]; + + /* + * Try to open the source file for reading. + * If it doesn't exist, we'll create an empty destination file. + */ + fd_src = sudo_edit_open(path_src, O_RDONLY, + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, edit_flags, user_cred, run_cred); + if (fd_src == -1) { + if (errno != ENOENT) { + if (errno == ELOOP) { + sudo_warnx(U_("%s: editing symbolic links is not " + "permitted"), path_src); + } else if (errno == EISDIR) { + sudo_warnx(U_("%s: editing files in a writable directory " + "is not permitted"), path_src); + } else { + sudo_warn("%s", path_src); + } + goto cleanup; + } + /* New file, verify parent dir exists and is not writable. */ + if (!sudo_edit_parent_valid(path_src, edit_flags, user_cred, run_cred)) + goto cleanup; + } + if (fd_src == -1) { + /* New file. */ + memset(&sb, 0, sizeof(sb)); + } else if (fstat(fd_src, &sb) == -1 || !S_ISREG(sb.st_mode)) { + sudo_warnx(U_("%s: not a regular file"), path_src); + goto cleanup; + } + + /* + * Create temporary file using O_EXCL to ensure that temporary + * files are created by us and that we do not open any symlinks. + */ + fd_dst = open(path_dst, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR); + if (fd_dst == -1) { + sudo_warn("%s", path_dst); + goto cleanup; + } + + if (fd_src != -1) { + if (sudo_copy_file(path_src, fd_src, -1, path_dst, fd_dst, -1) == -1) + goto cleanup; + close(fd_src); + } + + /* Make mtime on temp file match src (sb filled in above). */ + mtim_get(&sb, times[0]); + times[1].tv_sec = times[0].tv_sec; + times[1].tv_nsec = times[0].tv_nsec; + if (futimens(fd_dst, times) == -1) { + if (utimensat(AT_FDCWD, path_dst, times, 0) == -1) + sudo_warn("%s", path_dst); + } + close(fd_dst); + fd_dst = -1; + } + debug_return_int(SESH_SUCCESS); + +cleanup: + /* Remove temporary files. */ + for (i = 0; i < argc - 1; i += 2) + unlink(argv[i + 1]); + if (fd_src != -1) + close(fd_src); + if (fd_dst != -1) + close(fd_dst); + debug_return_int(SESH_ERR_NO_FILES); +} + +static int +sesh_edit_copy_tfiles(int edit_flags, struct sudo_cred *user_cred, + struct sudo_cred *run_cred, int argc, char *argv[]) +{ + int i, ret = SESH_SUCCESS; + int fd_src = -1, fd_dst = -1; + debug_decl(sesh_edit_copy_tfiles, SUDO_DEBUG_EDIT); + + for (i = 0; i < argc - 1; i += 2) { + const char *path_src = argv[i]; + char *path_dst = argv[i + 1]; + off_t len_src, len_dst; + struct stat sb; + + /* Open temporary file for reading. */ + if (fd_src != -1) + close(fd_src); + fd_src = open(path_src, O_RDONLY|O_NONBLOCK|O_NOFOLLOW); + if (fd_src == -1) { + sudo_warn("%s", path_src); + ret = SESH_ERR_SOME_FILES; + continue; + } + /* Make sure the temporary file is safe and has the proper owner. */ + if (!sudo_check_temp_file(fd_src, path_src, run_cred->uid, &sb)) { + sudo_warnx(U_("contents of edit session left in %s"), path_src); + ret = SESH_ERR_SOME_FILES; + continue; + } + (void) fcntl(fd_src, F_SETFL, fcntl(fd_src, F_GETFL, 0) & ~O_NONBLOCK); + + /* Create destination file. */ + if (fd_dst != -1) + close(fd_dst); + fd_dst = sudo_edit_open(path_dst, O_WRONLY|O_CREAT, + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, edit_flags, user_cred, run_cred); + if (fd_dst == -1) { + if (errno == ELOOP) { + sudo_warnx(U_("%s: editing symbolic links is not " + "permitted"), path_dst); + } else if (errno == EISDIR) { + sudo_warnx(U_("%s: editing files in a writable directory " + "is not permitted"), path_dst); + } else { + sudo_warn("%s", path_dst); + } + sudo_warnx(U_("contents of edit session left in %s"), path_src); + ret = SESH_ERR_SOME_FILES; + continue; + } + + /* sudo_check_temp_file() filled in sb for us. */ + len_src = sb.st_size; + if (fstat(fd_dst, &sb) != 0) { + sudo_warn("%s", path_dst); + sudo_warnx(U_("contents of edit session left in %s"), path_src); + ret = SESH_ERR_SOME_FILES; + continue; + } + len_dst = sb.st_size; + + if (sudo_copy_file(path_src, fd_src, len_src, path_dst, fd_dst, + len_dst) == -1) { + sudo_warnx(U_("contents of edit session left in %s"), path_src); + ret = SESH_ERR_SOME_FILES; + continue; + } + unlink(path_src); + } + if (fd_src != -1) + close(fd_src); + if (fd_dst != -1) + close(fd_dst); + + debug_return_int(ret); +} + +static int +sesh_sudoedit(int argc, char *argv[]) +{ + int edit_flags, post, ret; + struct sudo_cred user_cred, run_cred; + debug_decl(sesh_sudoedit, SUDO_DEBUG_EDIT); + + memset(&user_cred, 0, sizeof(user_cred)); + memset(&run_cred, 0, sizeof(run_cred)); + edit_flags = CD_SUDOEDIT_FOLLOW; + + /* Check for -h flag (don't follow links). */ + if (argv[2] != NULL && strcmp(argv[2], "-h") == 0) { + argv++; + argc--; + CLR(edit_flags, CD_SUDOEDIT_FOLLOW); // -V753 + } + + /* Check for -w flag (disallow directories writable by the user). */ + if (argv[2] != NULL && strcmp(argv[2], "-w") == 0) { + SET(edit_flags, CD_SUDOEDIT_CHECKDIR); + + /* Parse uid:gid:gid1,gid2,... */ + if (argv[3] == NULL || !parse_user(argv[3], &user_cred)) + debug_return_int(SESH_ERR_FAILURE); + argv += 2; + argc -= 2; + } + + if (argc < 3) + debug_return_int(SESH_ERR_FAILURE); + + /* + * We need to know whether we are performing the copy operation + * before or after the editing. Without this we would not know + * which files are temporary and which are the originals. + * post = 0 ... before + * post = 1 ... after + */ + if (strcmp(argv[2], "0") == 0) + post = 0; + else if (strcmp(argv[2], "1") == 0) + post = 1; + else /* invalid value */ + debug_return_int(SESH_ERR_INVALID); + + /* Align argv & argc to the beginning of the file list. */ + argv += 3; + argc -= 3; + + /* no files specified, nothing to do */ + if (argc == 0) + debug_return_int(SESH_SUCCESS); + /* odd number of paths specified */ + if (argc & 1) + debug_return_int(SESH_ERR_BAD_PATHS); + + /* Masquerade as sudoedit so the user gets consistent error messages. */ + setprogname("sudoedit"); + + /* + * sudoedit runs us with the effective user-ID and group-ID of + * the target user as well as with the target user's group list. + */ + run_cred.uid = run_cred.euid = geteuid(); + run_cred.gid = run_cred.egid = getegid(); + run_cred.ngroups = getgroups(0, NULL); // -V575 + if (run_cred.ngroups > 0) { + run_cred.groups = reallocarray(NULL, run_cred.ngroups, + sizeof(GETGROUPS_T)); + if (run_cred.groups == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + debug_return_int(SESH_ERR_FAILURE); + } + if (getgroups(run_cred.ngroups, run_cred.groups) < 0) { + sudo_warn("%s", U_("unable to get group list")); + debug_return_int(SESH_ERR_FAILURE); + } + } else { + run_cred.ngroups = 0; + run_cred.groups = NULL; + } + + ret = post ? + sesh_edit_copy_tfiles(edit_flags, &user_cred, &run_cred, argc, argv) : + sesh_edit_create_tfiles(edit_flags, &user_cred, &run_cred, argc, argv); + debug_return_int(ret); +} diff --git a/src/signal.c b/src/signal.c new file mode 100644 index 0000000..7f90d70 --- /dev/null +++ b/src/signal.c @@ -0,0 +1,184 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2016 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> +#include <string.h> +#include <errno.h> +#include <signal.h> + +#include "sudo.h" +#include "sudo_exec.h" + +static struct signal_state { + int signo; + int restore; + struct sigaction sa; +} saved_signals[] = { + { SIGALRM }, /* SAVED_SIGALRM */ + { SIGCHLD }, /* SAVED_SIGCHLD */ + { SIGCONT }, /* SAVED_SIGCONT */ + { SIGHUP }, /* SAVED_SIGHUP */ + { SIGINT }, /* SAVED_SIGINT */ + { SIGPIPE }, /* SAVED_SIGPIPE */ + { SIGQUIT }, /* SAVED_SIGQUIT */ + { SIGTERM }, /* SAVED_SIGTERM */ + { SIGTSTP }, /* SAVED_SIGTSTP */ + { SIGTTIN }, /* SAVED_SIGTTIN */ + { SIGTTOU }, /* SAVED_SIGTTOU */ + { SIGUSR1 }, /* SAVED_SIGUSR1 */ + { SIGUSR2 }, /* SAVED_SIGUSR2 */ + { -1 } +}; + +static sig_atomic_t pending_signals[NSIG]; + +static void +sudo_handler(int signo) +{ + /* Mark signal as pending. */ + pending_signals[signo] = 1; +} + +bool +signal_pending(int signo) +{ + return pending_signals[signo] == 1; +} + +/* + * Save signal handler state so it can be restored before exec. + */ +void +save_signals(void) +{ + struct signal_state *ss; + debug_decl(save_signals, SUDO_DEBUG_MAIN); + + for (ss = saved_signals; ss->signo != -1; ss++) { + if (sigaction(ss->signo, NULL, &ss->sa) != 0) + sudo_warn(U_("unable to save handler for signal %d"), ss->signo); + } + + debug_return; +} + +/* + * Restore signal handlers to initial state for exec. + */ +void +restore_signals(void) +{ + struct signal_state *ss; + debug_decl(restore_signals, SUDO_DEBUG_MAIN); + + for (ss = saved_signals; ss->signo != -1; ss++) { + if (ss->restore) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "restoring handler for signal %d: %s", ss->signo, + ss->sa.sa_handler == SIG_IGN ? "SIG_IGN" : + ss->sa.sa_handler == SIG_DFL ? "SIG_DFL" : "???"); + if (sigaction(ss->signo, &ss->sa, NULL) != 0) { + sudo_warn(U_("unable to restore handler for signal %d"), + ss->signo); + } + } + } + + debug_return; +} + +/* + * Trap tty-generated (and other) signals so we can't be killed before + * calling the policy close function. The signal pipe will be drained + * in sudo_execute() before running the command and new handlers will + * be installed in the parent. + */ +void +init_signals(void) +{ + struct sigaction sa; + struct signal_state *ss; + debug_decl(init_signals, SUDO_DEBUG_MAIN); + + memset(&sa, 0, sizeof(sa)); + sigfillset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = sudo_handler; + + for (ss = saved_signals; ss->signo > 0; ss++) { + switch (ss->signo) { + case SIGCONT: + case SIGPIPE: + case SIGTTIN: + case SIGTTOU: + /* Don't install these until exec time. */ + break; + default: + if (ss->sa.sa_handler != SIG_IGN) { + if (sigaction(ss->signo, &sa, NULL) != 0) { + sudo_warn(U_("unable to set handler for signal %d"), + ss->signo); + } + } + break; + } + } + /* Ignore SIGPIPE until exec. */ + if (saved_signals[SAVED_SIGPIPE].sa.sa_handler != SIG_IGN) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "will restore signal %d on exec", SIGPIPE); + saved_signals[SAVED_SIGPIPE].restore = true; + sa.sa_handler = SIG_IGN; + if (sigaction(SIGPIPE, &sa, NULL) != 0) + sudo_warn(U_("unable to set handler for signal %d"), SIGPIPE); + } + + debug_return; +} + +/* + * Like sigaction() but sets restore flag in saved_signals[] + * if needed. + */ +int +sudo_sigaction(int signo, struct sigaction *sa, struct sigaction *osa) +{ + struct signal_state *ss; + int ret; + debug_decl(sudo_sigaction, SUDO_DEBUG_MAIN); + + for (ss = saved_signals; ss->signo > 0; ss++) { + if (ss->signo == signo) { + /* If signal was or now is ignored, restore old handler on exec. */ + if (ss->sa.sa_handler == SIG_IGN || sa->sa_handler == SIG_IGN) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "will restore signal %d on exec", signo); + ss->restore = true; + } + break; + } + } + ret = sigaction(signo, sa, osa); + + debug_return_int(ret); +} diff --git a/src/solaris.c b/src/solaris.c new file mode 100644 index 0000000..754664f --- /dev/null +++ b/src/solaris.c @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2015 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#ifdef HAVE_PROJECT_H +# include <project.h> +# include <sys/task.h> +# include <errno.h> +# include <pwd.h> +#endif + +#include "sudo.h" +#include "sudo_dso.h" + +int +os_init(int argc, char *argv[], char *envp[]) +{ + /* + * Solaris 11 is unable to load the per-locale shared objects + * without this. We must keep the handle open for it to work. + * This bug was fixed in Solaris 11 Update 1. + */ + void *handle = sudo_dso_load("/usr/lib/locale/common/methods_unicode.so.3", + SUDO_DSO_LAZY|SUDO_DSO_GLOBAL); + (void)&handle; + + return os_init_common(argc, argv, envp); +} + +#ifdef HAVE_PROJECT_H +void +set_project(struct passwd *pw) +{ + struct project proj; + char buf[PROJECT_BUFSZ]; + int errval; + debug_decl(set_project, SUDO_DEBUG_UTIL); + + /* + * Collect the default project for the user and settaskid + */ + setprojent(); + if (getdefaultproj(pw->pw_name, &proj, buf, sizeof(buf)) != NULL) { + errval = setproject(proj.pj_name, pw->pw_name, TASK_NORMAL); + switch(errval) { + case 0: + break; + case SETPROJ_ERR_TASK: + switch (errno) { + case EAGAIN: + sudo_warnx("%s", U_("resource control limit has been reached")); + break; + case ESRCH: + sudo_warnx(U_("user \"%s\" is not a member of project \"%s\""), + pw->pw_name, proj.pj_name); + break; + case EACCES: + sudo_warnx("%s", U_("the invoking task is final")); + break; + default: + sudo_warnx(U_("could not join project \"%s\""), proj.pj_name); + break; + } + break; + case SETPROJ_ERR_POOL: + switch (errno) { + case EACCES: + sudo_warnx(U_("no resource pool accepting default bindings " + "exists for project \"%s\""), proj.pj_name); + break; + case ESRCH: + sudo_warnx(U_("specified resource pool does not exist for " + "project \"%s\""), proj.pj_name); + break; + default: + sudo_warnx(U_("could not bind to default resource pool for " + "project \"%s\""), proj.pj_name); + break; + } + break; + default: + if (errval <= 0) { + sudo_warnx(U_("setproject failed for project \"%s\""), proj.pj_name); + } else { + sudo_warnx(U_("warning, resource control assignment failed for " + "project \"%s\""), proj.pj_name); + } + break; + } + } else { + sudo_warn("getdefaultproj"); + } + endprojent(); + debug_return; +} +#endif /* HAVE_PROJECT_H */ diff --git a/src/sudo.c b/src/sudo.c new file mode 100644 index 0000000..4c6c60e --- /dev/null +++ b/src/sudo.c @@ -0,0 +1,2188 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 + */ + +#ifdef __TANDEM +# include <floss.h> +#endif + +#include <config.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <sys/resource.h> +#include <sys/socket.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#include <grp.h> +#include <pwd.h> +#include <time.h> +#ifdef HAVE_SELINUX +# include <selinux/selinux.h> /* for is_selinux_enabled() */ +#endif +#ifdef HAVE_SETAUTHDB +# include <usersec.h> +#endif /* HAVE_SETAUTHDB */ +#if defined(HAVE_GETPRPWNAM) && defined(HAVE_SET_AUTH_PARAMETERS) +# ifdef __hpux +# undef MAXINT +# include <hpsecurity.h> +# else +# include <sys/security.h> +# endif /* __hpux */ +# include <prot.h> +#endif /* HAVE_GETPRPWNAM && HAVE_SET_AUTH_PARAMETERS */ + +#include <sudo_usage.h> +#include "sudo.h" +#include "sudo_plugin.h" +#include "sudo_plugin_int.h" + +/* + * Local variables + */ +struct plugin_container policy_plugin; +struct plugin_container_list io_plugins = TAILQ_HEAD_INITIALIZER(io_plugins); +struct plugin_container_list audit_plugins = TAILQ_HEAD_INITIALIZER(audit_plugins); +struct plugin_container_list approval_plugins = TAILQ_HEAD_INITIALIZER(approval_plugins); +struct user_details user_details; +const char *list_user; /* extern for parse_args.c */ +int sudo_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; +static struct command_details command_details; +static int sudo_mode; +static struct sudo_event_base *sudo_event_base; + +struct sudo_gc_entry { + SLIST_ENTRY(sudo_gc_entry) entries; + enum sudo_gc_types type; + union { + char **vec; + void *ptr; + } u; +}; +SLIST_HEAD(sudo_gc_list, sudo_gc_entry); +#ifdef NO_LEAKS +static struct sudo_gc_list sudo_gc_list = SLIST_HEAD_INITIALIZER(sudo_gc_list); +#endif + +/* + * Local functions + */ +static void fix_fds(void); +static void sudo_check_suid(const char *path); +static char **get_user_info(struct user_details *); +static void command_info_to_details(char * const info[], + struct command_details *details); +static void gc_init(void); + +/* Policy plugin convenience functions. */ +static void policy_open(struct sudo_settings *settings, + char * const user_info[], char * const user_env[]); +static void policy_close(int exit_status, int error); +static int policy_show_version(int verbose); +static void policy_check(int argc, char * const argv[], char *env_add[], + char **command_info[], char **argv_out[], char **user_env_out[]); +static void policy_list(int argc, char * const argv[], + int verbose, const char *user, char * const envp[]); +static void policy_validate(char * const argv[], char * const envp[]); +static void policy_invalidate(int unlinkit); + +/* I/O log plugin convenience functions. */ +static void iolog_open(struct sudo_settings *settings, char * const user_info[], + char * const command_details[], int argc, char * const argv[], + char * const user_env[]); +static void iolog_close(int exit_status, int error); +static void iolog_show_version(int verbose, struct sudo_settings *settings, + char * const user_info[], int argc, char * const argv[], + char * const user_env[]); +static void unlink_plugin(struct plugin_container_list *plugin_list, struct plugin_container *plugin); +static void free_plugin_container(struct plugin_container *plugin, bool ioplugin); + +/* Audit plugin convenience functions. */ +static void audit_open(struct sudo_settings *settings, char * const user_info[], + int submit_optind, char * const submit_argv[], char * const submit_envp[]); +static void audit_close(int exit_status, int error); +static void audit_show_version(int verbose); +static void audit_accept(const char *plugin_name, + unsigned int plugin_type, char * const command_info[], + char * const run_argv[], char * const run_envp[]); + +/* Approval plugin convenience functions. */ +static void approval_check(struct sudo_settings *settings, + char * const user_info[], int submit_optind, char * const submit_argv[], + char * const submit_envp[], char * const command_info[], + char * const run_argv[], char * const run_envp[]); +static void approval_show_version(int verbose, struct sudo_settings *settings, + char * const user_info[], int submit_optind, char * const submit_argv[], + char * const submit_envp[]); + +sudo_dso_public int main(int argc, char *argv[], char *envp[]); + +int +main(int argc, char *argv[], char *envp[]) +{ + int nargc, status = 0; + char **nargv, **env_add, **user_info; + char **command_info = NULL, **argv_out = NULL, **user_env_out = NULL; + const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL }; + struct sudo_settings *settings; + int submit_optind; + sigset_t mask; + debug_decl_vars(main, SUDO_DEBUG_MAIN); + + /* Only allow "sudo" or "sudoedit" as the program name. */ + initprogname2(argc > 0 ? argv[0] : "sudo", allowed_prognames); + + /* Crank resource limits to unlimited. */ + unlimit_sudo(); + + /* Make sure fds 0-2 are open and do OS-specific initialization. */ + fix_fds(); + os_init(argc, argv, envp); + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE_NAME, LOCALEDIR); + textdomain(PACKAGE_NAME); + + (void) tzset(); + + /* Must be done before we do any password lookups */ +#if defined(HAVE_GETPRPWNAM) && defined(HAVE_SET_AUTH_PARAMETERS) + (void) set_auth_parameters(argc, argv); +# ifdef HAVE_INITPRIVS + initprivs(); +# endif +#endif /* HAVE_GETPRPWNAM && HAVE_SET_AUTH_PARAMETERS */ + + /* Initialize the debug subsystem. */ + if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1) + exit(EXIT_FAILURE); + sudo_debug_instance = sudo_debug_register(getprogname(), + NULL, NULL, sudo_conf_debug_files(getprogname())); + if (sudo_debug_instance == SUDO_DEBUG_INSTANCE_ERROR) + exit(EXIT_FAILURE); + + /* Make sure we are setuid root. */ + sudo_check_suid(argc > 0 ? argv[0] : "sudo"); + + /* Save original signal state and setup default signal handlers. */ + save_signals(); + init_signals(); + + /* Reset signal mask to the default value (unblock). */ + (void) sigemptyset(&mask); + (void) sigprocmask(SIG_SETMASK, &mask, NULL); + + /* Parse the rest of sudo.conf. */ + sudo_conf_read(NULL, SUDO_CONF_ALL & ~SUDO_CONF_DEBUG); + + /* Fill in user_info with user name, uid, cwd, etc. */ + if ((user_info = get_user_info(&user_details)) == NULL) + exit(EXIT_FAILURE); /* get_user_info printed error message */ + + /* Disable core dumps if not enabled in sudo.conf. */ + if (sudo_conf_disable_coredump()) + disable_coredump(); + + /* Parse command line arguments. */ + sudo_mode = parse_args(argc, argv, &submit_optind, &nargc, &nargv, + &settings, &env_add); + sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode); + + /* Print sudo version early, in case of plugin init failure. */ + if (ISSET(sudo_mode, MODE_VERSION)) { + printf(_("Sudo version %s\n"), PACKAGE_VERSION); + if (user_details.cred.uid == ROOT_UID) + (void) printf(_("Configure options: %s\n"), CONFIGURE_ARGS); + } + + /* Use conversation function for sudo_(warn|fatal)x? for plugins. */ + sudo_warn_set_conversation(sudo_conversation); + + /* Load plugins. */ + if (!sudo_load_plugins()) + sudo_fatalx("%s", U_("fatal error, unable to load plugins")); + + /* Allocate event base so plugin can use it. */ + if ((sudo_event_base = sudo_ev_base_alloc()) == NULL) + sudo_fatalx("%s", U_("unable to allocate memory")); + + /* Open policy and audit plugins. */ + /* XXX - audit policy_open errors */ + audit_open(settings, user_info, submit_optind, argv, envp); + policy_open(settings, user_info, envp); + + switch (sudo_mode & MODE_MASK) { + case MODE_VERSION: + policy_show_version(!user_details.cred.uid); + iolog_show_version(!user_details.cred.uid, settings, user_info, + nargc, nargv, envp); + approval_show_version(!user_details.cred.uid, settings, user_info, + submit_optind, argv, envp); + audit_show_version(!user_details.cred.uid); + break; + case MODE_VALIDATE: + case MODE_VALIDATE|MODE_INVALIDATE: + policy_validate(nargv, envp); + break; + case MODE_KILL: + case MODE_INVALIDATE: + policy_invalidate(sudo_mode == MODE_KILL); + break; + case MODE_CHECK: + case MODE_CHECK|MODE_INVALIDATE: + case MODE_LIST: + case MODE_LIST|MODE_INVALIDATE: + policy_list(nargc, nargv, ISSET(sudo_mode, MODE_LONG_LIST), + list_user, envp); + break; + case MODE_EDIT: + case MODE_RUN: + policy_check(nargc, nargv, env_add, &command_info, &argv_out, + &user_env_out); + + /* Reset nargv/nargc based on argv_out. */ + /* XXX - leaks old nargv in shell mode */ + for (nargv = argv_out, nargc = 0; nargv[nargc] != NULL; nargc++) + continue; + if (nargc == 0) + sudo_fatalx("%s", + U_("plugin did not return a command to execute")); + + /* Approval plugins run after policy plugin accepts the command. */ + approval_check(settings, user_info, submit_optind, argv, envp, + command_info, nargv, user_env_out); + + /* Open I/O plugin once policy and approval plugins succeed. */ + iolog_open(settings, user_info, command_info, nargc, nargv, + user_env_out); + + /* Audit the accept event on behalf of the sudo front-end. */ + audit_accept("sudo", SUDO_FRONT_END, command_info, + nargv, user_env_out); + + /* Setup command details and run command/edit. */ + command_info_to_details(command_info, &command_details); + command_details.tty = user_details.tty; + command_details.argv = argv_out; + command_details.envp = user_env_out; + command_details.evbase = sudo_event_base; + if (ISSET(sudo_mode, MODE_LOGIN_SHELL)) + SET(command_details.flags, CD_LOGIN_SHELL); + if (ISSET(sudo_mode, MODE_BACKGROUND)) + SET(command_details.flags, CD_BACKGROUND); + if (ISSET(command_details.flags, CD_SUDOEDIT)) { + status = sudo_edit(&command_details); + } else { + status = run_command(&command_details); + } + /* The close method was called by sudo_edit/run_command. */ + break; + default: + sudo_fatalx(U_("unexpected sudo mode 0x%x"), sudo_mode); + } + + /* + * If the command was terminated by a signal, sudo needs to terminated + * the same way. Otherwise, the shell may ignore a keyboard-generated + * signal. However, we want to avoid having sudo dump core itself. + */ + if (WIFSIGNALED(status)) { + struct sigaction sa; + + if (WCOREDUMP(status)) + disable_coredump(); + + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_handler = SIG_DFL; + sigaction(WTERMSIG(status), &sa, NULL); + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, + WTERMSIG(status) | 128); + kill(getpid(), WTERMSIG(status)); + } + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, + WEXITSTATUS(status)); + exit(WEXITSTATUS(status)); +} + +int +os_init_common(int argc, char *argv[], char *envp[]) +{ +#ifdef STATIC_SUDOERS_PLUGIN + preload_static_symbols(); +#endif + gc_init(); + return 0; +} + +/* + * Ensure that stdin, stdout and stderr are open; set to /dev/null if not. + * Some operating systems do this automatically in the kernel or libc. + */ +static void +fix_fds(void) +{ + int miss[3], devnull = -1; + debug_decl(fix_fds, SUDO_DEBUG_UTIL); + + /* + * stdin, stdout and stderr must be open; set them to /dev/null + * if they are closed. + */ + miss[STDIN_FILENO] = fcntl(STDIN_FILENO, F_GETFL, 0) == -1; + miss[STDOUT_FILENO] = fcntl(STDOUT_FILENO, F_GETFL, 0) == -1; + miss[STDERR_FILENO] = fcntl(STDERR_FILENO, F_GETFL, 0) == -1; + if (miss[STDIN_FILENO] || miss[STDOUT_FILENO] || miss[STDERR_FILENO]) { + devnull = open(_PATH_DEVNULL, O_RDWR, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); + if (devnull == -1) + sudo_fatal(U_("unable to open %s"), _PATH_DEVNULL); + if (miss[STDIN_FILENO] && dup2(devnull, STDIN_FILENO) == -1) + sudo_fatal("dup2"); + if (miss[STDOUT_FILENO] && dup2(devnull, STDOUT_FILENO) == -1) + sudo_fatal("dup2"); + if (miss[STDERR_FILENO] && dup2(devnull, STDERR_FILENO) == -1) + sudo_fatal("dup2"); + if (devnull > STDERR_FILENO) + close(devnull); + } + debug_return; +} + +/* + * Allocate space for groups and fill in using sudo_getgrouplist2() + * for when we cannot (or don't want to) use getgroups(). + * Returns 0 on success and -1 on failure. + */ +static int +fill_group_list(const char *user, struct sudo_cred *cred) +{ + int ret = -1; + debug_decl(fill_group_list, SUDO_DEBUG_UTIL); + + /* + * If user specified a max number of groups, use it, otherwise let + * sudo_getgrouplist2() allocate the group vector. + */ + cred->ngroups = sudo_conf_max_groups(); + if (cred->ngroups > 0) { + cred->groups = reallocarray(NULL, cred->ngroups, sizeof(GETGROUPS_T)); + if (cred->groups != NULL) { + /* No error on insufficient space if user specified max_groups. */ + (void)sudo_getgrouplist2(user, cred->gid, + &cred->groups, &cred->ngroups); + ret = 0; + } + } else { + cred->groups = NULL; + ret = sudo_getgrouplist2(user, cred->gid, &cred->groups, + &cred->ngroups); + } + if (ret == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: %s: unable to get groups via sudo_getgrouplist2()", + __func__, user); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %s: got %d groups via sudo_getgrouplist2()", + __func__, user, cred->ngroups); + } + debug_return_int(ret); +} + +static char * +get_user_groups(const char *user, struct sudo_cred *cred) +{ + char *cp, *gid_list = NULL; + size_t glsize; + int i, len, group_source; + debug_decl(get_user_groups, SUDO_DEBUG_UTIL); + + cred->groups = NULL; + group_source = sudo_conf_group_source(); + if (group_source != GROUP_SOURCE_DYNAMIC) { + int maxgroups = (int)sysconf(_SC_NGROUPS_MAX); + if (maxgroups < 0) + maxgroups = NGROUPS_MAX; + + /* Note that macOS may return ngroups > NGROUPS_MAX. */ + cred->ngroups = getgroups(0, NULL); // -V575 + if (cred->ngroups > 0) { + /* Use groups from kernel if not at limit or source is static. */ + if (cred->ngroups != maxgroups || group_source == GROUP_SOURCE_STATIC) { + cred->groups = reallocarray(NULL, cred->ngroups, sizeof(GETGROUPS_T)); + if (cred->groups == NULL) + goto done; + if (getgroups(cred->ngroups, cred->groups) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to get %d groups via getgroups()", + __func__, cred->ngroups); + free(cred->groups); + cred->groups = NULL; + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: got %d groups via getgroups()", + __func__, cred->ngroups); + } + } + } + } + if (cred->groups == NULL) { + /* + * Query group database if kernel list is too small or disabled. + * Typically, this is because NFS can only support up to 16 groups. + */ + if (fill_group_list(user, cred) == -1) + goto done; + } + + /* + * Format group list as a comma-separated string of gids. + */ + glsize = sizeof("groups=") - 1 + (cred->ngroups * (MAX_UID_T_LEN + 1)); + if ((gid_list = malloc(glsize)) == NULL) + goto done; + memcpy(gid_list, "groups=", sizeof("groups=") - 1); + cp = gid_list + sizeof("groups=") - 1; + for (i = 0; i < cred->ngroups; i++) { + len = snprintf(cp, glsize - (cp - gid_list), "%s%u", + i ? "," : "", (unsigned int)cred->groups[i]); + if (len < 0 || (size_t)len >= glsize - (cp - gid_list)) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + cp += len; + } +done: + debug_return_str(gid_list); +} + +/* + * Return user information as an array of name=value pairs. + * and fill in struct user_details (which shares the same strings). + */ +static char ** +get_user_info(struct user_details *ud) +{ + char *cp, **user_info, path[PATH_MAX]; + size_t user_info_max = 32 + RLIM_NLIMITS; + unsigned int i = 0; + mode_t mask; + struct passwd *pw; + int fd, n; + debug_decl(get_user_info, SUDO_DEBUG_UTIL); + + /* + * On BSD systems you can set a hint to keep the password and + * group databases open instead of having to open and close + * them all the time. Since sudo does a lot of password and + * group lookups, keeping the file open can speed things up. + */ +#ifdef HAVE_SETPASSENT + setpassent(1); +#endif /* HAVE_SETPASSENT */ +#ifdef HAVE_SETGROUPENT + setgroupent(1); +#endif /* HAVE_SETGROUPENT */ + + memset(ud, 0, sizeof(*ud)); + + /* XXX - bound check number of entries */ + user_info = reallocarray(NULL, user_info_max, sizeof(char *)); + if (user_info == NULL) + goto oom; + + ud->pid = getpid(); + ud->ppid = getppid(); + ud->pgid = getpgid(0); + fd = open(_PATH_TTY, O_RDWR); + if (fd != -1) { + if ((ud->tcpgid = tcgetpgrp(fd)) == -1) + ud->tcpgid = 0; + close(fd); + } + if ((ud->sid = getsid(0)) == -1) + ud->sid = 0; + + ud->cred.uid = getuid(); + ud->cred.euid = geteuid(); + ud->cred.gid = getgid(); + ud->cred.egid = getegid(); + +#ifdef HAVE_SETAUTHDB + aix_setauthdb(IDtouser(ud->cred.uid), NULL); +#endif + pw = getpwuid(ud->cred.uid); +#ifdef HAVE_SETAUTHDB + aix_restoreauthdb(); +#endif + if (pw == NULL) + sudo_fatalx(U_("you do not exist in the %s database"), "passwd"); + + user_info[i] = sudo_new_key_val("user", pw->pw_name); + if (user_info[i] == NULL) + goto oom; + ud->username = user_info[i] + sizeof("user=") - 1; + + /* Stash user's shell for use with the -s flag; don't pass to plugin. */ + if ((ud->shell = getenv("SHELL")) == NULL || ud->shell[0] == '\0') { + ud->shell = pw->pw_shell[0] ? pw->pw_shell : _PATH_SUDO_BSHELL; + } + if ((ud->shell = strdup(ud->shell)) == NULL) + goto oom; + + if (asprintf(&user_info[++i], "pid=%d", (int)ud->pid) == -1) + goto oom; + if (asprintf(&user_info[++i], "ppid=%d", (int)ud->ppid) == -1) + goto oom; + if (asprintf(&user_info[++i], "pgid=%d", (int)ud->pgid) == -1) + goto oom; + if (asprintf(&user_info[++i], "tcpgid=%d", (int)ud->tcpgid) == -1) + goto oom; + if (asprintf(&user_info[++i], "sid=%d", (int)ud->sid) == -1) + goto oom; + if (asprintf(&user_info[++i], "uid=%u", (unsigned int)ud->cred.uid) == -1) + goto oom; + if (asprintf(&user_info[++i], "euid=%u", (unsigned int)ud->cred.euid) == -1) + goto oom; + if (asprintf(&user_info[++i], "gid=%u", (unsigned int)ud->cred.gid) == -1) + goto oom; + if (asprintf(&user_info[++i], "egid=%u", (unsigned int)ud->cred.egid) == -1) + goto oom; + + if ((cp = get_user_groups(ud->username, &ud->cred)) == NULL) + goto oom; + user_info[++i] = cp; + + mask = umask(0); + umask(mask); + if (asprintf(&user_info[++i], "umask=0%o", (unsigned int)mask) == -1) + goto oom; + + if (getcwd(path, sizeof(path)) != NULL) { + user_info[++i] = sudo_new_key_val("cwd", path); + if (user_info[i] == NULL) + goto oom; + ud->cwd = user_info[i] + sizeof("cwd=") - 1; + } + + if (get_process_ttyname(path, sizeof(path)) != NULL) { + user_info[++i] = sudo_new_key_val("tty", path); + if (user_info[i] == NULL) + goto oom; + ud->tty = user_info[i] + sizeof("tty=") - 1; + } else { + /* tty may not always be present */ + if (errno != ENOENT) + sudo_warn("%s", U_("unable to determine tty")); + } + + cp = sudo_gethostname(); + user_info[++i] = sudo_new_key_val("host", cp ? cp : "localhost"); + free(cp); + if (user_info[i] == NULL) + goto oom; + ud->host = user_info[i] + sizeof("host=") - 1; + + sudo_get_ttysize(&ud->ts_rows, &ud->ts_cols); + if (asprintf(&user_info[++i], "lines=%d", ud->ts_rows) == -1) + goto oom; + if (asprintf(&user_info[++i], "cols=%d", ud->ts_cols) == -1) + goto oom; + + n = serialize_limits(&user_info[i + 1], user_info_max - (i + 1)); + if (n == -1) + goto oom; + i += n; + + user_info[++i] = NULL; + + /* Add to list of vectors to be garbage collected at exit. */ + if (!gc_add(GC_VECTOR, user_info)) + goto bad; + + debug_return_ptr(user_info); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); +bad: + while (i--) + free(user_info[i]); + free(user_info); + debug_return_ptr(NULL); +} + +/* + * Convert a command_info array into a command_details structure. + */ +static void +command_info_to_details(char * const info[], struct command_details *details) +{ + int i; + id_t id; + char *cp; + const char *errstr; + debug_decl(command_info_to_details, SUDO_DEBUG_PCOMM); + + memset(details, 0, sizeof(*details)); + details->info = info; + details->closefrom = -1; + details->execfd = -1; + details->flags = CD_SUDOEDIT_CHECKDIR | CD_SET_GROUPS; + TAILQ_INIT(&details->preserved_fds); + +#define SET_STRING(s, n) \ + if (strncmp(s, info[i], sizeof(s) - 1) == 0 && info[i][sizeof(s) - 1]) { \ + details->n = info[i] + sizeof(s) - 1; \ + break; \ + } +#define SET_FLAG(s, n) \ + if (strncmp(s, info[i], sizeof(s) - 1) == 0) { \ + switch (sudo_strtobool(info[i] + sizeof(s) - 1)) { \ + case true: \ + SET(details->flags, n); \ + break; \ + case false: \ + CLR(details->flags, n); \ + break; \ + default: \ + sudo_debug_printf(SUDO_DEBUG_ERROR, \ + "invalid boolean value for %s", info[i]); \ + break; \ + } \ + break; \ + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "command info from plugin:"); + for (i = 0; info[i] != NULL; i++) { + sudo_debug_printf(SUDO_DEBUG_INFO, " %d: %s", i, info[i]); + switch (info[i][0]) { + case 'c': + SET_STRING("chroot=", chroot) + SET_STRING("command=", command) + SET_STRING("cwd=", cwd) + if (strncmp("cwd_optional=", info[i], sizeof("cwd_optional=") - 1) == 0) { + cp = info[i] + sizeof("cwd_optional=") - 1; + details->cwd_optional = sudo_strtobool(cp); + if (details->cwd_optional == -1) { + errno = EINVAL; + sudo_fatal("%s", info[i]); + } + break; + } + if (strncmp("closefrom=", info[i], sizeof("closefrom=") - 1) == 0) { + cp = info[i] + sizeof("closefrom=") - 1; + details->closefrom = sudo_strtonum(cp, 0, INT_MAX, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + break; + } + break; + case 'e': + SET_FLAG("exec_background=", CD_EXEC_BG) + if (strncmp("execfd=", info[i], sizeof("execfd=") - 1) == 0) { + cp = info[i] + sizeof("execfd=") - 1; + details->execfd = sudo_strtonum(cp, 0, INT_MAX, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); +#ifdef HAVE_FEXECVE + /* Must keep fd open during exec. */ + add_preserved_fd(&details->preserved_fds, details->execfd); +#else + /* Plugin thinks we support fexecve() but we don't. */ + (void)fcntl(details->execfd, F_SETFD, FD_CLOEXEC); + details->execfd = -1; +#endif + break; + } + break; + case 'l': + SET_STRING("login_class=", login_class) + break; + case 'n': + if (strncmp("nice=", info[i], sizeof("nice=") - 1) == 0) { + cp = info[i] + sizeof("nice=") - 1; + details->priority = sudo_strtonum(cp, INT_MIN, INT_MAX, + &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + SET(details->flags, CD_SET_PRIORITY); + break; + } + SET_FLAG("noexec=", CD_NOEXEC) + break; + case 'p': + SET_FLAG("preserve_groups=", CD_PRESERVE_GROUPS) + if (strncmp("preserve_fds=", info[i], sizeof("preserve_fds=") - 1) == 0) { + parse_preserved_fds(&details->preserved_fds, + info[i] + sizeof("preserve_fds=") - 1); + break; + } + break; + case 'r': + if (strncmp("runas_egid=", info[i], sizeof("runas_egid=") - 1) == 0) { + cp = info[i] + sizeof("runas_egid=") - 1; + id = sudo_strtoid(cp, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + details->cred.egid = (gid_t)id; + SET(details->flags, CD_SET_EGID); + break; + } + if (strncmp("runas_euid=", info[i], sizeof("runas_euid=") - 1) == 0) { + cp = info[i] + sizeof("runas_euid=") - 1; + id = sudo_strtoid(cp, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + details->cred.euid = (uid_t)id; + SET(details->flags, CD_SET_EUID); + break; + } + if (strncmp("runas_gid=", info[i], sizeof("runas_gid=") - 1) == 0) { + cp = info[i] + sizeof("runas_gid=") - 1; + id = sudo_strtoid(cp, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + details->cred.gid = (gid_t)id; + SET(details->flags, CD_SET_GID); + break; + } + if (strncmp("runas_groups=", info[i], sizeof("runas_groups=") - 1) == 0) { + cp = info[i] + sizeof("runas_groups=") - 1; + details->cred.ngroups = sudo_parse_gids(cp, NULL, &details->cred.groups); + /* sudo_parse_gids() will print a warning on error. */ + if (details->cred.ngroups == -1) + exit(EXIT_FAILURE); /* XXX */ + break; + } + if (strncmp("runas_uid=", info[i], sizeof("runas_uid=") - 1) == 0) { + cp = info[i] + sizeof("runas_uid=") - 1; + id = sudo_strtoid(cp, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + details->cred.uid = (uid_t)id; + SET(details->flags, CD_SET_UID); + break; + } +#ifdef HAVE_PRIV_SET + if (strncmp("runas_privs=", info[i], sizeof("runas_privs=") - 1) == 0) { + const char *endp; + cp = info[i] + sizeof("runas_privs=") - 1; + if (*cp != '\0') { + details->privs = priv_str_to_set(cp, ",", &endp); + if (details->privs == NULL) + sudo_warn("invalid runas_privs %s", endp); + } + break; + } + if (strncmp("runas_limitprivs=", info[i], sizeof("runas_limitprivs=") - 1) == 0) { + const char *endp; + cp = info[i] + sizeof("runas_limitprivs=") - 1; + if (*cp != '\0') { + details->limitprivs = priv_str_to_set(cp, ",", &endp); + if (details->limitprivs == NULL) + sudo_warn("invalid runas_limitprivs %s", endp); + } + break; + } +#endif /* HAVE_PRIV_SET */ + SET_STRING("runas_user=", runas_user) + break; + case 's': + SET_STRING("selinux_role=", selinux_role) + SET_STRING("selinux_type=", selinux_type) + SET_FLAG("set_utmp=", CD_SET_UTMP) + SET_FLAG("sudoedit=", CD_SUDOEDIT) + SET_FLAG("sudoedit_checkdir=", CD_SUDOEDIT_CHECKDIR) + SET_FLAG("sudoedit_follow=", CD_SUDOEDIT_FOLLOW) + break; + case 't': + if (strncmp("timeout=", info[i], sizeof("timeout=") - 1) == 0) { + cp = info[i] + sizeof("timeout=") - 1; + details->timeout = sudo_strtonum(cp, 0, INT_MAX, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + SET(details->flags, CD_SET_TIMEOUT); + break; + } + break; + case 'u': + if (strncmp("umask=", info[i], sizeof("umask=") - 1) == 0) { + cp = info[i] + sizeof("umask=") - 1; + details->umask = sudo_strtomode(cp, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + SET(details->flags, CD_SET_UMASK); + break; + } + SET_FLAG("umask_override=", CD_OVERRIDE_UMASK) + SET_FLAG("use_pty=", CD_USE_PTY) + SET_STRING("utmp_user=", utmp_user) + break; + } + } + + if (!ISSET(details->flags, CD_SET_EUID)) + details->cred.euid = details->cred.uid; + if (!ISSET(details->flags, CD_SET_EGID)) + details->cred.egid = details->cred.gid; + if (!ISSET(details->flags, CD_SET_UMASK)) + CLR(details->flags, CD_OVERRIDE_UMASK); + +#ifdef HAVE_SETAUTHDB + aix_setauthdb(IDtouser(details->cred.euid), NULL); +#endif + if (details->runas_user != NULL) + details->pw = getpwnam(details->runas_user); + if (details->pw == NULL) + details->pw = getpwuid(details->cred.euid); +#ifdef HAVE_SETAUTHDB + aix_restoreauthdb(); +#endif + if (details->pw != NULL && (details->pw = pw_dup(details->pw)) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + +#ifdef HAVE_SELINUX + if (details->selinux_role != NULL && is_selinux_enabled() > 0) + SET(details->flags, CD_RBAC_ENABLED); +#endif + debug_return; +} + +static void +sudo_check_suid(const char *sudo) +{ + char pathbuf[PATH_MAX]; + struct stat sb; + bool qualified; + debug_decl(sudo_check_suid, SUDO_DEBUG_PCOMM); + + if (geteuid() != ROOT_UID) { + /* Search for sudo binary in PATH if not fully qualified. */ + qualified = strchr(sudo, '/') != NULL; + if (!qualified) { + char *path = getenv_unhooked("PATH"); + if (path != NULL) { + const char *cp, *ep; + const char *pathend = path + strlen(path); + + for (cp = sudo_strsplit(path, pathend, ":", &ep); cp != NULL; + cp = sudo_strsplit(NULL, pathend, ":", &ep)) { + + int len = snprintf(pathbuf, sizeof(pathbuf), "%.*s/%s", + (int)(ep - cp), cp, sudo); + if (len < 0 || len >= ssizeof(pathbuf)) + continue; + if (access(pathbuf, X_OK) == 0) { + sudo = pathbuf; + qualified = true; + break; + } + } + } + } + + if (qualified && stat(sudo, &sb) == 0) { + /* Try to determine why sudo was not running as root. */ + if (sb.st_uid != ROOT_UID || !ISSET(sb.st_mode, S_ISUID)) { + sudo_fatalx( + U_("%s must be owned by uid %d and have the setuid bit set"), + sudo, ROOT_UID); + } else { + sudo_fatalx(U_("effective uid is not %d, is %s on a file system " + "with the 'nosuid' option set or an NFS file system without" + " root privileges?"), ROOT_UID, sudo); + } + } else { + sudo_fatalx( + U_("effective uid is not %d, is sudo installed setuid root?"), + ROOT_UID); + } + } + debug_return; +} + +bool +set_user_groups(struct command_details *details) +{ + bool ret = false; + debug_decl(set_user_groups, SUDO_DEBUG_EXEC); + + if (!ISSET(details->flags, CD_PRESERVE_GROUPS)) { + if (details->cred.ngroups >= 0) { + if (sudo_setgroups(details->cred.ngroups, details->cred.groups) < 0) { + sudo_warn("%s", U_("unable to set supplementary group IDs")); + goto done; + } + } + } +#ifdef HAVE_SETEUID + if (ISSET(details->flags, CD_SET_EGID) && setegid(details->cred.egid)) { + sudo_warn(U_("unable to set effective gid to runas gid %u"), + (unsigned int)details->cred.egid); + goto done; + } +#endif + if (ISSET(details->flags, CD_SET_GID) && setgid(details->cred.gid)) { + sudo_warn(U_("unable to set gid to runas gid %u"), + (unsigned int)details->cred.gid); + goto done; + } + ret = true; + +done: + CLR(details->flags, CD_SET_GROUPS); + debug_return_bool(ret); +} + +/* + * Run the command and wait for it to complete. + * Returns wait status suitable for use with the wait(2) macros. + */ +int +run_command(struct command_details *details) +{ + struct command_status cstat; + int status = W_EXITCODE(1, 0); + debug_decl(run_command, SUDO_DEBUG_EXEC); + + cstat.type = CMD_INVALID; + cstat.val = 0; + + sudo_execute(details, &cstat); + + switch (cstat.type) { + case CMD_ERRNO: + /* exec_setup() or execve() returned an error. */ + iolog_close(0, cstat.val); + policy_close(0, cstat.val); + audit_close(SUDO_PLUGIN_EXEC_ERROR, cstat.val); + break; + case CMD_WSTATUS: + /* Command ran, exited or was killed. */ + status = cstat.val; + iolog_close(status, 0); + policy_close(status, 0); + audit_close(SUDO_PLUGIN_WAIT_STATUS, cstat.val); + break; + default: + /* TODO: handle front end error conditions. */ + sudo_warnx(U_("unexpected child termination condition: %d"), cstat.type); + break; + } + debug_return_int(status); +} + +/* + * Format struct sudo_settings as name=value pairs for the plugin + * to consume. Returns a NULL-terminated plugin-style array of pairs. + */ +static char ** +format_plugin_settings(struct plugin_container *plugin, + struct sudo_settings *sudo_settings) +{ + size_t plugin_settings_size; + struct sudo_debug_file *debug_file; + struct sudo_settings *setting; + char **plugin_settings; + unsigned int i = 0; + debug_decl(format_plugin_settings, SUDO_DEBUG_PCOMM); + + /* Determine sudo_settings array size (including plugin_path and NULL) */ + plugin_settings_size = 2; + for (setting = sudo_settings; setting->name != NULL; setting++) + plugin_settings_size++; + if (plugin->debug_files != NULL) { + TAILQ_FOREACH(debug_file, plugin->debug_files, entries) + plugin_settings_size++; + } + + /* Allocate and fill in. */ + plugin_settings = reallocarray(NULL, plugin_settings_size, sizeof(char *)); + if (plugin_settings == NULL) + goto bad; + plugin_settings[i] = sudo_new_key_val("plugin_path", plugin->path); + if (plugin_settings[i] == NULL) + goto bad; + for (setting = sudo_settings; setting->name != NULL; setting++) { + if (setting->value != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, "settings: %s=%s", + setting->name, setting->value); + plugin_settings[++i] = + sudo_new_key_val(setting->name, setting->value); + if (plugin_settings[i] == NULL) + goto bad; + } + } + if (plugin->debug_files != NULL) { + TAILQ_FOREACH(debug_file, plugin->debug_files, entries) { + /* XXX - quote filename? */ + if (asprintf(&plugin_settings[++i], "debug_flags=%s %s", + debug_file->debug_file, debug_file->debug_flags) == -1) + goto bad; + } + } + plugin_settings[++i] = NULL; + + /* Add to list of vectors to be garbage collected at exit. */ + if (!gc_add(GC_VECTOR, plugin_settings)) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + debug_return_ptr(plugin_settings); +bad: + while (i--) + free(plugin_settings[i]); + free(plugin_settings); + debug_return_ptr(NULL); +} + +static void +policy_open(struct sudo_settings *settings, char * const user_info[], + char * const user_env[]) +{ + char **plugin_settings; + const char *errstr = NULL; + int ok; + debug_decl(policy_open, SUDO_DEBUG_PCOMM); + + /* Convert struct sudo_settings to plugin_settings[] */ + plugin_settings = format_plugin_settings(&policy_plugin, settings); + if (plugin_settings == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* + * Backward compatibility for older API versions + */ + sudo_debug_set_active_instance(SUDO_DEBUG_INSTANCE_INITIALIZER); + switch (policy_plugin.u.generic->version) { + case SUDO_API_MKVERSION(1, 0): + case SUDO_API_MKVERSION(1, 1): + ok = policy_plugin.u.policy_1_0->open(policy_plugin.u.io_1_0->version, + sudo_conversation_1_7, sudo_conversation_printf, plugin_settings, + user_info, user_env); + break; + default: + ok = policy_plugin.u.policy->open(SUDO_API_VERSION, sudo_conversation, + sudo_conversation_printf, plugin_settings, user_info, user_env, + policy_plugin.options, &errstr); + } + + /* Stash plugin debug instance ID if set in open() function. */ + policy_plugin.debug_instance = sudo_debug_get_active_instance(); + sudo_debug_set_active_instance(sudo_debug_instance); + + if (ok != 1) { + if (ok == -2) + usage(); + else { + /* XXX - audit */ + sudo_fatalx("%s", U_("unable to initialize policy plugin")); + } + } + + debug_return; +} + +static void +policy_close(int exit_status, int error_code) +{ + debug_decl(policy_close, SUDO_DEBUG_PCOMM); + + if (error_code != 0) { + sudo_debug_printf(SUDO_DEBUG_DEBUG, + "%s: calling policy close with errno %d", + policy_plugin.name, error_code); + } else { + sudo_debug_printf(SUDO_DEBUG_DEBUG, + "%s: calling policy close with wait status %d", + policy_plugin.name, exit_status); + } + if (policy_plugin.u.policy->close != NULL) { + sudo_debug_set_active_instance(policy_plugin.debug_instance); + policy_plugin.u.policy->close(exit_status, error_code); + sudo_debug_set_active_instance(sudo_debug_instance); + } else if (error_code != 0) { + if (command_details.command != NULL) { + errno = error_code; + sudo_warn(U_("unable to execute %s"), command_details.command); + } + } + + debug_return; +} + +static int +policy_show_version(int verbose) +{ + int ret = true; + debug_decl(policy_show_version, SUDO_DEBUG_PCOMM); + + sudo_debug_set_active_instance(policy_plugin.debug_instance); + if (policy_plugin.u.policy->show_version != NULL) + ret = policy_plugin.u.policy->show_version(verbose); + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) { + if (policy_plugin.u.policy->close != NULL) + policy_plugin.u.policy->close(0, 0); + } + sudo_debug_set_active_instance(sudo_debug_instance); + + debug_return_int(ret); +} + +static void +policy_check(int argc, char * const argv[], + char *env_add[], char **command_info[], char **argv_out[], + char **user_env_out[]) +{ + const char *errstr = NULL; + int ok; + debug_decl(policy_check, SUDO_DEBUG_PCOMM); + + if (policy_plugin.u.policy->check_policy == NULL) { + sudo_fatalx(U_("policy plugin %s is missing the \"check_policy\" method"), + policy_plugin.name); + } + sudo_debug_set_active_instance(policy_plugin.debug_instance); + ok = policy_plugin.u.policy->check_policy(argc, argv, env_add, + command_info, argv_out, user_env_out, &errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + sudo_debug_printf(SUDO_DEBUG_INFO, "policy plugin returns %d (%s)", + ok, errstr ? errstr : ""); + + /* On success, the close method will be called by sudo_edit/run_command. */ + if (ok != 1) { + switch (ok) { + case 0: + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("command rejected by policy"), + *command_info); + break; + case -1: + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("policy plugin error"), + *command_info); + break; + case -2: + usage(); + break; + } + + /* Policy must be closed after auditing to avoid use after free. */ + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) + policy_close(0, 0); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + exit(EXIT_FAILURE); /* policy plugin printed error message */ + } + audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, *command_info, + *argv_out, *user_env_out); + + debug_return; +} + +static void +policy_list(int argc, char * const argv[], int verbose, + const char *user, char * const envp[]) +{ + const char *errstr = NULL; + /* TODO: add list_user */ + char * const command_info[] = { + "command=list", + NULL + }; + int ok; + debug_decl(policy_list, SUDO_DEBUG_PCOMM); + + if (policy_plugin.u.policy->list == NULL) { + sudo_fatalx(U_("policy plugin %s does not support listing privileges"), + policy_plugin.name); + } + sudo_debug_set_active_instance(policy_plugin.debug_instance); + ok = policy_plugin.u.policy->list(argc, argv, verbose, user, &errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + + switch (ok) { + case 1: + audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, + command_info, argv, envp); + break; + case 0: + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("command rejected by policy"), + command_info); + break; + default: + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("policy plugin error"), + command_info); + break; + } + + /* Policy must be closed after auditing to avoid use after free. */ + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) + policy_close(0, 0); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + + exit(ok != 1); +} + +static void +policy_validate(char * const argv[], char * const envp[]) +{ + const char *errstr = NULL; + char * const command_info[] = { + "command=validate", + NULL + }; + int ok = 0; + debug_decl(policy_validate, SUDO_DEBUG_PCOMM); + + if (policy_plugin.u.policy->validate == NULL) { + sudo_fatalx(U_("policy plugin %s does not support the -v option"), + policy_plugin.name); + } + sudo_debug_set_active_instance(policy_plugin.debug_instance); + ok = policy_plugin.u.policy->validate(&errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + + switch (ok) { + case 1: + audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, command_info, + argv, envp); + break; + case 0: + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("command rejected by policy"), + command_info); + break; + default: + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("policy plugin error"), + command_info); + break; + } + + /* Policy must be closed after auditing to avoid use after free. */ + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) + policy_close(0, 0); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + + exit(ok != 1); +} + +static void +policy_invalidate(int unlinkit) +{ + debug_decl(policy_invalidate, SUDO_DEBUG_PCOMM); + + if (policy_plugin.u.policy->invalidate == NULL) { + sudo_fatalx(U_("policy plugin %s does not support the -k/-K options"), + policy_plugin.name); + } + sudo_debug_set_active_instance(policy_plugin.debug_instance); + policy_plugin.u.policy->invalidate(unlinkit); + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) { + if (policy_plugin.u.policy->close != NULL) + policy_plugin.u.policy->close(0, 0); + } + sudo_debug_set_active_instance(sudo_debug_instance); + + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + + exit(EXIT_SUCCESS); +} + +int +policy_init_session(struct command_details *details) +{ + const char *errstr = NULL; + int ret = true; + debug_decl(policy_init_session, SUDO_DEBUG_PCOMM); + + /* + * We set groups, including supplementary group vector, + * as part of the session setup. This allows for dynamic + * groups to be set via pam_group(8) in pam_setcred(3). + */ + if (ISSET(details->flags, CD_SET_GROUPS)) { + /* set_user_groups() prints error message on failure. */ + if (!set_user_groups(details)) + goto done; + } + + /* Session setup may override sudoers umask so set it first. */ + if (ISSET(details->flags, CD_SET_UMASK)) + (void) umask(details->umask); + + if (policy_plugin.u.policy->init_session) { + /* + * Backward compatibility for older API versions + */ + sudo_debug_set_active_instance(policy_plugin.debug_instance); + switch (policy_plugin.u.generic->version) { + case SUDO_API_MKVERSION(1, 0): + case SUDO_API_MKVERSION(1, 1): + ret = policy_plugin.u.policy_1_0->init_session(details->pw); + break; + default: + ret = policy_plugin.u.policy->init_session(details->pw, + &details->envp, &errstr); + } + sudo_debug_set_active_instance(sudo_debug_instance); + if (ret != 1) { + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("policy plugin error"), + details->info); + } + } +done: + debug_return_int(ret); +} + +static int +iolog_open_int(struct plugin_container *plugin, struct sudo_settings *settings, + char * const user_info[], char * const command_info[], + int argc, char * const argv[], char * const user_env[], const char **errstr) +{ + char **plugin_settings; + int ret; + debug_decl(iolog_open_int, SUDO_DEBUG_PCOMM); + + /* Convert struct sudo_settings to plugin_settings[] */ + plugin_settings = format_plugin_settings(plugin, settings); + if (plugin_settings == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_int(-1); + } + + /* + * Backward compatibility for older API versions + */ + sudo_debug_set_active_instance(plugin->debug_instance); + switch (plugin->u.generic->version) { + case SUDO_API_MKVERSION(1, 0): + ret = plugin->u.io_1_0->open(plugin->u.io_1_0->version, + sudo_conversation_1_7, sudo_conversation_printf, plugin_settings, + user_info, argc, argv, user_env); + break; + case SUDO_API_MKVERSION(1, 1): + ret = plugin->u.io_1_1->open(plugin->u.io_1_1->version, + sudo_conversation_1_7, sudo_conversation_printf, plugin_settings, + user_info, command_info, argc, argv, user_env); + break; + default: + ret = plugin->u.io->open(SUDO_API_VERSION, sudo_conversation, + sudo_conversation_printf, plugin_settings, user_info, command_info, + argc, argv, user_env, plugin->options, errstr); + } + + /* Stash plugin debug instance ID if set in open() function. */ + plugin->debug_instance = sudo_debug_get_active_instance(); + sudo_debug_set_active_instance(sudo_debug_instance); + + debug_return_int(ret); +} + +static void +iolog_open(struct sudo_settings *settings, char * const user_info[], + char * const command_info[], int argc, char * const argv[], + char * const user_env[]) +{ + struct plugin_container *plugin, *next; + const char *errstr = NULL; + debug_decl(iolog_open, SUDO_DEBUG_PCOMM); + + /* XXX - iolog_open should audit errors */ + TAILQ_FOREACH_SAFE(plugin, &io_plugins, entries, next) { + int ok = iolog_open_int(plugin, settings, user_info, + command_info, argc, argv, user_env, &errstr); + switch (ok) { + case 1: + break; + case 0: + /* I/O plugin asked to be disabled, remove and free. */ + /* XXX - audit */ + unlink_plugin(&io_plugins, plugin); + break; + case -2: + usage(); + break; + default: + /* XXX - audit error */ + sudo_fatalx(U_("error initializing I/O plugin %s"), + plugin->name); + } + } + + debug_return; +} + +static void +iolog_close(int exit_status, int error_code) +{ + struct plugin_container *plugin; + debug_decl(iolog_close, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->close != NULL) { + if (error_code != 0) { + sudo_debug_printf(SUDO_DEBUG_DEBUG, + "%s: calling I/O close with errno %d", + plugin->name, error_code); + } else { + sudo_debug_printf(SUDO_DEBUG_DEBUG, + "%s: calling I/O close with wait status %d", + plugin->name, exit_status); + } + sudo_debug_set_active_instance(plugin->debug_instance); + plugin->u.io->close(exit_status, error_code); + sudo_debug_set_active_instance(sudo_debug_instance); + } + } + + debug_return; +} + +static void +iolog_show_version(int verbose, struct sudo_settings *settings, + char * const user_info[], int argc, char * const argv[], + char * const user_env[]) +{ + const char *errstr = NULL; + struct plugin_container *plugin; + debug_decl(iolog_show_version, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH(plugin, &io_plugins, entries) { + int ok = iolog_open_int(plugin, settings, user_info, NULL, + argc, argv, user_env, &errstr); + if (ok != -1) { + sudo_debug_set_active_instance(plugin->debug_instance); + if (plugin->u.io->show_version != NULL) { + /* Return value of show_version currently ignored. */ + plugin->u.io->show_version(verbose); + } + if (plugin->u.io->version >= SUDO_API_MKVERSION(1, 15)) { + if (plugin->u.io->close != NULL) + plugin->u.io->close(0, 0); + } + sudo_debug_set_active_instance(sudo_debug_instance); + } + } + + debug_return; +} + +/* + * Remove the specified plugin from the plugins list. + * Deregisters any hooks before unlinking, then frees the container. + */ +static void +unlink_plugin(struct plugin_container_list *plugin_list, + struct plugin_container *plugin) +{ + void (*deregister_hooks)(int , int (*)(struct sudo_hook *)) = NULL; + debug_decl(unlink_plugin, SUDO_DEBUG_PCOMM); + + /* Deregister hooks, if any. */ + if (plugin->u.generic->version >= SUDO_API_MKVERSION(1, 2)) { + switch (plugin->u.generic->type) { + case SUDO_IO_PLUGIN: + deregister_hooks = plugin->u.io->deregister_hooks; + break; + case SUDO_AUDIT_PLUGIN: + deregister_hooks = plugin->u.audit->deregister_hooks; + break; + default: + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: unsupported plugin type %d", __func__, + plugin->u.generic->type); + break; + } + } + if (deregister_hooks != NULL) { + sudo_debug_set_active_instance(plugin->debug_instance); + deregister_hooks(SUDO_HOOK_VERSION, deregister_hook); + sudo_debug_set_active_instance(sudo_debug_instance); + } + + /* Remove from plugin list and free. */ + TAILQ_REMOVE(plugin_list, plugin, entries); + free_plugin_container(plugin, true); + + debug_return; +} + +static int +audit_open_int(struct plugin_container *plugin, struct sudo_settings *settings, + char * const user_info[], int submit_optind, char * const submit_argv[], + char * const submit_envp[], const char **errstr) +{ + char **plugin_settings; + int ret; + debug_decl(audit_open_int, SUDO_DEBUG_PCOMM); + + /* Convert struct sudo_settings to plugin_settings[] */ + plugin_settings = format_plugin_settings(plugin, settings); + if (plugin_settings == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_int(-1); + } + + sudo_debug_set_active_instance(plugin->debug_instance); + ret = plugin->u.audit->open(SUDO_API_VERSION, sudo_conversation, + sudo_conversation_printf, plugin_settings, user_info, + submit_optind, submit_argv, submit_envp, plugin->options, errstr); + + /* Stash plugin debug instance ID if set in open() function. */ + plugin->debug_instance = sudo_debug_get_active_instance(); + sudo_debug_set_active_instance(sudo_debug_instance); + + debug_return_int(ret); +} + +static void +audit_open(struct sudo_settings *settings, char * const user_info[], + int submit_optind, char * const submit_argv[], char * const submit_envp[]) +{ + struct plugin_container *plugin, *next; + const char *errstr = NULL; + debug_decl(audit_open, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH_SAFE(plugin, &audit_plugins, entries, next) { + int ok = audit_open_int(plugin, settings, user_info, + submit_optind, submit_argv, submit_envp, &errstr); + switch (ok) { + case 1: + break; + case 0: + /* Audit plugin asked to be disabled, remove and free. */ + unlink_plugin(&audit_plugins, plugin); + break; + case -2: + usage(); + break; + default: + /* TODO: pass error message to other audit plugins */ + sudo_fatalx(U_("error initializing audit plugin %s"), + plugin->name); + } + } + + debug_return; +} + +static void +audit_close(int status_type, int status) +{ + struct plugin_container *plugin; + debug_decl(audit_close, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH(plugin, &audit_plugins, entries) { + if (plugin->u.audit->close != NULL) { + sudo_debug_set_active_instance(plugin->debug_instance); + plugin->u.audit->close(status_type, status); + sudo_debug_set_active_instance(sudo_debug_instance); + } + } + + debug_return; +} + +static void +audit_show_version(int verbose) +{ + struct plugin_container *plugin; + debug_decl(audit_show_version, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH(plugin, &audit_plugins, entries) { + sudo_debug_set_active_instance(plugin->debug_instance); + if (plugin->u.audit->show_version != NULL) { + /* Return value of show_version currently ignored. */ + plugin->u.audit->show_version(verbose); + } + if (plugin->u.audit->close != NULL) + plugin->u.audit->close(SUDO_PLUGIN_NO_STATUS, 0); + sudo_debug_set_active_instance(sudo_debug_instance); + } + + debug_return; +} + +/* + * Error from plugin or front-end. + * The error will not be sent to plugin source, if specified. + */ +static void +audit_error2(struct plugin_container *source, const char *plugin_name, + unsigned int plugin_type, const char *audit_msg, char * const command_info[]) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + int ok; + debug_decl(audit_error2, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH(plugin, &audit_plugins, entries) { + if (plugin->u.audit->error == NULL) + continue; + + /* Avoid a loop if the audit plugin itself has an error. */ + if (plugin == source) + continue; + + sudo_debug_set_active_instance(plugin->debug_instance); + ok = plugin->u.audit->error(plugin_name, plugin_type, + audit_msg, command_info, &errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + if (ok != 1) { + /* + * Don't propagate the error to other audit plugins. + * It is not worth the trouble to avoid potential loops. + */ + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: plugin %s error failed, ret %d", __func__, + plugin->name, ok); + sudo_warnx(U_("%s: unable to log error event%s%s"), + plugin->name, errstr ? ": " : "", errstr ? errstr : ""); + } + } + + debug_return; +} + +/* + * Command accepted by policy. + * See command_info[] for additional info. + * XXX - actual environment may be updated by policy_init_session(). + */ +static void +audit_accept(const char *plugin_name, unsigned int plugin_type, + char * const command_info[], char * const run_argv[], + char * const run_envp[]) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + int ok; + debug_decl(audit_accept, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH(plugin, &audit_plugins, entries) { + if (plugin->u.audit->accept == NULL) + continue; + + sudo_debug_set_active_instance(plugin->debug_instance); + ok = plugin->u.audit->accept(plugin_name, plugin_type, + command_info, run_argv, run_envp, &errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + if (ok != 1) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: plugin %s accept failed, ret %d", __func__, + plugin->name, ok); + sudo_warnx(U_("%s: unable to log accept event%s%s"), + plugin->name, errstr ? ": " : "", errstr ? errstr : ""); + + /* Notify other audit plugins and exit. */ + audit_error2(plugin, plugin->name, SUDO_AUDIT_PLUGIN, + errstr ? errstr : _("audit plugin error"), command_info); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + exit(EXIT_FAILURE); + } + } + + debug_return; +} + +/* + * Command rejected by policy or I/O plugin. + */ +void +audit_reject(const char *plugin_name, unsigned int plugin_type, + const char *audit_msg, char * const command_info[]) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + int ok; + debug_decl(audit_reject, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH(plugin, &audit_plugins, entries) { + if (plugin->u.audit->reject == NULL) + continue; + + sudo_debug_set_active_instance(plugin->debug_instance); + ok = plugin->u.audit->reject(plugin_name, plugin_type, + audit_msg, command_info, &errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + if (ok != 1) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: plugin %s reject failed, ret %d", __func__, + plugin->name, ok); + sudo_warnx(U_("%s: unable to log reject event%s%s"), + plugin->name, errstr ? ": " : "", errstr ? errstr : ""); + + /* Notify other audit plugins. */ + audit_error2(plugin, plugin->name, SUDO_AUDIT_PLUGIN, + errstr ? errstr : _("audit plugin error"), command_info); + } + } + + debug_return; +} + +/* + * Error from plugin or front-end. + */ +void +audit_error(const char *plugin_name, unsigned int plugin_type, + const char *audit_msg, char * const command_info[]) +{ + audit_error2(NULL, plugin_name, plugin_type, audit_msg, command_info); +} + +static int +approval_open_int(struct plugin_container *plugin, + struct sudo_settings *settings, char * const user_info[], + int submit_optind, char * const submit_argv[], char * const submit_envp[]) +{ + char **plugin_settings; + const char *errstr = NULL; + int ret; + debug_decl(approval_open_int, SUDO_DEBUG_PCOMM); + + /* Convert struct sudo_settings to plugin_settings[] */ + plugin_settings = format_plugin_settings(plugin, settings); + if (plugin_settings == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + sudo_debug_set_active_instance(SUDO_DEBUG_INSTANCE_INITIALIZER); + ret = plugin->u.approval->open(SUDO_API_VERSION, sudo_conversation, + sudo_conversation_printf, plugin_settings, user_info, submit_optind, + submit_argv, submit_envp, plugin->options, &errstr); + + /* Stash plugin debug instance ID if set in open() function. */ + plugin->debug_instance = sudo_debug_get_active_instance(); + sudo_debug_set_active_instance(sudo_debug_instance); + + switch (ret) { + case 1: + break; + case 0: + /* approval plugin asked to be disabled, remove and free. */ + unlink_plugin(&approval_plugins, plugin); + break; + case -2: + usage(); + break; + default: + /* XXX - audit */ + sudo_fatalx(U_("error initializing approval plugin %s"), + plugin->name); + } + + debug_return_int(ret); +} + +static void +approval_show_version(int verbose, struct sudo_settings *settings, + char * const user_info[], int submit_optind, char * const submit_argv[], + char * const submit_envp[]) +{ + struct plugin_container *plugin, *next; + int ok; + debug_decl(approval_show_version, SUDO_DEBUG_PCOMM); + + /* + * Approval plugin us only open for the life of the show_version() call. + */ + TAILQ_FOREACH_SAFE(plugin, &approval_plugins, entries, next) { + if (plugin->u.approval->show_version == NULL) + continue; + + ok = approval_open_int(plugin, settings, user_info, submit_optind, + submit_argv, submit_envp); + if (ok == 1) { + /* Return value of show_version currently ignored. */ + sudo_debug_set_active_instance(plugin->debug_instance); + plugin->u.approval->show_version(verbose); + if (plugin->u.approval->close != NULL) + plugin->u.approval->close(); + sudo_debug_set_active_instance(sudo_debug_instance); + } + } + + debug_return; +} + +/* + * Run approval checks (there may be more than one). + * This is a "one-shot" plugin that has no open/close and is only + * called if the policy plugin accepts the command first. + */ +static void +approval_check(struct sudo_settings *settings, char * const user_info[], + int submit_optind, char * const submit_argv[], char * const submit_envp[], + char * const command_info[], char * const run_argv[], + char * const run_envp[]) +{ + struct plugin_container *plugin, *next; + const char *errstr = NULL; + int ok; + debug_decl(approval_check, SUDO_DEBUG_PCOMM); + + /* + * Approval plugin us only open for the life of the check() call. + */ + TAILQ_FOREACH_SAFE(plugin, &approval_plugins, entries, next) { + if (plugin->u.approval->check == NULL) + continue; + + ok = approval_open_int(plugin, settings, user_info, submit_optind, + submit_argv, submit_envp); + if (ok != 1) + continue; + + sudo_debug_set_active_instance(plugin->debug_instance); + ok = plugin->u.approval->check(command_info, run_argv, run_envp, + &errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + sudo_debug_printf(SUDO_DEBUG_INFO, "approval plugin %s returns %d (%s)", + plugin->name, ok, errstr ? errstr : ""); + + switch (ok) { + case 0: + audit_reject(plugin->name, SUDO_APPROVAL_PLUGIN, + errstr ? errstr : _("command rejected by approver"), + command_info); + break; + case 1: + audit_accept(plugin->name, SUDO_APPROVAL_PLUGIN, command_info, + run_argv, run_envp); + break; + case -1: + audit_error(plugin->name, SUDO_APPROVAL_PLUGIN, + errstr ? errstr : _("approval plugin error"), + command_info); + break; + case -2: + usage(); + break; + } + + /* Close approval plugin now that errstr has been consumed. */ + if (plugin->u.approval->close != NULL) { + sudo_debug_set_active_instance(plugin->debug_instance); + plugin->u.approval->close(); + sudo_debug_set_active_instance(sudo_debug_instance); + } + + /* On error, close policy and audit plugins then exit. */ + if (ok != 1) { + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) + policy_close(0, EPERM); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + exit(EXIT_FAILURE); /* approval plugin printed error message */ + } + } + + debug_return; +} + +static void +plugin_event_callback(int fd, int what, void *v) +{ + struct sudo_plugin_event_int *ev_int = v; + int old_instance; + debug_decl(plugin_event_callback, SUDO_DEBUG_PCOMM); + + /* Run the real callback using the plugin's debug instance. */ + old_instance = sudo_debug_set_active_instance(ev_int->debug_instance); + ev_int->callback(fd, what, ev_int->closure); + sudo_debug_set_active_instance(old_instance); + + debug_return; +} + +/* + * Fill in a previously allocated struct sudo_plugin_event. + */ +static int +plugin_event_set(struct sudo_plugin_event *pev, int fd, int events, + sudo_ev_callback_t callback, void *closure) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_set, SUDO_DEBUG_PCOMM); + + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + if (sudo_ev_set(&ev_int->private, fd, events, plugin_event_callback, ev_int) == -1) + debug_return_int(-1); + + /* Stash active instance so we can restore it when callback runs. */ + ev_int->debug_instance = sudo_debug_get_active_instance(); + + /* Actual user-specified callback and closure. */ + ev_int->callback = callback; + ev_int->closure = closure; + + /* Plugin can only operate on the main event loop. */ + ev_int->private.base = sudo_event_base; + + debug_return_int(1); +} + +/* + * Add a struct sudo_plugin_event to the main event loop. + */ +static int +plugin_event_add(struct sudo_plugin_event *pev, struct timespec *timo) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_add, SUDO_DEBUG_PCOMM); + + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + if (sudo_ev_add(NULL, &ev_int->private, timo, 0) == -1) + debug_return_int(-1); + debug_return_int(1); +} + +/* + * Delete a struct sudo_plugin_event from the main event loop. + */ +static int +plugin_event_del(struct sudo_plugin_event *pev) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_del, SUDO_DEBUG_PCOMM); + + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + if (sudo_ev_del(NULL, &ev_int->private) == -1) + debug_return_int(-1); + debug_return_int(1); +} + +/* + * Get the amount of time remaining in a timeout event. + */ +static int +plugin_event_pending(struct sudo_plugin_event *pev, int events, + struct timespec *ts) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_pending, SUDO_DEBUG_PCOMM); + + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + debug_return_int(sudo_ev_pending(&ev_int->private, events, ts)); +} + +/* + * Get the file descriptor associated with an event. + */ +static int +plugin_event_fd(struct sudo_plugin_event *pev) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_fd, SUDO_DEBUG_PCOMM); + + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + debug_return_int(sudo_ev_get_fd(&ev_int->private)); +} + +/* + * Break out of the event loop, killing the command if it is running. + */ +static void +plugin_event_loopbreak(struct sudo_plugin_event *pev) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_loopbreak, SUDO_DEBUG_PCOMM); + + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + sudo_ev_loopbreak(ev_int->private.base); + debug_return; +} + +/* + * Reset the event base of a struct sudo_plugin_event. + * The event is removed from the old base (if any) first. + * A NULL base can be used to set the default sudo event base. + */ +static void +plugin_event_setbase(struct sudo_plugin_event *pev, void *base) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_setbase, SUDO_DEBUG_PCOMM); + + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + if (ev_int->private.base != NULL) + sudo_ev_del(ev_int->private.base, &ev_int->private); + ev_int->private.base = base ? base : sudo_event_base; + debug_return; +} + +/* + * Free a struct sudo_plugin_event allocated by plugin_event_alloc(). + */ +static void +plugin_event_free(struct sudo_plugin_event *pev) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_free, SUDO_DEBUG_PCOMM); + + /* The private field is first so sudo_ev_free() can free the struct. */ + ev_int = __containerof(pev, struct sudo_plugin_event_int, public); + sudo_ev_free(&ev_int->private); + + debug_return; +} + +/* + * Allocate a struct sudo_plugin_event and fill in the public fields. + */ +struct sudo_plugin_event * +sudo_plugin_event_alloc(void) +{ + struct sudo_plugin_event_int *ev_int; + debug_decl(plugin_event_alloc, SUDO_DEBUG_PCOMM); + + if ((ev_int = malloc(sizeof(*ev_int))) == NULL) + debug_return_ptr(NULL); + + /* Init public fields. */ + ev_int->public.set = plugin_event_set; + ev_int->public.add = plugin_event_add; + ev_int->public.del = plugin_event_del; + ev_int->public.fd = plugin_event_fd; + ev_int->public.pending = plugin_event_pending; + ev_int->public.setbase = plugin_event_setbase; + ev_int->public.loopbreak = plugin_event_loopbreak; + ev_int->public.free = plugin_event_free; + + /* Debug instance to use with the callback. */ + ev_int->debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; + + /* Clear private portion in case caller tries to use us uninitialized. */ + memset(&ev_int->private, 0, sizeof(ev_int->private)); + + debug_return_ptr(&ev_int->public); +} + +static void +free_plugin_container(struct plugin_container *plugin, bool ioplugin) +{ + debug_decl(free_plugin_container, SUDO_DEBUG_PLUGIN); + + free(plugin->path); + free(plugin->name); + if (plugin->options != NULL) { + int i = 0; + while (plugin->options[i] != NULL) + free(plugin->options[i++]); + free(plugin->options); + } + if (ioplugin) + free(plugin); + + debug_return; +} + +bool +gc_add(enum sudo_gc_types type, void *v) +{ +#ifdef NO_LEAKS + struct sudo_gc_entry *gc; + debug_decl(gc_add, SUDO_DEBUG_MAIN); + + if (v == NULL) + debug_return_bool(false); + + gc = calloc(1, sizeof(*gc)); + if (gc == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + switch (type) { + case GC_PTR: + gc->u.ptr = v; + break; + case GC_VECTOR: + gc->u.vec = v; + break; + default: + free(gc); + sudo_warnx("unexpected garbage type %d", type); + debug_return_bool(false); + } + gc->type = type; + SLIST_INSERT_HEAD(&sudo_gc_list, gc, entries); + debug_return_bool(true); +#else + return true; +#endif /* NO_LEAKS */ +} + +#ifdef NO_LEAKS +static void +gc_run(void) +{ + struct plugin_container *plugin; + struct sudo_gc_entry *gc; + char **cur; + debug_decl(gc_run, SUDO_DEBUG_MAIN); + + /* Collect garbage. */ + while ((gc = SLIST_FIRST(&sudo_gc_list))) { + SLIST_REMOVE_HEAD(&sudo_gc_list, entries); + switch (gc->type) { + case GC_PTR: + free(gc->u.ptr); + free(gc); + break; + case GC_VECTOR: + for (cur = gc->u.vec; *cur != NULL; cur++) + free(*cur); + free(gc->u.vec); + free(gc); + break; + default: + sudo_warnx("unexpected garbage type %d", gc->type); + } + } + + /* Free plugin structs. */ + free_plugin_container(&policy_plugin, false); + while ((plugin = TAILQ_FIRST(&io_plugins))) { + TAILQ_REMOVE(&io_plugins, plugin, entries); + free_plugin_container(plugin, true); + } + + debug_return; +} +#endif /* NO_LEAKS */ + +static void +gc_init(void) +{ +#ifdef NO_LEAKS + atexit(gc_run); +#endif +} diff --git a/src/sudo.h b/src/sudo.h new file mode 100644 index 0000000..7f48ece --- /dev/null +++ b/src/sudo.h @@ -0,0 +1,299 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1993-1996, 1998-2005, 2007-2021 + * Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +#ifndef SUDO_SUDO_H +#define SUDO_SUDO_H + +#include <limits.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#ifdef HAVE_PRIV_SET +# include <priv.h> +#endif + +#include "pathnames.h" +#include "sudo_compat.h" +#include "sudo_conf.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_queue.h" +#include "sudo_util.h" + +/* Enable asserts() to avoid static analyzer false positives. */ +#if !(defined(SUDO_DEVEL) || defined(__clang_analyzer__) || defined(__COVERITY__)) +# define NDEBUG +#endif + +/* + * Various modes sudo can be in (based on arguments) in hex + */ +#define MODE_RUN 0x00000001 +#define MODE_EDIT 0x00000002 +#define MODE_VALIDATE 0x00000004 +#define MODE_INVALIDATE 0x00000008 +#define MODE_KILL 0x00000010 +#define MODE_VERSION 0x00000020 +#define MODE_HELP 0x00000040 +#define MODE_LIST 0x00000080 +#define MODE_CHECK 0x00000100 +#define MODE_MASK 0x0000ffff + +/* Mode flags */ +/* XXX - prune this */ +#define MODE_BACKGROUND 0x00010000 +#define MODE_SHELL 0x00020000 +#define MODE_LOGIN_SHELL 0x00040000 +#define MODE_IMPLIED_SHELL 0x00080000 +#define MODE_RESET_HOME 0x00100000 +#define MODE_PRESERVE_GROUPS 0x00200000 +#define MODE_PRESERVE_ENV 0x00400000 +#define MODE_NONINTERACTIVE 0x00800000 +#define MODE_LONG_LIST 0x01000000 + +/* + * Flags for tgetpass() + */ +#define TGP_NOECHO 0x00 /* turn echo off reading pw (default) */ +#define TGP_ECHO 0x01 /* leave echo on when reading passwd */ +#define TGP_STDIN 0x02 /* read from stdin, not /dev/tty */ +#define TGP_ASKPASS 0x04 /* read from askpass helper program */ +#define TGP_MASK 0x08 /* mask user input when reading */ +#define TGP_NOECHO_TRY 0x10 /* turn off echo if possible */ +#define TGP_BELL 0x20 /* bell on password prompt */ + +/* name/value pairs for command line settings. */ +struct sudo_settings { + const char *name; + const char *value; +}; + +/* Sudo user credentials */ +struct sudo_cred { + uid_t uid; + uid_t euid; + uid_t gid; + uid_t egid; + int ngroups; + GETGROUPS_T *groups; +}; + +struct user_details { + struct sudo_cred cred; + pid_t pid; + pid_t ppid; + pid_t pgid; + pid_t tcpgid; + pid_t sid; + const char *username; + const char *cwd; + const char *tty; + const char *host; + const char *shell; + int ts_rows; + int ts_cols; +}; + +#define CD_SET_UID 0x000001 +#define CD_SET_EUID 0x000002 +#define CD_SET_GID 0x000004 +#define CD_SET_EGID 0x000008 +#define CD_PRESERVE_GROUPS 0x000010 +#define CD_NOEXEC 0x000020 +#define CD_SET_PRIORITY 0x000040 +#define CD_SET_UMASK 0x000080 +#define CD_SET_TIMEOUT 0x000100 +#define CD_SUDOEDIT 0x000200 +#define CD_BACKGROUND 0x000400 +#define CD_RBAC_ENABLED 0x000800 +#define CD_USE_PTY 0x001000 +#define CD_SET_UTMP 0x002000 +#define CD_EXEC_BG 0x004000 +#define CD_SUDOEDIT_FOLLOW 0x008000 +#define CD_SUDOEDIT_CHECKDIR 0x010000 +#define CD_SET_GROUPS 0x020000 +#define CD_LOGIN_SHELL 0x040000 +#define CD_OVERRIDE_UMASK 0x080000 + +struct preserved_fd { + TAILQ_ENTRY(preserved_fd) entries; + int lowfd; + int highfd; + int flags; +}; +TAILQ_HEAD(preserved_fd_list, preserved_fd); + +struct command_details { + struct sudo_cred cred; + mode_t umask; + int priority; + int timeout; + int closefrom; + int flags; + int execfd; + int cwd_optional; + struct preserved_fd_list preserved_fds; + struct passwd *pw; + const char *command; + const char *runas_user; + const char *cwd; + const char *login_class; + const char *chroot; + const char *selinux_role; + const char *selinux_type; + const char *utmp_user; + const char *tty; + char **argv; + char **envp; + struct sudo_event_base *evbase; +#ifdef HAVE_PRIV_SET + priv_set_t *privs; + priv_set_t *limitprivs; +#endif + char * const *info; +}; + +/* Status passed between parent and child via socketpair */ +struct command_status { +#define CMD_INVALID 0 +#define CMD_ERRNO 1 +#define CMD_WSTATUS 2 +#define CMD_SIGNO 3 +#define CMD_PID 4 +#define CMD_TTYWINCH 5 + int type; + int val; +}; + +/* Garbage collector data types. */ +enum sudo_gc_types { + GC_UNKNOWN, + GC_VECTOR, + GC_PTR +}; + +/* For fatal() and fatalx() (XXX - needed?) */ +void cleanup(int); + +/* tgetpass.c */ +char *tgetpass(const char *prompt, int timeout, int flags, + struct sudo_conv_callback *callback); + +/* exec.c */ +int sudo_execute(struct command_details *details, struct command_status *cstat); + +/* parse_args.c */ +int parse_args(int argc, char **argv, int *old_optind, int *nargc, + char ***nargv, struct sudo_settings **settingsp, char ***env_addp); +extern int tgetpass_flags; + +/* get_pty.c */ +bool get_pty(int *leader, int *follower, char *name, size_t namesz, uid_t uid); + +/* sudo.c */ +int policy_init_session(struct command_details *details); +int run_command(struct command_details *details); +int os_init_common(int argc, char *argv[], char *envp[]); +bool gc_add(enum sudo_gc_types type, void *v); +bool set_user_groups(struct command_details *details); +struct sudo_plugin_event *sudo_plugin_event_alloc(void); +void audit_reject(const char *plugin_name, unsigned int plugin_type, + const char *audit_msg, char * const command_info[]); +void audit_error(const char *plugin_name, unsigned int plugin_type, + const char *audit_msg, char * const command_info[]); +extern const char *list_user; +extern struct user_details user_details; +extern int sudo_debug_instance; + +/* sudo_edit.c */ +int sudo_edit(struct command_details *details); + +/* parse_args.c */ +void usage(void) __attribute__((__noreturn__)); + +/* openbsd.c */ +int os_init_openbsd(int argc, char *argv[], char *envp[]); + +/* selinux.c */ +int selinux_restore_tty(void); +int selinux_setup(const char *role, const char *type, const char *ttyn, + int ttyfd, bool label_tty); +int selinux_setcon(void); +void selinux_execve(int fd, const char *path, char *const argv[], + char *envp[], bool noexec); + +/* solaris.c */ +void set_project(struct passwd *); +int os_init_solaris(int argc, char *argv[], char *envp[]); + +/* hooks.c */ +/* XXX - move to sudo_plugin_int.h? */ +struct sudo_hook; +int register_hook(struct sudo_hook *hook); +int deregister_hook(struct sudo_hook *hook); +int process_hooks_getenv(const char *name, char **val); +int process_hooks_setenv(const char *name, const char *value, int overwrite); +int process_hooks_putenv(char *string); +int process_hooks_unsetenv(const char *name); + +/* env_hooks.c */ +char *getenv_unhooked(const char *name); + +/* interfaces.c */ +int get_net_ifs(char **addrinfo); + +/* ttyname.c */ +char *get_process_ttyname(char *name, size_t namelen); + +/* signal.c */ +struct sigaction; +int sudo_sigaction(int signo, struct sigaction *sa, struct sigaction *osa); +void init_signals(void); +void restore_signals(void); +void save_signals(void); +bool signal_pending(int signo); + +/* preload.c */ +void preload_static_symbols(void); + +/* preserve_fds.c */ +int add_preserved_fd(struct preserved_fd_list *pfds, int fd); +void closefrom_except(int startfd, struct preserved_fd_list *pfds); +void parse_preserved_fds(struct preserved_fd_list *pfds, const char *fdstr); + +/* setpgrp_nobg.c */ +int tcsetpgrp_nobg(int fd, pid_t pgrp_id); + +/* limits.c */ +void disable_coredump(); +void restore_limits(void); +void restore_nproc(void); +void unlimit_nproc(void); +void unlimit_sudo(void); +int serialize_limits(char **info, size_t info_max); + +#endif /* SUDO_SUDO_H */ diff --git a/src/sudo_edit.c b/src/sudo_edit.c new file mode 100644 index 0000000..b3eea98 --- /dev/null +++ b/src/sudo_edit.c @@ -0,0 +1,780 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2004-2008, 2010-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <signal.h> +#include <fcntl.h> + +#include "sudo.h" +#include "sudo_edit.h" +#include "sudo_exec.h" + +#if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID) + +/* + * Editor temporary file name along with original name, mtime and size. + */ +struct tempfile { + char *tfile; + char *ofile; + off_t osize; + struct timespec omtim; +}; + +static char edit_tmpdir[MAX(sizeof(_PATH_VARTMP), sizeof(_PATH_TMP))]; + +/* + * Find our temporary directory, one of /var/tmp, /usr/tmp, or /tmp + * Returns true on success, else false; + */ +static bool +set_tmpdir(struct sudo_cred *user_cred) +{ + const char *tdir = NULL; + const char *tmpdirs[] = { + _PATH_VARTMP, +#ifdef _PATH_USRTMP + _PATH_USRTMP, +#endif + _PATH_TMP + }; + struct sudo_cred saved_cred; + unsigned int i; + size_t len; + int dfd; + debug_decl(set_tmpdir, SUDO_DEBUG_EDIT); + + /* Stash old credentials. */ + saved_cred.uid = getuid(); + saved_cred.euid = geteuid(); + saved_cred.gid = getgid(); + saved_cred.egid = getegid(); + saved_cred.ngroups = getgroups(0, NULL); // -V575 + if (saved_cred.ngroups > 0) { + saved_cred.groups = + reallocarray(NULL, saved_cred.ngroups, sizeof(GETGROUPS_T)); + if (saved_cred.groups == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + if (getgroups(saved_cred.ngroups, saved_cred.groups) < 0) { + sudo_warn("%s", U_("unable to get group list")); + free(saved_cred.groups); + debug_return_bool(false); + } + } else { + saved_cred.ngroups = 0; + saved_cred.groups = NULL; + } + + for (i = 0; tdir == NULL && i < nitems(tmpdirs); i++) { + if ((dfd = open(tmpdirs[i], O_RDONLY)) != -1) { + if (dir_is_writable(dfd, user_cred, &saved_cred) == true) + tdir = tmpdirs[i]; + close(dfd); + } + } + free(saved_cred.groups); + + if (tdir == NULL) { + sudo_warnx("%s", U_("no writable temporary directory found")); + debug_return_bool(false); + } + + len = strlcpy(edit_tmpdir, tdir, sizeof(edit_tmpdir)); + if (len >= sizeof(edit_tmpdir)) { + errno = ENAMETOOLONG; + sudo_warn("%s", tdir); + debug_return_bool(false); + } + while (len > 0 && edit_tmpdir[--len] == '/') + edit_tmpdir[len] = '\0'; + debug_return_bool(true); +} + +/* + * Construct a temporary file name for file and return an + * open file descriptor. The temporary file name is stored + * in tfile which the caller is responsible for freeing. + */ +static int +sudo_edit_mktemp(const char *ofile, char **tfile) +{ + const char *cp, *suff; + int len, tfd; + debug_decl(sudo_edit_mktemp, SUDO_DEBUG_EDIT); + + if ((cp = strrchr(ofile, '/')) != NULL) + cp++; + else + cp = ofile; + suff = strrchr(cp, '.'); + if (suff != NULL) { + len = asprintf(tfile, "%s/%.*sXXXXXXXX%s", edit_tmpdir, + (int)(size_t)(suff - cp), cp, suff); + } else { + len = asprintf(tfile, "%s/%s.XXXXXXXX", edit_tmpdir, cp); + } + if (len == -1) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_int(-1); + } + tfd = mkstemps(*tfile, suff ? strlen(suff) : 0); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "%s -> %s, fd %d", ofile, *tfile, tfd); + debug_return_int(tfd); +} + +/* + * Create temporary copies of files[] and store the temporary path name + * along with the original name, size and mtime in tf. + * Returns the number of files copied (which may be less than nfiles) + * or -1 if a fatal error occurred. + */ +static int +sudo_edit_create_tfiles(struct command_details *command_details, + struct tempfile *tf, char *files[], int nfiles) +{ + int i, j, tfd, ofd, rc; + struct timespec times[2]; + struct stat sb; + debug_decl(sudo_edit_create_tfiles, SUDO_DEBUG_EDIT); + + /* + * For each file specified by the user, make a temporary version + * and copy the contents of the original to it. + */ + for (i = 0, j = 0; i < nfiles; i++) { + rc = -1; + switch_user(command_details->cred.euid, command_details->cred.egid, + command_details->cred.ngroups, command_details->cred.groups); + ofd = sudo_edit_open(files[i], O_RDONLY, + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, command_details->flags, + &user_details.cred, &command_details->cred); + if (ofd != -1 || errno == ENOENT) { + if (ofd != -1) { + rc = fstat(ofd, &sb); + } else { + /* New file, verify parent dir exists and is not writable. */ + memset(&sb, 0, sizeof(sb)); + if (sudo_edit_parent_valid(files[i], command_details->flags, &user_details.cred, &command_details->cred)) + rc = 0; + } + } + switch_user(ROOT_UID, user_details.cred.egid, + user_details.cred.ngroups, user_details.cred.groups); + if (ofd != -1 && !S_ISREG(sb.st_mode)) { + sudo_warnx(U_("%s: not a regular file"), files[i]); + close(ofd); + continue; + } + if (rc == -1) { + /* open() or fstat() error. */ + if (ofd == -1 && errno == ELOOP) { + sudo_warnx(U_("%s: editing symbolic links is not permitted"), + files[i]); + } else if (ofd == -1 && errno == EISDIR) { + sudo_warnx(U_("%s: editing files in a writable directory is not permitted"), + files[i]); + } else { + sudo_warn("%s", files[i]); + } + if (ofd != -1) + close(ofd); + continue; + } + tf[j].ofile = files[i]; + tf[j].osize = sb.st_size; // -V614 + mtim_get(&sb, tf[j].omtim); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "seteuid(%u)", (unsigned int)user_details.cred.uid); + if (seteuid(user_details.cred.uid) != 0) + sudo_fatal("seteuid(%u)", (unsigned int)user_details.cred.uid); + tfd = sudo_edit_mktemp(tf[j].ofile, &tf[j].tfile); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "seteuid(%u)", ROOT_UID); + if (seteuid(ROOT_UID) != 0) + sudo_fatal("seteuid(ROOT_UID)"); + if (tfd == -1) { + sudo_warn("mkstemps"); + if (ofd != -1) + close(ofd); + debug_return_int(-1); + } + if (ofd != -1) { + if (sudo_copy_file(tf[j].ofile, ofd, tf[j].osize, tf[j].tfile, tfd, -1) == -1) { + close(ofd); + close(tfd); + debug_return_int(-1); + } + close(ofd); + } + /* + * We always update the stashed mtime because the time + * resolution of the filesystem the temporary file is on may + * not match that of the filesystem where the file to be edited + * resides. It is OK if futimens() fails since we only use the + * info to determine whether or not a file has been modified. + */ + times[0].tv_sec = times[1].tv_sec = tf[j].omtim.tv_sec; + times[0].tv_nsec = times[1].tv_nsec = tf[j].omtim.tv_nsec; + if (futimens(tfd, times) == -1) { + if (utimensat(AT_FDCWD, tf[j].tfile, times, 0) == -1) + sudo_warn("%s", tf[j].tfile); + } + rc = fstat(tfd, &sb); + if (!rc) + mtim_get(&sb, tf[j].omtim); + close(tfd); + j++; + } + debug_return_int(j); +} + +/* + * Copy the temporary files specified in tf to the originals. + * Returns the number of copy errors or 0 if completely successful. + */ +static int +sudo_edit_copy_tfiles(struct command_details *command_details, + struct tempfile *tf, int nfiles, struct timespec *times) +{ + int i, tfd, ofd, errors = 0; + struct timespec ts; + struct stat sb; + mode_t oldmask; + debug_decl(sudo_edit_copy_tfiles, SUDO_DEBUG_EDIT); + + /* Copy contents of temp files to real ones. */ + for (i = 0; i < nfiles; i++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "seteuid(%u)", (unsigned int)user_details.cred.uid); + if (seteuid(user_details.cred.uid) != 0) + sudo_fatal("seteuid(%u)", (unsigned int)user_details.cred.uid); + tfd = sudo_edit_open(tf[i].tfile, O_RDONLY, + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0, &user_details.cred, NULL); + if (seteuid(ROOT_UID) != 0) + sudo_fatal("seteuid(ROOT_UID)"); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "seteuid(%u)", ROOT_UID); + if (tfd == -1 || !sudo_check_temp_file(tfd, tf[i].tfile, user_details.cred.uid, &sb)) { + sudo_warnx(U_("%s left unmodified"), tf[i].ofile); + if (tfd != -1) + close(tfd); + errors++; + continue; + } + mtim_get(&sb, ts); + if (tf[i].osize == sb.st_size && sudo_timespeccmp(&tf[i].omtim, &ts, ==)) { + /* + * If mtime and size match but the user spent no measurable + * time in the editor we can't tell if the file was changed. + */ + if (sudo_timespeccmp(×[0], ×[1], !=)) { + sudo_warnx(U_("%s unchanged"), tf[i].ofile); + unlink(tf[i].tfile); + close(tfd); + continue; + } + } + switch_user(command_details->cred.euid, command_details->cred.egid, + command_details->cred.ngroups, command_details->cred.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->flags, + &user_details.cred, &command_details->cred); + umask(oldmask); + switch_user(ROOT_UID, user_details.cred.egid, + user_details.cred.ngroups, user_details.cred.groups); + if (ofd == -1) { + sudo_warn(U_("unable to write to %s"), tf[i].ofile); + goto bad; + } + + /* Overwrite the old file with the new contents. */ + if (sudo_copy_file(tf[i].tfile, tfd, sb.st_size, tf[i].ofile, ofd, + tf[i].osize) == 0) { + /* success, remove temporary file. */ + unlink(tf[i].tfile); + } else { +bad: + sudo_warnx(U_("contents of edit session left in %s"), tf[i].tfile); + errors++; + } + + if (ofd != -1) + close(ofd); + close(tfd); + } + debug_return_int(errors); +} + +#ifdef HAVE_SELINUX +static int +selinux_run_helper(uid_t uid, gid_t gid, int ngroups, GETGROUPS_T *groups, + char *const argv[], char *const envp[]) +{ + int status, ret = SESH_ERR_FAILURE; + const char *sesh; + pid_t child, pid; + debug_decl(selinux_run_helper, SUDO_DEBUG_EDIT); + + sesh = sudo_conf_sesh_path(); + if (sesh == NULL) { + sudo_warnx("internal error: sesh path not set"); + debug_return_int(-1); + } + + child = sudo_debug_fork(); + switch (child) { + case -1: + sudo_warn("%s", U_("unable to fork")); + break; + case 0: + /* child runs sesh in new context */ + if (selinux_setcon() == 0) { + switch_user(uid, gid, ngroups, groups); + execve(sesh, argv, envp); + } + _exit(SESH_ERR_FAILURE); + default: + /* parent waits */ + do { + pid = waitpid(child, &status, 0); + } while (pid == -1 && errno == EINTR); + + ret = WIFSIGNALED(status) ? SESH_ERR_KILLED : WEXITSTATUS(status); + } + + debug_return_int(ret); +} + +static char * +selinux_fmt_sudo_user(void) +{ + char *cp, *user_str; + size_t user_size; + int i, len; + debug_decl(selinux_fmt_sudo_user, SUDO_DEBUG_EDIT); + + user_size = (MAX_UID_T_LEN + 1) * (2 + user_details.cred.ngroups); + if ((user_str = malloc(user_size)) == NULL) + debug_return_ptr(NULL); + + /* UID:GID: */ + len = snprintf(user_str, user_size, "%u:%u:", + (unsigned int)user_details.cred.uid, (unsigned int)user_details.cred.gid); + if (len < 0 || (size_t)len >= user_size) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + + /* Supplementary GIDs */ + cp = user_str + len; + for (i = 0; i < user_details.cred.ngroups; i++) { + len = snprintf(cp, user_size - (cp - user_str), "%s%u", + i ? "," : "", (unsigned int)user_details.cred.groups[i]); + if (len < 0 || (size_t)len >= user_size - (cp - user_str)) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + cp += len; + } + + debug_return_ptr(user_str); +} + +static int +selinux_edit_create_tfiles(struct command_details *command_details, + struct tempfile *tf, char *files[], int nfiles) +{ + char **sesh_args, **sesh_ap, *user_str = NULL; + int i, error, sesh_nargs, ret = -1; + struct stat sb; + debug_decl(selinux_edit_create_tfiles, SUDO_DEBUG_EDIT); + + if (nfiles < 1) + debug_return_int(0); + + /* Construct common args for sesh */ + sesh_nargs = 6 + (nfiles * 2) + 1; + sesh_args = sesh_ap = reallocarray(NULL, sesh_nargs, sizeof(char *)); + if (sesh_args == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + *sesh_ap++ = "sesh"; + *sesh_ap++ = "-e"; + if (!ISSET(command_details->flags, CD_SUDOEDIT_FOLLOW)) + *sesh_ap++ = "-h"; + if (ISSET(command_details->flags, CD_SUDOEDIT_CHECKDIR)) { + if ((user_str = selinux_fmt_sudo_user()) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + *sesh_ap++ = "-w"; + *sesh_ap++ = user_str; + } + *sesh_ap++ = "0"; + + for (i = 0; i < nfiles; i++) { + char *tfile, *ofile = files[i]; + int tfd; + *sesh_ap++ = ofile; + tf[i].ofile = ofile; + if (stat(ofile, &sb) == -1) + memset(&sb, 0, sizeof(sb)); /* new file */ + tf[i].osize = sb.st_size; + mtim_get(&sb, tf[i].omtim); + /* + * The temp file must be created by the sesh helper, + * which uses O_EXCL | O_NOFOLLOW to make this safe. + */ + tfd = sudo_edit_mktemp(ofile, &tfile); + if (tfd == -1) { + sudo_warn("mkstemps"); + free(tfile); + goto done; + } + /* Helper will re-create temp file with proper security context. */ + close(tfd); + unlink(tfile); + *sesh_ap++ = tfile; + tf[i].tfile = tfile; + } + *sesh_ap = NULL; + + /* Run sesh -e [-h] 0 <o1> <t1> ... <on> <tn> */ + error = selinux_run_helper(command_details->cred.uid, command_details->cred.gid, + command_details->cred.ngroups, command_details->cred.groups, sesh_args, + command_details->envp); + switch (error) { + case SESH_SUCCESS: + break; + case SESH_ERR_BAD_PATHS: + sudo_fatalx("%s", U_("sesh: internal error: odd number of paths")); + case SESH_ERR_NO_FILES: + sudo_fatalx("%s", U_("sesh: unable to create temporary files")); + case SESH_ERR_KILLED: + sudo_fatalx("%s", U_("sesh: killed by a signal")); + default: + sudo_warnx(U_("sesh: unknown error %d"), error); + goto done; + } + + for (i = 0; i < nfiles; i++) { + int tfd = open(tf[i].tfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW); + if (tfd == -1) { + sudo_warn(U_("unable to open %s"), tf[i].tfile); + goto done; + } + if (!sudo_check_temp_file(tfd, tf[i].tfile, command_details->cred.uid, NULL)) { + close(tfd); + goto done; + } + if (fchown(tfd, user_details.cred.uid, user_details.cred.gid) != 0) { + sudo_warn("unable to chown(%s) to %d:%d for editing", + tf[i].tfile, user_details.cred.uid, user_details.cred.gid); + close(tfd); + goto done; + } + close(tfd); + } + ret = nfiles; + +done: + /* Contents of tf will be freed by caller. */ + free(sesh_args); + free(user_str); + + debug_return_int(ret); +} + +static int +selinux_edit_copy_tfiles(struct command_details *command_details, + struct tempfile *tf, int nfiles, struct timespec *times) +{ + char **sesh_args, **sesh_ap, *user_str = NULL; + int i, error, sesh_nargs, ret = 1; + int tfd = -1; + struct timespec ts; + struct stat sb; + debug_decl(selinux_edit_copy_tfiles, SUDO_DEBUG_EDIT); + + if (nfiles < 1) + debug_return_int(0); + + /* Construct common args for sesh */ + sesh_nargs = 5 + (nfiles * 2) + 1; + sesh_args = sesh_ap = reallocarray(NULL, sesh_nargs, sizeof(char *)); + if (sesh_args == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + *sesh_ap++ = "sesh"; + *sesh_ap++ = "-e"; + if (ISSET(command_details->flags, CD_SUDOEDIT_CHECKDIR)) { + if ((user_str = selinux_fmt_sudo_user()) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + *sesh_ap++ = "-w"; + *sesh_ap++ = user_str; + } + *sesh_ap++ = "1"; + + /* Construct args for sesh -e 1 */ + for (i = 0; i < nfiles; i++) { + if (tfd != -1) + close(tfd); + if ((tfd = open(tf[i].tfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW)) == -1) { + sudo_warn(U_("unable to open %s"), tf[i].tfile); + continue; + } + if (!sudo_check_temp_file(tfd, tf[i].tfile, user_details.cred.uid, &sb)) + continue; + mtim_get(&sb, ts); + if (tf[i].osize == sb.st_size && sudo_timespeccmp(&tf[i].omtim, &ts, ==)) { + /* + * If mtime and size match but the user spent no measurable + * time in the editor we can't tell if the file was changed. + */ + if (sudo_timespeccmp(×[0], ×[1], !=)) { + sudo_warnx(U_("%s unchanged"), tf[i].ofile); + unlink(tf[i].tfile); + continue; + } + } + *sesh_ap++ = tf[i].tfile; + *sesh_ap++ = tf[i].ofile; + if (fchown(tfd, command_details->cred.uid, command_details->cred.gid) != 0) { + sudo_warn("unable to chown(%s) back to %d:%d", tf[i].tfile, + command_details->cred.uid, command_details->cred.gid); + } + } + *sesh_ap = NULL; + if (tfd != -1) + close(tfd); + + if (sesh_ap - sesh_args > 3) { + /* Run sesh -e 1 <t1> <o1> ... <tn> <on> */ + error = selinux_run_helper(command_details->cred.uid, command_details->cred.gid, + command_details->cred.ngroups, command_details->cred.groups, sesh_args, + command_details->envp); + switch (error) { + case SESH_SUCCESS: + ret = 0; + break; + case SESH_ERR_NO_FILES: + sudo_warnx("%s", + U_("unable to copy temporary files back to their original location")); + break; + case SESH_ERR_SOME_FILES: + sudo_warnx("%s", + U_("unable to copy some of the temporary files back to their original location")); + break; + case SESH_ERR_KILLED: + sudo_warnx("%s", U_("sesh: killed by a signal")); + break; + default: + sudo_warnx(U_("sesh: unknown error %d"), error); + break; + } + } + +done: + /* Contents of tf will be freed by caller. */ + free(sesh_args); + free(user_str); + + debug_return_int(ret); +} +#endif /* HAVE_SELINUX */ + +/* + * Wrapper to allow users to edit privileged files with their own uid. + * Returns the wait status of the command on success and a wait status + * of 1 on failure. + */ +int +sudo_edit(struct command_details *command_details) +{ + struct command_details saved_command_details; + char **nargv = NULL, **ap, **files = NULL; + int errors, i, ac, nargc, ret; + int editor_argc = 0, nfiles = 0; + struct timespec times[2]; + struct tempfile *tf = NULL; + debug_decl(sudo_edit, SUDO_DEBUG_EDIT); + + /* + * Set real, effective and saved uids to root. + * We will change the euid as needed below. + */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "setuid(%u)", ROOT_UID); + if (setuid(ROOT_UID) != 0) { + sudo_warn(U_("unable to change uid to root (%u)"), ROOT_UID); + goto cleanup; + } + + /* Find a temporary directory writable by the user. */ + if (!set_tmpdir(&user_details.cred)) + goto cleanup; + + /* + * The user's editor must be separated from the files to be + * edited by a "--" option. + */ + for (ap = command_details->argv; *ap != NULL; ap++) { + if (files) + nfiles++; + else if (strcmp(*ap, "--") == 0) + files = ap + 1; + else + editor_argc++; + } + if (nfiles == 0) { + sudo_warnx("%s", U_("plugin error: missing file list for sudoedit")); + goto cleanup; + } + +#ifdef HAVE_SELINUX + /* Compute new SELinux security context. */ + if (ISSET(command_details->flags, CD_RBAC_ENABLED)) { + if (selinux_setup(command_details->selinux_role, + command_details->selinux_type, NULL, -1, false) != 0) + goto cleanup; + } +#endif + + /* Copy editor files to temporaries. */ + tf = calloc(nfiles, sizeof(*tf)); + if (tf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto cleanup; + } +#ifdef HAVE_SELINUX + if (ISSET(command_details->flags, CD_RBAC_ENABLED)) + nfiles = selinux_edit_create_tfiles(command_details, tf, files, nfiles); + else +#endif + nfiles = sudo_edit_create_tfiles(command_details, tf, files, nfiles); + if (nfiles <= 0) + goto cleanup; + + /* + * Allocate space for the new argument vector and fill it in. + * We concatenate the editor with its args and the file list + * to create a new argv. + */ + nargc = editor_argc + nfiles; + nargv = reallocarray(NULL, nargc + 1, sizeof(char *)); + if (nargv == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto cleanup; + } + for (ac = 0; ac < editor_argc; ac++) + nargv[ac] = command_details->argv[ac]; + for (i = 0; i < nfiles && ac < nargc; ) + nargv[ac++] = tf[i++].tfile; + nargv[ac] = NULL; + + /* + * Run the editor with the invoking user's creds and drop setuid. + * Keep track of the time spent in the editor to distinguish between + * a user editing a file and a program doing it. + * XXX - should run editor with user's context + */ + if (sudo_gettime_real(×[0]) == -1) { + sudo_warn("%s", U_("unable to read the clock")); + goto cleanup; + } + memcpy(&saved_command_details, command_details, sizeof(struct command_details)); + command_details->cred = user_details.cred; + command_details->cred.euid = user_details.cred.uid; + command_details->cred.egid = user_details.cred.gid; + command_details->argv = nargv; + ret = run_command(command_details); + if (sudo_gettime_real(×[1]) == -1) { + sudo_warn("%s", U_("unable to read the clock")); + goto cleanup; + } + + /* Restore saved command_details. */ + command_details->cred = saved_command_details.cred; + command_details->argv = saved_command_details.argv; + + /* Copy contents of temp files to real ones. */ +#ifdef HAVE_SELINUX + if (ISSET(command_details->flags, CD_RBAC_ENABLED)) + errors = selinux_edit_copy_tfiles(command_details, tf, nfiles, times); + else +#endif + errors = sudo_edit_copy_tfiles(command_details, tf, nfiles, times); + if (errors) { + /* Preserve the edited temporary files. */ + ret = W_EXITCODE(1, 0); + } + + for (i = 0; i < nfiles; i++) + free(tf[i].tfile); + free(tf); + free(nargv); + debug_return_int(ret); + +cleanup: + /* Clean up temp files and return. */ + if (tf != NULL) { + for (i = 0; i < nfiles; i++) { + if (tf[i].tfile != NULL) + unlink(tf[i].tfile); + free(tf[i].tfile); + } + } + free(tf); + free(nargv); + debug_return_int(W_EXITCODE(1, 0)); +} + +#else /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */ + +/* + * Must have the ability to change the effective uid to use sudoedit. + */ +int +sudo_edit(struct command_details *command_details) +{ + debug_decl(sudo_edit, SUDO_DEBUG_EDIT); + debug_return_int(W_EXITCODE(1, 0)); +} + +#endif /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */ diff --git a/src/sudo_edit.h b/src/sudo_edit.h new file mode 100644 index 0000000..87e6e9f --- /dev/null +++ b/src/sudo_edit.h @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 */ +struct sudo_cred; +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, int sflags, struct sudo_cred *user_cred, struct sudo_cred *cur_cred); +int dir_is_writable(int dfd, struct sudo_cred *user_cred, struct sudo_cred *cur_cred); +bool sudo_edit_parent_valid(char *path, int sflags, struct sudo_cred *user_cred, struct sudo_cred *cur_cred); + +#endif /* SUDO_EDIT_H */ diff --git a/src/sudo_exec.h b/src/sudo_exec.h new file mode 100644 index 0000000..1e086d2 --- /dev/null +++ b/src/sudo_exec.h @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2010-2017, 2020-2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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_EXEC_H +#define SUDO_EXEC_H + +/* + * Older systems may not support MSG_WAITALL but it shouldn't really be needed. + */ +#ifndef MSG_WAITALL +# define MSG_WAITALL 0 +#endif + +/* + * Some older systems support siginfo but predate SI_USER. + */ +#ifdef SI_USER +# define USER_SIGNALED(_info) ((_info) != NULL && (_info)->si_code == SI_USER) +#else +# define USER_SIGNALED(_info) ((_info) != NULL && (_info)->si_code <= 0) +#endif + +/* + * Indices into io_fds[] when running a command in a pty. + */ +#define SFD_STDIN 0 +#define SFD_STDOUT 1 +#define SFD_STDERR 2 +#define SFD_LEADER 3 +#define SFD_FOLLOWER 4 +#define SFD_USERTTY 5 + +/* + * Special values to indicate whether continuing in foreground or background. + */ +#define SIGCONT_FG -2 +#define SIGCONT_BG -3 + +/* + * Positions in saved_signals[] + */ +#define SAVED_SIGALRM 0 +#define SAVED_SIGCHLD 1 +#define SAVED_SIGCONT 2 +#define SAVED_SIGHUP 3 +#define SAVED_SIGINT 4 +#define SAVED_SIGPIPE 5 +#define SAVED_SIGQUIT 6 +#define SAVED_SIGTERM 7 +#define SAVED_SIGTSTP 8 +#define SAVED_SIGTTIN 9 +#define SAVED_SIGTTOU 10 +#define SAVED_SIGUSR1 11 +#define SAVED_SIGUSR2 12 + +/* + * Error codes for sesh + */ +#define SESH_SUCCESS 0 /* successful operation */ +#define SESH_ERR_FAILURE 1 /* unspecified error */ +#define SESH_ERR_KILLED 2 /* killed by a signal */ +#define SESH_ERR_INVALID 30 /* invalid -e arg value */ +#define SESH_ERR_BAD_PATHS 31 /* odd number of paths */ +#define SESH_ERR_NO_FILES 32 /* copy error, no files copied */ +#define SESH_ERR_SOME_FILES 33 /* copy error, some files copied */ + +/* + * Symbols shared between exec.c, exec_nopty.c, exec_pty.c and exec_monitor.c + */ +struct command_details; +struct command_status; +struct stat; + +/* exec.c */ +void exec_cmnd(struct command_details *details, int errfd); +void terminate_command(pid_t pid, bool use_pgrp); +bool sudo_terminated(struct command_status *cstat); + +/* exec_common.c */ +int sudo_execve(int fd, const char *path, char *const argv[], char *envp[], bool noexec); +char **disable_execute(char *envp[], const char *dso); + +/* exec_nopty.c */ +void exec_nopty(struct command_details *details, struct command_status *cstat); + +/* exec_pty.c */ +bool exec_pty(struct command_details *details, struct command_status *cstat); +void pty_cleanup(void); +int pty_make_controlling(void); +extern int io_fds[6]; + +/* exec_monitor.c */ +int exec_monitor(struct command_details *details, sigset_t *omask, bool foreground, int backchannel); + +/* utmp.c */ +bool utmp_login(const char *from_line, const char *to_line, int ttyfd, + const char *user); +bool utmp_logout(const char *line, int status); + +#endif /* SUDO_EXEC_H */ diff --git a/src/sudo_noexec.c b/src/sudo_noexec.c new file mode 100644 index 0000000..6025d91 --- /dev/null +++ b/src/sudo_noexec.c @@ -0,0 +1,258 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2004-2005, 2010-2018 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/types.h> + +#if defined(HAVE_DECL_SECCOMP_SET_MODE_FILTER) && HAVE_DECL_SECCOMP_SET_MODE_FILTER +# include <sys/prctl.h> +# include <asm/unistd.h> +# include <linux/filter.h> +# include <linux/seccomp.h> +#endif + +#include <errno.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#ifdef HAVE_SPAWN_H +#include <spawn.h> +#endif +#include <string.h> +#ifdef HAVE_WORDEXP_H +#include <wordexp.h> +#endif +#if defined(HAVE_SHL_LOAD) +# include <dl.h> +#elif defined(HAVE_DLOPEN) +# include <dlfcn.h> +#endif + +#include "sudo_compat.h" +#include "pathnames.h" + +#ifdef HAVE___INTERPOSE +/* + * Mac OS X 10.4 and above has support for library symbol interposition. + * There is a good explanation of this in the Mac OS X Internals book. + */ +typedef struct interpose_s { + void *new_func; + void *orig_func; +} interpose_t; + +# define FN_NAME(fn) fake_ ## fn +# define INTERPOSE(fn) \ + __attribute__((__used__)) static const interpose_t interpose_ ## fn \ + __attribute__((__section__("__DATA,__interpose"))) = \ + { (void *)fake_ ## fn, (void *)fn }; +#else +# define FN_NAME(fn) fn +# define INTERPOSE(fn) +#endif + +/* + * Replacements for the exec(3) family of syscalls. It is not enough to + * just replace execve(2) since many C libraries do not call the public + * execve(2) interface. Note that it is still possible to access the real + * syscalls via the syscall(2) interface, but that is rarely done. + */ + +#define EXEC_REPL_BODY \ +{ \ + errno = EACCES; \ + return -1; \ +} + +#define EXEC_REPL1(fn, t1) \ +sudo_dso_public int \ +FN_NAME(fn)(t1 a1) \ +EXEC_REPL_BODY \ +INTERPOSE(fn) + +#define EXEC_REPL2(fn, t1, t2) \ +sudo_dso_public int \ +FN_NAME(fn)(t1 a1, t2 a2) \ +EXEC_REPL_BODY \ +INTERPOSE(fn) + +#define EXEC_REPL3(fn, t1, t2, t3) \ +sudo_dso_public int \ +FN_NAME(fn)(t1 a1, t2 a2, t3 a3) \ +EXEC_REPL_BODY \ +INTERPOSE(fn) + +#define EXEC_REPL6(fn, t1, t2, t3, t4, t5, t6) \ +sudo_dso_public int \ +FN_NAME(fn)(t1 a1, t2 a2, t3 a3, t4 a4, t5 a5, t6 a6) \ +EXEC_REPL_BODY \ +INTERPOSE(fn) + +#define EXEC_REPL_VA(fn, t1, t2) \ +sudo_dso_public int \ +FN_NAME(fn)(t1 a1, t2 a2, ...) \ +EXEC_REPL_BODY \ +INTERPOSE(fn) + +/* + * Standard exec(3) family of functions. + */ +EXEC_REPL_VA(execl, const char *, const char *) +EXEC_REPL_VA(execle, const char *, const char *) +EXEC_REPL_VA(execlp, const char *, const char *) +EXEC_REPL2(execv, const char *, char * const *) +EXEC_REPL2(execvp, const char *, char * const *) +EXEC_REPL3(execve, const char *, char * const *, char * const *) + +/* + * Non-standard exec(3) functions and corresponding private versions. + */ +#ifdef HAVE_EXECVP +EXEC_REPL3(execvP, const char *, const char *, char * const *) +#endif +#ifdef HAVE_EXECVPE +EXEC_REPL3(execvpe, const char *, char * const *, char * const *) +#endif +#ifdef HAVE_EXECT +EXEC_REPL3(exect, const char *, char * const *, char * const *) +#endif + +/* + * Not all systems support fexecve(2), posix_spawn(2) and posix_spawnp(2). + */ +#ifdef HAVE_FEXECVE +EXEC_REPL3(fexecve, int , char * const *, char * const *) +#endif +#ifdef HAVE_POSIX_SPAWN +EXEC_REPL6(posix_spawn, pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *, char * const *, char * const *) +#endif +#ifdef HAVE_POSIX_SPAWNP +EXEC_REPL6(posix_spawnp, pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *, char * const *, char * const *) +#endif + +/* + * system(3) and popen(3). + * We can't use a wrapper for popen since it returns FILE *, not int. + */ +EXEC_REPL1(system, const char *) + +sudo_dso_public FILE * +FN_NAME(popen)(const char *c, const char *t) +{ + errno = EACCES; + return NULL; +} +INTERPOSE(popen) + +#if defined(HAVE_WORDEXP) && (defined(RTLD_NEXT) || defined(HAVE_SHL_LOAD) || defined(HAVE___INTERPOSE)) +/* + * We can't use a wrapper for wordexp(3) since we still want to call + * the real wordexp(3) but with WRDE_NOCMD added to the flags argument. + */ +typedef int (*sudo_fn_wordexp_t)(const char *, wordexp_t *, int); + +sudo_dso_public int +FN_NAME(wordexp)(const char *words, wordexp_t *we, int flags) +{ +#if defined(HAVE___INTERPOSE) + return wordexp(words, we, flags | WRDE_NOCMD); +#else +# if defined(HAVE_DLOPEN) + void *fn = dlsym(RTLD_NEXT, "wordexp"); +# elif defined(HAVE_SHL_LOAD) + const char *name, *myname = _PATH_SUDO_NOEXEC; + struct shl_descriptor *desc; + void *fn = NULL; + int idx = 0; + + name = strrchr(myname, '/'); + if (name != NULL) + myname = name + 1; + + /* Search for wordexp() but skip this shared object. */ + while (shl_get(idx++, &desc) == 0) { + name = strrchr(desc->filename, '/'); + if (name == NULL) + name = desc->filename; + else + name++; + if (strcmp(name, myname) == 0) + continue; + if (shl_findsym(&desc->handle, "wordexp", TYPE_PROCEDURE, &fn) == 0) + break; + } +# else + void *fn = NULL; +# endif + if (fn == NULL) { + errno = EACCES; + return -1; + } + return ((sudo_fn_wordexp_t)fn)(words, we, flags | WRDE_NOCMD); +#endif /* HAVE___INTERPOSE */ +} +INTERPOSE(wordexp) +#endif /* HAVE_WORDEXP && (RTLD_NEXT || HAVE_SHL_LOAD || HAVE___INTERPOSE) */ + +/* + * On Linux we can use a seccomp() filter to disable exec. + */ +#if defined(HAVE_DECL_SECCOMP_SET_MODE_FILTER) && HAVE_DECL_SECCOMP_SET_MODE_FILTER + +/* Older systems may not support execveat(2). */ +#ifndef __NR_execveat +# define __NR_execveat -1 +#endif + +static void noexec_ctor(void) __attribute__((constructor)); + +static void +noexec_ctor(void) +{ + struct sock_filter exec_filter[] = { + /* Load syscall number into the accumulator */ + BPF_STMT(BPF_LD | BPF_ABS, offsetof(struct seccomp_data, nr)), + /* Jump to deny for execve/execveat */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 2, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 1, 0), + /* Allow non-matching syscalls */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + /* Deny execve/execveat syscall */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA)) + }; + const struct sock_fprog exec_fprog = { + nitems(exec_filter), + exec_filter + }; + + /* + * SECCOMP_MODE_FILTER will fail unless the process has + * CAP_SYS_ADMIN or the no_new_privs bit is set. + */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0) + (void)prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &exec_fprog); +} +#endif /* HAVE_DECL_SECCOMP_SET_MODE_FILTER */ diff --git a/src/sudo_plugin_int.h b/src/sudo_plugin_int.h new file mode 100644 index 0000000..63e4a7d --- /dev/null +++ b/src/sudo_plugin_int.h @@ -0,0 +1,133 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2010-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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_PLUGIN_INT_H +#define SUDO_PLUGIN_INT_H + +/* + * All plugin structures start with a type and a version. + */ +struct generic_plugin { + unsigned int type; + unsigned int version; + /* the rest depends on the type... */ +}; + +typedef int (*sudo_conv_1_7_t)(int num_msgs, + const struct sudo_conv_message msgs[], struct sudo_conv_reply replies[]); + +/* + * Backwards-compatible structures for API bumps. + */ +struct policy_plugin_1_0 { + unsigned int type; + unsigned int version; + int (*open)(unsigned int version, sudo_conv_1_7_t conversation, + sudo_printf_t sudo_printf, char * const settings[], + char * const user_info[], char * const user_env[]); + void (*close)(int exit_status, int error); /* wait status or error */ + int (*show_version)(int verbose); + int (*check_policy)(int argc, char * const argv[], + char *env_add[], char **command_info[], + char **argv_out[], char **user_env_out[]); + int (*list)(int argc, char * const argv[], int verbose, + const char *list_user); + int (*validate)(void); + void (*invalidate)(int remove); + int (*init_session)(struct passwd *pwd); +}; +struct io_plugin_1_0 { + unsigned int type; + unsigned int version; + int (*open)(unsigned int version, sudo_conv_1_7_t conversation, + sudo_printf_t sudo_printf, char * const settings[], + char * const user_info[], int argc, char * const argv[], + char * const user_env[]); + void (*close)(int exit_status, int error); + int (*show_version)(int verbose); + int (*log_ttyin)(const char *buf, unsigned int len); + int (*log_ttyout)(const char *buf, unsigned int len); + int (*log_stdin)(const char *buf, unsigned int len); + int (*log_stdout)(const char *buf, unsigned int len); + int (*log_stderr)(const char *buf, unsigned int len); +}; +struct io_plugin_1_1 { + unsigned int type; + unsigned int version; + int (*open)(unsigned int version, sudo_conv_1_7_t conversation, + sudo_printf_t sudo_printf, char * const settings[], + char * const user_info[], char * const command_info[], + int argc, char * const argv[], char * const user_env[]); + void (*close)(int exit_status, int error); /* wait status or error */ + int (*show_version)(int verbose); + int (*log_ttyin)(const char *buf, unsigned int len); + int (*log_ttyout)(const char *buf, unsigned int len); + int (*log_stdin)(const char *buf, unsigned int len); + int (*log_stdout)(const char *buf, unsigned int len); + int (*log_stderr)(const char *buf, unsigned int len); +}; + +/* + * Sudo plugin internals. + */ +struct plugin_container { + TAILQ_ENTRY(plugin_container) entries; + struct sudo_conf_debug_file_list *debug_files; + char *name; + char *path; + char **options; + void *handle; + int debug_instance; + union { + struct generic_plugin *generic; + struct policy_plugin *policy; + struct policy_plugin_1_0 *policy_1_0; + struct io_plugin *io; + struct io_plugin_1_0 *io_1_0; + struct io_plugin_1_1 *io_1_1; + struct audit_plugin *audit; + struct approval_plugin *approval; + } u; +}; +TAILQ_HEAD(plugin_container_list, plugin_container); + +/* + * Private implementation of struct sudo_plugin_event. + */ +struct sudo_plugin_event_int { + struct sudo_event private; /* must be first */ + int debug_instance; /* plugin's debug instance */ + void *closure; /* actual user closure */ + sudo_ev_callback_t callback; /* actual user callback */ + struct sudo_plugin_event public; /* user-visible portion */ +}; + +extern struct plugin_container policy_plugin; +extern struct plugin_container_list io_plugins; +extern struct plugin_container_list audit_plugins; +extern struct plugin_container_list approval_plugins; + +int sudo_conversation(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback); +int sudo_conversation_1_7(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[]); +int sudo_conversation_printf(int msg_type, const char *fmt, ...); + +bool sudo_load_plugins(void); + +#endif /* SUDO_PLUGIN_INT_H */ diff --git a/src/sudo_usage.h.in b/src/sudo_usage.h.in new file mode 100644 index 0000000..afccc15 --- /dev/null +++ b/src/sudo_usage.h.in @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2007-2010, 2013, 2015, 2017, 2020 + * Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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_USAGE_H +#define SUDO_USAGE_H + +/* + * Usage strings for sudo. These are here because we + * need to be able to substitute values from configure. + */ +#define SUDO_USAGE1 " -h | -K | -k | -V" +#define SUDO_USAGE2 " -v [-AknS] @BSDAUTH_USAGE@[-g group] [-h host] [-p prompt] [-u user]" +#define SUDO_USAGE3 " -l [-AknS] @BSDAUTH_USAGE@[-g group] [-h host] [-p prompt] [-U user] [-u user] [command]" +#define SUDO_USAGE4 " [-AbEHknPS] @BSDAUTH_USAGE@@SELINUX_USAGE@[-C num] [-D directory] @LOGINCAP_USAGE@[-g group] [-h host] [-p prompt] [-R directory] [-T timeout] [-u user] [VAR=value] [-i|-s] [<command>]" +#define SUDO_USAGE5 " -e [-AknS] @BSDAUTH_USAGE@@SELINUX_USAGE@[-C num] @LOGINCAP_USAGE@[-D directory] [-g group] [-h host] [-p prompt] [-R directory] [-T timeout] [-u user] file ..." + +/* + * Configure script arguments used to build sudo. + */ +#define CONFIGURE_ARGS "@CONFIGURE_ARGS@" + +#endif /* SUDO_USAGE_H */ diff --git a/src/tcsetpgrp_nobg.c b/src/tcsetpgrp_nobg.c new file mode 100644 index 0000000..bac0aa6 --- /dev/null +++ b/src/tcsetpgrp_nobg.c @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2017 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <signal.h> + +#include "sudo.h" + +static volatile sig_atomic_t got_sigttou; + +/* + * SIGTTOU signal handler for tcsetpgrp_nobg() that just sets a flag. + */ +static void +sigttou(int signo) +{ + got_sigttou = 1; +} + +/* + * Like tcsetpgrp() but restarts on EINTR _except_ for SIGTTOU. + * Returns 0 on success or -1 on failure, setting errno. + * Sets got_sigttou on failure if interrupted by SIGTTOU. + */ +int +tcsetpgrp_nobg(int fd, pid_t pgrp_id) +{ + struct sigaction sa, osa; + int rc; + + /* + * If we receive SIGTTOU from tcsetpgrp() it means we are + * not in the foreground process group. + * This avoid a TOCTOU race compared to using tcgetpgrp(). + */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; /* do not restart syscalls */ + sa.sa_handler = sigttou; + got_sigttou = 0; + (void)sigaction(SIGTTOU, &sa, &osa); + do { + rc = tcsetpgrp(fd, pgrp_id); + } while (rc != 0 && errno == EINTR && !got_sigttou); + (void)sigaction(SIGTTOU, &osa, NULL); + + return rc; +} diff --git a/src/tgetpass.c b/src/tgetpass.c new file mode 100644 index 0000000..87f01dc --- /dev/null +++ b/src/tgetpass.c @@ -0,0 +1,450 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1996, 1998-2005, 2007-2021 + * Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +/* + * 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 + */ + +#ifdef __TANDEM +# include <floss.h> +#endif + +#include <config.h> + +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <signal.h> +#include <fcntl.h> + +#include "sudo.h" +#include "sudo_plugin.h" + +enum tgetpass_errval { + TGP_ERRVAL_NOERROR, + TGP_ERRVAL_TIMEOUT, + TGP_ERRVAL_NOPASSWORD, + TGP_ERRVAL_READERROR +}; + +static volatile sig_atomic_t signo[NSIG]; + +static void tgetpass_handler(int); +static char *getln(int, char *, size_t, bool, enum tgetpass_errval *); +static char *sudo_askpass(const char *, const char *); + +static int +suspend(int sig, struct sudo_conv_callback *callback) +{ + int ret = 0; + debug_decl(suspend, SUDO_DEBUG_CONV); + + if (callback != NULL && SUDO_API_VERSION_GET_MAJOR(callback->version) != SUDO_CONV_CALLBACK_VERSION_MAJOR) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "callback major version mismatch, expected %u, got %u", + SUDO_CONV_CALLBACK_VERSION_MAJOR, + SUDO_API_VERSION_GET_MAJOR(callback->version)); + callback = NULL; + } + + if (callback != NULL && callback->on_suspend != NULL) { + if (callback->on_suspend(sig, callback->closure) == -1) + ret = -1; + } + kill(getpid(), sig); + if (callback != NULL && callback->on_resume != NULL) { + if (callback->on_resume(sig, callback->closure) == -1) + ret = -1; + } + debug_return_int(ret); +} + +static void +tgetpass_display_error(enum tgetpass_errval errval) +{ + debug_decl(tgetpass_display_error, SUDO_DEBUG_CONV); + + switch (errval) { + case TGP_ERRVAL_NOERROR: + break; + case TGP_ERRVAL_TIMEOUT: + sudo_warnx("%s", U_("timed out reading password")); + break; + case TGP_ERRVAL_NOPASSWORD: + sudo_warnx("%s", U_("no password was provided")); + break; + case TGP_ERRVAL_READERROR: + sudo_warn("%s", U_("unable to read password")); + break; + } + debug_return; +} + +/* + * Like getpass(3) but with timeout and echo flags. + */ +char * +tgetpass(const char *prompt, int timeout, int flags, + struct sudo_conv_callback *callback) +{ + struct sigaction sa, savealrm, saveint, savehup, savequit, saveterm; + struct sigaction savetstp, savettin, savettou; + char *pass; + static const char *askpass; + static char buf[SUDO_CONV_REPL_MAX + 1]; + int i, input, output, save_errno, ttyfd; + bool feedback, need_restart, neednl; + enum tgetpass_errval errval; + debug_decl(tgetpass, SUDO_DEBUG_CONV); + + (void) fflush(stdout); + + if (askpass == NULL) { + askpass = getenv_unhooked("SUDO_ASKPASS"); + if (askpass == NULL || *askpass == '\0') + askpass = sudo_conf_askpass_path(); + } + +restart: + /* Try to open /dev/tty if we are going to be using it for I/O. */ + ttyfd = -1; + if (!ISSET(flags, TGP_STDIN|TGP_ASKPASS)) { + /* If no tty present and we need to disable echo, try askpass. */ + ttyfd = open(_PATH_TTY, O_RDWR); + if (ttyfd == -1 && !ISSET(flags, TGP_ECHO|TGP_NOECHO_TRY)) { + if (askpass == NULL || getenv_unhooked("DISPLAY") == NULL) { + sudo_warnx("%s", + U_("a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper")); + debug_return_str(NULL); + } + SET(flags, TGP_ASKPASS); + } + } + + /* If using a helper program to get the password, run it instead. */ + if (ISSET(flags, TGP_ASKPASS)) { + if (askpass == NULL || *askpass == '\0') + sudo_fatalx("%s", + U_("no askpass program specified, try setting SUDO_ASKPASS")); + debug_return_str_masked(sudo_askpass(askpass, prompt)); + } + + /* Reset state. */ + for (i = 0; i < NSIG; i++) + signo[i] = 0; + pass = NULL; + save_errno = 0; + neednl = false; + need_restart = false; + feedback = false; + + /* Use tty for reading/writing if available else use stdin/stderr. */ + if (ttyfd == -1) { + input = STDIN_FILENO; + output = STDERR_FILENO; + /* Don't try to mask password if /dev/tty is not available. */ + CLR(flags, TGP_MASK); + } else { + input = ttyfd; + output = ttyfd; + } + + /* + * If we are using a tty but are not the foreground pgrp this will + * return EINTR. We send ourself SIGTTOU bracketed by callbacks. + */ + if (!ISSET(flags, TGP_ECHO)) { + for (;;) { + if (ISSET(flags, TGP_MASK)) + neednl = feedback = sudo_term_cbreak(input); + else + neednl = sudo_term_noecho(input); + if (neednl || errno != EINTR) + break; + /* Received SIGTTOU, suspend the process. */ + if (suspend(SIGTTOU, callback) == -1) { + if (ttyfd != -1) + (void) close(ttyfd); + debug_return_ptr(NULL); + } + } + } + + /* + * Catch signals that would otherwise cause the user to end + * up with echo turned off in the shell. + */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; /* don't restart system calls */ + sa.sa_handler = tgetpass_handler; + (void) sigaction(SIGALRM, &sa, &savealrm); + (void) sigaction(SIGINT, &sa, &saveint); + (void) sigaction(SIGHUP, &sa, &savehup); + (void) sigaction(SIGQUIT, &sa, &savequit); + (void) sigaction(SIGTERM, &sa, &saveterm); + (void) sigaction(SIGTSTP, &sa, &savetstp); + (void) sigaction(SIGTTIN, &sa, &savettin); + (void) sigaction(SIGTTOU, &sa, &savettou); + + if (ISSET(flags, TGP_BELL) && output != STDERR_FILENO) { + /* Ring the bell if requested and there is a tty. */ + if (write(output, "\a", 1) == -1) + goto restore; + } + if (prompt) { + if (write(output, prompt, strlen(prompt)) == -1) + goto restore; + } + + if (timeout > 0) + alarm(timeout); + pass = getln(input, buf, sizeof(buf), feedback, &errval); + alarm(0); + save_errno = errno; + + if (neednl || pass == NULL) { + if (write(output, "\n", 1) == -1) + goto restore; + } + tgetpass_display_error(errval); + +restore: + /* Restore old signal handlers. */ + (void) sigaction(SIGALRM, &savealrm, NULL); + (void) sigaction(SIGINT, &saveint, NULL); + (void) sigaction(SIGHUP, &savehup, NULL); + (void) sigaction(SIGQUIT, &savequit, NULL); + (void) sigaction(SIGTERM, &saveterm, NULL); + (void) sigaction(SIGTSTP, &savetstp, NULL); + (void) sigaction(SIGTTIN, &savettin, NULL); + (void) sigaction(SIGTTOU, &savettou, NULL); + + /* Restore old tty settings. */ + if (!ISSET(flags, TGP_ECHO)) { + /* Restore old tty settings if possible. */ + (void) sudo_term_restore(input, true); + } + if (ttyfd != -1) + (void) close(ttyfd); + + /* + * If we were interrupted by a signal, resend it to ourselves + * now that we have restored the signal handlers. + */ + for (i = 0; i < NSIG; i++) { + if (signo[i]) { + switch (i) { + case SIGALRM: + break; + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + if (suspend(i, callback) == 0) + need_restart = true; + break; + default: + kill(getpid(), i); + break; + } + } + } + if (need_restart) + goto restart; + + if (save_errno) + errno = save_errno; + + debug_return_str_masked(pass); +} + +/* + * Fork a child and exec sudo-askpass to get the password from the user. + */ +static char * +sudo_askpass(const char *askpass, const char *prompt) +{ + static char buf[SUDO_CONV_REPL_MAX + 1], *pass; + struct sigaction sa, savechld; + enum tgetpass_errval errval; + int pfd[2], status; + pid_t child; + debug_decl(sudo_askpass, SUDO_DEBUG_CONV); + + /* Set SIGCHLD handler to default since we call waitpid() below. */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_DFL; + (void) sigaction(SIGCHLD, &sa, &savechld); + + if (pipe2(pfd, O_CLOEXEC) == -1) + sudo_fatal("%s", U_("unable to create pipe")); + + child = sudo_debug_fork(); + if (child == -1) + sudo_fatal("%s", U_("unable to fork")); + + if (child == 0) { + /* child, set stdout to write side of the pipe */ + if (dup3(pfd[1], STDOUT_FILENO, 0) == -1) { + sudo_warn("dup3"); + _exit(255); + } + if (setuid(ROOT_UID) == -1) + sudo_warn("setuid(%d)", ROOT_UID); + /* Close fds before uid change to prevent prlimit sabotage on Linux. */ + closefrom(STDERR_FILENO + 1); + /* Run the askpass program with the user's original resource limits. */ + restore_limits(); + /* But avoid a setuid() failure on Linux due to RLIMIT_NPROC. */ + unlimit_nproc(); + if (setgid(user_details.cred.gid)) { + sudo_warn(U_("unable to set gid to %u"), (unsigned int)user_details.cred.gid); + _exit(255); + } + if (setuid(user_details.cred.uid)) { + sudo_warn(U_("unable to set uid to %u"), (unsigned int)user_details.cred.uid); + _exit(255); + } + restore_nproc(); + execl(askpass, askpass, prompt, (char *)NULL); + sudo_warn(U_("unable to run %s"), askpass); + _exit(255); + } + + /* Get response from child (askpass). */ + (void) close(pfd[1]); + pass = getln(pfd[0], buf, sizeof(buf), 0, &errval); + (void) close(pfd[0]); + + tgetpass_display_error(errval); + + /* Wait for child to exit. */ + for (;;) { + pid_t rv = waitpid(child, &status, 0); + if (rv == -1 && errno != EINTR) + break; + if (rv != -1 && !WIFSTOPPED(status)) + break; + } + + if (pass == NULL) + errno = EINTR; /* make cancel button simulate ^C */ + + /* Restore saved SIGCHLD handler. */ + (void) sigaction(SIGCHLD, &savechld, NULL); + + debug_return_str_masked(pass); +} + +extern int sudo_term_eof, sudo_term_erase, sudo_term_kill; + +static char * +getln(int fd, char *buf, size_t bufsiz, bool feedback, + enum tgetpass_errval *errval) +{ + size_t left = bufsiz; + ssize_t nr = -1; + char *cp = buf; + char c = '\0'; + debug_decl(getln, SUDO_DEBUG_CONV); + + *errval = TGP_ERRVAL_NOERROR; + + if (left == 0) { + *errval = TGP_ERRVAL_READERROR; + errno = EINVAL; + debug_return_str(NULL); + } + + while (--left) { + nr = read(fd, &c, 1); + if (nr != 1 || c == '\n' || c == '\r') + break; + if (feedback) { + if (c == sudo_term_eof) { + nr = 0; + break; + } else if (c == sudo_term_kill) { + while (cp > buf) { + if (write(fd, "\b \b", 3) == -1) + break; + cp--; + } + cp = buf; + left = bufsiz; + continue; + } else if (c == sudo_term_erase) { + if (cp > buf) { + ignore_result(write(fd, "\b \b", 3)); + cp--; + left++; + } + continue; + } + ignore_result(write(fd, "*", 1)); + } + *cp++ = c; + } + *cp = '\0'; + if (feedback) { + /* erase stars */ + while (cp > buf) { + if (write(fd, "\b \b", 3) == -1) + break; + --cp; + } + } + + switch (nr) { + case -1: + /* Read error */ + if (errno == EINTR) { + if (signo[SIGALRM] == 1) + *errval = TGP_ERRVAL_TIMEOUT; + } else { + *errval = TGP_ERRVAL_READERROR; + } + debug_return_str(NULL); + case 0: + /* EOF is only an error if no bytes were read. */ + if (left == bufsiz - 1) { + *errval = TGP_ERRVAL_NOPASSWORD; + debug_return_str(NULL); + } + FALLTHROUGH; + default: + debug_return_str_masked(buf); + } +} + +static void +tgetpass_handler(int s) +{ + signo[s] = 1; +} diff --git a/src/ttyname.c b/src/ttyname.c new file mode 100644 index 0000000..47e4ab1 --- /dev/null +++ b/src/ttyname.c @@ -0,0 +1,338 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2012-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +/* Large files not supported by procfs.h on Solaris. */ +#if defined(HAVE_STRUCT_PSINFO_PR_TTYDEV) +# undef _FILE_OFFSET_BITS +# undef _LARGE_FILES +#endif + +#include <sys/types.h> +#include <sys/stat.h> +#if defined(MAJOR_IN_MKDEV) +# include <sys/mkdev.h> +#elif defined(MAJOR_IN_SYSMACROS) +# include <sys/sysmacros.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <dirent.h> +#if defined(HAVE_KINFO_PROC2_NETBSD) || defined (HAVE_KINFO_PROC_OPENBSD) || defined(HAVE_KINFO_PROC_44BSD) +# include <sys/sysctl.h> +#elif defined(HAVE_KINFO_PROC_FREEBSD) +# include <sys/param.h> +# include <sys/sysctl.h> +# include <sys/user.h> +#endif +#if defined(HAVE_PROCFS_H) +# include <procfs.h> +#elif defined(HAVE_SYS_PROCFS_H) +# include <sys/procfs.h> +#endif +#ifdef HAVE_PSTAT_GETPROC +# include <sys/pstat.h> +#endif + +#include "sudo.h" + +/* + * How to access the tty device number in struct kinfo_proc. + */ +#if defined(HAVE_KINFO_PROC2_NETBSD) +# define SUDO_KERN_PROC KERN_PROC2 +# define sudo_kinfo_proc kinfo_proc2 +# define sudo_kp_tdev p_tdev +# define sudo_kp_namelen 6 +#elif defined(HAVE_KINFO_PROC_OPENBSD) +# define SUDO_KERN_PROC KERN_PROC +# define sudo_kinfo_proc kinfo_proc +# define sudo_kp_tdev p_tdev +# define sudo_kp_namelen 6 +#elif defined(HAVE_KINFO_PROC_FREEBSD) +# define SUDO_KERN_PROC KERN_PROC +# define sudo_kinfo_proc kinfo_proc +# define sudo_kp_tdev ki_tdev +# define sudo_kp_namelen 4 +#elif defined(HAVE_KINFO_PROC_44BSD) +# define SUDO_KERN_PROC KERN_PROC +# define sudo_kinfo_proc kinfo_proc +# define sudo_kp_tdev kp_eproc.e_tdev +# define sudo_kp_namelen 4 +#endif + +#if defined(sudo_kp_tdev) +/* + * Store the name of the tty to which the process is attached in name. + * Returns name on success and NULL on failure, setting errno. + */ +char * +get_process_ttyname(char *name, size_t namelen) +{ + struct sudo_kinfo_proc *ki_proc = NULL; + size_t size = sizeof(*ki_proc); + int mib[6], rc, serrno = errno; + char *ret = NULL; + debug_decl(get_process_ttyname, SUDO_DEBUG_UTIL); + + /* + * Lookup controlling tty for this process via sysctl. + * This will work even if std{in,out,err} are redirected. + */ + mib[0] = CTL_KERN; + mib[1] = SUDO_KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = (int)getpid(); + mib[4] = sizeof(*ki_proc); + mib[5] = 1; + for (;;) { + struct sudo_kinfo_proc *kp; + + size += size / 10; + if ((kp = realloc(ki_proc, size)) == NULL) { + rc = -1; + break; /* really out of memory. */ + } + ki_proc = kp; + rc = sysctl(mib, sudo_kp_namelen, ki_proc, &size, NULL, 0); + if (rc != -1 || errno != ENOMEM) + break; + } + errno = ENOENT; + if (rc != -1) { + if ((dev_t)ki_proc->sudo_kp_tdev != (dev_t)-1) { + errno = serrno; + ret = sudo_ttyname_dev(ki_proc->sudo_kp_tdev, name, namelen); + if (ret == NULL) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to map device number %u to name", + ki_proc->sudo_kp_tdev); + } + } + } else { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to resolve tty via KERN_PROC"); + } + free(ki_proc); + + debug_return_str(ret); +} +#elif defined(HAVE_STRUCT_PSINFO_PR_TTYDEV) +/* + * Store the name of the tty to which the process is attached in name. + * Returns name on success and NULL on failure, setting errno. + */ +char * +get_process_ttyname(char *name, size_t namelen) +{ + char path[PATH_MAX], *ret = NULL; + struct psinfo psinfo; + ssize_t nread; + int fd, serrno = errno; + debug_decl(get_process_ttyname, SUDO_DEBUG_UTIL); + + /* Try to determine the tty from pr_ttydev in /proc/pid/psinfo. */ + (void)snprintf(path, sizeof(path), "/proc/%u/psinfo", (unsigned int)getpid()); + if ((fd = open(path, O_RDONLY, 0)) != -1) { + nread = read(fd, &psinfo, sizeof(psinfo)); + close(fd); + if (nread == (ssize_t)sizeof(psinfo)) { + dev_t rdev = (dev_t)psinfo.pr_ttydev; +#if defined(_AIX) && defined(DEVNO64) + if ((psinfo.pr_ttydev & DEVNO64) && sizeof(dev_t) == 4) + rdev = makedev(major64(psinfo.pr_ttydev), minor64(psinfo.pr_ttydev)); +#endif + if (rdev != (dev_t)-1) { + errno = serrno; + ret = sudo_ttyname_dev(rdev, name, namelen); + goto done; + } + } + } + errno = ENOENT; + +done: + if (ret == NULL) + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to resolve tty via %s", path); + + debug_return_str(ret); +} +#elif defined(__linux__) +/* + * Store the name of the tty to which the process is attached in name. + * Returns name on success and NULL on failure, setting errno. + */ +char * +get_process_ttyname(char *name, size_t namelen) +{ + const char path[] = "/proc/self/stat"; + char *cp, buf[1024]; + char *ret = NULL; + int serrno = errno; + ssize_t nread; + int fd; + debug_decl(get_process_ttyname, SUDO_DEBUG_UTIL); + + /* + * Try to determine the tty from tty_nr in /proc/self/stat. + * Ignore /proc/self/stat if it contains embedded NUL bytes. + */ + if ((fd = open(path, O_RDONLY | O_NOFOLLOW)) != -1) { + cp = buf; + while ((nread = read(fd, cp, buf + sizeof(buf) - cp)) != 0) { + if (nread == -1) { + if (errno == EAGAIN || errno == EINTR) + continue; + break; + } + cp += nread; + if (cp >= buf + sizeof(buf)) + break; + } + if (nread == 0 && memchr(buf, '\0', cp - buf) == NULL) { + /* + * Field 7 is the tty dev (0 if no tty). + * Since the process name at field 2 "(comm)" may include + * whitespace (including newlines), start at the last ')' found. + */ + *cp = '\0'; + cp = strrchr(buf, ')'); + if (cp != NULL) { + char *ep = cp; + const char *errstr; + int field = 1; + + while (*++ep != '\0') { + if (*ep == ' ') { + *ep = '\0'; + if (++field == 7) { + int tty_nr = sudo_strtonum(cp, INT_MIN, INT_MAX, + &errstr); + if (errstr) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "%s: tty device %s: %s", path, cp, errstr); + } + if (tty_nr != 0) { + /* + * Avoid sign extension when assigning tdev. + * tty_nr in /proc/self/stat is printed as a + * signed int but the actual device number is an + * unsigned int and dev_t is unsigned long long. + */ + dev_t tdev = (unsigned int)tty_nr; + errno = serrno; + ret = sudo_ttyname_dev(tdev, name, namelen); + goto done; + } + break; + } + cp = ep + 1; + } + } + } + } + } + errno = ENOENT; + +done: + if (fd != -1) + close(fd); + if (ret == NULL) + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to resolve tty via %s", path); + + debug_return_str(ret); +} +#elif defined(HAVE_PSTAT_GETPROC) +/* + * Store the name of the tty to which the process is attached in name. + * Returns name on success and NULL on failure, setting errno. + */ +char * +get_process_ttyname(char *name, size_t namelen) +{ + struct pst_status pst; + char *ret = NULL; + int rc, serrno = errno; + debug_decl(get_process_ttyname, SUDO_DEBUG_UTIL); + + /* + * Determine the tty from psdev in struct pst_status. + * EOVERFLOW is not a fatal error for the fields we use. + * See the "EOVERFLOW Error" section of pstat_getvminfo(3). + */ + rc = pstat_getproc(&pst, sizeof(pst), 0, getpid()); + if (rc != -1 || errno == EOVERFLOW) { + if (pst.pst_term.psd_major != -1 && pst.pst_term.psd_minor != -1) { + errno = serrno; + ret = sudo_ttyname_dev(makedev(pst.pst_term.psd_major, + pst.pst_term.psd_minor), name, namelen); + goto done; + } + } + errno = ENOENT; + +done: + if (ret == NULL) + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to resolve tty via pstat"); + + debug_return_str(ret); +} +#else +/* + * Store the name of the tty to which the process is attached in name. + * Returns name on success and NULL on failure, setting errno. + */ +char * +get_process_ttyname(char *name, size_t namelen) +{ + char *tty; + debug_decl(get_process_ttyname, SUDO_DEBUG_UTIL); + + if ((tty = ttyname(STDIN_FILENO)) == NULL) { + if ((tty = ttyname(STDOUT_FILENO)) == NULL) + tty = ttyname(STDERR_FILENO); + } + if (tty != NULL) { + if (strlcpy(name, tty, namelen) < namelen) + debug_return_str(name); + errno = ERANGE; + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to store tty from ttyname"); + } else { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to resolve tty via ttyname"); + errno = ENOENT; + } + + debug_return_str(NULL); +} +#endif diff --git a/src/utmp.c b/src/utmp.c new file mode 100644 index 0000000..544a375 --- /dev/null +++ b/src/utmp.c @@ -0,0 +1,389 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2011-2018 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <sys/time.h> +#include <sys/wait.h> +#include <string.h> +#include <unistd.h> +#include <time.h> +#if defined(HAVE_UTMPS_H) +# include <utmps.h> +#elif defined(HAVE_UTMPX_H) +# include <utmpx.h> +#else +# include <utmp.h> +#endif /* HAVE_UTMPS_H */ +#ifdef HAVE_GETTTYENT +# include <ttyent.h> +#endif +#include <fcntl.h> +#include <signal.h> + +#include "sudo.h" +#include "sudo_exec.h" + +/* + * GCC 8 warns about strncpy() where the size field is the size of the buffer. + * However, strings in utmp may not be NUL terminated so this usage is correct. + */ +#if __GNUC_PREREQ__(8, 0) +# pragma GCC diagnostic ignored "-Wstringop-truncation" +#endif + +/* + * Simplify handling of different utmp types. + */ +#if defined(HAVE_GETUTSID) +# define sudo_getutline(u) GETUTSLINE(u) +# define sudo_pututline(u) PUTUTSLINE(u) +# define sudo_setutent() SETUTSENT() +# define sudo_endutent() ENDUTSENT() +#elif defined(HAVE_GETUTXID) +# define sudo_getutline(u) getutxline(u) +# define sudo_pututline(u) pututxline(u) +# define sudo_setutent() setutxent() +# define sudo_endutent() endutxent() +#elif defined(HAVE_GETUTID) +# define sudo_getutline(u) getutline(u) +# define sudo_pututline(u) pututline(u) +# define sudo_setutent() setutent() +# define sudo_endutent() endutent() +#endif + +#if defined(HAVE_GETUTSID) +typedef struct utmps sudo_utmp_t; +#elif defined(HAVE_GETUTXID) +typedef struct utmpx sudo_utmp_t; +#else +typedef struct utmp sudo_utmp_t; +/* Older systems have ut_name, not ut_user */ +# if !defined(HAVE_STRUCT_UTMP_UT_USER) && !defined(ut_user) +# define ut_user ut_name +# endif +#endif + +/* HP-UX has __e_termination and __e_exit, others may lack the __ */ +#if defined(HAVE_STRUCT_UTMP_UT_EXIT_E_TERMINATION) +# undef __e_termination +# define __e_termination e_termination +# undef __e_exit +# define __e_exit e_exit +#endif + +#if defined(HAVE_STRUCT_UTMP_UT_ID) +/* + * Create ut_id from the new ut_line and the old ut_id. + */ +static void +utmp_setid(sudo_utmp_t *old, sudo_utmp_t *new) +{ + const char *line = new->ut_line; + size_t idlen; + debug_decl(utmp_setid, SUDO_DEBUG_UTMP); + + /* Skip over "tty" in the id if old entry did too. */ + if (old != NULL) { + /* cppcheck-suppress uninitdata */ + if (strncmp(line, "tty", 3) == 0) { + idlen = MIN(sizeof(old->ut_id), 3); + if (strncmp(old->ut_id, "tty", idlen) != 0) + line += 3; + } + } + + /* Store as much as will fit, skipping parts of the beginning as needed. */ + /* cppcheck-suppress uninitdata */ + idlen = strlen(line); + if (idlen > sizeof(new->ut_id)) { + line += idlen - sizeof(new->ut_id); + idlen = sizeof(new->ut_id); + } + strncpy(new->ut_id, line, idlen); + + debug_return; +} +#endif /* HAVE_STRUCT_UTMP_UT_ID */ + +/* + * Store time in utmp structure. + */ +static void +utmp_settime(sudo_utmp_t *ut) +{ + struct timeval tv; + debug_decl(utmp_settime, SUDO_DEBUG_UTMP); + + if (gettimeofday(&tv, NULL) == 0) { +#if defined(HAVE_STRUCT_UTMP_UT_TV) + ut->ut_tv.tv_sec = tv.tv_sec; + ut->ut_tv.tv_usec = tv.tv_usec; +#else + ut->ut_time = tv.tv_sec; +#endif + } + + debug_return; +} + +/* + * Fill in a utmp entry, using an old entry as a template if there is one. + */ +static void +utmp_fill(const char *line, const char *user, sudo_utmp_t *ut_old, + sudo_utmp_t *ut_new) +{ + debug_decl(utmp_file, SUDO_DEBUG_UTMP); + + if (ut_old == NULL) { + memset(ut_new, 0, sizeof(*ut_new)); + if (user == NULL) { + strncpy(ut_new->ut_user, user_details.username, + sizeof(ut_new->ut_user)); + } + } else if (ut_old != ut_new) { + memcpy(ut_new, ut_old, sizeof(*ut_new)); + } + if (user != NULL) + strncpy(ut_new->ut_user, user, sizeof(ut_new->ut_user)); + strncpy(ut_new->ut_line, line, sizeof(ut_new->ut_line)); +#if defined(HAVE_STRUCT_UTMP_UT_ID) + utmp_setid(ut_old, ut_new); +#endif +#if defined(HAVE_STRUCT_UTMP_UT_PID) + ut_new->ut_pid = getpid(); +#endif + utmp_settime(ut_new); +#if defined(HAVE_STRUCT_UTMP_UT_TYPE) + ut_new->ut_type = USER_PROCESS; +#endif + debug_return; +} + +/* + * There are two basic utmp file types: + * + * POSIX: sequential access with new entries appended to the end. + * Manipulated via {get,put}[sx]?utent() + * + * Legacy: sparse file indexed by ttyslot() * sizeof(struct utmp) + */ +#if defined(HAVE_GETUTSID) || defined(HAVE_GETUTXID) || defined(HAVE_GETUTID) +bool +utmp_login(const char *from_line, const char *to_line, int ttyfd, + const char *user) +{ + sudo_utmp_t utbuf, *ut_old = NULL; + bool ret = false; + debug_decl(utmp_login, SUDO_DEBUG_UTMP); + + /* Strip off /dev/ prefix from line as needed. */ + if (strncmp(to_line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0) + to_line += sizeof(_PATH_DEV) - 1; + sudo_setutent(); + if (from_line != NULL) { + if (strncmp(from_line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0) + from_line += sizeof(_PATH_DEV) - 1; + + /* Lookup old line. */ + memset(&utbuf, 0, sizeof(utbuf)); + strncpy(utbuf.ut_line, from_line, sizeof(utbuf.ut_line)); + ut_old = sudo_getutline(&utbuf); + } + utmp_fill(to_line, user, ut_old, &utbuf); + if (sudo_pututline(&utbuf) != NULL) + ret = true; + sudo_endutent(); + + debug_return_bool(ret); +} + +bool +utmp_logout(const char *line, int status) +{ + bool ret = false; + sudo_utmp_t *ut, utbuf; + debug_decl(utmp_logout, SUDO_DEBUG_UTMP); + + /* Strip off /dev/ prefix from line as needed. */ + if (strncmp(line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0) + line += sizeof(_PATH_DEV) - 1; + + memset(&utbuf, 0, sizeof(utbuf)); + strncpy(utbuf.ut_line, line, sizeof(utbuf.ut_line)); + if ((ut = sudo_getutline(&utbuf)) != NULL) { + memset(ut->ut_user, 0, sizeof(ut->ut_user)); +# if defined(HAVE_STRUCT_UTMP_UT_TYPE) + ut->ut_type = DEAD_PROCESS; +# endif +# if defined(HAVE_STRUCT_UTMP_UT_EXIT) + ut->ut_exit.__e_termination = WIFSIGNALED(status) ? WTERMSIG(status) : 0; + ut->ut_exit.__e_exit = WIFEXITED(status) ? WEXITSTATUS(status) : 0; +# endif + utmp_settime(ut); + if (sudo_pututline(ut) != NULL) + ret = true; + } + debug_return_bool(ret); +} + +#else /* !HAVE_GETUTSID && !HAVE_GETUTXID && !HAVE_GETUTID */ + +/* + * Find the slot for the specified line (tty name and file descriptor). + * Returns a slot suitable for seeking into utmp on success or <= 0 on error. + * If getttyent() is available we can use that to compute the slot. + */ +# ifdef HAVE_GETTTYENT +static int +utmp_slot(const char *line, int ttyfd) +{ + int slot = 1; + struct ttyent *tty; + debug_decl(utmp_slot, SUDO_DEBUG_UTMP); + + setttyent(); + while ((tty = getttyent()) != NULL) { + if (strcmp(line, tty->ty_name) == 0) + break; + slot++; + } + endttyent(); + debug_return_int(tty ? slot : 0); +} +# elif defined(HAVE_TTYSLOT) +static int +utmp_slot(const char *line, int ttyfd) +{ + int sfd, slot; + debug_decl(utmp_slot, SUDO_DEBUG_UTMP); + + /* + * Temporarily point stdin to the tty since ttyslot() + * doesn't take an argument. + */ + if ((sfd = dup(STDIN_FILENO)) == -1) + sudo_fatal("%s", U_("unable to save stdin")); + if (dup2(ttyfd, STDIN_FILENO) == -1) + sudo_fatal("%s", U_("unable to dup2 stdin")); + slot = ttyslot(); + if (dup2(sfd, STDIN_FILENO) == -1) + sudo_fatal("%s", U_("unable to restore stdin")); + close(sfd); + + debug_return_int(slot); +} +# else /* !HAVE_TTYSLOT */ +static int +utmp_slot(const char *line, int ttyfd) +{ + return -1; +} +# endif /* HAVE_GETTTYENT */ + +bool +utmp_login(const char *from_line, const char *to_line, int ttyfd, + const char *user) +{ + sudo_utmp_t utbuf, *ut_old = NULL; + bool ret = false; + int slot; + FILE *fp; + debug_decl(utmp_login, SUDO_DEBUG_UTMP); + + /* Strip off /dev/ prefix from line as needed. */ + if (strncmp(to_line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0) + to_line += sizeof(_PATH_DEV) - 1; + + /* Find slot for new entry. */ + slot = utmp_slot(to_line, ttyfd); + if (slot <= 0) + goto done; + + if ((fp = fopen(_PATH_UTMP, "r+")) == NULL) + goto done; + + if (from_line != NULL) { + if (strncmp(from_line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0) + from_line += sizeof(_PATH_DEV) - 1; + + /* Lookup old line. */ + while (fread(&utbuf, sizeof(utbuf), 1, fp) == 1) { +# ifdef HAVE_STRUCT_UTMP_UT_ID + if (utbuf.ut_type != LOGIN_PROCESS && utbuf.ut_type != USER_PROCESS) + continue; +# endif + if (utbuf.ut_user[0] && + !strncmp(utbuf.ut_line, from_line, sizeof(utbuf.ut_line))) { + ut_old = &utbuf; + break; + } + } + } + utmp_fill(to_line, user, ut_old, &utbuf); + if (fseeko(fp, slot * (off_t)sizeof(utbuf), SEEK_SET) == 0) { + if (fwrite(&utbuf, sizeof(utbuf), 1, fp) == 1) + ret = true; + } + fclose(fp); + +done: + debug_return_bool(ret); +} + +bool +utmp_logout(const char *line, int status) +{ + sudo_utmp_t utbuf; + bool ret = false; + FILE *fp; + debug_decl(utmp_logout, SUDO_DEBUG_UTMP); + + if ((fp = fopen(_PATH_UTMP, "r+")) == NULL) + debug_return_int(ret); + + /* Strip off /dev/ prefix from line as needed. */ + if (strncmp(line, _PATH_DEV, sizeof(_PATH_DEV) - 1) == 0) + line += sizeof(_PATH_DEV) - 1; + + while (fread(&utbuf, sizeof(utbuf), 1, fp) == 1) { + if (!strncmp(utbuf.ut_line, line, sizeof(utbuf.ut_line))) { + memset(utbuf.ut_user, 0, sizeof(utbuf.ut_user)); +# if defined(HAVE_STRUCT_UTMP_UT_TYPE) + utbuf.ut_type = DEAD_PROCESS; +# endif + utmp_settime(&utbuf); + /* Back up and overwrite record. */ + if (fseeko(fp, (off_t)0 - (off_t)sizeof(utbuf), SEEK_CUR) == 0) { + if (fwrite(&utbuf, sizeof(utbuf), 1, fp) == 1) + ret = true; + } + break; + } + } + fclose(fp); + + debug_return_bool(ret); +} +#endif /* HAVE_GETUTSID || HAVE_GETUTXID || HAVE_GETUTID */ |