diff options
Diffstat (limited to '')
51 files changed, 24347 insertions, 0 deletions
diff --git a/src/Makefile.in b/src/Makefile.in new file mode 100644 index 0000000..e2ff919 --- /dev/null +++ b/src/Makefile.in @@ -0,0 +1,1099 @@ +# +# SPDX-License-Identifier: ISC +# +# Copyright (c) 2010-2023 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. +# +# @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@ +EGREP = @EGREP@ +SED = @SED@ +AWK = @AWK@ + +# Our install program supports extra flags... +INSTALL = $(SHELL) $(scriptdir)/install-sh -c +INSTALL_OWNER = -o $(install_uid) -g $(install_gid) +INSTALL_BACKUP = @INSTALL_BACKUP@ + +# Libraries +LIBPROTOBUF_C = $(top_builddir)/lib/protobuf-c/libprotobuf-c.la +LIBUTIL = $(top_builddir)/lib/util/libsudo_util.la +LT_LIBS = $(LIBUTIL) $(LIBPROTOBUF_C) +LIBS = @LIBS@ @SUDO_LIBS@ @GETGROUPS_LIB@ @NET_LIBS@ $(LT_LIBS) + +# C preprocessor defines +CPPDEFS = -D_PATH_SUDO_CONF=\"@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@ +INTERCEPT_LDFLAGS = @LT_LDFLAGS@ @LT_LDEXPORTS@ +NOEXEC_LDFLAGS = @LT_LDFLAGS@ + +# Flags to pass to libtool +LTFLAGS = --tag=disable-static + +# Flag to build sudo_module.so and sudo_noexec.so as modules instead of +# shared libs (except on macOS) +PRELOAD_MODULE = @PRELOAD_MODULE@ + +# 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 +HARDENING_CFLAGS = @HARDENING_CFLAGS@ +HARDENING_LDFLAGS = @HARDENING_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@ +adminconfdir = @adminconfdir@ +libexecdir = @libexecdir@ +datarootdir = @datarootdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +interceptfile = @INTERCEPTFILE@ +interceptdir = @INTERCEPTDIR@ +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 extension, mode and map file to use for shared libraries/objects +shlib_enable = @SHLIB_ENABLE@ +shlib_mode = @SHLIB_MODE@ +shlib_exp = ./intercept.exp +shlib_map = intercept.map +shlib_opt = intercept.opt + +# Optional init script and rc.d link +INIT_DIR=@INIT_DIR@ +INIT_SCRIPT=@INIT_SCRIPT@ +RC_LINK=@RC_LINK@ + +TEST_PROGS = check_net_ifs check_noexec check_ttyname +TEST_LIBS = @LIBS@ $(LT_LIBS) +TEST_LDFLAGS = @LDFLAGS@ +TEST_VERBOSE = + +# Set to non-empty for development mode +DEVEL = @DEVEL@ + +#### 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_intercept.o exec_iolog.o exec_monitor.o \ + exec_nopty.o exec_preload.o exec_ptrace.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 suspend_parent.o tgetpass.o ttyname.o \ + utmp.o @SUDO_OBJS@ + +IOBJS = $(OBJS:.o=.i) sesh.i sudo_intercept.i sudo_intercept_common.i + +POBJS = $(IOBJS:.i=.plog) + +SESH_OBJS = copy_file.o edit_open.o exec_common.o exec_preload.o sesh.o + +INTERCEPT_OBJS = exec_preload.lo sudo_intercept.lo sudo_intercept_common.lo \ + intercept.pb-c.lo + +CHECK_NET_IFS_OBJS = check_net_ifs.o net_ifs.o + +CHECK_NOEXEC_OBJS = check_noexec.o exec_common.o exec_preload.o + +CHECK_TTYNAME_OBJS = check_ttyname.o ttyname.o + +TEST_PTRACE_OBJS = suspend_parent.o test_ptrace.o + +LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/ + +VERSION = @PACKAGE_VERSION@ + +GENERATED = intercept.pb-c.h intercept.pb-c.c + +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) $(HARDENING_CFLAGS) $< + +.c.lo: + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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 $@ + +$(devdir)/intercept.pb-c.c: $(srcdir)/intercept.proto + @if [ -n "$(DEVEL)" ]; then \ + cmd='protoc-c --c_out=$(devdir) --proto_path=$(srcdir) $(srcdir)/intercept.proto'; \ + echo "$$cmd"; eval $$cmd; \ + cmd='$(scriptdir)/unanon $(devdir)/intercept.pb-c.h $(devdir)/intercept.pb-c.c'; \ + echo "$$cmd"; eval $$cmd; \ + if [ "$(devdir)" == "$(srcdir)" ]; then \ + cmd='mv -f $(devdir)/intercept.pb-c.h $(incdir)/intercept.pb-c.h'; \ + else \ + cmd='mv -f $(devdir)/intercept.pb-c.h $(top_builddir)/intercept.pb-c.h'; \ + fi; \ + echo "$$cmd"; eval $$cmd; \ + fi + +sudo: $(OBJS) $(LT_LIBS) @STATIC_SUDOERS@ + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(OBJS) $(SUDO_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(LIBS) @STATIC_SUDOERS@ + +$(shlib_map): $(shlib_exp) + @$(AWK) 'BEGIN { print "{\n\tglobal:" } { print "\t\t"$$0";" } END { print "\tlocal:\n\t\t*;\n};" }' $(shlib_exp) > $@ + +$(shlib_opt): $(shlib_exp) + @$(SED) 's/^/+e /' $(shlib_exp) > $@ + +sudo_intercept.la: $(INTERCEPT_OBJS) @LT_LDDEP@ + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) $(LDFLAGS) $(INTERCEPT_LDFLAGS) $(ASAN_LDFLAGS) $(HARDENING_LDFLAGS) $(LT_LIBS) @LIBDL@ -o $@ $(INTERCEPT_OBJS) $(PRELOAD_MODULE) -avoid-version -rpath $(interceptdir) -shrext .so + +sudo_noexec.la: sudo_noexec.lo + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) $(LDFLAGS) $(NOEXEC_LDFLAGS) $(HARDENING_LDFLAGS) @LIBDL@ -o $@ sudo_noexec.lo $(PRELOAD_MODULE) -avoid-version -rpath $(noexecdir) -shrext .so + +sesh: $(SESH_OBJS) $(LT_LIBS) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(SESH_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(LIBS) + +check_net_ifs: $(CHECK_NET_IFS_OBJS) $(LIBUTIL) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_NET_IFS_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS) + +check_noexec: $(CHECK_NOEXEC_OBJS) $(LIBUTIL) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_NOEXEC_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS) + +check_ttyname: $(CHECK_TTYNAME_OBJS) $(LIBUTIL) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_TTYNAME_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS) + +test_ptrace: $(TEST_PTRACE_OBJS) $(LIBUTIL) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(TEST_PTRACE_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS) + +pre-install: + +install: install-binaries install-rc @INSTALL_INTERCEPT@ @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) \ + $(DESTDIR)$(interceptdir) + 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-intercept: install-dirs sudo_intercept.la + if [ X"$(shlib_enable)" = X"yes" ]; then \ + INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m $(shlib_mode) sudo_intercept.la $(DESTDIR)$(interceptdir); \ + fi + +install-noexec: install-dirs sudo_noexec.la + if [ X"$(shlib_enable)" = X"yes" ]; then \ + INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m $(shlib_mode) sudo_noexec.la $(DESTDIR)$(noexecdir); \ + fi + +install-plugin: + +install-fuzzer: + +uninstall: + -$(LIBTOOL) $(LTFLAGS) --mode=uninstall \ + rm -f $(DESTDIR)$(interceptdir)/sudo_intercept.la \ + $(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)$(interceptdir)/sudo_intercept.so$(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) + +fuzz: + +check-fuzzer: + +check: $(TEST_PROGS) check-fuzzer + @if test X"$(cross_compiling)" != X"yes"; then \ + l=`locale -a 2>&1 | $(EGREP) -i '^C\.UTF-?8$$' | $(SED) 1q` || true; \ + test -n "$$l" || l="C"; \ + LC_ALL="$$l"; export LC_ALL; \ + unset LANG || LANG=; \ + unset LANGUAGE || LANGUAGE=; \ + MALLOC_OPTIONS=S; export MALLOC_OPTIONS; \ + MALLOC_CONF="abort:true,junk:true"; export MALLOC_CONF; \ + ./check_net_ifs $(TEST_VERBOSE); \ + if [ -f .libs/$(noexecfile) ]; then \ + ./check_noexec $(TEST_VERBOSE) .libs/$(noexecfile); \ + fi; \ + ./check_ttyname $(TEST_VERBOSE); \ + fi + +check-verbose: + exec $(MAKE) $(MFLAGS) TEST_VERBOSE=-v FUZZ_VERBOSE=-verbosity=1 check + +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 intercept.exp sudo_usage.h \ + .libs $(shlib_map) $(shlib_opt) + @if [ -n "$(DEVEL)" -a "$(devdir)" != "$(srcdir)" ]; then \ + cmd='rm -rf $(GENERATED)'; \ + echo "$$cmd"; eval $$cmd; \ + fi + +clobber: distclean + +realclean: distclean + rm -f TAGS tags + +cleandir: realclean + +.PHONY: clean mostlyclean distclean cleandir clobber 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) $(HARDENING_CFLAGS) $(srcdir)/sudo_noexec.c + +# Autogenerated dependencies, do not modify +apparmor.o: $(srcdir)/apparmor.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) $(HARDENING_CFLAGS) $(srcdir)/apparmor.c +apparmor.i: $(srcdir)/apparmor.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) $< +apparmor.plog: apparmor.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/apparmor.c --i-file $< --output-file $@ +check_net_ifs.o: $(srcdir)/regress/net_ifs/check_net_ifs.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_util.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/net_ifs/check_net_ifs.c +check_net_ifs.i: $(srcdir)/regress/net_ifs/check_net_ifs.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_util.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +check_net_ifs.plog: check_net_ifs.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/net_ifs/check_net_ifs.c --i-file $< --output-file $@ +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_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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_queue.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) $(HARDENING_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) $(HARDENING_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 $(srcdir)/sudo_edit.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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 $(srcdir)/sudo_edit.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) $(HARDENING_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) $(HARDENING_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) $(HARDENING_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) $(HARDENING_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_intercept.o: $(srcdir)/exec_intercept.c $(incdir)/compat/stdbool.h \ + $(incdir)/intercept.pb-c.h $(incdir)/protobuf-c/protobuf-c.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_rand.h $(incdir)/sudo_util.h \ + $(srcdir)/exec_intercept.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) $(HARDENING_CFLAGS) $(srcdir)/exec_intercept.c +exec_intercept.i: $(srcdir)/exec_intercept.c $(incdir)/compat/stdbool.h \ + $(incdir)/intercept.pb-c.h $(incdir)/protobuf-c/protobuf-c.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_rand.h $(incdir)/sudo_util.h \ + $(srcdir)/exec_intercept.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_intercept.plog: exec_intercept.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_intercept.c --i-file $< --output-file $@ +exec_iolog.o: $(srcdir)/exec_iolog.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) $(HARDENING_CFLAGS) $(srcdir)/exec_iolog.c +exec_iolog.i: $(srcdir)/exec_iolog.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_iolog.plog: exec_iolog.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_iolog.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) $(HARDENING_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) $(HARDENING_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_preload.lo: $(srcdir)/exec_preload.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 + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/exec_preload.c +exec_preload.i: $(srcdir)/exec_preload.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_preload.plog: exec_preload.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_preload.c --i-file $< --output-file $@ +exec_preload.o: $(srcdir)/exec_preload.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) $(HARDENING_CFLAGS) $(srcdir)/exec_preload.c +exec_ptrace.o: $(srcdir)/exec_ptrace.c $(incdir)/compat/endian.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_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/exec_intercept.h $(srcdir)/exec_ptrace.h \ + $(srcdir)/sudo.h $(srcdir)/sudo_exec.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/exec_ptrace.c +exec_ptrace.i: $(srcdir)/exec_ptrace.c $(incdir)/compat/endian.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_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/exec_intercept.h $(srcdir)/exec_ptrace.h \ + $(srcdir)/sudo.h $(srcdir)/sudo_exec.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +exec_ptrace.plog: exec_ptrace.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/exec_ptrace.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) $(HARDENING_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) $(HARDENING_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) $(HARDENING_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 $@ +intercept.pb-c.lo: $(srcdir)/intercept.pb-c.c $(incdir)/intercept.pb-c.h \ + $(incdir)/protobuf-c/protobuf-c.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/intercept.pb-c.c +intercept.pb-c.plog: $(srcdir)/intercept.pb-c.c + touch $@ +intercept.pb-c.o: $(srcdir)/intercept.pb-c.c $(incdir)/intercept.pb-c.h \ + $(incdir)/protobuf-c/protobuf-c.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/intercept.pb-c.c +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) $(HARDENING_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) $(HARDENING_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_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) $(HARDENING_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_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) $< +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) $(HARDENING_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) $(HARDENING_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)/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) $(HARDENING_CFLAGS) $(srcdir)/preload.c +preload.i: $(srcdir)/preload.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) $< +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) $(HARDENING_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) $(HARDENING_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/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_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) $(HARDENING_CFLAGS) $(srcdir)/sesh.c +sesh.i: $(srcdir)/sesh.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_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) $(HARDENING_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) $(HARDENING_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 + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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 + $(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) $(HARDENING_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 $@ +sudo_intercept.lo: $(srcdir)/sudo_intercept.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/sudo_intercept.c +sudo_intercept.i: $(srcdir)/sudo_intercept.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +sudo_intercept.plog: sudo_intercept.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sudo_intercept.c --i-file $< --output-file $@ +sudo_intercept_common.lo: $(srcdir)/sudo_intercept_common.c \ + $(incdir)/compat/stdbool.h \ + $(incdir)/intercept.pb-c.h \ + $(incdir)/protobuf-c/protobuf-c.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/sudo_intercept_common.c +sudo_intercept_common.i: $(srcdir)/sudo_intercept_common.c \ + $(incdir)/compat/stdbool.h \ + $(incdir)/intercept.pb-c.h \ + $(incdir)/protobuf-c/protobuf-c.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/sudo_exec.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +sudo_intercept_common.plog: sudo_intercept_common.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sudo_intercept_common.c --i-file $< --output-file $@ +suspend_parent.o: $(srcdir)/suspend_parent.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/suspend_parent.c +suspend_parent.i: $(srcdir)/suspend_parent.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(srcdir)/sudo_exec.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +suspend_parent.plog: suspend_parent.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/suspend_parent.c --i-file $< --output-file $@ +test_ptrace.o: $(srcdir)/regress/intercept/test_ptrace.c \ + $(incdir)/compat/endian.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_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/exec_intercept.h \ + $(srcdir)/exec_ptrace.c $(srcdir)/exec_ptrace.h \ + $(srcdir)/sudo.h $(srcdir)/sudo_exec.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/intercept/test_ptrace.c +test_ptrace.i: $(srcdir)/regress/intercept/test_ptrace.c \ + $(incdir)/compat/endian.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_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/exec_intercept.h \ + $(srcdir)/exec_ptrace.c $(srcdir)/exec_ptrace.h \ + $(srcdir)/sudo.h $(srcdir)/sudo_exec.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +test_ptrace.plog: test_ptrace.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/intercept/test_ptrace.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) $(HARDENING_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) $(HARDENING_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) $(HARDENING_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/apparmor.c b/src/apparmor.c new file mode 100644 index 0000000..8ec7a85 --- /dev/null +++ b/src/apparmor.c @@ -0,0 +1,118 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2022 Will Shand <wss2ec@virginia.edu> + * + * 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_APPARMOR + +# include <stdio.h> +# include <stdlib.h> +# include <sys/apparmor.h> + +# include <sudo.h> +# include <sudo_debug.h> + +/** + * @brief Check whether AppArmor is enabled. + * + * @return 1 if AppArmor is enabled, 0 otherwise. + */ +int +apparmor_is_enabled(void) +{ + int ret; + FILE *fd; + debug_decl(apparmor_is_enabled, SUDO_DEBUG_APPARMOR); + + /* + * Check whether AppArmor is enabled by reading + * /sys/module/apparmor/parameters/enabled + * + * When this file exists and its contents are equal to "Y", AppArmor + * is enabled. This is a little more reliable than using + * aa_is_enabled(2), which performs an additional check on securityfs + * that will fail in settings where securityfs isn't available + * (e.g. inside a container). + */ + + fd = fopen("/sys/module/apparmor/parameters/enabled", "r"); + if (fd == NULL) + debug_return_int(0); + + ret = (fgetc(fd) == 'Y'); + + fclose(fd); + debug_return_int(ret); +} + +/** + * @brief Prepare to transition into a new AppArmor profile. + * + * @param new_profile The AppArmor profile to transition into on the + * next exec. + * + * @return 0 on success, and a nonzero value on failure. + */ +int +apparmor_prepare(const char *new_profile) +{ + int ret; + char *mode, *old_profile; + debug_decl(apparmor_prepare, SUDO_DEBUG_APPARMOR); + + /* Determine the current AppArmor confinement status */ + if ((ret = aa_getcon(&old_profile, &mode)) == -1) { + sudo_warn("%s", U_("failed to determine AppArmor confinement")); + old_profile = NULL; + goto done; + } + + /* Tell AppArmor to transition into the new profile on the + * next exec */ + if ((ret = aa_change_onexec(new_profile)) != 0) { + sudo_warn(U_("unable to change AppArmor profile to %s"), new_profile); + goto done; + } + + if (mode == NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: changing AppArmor profile: %s -> %s", __func__, + old_profile, new_profile ? new_profile : "?"); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: changing AppArmor profile: %s (%s) -> %s", __func__, + old_profile, mode, new_profile ? new_profile : "?"); + } + +done: + /* + * The profile string returned by aa_getcon must be free'd, while the + * mode string must _not_ be free'd. + */ + if (old_profile != NULL) + free(old_profile); + + debug_return_int(ret); +} + +#endif /* HAVE_APPARMOR */ diff --git a/src/conversation.c b/src/conversation.c new file mode 100644 index 0000000..ea2469f --- /dev/null +++ b/src/conversation.c @@ -0,0 +1,239 @@ +/* + * 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> + +/* + * 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) +{ + const int conv_debug_instance = sudo_debug_get_active_instance(); + char *pass; + int n; + + sudo_debug_set_active_instance(sudo_debug_instance); + + for (n = 0; n < num_msgs; n++) { + const struct sudo_conv_message *msg = &msgs[n]; + unsigned int flags = tgetpass_flags; + FILE *fp = stdout; + + if (replies != NULL) + replies[n].reply = NULL; + + 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; + bool raw_tty = false; + + if (ISSET(msg->msg_type, SUDO_CONV_PREFER_TTY) && + !ISSET(flags, TGP_STDIN)) { + ttyfd = open(_PATH_TTY, O_WRONLY); + if (ttyfd != -1) + raw_tty = sudo_term_is_raw(ttyfd); + } else { + raw_tty = sudo_term_is_raw(fileno(fp)); + } + if (len != 0 && raw_tty) { + /* 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-- > 0); + } + + 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 * restrict fmt, ...) +{ + FILE *ttyfp = NULL; + FILE *fp = stdout; + char fmt2[1024]; + char sbuf[8192]; + char *buf = sbuf; + 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: + /* Convert nl -> cr nl in case tty is in raw mode. */ + if (sudo_term_is_raw(fileno(ttyfp ? ttyfp : fp))) { + size_t fmtlen = strlen(fmt); + if (fmtlen < sizeof(fmt2) - 1 && fmtlen && fmt[fmtlen - 1] == '\n') { + if (fmtlen == 1) { + /* Convert bare newline -> \r\n. */ + len = (int)fwrite("\r\n", 1, 2, ttyfp ? ttyfp : fp); + if (len != 2) + len = -1; + break; + } + if (fmt[fmtlen - 2] != '\r') { + /* Convert trailing \n -> \r\n. */ + memcpy(fmt2, fmt, fmtlen - 1); + fmt2[fmtlen - 1] = '\r'; + fmt2[fmtlen ] = '\n'; + fmt2[fmtlen + 1] = '\0'; + fmt = fmt2; + } + } + } + /* + * We use vsnprintf() instead of vfprintf() here to avoid + * problems on systems where the system printf(3) is not + * C99-compliant. We use our own snprintf() on such systems. + */ + va_start(ap, fmt); + len = vsnprintf(sbuf, sizeof(sbuf), fmt, ap); + va_end(ap); + if (len < 0 || len >= ssizeof(sbuf)) { + /* Try again with a dynamically-sized buffer. */ + va_start(ap, fmt); + len = vasprintf(&buf, fmt, ap); + va_end(ap); + } + if (len != -1) { + if (fwrite(buf, 1, len, ttyfp ? ttyfp : fp) == 0) + len = -1; + if (buf != sbuf) + free(buf); + } + 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..b9b5d1d --- /dev/null +++ b/src/copy_file.c @@ -0,0 +1,169 @@ +/* + * 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> +#include <sudo_edit.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 + * 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) { + off_t len = new_size - size; + if (len > ssizeof(zeroes)) + len = ssizeof(zeroes); + nwritten = write(fd, zeroes, (size_t)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, (size_t)(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..2fa58f5 --- /dev/null +++ b/src/edit_open.c @@ -0,0 +1,538 @@ +/* + * 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) + +static int +switch_user_int(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups, + bool nonfatal) +{ + int serrno = errno; + int ret = -1; + 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) { + if (nonfatal) + goto done; + sudo_fatal("seteuid(ROOT_UID)"); + } + } + if (setegid(egid) != 0) { + if (nonfatal) + goto done; + sudo_fatal("setegid(%d)", (int)egid); + } + if (ngroups != -1) { + if (sudo_setgroups(ngroups, groups) != 0) { + if (nonfatal) + goto done; + sudo_fatal("setgroups"); + } + } + if (euid != ROOT_UID) { + if (seteuid(euid) != 0) { + if (nonfatal) + goto done; + sudo_fatal("seteuid(%u)", (unsigned int)euid); + } + } + ret = 0; + +done: + errno = serrno; + debug_return_int(ret); +} + +#if defined(HAVE_FACCESSAT) && defined(AT_EACCESS) +static int +switch_user_nonfatal(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups) +{ + return switch_user_int(euid, egid, ngroups, groups, true); +} +#endif + +void +switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups) +{ + (void)switch_user_int(euid, egid, ngroups, groups, false); +} + +static bool +group_matches(gid_t target, const 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); +} + +static bool +is_writable(const struct sudo_cred *user_cred, struct stat *sb) +{ + debug_decl(is_writable, SUDO_DEBUG_EDIT); + + /* 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); +} + +#if defined(HAVE_FACCESSAT) && defined(AT_EACCESS) +/* + * Checks whether the open directory dfd is owned or writable by the user. + * Returns true if writable, false if not, or -1 on error. + */ +int +dir_is_writable(int dfd, const struct sudo_cred *user_cred, + const 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_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "seteuid(ROOT_UID)"); + goto fallback; + } + } + if (switch_user_nonfatal(user_cred->uid, user_cred->gid, user_cred->ngroups, + user_cred->groups) == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to switch to user_cred"); + goto fallback; + } + + /* 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 == EPERM || errno == EROFS) + debug_return_int(false); + debug_return_int(-1); + +fallback: + debug_return_int(is_writable(user_cred, &sb)); +} +#endif /* HAVE_FACCESSAT && AT_EACCESS */ + +#if !defined(HAVE_FACCESSAT) || !defined(AT_EACCESS) +/* + * Checks whether the open directory dfd is owned or writable by the user. + * Returns true if writable, false if not, or -1 on error. + */ +int +dir_is_writable(int dfd, const struct sudo_cred *user_cred, + const 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); + } + + debug_return_int(is_writable(user_cred, &sb)); +} +#endif /* HAVE_FACCESSAT && AT_EACCESS */ + +#ifdef O_NOFOLLOW +static int +sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode) +{ + int fd; + debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT); + + fd = openat(dfd, path, oflags|O_NOFOLLOW, mode); + if (fd == -1) { + /* Handle non-standard O_NOFOLLOW errno values. */ + if (errno == EMLINK) + errno = ELOOP; /* FreeBSD */ +#ifdef EFTYPE + else if (errno == EFTYPE) + errno = ELOOP; /* NetBSD */ +#endif + } + + debug_return_int(fd); +} +#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, + const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred) +{ + const int dflags = DIR_OPEN_FLAGS; + int dfd, fd, 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. + */ + writable = dir_is_writable(dfd, user_cred, cur_cred); + if (writable == -1) { + close(dfd); + debug_return_int(-1); + } + + path += strspn(path, "/"); + slash = strchr(path, '/'); + if (slash == NULL) + break; + *slash = '\0'; + if (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 (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, unsigned int sflags, + const struct sudo_cred *user_cred, const 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_NOFOLLOW)) { + /* Handle non-standard O_NOFOLLOW errno values. */ + if (errno == EMLINK) + errno = ELOOP; /* FreeBSD */ +#ifdef EFTYPE + else if (errno == EFTYPE) + errno = ELOOP; /* NetBSD */ +#endif + } + 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, unsigned int sflags, + const struct sudo_cred *user_cred, const 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, unsigned int sflags, + const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred) +{ + const int serrno = errno; + struct stat sb; + bool ret = false; + char *slash; + char pathbuf[2]; + 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 */ + pathbuf[0] = '.'; + pathbuf[1] = '\0'; + path = pathbuf; + } else if (slash == path) { + pathbuf[0] = '/'; + pathbuf[1] = '\0'; + path = pathbuf; + 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..e840c78 --- /dev/null +++ b/src/env_hooks.c @@ -0,0 +1,315 @@ +/* + * 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 * +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; + const char *equal; + size_t len; + bool found = false; + + /* Some putenv(3) implementations check for NULL. */ + if (string == NULL) { + errno = EINVAL; + return -1; + } + + /* The string must contain a '=' char but not start with one. */ + equal = strchr(string, '='); + if (equal == NULL || equal == string) { + errno = EINVAL; + return -1; + } + + /* Look for existing entry. */ + len = (size_t)(equal - 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); + +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); + +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 +# define UNSETENV_RTYPE void +#else +# define UNSETENV_RTYPE int +#endif + +sudo_dso_public UNSETENV_RTYPE unsetenv(const char *var); + +UNSETENV_RTYPE +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..46fc8bc --- /dev/null +++ b/src/exec.c @@ -0,0 +1,586 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2023 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> + +#ifdef HAVE_PTRACE_INTERCEPT +static void +handler(int signo) +{ + /* just return */ +} +#endif /* HAVE_PTRACE_INTERCEPT */ + +static void +close_fds(struct command_details *details, int errfd, int intercept_fd) +{ + 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); + if (intercept_fd != -1) + add_preserved_fd(&details->preserved_fds, intercept_fd); + + /* 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 intercept_fd, int errfd) +{ + bool ret = false; + debug_decl(exec_setup, SUDO_DEBUG_EXEC); + +#ifdef HAVE_PTRACE_INTERCEPT + if (ISSET(details->flags, CD_USE_PTRACE)) { + if (!set_exec_filter()) + goto done; + } +#endif /* HAVE_PTRACE_INTERCEPT */ + + 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("%s", U_("unable to set privileges")); + goto done; + } + } + if (details->limitprivs != NULL) { + if (setppriv(PRIV_SET, PRIV_LIMIT, details->limitprivs) != 0) { + sudo_warn("%s", U_("unable to set limit privileges")); + goto done; + } + } else if (details->privs != NULL) { + if (setppriv(PRIV_SET, PRIV_LIMIT, details->privs) != 0) { + sudo_warn("%s", U_("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) { + unsigned 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); + + /* Apply resource limits specified by the policy, if any. */ + set_policy_rlimits(); + + /* Close fds before chroot (need /dev) or uid change (prlimit on Linux). */ + close_fds(details, errfd, intercept_fd); + + 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->runcwd != NULL) { + if (details->chroot != NULL || details->submitcwd == NULL || + strcmp(details->runcwd, details->submitcwd) != 0) { + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + /* For SELinux, chdir(2) in sesh after the context change. */ + SET(details->flags, CD_RBAC_SET_CWD); + } else { + /* Note: runcwd is relative to the new root, if any. */ + if (chdir(details->runcwd) == -1) { + sudo_warn(U_("unable to change directory to %s"), + details->runcwd); + if (!ISSET(details->flags, CD_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, sigset_t *mask, + int intercept_fd, int errfd) +{ + debug_decl(exec_cmnd, SUDO_DEBUG_EXEC); + +#ifdef HAVE_PTRACE_INTERCEPT + if (ISSET(details->flags, CD_USE_PTRACE)) { + struct sigaction sa; + sigset_t set; + + /* Tracer will send us SIGUSR1 when it is time to proceed. */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = handler; + if (sudo_sigaction(SIGUSR1, &sa, NULL) != 0) { + sudo_warn(U_("unable to set handler for signal %d"), + SIGUSR1); + } + + /* Suspend child until tracer seizes control and sends SIGUSR1. */ + sigfillset(&set); + sigdelset(&set, SIGUSR1); + sigsuspend(&set); + } +#endif /* HAVE_PTRACE_INTERCEPT */ + + if (mask != NULL) + sigprocmask(SIG_SETMASK, mask, NULL); + restore_signals(); + if (exec_setup(details, intercept_fd, 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, details->runcwd, details->flags); + } else +#endif + { + sudo_execve(details->execfd, details->command, details->argv, + details->envp, intercept_fd, details->flags); + } + } + 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); +} + +static bool +sudo_needs_pty(const 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) + return true; + } + return false; +} + +/* + * Check whether the specified fd matches the device file that + * corresponds to tty_sb. If tty_sb is NULL, just check whether + * fd is a tty. Always fills in fd_sb (zeroed on error). + * Returns true on match, else false. + */ +bool +fd_matches_tty(int fd, struct stat *tty_sb, struct stat *fd_sb) +{ + debug_decl(fd_is_user_tty, SUDO_DEBUG_EXEC); + + if (fstat(fd, fd_sb) == -1) { + /* Always initialize fd_sb. */ + memset(fd_sb, 0, sizeof(*fd_sb)); + debug_return_bool(false); + } + if (!S_ISCHR(fd_sb->st_mode)) + debug_return_bool(false); + + /* Compare with tty_sb if available, else just check that fd is a tty. */ + debug_return_bool(tty_sb ? tty_sb->st_rdev == fd_sb->st_rdev : isatty(fd)); +} + +/* + * 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(const 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, + const struct user_details *user_details, + struct sudo_event_base *evbase, struct command_status *cstat) +{ + debug_decl(sudo_execute, SUDO_DEBUG_EXEC); + +#if defined(HAVE_SELINUX) && !defined(HAVE_PTRACE_INTERCEPT) + /* + * SELinux prevents LD_PRELOAD from functioning so we must use + * ptrace-based intercept mode. + */ + if (details->selinux_role != NULL || details->selinux_type != NULL) { + if (ISSET(details->flags, CD_INTERCEPT)) { + sudo_warnx("%s", + U_("intercept mode is not supported with SELinux RBAC on this system")); + CLR(details->flags, CD_INTERCEPT); + } + if (ISSET(details->flags, CD_LOG_SUBCMDS)) { + sudo_warnx("%s", + U_("unable to log sub-commands with SELinux RBAC on this system")); + CLR(details->flags, CD_LOG_SUBCMDS); + } + } +#endif /* HAVE_SELINUX && !HAVE_PTRACE_INTERCEPT */ + + /* 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 in an orphaned process group. + * Reads from the terminal fail with EIO. + * Writes succeed unless tostop is set on the 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, user_details, evbase, 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, NULL, -1, -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, user_details, evbase, 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; +} + +/* + * Free the dynamically-allocated contents of the exec closure. + */ +void +free_exec_closure(struct exec_closure *ec) +{ + debug_decl(free_exec_closure, SUDO_DEBUG_EXEC); + + /* Free any remaining intercept resources. */ + intercept_cleanup(ec); + + 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->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); + sudo_ev_free(ec->sigwinch_event); + free(ec->ptyname); + + debug_return; +} diff --git a/src/exec_common.c b/src/exec_common.c new file mode 100644 index 0000000..9e2d954 --- /dev/null +++ b/src/exec_common.c @@ -0,0 +1,136 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2022 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 <fcntl.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> + +/* + * 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 = sudo_preload_dso(envp, dso, -1); +#endif /* RTLD_PRELOAD_VAR */ + + debug_return_ptr(envp); +} + +/* + * Trap execution of child processes in the command we are about to run. + * Uses LD_PRELOAD and the like to perform a policy check on child commands. + */ +static char ** +enable_intercept(char *envp[], const char *dso, int intercept_fd) +{ + debug_decl(enable_intercept, SUDO_DEBUG_UTIL); + + if (dso != NULL) { +#ifdef RTLD_PRELOAD_VAR + if (intercept_fd == -1) + sudo_fatalx("%s: no intercept fd", __func__); + + envp = sudo_preload_dso(envp, dso, intercept_fd); +#else + /* Intercept not supported, envp unchanged. */ + if (intercept_fd != -1) + close(intercept_fd); +#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[], + int intercept_fd, unsigned int flags) +{ + debug_decl(sudo_execve, SUDO_DEBUG_UTIL); + + sudo_debug_execve(SUDO_DEBUG_INFO, path, argv, envp); + + /* Modify the environment as needed to trap execve(). */ + if (ISSET(flags, CD_NOEXEC)) + envp = disable_execute(envp, sudo_conf_noexec_path()); + if (ISSET(flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { + if (!ISSET(flags, CD_USE_PTRACE)) { + envp = enable_intercept(envp, sudo_conf_intercept_path(), + intercept_fd); + } + } + +#ifdef HAVE_FEXECVE + if (fd != -1) + fexecve(fd, argv, envp); + else +#endif + execve(path, argv, envp); + if (fd == -1 && errno == ENOEXEC) { + int argc; + const char **nargv; + + for (argc = 0; argv[argc] != NULL; argc++) + continue; + nargv = reallocarray(NULL, (size_t)argc + 2, sizeof(char *)); + if (nargv != NULL) { + nargv[0] = "sh"; + nargv[1] = path; + memcpy(nargv + 2, argv + 1, (size_t)argc * sizeof(char *)); + execve(_PATH_SUDO_BSHELL, (char **)nargv, envp); + free(nargv); + } + } + debug_return_int(-1); +} diff --git a/src/exec_intercept.c b/src/exec_intercept.c new file mode 100644 index 0000000..de9d894 --- /dev/null +++ b/src/exec_intercept.c @@ -0,0 +1,1109 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021-2023 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/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> + +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> + +#include <sudo.h> +#include <sudo_exec.h> +#include <sudo_plugin.h> +#include <sudo_plugin_int.h> +#include <sudo_rand.h> +#include <intercept.pb-c.h> +#include <exec_intercept.h> + +#ifdef _PATH_SUDO_INTERCEPT +static union sudo_token_un intercept_token; +static in_port_t intercept_listen_port; +static struct intercept_closure *accept_closure; +static void intercept_accept_cb(int fd, int what, void *v); +static void intercept_cb(int fd, int what, void *v); + +/* + * Enable the closure->ev event with the specified events and callback, + * and set the connection state to new_state if it is valid. + * Returns true on success, else false. + */ +static bool +intercept_enable_event(int fd, short events, enum intercept_state new_state, + sudo_ev_callback_t callback, struct intercept_closure *closure) +{ + int rc; + debug_decl(intercept_enable_event, SUDO_DEBUG_EXEC); + + rc = sudo_ev_set(&closure->ev, fd, events, callback, closure); + if (rc == -1 || sudo_ev_add(NULL, &closure->ev, NULL, false) == -1) { + sudo_warn("%s", U_("unable to add event to queue")); + debug_return_bool(false); + } + if (new_state != INVALID_STATE) + closure->state = new_state; + debug_return_bool(true); +} + +static bool +enable_read_event(int fd, enum intercept_state new_state, + sudo_ev_callback_t callback, struct intercept_closure *closure) +{ + return intercept_enable_event(fd, SUDO_EV_READ|SUDO_EV_PERSIST, + new_state, callback, closure); +} + +static bool +enable_write_event(int fd, sudo_ev_callback_t callback, + struct intercept_closure *closure) +{ + return intercept_enable_event(fd, SUDO_EV_WRITE|SUDO_EV_PERSIST, + INVALID_STATE, callback, closure); +} + +/* + * Create an intercept closure. + * Returns an opaque pointer to the closure, which is also + * passed to the event callback when not using ptrace(2). + */ +void * +intercept_setup(int fd, struct sudo_event_base *evbase, + const struct command_details *details) +{ + struct intercept_closure *closure; + debug_decl(intercept_setup, SUDO_DEBUG_EXEC); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "intercept fd %d\n", fd); + + closure = calloc(1, sizeof(*closure)); + if (closure == NULL) { + sudo_warnx("%s", U_("unable to allocate memory")); + goto bad; + } + closure->details = details; + closure->listen_sock = -1; + sudo_ev_set_base(&closure->ev, evbase); + + if (ISSET(details->flags, CD_USE_PTRACE)) { + /* + * We can perform a policy check immediately using ptrace(2) + * but should ignore the execve(2) of the initial command + * (and sesh for SELinux RBAC). + */ + closure->state = RECV_POLICY_CHECK; + closure->initial_command = 1; + if (ISSET(details->flags, CD_RBAC_ENABLED)) + closure->initial_command++; + } else { + /* + * Not using ptrace(2), use LD_PRELOAD (or its equivalent). If + * we've already seen an InterceptHello, expect a policy check first. + */ + const int new_state = sudo_token_isset(intercept_token) ? + RECV_SECRET : RECV_HELLO_INITIAL; + if (!enable_read_event(fd, new_state, intercept_cb, closure)) + goto bad; + } + + debug_return_ptr(closure); + +bad: + free(closure); + debug_return_ptr(NULL); +} + +/* + * Reset intercept_closure so it can be re-used. + */ +void +intercept_closure_reset(struct intercept_closure *closure) +{ + size_t n; + debug_decl(intercept_closure_reset, SUDO_DEBUG_EXEC); + + if (closure->listen_sock != -1) { + close(closure->listen_sock); + closure->listen_sock = -1; + } + free(closure->buf); + free(closure->command); + if (closure->run_argv != NULL) { + for (n = 0; closure->run_argv[n] != NULL; n++) + free(closure->run_argv[n]); + free(closure->run_argv); + } + if (closure->run_envp != NULL) { + for (n = 0; closure->run_envp[n] != NULL; n++) + free(closure->run_envp[n]); + free(closure->run_envp); + } + closure->errstr = NULL; + closure->command = NULL; + closure->run_argv = NULL; + closure->run_envp = NULL; + closure->buf = NULL; + closure->len = 0; + closure->off = 0; + /* Does not currently reset token. */ + + debug_return; +} + +/* + * Close intercept socket and free closure when we are done with + * the connection. + */ +static void +intercept_connection_close(struct intercept_closure *closure) +{ + const int fd = sudo_ev_get_fd(&closure->ev); + debug_decl(intercept_connection_close, SUDO_DEBUG_EXEC); + + sudo_ev_del(NULL, &closure->ev); + close(fd); + intercept_closure_reset(closure); + free(closure); + + debug_return; +} + +void +intercept_cleanup(struct exec_closure *ec) +{ + debug_decl(intercept_cleanup, SUDO_DEBUG_EXEC); + + if (accept_closure != NULL) { + /* DSO-based intercept. */ + intercept_connection_close(accept_closure); + accept_closure = NULL; + } else if (ec->intercept != NULL) { + /* ptrace-based intercept. */ + intercept_closure_reset(ec->intercept); + free(ec->intercept); + ec->intercept = NULL; + } + + debug_return; +} + +/* + * Prepare to listen on localhost using an ephemeral port. + * Sets intercept_token and intercept_listen_port as side effects. + */ +static bool +prepare_listener(struct intercept_closure *closure) +{ + struct sockaddr_in sin4; + socklen_t sin4_len = sizeof(sin4); + int sock; + debug_decl(prepare_listener, SUDO_DEBUG_EXEC); + + /* Generate a random token. */ + do { + arc4random_buf(&intercept_token, sizeof(intercept_token)); + } while (!sudo_token_isset(intercept_token)); + + /* Create localhost listener socket (currently AF_INET only). */ + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == -1) { + sudo_warn("socket"); + goto bad; + } + memset(&sin4, 0, sizeof(sin4)); + sin4.sin_family = AF_INET; + sin4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + sin4.sin_port = 0; + if (bind(sock, (struct sockaddr *)&sin4, sizeof(sin4)) == -1) { + sudo_warn("bind"); + goto bad; + } + if (getsockname(sock, (struct sockaddr *)&sin4, &sin4_len) == -1) { + sudo_warn("getsockname"); + goto bad; + } + if (listen(sock, SOMAXCONN) == -1) { + sudo_warn("listen"); + goto bad; + } + + closure->listen_sock = sock; + intercept_listen_port = ntohs(sin4.sin_port); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "%s: listening on port %hu", __func__, intercept_listen_port); + + debug_return_bool(true); + +bad: + if (sock != -1) + close(sock); + debug_return_bool(false); +} + +/* + * Allocate a new command_info[] and update command and runcwd in it. + * Fills in cmnd_out with a copy of the command if not NULL. + * Returns the new command_info[] which the caller must free. + */ +static char ** +update_command_info(char * const *old_command_info, const char *cmnd, + const char *runcwd, char **cmnd_out, struct intercept_closure *closure) +{ + char **command_info; + char * const *oci; + size_t n; + debug_decl(update_command_info, SUDO_DEBUG_EXEC); + + /* Rebuild command_info[] with new command and add a runcwd. */ + for (n = 0; old_command_info[n] != NULL; n++) + continue; + command_info = reallocarray(NULL, n + 3, sizeof(char *)); + if (command_info == NULL) { + goto bad; + } + for (oci = old_command_info, n = 0; *oci != NULL; oci++) { + const char *cp = *oci; + switch (*cp) { + case 'c': + if (strncmp(cp, "command=", sizeof("command=") - 1) == 0) { + if (cmnd == NULL) { + /* No new command specified, use old value. */ + cmnd = cp + sizeof("command=") - 1; + } + /* Filled in at the end. */ + continue; + } + break; + case 'r': + if (strncmp(cp, "runcwd=", sizeof("runcwd=") - 1) == 0) { + /* Filled in at the end. */ + continue; + } + break; + } + command_info[n] = strdup(cp); + if (command_info[n] == NULL) { + goto bad; + } + n++; + } + + /* Append new command. */ + if (cmnd == NULL) { + closure->errstr = N_("command not set by the security policy"); + goto bad; + } + command_info[n] = sudo_new_key_val("command", cmnd); + if (command_info[n] == NULL) { + goto oom; + } + n++; + + /* Append actual runcwd. */ + command_info[n] = sudo_new_key_val("runcwd", runcwd ? runcwd : "unknown"); + if (command_info[n] == NULL) { + goto oom; + } + n++; + + command_info[n] = NULL; + + if (cmnd_out != NULL) { + *cmnd_out = strdup(cmnd); + if (*cmnd_out == NULL) { + goto oom; + } + } + debug_return_ptr(command_info); + +oom: + closure->errstr = N_("unable to allocate memory"); + +bad: + if (command_info != NULL) { + for (n = 0; command_info[n] != NULL; n++) { + free(command_info[n]); + } + free(command_info); + } + debug_return_ptr(NULL); +} + +/* + * Perform a policy check for the given command. + * While argv must be NULL-terminated, envp need not be. + * Sets closure->state to the result of the policy check before returning. + * Return false on error, else true. + */ +bool +intercept_check_policy(const char *command, int argc, char **argv, int envc, + char **envp, const char *runcwd, int *oldcwd, void *v) +{ + struct intercept_closure *closure = v; + char **command_info = NULL; + char **command_info_copy = NULL; + char **user_env_out = NULL; + char **run_argv = NULL; + int rc, saved_dir = -1; + size_t i; + bool ret = true; + struct stat sb; + debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC); + + /* Change to runcwd before the policy check if necessary. */ + if (*command != '/') { + if (runcwd == NULL || (saved_dir = open(".", O_RDONLY)) == -1 || + chdir(runcwd) == -1) { + if (runcwd == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "relative command path but no runcwd specified"); + } else if (saved_dir == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to open current directory for reading"); + } else { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to chdir for %s", runcwd); + } + if (ISSET(closure->details->flags, CD_INTERCEPT)) { + /* Inability to change cwd is fatal in intercept mode. */ + if (closure->errstr == NULL) + closure->errstr = N_("command rejected by policy"); + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + closure->errstr, closure->details->info); + closure->state = POLICY_REJECT; + goto done; + } + } + } + + /* + * Short-circuit the policy check if the command doesn't exist. + * Otherwise, both sudo and the shell will report the error. + */ + if (stat(command, &sb) == -1) { + closure->errstr = NULL; + closure->state = POLICY_ERROR; + goto done; + } + + if (ISSET(closure->details->flags, CD_INTERCEPT)) { + /* We don't currently have a good way to validate the environment. */ + sudo_debug_set_active_instance(policy_plugin.debug_instance); + rc = policy_plugin.u.policy->check_policy(argc, argv, NULL, + &command_info, &run_argv, &user_env_out, &closure->errstr); + sudo_debug_set_active_instance(sudo_debug_instance); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "check_policy returns %d", rc); + + switch (rc) { + case 1: + /* Rebuild command_info[] with runcwd and extract command. */ + command_info_copy = update_command_info(command_info, NULL, + runcwd, &closure->command, closure); + if (command_info_copy == NULL) + goto oom; + command_info = command_info_copy; + closure->state = POLICY_ACCEPT; + break; + case 0: + if (closure->errstr == NULL) + closure->errstr = N_("command rejected by policy"); + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + closure->errstr, command_info); + closure->state = POLICY_REJECT; + goto done; + default: + /* Plugin error? */ + goto bad; + } + } else { + /* No actual policy check, just logging child processes. */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "not checking policy, audit only"); + closure->command = strdup(command); + if (closure->command == NULL) + goto oom; + + /* Rebuild command_info[] with new command and runcwd. */ + command_info_copy = update_command_info(closure->details->info, + command, runcwd, NULL, closure); + if (command_info_copy == NULL) + goto oom; + command_info = command_info_copy; + closure->state = POLICY_ACCEPT; + run_argv = argv; + } + + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_command: %s", closure->command); + for (i = 0; command_info[i] != NULL; i++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "command_info[%zu]: %s", i, command_info[i]); + } + for (i = 0; run_argv[i] != NULL; i++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_argv[%zu]: %s", i, run_argv[i]); + } + } + + /* Make a copy of run_argv, it may share contents of argv. */ + for (i = 0; run_argv[i] != NULL; i++) + continue; + closure->run_argv = reallocarray(NULL, i + 1, sizeof(char *)); + if (closure->run_argv == NULL) + goto oom; + for (i = 0; run_argv[i] != NULL; i++) { + closure->run_argv[i] = strdup(run_argv[i]); + if (closure->run_argv[i] == NULL) + goto oom; + } + closure->run_argv[i] = NULL; + + /* Make a copy of envp, which may not be NULL-terminated. */ + closure->run_envp = reallocarray(NULL, (size_t)envc + 1, sizeof(char *)); + if (closure->run_envp == NULL) + goto oom; + for (i = 0; i < (size_t)envc; i++) { + closure->run_envp[i] = strdup(envp[i]); + if (closure->run_envp[i] == NULL) + goto oom; + } + closure->run_envp[i] = NULL; + + if (ISSET(closure->details->flags, CD_INTERCEPT)) { + audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, command_info, + closure->run_argv, closure->run_envp); + + /* Call approval plugins and audit the result. */ + if (!approval_check(command_info, closure->run_argv, closure->run_envp)) { + if (closure->errstr == NULL) + closure->errstr = N_("approval plugin error"); + closure->state = POLICY_REJECT; + goto done; + } + } + + /* Audit the event again for the sudo front-end. */ + audit_accept("sudo", SUDO_FRONT_END, command_info, closure->run_argv, + closure->run_envp); + + goto done; + +oom: + closure->errstr = N_("unable to allocate memory"); + +bad: + if (saved_dir != -1) { + if (fchdir(saved_dir) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to restore saved cwd", __func__); + } + close(saved_dir); + saved_dir = -1; + } + if (closure->errstr == NULL) + closure->errstr = N_("policy plugin error"); + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, closure->errstr, + command_info ? command_info : closure->details->info); + closure->state = POLICY_ERROR; + ret = false; + +done: + if (command_info_copy != NULL) { + for (i = 0; command_info_copy[i] != NULL; i++) { + free(command_info_copy[i]); + } + free(command_info_copy); + } + *oldcwd = saved_dir; + + debug_return_bool(ret); +} + +static bool +intercept_check_policy_req(PolicyCheckRequest *req, + struct intercept_closure *closure) +{ + char **argv = NULL; + bool ret = false; + int oldcwd = -1; + size_t n; + debug_decl(intercept_check_policy_req, SUDO_DEBUG_EXEC); + + if (req->command == NULL || req->n_argv > INT_MAX || req->n_envp > INT_MAX) { + closure->errstr = N_("invalid PolicyCheckRequest"); + goto done; + } + + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "req_command: %s", req->command); + for (n = 0; n < req->n_argv; n++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "req_argv[%zu]: %s", n, req->argv[n]); + } + } + + /* If argv is empty, reserve an extra slot for the command. */ + if (req->n_argv == 0) + req->n_argv = 1; + + /* + * Rebuild argv from PolicyCheckReq so it is NULL-terminated. + * The plugin API requires us to pass the pathname to exec in argv[0]. + */ + argv = reallocarray(NULL, req->n_argv + 1, sizeof(char *)); + if (argv == NULL) { + closure->errstr = N_("unable to allocate memory"); + goto done; + } + argv[0] = req->command; + for (n = 1; n < req->n_argv; n++) { + argv[n] = req->argv[n]; + } + argv[n] = NULL; + + ret = intercept_check_policy(req->command, (int)req->n_argv, argv, + (int)req->n_envp, req->envp, req->cwd, &oldcwd, closure); + +done: + if (oldcwd != -1) { + if (fchdir(oldcwd) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to restore saved cwd", __func__); + } + close(oldcwd); + } + + free(argv); + + debug_return_bool(ret); +} + +/* + * Read token from sudo_intercept.so and verify w/ intercept_token. + * Returns true on success, false on mismatch and -1 on error. + */ +static int +intercept_verify_token(int fd, struct intercept_closure *closure) +{ + ssize_t nread; + debug_decl(intercept_verify_token, SUDO_DEBUG_EXEC); + + nread = recv(fd, closure->token.u8 + closure->off, + sizeof(closure->token) - closure->off, 0); + switch (nread) { + case 0: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "EOF reading token"); + debug_return_int(false); + case -1: + debug_return_int(-1); + default: + if (nread + closure->off == sizeof(closure->token)) + break; + /* partial read, update offset and try again */ + closure->off += (uint32_t)nread; + errno = EAGAIN; + debug_return_int(-1); + } + + closure->off = 0; + if (memcmp(&closure->token, &intercept_token, sizeof(closure->token)) != 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "token mismatch: got 0x%8x%8x%8x%8x, expected 0x%8x%8x%8x%8x", + closure->token.u32[3], closure->token.u32[2], + closure->token.u32[1], closure->token.u32[0], + intercept_token.u32[3], intercept_token.u32[2], + intercept_token.u32[1], intercept_token.u32[0]); + debug_return_int(false); + } + debug_return_int(true); +} + +/* + * Read a message from sudo_intercept.so and act on it. + */ +static bool +intercept_read(int fd, struct intercept_closure *closure) +{ + InterceptRequest *req = NULL; + bool ret = false; + ssize_t nread; + debug_decl(intercept_read, SUDO_DEBUG_EXEC); + + if (closure->state == RECV_SECRET) { + switch (intercept_verify_token(fd, closure)) { + case true: + closure->state = RECV_POLICY_CHECK; + break; + case false: + goto done; + default: + if (errno == EINTR || errno == EAGAIN) { + sudo_debug_printf( + SUDO_DEBUG_WARN|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO, + "reading intercept token"); + debug_return_bool(true); + } + sudo_warn("recv"); + goto done; + } + } + + if (closure->len == 0) { + uint32_t req_len; + + /* Read message size (uint32_t in host byte order). */ + nread = recv(fd, &req_len, sizeof(req_len), 0); + if (nread != sizeof(req_len)) { + if (nread == -1) { + if (errno == EINTR || errno == EAGAIN) { + sudo_debug_printf( + SUDO_DEBUG_WARN|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO, + "reading intercept message size"); + debug_return_bool(true); + } + sudo_warn("recv"); + } + goto done; + } + + if (req_len == 0) { + /* zero-length message is possible */ + goto unpack; + } + if (req_len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("client request too large: %zu"), (size_t)req_len); + goto done; + } + if ((closure->buf = malloc(req_len)) == NULL) { + sudo_warnx("%s", U_("unable to allocate memory")); + goto done; + } + closure->len = req_len; + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: expecting %u bytes from client", + __func__, closure->len); + } + + nread = recv(fd, closure->buf + closure->off, closure->len - closure->off, + 0); + switch (nread) { + case 0: + /* EOF, other side must have exited. */ + goto done; + case -1: + if (errno == EINTR || errno == EAGAIN) { + sudo_debug_printf( + SUDO_DEBUG_WARN|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO, + "reading intercept message"); + debug_return_bool(true); + } + sudo_warn("recv"); + goto done; + default: + closure->off += (uint32_t)nread; + break; + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from client", + __func__, nread); + + if (closure->off != closure->len) { + /* Partial read. */ + debug_return_bool(true); + } + +unpack: + req = intercept_request__unpack(NULL, closure->len, closure->buf); + if (req == NULL) { + sudo_warnx(U_("unable to unpack %s size %zu"), "InterceptRequest", + (size_t)closure->len); + goto done; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: finished receiving %u bytes from client", __func__, closure->len); + sudo_ev_del(NULL, &closure->ev); + free(closure->buf); + closure->buf = NULL; + closure->len = 0; + closure->off = 0; + + switch (req->type_case) { + case INTERCEPT_REQUEST__TYPE_POLICY_CHECK_REQ: + if (closure->state != RECV_POLICY_CHECK) { + /* Only a single policy check request is allowed. */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "state mismatch, expected RECV_POLICY_CHECK (%d), got %d", + RECV_POLICY_CHECK, closure->state); + goto done; + } + + ret = intercept_check_policy_req(req->u.policy_check_req, closure); + if (!ret) + goto done; + if (!ISSET(closure->details->flags, CD_INTERCEPT)) { + /* Just logging, re-use event to read next InterceptHello. */ + ret = enable_read_event(fd, RECV_HELLO, intercept_cb, closure); + goto done; + } + break; + case INTERCEPT_REQUEST__TYPE_HELLO: + switch (closure->state) { + case RECV_HELLO_INITIAL: + if (!prepare_listener(closure)) + goto done; + break; + case RECV_HELLO: + break; + default: + /* Only accept hello on a socket with an accepted command. */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "got InterceptHello without an accepted command"); + goto done; + } + break; + default: + sudo_warnx(U_("unexpected type_case value %d in %s from %s"), + req->type_case, "InterceptRequest", "sudo_intercept.so"); + goto done; + } + + /* Switch event to write mode for the reply. */ + if (!enable_write_event(fd, intercept_cb, closure)) + goto done; + + ret = true; + +done: + intercept_request__free_unpacked(req, NULL); + debug_return_bool(ret); +} + +static bool +fmt_intercept_response(InterceptResponse *resp, + struct intercept_closure *closure) +{ + uint32_t resp_len; + bool ret = false; + debug_decl(fmt_intercept_response, SUDO_DEBUG_EXEC); + + closure->len = (uint32_t)intercept_response__get_packed_size(resp); + if (closure->len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("server message too large: %zu"), (size_t)closure->len); + goto done; + } + + /* Wire message size is used for length encoding, precedes message. */ + resp_len = closure->len; + closure->len += sizeof(resp_len); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "size + InterceptResponse %zu bytes", (size_t)closure->len); + + if ((closure->buf = malloc(closure->len)) == NULL) { + sudo_warnx("%s", U_("unable to allocate memory")); + goto done; + } + memcpy(closure->buf, &resp_len, sizeof(resp_len)); + intercept_response__pack(resp, closure->buf + sizeof(resp_len)); + + ret = true; + +done: + debug_return_bool(ret); +} + +static bool +fmt_hello_response(struct intercept_closure *closure) +{ + HelloResponse hello_resp = HELLO_RESPONSE__INIT; + InterceptResponse resp = INTERCEPT_RESPONSE__INIT; + debug_decl(fmt_hello_response, SUDO_DEBUG_EXEC); + + hello_resp.portno = intercept_listen_port; + hello_resp.token_lo = intercept_token.u64[0]; + hello_resp.token_hi = intercept_token.u64[1]; + hello_resp.log_only = !ISSET(closure->details->flags, CD_INTERCEPT); + + resp.u.hello_resp = &hello_resp; + resp.type_case = INTERCEPT_RESPONSE__TYPE_HELLO_RESP; + + debug_return_bool(fmt_intercept_response(&resp, closure)); +} + +static bool +fmt_accept_message(struct intercept_closure *closure) +{ + PolicyAcceptMessage msg = POLICY_ACCEPT_MESSAGE__INIT; + InterceptResponse resp = INTERCEPT_RESPONSE__INIT; + size_t n; + debug_decl(fmt_accept_message, SUDO_DEBUG_EXEC); + + msg.run_command = closure->command; + msg.run_argv = closure->run_argv; + for (n = 0; closure->run_argv[n] != NULL; n++) + continue; + msg.n_run_argv = n; + msg.run_envp = closure->run_envp; + for (n = 0; closure->run_envp[n] != NULL; n++) + continue; + msg.n_run_envp = n; + + resp.u.accept_msg = &msg; + resp.type_case = INTERCEPT_RESPONSE__TYPE_ACCEPT_MSG; + + debug_return_bool(fmt_intercept_response(&resp, closure)); +} + +static bool +fmt_reject_message(struct intercept_closure *closure) +{ + PolicyRejectMessage msg = POLICY_REJECT_MESSAGE__INIT; + InterceptResponse resp = INTERCEPT_RESPONSE__INIT; + debug_decl(fmt_reject_message, SUDO_DEBUG_EXEC); + + msg.reject_message = (char *)closure->errstr; + + resp.u.reject_msg = &msg; + resp.type_case = INTERCEPT_RESPONSE__TYPE_REJECT_MSG; + + debug_return_bool(fmt_intercept_response(&resp, closure)); +} + +static bool +fmt_error_message(struct intercept_closure *closure) +{ + PolicyErrorMessage msg = POLICY_ERROR_MESSAGE__INIT; + InterceptResponse resp = INTERCEPT_RESPONSE__INIT; + debug_decl(fmt_error_message, SUDO_DEBUG_EXEC); + + msg.error_message = (char *)closure->errstr; + + resp.u.error_msg = &msg; + resp.type_case = INTERCEPT_RESPONSE__TYPE_ERROR_MSG; + + debug_return_bool(fmt_intercept_response(&resp, closure)); +} + +/* + * Write a response to sudo_intercept.so. + */ +static bool +intercept_write(int fd, struct intercept_closure *closure) +{ + bool ret = false; + ssize_t nwritten; + debug_decl(intercept_write, SUDO_DEBUG_EXEC); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "state %d", + closure->state); + + if (closure->len == 0) { + /* Format new message. */ + switch (closure->state) { + case RECV_HELLO_INITIAL: + case RECV_HELLO: + if (!fmt_hello_response(closure)) + goto done; + break; + case POLICY_ACCEPT: + if (!fmt_accept_message(closure)) + goto done; + break; + case POLICY_REJECT: + if (!fmt_reject_message(closure)) + goto done; + break; + default: + if (!fmt_error_message(closure)) + goto done; + break; + } + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %u bytes to client", + __func__, closure->len - closure->off); + nwritten = send(fd, closure->buf + closure->off, + closure->len - closure->off, 0); + if (nwritten == -1) { + if (errno == EINTR || errno == EAGAIN) { + sudo_debug_printf( + SUDO_DEBUG_WARN|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO, + "writing intercept message"); + debug_return_bool(true); + } + sudo_warn("send"); + goto done; + } + closure->off += (uint32_t)nwritten; + + if (closure->off != closure->len) { + /* Partial write. */ + debug_return_bool(true); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sent %u bytes to client", __func__, closure->len); + sudo_ev_del(NULL, &closure->ev); + free(closure->buf); + closure->buf = NULL; + closure->len = 0; + closure->off = 0; + + switch (closure->state) { + case RECV_HELLO_INITIAL: + /* Re-use the listener event. */ + close(fd); + if (!enable_read_event(closure->listen_sock, RECV_CONNECTION, + intercept_accept_cb, closure)) + goto done; + closure->listen_sock = -1; + closure->state = RECV_CONNECTION; + accept_closure = closure; + break; + case POLICY_ACCEPT: + /* Re-use event to read InterceptHello from sudo_intercept.so ctor. */ + if (!enable_read_event(fd, RECV_HELLO, intercept_cb, closure)) + goto done; + break; + default: + /* Done with this connection. */ + intercept_connection_close(closure); + } + + ret = true; + +done: + debug_return_bool(ret); +} + +static void +intercept_cb(int fd, int what, void *v) +{ + struct intercept_closure *closure = v; + bool success = false; + debug_decl(intercept_cb, SUDO_DEBUG_EXEC); + + switch (what) { + case SUDO_EV_READ: + success = intercept_read(fd, closure); + break; + case SUDO_EV_WRITE: + success = intercept_write(fd, closure); + break; + default: + sudo_warnx("%s: unexpected event type %d", __func__, what); + break; + } + + if (!success) + intercept_connection_close(closure); + + debug_return; +} + +/* + * Accept a new connection from the client register a new event for it. + */ +static void +intercept_accept_cb(int fd, int what, void *v) +{ + struct intercept_closure *closure = v; + struct sudo_event_base *evbase = sudo_ev_get_base(&closure->ev); + struct sockaddr_in sin4; + socklen_t sin4_len = sizeof(sin4); + int client_sock, flags, on = 1; + debug_decl(intercept_accept_cb, SUDO_DEBUG_EXEC); + + if (closure->state != RECV_CONNECTION) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "state mismatch, expected RECV_CONNECTION (%d), got %d", + RECV_CONNECTION, closure->state); + intercept_connection_close(closure); + accept_closure = NULL; + debug_return; + } + + client_sock = accept(fd, (struct sockaddr *)&sin4, &sin4_len); + if (client_sock == -1) { + sudo_warn("accept"); + goto bad; + } + flags = fcntl(client_sock, F_GETFL, 0); + if (flags != -1) + (void)fcntl(client_sock, F_SETFL, flags | O_NONBLOCK); + + /* Send data immediately, we need low latency IPC. */ + (void)setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); + + /* + * Create a new intercept closure and register an event for client_sock. + */ + if (intercept_setup(client_sock, evbase, closure->details) == NULL) { + goto bad; + } + + debug_return; + +bad: + if (client_sock != -1) + close(client_sock); + debug_return; +} +#else /* _PATH_SUDO_INTERCEPT */ +void * +intercept_setup(int fd, struct sudo_event_base *evbase, + const struct command_details *details) +{ + debug_decl(intercept_setup, SUDO_DEBUG_EXEC); + + /* Intercept support not compiled in. */ + + debug_return_ptr(NULL); +} + +void +intercept_cleanup(struct exec_closure *ec) +{ + debug_decl(intercept_cleanup, SUDO_DEBUG_EXEC); + + /* Intercept support not compiled in. */ + + debug_return; +} +#endif /* _PATH_SUDO_INTERCEPT */ diff --git a/src/exec_intercept.h b/src/exec_intercept.h new file mode 100644 index 0000000..e7f1732 --- /dev/null +++ b/src/exec_intercept.h @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021-2022 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_INTERCEPT_H +#define SUDO_EXEC_INTERCEPT_H + +enum intercept_state { + INVALID_STATE, + RECV_HELLO_INITIAL, + RECV_HELLO, + RECV_SECRET, + RECV_POLICY_CHECK, + RECV_CONNECTION, + POLICY_ACCEPT, + POLICY_REJECT, + POLICY_TEST, + POLICY_ERROR +}; + +/* Closure for intercept_cb() */ +struct intercept_closure { + union sudo_token_un token; + const struct command_details *details; + struct sudo_event ev; + const char *errstr; + char *command; /* dynamically allocated */ + char **run_argv; /* owned by plugin */ + char **run_envp; /* dynamically allocated */ + uint8_t *buf; /* dynamically allocated */ + uint32_t len; + uint32_t off; + int listen_sock; + enum intercept_state state; + int initial_command; +}; + +void intercept_closure_reset(struct intercept_closure *closure); +bool intercept_check_policy(const char *command, int argc, char **argv, int envc, char **envp, const char *runcwd, int *oldcwd, void *closure); + +#endif /* SUDO_EXEC_INTERCEPT_H */ diff --git a/src/exec_iolog.c b/src/exec_iolog.c new file mode 100644 index 0000000..239fc41 --- /dev/null +++ b/src/exec_iolog.c @@ -0,0 +1,612 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2023 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 <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> + +int io_fds[6] = { -1, -1, -1, -1, -1, -1 }; + +static struct io_buffer_list iobufs = SLIST_HEAD_INITIALIZER(&iobufs); + +static sigset_t ttyblock; + +/* + * Remove and free any events associated with the specified + * file descriptor present in the I/O buffers list. + */ +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). + */ +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)); +} + +/* + * Allocate a new I/O buffer and associated read/write events. + */ +void +io_buf_new(int rfd, int wfd, + bool (*action)(const char *, unsigned int, struct io_buffer *), + void (*read_cb)(int fd, int what, void *v), + void (*write_cb)(int fd, int what, void *v), struct exec_closure *ec) +{ + 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|SUDO_EV_PERSIST, + read_cb, iob); + iob->wevent = sudo_ev_alloc(wfd, SUDO_EV_WRITE|SUDO_EV_PERSIST, + write_cb, 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(&iobufs, iob, entries); + + debug_return; +} + +/* + * Schedule I/O events before starting the main event loop or + * resuming from suspend. + */ +void +add_io_events(struct exec_closure *ec) +{ + 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 && + (ec->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(ec->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(ec->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. + */ +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; +} + +/* + * Free the contents of the I/O buffers queue. + */ +void +free_io_bufs(void) +{ + struct io_buffer *iob; + debug_decl(free_io_bufs, SUDO_DEBUG_EXEC); + + 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); + } + + debug_return; +} + +/* Call I/O plugin tty input log method. */ +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. */ +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. */ +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. */ +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. */ +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. */ +void +log_suspend(void *v, int signo) +{ + struct exec_closure *ec = v; + 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. */ +void +log_winchange(struct exec_closure *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; +} + +void +init_ttyblock(void) +{ + /* So we can block tty-generated signals */ + sigemptyset(&ttyblock); + sigaddset(&ttyblock, SIGINT); + sigaddset(&ttyblock, SIGQUIT); + sigaddset(&ttyblock, SIGTSTP); + sigaddset(&ttyblock, SIGTTIN); + sigaddset(&ttyblock, SIGTTOU); +} diff --git a/src/exec_monitor.c b/src/exec_monitor.c new file mode 100644 index 0000000..9681e56 --- /dev/null +++ b/src/exec_monitor.c @@ -0,0 +1,727 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2023 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/socket.h> +#include <sys/wait.h> +#include <sys/ioctl.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 <sudo.h> +#include <sudo_exec.h> +#include <sudo_plugin.h> +#include <sudo_plugin_int.h> + +struct monitor_closure { + const struct command_details *details; + struct sudo_event_base *evbase; + struct sudo_event *errsock_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; + struct command_status *cstat; + pid_t cmnd_pid; + pid_t cmnd_pgrp; + pid_t mon_pgrp; + int backchannel; +}; + +/* + * 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) +{ + 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 (sudo_debug_needed(SUDO_DEBUG_INFO)) { + char signame[SIG2STR_MAX]; + 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); + sudo_debug_printf(SUDO_DEBUG_INFO, "received SIG%s%s", + signame, from_parent ? " from parent" : ""); + } + + /* Handle signal from parent or monitor. */ + 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); + } + 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; +} + +/* + * Send status to parent over socketpair. + * Return value is the same as send(2). + */ +static ssize_t +send_status(int fd, struct command_status *cstat) +{ + ssize_t 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 != ssizeof(*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_ssize_t(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|WNOHANG); + } while (pid == -1 && errno == EINTR); + switch (pid) { + case -1: + if (errno != ECHILD) { + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + debug_return; + } + FALLTHROUGH; + case 0: + /* Nothing to wait for. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: no process to wait for", + __func__); + debug_return; + } + + 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 0x%x 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. + */ + 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; + + if (sc->siginfo->si_pid == mc->cmnd_pid) + debug_return; + si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == mc->cmnd_pgrp) + debug_return; + } + } + deliver_signal(mc, signo, false); + } + debug_return; +} + +/* This is essentially the same as errpipe_cb() in exec_nopty.c */ +static void +mon_errsock_cb(int fd, int what, void *v) +{ + struct monitor_closure *mc = v; + ssize_t nread; + int errval; + debug_decl(mon_errsock_cb, SUDO_DEBUG_EXEC); + + /* + * Read errno from child or EOF when command is executed. + * Note that the error socket 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 socket", __func__); + sudo_ev_loopbreak(mc->evbase); + } + break; + default: + if (nread == 0) { + /* The error socket closes when the command is executed. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "EOF on error socket"); + } 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->errsock_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 != ssizeof(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 { + if (cstmp.type == CMD_SIGNO) { + deliver_signal(mc, cstmp.val, true); + } else { + sudo_warnx(U_("unexpected reply type on backchannel: %d"), + cstmp.type); + } + } + 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, sigset_t *mask, + bool foreground, int intercept_fd, int errfd) +{ + volatile pid_t self = getpid(); + debug_decl(exec_cmnd_pty, SUDO_DEBUG_EXEC); + + /* 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) { + char ch; + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: waiting for controlling tty", + __func__); + if (recv(errfd, &ch, sizeof(ch), 0) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to receive message from parent", __func__); + debug_return; + } + if (tcgetpgrp(io_fds[SFD_FOLLOWER]) == self) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: got controlling tty", + __func__); + } else { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: unable to get controlling tty", __func__); + foreground = false; + } + } + + /* 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, mask, intercept_fd, 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, + const struct command_details *details, 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->details = details; + 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->errsock_event = sudo_ev_alloc(errfd, + SUDO_EV_READ|SUDO_EV_PERSIST, mon_errsock_cb, mc); + if (mc->errsock_event == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (sudo_ev_add(mc->evbase, mc->errsock_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; +} + +/* + * Make the tty follower the controlling tty. + */ +static bool +pty_make_controlling(const char *follower) +{ + debug_decl(pty_make_controlling, SUDO_DEBUG_EXEC); + + if (io_fds[SFD_FOLLOWER] != -1) { +#ifdef TIOCSCTTY + if (ioctl(io_fds[SFD_FOLLOWER], TIOCSCTTY, NULL) != 0) + debug_return_bool(false); +#else + /* Set controlling tty by reopening pty follower. */ + int fd = open(follower, O_RDWR); + if (fd == -1) + debug_return_bool(false); + close(fd); +#endif + } + debug_return_bool(true); +} + +/* + * 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, int intercept_fd) +{ + struct monitor_closure mc = { 0 }; + struct command_status cstat; + struct sigaction sa; + int errsock[2]; + debug_decl(exec_monitor, SUDO_DEBUG_EXEC); + + /* Close fds the monitor doesn't use. */ + if (io_fds[SFD_LEADER] != -1) + close(io_fds[SFD_LEADER]); + if (io_fds[SFD_USERTTY] != -1) + close(io_fds[SFD_USERTTY]); + + /* 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); + + /* + * 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(details->tty)) { + sudo_warn("%s", U_("unable to set controlling tty")); + goto bad; + } + + /* + * The child waits on the other end of a socketpair for the + * parent to set the controlling terminal. It also writes + * error to the socket on execve(2) failure. + */ + if (socketpair(PF_UNIX, SOCK_STREAM, 0, errsock) == -1 || + fcntl(errsock[0], F_SETFD, FD_CLOEXEC) == -1 || + fcntl(errsock[1], F_SETFD, FD_CLOEXEC) == -1) { + sudo_warn("%s", U_("unable to create sockets")); + goto bad; + } + + /* + * Before forking, wait for the main sudo process to tell us to go. + * Avoids race conditions when the command exits quickly. + */ + if (recv(backchannel, &cstat, sizeof(cstat), MSG_WAITALL) == -1) { + sudo_warn("%s", U_("unable to receive message from parent")); + goto bad; + } + +#ifdef HAVE_SELINUX + if (ISSET(details->flags, CD_RBAC_ENABLED)) { + if (selinux_relabel_tty(details->tty, io_fds[SFD_FOLLOWER]) == -1) + goto bad; + selinux_audit_role_change(); + } +#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 */ + close(backchannel); + close(errsock[0]); + /* setup tty and exec command */ + exec_cmnd_pty(details, oset, foreground, intercept_fd, errsock[1]); + if (send(errsock[1], &errno, sizeof(int), 0) == -1) + sudo_warn(U_("unable to execute %s"), details->command); + _exit(EXIT_FAILURE); + /* NOTREACHED */ + } + close(errsock[1]); + if (intercept_fd != -1) + close(intercept_fd); + + /* 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, details, &cstat, errsock[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) { + 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); + } + /* Tell the child to go ahead now that it is the foreground pgrp. */ + if (send(errsock[0], "", 1, 0) == -1) { + sudo_warn(U_("unable to execute %s"), details->command); + terminate_command(mc.cmnd_pid, true); + } + } + + /* + * 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); + /* NOTREACHED */ + +bad: + debug_return_int(-1); +} diff --git a/src/exec_nopty.c b/src/exec_nopty.c new file mode 100644 index 0000000..1b1ae6d --- /dev/null +++ b/src/exec_nopty.c @@ -0,0 +1,812 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2023 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/socket.h> +#include <sys/wait.h> +#include <sys/ioctl.h> + +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#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> + +static void handle_sigchld_nopty(struct exec_closure *ec); + +/* + * Handle window size change events. + */ +static void +handle_sigwinch(struct exec_closure *ec, int fd) +{ + struct winsize wsize; + debug_decl(handle_sigwinch, SUDO_DEBUG_EXEC); + + if (fd != -1 && ioctl(fd, TIOCGWINSZ, &wsize) == 0) { + if (wsize.ws_row != ec->rows || wsize.ws_col != ec->cols) { + /* Log window change event. */ + log_winchange(ec, wsize.ws_row, wsize.ws_col); + + /* Update rows/cols. */ + ec->rows = wsize.ws_row; + ec->cols = wsize.ws_col; + } + } +} + +/* 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 *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->backchannel_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 *ec = sc->closure; + char signame[SIG2STR_MAX]; + pid_t si_pgrp; + 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; + case SIGWINCH: + handle_sigwinch(ec, io_fds[SFD_USERTTY]); + FALLTHROUGH; +#ifdef SIGINFO + case SIGINFO: +#endif + case SIGINT: + case SIGQUIT: + case SIGTSTP: + /* + * Only forward user-generated signals not sent by a process other than + * the command itself or a member of the command's process group (but + * only when either sudo or the command is the process group leader). + * Signals sent by the kernel may include SIGTSTP when the user presses + * ^Z. Curses programs often trap ^Z and send SIGTSTP to their own + * process group, so we don't want to send an extra SIGTSTP. + */ + if (!USER_SIGNALED(sc->siginfo)) + debug_return; + if (sc->siginfo->si_pid != 0) { + if (sc->siginfo->si_pid == ec->cmnd_pid) + debug_return; + si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == ec->cmnd_pid || si_pgrp == ec->sudo_pid) + debug_return; + } + } + break; + default: + /* + * Do not forward signals sent by the command itself or a member of the + * command's process group (but only when either sudo or the command is + * the process group leader). 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) { + if (sc->siginfo->si_pid == ec->cmnd_pid) + debug_return; + si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == ec->cmnd_pid || si_pgrp == ec->sudo_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(struct exec_closure *ec, struct command_status *cstat, + struct command_details *details, const struct user_details *user_details, + struct sudo_event_base *evbase, int errfd) +{ + debug_decl(fill_exec_closure, SUDO_DEBUG_EXEC); + + /* Fill in the non-event part of the closure. */ + ec->sudo_pid = getpid(); + ec->ppgrp = getpgrp(); + ec->cstat = cstat; + ec->details = details; + ec->rows = user_details->ts_rows; + ec->cols = user_details->ts_cols; + + /* Setup event base and events. */ + ec->evbase = evbase; + + /* Event for command status via errfd. */ + ec->backchannel_event = sudo_ev_alloc(errfd, + SUDO_EV_READ|SUDO_EV_PERSIST, errpipe_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, "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 + + ec->sigwinch_event = sudo_ev_alloc(SIGWINCH, + SUDO_EV_SIGINFO, signal_cb_nopty, 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")); + + /* Set the default event base. */ + sudo_ev_base_setdef(ec->evbase); + + debug_return; +} + +/* + * 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); + ssize_t n; + debug_decl(read_callback, SUDO_DEBUG_EXEC); + + n = read(fd, iob->buf + iob->len, sizeof(iob->buf) - iob->len); + switch (n) { + case -1: + if (errno == EAGAIN || errno == EINTR) { + /* Not an error, retry later. */ + 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 */ + 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, (unsigned int)n, iob)) { + terminate_command(iob->ec->cmnd_pid, false); + iob->ec->cmnd_pid = -1; + } + iob->len += (unsigned int)n; + /* Disable reader if buffer is full. */ + if (iob->len == sizeof(iob->buf)) + sudo_ev_del(evbase, iob->revent); + /* Enable writer now that there is new 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")); + } + break; + } + + debug_return; +} + +/* + * 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); + ssize_t n; + debug_decl(write_callback, SUDO_DEBUG_EXEC); + + n = write(fd, iob->buf + iob->off, iob->len - iob->off); + if (n == -1) { + switch (errno) { + case EPIPE: + case EBADF: + /* other end of pipe closed */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "unable to write %u 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: + case EAGAIN: + /* Not an error, retry later. */ + 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 += (unsigned int)n; + /* Disable writer and reset the buffer if fully consumed. */ + if (iob->off == iob->len) { + iob->off = iob->len = 0; + sudo_ev_del(evbase, iob->wevent); + /* Forward the EOF from reader to writer. */ + if (iob->revent == NULL) { + safe_close(fd); + ev_free_by_fd(evbase, fd); + } + } + /* + * Enable reader if buffer is not full but avoid reading + * /dev/tty if the command is no longer running. + */ + if (iob->revent != NULL && iob->len != sizeof(iob->buf)) { + if (!USERTTY_EVENT(iob->revent) || iob->ec->cmnd_pid != -1) { + if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + } + + debug_return; +} + +/* + * If std{in,out,err} are not connected to a terminal, interpose + * ourselves using a pipe. Fills in io_pipe[][]. + */ +static void +interpose_pipes(struct exec_closure *ec, const char *tty, int io_pipe[3][2]) +{ + bool interpose[3] = { false, false, false }; + struct stat sb, tty_sbuf, *tty_sb = NULL; + struct plugin_container *plugin; + bool want_winch = false; + debug_decl(interpose_pipes, SUDO_DEBUG_EXEC); + + /* + * Determine whether any of std{in,out,err} or window size changes + * 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; + if (plugin->u.io->version >= SUDO_API_MKVERSION(1, 12)) { + if (plugin->u.io->change_winsize) + want_winch = true; + } + } + + /* + * If stdin, stdout or stderr is not the user's tty and logging is + * enabled, use a pipe to interpose ourselves. + */ + if (tty != NULL && stat(tty, &tty_sbuf) != -1) + tty_sb = &tty_sbuf; + + if (interpose[STDIN_FILENO]) { + if (!fd_matches_tty(STDIN_FILENO, tty_sb, &sb)) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdin not user's tty, creating a pipe"); + 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, read_callback, write_callback, ec); + } + } + if (interpose[STDOUT_FILENO]) { + if (!fd_matches_tty(STDOUT_FILENO, tty_sb, &sb)) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdout not user's tty, creating a pipe"); + 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, read_callback, write_callback, ec); + } + } + if (interpose[STDERR_FILENO]) { + if (!fd_matches_tty(STDERR_FILENO, tty_sb, &sb)) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "stderr not user's 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, read_callback, write_callback, ec); + } + } + if (want_winch) { + /* Need /dev/tty for SIGWINCH handling. */ + io_fds[SFD_USERTTY] = open(_PATH_TTY, O_RDWR); + } +} + +/* + * Execute a command and wait for it to finish. + */ +void +exec_nopty(struct command_details *details, + const struct user_details *user_details, + struct sudo_event_base *evbase, struct command_status *cstat) +{ + int io_pipe[3][2] = { { -1, -1 }, { -1, -1 }, { -1, -1 } }; + int errpipe[2], intercept_sv[2] = { -1, -1 }; + struct exec_closure ec = { 0 }; + sigset_t set, oset; + 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")); + + if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { + if (!ISSET(details->flags, CD_USE_PTRACE)) { + /* + * Allocate a socketpair for communicating with sudo_intercept.so. + * This must be inherited across exec, hence no FD_CLOEXEC. + */ + if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -1) + sudo_fatal("%s", U_("unable to create sockets")); + } + } + + /* Interpose std{in,out,err} with pipes if logging I/O. */ + interpose_pipes(&ec, user_details->tty, io_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_relabel_tty(details->tty, -1) == -1) { + cstat->type = CMD_ERRNO; + cstat->val = errno; + debug_return; + } + selinux_audit_role_change(); + } +#endif + + ec.cmnd_pid = sudo_debug_fork(); + switch (ec.cmnd_pid) { + case -1: + sudo_fatal("%s", U_("unable to fork")); + break; + case 0: + /* child */ + close(errpipe[0]); + if (intercept_sv[0] != -1) + close(intercept_sv[0]); + /* Replace stdin/stdout/stderr with pipes as needed and exec. */ + if (io_pipe[STDIN_FILENO][0] != -1) { + if (dup3(io_pipe[STDIN_FILENO][0], STDIN_FILENO, 0) == -1) + sudo_fatal("dup3"); + close(io_pipe[STDIN_FILENO][0]); + close(io_pipe[STDIN_FILENO][1]); + } + if (io_pipe[STDOUT_FILENO][0] != -1) { + if (dup3(io_pipe[STDOUT_FILENO][1], STDOUT_FILENO, 0) == -1) + sudo_fatal("dup3"); + close(io_pipe[STDOUT_FILENO][0]); + close(io_pipe[STDOUT_FILENO][1]); + } + if (io_pipe[STDERR_FILENO][0] != -1) { + if (dup3(io_pipe[STDERR_FILENO][1], STDERR_FILENO, 0) == -1) + sudo_fatal("dup3"); + close(io_pipe[STDERR_FILENO][0]); + close(io_pipe[STDERR_FILENO][1]); + } + exec_cmnd(details, &oset, intercept_sv[1], 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); + /* NOTREACHED */ + } + sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", details->command, + (int)ec.cmnd_pid); + /* Close the other end of the pipes and socketpairs. */ + 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(errpipe[1]); + if (intercept_sv[1] != -1) + close(intercept_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 error pipe event. + */ + fill_exec_closure(&ec, cstat, details, user_details, evbase, errpipe[0]); + + if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { + int rc = 1; + + /* Create event and closure for intercept mode. */ + ec.intercept = intercept_setup(intercept_sv[0], ec.evbase, details); + if (ec.intercept == NULL) { + rc = -1; + } else if (ISSET(details->flags, CD_USE_PTRACE)) { + /* Try to seize control of the command using ptrace(2). */ + rc = exec_ptrace_seize(ec.cmnd_pid); + if (rc == 0) { + /* There is another tracer present. */ + CLR(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS|CD_USE_PTRACE); + } + } + if (rc == -1) + terminate_command(ec.cmnd_pid, false); + } + + /* Enable any I/O log events. */ + add_io_events(&ec); + + /* 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, false); + 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 + + /* Flush any remaining output. */ + del_io_events(true); + + /* Free things up. */ + free_io_bufs(); + free_exec_closure(&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 *ec) +{ + pid_t pid; + int status; + char signame[SIG2STR_MAX]; + debug_decl(handle_sigchld_nopty, SUDO_DEBUG_EXEC); + + /* There may be multiple children in intercept mode. */ + for (;;) { + do { + pid = waitpid(-1, &status, __WALL|WUNTRACED|WNOHANG); + } while (pid == -1 && errno == EINTR); + switch (pid) { + case -1: + if (errno != ECHILD) { + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + debug_return; + } + FALLTHROUGH; + case 0: + /* Nothing left to wait for. */ + debug_return; + } + + if (WIFSTOPPED(status)) { + const int signo = WSTOPSIG(status); + + if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: process %d stopped, SIG%s", __func__, (int)pid, signame); + + if (ISSET(ec->details->flags, CD_USE_PTRACE)) { + /* If not a group-stop signal, just continue. */ + if (!exec_ptrace_stopped(pid, status, ec->intercept)) + continue; + } + + /* If the main command is suspended, suspend sudo too. */ + if (pid == ec->cmnd_pid) { + sudo_suspend_parent(signo, ec->sudo_pid, ec->ppgrp, + ec->cmnd_pid, ec, log_suspend); + } + } 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: process %d killed, SIG%s", __func__, + (int)pid, signame); + } else if (WIFEXITED(status)) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: process %d exited: %d", __func__, + (int)pid, WEXITSTATUS(status)); + } else { + sudo_debug_printf(SUDO_DEBUG_WARN, + "%s: unexpected wait status 0x%x for process %d", + __func__, status, (int)pid); + } + + /* Only store exit status of the main command. */ + if (pid != ec->cmnd_pid) + continue; + + /* 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; + } + } +} diff --git a/src/exec_preload.c b/src/exec_preload.c new file mode 100644 index 0000000..f463716 --- /dev/null +++ b/src/exec_preload.c @@ -0,0 +1,419 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2022 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 <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <sudo.h> +#include <sudo_exec.h> +#include <sudo_util.h> + +#ifdef RTLD_PRELOAD_VAR +typedef void * (*sudo_alloc_fn_t)(size_t, size_t); +typedef void (*sudo_free_fn_t)(void *); + +static void * +sudo_allocarray(size_t nmemb, size_t size) +{ + return reallocarray(NULL, nmemb, size); +} + +/* + * Allocate space for the string described by fmt and return it, + * or NULL on error. + * Currently only supports %%, %c, %d, and %s escapes. + */ +static char * +fmtstr(sudo_alloc_fn_t alloc_fn, sudo_free_fn_t free_fn, const char * restrict ofmt, ...) +{ + char *cp, *cur, *newstr = NULL; + size_t len, size = 1; + const char *fmt; + va_list ap; + debug_decl(fmtstr, SUDO_DEBUG_UTIL); + + /* Determine size. */ + va_start(ap, ofmt); + for (fmt = ofmt; *fmt != '\0'; ) { + if (fmt[0] == '%') { + switch (fmt[1]) { + case 'c': + (void)va_arg(ap, int); + FALLTHROUGH; + case '%': + size++; + fmt += 2; + continue; + case 's': + cp = va_arg(ap, char *); + size += strlen(cp ? cp : "(NULL)"); + fmt += 2; + continue; + case 'd': { + char numbuf[STRLEN_MAX_SIGNED(int) + 1]; + len = (size_t)snprintf(numbuf, sizeof(numbuf), "%d", + va_arg(ap, int)); + if (len >= sizeof(numbuf)) { + goto oflow; + } + size += len; + fmt += 2; + continue; + } + default: + /* Treat as literal. */ + break; + } + } + size++; + fmt++; + } + va_end(ap); + + newstr = alloc_fn(1, size); + if (newstr == NULL) + debug_return_str(NULL); + + /* Format/copy data. */ + cur = newstr; + va_start(ap, ofmt); + for (fmt = ofmt; *fmt != '\0'; ) { + if (fmt[0] == '%') { + switch (fmt[1]) { + case '%': + if (size < 2) { + goto oflow; + } + *cur++ = '%'; + size--; + fmt += 2; + continue; + case 'c': + if (size < 2) { + goto oflow; + } + *cur++ = (char )va_arg(ap, int); + size--; + fmt += 2; + continue; + case 's': + cp = va_arg(ap, char *); + len = strlcpy(cur, cp ? cp : "(NULL)", size); + if (len >= size) { + goto oflow; + } + cur += len; + size -= len; + fmt += 2; + continue; + case 'd': + len = (size_t)snprintf(cur, size, "%d", va_arg(ap, int)); + if (len >= size) { + goto oflow; + } + cur += len; + size -= len; + fmt += 2; + continue; + default: + /* Treat as literal. */ + break; + } + } + if (size < 2) { + goto oflow; + } + *cur++ = *fmt++; + size++; + } + + if (size < 1) { + goto oflow; + } + *cur = '\0'; + va_end(ap); + + debug_return_str(newstr); + +oflow: + /* We pre-allocate enough space, so this should never happen. */ + va_end(ap); + free_fn(newstr); + sudo_warnx(U_("internal error, %s overflow"), __func__); + debug_return_str(NULL); +} + +/* + * Add a DSO file to LD_PRELOAD or the system equivalent. + */ +static char ** +sudo_preload_dso_alloc(char *const envp[], const char *preload_var, + const char *dso_file, int intercept_fd, + sudo_alloc_fn_t alloc_fn, sudo_free_fn_t free_fn) +{ + const size_t preload_var_len = strlen(preload_var); + char *preload = NULL; + char **nep, **nenvp = NULL; + char *const *ep; + char **preload_ptr = NULL; + char **intercept_ptr = NULL; + char *const empty[1] = { NULL }; + bool fd_present = false; + bool dso_present = false; +# ifdef RTLD_PRELOAD_ENABLE_VAR + bool dso_enabled = false; +# else + const bool dso_enabled = true; +# endif +# ifdef _PATH_ASAN_LIB + char *dso_buf = NULL; +# endif + size_t env_size; + debug_decl(sudo_preload_dso_alloc, SUDO_DEBUG_UTIL); + +# ifdef _PATH_ASAN_LIB + /* + * The address sanitizer DSO needs to be first in the list. + */ + dso_buf = fmtstr(alloc_fn, free_fn, "%s%c%s", _PATH_ASAN_LIB, + RTLD_PRELOAD_DELIM, dso_file); + if (dso_buf == NULL) { + goto oom; + } + dso_file = dso_buf; +# endif + + /* + * 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 + */ + + /* Treat a NULL envp as empty, thanks Linux. */ + if (envp == NULL) + envp = empty; + + /* Determine max size for new envp. */ + for (env_size = 0; envp[env_size] != NULL; env_size++) + continue; + if (!dso_enabled) + env_size++; + if (intercept_fd != -1) + env_size++; + env_size += 2; /* dso_file + terminating NULL */ + + /* Allocate new envp. */ + nenvp = alloc_fn(env_size, sizeof(*nenvp)); + if (nenvp == NULL) + goto oom; + + /* + * Shallow copy envp, with special handling for preload_var, + * RTLD_PRELOAD_ENABLE_VAR and SUDO_INTERCEPT_FD. + */ + for (ep = envp, nep = nenvp; *ep != NULL; ep++) { + if (strncmp(*ep, preload_var, preload_var_len) == 0 && + (*ep)[preload_var_len] == '=') { + const char *cp = *ep + preload_var_len + 1; + const size_t dso_len = strlen(dso_file); + + /* Skip duplicates. */ + if (preload_ptr != NULL) + continue; + + /* + * Check to see if dso_file is already first in the list. + * We don't bother checking for it later in the list. + */ + if (strncmp(cp, dso_file, dso_len) == 0) { + if (cp[dso_len] == '\0' || cp[dso_len] == RTLD_PRELOAD_DELIM) + dso_present = true; + } + + /* Save pointer to LD_PRELOAD variable. */ + preload_ptr = nep; + + goto copy; + } + if (intercept_fd != -1 && strncmp(*ep, "SUDO_INTERCEPT_FD=", + sizeof("SUDO_INTERCEPT_FD=") - 1) == 0) { + const char *cp = *ep + sizeof("SUDO_INTERCEPT_FD=") - 1; + const char *errstr; + int fd; + + /* Skip duplicates. */ + if (intercept_ptr != NULL) + continue; + + fd = (int)sudo_strtonum(cp, 0, INT_MAX, &errstr); + if (fd == intercept_fd && errstr == NULL) + fd_present = true; + + /* Save pointer to SUDO_INTERCEPT_FD variable. */ + intercept_ptr = nep; + + goto copy; + } +# ifdef RTLD_PRELOAD_ENABLE_VAR + if (strncmp(*ep, RTLD_PRELOAD_ENABLE_VAR "=", + sizeof(RTLD_PRELOAD_ENABLE_VAR)) == 0) { + dso_enabled = true; + } +# endif +copy: + *nep++ = *ep; /* shallow copy */ + } + + /* Prepend our LD_PRELOAD to existing value or add new entry at the end. */ + if (!dso_present) { + if (preload_ptr == NULL) { +# ifdef RTLD_PRELOAD_DEFAULT + preload = fmtstr(alloc_fn, free_fn, "%s=%s%c%s", preload_var, + dso_file, RTLD_PRELOAD_DELIM, RTLD_PRELOAD_DEFAULT); + if (preload == NULL) { + goto oom; + } +# else + preload = fmtstr(alloc_fn, free_fn, "%s=%s", preload_var, + dso_file); + if (preload == NULL) { + goto oom; + } +# endif + *nep++ = preload; + } else { + const char *old_val = *preload_ptr + preload_var_len + 1; + preload = fmtstr(alloc_fn, free_fn, "%s=%s%c%s", preload_var, + dso_file, RTLD_PRELOAD_DELIM, old_val); + if (preload == NULL) { + goto oom; + } + *preload_ptr = preload; + } + } +# ifdef RTLD_PRELOAD_ENABLE_VAR + if (!dso_enabled) { + *nenvp++ = RTLD_PRELOAD_ENABLE_VAR "="; + } +# endif + if (!fd_present && intercept_fd != -1) { + char *fdstr = fmtstr(alloc_fn, free_fn, "SUDO_INTERCEPT_FD=%d", + intercept_fd); + if (fdstr == NULL) { + goto oom; + } + if (intercept_ptr != NULL) { + *intercept_ptr = fdstr; + } else { + *nep++ = fdstr; + } + } + + /* NULL terminate nenvp at last. */ + *nep = NULL; + +# ifdef _PATH_ASAN_LIB + free_fn(dso_buf); +# endif + + debug_return_ptr(nenvp); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); +# ifdef _PATH_ASAN_LIB + free_fn(dso_buf); +# endif + free_fn(preload); + free_fn(nenvp); + debug_return_ptr(NULL); +} + +static char ** +sudo_preload_dso_path(char *const envp[], const char *dso_file, + int intercept_fd, sudo_alloc_fn_t alloc_fn, sudo_free_fn_t free_fn) +{ + char **ret = NULL; + const char *ep; + debug_decl(sudo_preload_dso_path, SUDO_DEBUG_UTIL); + + ep = strchr(dso_file, ':'); + if (ep == NULL) { + /* Use default LD_PRELOAD */ + return sudo_preload_dso_alloc(envp, RTLD_PRELOAD_VAR, dso_file, + intercept_fd, alloc_fn, free_fn); + } + + /* Add 32-bit LD_PRELOAD if present. */ + if (ep != dso_file) { +#ifdef RTLD_PRELOAD_VAR_32 + const size_t len = (size_t)(ep - dso_file); + char name[PATH_MAX]; + + if (len >= sizeof(name)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "%.*s: path too long", (int)len, dso_file); + } else { + memcpy(name, dso_file, len); + name[len] = '\0'; + ret = sudo_preload_dso_alloc(envp, RTLD_PRELOAD_VAR_32, name, + intercept_fd, alloc_fn, free_fn); + envp = ret; + } +#endif /* RTLD_PRELOAD_VAR_32 */ + dso_file = ep + 1; + } + +#ifdef RTLD_PRELOAD_VAR_64 + /* Add 64-bit LD_PRELOAD if present. */ + if (*dso_file != '\0') { + char **new_envp = sudo_preload_dso_alloc(envp, RTLD_PRELOAD_VAR_64, + dso_file, intercept_fd, alloc_fn, free_fn); + free_fn(ret); + ret = new_envp; + } +#endif /* RTLD_PRELOAD_VAR_64 */ + + debug_return_ptr(ret); +} + +char ** +sudo_preload_dso_mmap(char *const envp[], const char *dso_file, + int intercept_fd) +{ + return sudo_preload_dso_path(envp, dso_file, intercept_fd, + sudo_mmap_allocarray_v1, sudo_mmap_free_v1); +} + +char ** +sudo_preload_dso(char *const envp[], const char *dso_file, + int intercept_fd) +{ + return sudo_preload_dso_path(envp, dso_file, intercept_fd, + sudo_allocarray, free); +} +#endif /* RTLD_PRELOAD_VAR */ diff --git a/src/exec_ptrace.c b/src/exec_ptrace.c new file mode 100644 index 0000000..b8a0789 --- /dev/null +++ b/src/exec_ptrace.c @@ -0,0 +1,2135 @@ +/* + * Copyright (c) 2022 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/types.h> +#include <sys/uio.h> +#include <sys/wait.h> + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#if defined(HAVE_ENDIAN_H) +# include <endian.h> +#elif defined(HAVE_SYS_ENDIAN_H) +# include <sys/endian.h> +#elif defined(HAVE_MACHINE_ENDIAN_H) +# include <machine/endian.h> +#else +# include <compat/endian.h> +#endif + +#include <sudo.h> +#include <sudo_exec.h> + +#ifdef HAVE_PTRACE_INTERCEPT +# include <exec_intercept.h> +# include <exec_ptrace.h> + +/* We need to take care when ptracing 32-bit binaries on 64-bit kernels. */ +# ifdef __LP64__ +# define COMPAT_FLAG 0x01 +# else +# define COMPAT_FLAG 0x00 +# endif + +static int seccomp_trap_supported = -1; +#ifdef HAVE_PROCESS_VM_READV +static size_t page_size; +#endif +static size_t arg_max; + +/* Register getters and setters. */ +# ifdef SECCOMP_AUDIT_ARCH_COMPAT +static inline unsigned long +get_stack_pointer(struct sudo_ptrace_regs *regs) +{ + if (regs->compat) { + return compat_reg_sp(regs->u.compat); + } else { + return reg_sp(regs->u.native); + } +} + +static inline void +set_sc_retval(struct sudo_ptrace_regs *regs, int retval) +{ + if (regs->compat) { + compat_reg_set_retval(regs->u.compat, retval); + } else { + reg_set_retval(regs->u.native, retval); + } +} + +static inline int +get_syscallno(struct sudo_ptrace_regs *regs) +{ + if (regs->compat) { + return compat_reg_syscall(regs->u.compat); + } else { + return reg_syscall(regs->u.native); + } +} + +static inline void +set_syscallno(pid_t pid, struct sudo_ptrace_regs *regs, int syscallno) +{ + if (regs->compat) { + compat_reg_set_syscall(regs->u.compat, syscallno); + } else { + reg_set_syscall(regs->u.native, syscallno); + } +} + +static inline unsigned long +get_sc_arg1(struct sudo_ptrace_regs *regs) +{ + if (regs->compat) { + return compat_reg_arg1(regs->u.compat); + } else { + return reg_arg1(regs->u.native); + } +} + +static inline void +set_sc_arg1(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + if (regs->compat) { + compat_reg_set_arg1(regs->u.compat, addr); + } else { + reg_set_arg1(regs->u.native, addr); + } +} + +static inline unsigned long +get_sc_arg2(struct sudo_ptrace_regs *regs) +{ + if (regs->compat) { + return compat_reg_arg2(regs->u.compat); + } else { + return reg_arg2(regs->u.native); + } +} + +static inline void +set_sc_arg2(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + if (regs->compat) { + compat_reg_set_arg2(regs->u.compat, addr); + } else { + reg_set_arg2(regs->u.native, addr); + } +} + +static inline unsigned long +get_sc_arg3(struct sudo_ptrace_regs *regs) +{ + if (regs->compat) { + return compat_reg_arg3(regs->u.compat); + } else { + return reg_arg3(regs->u.native); + } +} + +# ifdef notyet +static inline void +set_sc_arg3(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + if (regs->compat) { + compat_reg_set_arg3(regs->u.compat, addr); + } else { + reg_set_arg3(regs->u.native, addr); + } +} + +static inline unsigned long +get_sc_arg4(struct sudo_ptrace_regs *regs) +{ + if (regs->compat) { + return compat_reg_arg4(regs->u.compat); + } else { + return reg_arg4(regs->u.native); + } +} + +static inline void +set_sc_arg4(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + if (regs->compat) { + compat_reg_set_arg4(regs->u.compat, addr); + } else { + reg_set_arg4(regs->u.native, addr); + } +} +# endif /* notyet */ + +# else /* SECCOMP_AUDIT_ARCH_COMPAT */ + +static inline unsigned long +get_stack_pointer(struct sudo_ptrace_regs *regs) +{ + return reg_sp(regs->u.native); +} + +static inline void +set_sc_retval(struct sudo_ptrace_regs *regs, int retval) +{ + reg_set_retval(regs->u.native, retval); +} + +static inline int +get_syscallno(struct sudo_ptrace_regs *regs) +{ + return reg_syscall(regs->u.native); +} + +static inline void +set_syscallno(pid_t pid, struct sudo_ptrace_regs *regs, int syscallno) +{ + reg_set_syscall(regs->u.native, syscallno); +} + +static inline unsigned long +get_sc_arg1(struct sudo_ptrace_regs *regs) +{ + return reg_arg1(regs->u.native); +} + +static inline void +set_sc_arg1(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + reg_set_arg1(regs->u.native, addr); +} + +static inline unsigned long +get_sc_arg2(struct sudo_ptrace_regs *regs) +{ + return reg_arg2(regs->u.native); +} + +static inline void +set_sc_arg2(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + reg_set_arg2(regs->u.native, addr); +} + +static inline unsigned long +get_sc_arg3(struct sudo_ptrace_regs *regs) +{ + return reg_arg3(regs->u.native); +} + +# ifdef notyet +static inline void +set_sc_arg3(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + reg_set_arg3(regs->u.native, addr); +} + +static inline unsigned long +get_sc_arg4(struct sudo_ptrace_regs *regs) +{ + return reg_arg4(regs->u.native); +} + +static inline void +set_sc_arg4(struct sudo_ptrace_regs *regs, unsigned long addr) +{ + reg_set_arg4(regs->u.native, addr); +} +# endif /* notyet */ +# endif /* SECCOMP_AUDIT_ARCH_COMPAT */ + +/* + * Get the registers for the given process and store in regs, which + * must be large enough. If the compat flag is set, pid is expected + * to refer to a 32-bit process and the md parameters will be filled + * in accordingly. + * Returns true on success, else false. + */ +static bool +ptrace_getregs(int pid, struct sudo_ptrace_regs *regs, int compat) +{ + struct iovec iov; + debug_decl(ptrace_getregs, SUDO_DEBUG_EXEC); + + iov.iov_base = ®s->u; + iov.iov_len = sizeof(regs->u); + +# ifdef __mips__ + /* PTRACE_GETREGSET has bugs with the MIPS o32 ABI at least. */ + if (ptrace(PTRACE_GETREGS, pid, NULL, iov.iov_base) == -1) + debug_return_bool(false); +# else + if (ptrace(PTRACE_GETREGSET, pid, (void *)NT_PRSTATUS, &iov) == -1) + debug_return_bool(false); +# endif /* __mips__ */ + if (compat == -1) { +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + if (sizeof(regs->u.native) != sizeof(regs->u.compat)) { + /* Guess compat based on size of register struct returned. */ + compat = iov.iov_len != sizeof(regs->u.native); + } else { + /* Assume a 64-bit executable will have a 64-bit stack pointer. */ + compat = reg_sp(regs->u.native) < 0xffffffff; + } +# else + compat = false; +# endif /* SECCOMP_AUDIT_ARCH_COMPAT */ + } + + /* Machine-dependent parameters to support compat binaries. */ + if (compat) { + regs->compat = true; + regs->wordsize = sizeof(int); + } else { + regs->compat = false; + regs->wordsize = sizeof(long); + } + + debug_return_bool(true); +} + +/* + * Set the registers, specified by regs, for the given process. + * Returns true on success, else false. + */ +static bool +ptrace_setregs(int pid, struct sudo_ptrace_regs *regs) +{ + debug_decl(ptrace_setregs, SUDO_DEBUG_EXEC); + +# ifdef __mips__ + /* PTRACE_SETREGSET has bugs with the MIPS o32 ABI at least. */ + if (ptrace(PTRACE_SETREGS, pid, NULL, ®s->u) == -1) + debug_return_bool(false); +# else + struct iovec iov; + iov.iov_base = ®s->u; + iov.iov_len = sizeof(regs->u); + if (ptrace(PTRACE_SETREGSET, pid, (void *)NT_PRSTATUS, &iov) == -1) + debug_return_bool(false); +# endif /* __mips__ */ + + debug_return_bool(true); +} + +#ifdef HAVE_PROCESS_VM_READV +/* + * Read the string at addr and store in buf using process_vm_readv(2). + * Returns the number of bytes stored, including the NUL. + */ +static ssize_t +ptrace_readv_string(pid_t pid, unsigned long addr, char *buf, size_t bufsize) +{ + const char *cp, *buf0 = buf; + struct iovec local, remote; + ssize_t nread; + debug_decl(ptrace_read_string, SUDO_DEBUG_EXEC); + + /* + * Read the string via process_vm_readv(2) one page at a time. + * We could do larger reads but since we don't know the length + * of the string, going one page at a time is simplest. + */ + for (;;) { + if (bufsize == 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d: out of space reading string", __func__, (int)pid); + errno = ENOSPC; + debug_return_ssize_t(-1); + } + + local.iov_base = buf; + local.iov_len = bufsize; + remote.iov_base = (void *)addr; + remote.iov_len = MIN(bufsize, page_size); + + nread = process_vm_readv(pid, &local, 1, &remote, 1, 0); + switch (nread) { + case -1: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "process_vm_readv(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0)", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len); + debug_return_ssize_t(-1); + case 0: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "process_vm_readv(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0): %s", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len, "premature EOF"); + debug_return_ssize_t(-1); + default: + /* Check for NUL terminator in page. */ + cp = memchr(buf, '\0', (size_t)nread); + if (cp != NULL) + debug_return_ssize_t((cp - buf0) + 1); /* includes NUL */ + buf += nread; + bufsize -= (size_t)nread; + addr += sizeof(unsigned long); + break; + } + } + debug_return_ssize_t(-1); +} +#endif /* HAVE_PROCESS_VM_READV */ + +/* + * Read the string at addr and store in buf using ptrace(2). + * Returns the number of bytes stored, including the NUL. + */ +static ssize_t +ptrace_read_string(pid_t pid, unsigned long addr, char *buf, size_t bufsize) +{ + const char *cp, *buf0 = buf; + unsigned long word; + size_t i; + debug_decl(ptrace_read_string, SUDO_DEBUG_EXEC); + +#ifdef HAVE_PROCESS_VM_READV + ssize_t nread = ptrace_readv_string(pid, addr, buf, bufsize); + if (nread != -1 || errno != ENOSYS) + debug_return_ssize_t(nread); +#endif /* HAVE_PROCESS_VM_READV */ + + /* + * Read the string via ptrace(2) one (native) word at a time. + * We use the native word size even in compat mode because that + * is the unit ptrace(2) uses. + */ + for (;;) { + word = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); + if (word == (unsigned long)-1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "ptrace(PTRACE_PEEKDATA, %d, 0x%lx, NULL)", (int)pid, addr); + debug_return_ssize_t(-1); + } + + cp = (char *)&word; + for (i = 0; i < sizeof(unsigned long); i++) { + if (bufsize == 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d: out of space reading string", __func__, (int)pid); + errno = ENOSPC; + debug_return_ssize_t(-1); + } + *buf = cp[i]; + if (*buf++ == '\0') + debug_return_ssize_t(buf - buf0); + bufsize--; + } + addr += sizeof(unsigned long); + } +} + +/* + * Expand buf by doubling its size. + * Updates bufp and bufsizep and recalculates curp and remp if non-NULL. + * Returns true on success, else false. + */ +static bool +growbuf(char **bufp, size_t *bufsizep, char **curp, size_t *remp) +{ + const size_t oldsize = *bufsizep; + char *newbuf; + debug_decl(growbuf, SUDO_DEBUG_EXEC); + + /* Double the size of the buffer. */ + newbuf = reallocarray(*bufp, 2, oldsize); + if (newbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + if (curp != NULL) + *curp = newbuf + (*curp - *bufp); + if (remp != NULL) + *remp += oldsize; + *bufp = newbuf; + *bufsizep = 2 * oldsize; + debug_return_bool(true); +} + +/* + * Build a NULL-terminated string vector from a string table. + * On success, returns number of bytes used for the vector and sets + * vecp to the start of the vector and countp to the number of elements + * (not including the NULL). The buffer is resized as needed. + * Both vecp and its elements are stored as offsets into buf, not pointers. + * However, NULL is still stored as NULL. + * Returns (size_t)-1 on failure. + */ +static ssize_t +strtab_to_vec(char *strtab, size_t strtab_len, int *countp, char ***vecp, + char **bufp, size_t *bufsizep, size_t remainder) +{ + char *strend = strtab + strtab_len; + char **vec, **vp; + int count = 0; + debug_decl(strtab_to_vec, SUDO_DEBUG_EXEC); + + /* Store vector in buf after string table and make it aligned. */ + while (remainder < 2 * sizeof(char *)) { + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + strend = strtab + strtab_len; + } + vec = (char **)LONGALIGN(strend); + remainder -= (size_t)((char *)vec - strend); + + /* Fill in vector with the strings we read. */ + for (vp = vec; strtab < strend; ) { + while (remainder < 2 * sizeof(char *)) { + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + strend = strtab + strtab_len; + vec = (char **)LONGALIGN(strend); + vp = vec + count; + } + /* Store offset into buf (not a pointer) in case of realloc(). */ + *vp++ = (char *)(strtab - *bufp); + remainder -= sizeof(char *); + strtab = memchr(strtab, '\0', (size_t)(strend - strtab)); + if (strtab == NULL) + break; + strtab++; + count++; + } + *vp++ = NULL; /* we always leave room for NULL */ + + *countp = count; + *vecp = (char **)((char *)vec - *bufp); + + debug_return_ssize_t((char *)vp - strend); +} + +/* + * Read the string vector at addr and store it in bufp, which + * is reallocated as needed. The actual vector is returned in vecp. + * The count stored in countp does not include the terminating NULL pointer. + * The vecp and its contents are _offsets_, not pointers, in case the buffer + * gets reallocated later. The caller is responsible for converting the + * offsets into pointers based on the buffer before using. + * Returns the number of bytes in buf consumed (including NULs). + */ +static ssize_t +ptrace_read_vec(pid_t pid, struct sudo_ptrace_regs *regs, unsigned long addr, + int *countp, char ***vecp, char **bufp, size_t *bufsizep, size_t off) +{ +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + unsigned long next_word = (unsigned long)-1; +# endif + size_t strtab_len, remainder = *bufsizep - off; + char *strtab = *bufp + off; + unsigned long word; + ssize_t len; + debug_decl(ptrace_read_vec, SUDO_DEBUG_EXEC); + + /* Treat a NULL vector as empty, thanks Linux. */ + if (addr == 0) { + char **vp; + + while (remainder < 2 * sizeof(char *)) { + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + } + vp = (char **)LONGALIGN(strtab); + *vecp = (char **)((char *)vp - *bufp); + *countp = 0; + *vp++ = NULL; + debug_return_ssize_t((char *)vp - strtab); + } + + /* Fill in string table. */ + do { +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + if (next_word == (unsigned long)-1) { + word = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); + if (regs->compat) { + /* Stash the next compat word in next_word. */ +# if BYTE_ORDER == BIG_ENDIAN + next_word = word & 0xffffffffU; + word >>= 32; +# else + next_word = word >> 32; + word &= 0xffffffffU; +# endif + } + } else { + /* Use the stashed value of the next word. */ + word = next_word; + next_word = (unsigned long)-1; + } +# else /* SECCOMP_AUDIT_ARCH_COMPAT */ + word = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); +# endif /* SECCOMP_AUDIT_ARCH_COMPAT */ + switch (word) { + case -1: + sudo_warn("%s: ptrace(PTRACE_PEEKDATA, %d, 0x%lx, NULL)", + __func__, (int)pid, addr); + debug_return_ssize_t(-1); + case 0: + /* NULL terminator */ + break; + default: + for (;;) { + len = ptrace_read_string(pid, word, strtab, remainder); + if (len != -1) + break; + if (errno != ENOSPC) + debug_return_ssize_t(-1); + if (!growbuf(bufp, bufsizep, &strtab, &remainder)) + debug_return_ssize_t(-1); + } + strtab += len; + remainder -= (size_t)len; + addr += regs->wordsize; + continue; + } + } while (word != 0); + + /* Store strings in a vector after the string table. */ + strtab_len = (size_t)(strtab - (*bufp + off)); + strtab = *bufp + off; + len = strtab_to_vec(strtab, strtab_len, countp, vecp, bufp, bufsizep, + remainder); + if (len == -1) + debug_return_ssize_t(-1); + + debug_return_ssize_t((ssize_t)strtab_len + len); +} + +#ifdef HAVE_PROCESS_VM_READV +/* + * Write the NUL-terminated string str to addr in the tracee using + * process_vm_writev(2). + * Returns the number of bytes written, including trailing NUL. + */ +static ssize_t +ptrace_writev_string(pid_t pid, unsigned long addr, const char *str0) +{ + const char *str = str0; + size_t len = strlen(str) + 1; + debug_decl(ptrace_writev_string, SUDO_DEBUG_EXEC); + + /* + * Write the string via process_vm_writev(2), handling partial writes. + */ + for (;;) { + struct iovec local, remote; + ssize_t nwritten; + + local.iov_base = (void *)str; + local.iov_len = len; + remote.iov_base = (void *)addr; + remote.iov_len = len; + + nwritten = process_vm_writev(pid, &local, 1, &remote, 1, 0); + switch (nwritten) { + case -1: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "process_vm_writev(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0)", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len); + debug_return_ssize_t(-1); + case 0: + /* Should not be possible. */ + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "process_vm_writev(%d, [0x%lx, %zu], 1, [0x%lx, %zu], 1, 0): %s", + (int)pid, (unsigned long)local.iov_base, local.iov_len, + (unsigned long)remote.iov_base, remote.iov_len, + "zero bytes written"); + debug_return_ssize_t(-1); + default: + str += nwritten; + len -= (size_t)nwritten; + addr += (size_t)nwritten; + if (len == 0) + debug_return_ssize_t(str - str0); /* includes NUL */ + break; + } + } + debug_return_ssize_t(-1); +} +#endif /* HAVE_PROCESS_VM_READV */ + +/* + * Write the NUL-terminated string str to addr in the tracee using ptrace(2). + * Returns the number of bytes written, including trailing NUL. + */ +static ssize_t +ptrace_write_string(pid_t pid, unsigned long addr, const char *str) +{ + const char *str0 = str; + size_t i; + union { + unsigned long word; + char buf[sizeof(unsigned long)]; + } u; + debug_decl(ptrace_write_string, SUDO_DEBUG_EXEC); + +#ifdef HAVE_PROCESS_VM_READV + ssize_t nwritten = ptrace_writev_string(pid, addr, str); + if (nwritten != -1 || errno != ENOSYS) + debug_return_ssize_t(nwritten); +#endif /* HAVE_PROCESS_VM_READV */ + + /* + * Write the string via ptrace(2) one (native) word at a time. + * We use the native word size even in compat mode because that + * is the unit ptrace(2) writes in terms of. + */ + for (;;) { + for (i = 0; i < sizeof(u.buf); i++) { + if (*str == '\0') { + /* NUL-pad buf to sizeof(unsigned long). */ + u.buf[i] = '\0'; + continue; + } + u.buf[i] = *str++; + } + if (ptrace(PTRACE_POKEDATA, pid, addr, u.word) == -1) { + sudo_warn("%s: ptrace(PTRACE_POKEDATA, %d, 0x%lx, %.*s)", + __func__, (int)pid, addr, (int)sizeof(u.buf), u.buf); + debug_return_ssize_t(-1); + } + if ((u.word & 0xff) == 0) { + /* If the last byte we wrote is a NUL we are done. */ + debug_return_ssize_t(str - str0 + 1); + } + addr += sizeof(unsigned long); + } +} + +#ifdef HAVE_PROCESS_VM_READV +/* + * Write the string vector vec to addr in the tracee which must have + * sufficient space. Strings are written to strtab. + * Returns the number of bytes used in strtab (including NULs). + * process_vm_writev() version. + */ +static ssize_t +ptrace_writev_vec(pid_t pid, struct sudo_ptrace_regs *regs, char **vec, + unsigned long addr, unsigned long strtab) +{ + const unsigned long addr0 = addr; + const unsigned long strtab0 = strtab; + unsigned long *addrbuf = NULL; + struct iovec *local, *remote; + struct iovec local_addrs, remote_addrs; + size_t i, j, len, off = 0; + ssize_t expected = -1, nwritten, total_written = 0; + debug_decl(ptrace_writev_vec, SUDO_DEBUG_EXEC); + + /* Build up local and remote iovecs for process_vm_writev(2). */ + for (len = 0; vec[len] != NULL; len++) + continue; + local = reallocarray(NULL, len, sizeof(struct iovec)); + remote = reallocarray(NULL, len, sizeof(struct iovec)); + j = regs->compat && (len & 1) != 0; /* pad for final NULL in compat */ + addrbuf = reallocarray(NULL, len + 1 + j, regs->wordsize); + if (local == NULL || remote == NULL || addrbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + for (i = 0, j = 0; i < len; i++) { + unsigned long word = strtab; + + /* Store remote string. */ + const size_t size = strlen(vec[i]) + 1; + local[i].iov_base = vec[i]; + local[i].iov_len = size; + remote[i].iov_base = (void *)strtab; + remote[i].iov_len = size; + strtab += size; + + /* Store address of remote string. */ +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + if (regs->compat) { + /* + * For compat binaries we need to pack two 32-bit string addresses + * into a single 64-bit word. If this is the last string, NULL + * will be written as the second 32-bit address. + */ + if ((i & 1) == 1) { + /* Wrote this string address last iteration. */ + continue; + } +# if BYTE_ORDER == BIG_ENDIAN + word <<= 32; + if (vec[i + 1] != NULL) + word |= strtab; +# else + if (vec[i + 1] != NULL) + word |= strtab << 32; +# endif + } +# endif + addrbuf[j++] = word; + addr += sizeof(unsigned long); + } + if (!regs->compat || (len & 1) == 0) { + addrbuf[j] = 0; + } + + /* Write strings addresses to addr0 on remote. */ + local_addrs.iov_base = addrbuf; + local_addrs.iov_len = (len + 1) * regs->wordsize; + remote_addrs.iov_base = (void *)addr0; + remote_addrs.iov_len = local_addrs.iov_len; + if (process_vm_writev(pid, &local_addrs, 1, &remote_addrs, 1, 0) == -1) + goto done; + + /* Copy the strings to the (remote) string table. */ + expected = (ssize_t)(strtab - strtab0); + for (;;) { + nwritten = process_vm_writev(pid, local + off, len - off, + remote + off, len - off, 0); + switch (nwritten) { + case -1: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "process_vm_writev(%d, 0x%lx, %zu, 0x%lx, %zu, 0)", + (int)pid, (unsigned long)local + off, len - off, + (unsigned long)remote + off, len - off); + goto done; + case 0: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "process_vm_writev(%d, 0x%lx, %zu, 0x%lx, %zu, 0): %s", + (int)pid, (unsigned long)local + off, len - off, + (unsigned long)remote + off, len - off, + "zero bytes written"); + goto done; + default: + total_written += nwritten; + if (total_written >= expected) + goto done; + + /* Adjust offset for partial write (doesn't cross iov boundary). */ + while (off < len) { + nwritten -= (ssize_t)local[off].iov_len; + off++; + if (nwritten <= 0) + break; + } + if (off == len) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "overflow while resuming process_vm_writev()"); + goto done; + } + break; + } + } +done: + free(local); + free(remote); + free(addrbuf); + if (total_written == expected) + debug_return_ssize_t(total_written); + debug_return_ssize_t(-1); +} +#endif /* HAVE_PROCESS_VM_READV */ + +/* + * Write the string vector vec to addr in the tracee which must have + * sufficient space. Strings are written to strtab. + * Returns the number of bytes used in strtab (including NULs). + */ +static ssize_t +ptrace_write_vec(pid_t pid, struct sudo_ptrace_regs *regs, char **vec, + unsigned long addr, unsigned long strtab) +{ + const unsigned long strtab0 = strtab; + size_t i; + debug_decl(ptrace_write_vec, SUDO_DEBUG_EXEC); + +#ifdef HAVE_PROCESS_VM_READV + ssize_t nwritten = ptrace_writev_vec(pid, regs, vec, addr, strtab); + if (nwritten != -1 || errno != ENOSYS) + debug_return_ssize_t(nwritten); +#endif /* HAVE_PROCESS_VM_READV */ + + /* Copy string vector into tracee one word at a time. */ + for (i = 0; vec[i] != NULL; i++) { + unsigned long word = strtab; + + /* First write the actual string to tracee's string table. */ + nwritten = ptrace_write_string(pid, strtab, vec[i]); + if (nwritten == -1) + debug_return_ssize_t(-1); + strtab += (size_t)nwritten; + +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + if (regs->compat) { + /* + * For compat binaries we need to pack two 32-bit string addresses + * into a single 64-bit word. If this is the last string, NULL + * will be written as the second 32-bit address. + */ + if ((i & 1) == 1) { + /* Wrote this string address last iteration. */ + continue; + } +# if BYTE_ORDER == BIG_ENDIAN + word <<= 32; + if (vec[i + 1] != NULL) + word |= strtab; +# else + if (vec[i + 1] != NULL) + word |= strtab << 32; +# endif + } +# endif + /* Next write the string address to tracee at addr. */ + if (ptrace(PTRACE_POKEDATA, pid, addr, word) == -1) { + sudo_warn("%s: ptrace(PTRACE_POKEDATA, %d, 0x%lx, 0x%lx)", + __func__, (int)pid, addr, word); + debug_return_ssize_t(-1); + } + addr += sizeof(unsigned long); + } + + /* Finally, write the terminating NULL to tracee if needed. */ + if (!regs->compat || (i & 1) == 0) { + if (ptrace(PTRACE_POKEDATA, pid, addr, NULL) == -1) { + sudo_warn("%s: ptrace(PTRACE_POKEDATA, %d, 0x%lx, NULL)", + __func__, (int)pid, addr); + debug_return_ssize_t(-1); + } + } + + debug_return_ssize_t((ssize_t)(strtab - strtab0)); +} + +/* + * Read a link from /proc/PID and store the result in buf. + * Used to read the cwd and exe links in /proc/PID. + * Returns true on success, else false. + */ +static bool +proc_read_link(pid_t pid, const char *name, char *buf, size_t bufsize) +{ + ssize_t len; + char path[PATH_MAX]; + debug_decl(proc_read_link, SUDO_DEBUG_EXEC); + + len = snprintf(path, sizeof(path), "/proc/%d/%s", (int)pid, name); + if (len > 0 && len < ssizeof(path)) { + len = readlink(path, buf, bufsize - 1); + if (len != -1) { + /* readlink(2) does not add the NUL for us. */ + buf[len] = '\0'; + debug_return_bool(true); + } + } + debug_return_bool(false); +} + +/* + * Read the filename, argv and envp of the execve(2) system call. + * Returns a dynamically allocated buffer the parent is responsible for. + */ +static char * +get_execve_info(pid_t pid, struct sudo_ptrace_regs *regs, char **pathname_out, + int *argc_out, char ***argv_out, int *envc_out, char ***envp_out) +{ + char *argbuf, **argv, **envp, *pathname = NULL; + unsigned long argv_addr, envp_addr, path_addr; + size_t bufsize, off = 0; + int i, argc, envc = 0; + ssize_t nread; + debug_decl(get_execve_info, SUDO_DEBUG_EXEC); + + bufsize = PATH_MAX + arg_max; + argbuf = malloc(bufsize); + if (argbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + + /* execve(2) takes three arguments: pathname, argv, envp. */ + path_addr = get_sc_arg1(regs); + argv_addr = get_sc_arg2(regs); + envp_addr = get_sc_arg3(regs); + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d: path 0x%lx, argv 0x%lx, envp 0x%lx", __func__, + (int)pid, path_addr, argv_addr, envp_addr); + + /* Read the pathname, if not NULL. */ + if (path_addr != 0) { + nread = ptrace_read_string(pid, path_addr, argbuf, bufsize); + if (nread == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve pathname for process %d", (int)pid); + goto bad; + } + /* Defer setting pathname until after all reallocations are done. */ + off = (size_t)nread; + } + + /* Read argv */ + nread = ptrace_read_vec(pid, regs, argv_addr, &argc, &argv, &argbuf, + &bufsize, off); + if (nread == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve argv for process %d", (int)pid); + goto bad; + } + off += (size_t)nread; + + if (argc == 0) { + /* Reserve an extra slot so we can store argv[0]. */ + while (bufsize - off < sizeof(char *)) { + if (!growbuf(&argbuf, &bufsize, NULL, NULL)) + goto bad; + } + off += sizeof(char *); + } + + /* Read envp */ + nread = ptrace_read_vec(pid, regs, envp_addr, &envc, &envp, &argbuf, + &bufsize, off); + if (nread == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve envp for process %d", (int)pid); + goto bad; + } + + /* Set pathname now that argbuf has been fully allocated. */ + if (path_addr != 0) + pathname = argbuf; + + /* Convert offsets in argv and envp to pointers. */ + argv = (char **)(argbuf + (unsigned long)argv); + for (i = 0; i < argc; i++) { + argv[i] = argbuf + (unsigned long)argv[i]; + } + envp = (char **)(argbuf + (unsigned long)envp); + for (i = 0; i < envc; i++) { + envp[i] = argbuf + (unsigned long)envp[i]; + } + + sudo_debug_execve(SUDO_DEBUG_DIAG, pathname, argv, envp); + + *pathname_out = pathname; + *argc_out = argc; + *argv_out = argv; + *envc_out = envc; + *envp_out = envp; + + debug_return_ptr(argbuf); +bad: + free(argbuf); + debug_return_ptr(NULL); +} + +/* + * Cause the current syscall to fail and set the error value to ecode. + */ +static bool +ptrace_fail_syscall(pid_t pid, struct sudo_ptrace_regs *regs, int ecode) +{ + sigset_t chldmask; + bool ret = false; + int status; + debug_decl(ptrace_fail_syscall, SUDO_DEBUG_EXEC); + + /* Cause the syscall to fail by changing its number to -1. */ + set_syscallno(pid, regs, -1); + if (!ptrace_setregs(pid, regs)) { + sudo_warn(U_("unable to set registers for process %d"), (int)pid); + debug_return_bool(false); + } + + /* Block SIGCHLD for the critical section (waitpid). */ + sigemptyset(&chldmask); + sigaddset(&chldmask, SIGCHLD); + sigprocmask(SIG_BLOCK, &chldmask, NULL); + + /* Allow the syscall to continue and change return value to ecode. */ + ptrace(PTRACE_SYSCALL, pid, NULL, NULL); + for (;;) { + if (waitpid(pid, &status, __WALL) != -1) + break; + if (errno == EINTR) + continue; + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + goto done; + } + if (!WIFSTOPPED(status)) { + sudo_warnx(U_("process %d exited unexpectedly"), (int)pid); + goto done; + } + set_sc_retval(regs, -ecode); + if (!ptrace_setregs(pid, regs)) { + sudo_warn(U_("unable to set registers for process %d"), (int)pid); + goto done; + } + + ret = true; + +done: + sigprocmask(SIG_UNBLOCK, &chldmask, NULL); + + debug_return_bool(ret); +} + +/* + * Check whether seccomp(2) filtering supports ptrace(2) traps. + * Only supported by Linux 4.14 and higher. + */ +static bool +have_seccomp_action(const char *action) +{ + char line[LINE_MAX]; + bool ret = false; + FILE *fp; + debug_decl(have_seccomp_action, SUDO_DEBUG_EXEC); + + fp = fopen("/proc/sys/kernel/seccomp/actions_avail", "r"); + if (fp != NULL) { + if (fgets(line, sizeof(line), fp) != NULL) { + char *cp, *last; + + for ((cp = strtok_r(line, " \t\n", &last)); cp != NULL; + (cp = strtok_r(NULL, " \t\n", &last))) { + if (strcmp(cp, action) == 0) { + ret = true; + break; + } + } + } + fclose(fp); + } + debug_return_bool(ret); +} + +/* + * Intercept execve(2) and execveat(2) using seccomp(2) and ptrace(2). + * If no tracer is present, execve(2) and execveat(2) will fail with ENOSYS. + * Must be called with CAP_SYS_ADMIN, before privs are dropped. + */ +bool +set_exec_filter(void) +{ + struct sock_filter exec_filter[] = { + /* Load architecture value (AUDIT_ARCH_*) into the accumulator. */ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)), +# ifdef SECCOMP_AUDIT_ARCH_COMPAT2 + /* Match on the compat2 architecture or jump to the compat check. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SECCOMP_AUDIT_ARCH_COMPAT2, 0, 4), + /* Load syscall number into the accumulator. */ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + /* Jump to trace for compat2 execve(2)/execveat(2), else allow. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, COMPAT2_execve, 1, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, COMPAT2_execveat, 0, 13), + /* Trace execve(2)/execveat(2) syscalls (w/ compat flag) */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE | COMPAT_FLAG), +# endif /* SECCOMP_AUDIT_ARCH_COMPAT2 */ +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + /* Match on the compat architecture or jump to the native arch check. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SECCOMP_AUDIT_ARCH_COMPAT, 0, 4), + /* Load syscall number into the accumulator. */ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + /* Jump to trace for compat execve(2)/execveat(2), else allow. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, COMPAT_execve, 1, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, COMPAT_execveat, 0, 8), + /* Trace execve(2)/execveat(2) syscalls (w/ compat flag) */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE | COMPAT_FLAG), +# endif /* SECCOMP_AUDIT_ARCH_COMPAT */ + /* Jump to the end unless the architecture matches. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SECCOMP_AUDIT_ARCH, 0, 6), + /* Load syscall number into the accumulator. */ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + /* Jump to trace for execve(2)/execveat(2), else allow. */ +# ifdef X32_execve + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, X32_execve, 3, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, X32_execveat, 2, 0), +# else + /* No x32 support, check native system call numbers. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 3, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 2, 3), +# endif /* X32_execve */ + /* If no x32 support, these two instructions are never reached. */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 1, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 0, 1), + /* Trace execve(2)/execveat(2) syscalls */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE), + /* Allow non-matching syscalls */ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW) + }; + const struct sock_fprog exec_fprog = { + nitems(exec_filter), + exec_filter + }; + debug_decl(set_exec_filter, SUDO_DEBUG_EXEC); + + /* We must set SECCOMP_MODE_FILTER before dropping privileges. */ + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &exec_fprog) == -1) { + sudo_warn("%s", U_("unable to set seccomp filter")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * Seize control of the specified child process which must be in + * ptrace wait. Returns true on success, false if child is already + * being traced and -1 on error. + */ +int +exec_ptrace_seize(pid_t child) +{ + const long ptrace_opts = PTRACE_O_TRACESECCOMP|PTRACE_O_TRACECLONE| + PTRACE_O_TRACEFORK|PTRACE_O_TRACEVFORK| + PTRACE_O_TRACEEXEC; + int ret = -1; + int status; + debug_decl(exec_ptrace_seize, SUDO_DEBUG_EXEC); + +#ifdef HAVE_PROCESS_VM_READV + page_size = (size_t)sysconf(_SC_PAGESIZE); + if (page_size == (size_t)-1) + page_size = 4096; +#endif + arg_max = (size_t)sysconf(_SC_ARG_MAX); + if (arg_max == (size_t)-1) + arg_max = 128 * 1024; + + /* Seize control of the child process. */ + if (ptrace(PTRACE_SEIZE, child, NULL, ptrace_opts) == -1) { + /* + * If the process is already being traced, we will get EPERM. + * We don't treat that as a fatal error since we want it to be + * possible to run sudo inside a sudo shell with intercept enabled. + */ + if (errno != EPERM) { + sudo_warn("%s: ptrace(PTRACE_SEIZE, %d, NULL, 0x%lx)", + __func__, (int)child, ptrace_opts); + goto done; + } + sudo_debug_printf(SUDO_DEBUG_WARN, + "%s: unable to trace process %d, already being traced?", + __func__, (int)child); + ret = false; + } + + /* The child is suspended waiting for SIGUSR1, wake it up. */ + if (kill(child, SIGUSR1) == -1) { + sudo_warn("kill(%d, SIGUSR1)", (int)child); + goto done; + } + if (!ret) + goto done; + + /* Wait for the child to enter trace stop and continue it. */ + for (;;) { + if (waitpid(child, &status, __WALL) != -1) + break; + if (errno == EINTR) + continue; + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + goto done; + } + if (!WIFSTOPPED(status)) { + sudo_warnx(U_("process %d exited unexpectedly"), (int)child); + goto done; + } + if (ptrace(PTRACE_CONT, child, NULL, (void *)SIGUSR1) == -1) { + sudo_warn("%s: ptrace(PTRACE_CONT, %d, NULL, SIGUSR1)", + __func__, (int)child); + goto done; + } + + ret = true; + +done: + debug_return_int(ret); +} + +/* + * Compare two pathnames. If do_stat is true, fall back to stat(2)ing + * the paths for a dev/inode match if the strings don't match. + * Returns true on match, else false. + */ +static bool +pathname_matches(const char *path1, const char *path2, bool do_stat) +{ + struct stat sb1, sb2; + debug_decl(pathname_matches, SUDO_DEBUG_EXEC); + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: compare %s to %s", __func__, + path1 ? path1 : "(NULL)", path2 ? path2 : "(NULL)"); + + if (path1 == NULL || path2 == NULL) + debug_return_bool(false); + + if (strcmp(path1, path2) == 0) + debug_return_bool(true); + + if (do_stat && stat(path1, &sb1) == 0 && stat(path2, &sb2) == 0) { + if (sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino) + debug_return_bool(true); + } + + debug_return_bool(false); +} + +/* + * Open script and check for '#!' magic number followed by an interpreter. + * If present, check the interpreter against execpath, and argument string + * (if any) against argv[1]. + * Returns number of argv entries to skip on success, else 0. + */ +static int +script_matches(const char *script, const char *execpath, int argc, + char * const *argv) +{ + char * const *orig_argv = argv; + size_t linesize = 0; + char *interp, *interp_args, *line = NULL; + char magic[2]; + int count; + FILE *fp = NULL; + ssize_t len; + debug_decl(get_interpreter, SUDO_DEBUG_EXEC); + + /* Linux allows up to 4 nested interpreters. */ + for (count = 0; count < 4; count++) { + if (fp != NULL) + fclose(fp); + fp = fopen(script, "r"); + if (fp == NULL) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_ERRNO, + "%s: unable to open %s for reading", __func__, script); + goto done; + } + + if (fread(magic, 1, 2, fp) != 2 || memcmp(magic, "#!", 2) != 0) { + sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: %s: not a script", + __func__, script); + goto done; + } + + /* Check interpreter, skipping the shebang and trim trailing space. */ + len = getdelim(&line, &linesize, '\n', fp); + if (len == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: %s: can't get interpreter", + __func__, script); + goto done; + } + while (len > 0 && isspace((unsigned char)line[len - 1])) { + len--; + line[len] = '\0'; + } + sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: %s: shebang line \"%s\"", + __func__, script, line); + + /* + * Split line into interpreter and args. + * Whitespace is not supported in the interpreter path. + */ + for (interp = line; isspace((unsigned char)*interp); interp++) + continue; + interp_args = strpbrk(interp, " \t"); + if (interp_args != NULL) { + *interp_args++ = '\0'; + while (isspace((unsigned char)*interp_args)) + interp_args++; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: interpreter %s, args \"%s\"", + __func__, interp, interp_args ? interp_args : ""); + + /* Match interpreter. */ + if (!pathname_matches(execpath, interp, true)) { + /* It is possible for the interpreter to be a script too. */ + if (argv > 0 && strcmp(interp, argv[1]) == 0) { + /* Interpreter args must match for *this* interpreter. */ + if (interp_args == NULL || + (argc > 1 && strcmp(interp_args, argv[2]) == 0)) { + script = interp; + argv++; + argc--; + if (interp_args != NULL) { + argv++; + argc--; + } + /* Check whether interp is itself a script. */ + continue; + } + } + } + if (argc > 0 && interp_args != NULL) { + if (strcmp(interp_args, argv[1]) != 0) { + sudo_warnx( + U_("interpreter argument , expected \"%s\", got \"%s\""), + interp_args, argc > 1 ? argv[1] : "(NULL)"); + goto done; + } + argv++; + } + argv++; + break; + } + +done: + free(line); + if (fp != NULL) + fclose(fp); + debug_return_int((int)(argv - orig_argv)); +} + +static ssize_t +proc_read_vec(pid_t pid, const char *name, int *countp, char ***vecp, + char **bufp, size_t *bufsizep, size_t off) +{ + size_t strtab_len, remainder = *bufsizep - off; + char path[PATH_MAX], *strtab = *bufp + off; + ssize_t len, nread; + int fd; + debug_decl(proc_read_vec, SUDO_DEBUG_EXEC); + + len = snprintf(path, sizeof(path), "/proc/%d/%s", (int)pid, name); + if (len >= ssizeof(path)) + debug_return_ssize_t(-1); + + fd = open(path, O_RDONLY); + if (fd == -1) + debug_return_ssize_t(-1); + + /* Read in strings until EOF. */ + do { + nread = read(fd, strtab, remainder); + if (nread == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to read %s", __func__, path); + close(fd); + debug_return_ssize_t(-1); + } + strtab += nread; + remainder -= (size_t)nread; + if (remainder < sizeof(char *)) { + while (!growbuf(bufp, bufsizep, &strtab, &remainder)) { + close(fd); + debug_return_ssize_t(-1); + } + } + } while (nread != 0); + close(fd); + + /* Trim off the extra NUL byte at the end of the string table. */ + if (strtab - *bufp >= 2 && strtab[-1] == '\0' && strtab[-2] == '\0') { + strtab--; + remainder++; + } + + /* Store strings in a vector after the string table. */ + strtab_len = (size_t)(strtab - (*bufp + off)); + strtab = *bufp + off; + len = strtab_to_vec(strtab, strtab_len, countp, vecp, bufp, bufsizep, + remainder); + if (len == -1) + debug_return_ssize_t(-1); + + debug_return_ssize_t((ssize_t)strtab_len + len); +} + +/* + * Check if the execve(2) arguments match the contents of closure. + * Returns true if they match, else false. + */ +static bool +execve_args_match(const char *pathname, int argc, char * const *argv, + int envc, char * const *envp, bool do_stat, + struct intercept_closure *closure) +{ + bool ret = true; + int i; + debug_decl(execve_args_match, SUDO_DEBUG_EXEC); + + if (!pathname_matches(pathname, closure->command, do_stat)) { + /* For scripts, pathname will refer to the interpreter instead. */ + if (do_stat) { + int skip = script_matches(closure->command, pathname, + argc, argv); + if (skip != 0) { + /* Skip interpreter (and args) in argv. */ + argv += skip; + argc -= skip; + goto check_argv; + } + } + sudo_warnx( + U_("pathname mismatch, expected \"%s\", got \"%s\""), + closure->command, pathname ? pathname : "(NULL)"); + ret = false; + } +check_argv: + for (i = 0; i < argc; i++) { + if (closure->run_argv[i] == NULL) { + ret = false; + sudo_warnx( + U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), + "argv", i, "(NULL)", argv[i] ? argv[i] : "(NULL)"); + break; + } + if (argv[i] == NULL) { + ret = false; + sudo_warnx( + U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), + "argv", i, closure->run_argv[i], "(NULL)"); + break; + } + if (strcmp(argv[i], closure->run_argv[i]) != 0) { + if (i == 0) { + /* Special case for argv[0] which may contain the basename. */ + const char *base; + if (argv[0][0] == '/') { + if (closure->run_argv[0][0] != '/') { + base = sudo_basename(argv[0]); + if (strcmp(base, closure->run_argv[0]) == 0) + continue; + } + } else { + if (closure->run_argv[0][0] == '/') { + base = sudo_basename(closure->run_argv[0]); + if (strcmp(argv[0], base) == 0) + continue; + } + } + } + ret = false; + sudo_warnx( + U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), + "argv", i, closure->run_argv[i], argv[i]); + } + } + for (i = 0; i < envc; i++) { + if (closure->run_envp[i] == NULL) { + ret = false; + sudo_warnx( + U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), + "envp", i, "(NULL)", envp[i] ? envp[i] : "(NULL)"); + break; + } else if (envp[i] == NULL) { + ret = false; + sudo_warnx( + U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), + "envp", i, closure->run_envp[i], "(NULL)"); + break; + } else if (strcmp(envp[i], closure->run_envp[i]) != 0) { + ret = false; + sudo_warnx( + U_("%s[%d] mismatch, expected \"%s\", got \"%s\""), + "envp", i, closure->run_envp[i], envp[i]); + } + } + + debug_return_bool(ret); +} + +/* + * Verify that the execve(2) argument we wrote match the contents of closure. + * Returns true if they match, else false. + */ +static bool +verify_execve_args(pid_t pid, struct sudo_ptrace_regs *regs, + struct intercept_closure *closure) +{ + char *pathname, **argv, **envp, *buf; + int argc, envc; + bool ret = false; + debug_decl(verify_execve_args, SUDO_DEBUG_EXEC); + + buf = get_execve_info(pid, regs, &pathname, &argc, &argv, + &envc, &envp); + if (buf != NULL) { + ret = execve_args_match(pathname, argc, argv, envc, envp, false, closure); + free(buf); + } + + debug_return_bool(ret); +} + +/* + * Verify that the command executed matches the arguments we checked. + * Returns true on success and false on error. + */ +static bool +ptrace_verify_post_exec(pid_t pid, struct sudo_ptrace_regs *regs, + struct intercept_closure *closure) +{ + char **argv, **envp, *argbuf = NULL; + char pathname[PATH_MAX]; + sigset_t chldmask; + bool ret = false; + int argc, envc, i, status; + size_t bufsize; + ssize_t len; + debug_decl(ptrace_verify_post_exec, SUDO_DEBUG_EXEC); + + /* Block SIGCHLD for the critical section (waitpid). */ + sigemptyset(&chldmask); + sigaddset(&chldmask, SIGCHLD); + sigprocmask(SIG_BLOCK, &chldmask, NULL); + + /* Allow execve(2) to continue and wait for PTRACE_EVENT_EXEC. */ + ptrace(PTRACE_SYSCALL, pid, NULL, NULL); + for (;;) { + if (waitpid(pid, &status, __WALL) != -1) + break; + if (errno == EINTR) + continue; + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + goto done; + } + if (!WIFSTOPPED(status)) { + sudo_warnx(U_("process %d exited unexpectedly"), (int)pid); + goto done; + } + if (status >> 8 != (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) { + sudo_warnx(U_("process %d unexpected status 0x%x"), (int)pid, status); + goto done; + } + + /* Get the executable path. */ + if (!proc_read_link(pid, "exe", pathname, sizeof(pathname))) { + /* Missing /proc file system is not a fatal error. */ + sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: unable to read /proc/%d/exe", + __func__, (int)pid); + ret = true; + goto done; + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d: verify %s", __func__, + (int)pid, pathname); + + /* Allocate a single buffer for argv, envp and their strings. */ + bufsize = arg_max; + argbuf = malloc(bufsize); + if (argbuf == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + + len = proc_read_vec(pid, "cmdline", &argc, &argv, &argbuf, &bufsize, 0); + if (len == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve argv for process %d", (int)pid); + goto done; + } + + len = proc_read_vec(pid, "environ", &envc, &envp, &argbuf, &bufsize, + (size_t)len); + if (len == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to read execve envp for process %d", (int)pid); + goto done; + } + + /* Convert offsets in argv and envp to pointers. */ + argv = (char **)(argbuf + (unsigned long)argv); + for (i = 0; i < argc; i++) { + argv[i] = argbuf + (unsigned long)argv[i]; + } + envp = (char **)(argbuf + (unsigned long)envp); + for (i = 0; i < envc; i++) { + envp[i] = argbuf + (unsigned long)envp[i]; + } + + ret = execve_args_match(pathname, argc, argv, envc, envp, true, closure); + if (!ret) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d new execve args don't match closure", __func__, (int)pid); + } + +done: + free(argbuf); + sigprocmask(SIG_UNBLOCK, &chldmask, NULL); + + debug_return_bool(ret); +} + +/* + * Intercept execve(2) and perform a policy check. + * Reads current registers and execve(2) arguments. + * If the command is not allowed by policy, fail with EACCES. + * If the command is allowed, update argv if needed before continuing. + * Returns true on success and false on error. + */ +static bool +ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure) +{ + char *pathname, **argv, **envp, *buf; + const unsigned int flags = closure->details->flags; + int argc, envc, syscallno; + struct sudo_ptrace_regs regs; + bool path_mismatch = false; + bool argv_mismatch = false; + char cwd[PATH_MAX], *orig_argv0; + unsigned long msg; + bool ret = false; + int i, oldcwd = -1; + debug_decl(ptrace_intercept_execve, SUDO_DEBUG_EXEC); + + /* Do not check the policy if we are executing the initial command. */ + if (closure->initial_command != 0) { + closure->initial_command--; + debug_return_bool(true); + } + + /* Get compat flag. */ + if (ptrace(PTRACE_GETEVENTMSG, pid, NULL, &msg) == -1) { + sudo_warn(U_("unable to get event message for process %d"), (int)pid); + debug_return_bool(false); + } + + /* Get the registers. */ + memset(®s, 0, sizeof(regs)); + if (!ptrace_getregs(pid, ®s, msg)) { + sudo_warn(U_("unable to get registers for process %d"), (int)pid); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d: compat: %s, wordsize: %u", + __func__, (int)pid, regs.compat ? "true" : "false", regs.wordsize); + +# ifdef SECCOMP_AUDIT_ARCH_COMPAT + if (regs.compat) { + syscallno = get_syscallno(®s); + switch (syscallno) { + case COMPAT_execve: + /* Handled below. */ + break; + case COMPAT_execveat: + /* We don't currently check execveat(2). */ + debug_return_bool(true); + break; + default: + sudo_warnx("%s: unexpected compat system call %d", + __func__, syscallno); + debug_return_bool(false); + } + } else +# endif /* SECCOMP_AUDIT_ARCH_COMPAT */ + { + syscallno = get_syscallno(®s); + switch (syscallno) { +# ifdef X32_execve + case X32_execve: +# endif + case __NR_execve: + /* Handled below. */ + break; +# ifdef X32_execveat + case X32_execveat: +# endif + case __NR_execveat: + /* We don't currently check execveat(2). */ + debug_return_bool(true); + break; + default: + sudo_warnx("%s: unexpected system call %d", __func__, syscallno); + debug_return_bool(false); + } + } + + /* Get the current working directory and execve info. */ + if (!proc_read_link(pid, "cwd", cwd, sizeof(cwd))) + (void)strlcpy(cwd, "unknown", sizeof(cwd)); + buf = get_execve_info(pid, ®s, &pathname, &argc, &argv, + &envc, &envp); + if (buf == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: %d: unable to get execve info", __func__, (int)pid); + /* EIO from ptrace is like EFAULT from the kernel. */ + if (errno == EIO) + errno = EFAULT; + ptrace_fail_syscall(pid, ®s, errno); + goto done; + } + + /* Must have a pathname. */ + if (pathname == NULL) { + ptrace_fail_syscall(pid, ®s, EINVAL); + goto done; + } + + /* We can only pass the pathname to exececute via argv[0] (plugin API). */ + orig_argv0 = argv[0] ? argv[0] : (char *)""; + argv[0] = pathname; + if (argc == 0) { + /* Rewrite an empty argv[] with the path to execute. */ + argv[1] = NULL; + argc = 1; + argv_mismatch = true; + } + + /* Perform a policy check. */ + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d: checking policy for %s", + __func__, (int)pid, pathname); + if (!intercept_check_policy(pathname, argc, argv, envc, envp, cwd, + &oldcwd, closure)) { + if (closure->errstr != NULL) + sudo_warnx("%s", U_(closure->errstr)); + } + + switch (closure->state) { + case POLICY_TEST: + path_mismatch = true; + argv_mismatch = true; + if (closure->command == NULL) + closure->command = pathname; + if (closure->run_argv == NULL) + closure->run_argv = argv; + if (closure->run_envp == NULL) + closure->run_envp = envp; + FALLTHROUGH; + case POLICY_ACCEPT: + /* + * Update pathname and argv if the policy modified it. + * We don't currently ever modify envp. + */ + if (strcmp(pathname, closure->command) != 0) + path_mismatch = true; + if (!path_mismatch) { + /* Path unchanged, restore original argv[0]. */ + if (strcmp(argv[0], orig_argv0) != 0) { + argv[0] = orig_argv0; + free(closure->run_argv[0]); + closure->run_argv[0] = strdup(orig_argv0); + if (closure->run_argv[0] == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + } + } + for (i = 0; closure->run_argv[i] != NULL && argv[i] != NULL; i++) { + if (strcmp(closure->run_argv[i], argv[i]) != 0) { + argv_mismatch = true; + break; + } + } + if (closure->run_argv[i] != NULL || argv[i] != NULL) + argv_mismatch = true; + + if (path_mismatch || argv_mismatch) { + /* + * Need to rewrite pathname and/or argv. + * We can use space below the stack pointer to store the data. + * On amd64 there is a 128 byte red zone that must be avoided. + * Note: on pa-risc the stack grows up, not down. + */ + unsigned long sp = get_stack_pointer(®s) - 128; + unsigned long strtab; + size_t space = 0; + ssize_t nwritten; + + sudo_debug_execve(SUDO_DEBUG_DIAG, closure->command, + closure->run_argv, envp); + + /* + * Calculate the amount of space required for pointers + strings. + * Since ptrace(2) always writes in sizeof(long) increments we + * need to be careful to avoid overwriting what we have already + * written for compat binaries (where the word size doesn't match). + * + * This is mostly a problem for the string table since we do + * interleaved writes of the argument vector pointers and the + * strings they refer to. For native binaries, it is sufficient + * to align the string table on a word boundary. For compat + * binaries, if argc is odd, writing the last pointer will overlap + * the first string so leave an extra word in between them. + */ + if (argv_mismatch) { + /* argv pointers */ + space += ((size_t)argc + 1 + regs.compat) * regs.wordsize; + + /* argv strings */ + for (argc = 0; closure->run_argv[argc] != NULL; argc++) { + space += strlen(closure->run_argv[argc]) + 1; + } + } + if (path_mismatch) { + /* pathname string */ + space += strlen(closure->command) + 1; + } + + /* Reserve stack space for path, argv (w/ NULL) and its strings. */ + sp -= WORDALIGN(space, regs); + strtab = sp; + + if (argv_mismatch) { + /* Update argv address in the tracee to our new value. */ + set_sc_arg2(®s, sp); + + /* Skip over argv pointers (plus NULL) for string table. */ + strtab += ((size_t)argc + 1 + regs.compat) * regs.wordsize; + + nwritten = ptrace_write_vec(pid, ®s, closure->run_argv, + sp, strtab); + if (nwritten == -1) + goto done; + strtab += (unsigned long)nwritten; + } + if (path_mismatch) { + /* Update pathname address in the tracee to our new value. */ + set_sc_arg1(®s, strtab); + + /* Write pathname to the string table. */ + nwritten = ptrace_write_string(pid, strtab, closure->command); + if (nwritten == -1) + goto done; + } + + /* Update args in the tracee to the new values. */ + if (!ptrace_setregs(pid, ®s)) { + sudo_warn(U_("unable to set registers for process %d"), + (int)pid); + goto done; + } + + if (closure->state == POLICY_TEST) { + /* Verify the contents of what we just wrote. */ + if (!verify_execve_args(pid, ®s, closure)) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: new execve args don't match closure", __func__); + } + } + } + if (closure->state == POLICY_ACCEPT && ISSET(flags, CD_INTERCEPT)) { + if (ISSET(flags, CD_INTERCEPT_VERIFY)) { + /* Verify execve(2) args post-exec. */ + if (!ptrace_verify_post_exec(pid, ®s, closure)) { + if (errno != ESRCH) + kill(pid, SIGKILL); + } + } + } + break; + case POLICY_REJECT: + /* If rejected, fake the syscall and set return to EACCES */ + errno = EACCES; + FALLTHROUGH; + default: + ptrace_fail_syscall(pid, ®s, errno); + break; + } + + ret = true; + +done: + if (oldcwd != -1) { + if (fchdir(oldcwd) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to restore saved cwd", __func__); + } + close(oldcwd); + } + free(buf); + intercept_closure_reset(closure); + + debug_return_bool(ret); +} + +/* + * Handle a process stopped due to ptrace. + * Restarts the tracee with PTRACE_LISTEN (for a group-stop) + * or PTRACE_CONT (for signal-delivery-stop). + * Returns true if stopped by a group-stop, else false. + */ +bool +exec_ptrace_stopped(pid_t pid, int status, void *intercept) +{ + struct intercept_closure *closure = intercept; + const int stopsig = WSTOPSIG(status); + const int sigtrap = status >> 8; + long signo = 0; + bool group_stop = false; + debug_decl(exec_ptrace_stopped, SUDO_DEBUG_EXEC); + + if (sigtrap == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) { + if (!ptrace_intercept_execve(pid, closure)) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d failed to intercept execve", __func__, (int)pid); + } + } else if (sigtrap == (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: %d PTRACE_EVENT_EXEC", __func__, (int)pid); + } else if (sigtrap == (SIGTRAP | (PTRACE_EVENT_CLONE << 8)) || + sigtrap == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)) || + sigtrap == (SIGTRAP | (PTRACE_EVENT_FORK << 8))) { + unsigned long new_pid; + + /* New child process, it will inherit the parent's trace flags. */ + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + if (ptrace(PTRACE_GETEVENTMSG, pid, NULL, &new_pid) != -1) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d forked new child %lu", __func__, (int)pid, new_pid); + } else { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "ptrace(PTRACE_GETEVENTMSG, %d, NULL, %p)", (int)pid, + &new_pid); + } + } + } else { + switch (stopsig) { + case SIGSTOP: + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + /* Is this a group-stop? */ + if (status >> 16 == PTRACE_EVENT_STOP) { + /* Group-stop, do not deliver signal. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d: group-stop signal %d", + __func__, (int)pid, stopsig); + group_stop = true; + break; + } + FALLTHROUGH; + default: + /* Signal-delivery-stop, deliver signal. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: %d: signal-delivery-stop signal %d", + __func__, (int)pid, stopsig); + signo = stopsig; + break; + } + } + + if (group_stop) { + /* + * Restart child but prevent it from executing + * until SIGCONT is received (simulate SIGSTOP, etc). + */ + if (ptrace(PTRACE_LISTEN, pid, NULL, 0L) == -1 && errno != ESRCH) + sudo_warn("%s: ptrace(PTRACE_LISTEN, %d, NULL, 0L)", + __func__, (int)pid); + } else { + /* Restart child immediately. */ + if (ptrace(PTRACE_CONT, pid, NULL, signo) == -1 && errno != ESRCH) + sudo_warn("%s: ptrace(PTRACE_CONT, %d, NULL, %ld)", + __func__, (int)pid, signo); + } + + debug_return_bool(group_stop); +} + +bool +exec_ptrace_intercept_supported(void) +{ +# ifdef __mips__ + /* MIPS doesn't support changing the syscall return value. */ + return false; +# else + if (seccomp_trap_supported == -1) + seccomp_trap_supported = have_seccomp_action("trap"); + + return seccomp_trap_supported == true; +# endif +} + +bool +exec_ptrace_subcmds_supported(void) +{ + if (seccomp_trap_supported == -1) + seccomp_trap_supported = have_seccomp_action("trap"); + + return seccomp_trap_supported == true; +} +#else +/* STUB */ +bool +exec_ptrace_stopped(pid_t pid, int status, void *intercept) +{ + return true; +} + +/* STUB */ +int +exec_ptrace_seize(pid_t child) +{ + return true; +} + +/* STUB */ +bool +exec_ptrace_intercept_supported(void) +{ + return false; +} + +/* STUB */ +bool +exec_ptrace_subcmds_supported(void) +{ + return false; +} +#endif /* HAVE_PTRACE_INTERCEPT */ + +/* + * Adjust flags based on the availability of ptrace support. + */ +void +exec_ptrace_fix_flags(struct command_details *details) +{ + debug_decl(exec_ptrace_fix_flags, SUDO_DEBUG_EXEC); + + if (ISSET(details->flags, CD_USE_PTRACE)) { + /* If both CD_INTERCEPT and CD_LOG_SUBCMDS set, CD_INTERCEPT wins. */ + if (ISSET(details->flags, CD_INTERCEPT)) { + if (!exec_ptrace_intercept_supported()) + CLR(details->flags, CD_USE_PTRACE); + } else if (ISSET(details->flags, CD_LOG_SUBCMDS)) { + if (!exec_ptrace_subcmds_supported()) + CLR(details->flags, CD_USE_PTRACE); + } else { + CLR(details->flags, CD_USE_PTRACE); + } + } + debug_return; +} diff --git a/src/exec_ptrace.h b/src/exec_ptrace.h new file mode 100644 index 0000000..f75f2e5 --- /dev/null +++ b/src/exec_ptrace.h @@ -0,0 +1,532 @@ +/* + * Copyright (c) 2022 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_PTRACE_H +#define SUDO_EXEC_PTRACE_H + +#include <sys/prctl.h> +#include <sys/ptrace.h> +#include <sys/user.h> +#include <asm/unistd.h> +#include <linux/audit.h> +#include <linux/ptrace.h> +#include <linux/seccomp.h> +#include <linux/filter.h> + +/* Older kernel headers may be missing some EM_* defines in linux/elf.h. */ +#include <elf.h> + +/* Older systems may not support execveat(2). */ +#ifndef __NR_execveat +# define __NR_execveat -1 +#endif + +/* In case userland elf.h doesn't define NT_ARM_SYSTEM_CALL. */ +#if defined(__aarch64__) && !defined(NT_ARM_SYSTEM_CALL) +# define NT_ARM_SYSTEM_CALL 0x404 +#endif + +/* In case userland doesn't define __X32_SYSCALL_BIT. */ +#if defined(__x86_64__) && !defined(__X32_SYSCALL_BIT) +# define __X32_SYSCALL_BIT 0x40000000 +#endif + +#ifdef __mips__ +# ifndef __NR_O32_Linux +# define __NR_O32_Linux 4000 +# endif +# ifndef __NR_N32_Linux +# define __NR_N32_Linux 6000 +# endif +#endif + +/* Align address to a (compat) word boundary. */ +#define WORDALIGN(_a, _r) \ + (((_a) + ((unsigned long)(_r).wordsize - 1UL)) & ~((unsigned long)(_r).wordsize - 1UL)) + +/* Align pointer to a native word boundary. */ +#define LONGALIGN(_p) \ + (((unsigned long)(_p) + (sizeof(unsigned long) - 1UL)) & ~(sizeof(unsigned long) - 1UL)) + +/* + * See syscall(2) for a list of registers used in system calls. + * For example code, see tools/testing/selftests/seccomp/seccomp_bpf.c + * + * The structs and registers vary among the different platforms. + * We define user_regs_struct as the struct to use for gettings + * and setting the general registers and define accessor + * macros to get/set the individual struct members. + * + * The value of SECCOMP_AUDIT_ARCH is used when matching the architecture + * in the seccomp(2) filter. + */ +#if defined(__x86_64__) +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64 +# ifndef __ILP32__ +# define X32_execve __X32_SYSCALL_BIT + 520 +# define X32_execveat __X32_SYSCALL_BIT + 545 +# endif +# define sudo_pt_regs struct user_regs_struct +# define reg_syscall(x) (x).orig_rax +# define reg_retval(x) (x).rax +# define reg_sp(x) (x).rsp +# define reg_arg1(x) (x).rdi +# define reg_arg2(x) (x).rsi +# define reg_arg3(x) (x).rdx +# define reg_arg4(x) (x).r10 +#elif defined(__aarch64__) +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_AARCH64 +# define sudo_pt_regs struct user_pt_regs +# define reg_syscall(x) (x).regs[8] /* w8 */ +# define reg_retval(x) (x).regs[0] /* x0 */ +# define reg_sp(x) (x).sp /* sp */ +# define reg_arg1(x) (x).regs[0] /* x0 */ +# define reg_arg2(x) (x).regs[1] /* x1 */ +# define reg_arg3(x) (x).regs[2] /* x2 */ +# define reg_arg4(x) (x).regs[3] /* x3 */ +# define reg_set_syscall(_r, _nr) do { \ + struct iovec _iov; \ + long _syscallno = (_nr); \ + _iov.iov_base = &_syscallno; \ + _iov.iov_len = sizeof(_syscallno); \ + ptrace(PTRACE_SETREGSET, pid, NT_ARM_SYSTEM_CALL, &_iov); \ +} while (0) +#elif defined(__arm__) +/* Note: assumes arm EABI, not OABI */ +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARM +# define sudo_pt_regs struct pt_regs +# define reg_syscall(x) (x).ARM_r7 +# define reg_retval(x) (x).ARM_r0 +# define reg_sp(x) (x).ARM_sp +# define reg_arg1(x) (x).ARM_r0 +# define reg_arg2(x) (x).ARM_r1 +# define reg_arg3(x) (x).ARM_r2 +# define reg_arg4(x) (x).ARM_r3 +# define reg_set_syscall(_r, _nr) do { \ + ptrace(PTRACE_SET_SYSCALL, pid, NULL, _nr); \ +} while (0) +#elif defined (__hppa__) +/* Untested (should also support hppa64) */ +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PARISC +# define sudo_pt_regs struct user_regs_struct +# define reg_syscall(x) (x).gr[20] /* r20 */ +# define reg_retval(x) (x).gr[28] /* r28 */ +# define reg_sp(x) (x).gr[30] /* r30 */ +# define reg_arg1(x) (x).gr[26] /* r26 */ +# define reg_arg2(x) (x).gr[25] /* r25 */ +# define reg_arg3(x) (x).gr[24] /* r24 */ +# define reg_arg4(x) (x).gr[23] /* r23 */ +#elif defined(__i386__) +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_I386 +# define sudo_pt_regs struct user_regs_struct +# define reg_syscall(x) (x).orig_eax +# define reg_retval(x) (x).eax +# define reg_sp(x) (x).esp +# define reg_arg1(x) (x).ebx +# define reg_arg2(x) (x).ecx +# define reg_arg3(x) (x).edx +# define reg_arg4(x) (x).esi +#elif defined(__mips__) +# if _MIPS_SIM == _MIPS_SIM_ABI32 +# /* Linux o32 style syscalls, 4000-4999. */ +# if BYTE_ORDER == LITTLE_ENDIAN +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPSEL +# else +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPS +# endif +# elif _MIPS_SIM == _MIPS_SIM_ABI64 +# /* Linux 64-bit syscalls, 5000-5999. */ +# if BYTE_ORDER == LITTLE_ENDIAN +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPSEL64 +# else +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPS64 +# endif +# elif _MIPS_SIM == _MIPS_SIM_NABI32 +# /* Linux N32 syscalls, 6000-6999. */ +# if BYTE_ORDER == LITTLE_ENDIAN +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPSEL64N32 +# else +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPS64N32 +# endif +# else +# error "Unsupported MIPS ABI" +# endif +/* + * If called via syscall(__NR_###), v0 holds __NR_O32_Linux and the real + * syscall is in the first arg (a0). The actual args are shifted by one. + * MIPS does not support setting the syscall return value via ptrace. + */ +# define sudo_pt_regs struct pt_regs +# define reg_syscall(_r) ({ \ + __u64 _nr; \ + if ((_r).regs[2] == __NR_O32_Linux) \ + _nr = (_r).regs[4]; /* a0 */ \ + else \ + _nr = (_r).regs[2]; /* v0 */ \ + _nr; \ +}) +# define reg_retval(x) (x).regs[2] /* v0 */ +# define reg_sp(x) (x).regs[29] /* sp */ +# define reg_arg1(x) \ + ((x).regs[2] == __NR_O32_Linux ? (x).regs[5] : (x).regs[4]) +# define reg_set_arg1(_r, _v) do { \ + if ((_r).regs[2] == __NR_O32_Linux) \ + (_r).regs[5] = _v; /* a1 */ \ + else \ + (_r).regs[4] = _v; /* a0 */ \ +} while (0) +# define reg_arg2(x) \ + ((x).regs[2] == __NR_O32_Linux ? (x).regs[6] : (x).regs[5]) +# define reg_set_arg2(_r, _v) do { \ + if ((_r).regs[2] == __NR_O32_Linux) \ + (_r).regs[6] = _v; /* a2 */ \ + else \ + (_r).regs[5] = _v; /* a1 */ \ +} while (0) +# define reg_arg3(x) \ + ((x).regs[2] == __NR_O32_Linux ? (x).regs[7] : (x).regs[6]) +# define reg_set_arg3(_r, _v) do { \ + if ((_r).regs[2] == __NR_O32_Linux) \ + (_r).regs[7] = _v; /* a3 */ \ + else \ + (_r).regs[6] = _v; /* a2 */ \ +} while (0) +/* XXX - reg_arg4 probably wrong for syscall() type calls on 032. */ +# define reg_arg4(x) \ + ((x).regs[2] == __NR_O32_Linux ? (x).regs[8] : (x).regs[7]) +# define reg_set_arg4(_r, _v) do { \ + if ((_r).regs[2] == __NR_O32_Linux) \ + (_r).regs[8] = _v; /* a4 */ \ + else \ + (_r).regs[7] = _v; /* a3 */ \ +} while (0) +# define reg_set_syscall(_r, _nr) do { \ + if ((_r).regs[2] == __NR_O32_Linux) \ + (_r).regs[4] = _nr; /* a0 */ \ + else \ + (_r).regs[2] = _nr; /* v0 */ \ +} while (0) +#elif defined(__powerpc__) +# if defined(__powerpc64__) +# if BYTE_ORDER == LITTLE_ENDIAN +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PPC64LE +# else +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PPC64 +# endif +# else +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PPC +# endif +# define sudo_pt_regs struct pt_regs +# define reg_syscall(x) (x).gpr[0] /* r0 */ +# define reg_retval(x) (x).gpr[3] /* r3 */ +# define reg_sp(x) (x).gpr[1] /* r1 */ +# define reg_arg1(x) (x).orig_gpr3 /* r3 */ +# define reg_arg2(x) (x).gpr[4] /* r4 */ +# define reg_arg3(x) (x).gpr[5] /* r5 */ +# define reg_arg4(x) (x).gpr[6] /* r6 */ +# define reg_set_retval(_r, _v) do { \ + if (((_r).trap & 0xfff0) == 0x3000) { \ + /* scv 0 system call, uses negative error code for result. */ \ + reg_retval(_r) = (_v); \ + } else { \ + /* \ + * Set CR0 SO bit to indicate a syscall error, which is stored \ + * as a positive error code. \ + */ \ + reg_retval(_r) = -(_v); \ + (_r).ccr |= 0x10000000; \ + } \ +} while (0) +#elif defined(__riscv) && __riscv_xlen == 64 +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_RISCV64 +# define sudo_pt_regs struct user_regs_struct +# define reg_syscall(x) (x).a7 +# define reg_retval(x) (x).a0 +# define reg_sp(x) (x).sp +# define reg_arg1(x) (x).a0 +# define reg_arg2(x) (x).a1 +# define reg_arg3(x) (x).a2 +# define reg_arg4(x) (x).a3 +#elif defined(__s390__) +/* + * Both the syscall number and return value are stored in r2 for + * the s390 ptrace API. The first argument is stored in orig_gpr2. + */ +# if defined(__s390x__) +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_S390X +# else +# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_S390 +# endif +# define sudo_pt_regs s390_regs +# define reg_syscall(x) (x).gprs[2] /* r2 */ +# define reg_retval(x) (x).gprs[2] /* r2 */ +# define reg_sp(x) (x).gprs[15] /* r15 */ +# define reg_arg1(x) (x).orig_gpr2 /* r2 */ +# define reg_arg2(x) (x).gprs[3] /* r3 */ +# define reg_arg3(x) (x).gprs[4] /* r4 */ +# define reg_arg4(x) (x).gprs[5] /* r6 */ +#else +# error "Do not know how to find your architecture's registers" +#endif + +/* + * Compat definitions for running 32-bit binaries on 64-bit platforms. + * We must define the register struct too since there is no way to + * get it directly from the system headers. + * + * The value of SECCOMP_AUDIT_ARCH_COMPAT is used when matching the + * architecture in the seccomp(2) filter. We can tell when the compat + * arch matched by inspecting the message returned by PTRACE_GETEVENTMSG. + */ +#if defined(__x86_64__) +struct i386_user_regs_struct { + unsigned int ebx; + unsigned int ecx; + unsigned int edx; + unsigned int esi; + unsigned int edi; + unsigned int ebp; + unsigned int eax; + unsigned int xds; + unsigned int xes; + unsigned int xfs; + unsigned int xgs; + unsigned int orig_eax; + unsigned int eip; + unsigned int xcs; + unsigned int eflags; + unsigned int esp; + unsigned int xss; +}; +# define SECCOMP_AUDIT_ARCH_COMPAT AUDIT_ARCH_I386 +# define COMPAT_execve 11 +# define COMPAT_execveat 358 +# define compat_sudo_pt_regs struct i386_user_regs_struct +# define compat_reg_syscall(x) (x).orig_eax +# define compat_reg_retval(x) (x).eax +# define compat_reg_sp(x) (x).esp +# define compat_reg_arg1(x) (x).ebx +# define compat_reg_arg2(x) (x).ecx +# define compat_reg_arg3(x) (x).edx +# define compat_reg_arg4(x) (x).esi +#elif defined(__aarch64__) +struct arm_pt_regs { + unsigned int uregs[18]; +}; +# define SECCOMP_AUDIT_ARCH_COMPAT AUDIT_ARCH_ARM +# define COMPAT_execve 11 +# define COMPAT_execveat 387 +# define compat_sudo_pt_regs struct arm_pt_regs +# define compat_reg_syscall(x) (x).uregs[7] /* r7 */ +# define compat_reg_retval(x) (x).uregs[0] /* r0 */ +# define compat_reg_sp(x) (x).uregs[13] /* r13 */ +# define compat_reg_arg1(x) (x).uregs[0] /* r0 */ +# define compat_reg_arg2(x) (x).uregs[1] /* r1 */ +# define compat_reg_arg3(x) (x).uregs[2] /* r2 */ +# define compat_reg_arg4(x) (x).uregs[3] /* r3 */ +# define compat_reg_set_syscall(_r, _nr) reg_set_syscall(_r, _nr) +#elif defined(__mips__) +# if _MIPS_SIM == _MIPS_SIM_ABI64 +/* MIPS o32/n32 binary compatibility on a mips64 system. */ +# if BYTE_ORDER == LITTLE_ENDIAN +# define SECCOMP_AUDIT_ARCH_COMPAT AUDIT_ARCH_MIPSEL +# define SECCOMP_AUDIT_ARCH_COMPAT2 AUDIT_ARCH_MIPSEL64N32 +# else +# define SECCOMP_AUDIT_ARCH_COMPAT AUDIT_ARCH_MIPS +# define SECCOMP_AUDIT_ARCH_COMPAT2 AUDIT_ARCH_MIPS64N32 +# endif +# define COMPAT_execve __NR_O32_Linux + 11 +# define COMPAT_execveat __NR_O32_Linux + 356 +# define COMPAT2_execve __NR_N32_Linux + 57 +# define COMPAT2_execveat __NR_N32_Linux + 320 +# elif _MIPS_SIM == _MIPS_SIM_NABI32 +# if BYTE_ORDER == LITTLE_ENDIAN +# define SECCOMP_AUDIT_ARCH_COMPAT AUDIT_ARCH_MIPSEL +# else +# define SECCOMP_AUDIT_ARCH_COMPAT AUDIT_ARCH_MIPS +# endif +# define COMPAT_execve __NR_O32_Linux + 11 +# define COMPAT_execveat __NR_O32_Linux + 356 +# endif /* _MIPS_SIM_ABI64 */ +/* MIPS ABIs use a common ptrace interface. */ +# define compat_sudo_pt_regs struct pt_regs +# define compat_reg_syscall(x) reg_syscall(x) +# define compat_reg_retval(x) reg_retval(x) +# define compat_reg_sp(x) reg_sp(x) +# define compat_reg_arg1(x) reg_arg1(x) +# define compat_reg_set_arg1(_r, _v) reg_set_arg1(_r, _v) +# define compat_reg_arg2(x) reg_arg2(x) +# define compat_reg_set_arg2(_r, _v) reg_set_arg2(_r, _v) +# define compat_reg_arg3(x) reg_arg3(x) +# define compat_reg_set_arg3(_r, _v) reg_set_arg3(_r, _v) +# define compat_reg_arg4(x) reg_arg4(x) +# define compat_reg_set_arg4(_r, _v) reg_set_arg4(_r, _v) +# define compat_reg_set_syscall(_r, _nr) reg_set_syscall(_r, _nr) +#elif defined(__powerpc64__) +struct ppc_pt_regs { + unsigned int gpr[32]; + unsigned int nip; + unsigned int msr; + unsigned int orig_gpr3; + unsigned int ctr; + unsigned int link; + unsigned int xer; + unsigned int ccr; + unsigned int mq; + unsigned int trap; + unsigned int dar; + unsigned int dsisr; + unsigned int result; +}; +# if BYTE_ORDER == LITTLE_ENDIAN +/* There is no AUDIT_ARCH_PPCLE define. */ +# define SECCOMP_AUDIT_ARCH_COMPAT (AUDIT_ARCH_PPC|__AUDIT_ARCH_LE) +# else +# define SECCOMP_AUDIT_ARCH_COMPAT AUDIT_ARCH_PPC +# endif +# define COMPAT_execve __NR_execve +# define COMPAT_execveat __NR_execveat +# define compat_sudo_pt_regs struct ppc_pt_regs +# define compat_reg_syscall(x) (x).gpr[0] /* r0 */ +# define compat_reg_retval(x) (x).gpr[3] /* r3 */ +# define compat_reg_sp(x) (x).gpr[1] /* r1 */ +# define compat_reg_arg1(x) (x).orig_gpr3 /* r3 */ +# define compat_reg_arg2(x) (x).gpr[4] /* r4 */ +# define compat_reg_arg3(x) (x).gpr[5] /* r5 */ +# define compat_reg_arg4(x) (x).gpr[6] /* r6 */ +# define compat_reg_set_retval(_r, _v) reg_set_retval(_r, _v) +#endif + +/* Set the syscall number the "normal" way by default. */ +#ifndef reg_set_syscall +# define reg_set_syscall(_r, _nr) do { \ + reg_syscall(_r) = (_nr); \ +} while (0) +#endif +#ifndef compat_reg_set_syscall +# define compat_reg_set_syscall(_r, _nr) do { \ + compat_reg_syscall(_r) = (_nr); \ +} while (0) +#endif + +/* Set the syscall return value the "normal" way by default. */ +#ifndef reg_set_retval +# define reg_set_retval(_r, _v) do { \ + reg_retval(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_retval +# define compat_reg_set_retval(_r, _v) do { \ + compat_reg_retval(_r) = (_v); \ +} while (0) +#endif + +/* Set the syscall arguments the "normal" way by default. */ +#ifndef reg_set_arg1 +# define reg_set_arg1(_r, _v) do { \ + reg_arg1(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg1 +# define compat_reg_set_arg1(_r, _v) do { \ + compat_reg_arg1(_r) = (_v); \ +} while (0) +#endif +#ifndef reg_set_arg2 +# define reg_set_arg2(_r, _v) do { \ + reg_arg2(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg2 +# define compat_reg_set_arg2(_r, _v) do { \ + compat_reg_arg2(_r) = (_v); \ +} while (0) +#endif +#ifndef reg_set_arg3 +# define reg_set_arg3(_r, _v) do { \ + reg_arg3(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg3 +# define compat_reg_set_arg3(_r, _v) do { \ + compat_reg_arg3(_r) = (_v); \ +} while (0) +#endif +#ifndef reg_set_arg4 +# define reg_set_arg4(_r, _v) do { \ + reg_arg4(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg4 +# define compat_reg_set_arg4(_r, _v) do { \ + compat_reg_arg4(_r) = (_v); \ +} while (0) +#endif + +/* Set the syscall arguments the "normal" way by default. */ +#ifndef reg_set_arg1 +# define reg_set_arg1(_r, _v) do { \ + reg_arg1(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg1 +# define compat_reg_set_arg1(_r, _v) do { \ + compat_reg_arg1(_r) = (_v); \ +} while (0) +#endif +#ifndef reg_set_arg2 +# define reg_set_arg2(_r, _v) do { \ + reg_arg2(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg2 +# define compat_reg_set_arg2(_r, _v) do { \ + compat_reg_arg2(_r) = (_v); \ +} while (0) +#endif +#ifndef reg_set_arg3 +# define reg_set_arg3(_r, _v) do { \ + reg_arg3(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg3 +# define compat_reg_set_arg3(_r, _v) do { \ + compat_reg_arg3(_r) = (_v); \ +} while (0) +#endif +#ifndef reg_set_arg4 +# define reg_set_arg4(_r, _v) do { \ + reg_arg4(_r) = (_v); \ +} while (0) +#endif +#ifndef compat_reg_set_arg4 +# define compat_reg_set_arg4(_r, _v) do { \ + compat_reg_arg4(_r) = (_v); \ +} while (0) +#endif + +struct sudo_ptrace_regs { + union { + sudo_pt_regs native; +#ifdef SECCOMP_AUDIT_ARCH_COMPAT + compat_sudo_pt_regs compat; +#endif + } u; + unsigned int wordsize; + bool compat; +}; + +#endif /* SUDO_EXEC_PTRACE_H */ diff --git a/src/exec_pty.c b/src/exec_pty.c new file mode 100644 index 0000000..1b4bc0e --- /dev/null +++ b/src/exec_pty.c @@ -0,0 +1,1452 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2023 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> + +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#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> + +/* 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); +static struct monitor_message_list monitor_messages = + TAILQ_HEAD_INITIALIZER(monitor_messages); +static unsigned int term_raw_flags; +static struct exec_closure pty_ec; + +static void sync_ttysize(struct exec_closure *ec); +static void schedule_signal(struct exec_closure *ec, int signo); + +/* + * Allocate a pty if /dev/tty is a tty. + * Fills in io_fds[SFD_USERTTY], io_fds[SFD_LEADER] and io_fds[SFD_FOLLOWER]. + * Returns the dyamically allocated pty name on success, NULL on failure. + */ +static char * +pty_setup(struct command_details *details) +{ + char *ptyname = NULL; + 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_ptr(NULL); + } + + ptyname = get_pty(&io_fds[SFD_LEADER], &io_fds[SFD_FOLLOWER], + details->cred.euid); + if (ptyname == NULL) + sudo_fatal("%s", U_("unable to allocate pty")); + + /* Add entry to utmp/utmpx? */ + if (ISSET(details->flags, CD_SET_UTMP)) + utmp_login(details->tty, ptyname, io_fds[SFD_FOLLOWER], details->utmp_user); + + /* Update tty name in command details (used by monitor, SELinux, AIX). */ + details->tty = ptyname; + + 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_str(ptyname); +} + +/* + * Restore user's terminal settings and update utmp, as needed. + */ +static void +pty_cleanup(struct exec_closure *ec, int wstatus) +{ + debug_decl(pty_cleanup, SUDO_DEBUG_EXEC); + + /* Restore terminal settings. */ + if (ec->term_raw) { + /* Only restore the terminal if sudo is the foreground process. */ + const pid_t tcpgrp = tcgetpgrp(io_fds[SFD_USERTTY]); + if (tcpgrp == ec->ppgrp) { + if (!sudo_term_restore(io_fds[SFD_USERTTY], false)) + sudo_warn("%s", U_("unable to restore terminal settings")); + ec->term_raw = false; + } + } + + /* Update utmp */ + if (ISSET(ec->details->flags, CD_SET_UTMP) && ec->ptyname != NULL) + utmp_logout(ec->ptyname, wstatus); + + debug_return; +} + +/* + * Cleanup hook for sudo_fatal()/sudo_fatalx() + */ +static void +pty_cleanup_hook(void) +{ + pty_cleanup(&pty_ec, 0); +} + +/* + * Check whether sudo is running in the foreground. + * Updates the foreground flag in the closure. + * 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 *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) { + ec->foreground = ret == ec->ppgrp; + } + } + debug_return_int(ret); +} + +/* + * Restore the terminal when sudo is resumed in response to SIGCONT. + */ +static bool +resume_terminal(struct exec_closure *ec) +{ + debug_decl(resume_terminal, SUDO_DEBUG_EXEC); + + if (check_foreground(ec) == -1) { + /* User's tty was revoked. */ + debug_return_bool(false); + } + + /* Update the pty settings based on the user's terminal. */ + if (!sudo_term_copy(io_fds[SFD_USERTTY], io_fds[SFD_LEADER])) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to copy terminal settings to pty", __func__); + debug_return_bool(false); + } + sync_ttysize(ec); + + sudo_debug_printf(SUDO_DEBUG_INFO, "parent is in %s (%s -> %s)", + ec->foreground ? "foreground" : "background", + ec->term_raw ? "raw" : "cooked", + ec->foreground ? "raw" : "cooked"); + + if (ec->foreground) { + /* Foreground process, set tty to raw mode. */ + ec->term_raw = sudo_term_raw(io_fds[SFD_USERTTY], term_raw_flags); + } else { + /* Background process, no access to tty. */ + ec->term_raw = false; + } + + debug_return_bool(true); +} + +/* + * 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_pty(struct exec_closure *ec, int signo) +{ + char signame[SIG2STR_MAX]; + struct sigaction sa, osa, saved_sigcont; + int ret = 0; + debug_decl(suspend_sudo_pty, SUDO_DEBUG_EXEC); + + /* + * Ignore SIGCONT when we suspend to avoid calling resume_terminal() + * multiple times. + */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_DFL; + if (sudo_sigaction(SIGCONT, &sa, &saved_sigcont) != 0) + sudo_warn("%s", U_("unable to set handler for SIGCONT")); + + if (sig2str(signo, signame) == -1) + (void)snprintf(signame, sizeof(signame), "%d", signo); + + 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 (!ec->foreground) { + if (check_foreground(ec) == -1) { + /* User's tty was revoked. */ + break; + } + } + if (ec->foreground) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: command received SIG%s, parent running in the foregound", + __func__, signame); + if (!ec->term_raw) { + if (sudo_term_raw(io_fds[SFD_USERTTY], term_raw_flags)) + ec->term_raw = true; + } + ret = SIGCONT_FG; /* resume command in foreground */ + break; + } + FALLTHROUGH; + case SIGSTOP: + case SIGTSTP: + default: + /* Flush any remaining output and deschedule I/O events. */ + del_io_events(true); + + /* Restore original tty mode before suspending. */ + if (ec->term_raw) { + if (!sudo_term_restore(io_fds[SFD_USERTTY], false)) + sudo_warn("%s", U_("unable to restore terminal settings")); + ec->term_raw = false; + } + + /* Log the suspend event. */ + log_suspend(ec, signo); + + /* Suspend self and continue command when we resume. */ + if (signo != SIGSTOP) { + if (sudo_sigaction(signo, &sa, &osa) != 0) + sudo_warn(U_("unable to set handler for SIG%s"), signame); + } + + /* + * We stop sudo's process group, even if sudo is not the process + * group leader. If we only send the signal to sudo itself, + * the shell will not notice if it is not in monitor mode. + * This can happen when sudo is run from a shell script, for + * example. In this case we need to signal the shell itself. + * If the process group leader is no longer present, we must kill + * the command since there will be no one to resume us. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: killpg(%d, SIG%s) [parent]", + __func__, (int)ec->ppgrp, signame); + if ((ec->ppgrp != ec->sudo_pid && kill(ec->ppgrp, 0) == -1) || + killpg(ec->ppgrp, signo) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: no parent to suspend, terminating command.", __func__); + terminate_command(ec->cmnd_pid, true); + ec->cmnd_pid = -1; + } + + if (signo != SIGSTOP) { + if (sudo_sigaction(signo, &osa, NULL) != 0) + sudo_warn(U_("unable to restore handler for SIG%s"), signame); + } + + /* If we failed to suspend, the command is no longer running. */ + if (ec->cmnd_pid == -1) + break; + + /* Log the resume event. */ + log_suspend(ec, SIGCONT); + + /* Update the pty's terminal settings and restore /dev/tty settings. */ + if (!resume_terminal(ec)) + break; + + /* + * We always resume the command in the foreground if sudo itself + * is the foreground process (and we were able to set /dev/tty to + * raw mode). 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. + */ + ret = ec->term_raw ? SIGCONT_FG : SIGCONT_BG; + break; + } + + if (sudo_sigaction(SIGCONT, &saved_sigcont, NULL) != 0) + sudo_warn("%s", U_("unable to restore handler for SIGCONT")); + + 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) { + /* Not an error, retry later. */ + 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, (unsigned int)n, iob)) { + terminate_command(iob->ec->cmnd_pid, true); + iob->ec->cmnd_pid = -1; + } + iob->len += (unsigned int)n; + /* Disable reader if buffer is full. */ + if (iob->len == sizeof(iob->buf)) + sudo_ev_del(evbase, iob->revent); + /* Enable writer now that there is new 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")); + } + break; + } + + debug_return; +} + +/* + * 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 %u 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, retry later. */ + 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 of %u bytes to fd %d", n, iob->len - iob->off, fd); + iob->off += (unsigned int)n; + /* Disable writer and reset the buffer if fully consumed. */ + if (iob->off == iob->len) { + iob->off = iob->len = 0; + sudo_ev_del(evbase, iob->wevent); + /* Forward the EOF from reader to writer. */ + if (iob->revent == NULL) { + safe_close(fd); + ev_free_by_fd(evbase, fd); + } + } + /* + * Enable reader if buffer is not full but avoid reading /dev/tty + * if not in raw mode or the command is no longer running. + */ + if (iob->revent != NULL && iob->len != sizeof(iob->buf)) { + if (!USERTTY_EVENT(iob->revent) || + (iob->ec->term_raw && iob->ec->cmnd_pid != -1)) { + if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1) + sudo_fatal("%s", U_("unable to add event to queue")); + } + } + } + + debug_return; +} + +/* + * We already closed the follower so reads from the leader will not block. + */ +static void +pty_finish(struct exec_closure *ec, struct command_status *cstat) +{ + int flags; + debug_decl(pty_finish, SUDO_DEBUG_EXEC); + + /* Flush any remaining output (the plugin already got it) and free bufs. */ + if (io_fds[SFD_USERTTY] != -1) { + flags = fcntl(io_fds[SFD_USERTTY], F_GETFL, 0); + if (flags != -1 && ISSET(flags, O_NONBLOCK)) { + CLR(flags, O_NONBLOCK); + (void) fcntl(io_fds[SFD_USERTTY], F_SETFL, flags); + } + } + del_io_events(false); + free_io_bufs(); + + /* Restore terminal settings and update utmp. */ + pty_cleanup(ec, cstat->type == CMD_WSTATUS ? cstat->val : 0); + + debug_return; +} + +/* + * Send command status to the monitor (currently just signal forwarding). + */ +static void +send_command_status(struct exec_closure *ec, int type, int val) +{ + struct monitor_message *msg; + debug_decl(send_command_status, 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(&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 *ec, int signo) +{ + debug_decl(schedule_signal, SUDO_DEBUG_EXEC); + + if (signo == 0) + debug_return; + + if (sudo_debug_needed(SUDO_DEBUG_DIAG)) { + char signame[SIG2STR_MAX]; + 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; +} + +/* + * Free any remaining monitor messages in the queue. + */ +static void +flush_monitor_messages(void) +{ + struct monitor_message *msg; + debug_decl(flush_monitor_messages, SUDO_DEBUG_EXEC); + + while ((msg = TAILQ_FIRST(&monitor_messages)) != NULL) { + TAILQ_REMOVE(&monitor_messages, msg, entries); + free(msg); + } + + debug_return; +} + +static void +backchannel_cb(int fd, int what, void *v) +{ + struct exec_closure *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); + if (ISSET(ec->details->flags, CD_USE_PTRACE)) { + /* Try to seize control of the command using ptrace(2). */ + int rc = exec_ptrace_seize(ec->cmnd_pid); + if (rc == 0) { + /* There is another tracer present. */ + CLR(ec->details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS|CD_USE_PTRACE); + } else if (rc == -1) { + if (ec->cstat->type == CMD_INVALID) { + ec->cstat->type = CMD_ERRNO; + ec->cstat->val = errno; + } + sudo_ev_loopbreak(ec->evbase); + } + } + 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_pty(ec, WSTOPSIG(cstat.val)); + schedule_signal(ec, signo); + /* Re-enable I/O events */ + add_io_events(ec); + } 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 *ec) +{ + int n, status; + pid_t pid; + debug_decl(handle_sigchld_pty, SUDO_DEBUG_EXEC); + + /* There may be multiple children in intercept mode. */ + for (;;) { + do { + pid = waitpid(-1, &status, __WALL|WUNTRACED|WNOHANG); + } while (pid == -1 && errno == EINTR); + switch (pid) { + case -1: + if (errno != ECHILD) { + sudo_warn(U_("%s: %s"), __func__, "waitpid"); + debug_return; + } + FALLTHROUGH; + case 0: + /* Nothing left to wait for. */ + debug_return; + } + + if (WIFEXITED(status)) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: process %d exited: %d", + __func__, (int)pid, WEXITSTATUS(status)); + if (pid == ec->monitor_pid) + ec->monitor_pid = -1; + } else if (WIFSIGNALED(status)) { + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + 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: process %d killed, SIG%s", + __func__, (int)pid, signame); + } + if (pid == ec->monitor_pid) + ec->monitor_pid = -1; + } else if (WIFSTOPPED(status)) { + if (pid != ec->monitor_pid) { + if (ISSET(ec->details->flags, CD_USE_PTRACE)) + exec_ptrace_stopped(pid, status, ec->intercept); + continue; + } + + /* + * 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. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "monitor stopped, suspending sudo"); + n = suspend_sudo_pty(ec, WSTOPSIG(status)); + kill(pid, SIGCONT); + schedule_signal(ec, n); + /* Re-enable I/O events */ + add_io_events(ec); + } else { + sudo_debug_printf(SUDO_DEBUG_WARN, + "%s: unexpected wait status 0x%x for process (%d)", + __func__, status, (int)pid); + } + } +} + +/* Signal callback */ +static void +signal_cb_pty(int signo, int what, void *v) +{ + struct sudo_ev_siginfo_container *sc = v; + struct exec_closure *ec = sc->closure; + debug_decl(signal_cb_pty, SUDO_DEBUG_EXEC); + + if (ec->monitor_pid == -1) + debug_return; + + if (sudo_debug_needed(SUDO_DEBUG_DIAG)) { + char signame[SIG2STR_MAX]; + 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 SIGCONT: + resume_terminal(ec); + break; + case SIGWINCH: + sync_ttysize(ec); + break; + default: + /* + * Do not forward signals sent by the command itself or a member of the + * command's process group (but only when either sudo or the command is + * the process group leader). 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; + + if (sc->siginfo->si_pid == ec->cmnd_pid) + debug_return; + si_pgrp = getpgid(sc->siginfo->si_pid); + if (si_pgrp != -1) { + if (si_pgrp == ec->cmnd_pid || si_pgrp == ec->sudo_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 *ec = v; + struct monitor_message *msg; + ssize_t nsent; + debug_decl(fwdchannel_cb, SUDO_DEBUG_EXEC); + + while ((msg = TAILQ_FIRST(&monitor_messages)) != NULL) { + switch (msg->cstat.type) { + case CMD_SIGNO: + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + char signame[SIG2STR_MAX]; + 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; + 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(&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); + flush_monitor_messages(); + /* 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(struct exec_closure *ec, struct command_status *cstat, + struct command_details *details, const struct user_details *user_details, + struct sudo_event_base *evbase, pid_t sudo_pid, pid_t ppgrp, int backchannel) +{ + debug_decl(fill_exec_closure, SUDO_DEBUG_EXEC); + + /* Fill in the non-event part of the closure. */ + ec->sudo_pid = sudo_pid; + ec->ppgrp = ppgrp; + ec->cmnd_pid = -1; + ec->cstat = cstat; + ec->details = details; + ec->rows = user_details->ts_rows; + ec->cols = user_details->ts_cols; + + /* Reset cstat for running the command. */ + cstat->type = CMD_INVALID; + cstat->val = 0; + + /* Setup event base and events. */ + ec->evbase = evbase; + + /* 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->sigcont_event = sudo_ev_alloc(SIGCONT, + SUDO_EV_SIGINFO, signal_cb_pty, 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")); + + 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; +} + +/* + * 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, + const struct user_details *user_details, struct sudo_event_base *evbase, + struct command_status *cstat) +{ + int io_pipe[3][2] = { { -1, -1 }, { -1, -1 }, { -1, -1 } }; + bool interpose[3] = { false, false, false }; + struct stat sb, tty_sbuf, *tty_sb = NULL; + int sv[2], intercept_sv[2] = { -1, -1 }; + struct exec_closure *ec = &pty_ec; + struct plugin_container *plugin; + int evloop_retries = -1; + bool cmnd_foreground; + sigset_t set, oset; + struct sigaction sa; + pid_t ppgrp, sudo_pid; + debug_decl(exec_pty, SUDO_DEBUG_EXEC); + + /* + * Allocate a pty if sudo is running in a terminal. + */ + ec->ptyname = pty_setup(details); + if (ec->ptyname == NULL) + debug_return_bool(false); + + /* Register cleanup function */ + sudo_fatal_callback_register(pty_cleanup_hook); + + /* + * 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")); + + if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { + if (!ISSET(details->flags, CD_USE_PTRACE)) { + /* + * Allocate a socketpair for communicating with sudo_intercept.so. + * This must be inherited across exec, hence no FD_CLOEXEC. + */ + if (socketpair(PF_UNIX, SOCK_STREAM, 0, intercept_sv) == -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. + */ + init_ttyblock(); + ppgrp = getpgrp(); /* parent's pgrp, so child can signal us */ + sudo_pid = getpid(); + + /* 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. + */ + 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, read_callback, write_callback, ec); + } + + /* Read from pty leader, write to /dev/tty */ + io_buf_new(io_fds[SFD_LEADER], io_fds[SFD_USERTTY], + log_ttyout, read_callback, write_callback, ec); + + /* Are we the foreground process? */ + ec->foreground = tcgetpgrp(io_fds[SFD_USERTTY]) == ppgrp; + sudo_debug_printf(SUDO_DEBUG_INFO, "sudo is running in the %s", + ec->foreground ? "foreground" : "background"); + } + + /* + * If stdin, stdout or stderr is not the user's tty and logging is + * enabled, use a pipe to interpose ourselves instead of using the + * pty fd. We always use a pipe for stdin when in background mode. + */ + if (user_details->tty != NULL && stat(user_details->tty, &tty_sbuf) != -1) + tty_sb = &tty_sbuf; + + if (!fd_matches_tty(STDIN_FILENO, tty_sb, &sb)) { + if (!interpose[STDIN_FILENO]) { + /* Not logging stdin, do not interpose. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdin not user's tty, not logging"); + if (S_ISFIFO(sb.st_mode)) + SET(details->flags, CD_EXEC_BG); + 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 user's tty, creating a pipe"); + SET(details->flags, CD_EXEC_BG); + 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, read_callback, write_callback, ec); + io_fds[SFD_STDIN] = io_pipe[STDIN_FILENO][0]; + } + + if (ec->foreground && ppgrp != sudo_pid) { + /* + * If sudo is not the process group leader and stdin is not + * a tty we may be running as a background job via a shell + * script. Start the command in the background to avoid + * changing the terminal mode from a background process. + */ + SET(details->flags, CD_EXEC_BG); + } + } else if (ISSET(details->flags, CD_BACKGROUND)) { + /* + * Running in background (sudo -b), no access to terminal input. + * In non-pty mode, the command runs in an orphaned process + * group and reads from the controlling terminal fail with EIO. + * We cannot do the same while running in a pty but if we set + * stdin to a half-closed pipe, reads from it will get EOF. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "terminal input not available, creating empty pipe"); + SET(details->flags, CD_EXEC_BG); + if (pipe2(io_pipe[STDIN_FILENO], O_CLOEXEC) != 0) + sudo_fatal("%s", U_("unable to create pipe")); + io_fds[SFD_STDIN] = io_pipe[STDIN_FILENO][0]; + close(io_pipe[STDIN_FILENO][1]); + io_pipe[STDIN_FILENO][1] = -1; + } + if (!fd_matches_tty(STDOUT_FILENO, tty_sb, &sb)) { + if (!interpose[STDOUT_FILENO]) { + /* Not logging stdout, do not interpose. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "stdout not user's tty, not logging"); + if (S_ISFIFO(sb.st_mode)) { + SET(details->flags, CD_EXEC_BG); + term_raw_flags = SUDO_TERM_OFLAG; + } + 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 user's tty, creating a pipe"); + SET(details->flags, CD_EXEC_BG); + term_raw_flags = SUDO_TERM_OFLAG; + 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, read_callback, write_callback, ec); + io_fds[SFD_STDOUT] = io_pipe[STDOUT_FILENO][1]; + } + } + if (!fd_matches_tty(STDERR_FILENO, tty_sb, &sb)) { + if (!interpose[STDERR_FILENO]) { + /* Not logging stderr, do not interpose. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "stderr not user's tty, not logging"); + io_fds[SFD_STDERR] = dup(STDERR_FILENO); + if (io_fds[SFD_STDERR] == -1) + sudo_fatal("dup"); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "stderr /dev/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, read_callback, write_callback, ec); + io_fds[SFD_STDERR] = io_pipe[STDERR_FILENO][1]; + } + } + + /* + * Copy terminal settings from user tty -> pty. If sudo is a + * background process, we'll re-init the pty when foregrounded. + */ + if (!sudo_term_copy(io_fds[SFD_USERTTY], io_fds[SFD_LEADER])) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, + "%s: unable to copy terminal settings to pty", __func__); + ec->foreground = false; + } + /* Start in raw mode unless the command will run in the background. */ + cmnd_foreground = ec->foreground && !ISSET(details->flags, CD_EXEC_BG); + if (cmnd_foreground) { + if (sudo_term_raw(io_fds[SFD_USERTTY], 0)) + ec->term_raw = true; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: follower: %d, stdin: %d, stdout: %d, stderr: %d", __func__, + io_fds[SFD_FOLLOWER], io_fds[SFD_STDIN], io_fds[SFD_STDOUT], + io_fds[SFD_STDERR]); + + /* + * 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_bool(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]); + if (intercept_sv[0] != -1) + close(intercept_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]); + + /* Only run the cleanup hook in the parent. */ + sudo_fatal_callback_deregister(pty_cleanup_hook); + + /* + * If stdin/stdout is not the user's tty, start the 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, cmnd_foreground, sv[1], intercept_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); + /* NOTREACHED */ + } + + /* + * 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; + if (send(sv[0], cstat, sizeof(*cstat), 0) == -1) + 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(ec, cstat, details, user_details, evbase, + sudo_pid, ppgrp, sv[0]); + + /* Create event and closure for intercept mode. */ + if (ISSET(details->flags, CD_INTERCEPT|CD_LOG_SUBCMDS)) { + ec->intercept = intercept_setup(intercept_sv[0], ec->evbase, details); + if (ec->intercept == NULL) + terminate_command(ec->cmnd_pid, true); + } + + /* 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); + 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); + + /* De-register cleanup hook, the pty is going away. */ + sudo_fatal_callback_deregister(pty_cleanup_hook); + + /* Flush any remaining output, free I/O bufs and events, do logout. */ + pty_finish(ec, cstat); + + /* Free things up. */ + free_exec_closure(ec); + + debug_return_bool(true); +} + +/* + * Propagate tty size change to pty being used by the command, pass + * new window size to I/O plugins and deliver SIGWINCH to the command. + */ +static void +sync_ttysize(struct exec_closure *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) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %d x %d -> %hd x %hd", + __func__, ec->rows, ec->cols, wsize.ws_row, wsize.ws_col); + + /* Log window change event. */ + log_winchange(ec, wsize.ws_row, wsize.ws_col); + + /* Update pty window size and send command SIGWINCH. */ + (void)ioctl(io_fds[SFD_LEADER], IOCTL_REQ_CAST TIOCSWINSZ, &wsize); + killpg(ec->cmnd_pid, SIGWINCH); + + /* Update rows/cols. */ + ec->rows = wsize.ws_row; + ec->cols = wsize.ws_col; + } + } + + debug_return; +} diff --git a/src/get_pty.c b/src/get_pty.c new file mode 100644 index 0000000..666a295 --- /dev/null +++ b/src/get_pty.c @@ -0,0 +1,189 @@ +/* + * 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) +char * +get_pty(int *leader, int *follower, uid_t ttyuid) +{ + struct group *gr; + gid_t ttygid = (gid_t)-1; + char name[PATH_MAX]; + char *ret = NULL; + 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 = strdup(name); + } + + debug_return_str(ret); +} + +#elif defined(HAVE__GETPTY) +char * +get_pty(int *leader, int *follower, uid_t ttyuid) +{ + char *line; + char *ret = NULL; + 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); + ret = strdup(line); + } else { + close(*leader); + *leader = -1; + } + } + debug_return_str(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 */ + +char * +get_pty(int *leader, int *follower, uid_t ttyuid) +{ + char *line, *ret = NULL; + 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); + ret = strdup(line); + } +done: + debug_return_str(ret); +} + +#else /* Old-style BSD ptys */ + +static char line[] = _PATH_DEV "ptyXX"; + +char * +get_pty(int *leader, int *follower, uid_t ttyuid) +{ + char *bank, *cp; + struct group *gr; + gid_t ttygid = -1; + char *ret = NULL; + 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) { + ret = strdup(line); + goto done; + } + (void) close(*leader); + } + } +done: + debug_return_str(ret); +} +#endif /* HAVE_OPENPTY */ diff --git a/src/hooks.c b/src/hooks.c new file mode 100644 index 0000000..8ef3e93 --- /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/intercept.exp.in b/src/intercept.exp.in new file mode 100644 index 0000000..cb22c8a --- /dev/null +++ b/src/intercept.exp.in @@ -0,0 +1,7 @@ +@INTERCEPT_EXP@execl +execle +execlp +execv +execve +execvp +system diff --git a/src/intercept.pb-c.c b/src/intercept.pb-c.c new file mode 100644 index 0000000..2c58e69 --- /dev/null +++ b/src/intercept.pb-c.c @@ -0,0 +1,842 @@ +/* Generated by the protocol buffer compiler. DO NOT EDIT! */ +/* Generated from: intercept.proto */ + +/* Do not generate deprecated warnings for self */ +#ifndef PROTOBUF_C__NO_DEPRECATED +#define PROTOBUF_C__NO_DEPRECATED +#endif + +#include <intercept.pb-c.h> +void intercept_request__init + (InterceptRequest *message) +{ + static const InterceptRequest init_value = INTERCEPT_REQUEST__INIT; + *message = init_value; +} +size_t intercept_request__get_packed_size + (const InterceptRequest *message) +{ + assert(message->base.descriptor == &intercept_request__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t intercept_request__pack + (const InterceptRequest *message, + uint8_t *out) +{ + assert(message->base.descriptor == &intercept_request__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t intercept_request__pack_to_buffer + (const InterceptRequest *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &intercept_request__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +InterceptRequest * + intercept_request__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (InterceptRequest *) + protobuf_c_message_unpack (&intercept_request__descriptor, + allocator, len, data); +} +void intercept_request__free_unpacked + (InterceptRequest *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &intercept_request__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void intercept_hello__init + (InterceptHello *message) +{ + static const InterceptHello init_value = INTERCEPT_HELLO__INIT; + *message = init_value; +} +size_t intercept_hello__get_packed_size + (const InterceptHello *message) +{ + assert(message->base.descriptor == &intercept_hello__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t intercept_hello__pack + (const InterceptHello *message, + uint8_t *out) +{ + assert(message->base.descriptor == &intercept_hello__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t intercept_hello__pack_to_buffer + (const InterceptHello *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &intercept_hello__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +InterceptHello * + intercept_hello__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (InterceptHello *) + protobuf_c_message_unpack (&intercept_hello__descriptor, + allocator, len, data); +} +void intercept_hello__free_unpacked + (InterceptHello *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &intercept_hello__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void hello_response__init + (HelloResponse *message) +{ + static const HelloResponse init_value = HELLO_RESPONSE__INIT; + *message = init_value; +} +size_t hello_response__get_packed_size + (const HelloResponse *message) +{ + assert(message->base.descriptor == &hello_response__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t hello_response__pack + (const HelloResponse *message, + uint8_t *out) +{ + assert(message->base.descriptor == &hello_response__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t hello_response__pack_to_buffer + (const HelloResponse *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &hello_response__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +HelloResponse * + hello_response__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (HelloResponse *) + protobuf_c_message_unpack (&hello_response__descriptor, + allocator, len, data); +} +void hello_response__free_unpacked + (HelloResponse *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &hello_response__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void policy_check_request__init + (PolicyCheckRequest *message) +{ + static const PolicyCheckRequest init_value = POLICY_CHECK_REQUEST__INIT; + *message = init_value; +} +size_t policy_check_request__get_packed_size + (const PolicyCheckRequest *message) +{ + assert(message->base.descriptor == &policy_check_request__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t policy_check_request__pack + (const PolicyCheckRequest *message, + uint8_t *out) +{ + assert(message->base.descriptor == &policy_check_request__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t policy_check_request__pack_to_buffer + (const PolicyCheckRequest *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &policy_check_request__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +PolicyCheckRequest * + policy_check_request__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (PolicyCheckRequest *) + protobuf_c_message_unpack (&policy_check_request__descriptor, + allocator, len, data); +} +void policy_check_request__free_unpacked + (PolicyCheckRequest *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &policy_check_request__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void policy_accept_message__init + (PolicyAcceptMessage *message) +{ + static const PolicyAcceptMessage init_value = POLICY_ACCEPT_MESSAGE__INIT; + *message = init_value; +} +size_t policy_accept_message__get_packed_size + (const PolicyAcceptMessage *message) +{ + assert(message->base.descriptor == &policy_accept_message__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t policy_accept_message__pack + (const PolicyAcceptMessage *message, + uint8_t *out) +{ + assert(message->base.descriptor == &policy_accept_message__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t policy_accept_message__pack_to_buffer + (const PolicyAcceptMessage *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &policy_accept_message__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +PolicyAcceptMessage * + policy_accept_message__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (PolicyAcceptMessage *) + protobuf_c_message_unpack (&policy_accept_message__descriptor, + allocator, len, data); +} +void policy_accept_message__free_unpacked + (PolicyAcceptMessage *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &policy_accept_message__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void policy_reject_message__init + (PolicyRejectMessage *message) +{ + static const PolicyRejectMessage init_value = POLICY_REJECT_MESSAGE__INIT; + *message = init_value; +} +size_t policy_reject_message__get_packed_size + (const PolicyRejectMessage *message) +{ + assert(message->base.descriptor == &policy_reject_message__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t policy_reject_message__pack + (const PolicyRejectMessage *message, + uint8_t *out) +{ + assert(message->base.descriptor == &policy_reject_message__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t policy_reject_message__pack_to_buffer + (const PolicyRejectMessage *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &policy_reject_message__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +PolicyRejectMessage * + policy_reject_message__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (PolicyRejectMessage *) + protobuf_c_message_unpack (&policy_reject_message__descriptor, + allocator, len, data); +} +void policy_reject_message__free_unpacked + (PolicyRejectMessage *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &policy_reject_message__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void policy_error_message__init + (PolicyErrorMessage *message) +{ + static const PolicyErrorMessage init_value = POLICY_ERROR_MESSAGE__INIT; + *message = init_value; +} +size_t policy_error_message__get_packed_size + (const PolicyErrorMessage *message) +{ + assert(message->base.descriptor == &policy_error_message__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t policy_error_message__pack + (const PolicyErrorMessage *message, + uint8_t *out) +{ + assert(message->base.descriptor == &policy_error_message__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t policy_error_message__pack_to_buffer + (const PolicyErrorMessage *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &policy_error_message__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +PolicyErrorMessage * + policy_error_message__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (PolicyErrorMessage *) + protobuf_c_message_unpack (&policy_error_message__descriptor, + allocator, len, data); +} +void policy_error_message__free_unpacked + (PolicyErrorMessage *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &policy_error_message__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +void intercept_response__init + (InterceptResponse *message) +{ + static const InterceptResponse init_value = INTERCEPT_RESPONSE__INIT; + *message = init_value; +} +size_t intercept_response__get_packed_size + (const InterceptResponse *message) +{ + assert(message->base.descriptor == &intercept_response__descriptor); + return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); +} +size_t intercept_response__pack + (const InterceptResponse *message, + uint8_t *out) +{ + assert(message->base.descriptor == &intercept_response__descriptor); + return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); +} +size_t intercept_response__pack_to_buffer + (const InterceptResponse *message, + ProtobufCBuffer *buffer) +{ + assert(message->base.descriptor == &intercept_response__descriptor); + return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); +} +InterceptResponse * + intercept_response__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data) +{ + return (InterceptResponse *) + protobuf_c_message_unpack (&intercept_response__descriptor, + allocator, len, data); +} +void intercept_response__free_unpacked + (InterceptResponse *message, + ProtobufCAllocator *allocator) +{ + if(!message) + return; + assert(message->base.descriptor == &intercept_response__descriptor); + protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); +} +static const ProtobufCFieldDescriptor intercept_request__field_descriptors[2] = +{ + { + "policy_check_req", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(InterceptRequest, type_case), + offsetof(InterceptRequest, u.policy_check_req), + &policy_check_request__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "hello", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(InterceptRequest, type_case), + offsetof(InterceptRequest, u.hello), + &intercept_hello__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned intercept_request__field_indices_by_name[] = { + 1, /* field[1] = hello */ + 0, /* field[0] = policy_check_req */ +}; +static const ProtobufCIntRange intercept_request__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 2 } +}; +const ProtobufCMessageDescriptor intercept_request__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "InterceptRequest", + "InterceptRequest", + "InterceptRequest", + "", + sizeof(InterceptRequest), + 2, + intercept_request__field_descriptors, + intercept_request__field_indices_by_name, + 1, intercept_request__number_ranges, + (ProtobufCMessageInit) intercept_request__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor intercept_hello__field_descriptors[1] = +{ + { + "pid", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_INT32, + 0, /* quantifier_offset */ + offsetof(InterceptHello, pid), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned intercept_hello__field_indices_by_name[] = { + 0, /* field[0] = pid */ +}; +static const ProtobufCIntRange intercept_hello__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 1 } +}; +const ProtobufCMessageDescriptor intercept_hello__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "InterceptHello", + "InterceptHello", + "InterceptHello", + "", + sizeof(InterceptHello), + 1, + intercept_hello__field_descriptors, + intercept_hello__field_indices_by_name, + 1, intercept_hello__number_ranges, + (ProtobufCMessageInit) intercept_hello__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor hello_response__field_descriptors[4] = +{ + { + "token_lo", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_FIXED64, + 0, /* quantifier_offset */ + offsetof(HelloResponse, token_lo), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "token_hi", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_FIXED64, + 0, /* quantifier_offset */ + offsetof(HelloResponse, token_hi), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "portno", + 3, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_INT32, + 0, /* quantifier_offset */ + offsetof(HelloResponse, portno), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "log_only", + 4, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_BOOL, + 0, /* quantifier_offset */ + offsetof(HelloResponse, log_only), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned hello_response__field_indices_by_name[] = { + 3, /* field[3] = log_only */ + 2, /* field[2] = portno */ + 1, /* field[1] = token_hi */ + 0, /* field[0] = token_lo */ +}; +static const ProtobufCIntRange hello_response__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 4 } +}; +const ProtobufCMessageDescriptor hello_response__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "HelloResponse", + "HelloResponse", + "HelloResponse", + "", + sizeof(HelloResponse), + 4, + hello_response__field_descriptors, + hello_response__field_indices_by_name, + 1, hello_response__number_ranges, + (ProtobufCMessageInit) hello_response__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor policy_check_request__field_descriptors[5] = +{ + { + "command", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(PolicyCheckRequest, command), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "cwd", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(PolicyCheckRequest, cwd), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "argv", + 3, + PROTOBUF_C_LABEL_REPEATED, + PROTOBUF_C_TYPE_STRING, + offsetof(PolicyCheckRequest, n_argv), + offsetof(PolicyCheckRequest, argv), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "envp", + 4, + PROTOBUF_C_LABEL_REPEATED, + PROTOBUF_C_TYPE_STRING, + offsetof(PolicyCheckRequest, n_envp), + offsetof(PolicyCheckRequest, envp), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "intercept_fd", + 5, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_INT32, + 0, /* quantifier_offset */ + offsetof(PolicyCheckRequest, intercept_fd), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned policy_check_request__field_indices_by_name[] = { + 2, /* field[2] = argv */ + 0, /* field[0] = command */ + 1, /* field[1] = cwd */ + 3, /* field[3] = envp */ + 4, /* field[4] = intercept_fd */ +}; +static const ProtobufCIntRange policy_check_request__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 5 } +}; +const ProtobufCMessageDescriptor policy_check_request__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "PolicyCheckRequest", + "PolicyCheckRequest", + "PolicyCheckRequest", + "", + sizeof(PolicyCheckRequest), + 5, + policy_check_request__field_descriptors, + policy_check_request__field_indices_by_name, + 1, policy_check_request__number_ranges, + (ProtobufCMessageInit) policy_check_request__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor policy_accept_message__field_descriptors[3] = +{ + { + "run_command", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(PolicyAcceptMessage, run_command), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "run_argv", + 2, + PROTOBUF_C_LABEL_REPEATED, + PROTOBUF_C_TYPE_STRING, + offsetof(PolicyAcceptMessage, n_run_argv), + offsetof(PolicyAcceptMessage, run_argv), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "run_envp", + 3, + PROTOBUF_C_LABEL_REPEATED, + PROTOBUF_C_TYPE_STRING, + offsetof(PolicyAcceptMessage, n_run_envp), + offsetof(PolicyAcceptMessage, run_envp), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned policy_accept_message__field_indices_by_name[] = { + 1, /* field[1] = run_argv */ + 0, /* field[0] = run_command */ + 2, /* field[2] = run_envp */ +}; +static const ProtobufCIntRange policy_accept_message__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 3 } +}; +const ProtobufCMessageDescriptor policy_accept_message__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "PolicyAcceptMessage", + "PolicyAcceptMessage", + "PolicyAcceptMessage", + "", + sizeof(PolicyAcceptMessage), + 3, + policy_accept_message__field_descriptors, + policy_accept_message__field_indices_by_name, + 1, policy_accept_message__number_ranges, + (ProtobufCMessageInit) policy_accept_message__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor policy_reject_message__field_descriptors[1] = +{ + { + "reject_message", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(PolicyRejectMessage, reject_message), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned policy_reject_message__field_indices_by_name[] = { + 0, /* field[0] = reject_message */ +}; +static const ProtobufCIntRange policy_reject_message__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 1 } +}; +const ProtobufCMessageDescriptor policy_reject_message__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "PolicyRejectMessage", + "PolicyRejectMessage", + "PolicyRejectMessage", + "", + sizeof(PolicyRejectMessage), + 1, + policy_reject_message__field_descriptors, + policy_reject_message__field_indices_by_name, + 1, policy_reject_message__number_ranges, + (ProtobufCMessageInit) policy_reject_message__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor policy_error_message__field_descriptors[1] = +{ + { + "error_message", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(PolicyErrorMessage, error_message), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned policy_error_message__field_indices_by_name[] = { + 0, /* field[0] = error_message */ +}; +static const ProtobufCIntRange policy_error_message__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 1 } +}; +const ProtobufCMessageDescriptor policy_error_message__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "PolicyErrorMessage", + "PolicyErrorMessage", + "PolicyErrorMessage", + "", + sizeof(PolicyErrorMessage), + 1, + policy_error_message__field_descriptors, + policy_error_message__field_indices_by_name, + 1, policy_error_message__number_ranges, + (ProtobufCMessageInit) policy_error_message__init, + NULL,NULL,NULL /* reserved[123] */ +}; +static const ProtobufCFieldDescriptor intercept_response__field_descriptors[4] = +{ + { + "hello_resp", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(InterceptResponse, type_case), + offsetof(InterceptResponse, u.hello_resp), + &hello_response__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "accept_msg", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(InterceptResponse, type_case), + offsetof(InterceptResponse, u.accept_msg), + &policy_accept_message__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "reject_msg", + 3, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(InterceptResponse, type_case), + offsetof(InterceptResponse, u.reject_msg), + &policy_reject_message__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "error_msg", + 4, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(InterceptResponse, type_case), + offsetof(InterceptResponse, u.error_msg), + &policy_error_message__descriptor, + NULL, + 0 | PROTOBUF_C_FIELD_FLAG_ONEOF, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned intercept_response__field_indices_by_name[] = { + 1, /* field[1] = accept_msg */ + 3, /* field[3] = error_msg */ + 0, /* field[0] = hello_resp */ + 2, /* field[2] = reject_msg */ +}; +static const ProtobufCIntRange intercept_response__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 4 } +}; +const ProtobufCMessageDescriptor intercept_response__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "InterceptResponse", + "InterceptResponse", + "InterceptResponse", + "", + sizeof(InterceptResponse), + 4, + intercept_response__field_descriptors, + intercept_response__field_indices_by_name, + 1, intercept_response__number_ranges, + (ProtobufCMessageInit) intercept_response__init, + NULL,NULL,NULL /* reserved[123] */ +}; diff --git a/src/intercept.proto b/src/intercept.proto new file mode 100644 index 0000000..53a068a --- /dev/null +++ b/src/intercept.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +/* + * Intercept message from sudo_intercept.so. Messages on the + * wire are prefixed with a 32-bit size in network byte order. + */ +message InterceptRequest { + oneof type { + PolicyCheckRequest policy_check_req = 1; + InterceptHello hello = 2; + } +} + +/* + * Hello message from sudo_intercept.so to main sudo process. + * Sudo sends back the token and localhost port number. + */ +message InterceptHello { + int32 pid = 1; +} + +/* + * Sudo response to an InterceptHello from sudo_intercept.so. + * The client uses the port number and token to connect back to sudo. + * If log_only is set there is no InterceptResponse to a PolicyCheckRequest. + */ +message HelloResponse { + fixed64 token_lo = 1; + fixed64 token_hi = 2; + int32 portno = 3; + bool log_only = 4; +} + +/* + * Policy check request from sudo_intercept.so. + * Note that the plugin API only currently supports passing + * the new environment in to the open() function. + */ +message PolicyCheckRequest { + string command = 1; + string cwd = 2; + repeated string argv = 3; + repeated string envp = 4; + int32 intercept_fd = 5; +} + +message PolicyAcceptMessage { + string run_command = 1; + repeated string run_argv = 2; + repeated string run_envp = 3; +} + +message PolicyRejectMessage { + string reject_message = 1; +} + +message PolicyErrorMessage { + string error_message = 1; +} + +/* + * Response sent back to sudo_intercept.so. + */ +message InterceptResponse { + oneof type { + HelloResponse hello_resp = 1; + PolicyAcceptMessage accept_msg = 2; + PolicyRejectMessage reject_msg = 3; + PolicyErrorMessage error_msg = 4; + } +} diff --git a/src/limits.c b/src/limits.c new file mode 100644 index 0000000..2ed69c3 --- /dev/null +++ b/src/limits.c @@ -0,0 +1,714 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1999-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 <stdlib.h> +#include <string.h> +#include <unistd.h> +#ifdef __linux__ +# include <sys/prctl.h> +#endif +#include <ctype.h> +#include <errno.h> +#include <limits.h> + +#include <sudo.h> + +/* + * Avoid using RLIM_INFINITY for the nofile soft limit to prevent + * closefrom_fallback() from closing too many file descriptors. + */ +#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 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 */ + bool policy; /* true if policy specified an rlimit */ + bool preserve; /* true if policy says to preserve user limit */ + rlim_t minlimit; /* only modify limit if less than this 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 */ + struct rlimit policylimit; /* limit from policy, valid if policy is true */ +} saved_limits[] = { +#ifdef RLIMIT_AS + { + "rlimit_as", + RLIMIT_AS, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + 1 * 1024 * 1024 * 1024, /* minlimit */ + NULL, /* fallback */ + { RLIM_INFINITY, RLIM_INFINITY } /* newlimit */ + }, +#endif + { + "rlimit_core", + RLIMIT_CORE, + false /* override */ + }, + { + "rlimit_cpu", + RLIMIT_CPU, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + RLIM_INFINITY, /* minlimit */ + NULL, + { RLIM_INFINITY, RLIM_INFINITY } + }, + { + "rlimit_data", + RLIMIT_DATA, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + 1 * 1024 * 1024 * 1024, /* minlimit */ + NULL, + { RLIM_INFINITY, RLIM_INFINITY } + }, + { + "rlimit_fsize", + RLIMIT_FSIZE, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + RLIM_INFINITY, /* minlimit */ + NULL, + { RLIM_INFINITY, RLIM_INFINITY } + }, +#ifdef RLIMIT_LOCKS + { + "rlimit_locks", + RLIMIT_LOCKS, + false /* override */ + }, +#endif +#ifdef RLIMIT_MEMLOCK + { + "rlimit_memlock", + RLIMIT_MEMLOCK, + false /* override */ + }, +#endif + { + "rlimit_nofile", + RLIMIT_NOFILE, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + SUDO_OPEN_MAX, /* minlimit */ + NULL, + { SUDO_OPEN_MAX, RLIM_INFINITY } + }, +#ifdef RLIMIT_NPROC + { + "rlimit_nproc", + RLIMIT_NPROC, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + RLIM_INFINITY, /* minlimit */ + NULL, + { RLIM_INFINITY, RLIM_INFINITY } + }, +#endif +#ifdef RLIMIT_RSS + { + "rlimit_rss", + RLIMIT_RSS, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + RLIM_INFINITY, /* minlimit */ + NULL, + { RLIM_INFINITY, RLIM_INFINITY } + }, +#endif + { + "rlimit_stack", + RLIMIT_STACK, + true, /* override */ + false, /* saved */ + false, /* policy */ + false, /* preserve */ + SUDO_STACK_MIN, /* minlimit */ + &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) +{ + debug_decl(disable_coredump, SUDO_DEBUG_UTIL); + + if (getrlimit(RLIMIT_CORE, &corelimit) == 0) { + /* + * Set the soft limit to 0 but leave the existing hard limit. + * On Linux, we need CAP_SYS_RESOURCE to raise the hard limit + * which may not be the case in, e.g. an unprivileged container. + */ + struct rlimit rl = corelimit; + rl.rlim_cur = 0; + sudo_debug_printf(SUDO_DEBUG_INFO, + "RLIMIT_CORE [%lld, %lld] -> [%lld, %lld]", + (long long)corelimit.rlim_cur, (long long)corelimit.rlim_max, + (long long)rl.rlim_cur, (long long)rl.rlim_max); + if (setrlimit(RLIMIT_CORE, &rl) == -1) { + sudo_warn("setrlimit(RLIMIT_CORE)"); + } else { + coredump_disabled = true; +#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, 0, 0, 0, 0)"); + } +#endif /* __linux__ */ + } + } else { + sudo_warn("getrlimit(RLIMIT_CORE)"); + } + + 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) { + /* + * Do not warn about a failure to restore the core dump size limit. + * This is mostly harmless and should not happen in practice. + */ + 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 pass, 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(%s) -> [%lld, %lld]", lim->name, + (long long)lim->oldlimit.rlim_cur, + (long long)lim->oldlimit.rlim_max); + lim->saved = true; + + /* Only override the existing limit if it is smaller than minlimit. */ + if (lim->minlimit != RLIM_INFINITY) { + if (lim->oldlimit.rlim_cur >= lim->minlimit) + lim->override = false; + } + if (!lim->override) + continue; + + for (pass = 0; pass < 2; pass++) { + 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 (pass == 0 && lim->fallback != NULL) { + /* Try again using fallback values. */ + lim->newlimit.rlim_cur = lim->fallback->rlim_cur; + lim->newlimit.rlim_max = lim->fallback->rlim_max; + continue; + } + } + break; + } + 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; +} + +static bool +store_rlimit(const char *str, rlim_t *val, bool soft) +{ + const size_t inflen = sizeof("infinity") - 1; + debug_decl(store_rlimit, SUDO_DEBUG_UTIL); + + if (isdigit((unsigned char)*str)) { + unsigned long long ullval = 0; + char *ep; + + errno = 0; +#ifdef HAVE_STRTOULL + ullval = strtoull(str, &ep, 10); + if (str == ep || (errno == ERANGE && ullval == ULLONG_MAX)) + debug_return_bool(false); +#else + ullval = strtoul(str, &ep, 10); + if (str == ep || (errno == ERANGE && ullval == ULONG_MAX)) + debug_return_bool(false); +#endif + if (*ep == '\0' || (soft && *ep == ',')) { + *val = ullval; + debug_return_bool(true); + } + goto done; + } + if (strncmp(str, "infinity", inflen) == 0) { + if (str[inflen] == '\0' || (soft && str[inflen] == ',')) { + *val = RLIM_INFINITY; + debug_return_bool(true); + } + } +done: + debug_return_bool(false); +} + +static bool +set_policy_rlimit(int resource, const char *val) +{ + unsigned int idx; + debug_decl(set_policy_rlimit, SUDO_DEBUG_UTIL); + + for (idx = 0; idx < nitems(saved_limits); idx++) { + struct saved_limit *lim = &saved_limits[idx]; + const char *hard, *soft = val; + + if (lim->resource != resource) + continue; + + if (strcmp(val, "default") == 0) { + /* Use system-assigned limit set by begin_session(). */ + lim->policy = false; + lim->preserve = false; + debug_return_bool(true); + } + if (strcmp(val, "user") == 0) { + /* Preserve invoking user's limit. */ + lim->policy = false; + lim->preserve = true; + debug_return_bool(true); + } + + /* + * Expect limit in the form "soft,hard" or "limit" (both soft+hard). + */ + hard = strchr(val, ','); + if (hard != NULL) + hard++; + else + hard = soft; + + if (store_rlimit(soft, &lim->policylimit.rlim_cur, true) && + store_rlimit(hard, &lim->policylimit.rlim_max, false)) { + lim->policy = true; + lim->preserve = false; + debug_return_bool(true); + } + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "%s: invalid rlimit: %s", lim->name, val); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "invalid resource limit: %d", resource); + debug_return_bool(false); +} + +bool +parse_policy_rlimit(const char *str) +{ + bool ret = false; + debug_decl(parse_policy_rlimit, SUDO_DEBUG_UTIL); + +#ifdef RLIMIT_AS + if (strncmp(str, "as=", sizeof("as=") - 1) == 0) { + str += sizeof("as=") - 1; + ret = set_policy_rlimit(RLIMIT_AS, str); + } else +#endif +#ifdef RLIMIT_CORE + if (strncmp(str, "core=", sizeof("core=") - 1) == 0) { + str += sizeof("core=") - 1; + ret = set_policy_rlimit(RLIMIT_CORE, str); + } else +#endif +#ifdef RLIMIT_CPU + if (strncmp(str, "cpu=", sizeof("cpu=") - 1) == 0) { + str += sizeof("cpu=") - 1; + ret = set_policy_rlimit(RLIMIT_CPU, str); + } else +#endif +#ifdef RLIMIT_DATA + if (strncmp(str, "data=", sizeof("data=") - 1) == 0) { + str += sizeof("data=") - 1; + ret = set_policy_rlimit(RLIMIT_DATA, str); + } else +#endif +#ifdef RLIMIT_FSIZE + if (strncmp(str, "fsize=", sizeof("fsize=") - 1) == 0) { + str += sizeof("fsize=") - 1; + ret = set_policy_rlimit(RLIMIT_FSIZE, str); + } else +#endif +#ifdef RLIMIT_LOCKS + if (strncmp(str, "locks=", sizeof("locks=") - 1) == 0) { + str += sizeof("locks=") - 1; + ret = set_policy_rlimit(RLIMIT_LOCKS, str); + } else +#endif +#ifdef RLIMIT_MEMLOCK + if (strncmp(str, "memlock=", sizeof("memlock=") - 1) == 0) { + str += sizeof("memlock=") - 1; + ret = set_policy_rlimit(RLIMIT_MEMLOCK, str); + } else +#endif +#ifdef RLIMIT_NOFILE + if (strncmp(str, "nofile=", sizeof("nofile=") - 1) == 0) { + str += sizeof("nofile=") - 1; + ret = set_policy_rlimit(RLIMIT_NOFILE, str); + } else +#endif +#ifdef RLIMIT_NPROC + if (strncmp(str, "nproc=", sizeof("nproc=") - 1) == 0) { + str += sizeof("nproc=") - 1; + ret = set_policy_rlimit(RLIMIT_NPROC, str); + } else +#endif +#ifdef RLIMIT_RSS + if (strncmp(str, "rss=", sizeof("rss=") - 1) == 0) { + str += sizeof("rss=") - 1; + ret = set_policy_rlimit(RLIMIT_RSS, str); + } else +#endif +#ifdef RLIMIT_STACK + if (strncmp(str, "stack=", sizeof("stack=") - 1) == 0) { + str += sizeof("stack=") - 1; + ret = set_policy_rlimit(RLIMIT_STACK, str); + } +#endif + debug_return_bool(ret); +} + +/* + * Set resource limits as specified by the security policy (if any). + * This should be run as part of the session setup but after PAM, + * login.conf, etc. + */ +void +set_policy_rlimits(void) +{ + unsigned int idx; + debug_decl(set_policy_rlimits, SUDO_DEBUG_UTIL); + + for (idx = 0; idx < nitems(saved_limits); idx++) { + struct saved_limit *lim = &saved_limits[idx]; + struct rlimit *rl; + int rc; + + if (!lim->policy && (!lim->preserve || !lim->saved)) + continue; + + rl = lim->preserve ? &lim->oldlimit : &lim->policylimit; + if ((rc = setrlimit(lim->resource, rl)) == 0) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "setrlimit(%s, [%lld, %lld])", lim->name, + (long long)rl->rlim_cur, (long long)rl->rlim_max); + continue; + } + + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "setrlimit(%s, [%lld, %lld])", lim->name, + (long long)rl->rlim_cur, (long long)rl->rlim_max); + + if (rl->rlim_cur > lim->oldlimit.rlim_max || rl->rlim_max > lim->oldlimit.rlim_max) { + /* Try setting policy rlim_cur to old rlim_max. */ + if (rl->rlim_cur > lim->oldlimit.rlim_max) + rl->rlim_cur = lim->oldlimit.rlim_max; + if (rl->rlim_max > lim->oldlimit.rlim_max) + rl->rlim_max = lim->oldlimit.rlim_max; + if ((rc = setrlimit(lim->resource, rl)) == -1) { + 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); + } + } + if (rc == -1) + sudo_warn("setrlimit(%s)", lim->name); + } + + debug_return; +} + +size_t +serialize_rlimits(char **info, size_t info_max) +{ + char *str; + size_t idx, nstored = 0; + debug_decl(serialize_rlimits, 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[STRLEN_MAX_UNSIGNED(unsigned long long) + 1]; + char maxlim[STRLEN_MAX_UNSIGNED(unsigned long long) + 1]; + + 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_size_t(nstored); +oom: + while (nstored) + free(info[--nstored]); + debug_return_size_t((size_t)-1); +} diff --git a/src/load_plugins.c b/src/load_plugins.c new file mode 100644 index 0000000..df08b95 --- /dev/null +++ b/src/load_plugins.c @@ -0,0 +1,488 @@ +/* + * 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> + +#ifdef ENABLE_SUDO_PLUGIN_API +static bool +sudo_qualify_plugin(struct plugin_info *info, char *fullpath, size_t pathsize) +{ + const char *plugin_dir = sudo_conf_plugin_dir_path(); + int len; + debug_decl(sudo_stat_plugin, SUDO_DEBUG_PLUGIN); + + if (info->path[0] == '/') { + if (strlcpy(fullpath, info->path, pathsize) >= pathsize) { + errno = ENAMETOOLONG; + goto bad; + } + } else { +#ifdef STATIC_SUDOERS_PLUGIN + /* Check static symbols. */ + if (strcmp(info->path, _PATH_SUDOERS_PLUGIN) == 0) { + if (strlcpy(fullpath, info->path, pathsize) >= pathsize) { + errno = ENAMETOOLONG; + goto bad; + } + /* Plugin is static, do not fully-qualify. */ + debug_return_bool(true); + } +#endif /* STATIC_SUDOERS_PLUGIN */ + + if (plugin_dir == NULL) { + errno = ENOENT; + goto bad; + } + len = snprintf(fullpath, pathsize, "%s%s", plugin_dir, info->path); + if (len < 0 || (size_t)len >= pathsize) { + errno = ENAMETOOLONG; + goto bad; + } + } + debug_return_bool(true); +bad: + sudo_warnx(U_("error in %s, line %d while loading plugin \"%s\""), + _PATH_SUDO_CONF, info->lineno, info->symbol_name); + if (info->path[0] != '/' && plugin_dir != NULL) + sudo_warn("%s%s", plugin_dir, info->path); + else + sudo_warn("%s", info->path); + debug_return_bool(false); +} +#else +static bool +sudo_qualify_plugin(struct plugin_info *info, char *fullpath, size_t pathsize) +{ + debug_decl(sudo_qualify_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; + 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(plugin_exists, 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); + +static struct generic_plugin * +sudo_plugin_try_to_clone(void *so_handle, const char *symbol_name) +{ + debug_decl(sudo_plugin_try_to_clone, SUDO_DEBUG_PLUGIN); + struct generic_plugin *plugin = NULL; + plugin_clone_func *clone_func; + 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; + } + + 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); + + /* Fill in path from info and plugin dir. */ + if (!sudo_qualify_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. + */ +static 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(_PATH_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..678730f --- /dev/null +++ b/src/net_ifs.c @@ -0,0 +1,876 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1996, 1998-2005, 2007-2015, 2018-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 + */ + +/* + * 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> +#include <unistd.h> +#include <netdb.h> +#include <errno.h> +#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.h" + +/* Minix apparently lacks IFF_LOOPBACK */ +#ifndef IFF_LOOPBACK +# define IFF_LOOPBACK 0 +#endif + +#ifndef INET6_ADDRSTRLEN +# define INET6_ADDRSTRLEN 46 +#endif + +#ifndef INADDR_NONE +# define INADDR_NONE 0xffffffffU +#endif + +#if defined(STUB_LOAD_INTERFACES) || \ + !(defined(HAVE_GETIFADDRS) || defined(SIOCGIFCONF) || defined(SIOCGLIFCONF)) + +/* + * Stub function for those without SIOCGIFCONF or getifaddrs() + */ +int +get_net_ifs(char **addrinfo_out) +{ + debug_decl(get_net_ifs, SUDO_DEBUG_NETIF); + debug_return_int(0); +} + +#elif defined(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_out) +{ + struct ifaddrs *ifa, *ifaddrs; + struct sockaddr_in *sin4; +# ifdef HAVE_STRUCT_IN6_ADDR + struct sockaddr_in6 *sin6; +# endif + char addrstr[INET6_ADDRSTRLEN], maskstr[INET6_ADDRSTRLEN]; + char *addrinfo = NULL; + 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 = (size_t)num_interfaces * 2 * INET6_ADDRSTRLEN; + if ((cp = malloc(ailen)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + goto bad; + } + addrinfo = cp; + + 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: + sin4 = (struct sockaddr_in *)ifa->ifa_addr; + if (sin4->sin_addr.s_addr == INADDR_ANY || sin4->sin_addr.s_addr == INADDR_NONE) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET addr for %s", ifa->ifa_name); + continue; + } + if (inet_ntop(AF_INET, &sin4->sin_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET addr for %s", ifa->ifa_name); + continue; + } + sin4 = (struct sockaddr_in *)ifa->ifa_netmask; + if (inet_ntop(AF_INET, &sin4->sin_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET mask for %s", ifa->ifa_name); + continue; + } + break; +# ifdef HAVE_STRUCT_IN6_ADDR + case AF_INET6: + sin6 = (struct sockaddr_in6 *)ifa->ifa_addr; + if (IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET6 addr for %s", ifa->ifa_name); + continue; + } + if (inet_ntop(AF_INET6, &sin6->sin6_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 addr for %s", ifa->ifa_name); + continue; + } + sin6 = (struct sockaddr_in6 *)ifa->ifa_netmask; + if (inet_ntop(AF_INET6, &sin6->sin6_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 mask for %s", ifa->ifa_name); + continue; + } + break; +# endif /* HAVE_STRUCT_IN6_ADDR */ + default: + continue; + } + + /* Store the IP addr/netmask pairs. */ + 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 bad; + } + cp += len; + ailen -= (size_t)len; + } + *addrinfo_out = addrinfo; + goto done; + +bad: + free(addrinfo); + num_interfaces = -1; +done: +# ifdef HAVE_FREEIFADDRS + freeifaddrs(ifaddrs); +# else + free(ifaddrs); +# endif + debug_return_int(num_interfaces); +} + +#elif defined(SIOCGLIFCONF) + +# if defined(__hpux) + +/* + * Fill in the interfaces string with the machine's ip addresses and netmasks + * and return the number of interfaces found. Returns -1 on error. + * HP-UX has incompatible SIOCGLIFNUM and SIOCGLIFCONF ioctls. + */ +int +get_net_ifs(char **addrinfo_out) +{ + struct if_laddrconf laddrconf; + struct ifconf ifconf; + char addrstr[INET6_ADDRSTRLEN], maskstr[INET6_ADDRSTRLEN]; + char *addrinfo = NULL; + int i, n, sock4, sock6 = -1; + int 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); + + memset(&ifconf, 0, sizeof(ifconf)); + memset(&laddrconf, 0, sizeof(laddrconf)); + + /* Allocate and fill in the IPv4 interface list. */ + sock4 = socket(AF_INET, SOCK_DGRAM, 0); + if (sock4 != -1 && ioctl(sock4, SIOCGIFNUM, &n) != -1) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "SIOCGIFNUM reports %d interfaces", n); + n += 4; /* in case new interfaces come up */ + + ifconf.ifc_len = n * sizeof(struct ifreq); + ifconf.ifc_buf = malloc(ifconf.ifc_len); + if (ifconf.ifc_buf == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + goto bad; + } + + if (ioctl(sock4, SIOCGIFCONF, &ifconf) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to get interface list (SIOCGIFCONF)"); + goto bad; + } + } + + /* Allocate and fill in the IPv6 interface list. */ + sock6 = socket(AF_INET6, SOCK_DGRAM, 0); + if (sock6 != -1 && ioctl(sock6, SIOCGLIFNUM, &n) != -1) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "SIOCGLIFNUM reports %d interfaces", n); + n += 4; /* in case new interfaces come up */ + + laddrconf.iflc_len = n * sizeof(struct if_laddrreq); + laddrconf.iflc_buf = malloc(laddrconf.iflc_len); + if (laddrconf.iflc_buf == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + goto bad; + } + + if (ioctl(sock4, SIOCGLIFCONF, &laddrconf) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to get interface list (SIOCGLIFCONF)"); + goto bad; + } + } + + /* Allocate space for the maximum number of interfaces that could exist. */ + n = ifconf.ifc_len / sizeof(struct ifconf) + + laddrconf.iflc_len / sizeof(struct if_laddrreq); + if (n == 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"); + goto bad; + } + addrinfo = cp; + + /* + * For each interface, store the ip address and netmask. + * Keep a copy of the address family, else it will be overwritten. + */ + for (i = 0; i < ifconf.ifc_len; ) { + struct ifreq *ifr = (struct ifreq *)&ifconf.ifc_buf[i]; + struct sockaddr_in *sin4; + + /* Set i to the subscript of the next interface (no sa_len). */ + i += sizeof(struct ifreq); + + /* IPv4 only. */ + if (ifr->ifr_addr.sa_family != AF_INET) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected address family %d for %s", + ifr->ifr_addr.sa_family, ifr->ifr_name); + continue; + } + + /* Store the address. */ + sin4 = (struct sockaddr_in *)&ifr->ifr_addr; + if (sin4->sin_addr.s_addr == INADDR_ANY || sin4->sin_addr.s_addr == INADDR_NONE) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET addr for %s", ifr->ifr_name); + continue; + } + if (inet_ntop(AF_INET, &sin4->sin_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET addr for %s", ifr->ifr_name); + continue; + } + + /* Skip interfaces marked "down" and "loopback". */ + if (ioctl(sock4, SIOCGIFFLAGS, ifr) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFFLAGS for %s", ifr->ifr_name); + continue; + } + if (!ISSET(ifr->ifr_flags, IFF_UP) || + ISSET(ifr->ifr_flags, IFF_LOOPBACK)) + continue; + + /* Fetch and store the netmask. */ + if (ioctl(sock4, SIOCGIFNETMASK, ifr) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFNETMASK for %s", ifr->ifr_name); + continue; + } + + /* Convert the mask to string form. */ + sin4 = (struct sockaddr_in *)&ifr->ifr_addr; + if (inet_ntop(AF_INET, &sin4->sin_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET mask for %s", ifr->ifr_name); + continue; + } + + n = snprintf(cp, ailen, "%s%s/%s", + cp == addrinfo ? "" : " ", addrstr, maskstr); + if (n < 0 || (size_t)n >= ailen) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto bad; + } + cp += n; + ailen -= n; + + num_interfaces++; + } + for (i = 0; i < laddrconf.iflc_len; ) { + struct if_laddrreq *lreq = (struct if_laddrreq *)&laddrconf.iflc_buf[i]; + struct sockaddr_in6 *sin6; + + /* Set i to the subscript of the next interface (no sa_len). */ + i += sizeof(struct if_laddrreq); + + /* IPv6 only. */ + if (lreq->iflr_addr.sa_family != AF_INET6) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected address family %d for %s", + lreq->iflr_addr.sa_family, lreq->iflr_name); + continue; + } + + sin6 = (struct sockaddr_in6 *)&lreq->iflr_addr; + if (IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET6 addr for %s", lreq->iflr_name); + continue; + } + if (inet_ntop(AF_INET6, &sin6->sin6_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 addr for %s", lreq->iflr_name); + continue; + } + + /* Skip interfaces marked "down" and "loopback". */ + if (ioctl(sock6, SIOCGLIFFLAGS, lreq) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFFLAGS for %s", lreq->iflr_name); + continue; + } + if (!ISSET(lreq->iflr_flags, IFF_UP) || + ISSET(lreq->iflr_flags, IFF_LOOPBACK)) + continue; + + /* Fetch and store the netmask. */ + if (ioctl(sock6, SIOCGLIFNETMASK, lreq) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFNETMASK for %s", lreq->iflr_name); + continue; + } + sin6 = (struct sockaddr_in6 *)&lreq->iflr_addr; + if (inet_ntop(AF_INET6, &sin6->sin6_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 mask for %s", lreq->iflr_name); + continue; + } + + n = snprintf(cp, ailen, "%s%s/%s", + cp == addrinfo ? "" : " ", addrstr, maskstr); + if (n < 0 || (size_t)n >= ailen) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto bad; + } + cp += n; + ailen -= n; + + num_interfaces++; + } + *addrinfo_out = addrinfo; + goto done; + +bad: + free(addrinfo); + num_interfaces = -1; +done: + free(ifconf.ifc_buf); + free(laddrconf.iflc_buf); + if (sock4 != -1) + close(sock4); + if (sock6 != -1) + close(sock6); + + debug_return_int(num_interfaces); +} + +# else + +/* + * Fill in the interfaces string with the machine's ip addresses and netmasks + * and return the number of interfaces found. Returns -1 on error. + * SIOCGLIFCONF version (IPv6 compatible). + */ +int +get_net_ifs(char **addrinfo_out) +{ + struct lifconf lifconf; + struct lifnum lifn; + struct sockaddr_in *sin4; + struct sockaddr_in6 *sin6; + char addrstr[INET6_ADDRSTRLEN], maskstr[INET6_ADDRSTRLEN]; + char *addrinfo = NULL; + int i, n, sock, sock4, sock6 = -1; + int 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); + + /* We need both INET4 and INET6 sockets to get flags and netmask. */ + sock4 = socket(AF_INET, SOCK_DGRAM, 0); + sock6 = socket(AF_INET6, SOCK_DGRAM, 0); + if (sock4 == -1 && sock6 == -1) + debug_return_int(-1); + + /* Use INET6 socket with SIOCGLIFCONF if possible (may not matter). */ + sock = sock6 != -1 ? sock6 : sock4; + + /* Get number of interfaces if possible. */ + memset(&lifn, 0, sizeof(lifn)); + if (ioctl(sock, SIOCGLIFNUM, &lifn) != -1) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "SIOCGLIFNUM reports %d interfaces", lifn.lifn_count); + lifn.lifn_count += 4; /* in case new interfaces come up */ + } else { + lifn.lifn_count = 512; + } + + /* Allocate and fill in the interface buffer. */ + memset(&lifconf, 0, sizeof(lifconf)); + lifconf.lifc_len = lifn.lifn_count * sizeof(struct lifreq); + lifconf.lifc_buf = malloc(lifconf.lifc_len); + if (lifconf.lifc_buf == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + goto bad; + } + if (ioctl(sock, SIOCGLIFCONF, &lifconf) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to get interface list (SIOCGLIFCONF)"); + goto bad; + } + + /* Allocate space for the maximum number of interfaces that could exist. */ + n = lifconf.lifc_len / sizeof(struct lifreq); + if (n == 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"); + goto bad; + } + addrinfo = cp; + + /* + * For each interface, store the ip address and netmask. + * Keep a copy of the address family, else it will be overwritten. + */ + for (i = 0; i < lifconf.lifc_len; ) { + struct lifreq *lifr = (struct lifreq *)&lifconf.lifc_buf[i]; + const int family = lifr->lifr_addr.ss_family; + + /* Set i to the subscript of the next interface (no sa_len). */ + i += sizeof(struct lifreq); + + /* Store the address. */ + switch (family) { + case AF_INET: + sin4 = (struct sockaddr_in *)&lifr->lifr_addr; + if (sin4->sin_addr.s_addr == INADDR_ANY || sin4->sin_addr.s_addr == INADDR_NONE) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET addr for %s", lifr->lifr_name); + continue; + } + if (inet_ntop(AF_INET, &sin4->sin_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET addr for %s", lifr->lifr_name); + continue; + } + sock = sock4; + break; + case AF_INET6: + sin6 = (struct sockaddr_in6 *)&lifr->lifr_addr; + if (IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET6 addr for %s", lifr->lifr_name); + continue; + } + if (inet_ntop(AF_INET6, &sin6->sin6_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 addr for %s", lifr->lifr_name); + continue; + } + sock = sock6; + break; + default: + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "ignoring address with family %d for %s", + family, lifr->lifr_name); + continue; + } + + /* Skip interfaces marked "down" and "loopback". */ + if (ioctl(sock, SIOCGLIFFLAGS, lifr) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFFLAGS for %s", lifr->lifr_name); + continue; + } + if (!ISSET(lifr->lifr_flags, IFF_UP) || + ISSET(lifr->lifr_flags, IFF_LOOPBACK)) + continue; + + /* Fetch and store the netmask. */ + if (ioctl(sock, SIOCGLIFNETMASK, lifr) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFNETMASK for %s", lifr->lifr_name); + continue; + } + switch (family) { + case AF_INET: + sin4 = (struct sockaddr_in *)&lifr->lifr_addr; + if (inet_ntop(AF_INET, &sin4->sin_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET mask for %s", lifr->lifr_name); + continue; + } + break; + case AF_INET6: + sin6 = (struct sockaddr_in6 *)&lifr->lifr_addr; + if (inet_ntop(AF_INET6, &sin6->sin6_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 mask for %s", lifr->lifr_name); + continue; + } + break; + default: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected address family %d for %s", + family, lifr->lifr_name); + continue; + } + + n = snprintf(cp, ailen, "%s%s/%s", + cp == addrinfo ? "" : " ", addrstr, maskstr); + if (n < 0 || (size_t)n >= ailen) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto bad; + } + cp += n; + ailen -= n; + + num_interfaces++; + } + *addrinfo_out = addrinfo; + goto done; + +bad: + free(addrinfo); + num_interfaces = -1; +done: + free(lifconf.lifc_buf); + if (sock4 != -1) + close(sock4); + if (sock6 != -1) + close(sock6); + + debug_return_int(num_interfaces); +} +# endif /* !__hpux */ + +#elif defined(SIOCGIFCONF) + +/* + * Fill in the interfaces string with the machine's ip addresses and netmasks + * and return the number of interfaces found. Returns -1 on error. + * SIOCGIFCONF version. + */ +int +get_net_ifs(char **addrinfo_out) +{ + struct ifconf ifconf; + struct ifreq *ifr; + struct sockaddr_in *sin4; +# ifdef HAVE_STRUCT_IN6_ADDR + struct sockaddr_in6 *sin6; +# endif + char addrstr[INET6_ADDRSTRLEN], maskstr[INET6_ADDRSTRLEN]; + char *addrinfo = NULL; + int i, n, sock, sock4, sock6 = -1; + int num_interfaces = 0; + size_t ailen, buflen; + char *cp, *ifconf_buf = NULL; + debug_decl(get_net_ifs, SUDO_DEBUG_NETIF); + + if (!sudo_conf_probe_interfaces()) + debug_return_int(0); + + sock4 = socket(AF_INET, SOCK_DGRAM, 0); +# ifdef HAVE_STRUCT_IN6_ADDR + sock6 = socket(AF_INET6, SOCK_DGRAM, 0); +# endif + if (sock4 == -1 && sock6 == -1) + debug_return_int(-1); + + /* Use INET6 socket with SIOCGIFCONF if possible (may not matter). */ + sock = sock6 != -1 ? sock6 : sock4; + + /* + * Get the size of the interface buffer (if possible). + * We over-allocate a bit in case interfaces come up afterward. + */ + i = 0; +# if defined(SIOCGSIZIFCONF) + /* AIX */ + if (ioctl(sock, SIOCGSIZIFCONF, &i) != -1) { + buflen = i + (sizeof(struct ifreq) * 4); + } else +# elif defined(SIOCGIFANUM) + /* SCO OpenServer 5/6 */ + if (ioctl(sock, SIOCGIFANUM, &i) != -1) { + buflen = (i + 4) * sizeof(struct ifreq); + } else +# elif defined(SIOCGIFNUM) + /* HP-UX, Solaris, others? */ + if (ioctl(sock, SIOCGIFNUM, &i) != -1) { + buflen = (i + 4) * sizeof(struct ifreq); + } else +# endif + { + buflen = 256 * sizeof(struct ifreq); + } + + /* Get interface configuration. */ + memset(&ifconf, 0, sizeof(ifconf)); + for (i = 0; i < 4; i++) { + ifconf.ifc_len = buflen; + ifconf.ifc_buf = malloc(buflen); + if (ifconf.ifc_buf == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate memory"); + goto bad; + } + + /* Note that some kernels return EINVAL if the buffer is too small */ + if (ioctl(sock, SIOCGIFCONF, &ifconf) < 0 && errno != EINVAL) + goto bad; + + /* Break out of loop if we have a big enough buffer. */ + if (ifconf.ifc_len + sizeof(struct ifreq) < buflen) + break; + buflen *= 2; + free(ifconf.ifc_buf); + } + + /* + * Allocate space for the maximum number of interfaces that could exist. + * We walk the list for systems with sa_len in struct sockaddr. + */ + for (i = 0, n = 0; i < ifconf.ifc_len; n++) { + /* Set i to the subscript of the next interface. */ + i += sizeof(struct ifreq); +#ifdef HAVE_STRUCT_SOCKADDR_SA_LEN + ifr = (struct ifreq *)&ifconf.ifc_buf[i]; + 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 */ + } + if (n == 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"); + goto bad; + } + addrinfo = cp; + + /* + * For each interface, store the ip address and netmask. + * Keep a copy of the address family, else it will be overwritten. + */ + for (i = 0; i < ifconf.ifc_len; ) { + int family; + + ifr = (struct ifreq *)&ifconf.ifc_buf[i]; + family = ifr->ifr_addr.sa_family; + + /* 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 */ + + /* Store the address. */ + switch (family) { + case AF_INET: + sin4 = (struct sockaddr_in *)&ifr->ifr_addr; + if (sin4->sin_addr.s_addr == INADDR_ANY || sin4->sin_addr.s_addr == INADDR_NONE) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET addr for %s", ifr->ifr_name); + continue; + } + if (inet_ntop(AF_INET, &sin4->sin_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET addr for %s", ifr->ifr_name); + continue; + } + sock = sock4; + break; +# ifdef HAVE_STRUCT_IN6_ADDR + case AF_INET6: + sin6 = (struct sockaddr_in6 *)&ifr->ifr_addr; + if (IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring unspecified AF_INET6 addr for %s", ifr->ifr_name); + continue; + } + if (inet_ntop(AF_INET6, &sin6->sin6_addr, addrstr, sizeof(addrstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 addr for %s", ifr->ifr_name); + continue; + } + sock = sock6; + break; +# endif /* HAVE_STRUCT_IN6_ADDR */ + default: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected address family %d for %s", + family, ifr->ifr_name); + continue; + } + + /* Skip interfaces marked "down" and "loopback". */ + if (ioctl(sock, SIOCGIFFLAGS, ifr) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFFLAGS for %s", ifr->ifr_name); + continue; + } + if (!ISSET(ifr->ifr_flags, IFF_UP) || + ISSET(ifr->ifr_flags, IFF_LOOPBACK)) + continue; + + /* Fetch and store the netmask. */ + if (ioctl(sock, SIOCGIFNETMASK, ifr) < 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "SIOCGLIFNETMASK for %s", ifr->ifr_name); + continue; + } + + /* Convert the mask to string form. */ + switch (family) { + case AF_INET: + sin4 = (struct sockaddr_in *)&ifr->ifr_addr; + if (inet_ntop(AF_INET, &sin4->sin_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET mask for %s", ifr->ifr_name); + continue; + } + break; +# ifdef HAVE_STRUCT_IN6_ADDR + case AF_INET6: + sin6 = (struct sockaddr_in6 *)&ifr->ifr_addr; + if (inet_ntop(AF_INET6, &sin6->sin6_addr, maskstr, sizeof(maskstr)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "ignoring bad AF_INET6 mask for %s", ifr->ifr_name); + continue; + } + break; +# endif /* HAVE_STRUCT_IN6_ADDR */ + default: + continue; + } + + n = snprintf(cp, ailen, "%s%s/%s", + cp == addrinfo ? "" : " ", addrstr, maskstr); + if (n < 0 || (size_t)n >= ailen) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto bad; + } + cp += n; + ailen -= n; + + num_interfaces++; + } + *addrinfo_out = addrinfo; + goto done; + +bad: + free(addrinfo); + num_interfaces = -1; +done: + free(ifconf_buf); + if (sock4 != -1) + close(sock4); + if (sock6 != -1) + close(sock6); + + debug_return_int(num_interfaces); +} + +#endif /* SIOCGIFCONF */ diff --git a/src/openbsd.c b/src/openbsd.c new file mode 100644 index 0000000..72d4374 --- /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..f74738f --- /dev/null +++ b/src/parse_args.c @@ -0,0 +1,881 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1993-1996, 1998-2023 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> + +unsigned int tgetpass_flags; + +/* + * Local functions. + */ +sudo_noreturn static void help(void); +sudo_noreturn static void usage_excl(void); +sudo_noreturn static void usage_excl_ticket(void); + +/* + * Mapping of command line options to name/value settings. + * Do not reorder, indexes must match ARG_ defines in sudo.h. + */ +static struct sudo_settings sudo_settings[] = { + { "bsdauth_type" }, + { "login_class" }, + { "preserve_environment" }, + { "runas_group" }, + { "set_home" }, + { "run_shell" }, + { "login_shell" }, + { "ignore_ticket" }, + { "update_ticket" }, + { "prompt" }, + { "selinux_role" }, + { "selinux_type" }, + { "runas_user" }, + { "progname" }, + { "implied_shell" }, + { "preserve_groups" }, + { "noninteractive" }, + { "sudoedit" }, + { "closefrom" }, + { "network_addrs" }, + { "max_groups" }, + { "plugin_dir" }, + { "remote_host" }, + { "timeout" }, + { "cmnd_chroot" }, + { "cmnd_cwd" }, + { "askpass" }, + { "intercept_setid" }, + { "intercept_ptrace" }, + { "apparmor_profile" }, + { 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. + * There is a more limited set of options for sudoedit (the sudo-specific + * long options are listed first). + */ +static const char sudo_short_opts[] = "+Aa:BbC:c:D:Eeg:Hh::iKklNnPp:R:r:SsT:t:U:u:Vv"; +static const char edit_short_opts[] = "+Aa:BC:c:D:g:h::KkNnp:R:r:ST:t:u:V"; +static struct option sudo_long_opts[] = { + /* sudo-specific long options */ + { "background", no_argument, NULL, 'b' }, + { "preserve-env", optional_argument, NULL, 'E' }, + { "edit", no_argument, NULL, 'e' }, + { "set-home", no_argument, NULL, 'H' }, + { "login", no_argument, NULL, 'i' }, + { "remove-timestamp", no_argument, NULL, 'K' }, + { "list", no_argument, NULL, 'l' }, + { "preserve-groups", no_argument, NULL, 'P' }, + { "shell", no_argument, NULL, 's' }, + { "other-user", required_argument, NULL, 'U' }, + { "validate", no_argument, NULL, 'v' }, + /* common long options */ + { "askpass", no_argument, NULL, 'A' }, + { "auth-type", required_argument, NULL, 'a' }, + { "bell", no_argument, NULL, 'B' }, + { "close-from", required_argument, NULL, 'C' }, + { "login-class", required_argument, NULL, 'c' }, + { "chdir", required_argument, NULL, 'D' }, + { "group", required_argument, NULL, 'g' }, + { "help", no_argument, NULL, 'h' }, + { "host", required_argument, NULL, OPT_HOSTNAME }, + { "reset-timestamp", no_argument, NULL, 'k' }, + { "no-update", no_argument, NULL, 'N' }, + { "non-interactive", no_argument, NULL, 'n' }, + { "prompt", required_argument, NULL, 'p' }, + { "chroot", required_argument, NULL, 'R' }, + { "role", required_argument, NULL, 'r' }, + { "stdin", no_argument, NULL, 'S' }, + { "command-timeout",required_argument, NULL, 'T' }, + { "type", required_argument, NULL, 't' }, + { "user", required_argument, NULL, 'u' }, + { "version", no_argument, NULL, 'V' }, + { NULL, no_argument, NULL, '\0' }, +}; +static struct option *edit_long_opts = &sudo_long_opts[11]; + +/* + * 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). + */ +unsigned int +parse_args(int argc, char **argv, const char *shell, int *old_optind, + int *nargc, char ***nargv, struct sudo_settings **settingsp, + char ***env_addp, const char **list_userp) +{ + const char *progname, *short_opts = sudo_short_opts; + struct option *long_opts = sudo_long_opts; + struct environment extra_env; + const char *list_user = NULL; + unsigned int mode = 0; /* what mode is sudo to be run in? */ + unsigned int flags = 0; /* mode flags */ + unsigned int valid_flags = DEFAULT_VALID_FLAGS; + int ch, i; + char *cp; + debug_decl(parse_args, SUDO_DEBUG_ARGS); + + /* Is someone trying something funny? */ + if (argc <= 0) + usage(); + + /* The plugin API includes the program name (either sudo or sudoedit). */ + progname = getprogname(); + sudo_settings[ARG_PROGNAME].value = progname; + + /* First, check to see if we were invoked as "sudoedit". */ + if (strcmp(progname, "sudoedit") == 0) { + mode = MODE_EDIT; + sudo_settings[ARG_SUDOEDIT].value = "true"; + valid_flags = EDIT_VALID_FLAGS; + short_opts = edit_short_opts; + long_opts = edit_long_opts; + } + + /* 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] != '/' && \ + argv[optind][0] != '=' && strchr(argv[optind], '=') != NULL) + + /* Space for environment variables is lazy allocated. */ + memset(&extra_env, 0, sizeof(extra_env)); + + 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); + sudo_settings[ARG_ASKPASS].value = "true"; + 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': + if (mode && mode != MODE_KILL) + usage_excl(); + mode = MODE_KILL; + valid_flags = 0; + FALLTHROUGH; + case 'k': + if (sudo_settings[ARG_UPDATE_TICKET].value != NULL) + usage_excl_ticket(); + sudo_settings[ARG_IGNORE_TICKET].value = "true"; + 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': + if (sudo_settings[ARG_IGNORE_TICKET].value != NULL) + usage_excl_ticket(); + sudo_settings[ARG_UPDATE_TICKET].value = "false"; + 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) { + if (strcmp(progname, "sudoedit") != 0) + 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 (exec_ptrace_intercept_supported()) + sudo_settings[ARG_INTERCEPT_SETID].value = "true"; + if (exec_ptrace_subcmds_supported()) + sudo_settings[ARG_INTERCEPT_PTRACE].value = "true"; + + if (mode == MODE_HELP) + help(); + + /* + * For shell mode we need to rewrite argv + * TODO: move this to the policy plugin and make escaping configurable + */ + 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, (size_t)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 *)shell; /* plugin may override shell */ + if (cmnd != NULL) { + av[1] = (char *)"-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, (size_t)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] = (char *)"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; + *list_userp = list_user; + debug_return_uint(mode | flags); +} + +/* + * Display usage message. + * The actual usage strings are in sudo_usage.h for configure substitution. + */ +static void +display_usage(FILE *fp) +{ + const char * const **uvecs = sudo_usage; + const char * const *uvec; + size_t i; + int indent; + + /* + * Use usage vectors appropriate to the progname. + */ + if (strcmp(getprogname(), "sudoedit") == 0) + uvecs = sudoedit_usage; + + indent = (int)strlen(getprogname()) + 8; + while ((uvec = *uvecs) != NULL) { + (void)fprintf(fp, "usage: %s %s\n", getprogname(), uvec[0]); + for (i = 1; uvec[i] != NULL; i++) { + (void)fprintf(fp, "%*s%s\n", indent, "", uvec[i]); + } + uvecs++; + } +} + +/* + * Display usage message and exit. + */ +sudo_noreturn void +usage(void) +{ + display_usage(stderr); + 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(); +} + +/* + * Tell which options are mutually exclusive and exit. + */ +static void +usage_excl_ticket(void) +{ + debug_decl(usage_excl_ticket, SUDO_DEBUG_ARGS); + + sudo_warnx("%s", + U_("Only one of the -K, -k or -N options may be specified")); + usage(); +} + +static int +help_out(const char * restrict buf) +{ + return fputs(buf, stdout); +} + +sudo_noreturn static void +help(void) +{ + struct sudo_lbuf lbuf; + const int indent = 32; + const char *pname = getprogname(); + bool sudoedit = false; + debug_decl(help, SUDO_DEBUG_ARGS); + + if (strcmp(pname, "sudoedit") == 0) { + sudoedit = true; + (void)printf(_("%s - edit files as another user\n\n"), pname); + } else { + (void)printf(_("%s - execute a command as another user\n\n"), pname); + } + display_usage(stdout); + + sudo_lbuf_init(&lbuf, help_out, indent, NULL, 80); + 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 + if (!sudoedit) { + 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")); + if (!sudoedit) { + 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")); + if (!sudoedit) { + 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)")); + if (!sudoedit) { + 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")); + if (!sudoedit) { + 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")); + if (!sudoedit) { + 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")); + if (!sudoedit) { + 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")); + if (!sudoedit) { + 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")); + if (!sudoedit) { + 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..e18cc95 --- /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.h> +#include <sudo_dso.h> +#include <sudo_plugin.h> + +#ifdef STATIC_SUDOERS_PLUGIN + +extern struct policy_plugin sudoers_policy; +extern struct io_plugin sudoers_io; +extern struct audit_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 }, + { _PATH_SUDOERS_PLUGIN, &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..ebe4797 --- /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((size_t)(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/intercept/test_ptrace.c b/src/regress/intercept/test_ptrace.c new file mode 100644 index 0000000..30b73e7 --- /dev/null +++ b/src/regress/intercept/test_ptrace.c @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2022 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 + */ + +/* + * Test program to exercise seccomp(2) and ptrace(2) intercept code. + * + * Usage: test_ptrace [-d 1-3] [command] + */ + +/* Ignore architecture restrictions and define this unilaterally. */ +#define HAVE_PTRACE_INTERCEPT +#include "exec_ptrace.c" + +static sig_atomic_t got_sigchld; +static int debug; +int sudo_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; + +sudo_dso_public int main(int argc, char *argv[]); + +static void +handler(int signo) +{ + if (signo == SIGCHLD) + got_sigchld = 1; +} + +void +intercept_closure_reset(struct intercept_closure *closure) +{ + memset(closure, 0, sizeof(*closure)); +} + +bool +intercept_check_policy(const char *command, int argc, char **argv, int envc, + char **envp, const char *runcwd, int *oldcwd, void *v) +{ + struct intercept_closure *closure = v; + struct stat sb1, sb2; + bool is_denied; + debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC); + + /* Fake policy decisions. */ + is_denied = stat(command, &sb1) == 0 && stat("/usr/bin/who", &sb2) == 0 && + sb1.st_ino == sb2.st_ino && sb1.st_dev == sb2.st_dev; + if (is_denied) { + sudo_debug_printf(SUDO_DEBUG_DIAG, "denied %s", command); + closure->state = POLICY_REJECT; + } else { + sudo_debug_printf(SUDO_DEBUG_DIAG, "allowed %s", command); + closure->state = POLICY_TEST; + } + *oldcwd = -1; + + debug_return_bool(true); +} + +static void +init_debug_files(struct sudo_conf_debug_file_list *file_list, + struct sudo_debug_file *file) +{ + debug_decl(init_debug_files, SUDO_DEBUG_EXEC); + + TAILQ_INIT(file_list); + switch (debug) { + case 0: + debug_return; + case 1: + file->debug_flags = (char *)"exec@diag"; + break; + case 2: + file->debug_flags = (char *)"exec@info"; + break; + default: + file->debug_flags = (char *)"exec@debug"; + break; + } + file->debug_file = (char *)"/dev/stderr"; + TAILQ_INSERT_HEAD(file_list, file, entries); + + debug_return; +} + +int +sudo_sigaction(int signo, struct sigaction *sa, struct sigaction *osa) +{ + return sigaction(signo, sa, osa); +} + +int +main(int argc, char *argv[]) +{ + struct sudo_conf_debug_file_list debug_files; + struct sudo_debug_file debug_file; + const char *base, *shell = _PATH_SUDO_BSHELL; + struct intercept_closure closure = { 0 }; + const char *errstr; + sigset_t blocked, empty; + struct sigaction sa; + pid_t child, my_pid, pid, my_pgrp; + int ch, status; + debug_decl_vars(main, SUDO_DEBUG_MAIN); + + initprogname(argc > 0 ? argv[0] : "test_ptrace"); + + if (!have_seccomp_action("trap")) + sudo_fatalx("SECCOMP_MODE_FILTER not available in this kernel"); + + while ((ch = getopt(argc, argv, "d:")) != -1) { + switch (ch) { + case 'd': + debug = sudo_strtonum(optarg, 1, INT_MAX, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), optarg, U_(errstr)); + break; + default: + fprintf(stderr, "usage: %s [-d 1-3] [command]\n", getprogname()); + return EXIT_FAILURE; + } + } + argc -= optind; + argv += optind; + + if (argc > 0) + shell = argv[0]; + base = strrchr(shell, '/'); + base = base ? base + 1 : shell; + + /* Set debug level based on the debug flag. */ + init_debug_files(&debug_files, &debug_file); + sudo_debug_instance = sudo_debug_register(getprogname(), + NULL, NULL, &debug_files, -1); + if (sudo_debug_instance == SUDO_DEBUG_INSTANCE_ERROR) + return EXIT_FAILURE; + + /* Block SIGCHLD and SIGUSR during critical section. */ + sigemptyset(&empty); + sigemptyset(&blocked); + sigaddset(&blocked, SIGCHLD); + sigaddset(&blocked, SIGUSR1); + sigprocmask(SIG_BLOCK, &blocked, NULL); + + /* Signal handler sets a flag for SIGCHLD, nothing for SIGUSR1. */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = handler; + sigaction(SIGCHLD, &sa, NULL); + sigaction(SIGUSR1, &sa, NULL); + + /* Fork a shell. */ + my_pid = getpid(); + my_pgrp = getpgrp(); + child = fork(); + switch (child) { + case -1: + sudo_fatal("fork"); + case 0: + /* child */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) + sudo_fatal("%s", "unable to set no_new_privs bit"); + if (!set_exec_filter()) + _exit(EXIT_FAILURE); + + /* Suspend child until tracer seizes control and sends SIGUSR1. */ + sigsuspend(&empty); + execl(shell, base, NULL); + sudo_fatal("execl"); + default: + /* Parent attaches to child and allows it to continue. */ + if (exec_ptrace_seize(child) == -1) + return EXIT_FAILURE; + break; + } + + /* Wait for SIGCHLD. */ + for (;;) { + sigsuspend(&empty); + if (!got_sigchld) + continue; + got_sigchld = 0; + + for (;;) { + do { + pid = waitpid(-1, &status, __WALL|WNOHANG); + } while (pid == -1 && errno == EINTR); + if (pid <= 0) { + if (pid == -1 && errno != ECHILD) + sudo_fatal("waitpid"); + /* No child to wait for. */ + break; + } + + if (WIFEXITED(status)) { + sudo_debug_printf(SUDO_DEBUG_DIAG, "%d: exited %d", + (int)pid, WEXITSTATUS(status)); + if (pid == child) + return WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + sudo_debug_printf(SUDO_DEBUG_DIAG, "%d: killed by signal %d", + (int)pid, WTERMSIG(status)); + if (pid == child) + return WTERMSIG(status) | 128; + } else if (WIFSTOPPED(status)) { + if (exec_ptrace_stopped(pid, status, &closure)) { + if (pid == child) { + sudo_suspend_parent(WSTOPSIG(status), my_pid, + my_pgrp, child, NULL, NULL); + if (kill(child, SIGCONT) != 0) + sudo_warn("kill(%d, SIGCONT)", (int)child); + } + } + } else { + sudo_fatalx("%d: unknown status 0x%x", (int)pid, status); + } + } + } +} diff --git a/src/regress/net_ifs/check_net_ifs.c b/src/regress/net_ifs/check_net_ifs.c new file mode 100644 index 0000000..60adcfa --- /dev/null +++ b/src/regress/net_ifs/check_net_ifs.c @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021-2022 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> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include <compat/stdbool.h> +#endif /* HAVE_STDBOOL_H */ +#include <string.h> +#include <unistd.h> +#include <limits.h> +#include <errno.h> + +#include <sudo_compat.h> +#include <sudo_util.h> + +sudo_dso_public int main(int argc, char *argv[]); + +extern int get_net_ifs(char **addrinfo); + +int +main(int argc, char *argv[]) +{ + int ch, ninterfaces, errors = 0, ntests = 1; + char *interfaces = NULL; + bool verbose = false; + + initprogname(argc > 0 ? argv[0] : "check_net_ifs"); + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbose = true; + break; + default: + fprintf(stderr, "usage: %s [-v]\n", getprogname()); + return EXIT_FAILURE; + } + } + + ninterfaces = get_net_ifs(&interfaces); + switch (ninterfaces) { + case -1: + printf("FAIL: unable to get network interfaces\n"); + errors++; + break; + case 0: + /* no interfaces or STUB_LOAD_INTERFACES defined. */ + if (verbose) + printf("OK: (0 interfaces)\n"); + break; + default: + if (verbose) { + printf("OK: (%d interface%s, %s)\n", ninterfaces, + ninterfaces > 1 ? "s" : "", interfaces); + } + break; + } + free(interfaces); + + if (ntests != 0) { + printf("%s: %d tests run, %d errors, %d%% success rate\n", + getprogname(), ntests, errors, (ntests - errors) * 100 / ntests); + } + return errors; +} diff --git a/src/regress/noexec/check_noexec.c b/src/regress/noexec/check_noexec.c new file mode 100644 index 0000000..a5f7ff4 --- /dev/null +++ b/src/regress/noexec/check_noexec.c @@ -0,0 +1,232 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2016, 2022 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_queue.h> +#include <sudo_exec.h> + +static bool verbose; + +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) { + if (verbose) + 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) { + if (verbose) + 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 + if (verbose) + 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 */ + if (verbose) + printf("%s: OK (wordexp) [WRDE_SYNTAX]\n", getprogname()); + ret = 0; + break; + case WRDE_CMDSUB: + if (verbose) + 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) { + if (verbose) + 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 + +sudo_noreturn static void +usage(void) +{ + fprintf(stderr, "usage: %s [-v] rexec | /path/to/sudo_noexec.so\n", + getprogname()); + exit(EXIT_FAILURE); +} + +int +main(int argc, char *argv[], char *envp[]) +{ + int ch, errors = 0, ntests = 0; + + initprogname(argc > 0 ? argv[0] : "check_noexec"); + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbose = true; + break; + default: + usage(); + } + } + + if (argc - optind != 1) + usage(); + + /* Disable execution for post-exec and re-exec ourself. */ + if (strcmp(argv[optind], "rexec") != 0) { + const char *noexec = argv[optind]; + argv[optind] = (char *)"rexec"; + execve(argv[0], argv, disable_execute(envp, noexec)); + sudo_fatalx_nodebug("execve"); + } + + ntests++; + errors += try_execl(); + ntests++; + errors += try_system(); +#ifdef HAVE_WORDEXP_H + ntests++; + errors += try_wordexp(); +#endif + + if (ntests != 0) { + printf("%s: %d tests run, %d errors, %d%% success rate\n", + getprogname(), ntests, errors, (ntests - errors) * 100 / ntests); + } + return errors; +} diff --git a/src/regress/ttyname/check_ttyname.c b/src/regress/ttyname/check_ttyname.c new file mode 100644 index 0000000..715423d --- /dev/null +++ b/src/regress/ttyname/check_ttyname.c @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2013-2020, 2022 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> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include <compat/stdbool.h> +#endif /* HAVE_STDBOOL_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]; + bool verbose = false; + int ch, errors = 0, ntests = 1; + + initprogname(argc > 0 ? argv[0] : "check_ttyname"); + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbose = true; + break; + default: + fprintf(stderr, "usage: %s [-v]\n", getprogname()); + return EXIT_FAILURE; + } + } + + /* 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_DFLY) || \ + 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. */ + if (match_ttys(tty_libc, tty_sudo) == 0) { + if (verbose) + printf("%s: OK (%s)\n", getprogname(), tty_sudo ? tty_sudo : "none"); + } else if (tty_libc == NULL) { + if (verbose) + printf("%s: SKIP (%s)\n", getprogname(), tty_sudo ? tty_sudo : "none"); + ntests = 0; + } else { + printf("%s: FAIL %s (sudo) vs. %s (libc)\n", getprogname(), + tty_sudo ? tty_sudo : "none", tty_libc ? tty_libc : "none"); + errors++; + } + + if (ntests != 0) { + printf("%s: %d tests run, %d errors, %d%% success rate\n", + getprogname(), ntests, errors, (ntests - errors) * 100 / ntests); + } + return errors; +} diff --git a/src/selinux.c b/src/selinux.c new file mode 100644 index 0000000..74bc571 --- /dev/null +++ b/src/selinux.c @@ -0,0 +1,515 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2022 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 { + char * old_context; + char * new_context; + char * tty_con_raw; + char * new_tty_con_raw; + const char *ttyn; + int ttyfd; + int enforcing; +} se_state; + +int +selinux_audit_role_change(void) +{ +#ifdef HAVE_LINUX_AUDIT + int au_fd, rc = -1; + char *message; + debug_decl(selinux_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", + se_state.old_context, se_state.new_context ? se_state.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, se_state.ttyn, se_state.new_context ? 1 : 0); + if (rc <= 0) + sudo_warn("%s", U_("unable to send audit message")); + free(message); + close(au_fd); + } + + debug_return_int(rc); +#else + return 0; +#endif /* HAVE_LINUX_AUDIT */ +} + +/* + * 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; + char * 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. + */ +int +selinux_relabel_tty(const char *ttyn, int ptyfd) +{ + char * tty_con = NULL; + char * 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 (sudo_isatty(fd, &sb) && 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); +} + +/* + * Determine the new security context based on the old context and the + * specified role and type. + * Returns 0 on success, and -1 on failure. + */ +static int +get_exec_context(const char *role, const char *type) +{ + char *new_context = NULL; + context_t context = NULL; + char *typebuf = NULL; + int ret = -1; + debug_decl(get_exec_context, SUDO_DEBUG_SELINUX); + + if (role == NULL) { + sudo_warnx(U_("you must specify a role for type %s"), type); + errno = EINVAL; + goto done; + } + if (type == NULL) { + if (get_default_type(role, &typebuf)) { + sudo_warnx(U_("unable to get default type for role %s"), role); + errno = EINVAL; + goto done; + } + type = typebuf; + } + + /* + * Expand old_context into a context_t so that we can extract and modify + * its components easily. + */ + if ((context = context_new(se_state.old_context)) == NULL) { + sudo_warn("%s", U_("failed to get new context")); + goto done; + } + + /* + * 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 done; + } + if (context_type_set(context, type)) { + sudo_warn(U_("failed to set new type %s"), type); + goto done; + } + + /* + * 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 done; + } + if (security_check_context(new_context) == -1) { + sudo_warnx(U_("%s is not a valid context"), new_context); + errno = EINVAL; + goto done; + } + + se_state.new_context = new_context; + new_context = NULL; + ret = 0; + +done: + free(typebuf); + context_free(context); + freecon(new_context); + debug_return_int(ret); +} + +/* + * Determine the exec and tty contexts the command will run in. + * Returns 0 on success and -1 on failure. + */ +int +selinux_getexeccon(const char *role, const char *type) +{ + int ret = -1; + debug_decl(selinux_getexeccon, 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); + ret = get_exec_context(role, type); + if (ret == -1) { + /* Audit role change failure (success is logged later). */ + selinux_audit_role_change(); + goto done; + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: new context %s", __func__, + se_state.new_context); + +done: + debug_return_int(ret); +} + +int +selinux_setexeccon(void) +{ + debug_decl(selinux_setexeccon, 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[], + const char *rundir, unsigned int flags) +{ + char **nargv; + const char *sesh; + int argc, len, 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_setexeccon() == -1) + debug_return; + + /* + * Build new argv with sesh as argv[0]. + */ + for (argc = 0; argv[argc] != NULL; argc++) + continue; + if (argc == 0) { + errno = EINVAL; + debug_return; + } + nargv = reallocarray(NULL, 5 + argc + 1, sizeof(char *)); + if (nargv == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return; + } + if (*argv[0] == '-') + nargv[0] = (char *)"-sesh"; + else + nargv[0] = (char *)"sesh"; + nargc = 1; + if (ISSET(flags, CD_RBAC_SET_CWD)) { + const char *prefix = ISSET(flags, CD_CWD_OPTIONAL) ? "+" : ""; + if (rundir == NULL) { + sudo_warnx("internal error: sesh rundir not set"); + errno = EINVAL; + debug_return; + } + len = asprintf(&nargv[nargc++], "--directory=%s%s", prefix, rundir); + if (len == -1) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return; + } + } + if (fd != -1) { + len = asprintf(&nargv[nargc++], "--execfd=%d", fd); + if (len == -1) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return; + } + } + if (ISSET(flags, CD_NOEXEC)) { + CLR(flags, CD_NOEXEC); + nargv[nargc++] = (char *)"--noexec"; + } + nargv[nargc++] = (char *)"--"; + nargv[nargc++] = (char *)path; + memcpy(&nargv[nargc], &argv[1], argc * sizeof(char *)); /* copies NULL */ + + sudo_execve(-1, sesh, nargv, envp, -1, flags); + 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..44faa11 --- /dev/null +++ b/src/sesh.c @@ -0,0 +1,489 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2008, 2010-2018, 2020-2022 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 */ +#ifdef HAVE_GETOPT_LONG +# include <getopt.h> +# else +# include <compat/getopt.h> +#endif /* HAVE_GETOPT_LONG */ + +#include <sudo.h> +#include <sudo_exec.h> +#include <sudo_edit.h> + +enum sesh_mode { + SESH_RUN_COMMAND, + SESH_EDIT_CREATE, + SESH_EDIT_INSTALL +}; + +static const char short_opts[] = "+cd:e:ihnw:"; +static struct option long_opts[] = { + { "edit-create", no_argument, NULL, 'c' }, + { "directory", required_argument, NULL, 'd' }, + { "execfd", required_argument, NULL, 'e' }, + { "edit-install", no_argument, NULL, 'i' }, + { "no-dereference", no_argument, NULL, 'h' }, + { "noexec", no_argument, NULL, 'n' }, + { "edit-checkdir", required_argument, NULL, 'w' }, + { NULL, no_argument, NULL, '\0' }, +}; + +sudo_dso_public int main(int argc, char *argv[], char *envp[]); + +static int sesh_sudoedit(enum sesh_mode mode, int flags, char *user, int argc, + char *argv[]); + +sudo_noreturn void +usage(void) +{ + (void)fprintf(stderr, + "usage: %s [-n] [-d directory] [-e fd] command [...]\n" + " %s [-cih] [-w uid:gids] file [...]\n", + getprogname(), getprogname()); + exit(EXIT_FAILURE); +} + +/* + * 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[]) +{ + enum sesh_mode mode = SESH_RUN_COMMAND; + const char *errstr, *rundir = NULL; + unsigned int flags = CD_SUDOEDIT_FOLLOW; + char *edit_user = NULL; + int ch, ret, fd = -1; + debug_decl(main, SUDO_DEBUG_MAIN); + + initprogname(argc > 0 ? argv[0] : "sesh"); + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE_NAME, LOCALEDIR); + textdomain(PACKAGE_NAME); + + while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { + switch (ch) { + case 'c': + if (mode != SESH_RUN_COMMAND) { + sudo_warnx("%s", + U_("Only one of the -c or -i options may be specified")); + usage(); + } + mode = SESH_EDIT_CREATE; + break; + case 'd': + rundir = optarg; + if (*rundir == '+') { + SET(flags, CD_CWD_OPTIONAL); + rundir++; + } + break; + case 'e': + fd = sudo_strtonum(optarg, 0, INT_MAX, &errstr); + if (errstr != NULL) + sudo_fatalx(U_("invalid file descriptor number: %s"), optarg); + break; + case 'i': + if (mode != SESH_RUN_COMMAND) { + sudo_warnx("%s", + U_("Only one of the -c or -i options may be specified")); + usage(); + } + mode = SESH_EDIT_INSTALL; + break; + case 'h': + CLR(flags, CD_SUDOEDIT_FOLLOW); + break; + case 'n': + SET(flags, CD_NOEXEC); + break; + case 'w': + SET(flags, CD_SUDOEDIT_CHECKDIR); + edit_user = optarg; + break; + default: + usage(); + /* NOTREACHED */ + } + } + argc -= optind; + argv += optind; + if (argc == 0) + usage(); + + /* Read sudo.conf and initialize the debug subsystem. */ + if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1) + return EXIT_FAILURE; + sudo_debug_register(getprogname(), NULL, NULL, + sudo_conf_debug_files(getprogname()), -1); + + if (mode != SESH_RUN_COMMAND) { + if (rundir != NULL) { + sudo_warnx(U_("The -%c option may not be used in edit mode."), 'd'); + usage(); + } + if (fd != -1) { + sudo_warnx(U_("The -%c option may not be used in edit mode."), 'e'); + usage(); + } + if (ISSET(flags, CD_NOEXEC)) { + sudo_warnx(U_("The -%c option may not be used in edit mode."), 'n'); + usage(); + } + ret = sesh_sudoedit(mode, flags, edit_user, argc, argv); + } else { + bool login_shell; + char *cmnd; + + if (!ISSET(flags, CD_SUDOEDIT_FOLLOW)) { + sudo_warnx(U_("The -%c option may only be used in edit mode."), + 'h'); + usage(); + } + if (edit_user != NULL) { + sudo_warnx(U_("The -%c option may only be used in edit mode."), + 'w'); + usage(); + } + + /* If the first char of argv[0] is '-', we are running a login shell. */ + login_shell = argv[0][0] == '-'; + + /* We must change the directory in sesh after the context changes. */ + if (rundir != NULL && chdir(rundir) == -1) { + sudo_warnx(U_("unable to change directory to %s"), rundir); + if (!ISSET(flags, CD_CWD_OPTIONAL)) + return EXIT_FAILURE; + } + + /* Make a copy of the command to execute. */ + 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) { + char *cp = strrchr(argv[0], '/'); + if (cp != NULL) { + *cp = '-'; + argv[0] = cp; + } + } + sudo_execve(fd, cmnd, argv, envp, -1, flags); + 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(enum sesh_mode mode, int flags, char *user, + int argc, char *argv[]) +{ + struct sudo_cred user_cred, run_cred; + int ret; + debug_decl(sesh_sudoedit, SUDO_DEBUG_EDIT); + + memset(&user_cred, 0, sizeof(user_cred)); + memset(&run_cred, 0, sizeof(run_cred)); + + /* Parse user for -w option, "uid:gid:gid1,gid2,..." */ + if (user != NULL && !parse_user(user, &user_cred)) + debug_return_int(SESH_ERR_FAILURE); + + /* 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); + } + run_cred.ngroups = getgroups(run_cred.ngroups, run_cred.groups); + if (run_cred.ngroups < 0) { + sudo_warn("%s", U_("unable to get group list")); + free(run_cred.groups); + debug_return_int(SESH_ERR_FAILURE); + } + } else { + run_cred.ngroups = 0; + run_cred.groups = NULL; + } + + ret = mode == SESH_EDIT_CREATE ? + sesh_edit_create_tfiles(flags, &user_cred, &run_cred, argc, argv) : + sesh_edit_copy_tfiles(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..59cb5f4 --- /dev/null +++ b/src/signal.c @@ -0,0 +1,196 @@ +/* + * 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; + case SIGCHLD: + /* Sudo needs to be able to catch SIGCHLD. */ + if (ss->sa.sa_handler == SIG_IGN) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "will restore signal %d on exec", SIGCHLD); + ss->restore = true; + } + if (sigaction(SIGCHLD, &sa, NULL) != 0) { + sudo_warn(U_("unable to set handler for signal %d"), + SIGCHLD); + } + 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..49cc6eb --- /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..9d929e4 --- /dev/null +++ b/src/sudo.c @@ -0,0 +1,2255 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2023 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> +#ifdef __linux__ +# include <sys/prctl.h> +#endif +#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.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); +int sudo_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; +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(void); +static void policy_close(const char *cmnd, int exit_status, int error); +static int policy_show_version(int verbose); +static bool policy_check(int argc, char * const argv[], char *env_add[], + char **command_info[], char **run_argv[], char **run_envp[]); +static void policy_list(int argc, char * const argv[], + int verbose, const char *user); +static void policy_validate(char * const argv[]); +static void policy_invalidate(int unlinkit); + +/* I/O log plugin convenience functions. */ +static bool iolog_open(char * const command_info[], int run_argc, + char * const run_argv[], char * const run_envp[]); +static void iolog_close(int exit_status, int error); +static void iolog_show_version(int verbose, int argc, char * const argv[], + char * const envp[]); +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 (some are public). */ +static void audit_open(void); +static void audit_close(int exit_status, int error); +static void audit_show_version(int verbose); + +/* Approval plugin convenience functions (some are public). */ +static void approval_show_version(int verbose); + +sudo_dso_public int main(int argc, char *argv[], char *envp[]); + +static struct sudo_settings *sudo_settings; +static char * const *user_info, * const *submit_argv, * const *submit_envp; +static int submit_optind; + +int +main(int argc, char *argv[], char *envp[]) +{ + struct command_details command_details; + struct user_details user_details; + unsigned int sudo_mode; + int nargc, status = 0; + char **nargv, **env_add; + char **command_info = NULL, **argv_out = NULL, **run_envp = NULL; + const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL }; + const char *list_user; + 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) + return EXIT_FAILURE; + sudo_debug_instance = sudo_debug_register(getprogname(), + NULL, NULL, sudo_conf_debug_files(getprogname()), -1); + if (sudo_debug_instance == SUDO_DEBUG_INSTANCE_ERROR) + return 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) + return 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, preserving the original argv/envp. */ + submit_argv = argv; + submit_envp = envp; + sudo_mode = parse_args(argc, argv, user_details.shell, &submit_optind, + &nargc, &nargv, &sudo_settings, &env_add, &list_user); + sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode 0x%x", 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(); + policy_open(); + + switch (sudo_mode & MODE_MASK) { + case MODE_VERSION: + policy_show_version(!user_details.cred.uid); + iolog_show_version(!user_details.cred.uid, nargc, nargv, + submit_envp); + approval_show_version(!user_details.cred.uid); + audit_show_version(!user_details.cred.uid); + break; + case MODE_VALIDATE: + case MODE_VALIDATE|MODE_INVALIDATE: + policy_validate(nargv); + 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); + break; + case MODE_EDIT: + case MODE_RUN: + if (!policy_check(nargc, nargv, env_add, &command_info, &argv_out, + &run_envp)) + goto access_denied; + + /* 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. */ + if (!approval_check(command_info, nargv, run_envp)) + goto access_denied; + + /* Open I/O plugin once policy and approval plugins succeed. */ + if (!iolog_open(command_info, nargc, nargv, run_envp)) + goto access_denied; + + /* Audit the accept event on behalf of the sudo front-end. */ + if (!audit_accept("sudo", SUDO_FRONT_END, command_info, + nargv, run_envp)) + goto access_denied; + + /* Setup command details and run command/edit. */ + command_info_to_details(command_info, &command_details); + if (command_details.utmp_user == NULL) + command_details.utmp_user = user_details.username; + command_details.submitcwd = user_details.cwd; + command_details.tty = user_details.tty; + command_details.argv = nargv; + command_details.argc = nargc; + command_details.envp = run_envp; + 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, &user_details); + } else { + status = run_command(&command_details, &user_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; + + /* Make sure sudo doesn't dump core itself. */ + disable_coredump(); + + /* Re-send the signal to the main sudo process. */ + 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)); + return WEXITSTATUS(status); + +access_denied: + /* Policy/approval failure, close policy and audit plugins before exit. */ + if (policy_plugin.u.policy->version >= SUDO_API_MKVERSION(1, 15)) + policy_close(NULL, 0, EACCES); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, + EXIT_FAILURE); + return EXIT_FAILURE; +} + +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]; + 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]) { + int 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, (size_t)cred->ngroups, sizeof(GETGROUPS_T)); + if (cred->groups != NULL) { + /* Clamp to max_groups if insufficient space for all groups. */ + if (sudo_getgrouplist2(user, cred->gid, &cred->groups, + &cred->ngroups) == -1) { + cred->ngroups = sudo_conf_max_groups(); + } + 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) { + long maxgroups = 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, (size_t)cred->ngroups, + sizeof(GETGROUPS_T)); + if (cred->groups == NULL) + goto done; + cred->ngroups = getgroups(cred->ngroups, cred->groups); + if (cred->ngroups < 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 + + ((size_t)cred->ngroups * (STRLEN_MAX_UNSIGNED(gid_t) + 1)); + if ((gid_list = malloc(glsize)) == NULL) + goto done; + memcpy(gid_list, "groups=", sizeof("groups=") - 1); + cp = gid_list + sizeof("groups=") - 1; + glsize -= (size_t)(cp - gid_list); + for (i = 0; i < cred->ngroups; i++) { + len = snprintf(cp, glsize, "%s%u", i ? "," : "", + (unsigned int)cred->groups[i]); + if (len < 0 || (size_t)len >= glsize) + sudo_fatalx(U_("internal error, %s overflow"), __func__); + cp += len; + glsize -= (size_t)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, **info, path[PATH_MAX]; + size_t info_max = 32 + RLIM_NLIMITS; + size_t i = 0, n; + mode_t mask; + struct passwd *pw; + int ttyfd; + 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 */ + info = reallocarray(NULL, info_max, sizeof(char *)); + if (info == NULL) + goto oom; + + ud->pid = getpid(); + ud->ppid = getppid(); + ud->pgid = getpgid(0); + ttyfd = open(_PATH_TTY, O_RDWR); + sudo_get_ttysize(ttyfd, &ud->ts_rows, &ud->ts_cols); + if (ttyfd != -1) { + if ((ud->tcpgid = tcgetpgrp(ttyfd)) == -1) + ud->tcpgid = 0; + close(ttyfd); + } + 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(); + + /* Store cred for use by sudo_askpass(). */ + sudo_askpass_cred(&ud->cred); + +#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"); + + info[i] = sudo_new_key_val("user", pw->pw_name); + if (info[i] == NULL) + goto oom; + ud->username = 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 ((cp = strdup(ud->shell)) == NULL) + goto oom; + if (!gc_add(GC_PTR, cp)) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + ud->shell = cp; + + if (asprintf(&info[++i], "pid=%d", (int)ud->pid) == -1) + goto oom; + if (asprintf(&info[++i], "ppid=%d", (int)ud->ppid) == -1) + goto oom; + if (asprintf(&info[++i], "pgid=%d", (int)ud->pgid) == -1) + goto oom; + if (asprintf(&info[++i], "tcpgid=%d", (int)ud->tcpgid) == -1) + goto oom; + if (asprintf(&info[++i], "sid=%d", (int)ud->sid) == -1) + goto oom; + if (asprintf(&info[++i], "uid=%u", (unsigned int)ud->cred.uid) == -1) + goto oom; + if (asprintf(&info[++i], "euid=%u", (unsigned int)ud->cred.euid) == -1) + goto oom; + if (asprintf(&info[++i], "gid=%u", (unsigned int)ud->cred.gid) == -1) + goto oom; + if (asprintf(&info[++i], "egid=%u", (unsigned int)ud->cred.egid) == -1) + goto oom; + + if ((cp = get_user_groups(ud->username, &ud->cred)) == NULL) + goto oom; + info[++i] = cp; + if (!gc_add(GC_PTR, ud->cred.groups)) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + mask = umask(0); + umask(mask); + if (asprintf(&info[++i], "umask=0%o", (unsigned int)mask) == -1) + goto oom; + + if (getcwd(path, sizeof(path)) != NULL) { + info[++i] = sudo_new_key_val("cwd", path); + if (info[i] == NULL) + goto oom; + ud->cwd = info[i] + sizeof("cwd=") - 1; + } + + if (get_process_ttyname(path, sizeof(path)) != NULL) { + info[++i] = sudo_new_key_val("tty", path); + if (info[i] == NULL) + goto oom; + ud->tty = 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(); + info[++i] = sudo_new_key_val("host", cp ? cp : "localhost"); + free(cp); + if (info[i] == NULL) + goto oom; + ud->host = info[i] + sizeof("host=") - 1; + + if (asprintf(&info[++i], "lines=%d", ud->ts_rows) == -1) + goto oom; + if (asprintf(&info[++i], "cols=%d", ud->ts_cols) == -1) + goto oom; + + n = serialize_rlimits(&info[i + 1], info_max - (i + 1)); + if (n == (size_t)-1) + goto oom; + i += n; + + info[++i] = NULL; + + /* Add to list of vectors to be garbage collected at exit. */ + if (!gc_add(GC_VECTOR, info)) + goto bad; + + debug_return_ptr(info); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); +bad: + while (i) + free(info[--i]); + free(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) +{ + const char *errstr; + char *cp; + id_t id; + size_t i; + 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, " %zu: %s", i, info[i]); + switch (info[i][0]) { + case 'a': + SET_STRING("apparmor_profile=", apparmor_profile); + break; + case 'c': + SET_STRING("chroot=", chroot) + SET_STRING("command=", command) + SET_STRING("cwd=", runcwd) + SET_FLAG("cwd_optional=", CD_CWD_OPTIONAL) + if (strncmp("closefrom=", info[i], sizeof("closefrom=") - 1) == 0) { + cp = info[i] + sizeof("closefrom=") - 1; + details->closefrom = + (int)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 = + (int)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); + SET(details->flags, CD_FEXECVE); +#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 'i': + SET_FLAG("intercept=", CD_INTERCEPT) + SET_FLAG("intercept_verify=", CD_INTERCEPT_VERIFY) + break; + case 'l': + SET_STRING("login_class=", login_class) + SET_FLAG("log_subcmds=", CD_LOG_SUBCMDS) + break; + case 'n': + if (strncmp("nice=", info[i], sizeof("nice=") - 1) == 0) { + cp = info[i] + sizeof("nice=") - 1; + details->priority = (int)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("rlimit_", info[i], sizeof("rlimit_") - 1) == 0) { + parse_policy_rlimit(info[i] + sizeof("rlimit_") - 1); + break; + } + 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 */ + if (!gc_add(GC_PTR, details->cred.groups)) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + 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) + if (strncmp("sudoedit_nfiles=", info[i], sizeof("sudoedit_nfiles=") - 1) == 0) { + cp = info[i] + sizeof("sudoedit_nfiles=") - 1; + details->nfiles = (int)sudo_strtonum(cp, 1, INT_MAX, + &errstr); + if (errstr != NULL) + sudo_fatalx(U_("%s: %s"), info[i], U_(errstr)); + break; + } + break; + case 't': + if (strncmp("timeout=", info[i], sizeof("timeout=") - 1) == 0) { + cp = info[i] + sizeof("timeout=") - 1; + details->timeout = + (unsigned int)sudo_strtonum(cp, 0, UINT_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_ptrace=", CD_USE_PTRACE) + SET_FLAG("use_pty=", CD_USE_PTY) + SET_STRING("utmp_user=", utmp_user) + break; + } + } + + /* Only use ptrace(2) for intercept/log_subcmds if supported. */ + exec_ptrace_fix_flags(details); + + 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")); + if (!gc_add(GC_PTR, details->pw)) + 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); + if (selinux_getexeccon(details->selinux_role, details->selinux_type) != 0) + exit(EXIT_FAILURE); + } +#endif + +#ifdef HAVE_APPARMOR + if (details->apparmor_profile != NULL && apparmor_is_enabled()) { + if (apparmor_prepare(details->apparmor_profile) != 0) + exit(EXIT_FAILURE); + } +#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) { +#if defined(__linux__) && defined(PR_GET_NO_NEW_PRIVS) + /* The no_new_privs flag disables set-user-ID at execve(2) time. */ + if (prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) == 1) { + sudo_warnx("%s", U_("The \"no new privileges\" flag is set, which " + "prevents sudo from running as root.")); + sudo_warnx("%s", U_("If sudo is running in a container, you may need" + " to adjust the container configuration to disable the flag.")); + exit(EXIT_FAILURE); + } +#endif /* __linux__ && PR_GET_NO_NEW_PRIVS */ + + /* 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 *command_details, + const struct user_details *user_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; + + if (command_details->command == NULL) { + sudo_warnx("%s", U_("command not set by the security policy")); + debug_return_int(status); + } + if (command_details->argv == NULL) { + sudo_warnx("%s", U_("argv not set by the security policy")); + debug_return_int(status); + } + if (command_details->envp == NULL) { + sudo_warnx("%s", U_("envp not set by the security policy")); + debug_return_int(status); + } + + sudo_execute(command_details, user_details, sudo_event_base, &cstat); + + switch (cstat.type) { + case CMD_ERRNO: + /* exec_setup() or execve() returned an error. */ + iolog_close(0, cstat.val); + policy_close(command_details->command, 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(command_details->command, 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) +{ + size_t plugin_settings_size; + struct sudo_debug_file *debug_file; + struct sudo_settings *setting; + char **plugin_settings; + size_t i = 0; + debug_decl(format_plugin_settings, SUDO_DEBUG_PCOMM); + + /* We update the ticket entry by default. */ + if (sudo_settings[ARG_IGNORE_TICKET].value == NULL && + sudo_settings[ARG_UPDATE_TICKET].value == NULL) { + sudo_settings[ARG_UPDATE_TICKET].value = "true"; + } + + /* 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(void) +{ + 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); + 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, submit_envp); + break; + default: + ok = policy_plugin.u.policy->open(SUDO_API_VERSION, sudo_conversation, + sudo_conversation_printf, plugin_settings, user_info, submit_envp, + 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(); + /* XXX - audit */ + sudo_fatalx("%s", U_("unable to initialize policy plugin")); + } + + debug_return; +} + +static void +policy_close(const char *cmnd, 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 (cmnd != NULL) { + errno = error_code; + sudo_warn(U_("unable to execute %s"), cmnd); + } + } + + 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 bool +policy_check(int argc, char * const argv[], char *env_add[], + char **command_info[], char **run_argv[], char **run_envp[]) +{ + 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, run_argv, run_envp, &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(); + /* NOTREACHED */ + } + debug_return_bool(false); + } + debug_return_bool(audit_accept(policy_plugin.name, SUDO_POLICY_PLUGIN, + *command_info, *run_argv, *run_envp)); +} + +sudo_noreturn static void +policy_list(int argc, char * const argv[], int verbose, const char *user) +{ + const char *errstr = NULL; + /* TODO: add list_user */ + const 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, + (char **)command_info, argv, submit_envp); + break; + case 0: + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("command rejected by policy"), + (char **)command_info); + break; + default: + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("policy plugin error"), + (char **)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(NULL, 0, 0); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + + exit(ok != 1); +} + +sudo_noreturn static void +policy_validate(char * const argv[]) +{ + const char *errstr = NULL; + const 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, + (char **)command_info, argv, submit_envp); + break; + case 0: + audit_reject(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("command rejected by policy"), + (char **)command_info); + break; + default: + audit_error(policy_plugin.name, SUDO_POLICY_PLUGIN, + errstr ? errstr : _("policy plugin error"), + (char **)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(NULL, 0, 0); + audit_close(SUDO_PLUGIN_NO_STATUS, 0); + + exit(ok != 1); +} + +sudo_noreturn 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, char * const command_info[], + int argc, char * const argv[], char * const run_envp[], 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); + 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, run_envp); + 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, run_envp); + break; + default: + ret = plugin->u.io->open(SUDO_API_VERSION, sudo_conversation, + sudo_conversation_printf, plugin_settings, user_info, command_info, + argc, argv, run_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 bool +iolog_open(char * const command_info[], int argc, char * const argv[], + char * const run_envp[]) +{ + struct plugin_container *plugin, *next; + const char *errstr = NULL; + debug_decl(iolog_open, SUDO_DEBUG_PCOMM); + + TAILQ_FOREACH_SAFE(plugin, &io_plugins, entries, next) { + int ok = iolog_open_int(plugin, command_info, argc, argv, run_envp, + &errstr); + switch (ok) { + case 1: + break; + case 0: + /* I/O plugin asked to be disabled, remove and free. */ + unlink_plugin(&io_plugins, plugin); + break; + case -2: + usage(); + /* NOTREACHED */ + default: + sudo_warnx(U_("error initializing I/O plugin %s"), + plugin->name); + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("error initializing I/O plugin"), + command_info); + debug_return_bool(false); + } + } + + debug_return_bool(true); +} + +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, int argc, char * const argv[], + char * const envp[]) +{ + 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, NULL, argc, argv, envp, &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, 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); + 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(void) +{ + 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, &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(); + /* NOTREACHED */ + 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 bool +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; + bool ret = true; + 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 : ""); + ret = false; + } + } + + debug_return_bool(ret); +} + +/* + * Command accepted by policy. + * See command_info[] for additional info. + * XXX - actual environment may be updated by policy_init_session(). + */ +bool +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 return. */ + audit_error2(plugin, plugin->name, SUDO_AUDIT_PLUGIN, + errstr ? errstr : _("audit plugin error"), command_info); + debug_return_bool(false); + } + } + + debug_return_bool(true); +} + +/* + * Command rejected by policy or I/O plugin. + */ +bool +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; + bool ret = true; + 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); + + ret = false; + break; + } + } + + debug_return_bool(ret); +} + +/* + * Error from plugin or front-end. + */ +bool +audit_error(const char *plugin_name, unsigned int plugin_type, + const char *audit_msg, char * const command_info[]) +{ + return audit_error2(NULL, plugin_name, plugin_type, audit_msg, + command_info); +} + +static int +approval_open_int(struct plugin_container *plugin) +{ + 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); + 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(); + /* NOTREACHED */ + 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 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); + 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. + */ +bool +approval_check(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 is 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); + 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: + if (!audit_accept(plugin->name, SUDO_APPROVAL_PLUGIN, command_info, + run_argv, run_envp)) + ok = -1; + break; + case -1: + audit_error(plugin->name, SUDO_APPROVAL_PLUGIN, + errstr ? errstr : _("approval plugin error"), + command_info); + break; + case -2: + usage(); + } + + /* 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); + } + + if (ok != 1) + debug_return_bool(false); + } + + debug_return_bool(true); +} + +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..5156cc3 --- /dev/null +++ b/src/sudo.h @@ -0,0 +1,352 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1993-1996, 1998-2005, 2007-2023 + * 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 0x00000001U +#define MODE_EDIT 0x00000002U +#define MODE_VALIDATE 0x00000004U +#define MODE_INVALIDATE 0x00000008U +#define MODE_KILL 0x00000010U +#define MODE_VERSION 0x00000020U +#define MODE_HELP 0x00000040U +#define MODE_LIST 0x00000080U +#define MODE_CHECK 0x00000100U +#define MODE_MASK 0x0000ffffU + +/* Mode flags */ +/* XXX - prune this */ +#define MODE_BACKGROUND 0x00010000U +#define MODE_SHELL 0x00020000U +#define MODE_LOGIN_SHELL 0x00040000U +#define MODE_IMPLIED_SHELL 0x00080000U +#define MODE_RESET_HOME 0x00100000U +#define MODE_PRESERVE_GROUPS 0x00200000U +#define MODE_PRESERVE_ENV 0x00400000U +#define MODE_NONINTERACTIVE 0x00800000U +#define MODE_LONG_LIST 0x01000000U + +/* Indexes into sudo_settings[] args, must match parse_args.c. */ +#define ARG_BSDAUTH_TYPE 0 +#define ARG_LOGIN_CLASS 1 +#define ARG_PRESERVE_ENVIRONMENT 2 +#define ARG_RUNAS_GROUP 3 +#define ARG_SET_HOME 4 +#define ARG_USER_SHELL 5 +#define ARG_LOGIN_SHELL 6 +#define ARG_IGNORE_TICKET 7 +#define ARG_UPDATE_TICKET 8 +#define ARG_PROMPT 9 +#define ARG_SELINUX_ROLE 10 +#define ARG_SELINUX_TYPE 11 +#define ARG_RUNAS_USER 12 +#define ARG_PROGNAME 13 +#define ARG_IMPLIED_SHELL 14 +#define ARG_PRESERVE_GROUPS 15 +#define ARG_NONINTERACTIVE 16 +#define ARG_SUDOEDIT 17 +#define ARG_CLOSEFROM 18 +#define ARG_NET_ADDRS 19 +#define ARG_MAX_GROUPS 20 +#define ARG_PLUGIN_DIR 21 +#define ARG_REMOTE_HOST 22 +#define ARG_TIMEOUT 23 +#define ARG_CHROOT 24 +#define ARG_CWD 25 +#define ARG_ASKPASS 26 +#define ARG_INTERCEPT_SETID 27 +#define ARG_INTERCEPT_PTRACE 28 +#define ARG_APPARMOR_PROFILE 29 + +/* + * Flags for tgetpass() + */ +#define TGP_NOECHO 0x00U /* turn echo off reading pw (default) */ +#define TGP_ECHO 0x01U /* leave echo on when reading passwd */ +#define TGP_STDIN 0x02U /* read from stdin, not /dev/tty */ +#define TGP_ASKPASS 0x04U /* read from askpass helper program */ +#define TGP_MASK 0x08U /* mask user input when reading */ +#define TGP_NOECHO_TRY 0x10U /* turn off echo if possible */ +#define TGP_BELL 0x20U /* 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 0x00000001U +#define CD_SET_EUID 0x00000002U +#define CD_SET_GID 0x00000004U +#define CD_SET_EGID 0x00000008U +#define CD_PRESERVE_GROUPS 0x00000010U +#define CD_INTERCEPT 0x00000020U +#define CD_NOEXEC 0x00000040U +#define CD_SET_PRIORITY 0x00000080U +#define CD_SET_UMASK 0x00000100U +#define CD_SET_TIMEOUT 0x00000200U +#define CD_SUDOEDIT 0x00000400U +#define CD_BACKGROUND 0x00000800U +#define CD_RBAC_ENABLED 0x00001000U +#define CD_USE_PTY 0x00002000U +#define CD_SET_UTMP 0x00004000U +#define CD_EXEC_BG 0x00008000U +#define CD_SUDOEDIT_FOLLOW 0x00010000U +#define CD_SUDOEDIT_CHECKDIR 0x00020000U +#define CD_SET_GROUPS 0x00040000U +#define CD_LOGIN_SHELL 0x00080000U +#define CD_OVERRIDE_UMASK 0x00100000U +#define CD_LOG_SUBCMDS 0x00200000U +#define CD_USE_PTRACE 0x00400000U +#define CD_FEXECVE 0x00800000U +#define CD_INTERCEPT_VERIFY 0x01000000U +#define CD_RBAC_SET_CWD 0x02000000U +#define CD_CWD_OPTIONAL 0x04000000U + +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 argc; + int priority; + unsigned int timeout; + int closefrom; + unsigned int flags; + int execfd; + int nfiles; + struct preserved_fd_list preserved_fds; + struct passwd *pw; + const char *command; + const char *runas_user; + const char *runcwd; + const char *submitcwd; + const char *login_class; + const char *chroot; + const char *selinux_role; + const char *selinux_type; + const char *apparmor_profile; + const char *utmp_user; + const char *tty; + char **argv; + char **envp; +#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 + 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, unsigned int flags, + struct sudo_conv_callback *callback); +const struct sudo_cred *sudo_askpass_cred(const struct sudo_cred *cred); + +/* exec.c */ +int sudo_execute(struct command_details *details, const struct user_details *ud, struct sudo_event_base *evbase, struct command_status *cstat); + +/* parse_args.c */ +unsigned int parse_args(int argc, char **argv, const char *shell, + int *old_optind, int *nargc, char ***nargv, + struct sudo_settings **settingsp, char ***env_addp, const char **list_user); +extern unsigned int tgetpass_flags; + +/* get_pty.c */ +char *get_pty(int *leader, int *follower, uid_t uid); + +/* sudo.c */ +int policy_init_session(struct command_details *details); +int run_command(struct command_details *command_details, const struct user_details *user_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); +bool audit_accept(const char *plugin_name, unsigned int plugin_type, + char * const command_info[], char * const run_argv[], + char * const run_envp[]); +bool audit_reject(const char *plugin_name, unsigned int plugin_type, + const char *audit_msg, char * const command_info[]); +bool audit_error(const char *plugin_name, unsigned int plugin_type, + const char *audit_msg, char * const command_info[]); +bool approval_check(char * const command_info[], char * const run_argv[], + char * const run_envp[]); +extern int sudo_debug_instance; + +/* sudo_edit.c */ +int sudo_edit(struct command_details *command_details, const struct user_details *user_details); + +/* parse_args.c */ +sudo_noreturn void usage(void); + +/* openbsd.c */ +int os_init_openbsd(int argc, char *argv[], char *envp[]); + +/* selinux.c */ +int selinux_audit_role_change(void); +int selinux_getexeccon(const char *role, const char *type); +int selinux_relabel_tty(const char *ttyn, int ttyfd); +int selinux_restore_tty(void); +int selinux_setexeccon(void); +void selinux_execve(int fd, const char *path, char *const argv[], + char *envp[], const char *rundir, unsigned int flags); + +/* apparmor.c */ +int apparmor_is_enabled(void); +int apparmor_prepare(const char* new_profile); + +/* 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); + +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); + +/* limits.c */ +void disable_coredump(void); +void restore_limits(void); +void restore_nproc(void); +void set_policy_rlimits(void); +void unlimit_nproc(void); +void unlimit_sudo(void); +size_t serialize_rlimits(char **info, size_t info_max); +bool parse_policy_rlimit(const char *str); + +/* exec_ptrace.c */ +void exec_ptrace_fix_flags(struct command_details *details); +bool exec_ptrace_intercept_supported(void); +bool exec_ptrace_subcmds_supported(void); + +#endif /* SUDO_SUDO_H */ diff --git a/src/sudo_edit.c b/src/sudo_edit.c new file mode 100644 index 0000000..b7f7ca5 --- /dev/null +++ b/src/sudo_edit.c @@ -0,0 +1,796 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2004-2008, 2010-2023 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(const 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; + size_t 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, (size_t)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); + } + saved_cred.ngroups = getgroups(saved_cred.ngroups, saved_cred.groups); + if (saved_cred.ngroups < 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 *base, *suff; + int len, tfd; + debug_decl(sudo_edit_mktemp, SUDO_DEBUG_EDIT); + + base = sudo_basename(ofile); + suff = strrchr(base, '.'); + if (suff != NULL) { + len = asprintf(tfile, "%s/%.*sXXXXXXXX%s", edit_tmpdir, + (int)(size_t)(suff - base), base, suff); + } else { + len = asprintf(tfile, "%s/%s.XXXXXXXX", edit_tmpdir, base); + } + if (len == -1) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_int(-1); + } + tfd = mkstemps(*tfile, suff ? (int)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(const struct command_details *command_details, + const struct sudo_cred *user_cred, 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_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_cred, &command_details->cred)) + rc = 0; + } + } + switch_user(ROOT_UID, user_cred->egid, user_cred->ngroups, user_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_cred->uid); + if (seteuid(user_cred->uid) != 0) + sudo_fatal("seteuid(%u)", (unsigned int)user_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(const struct command_details *command_details, + const struct sudo_cred *user_cred, 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_cred->uid); + if (seteuid(user_cred->uid) != 0) + sudo_fatal("seteuid(%u)", (unsigned int)user_cred->uid); + tfd = sudo_edit_open(tf[i].tfile, O_RDONLY, + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0, user_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_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_cred, &command_details->cred); + umask(oldmask); + switch_user(ROOT_UID, user_cred->egid, user_cred->ngroups, user_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_setexeccon() == 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(const struct sudo_cred *user_cred) +{ + char *cp, *user_str; + size_t user_size; + int i, len; + debug_decl(selinux_fmt_sudo_user, SUDO_DEBUG_EDIT); + + user_size = (STRLEN_MAX_UNSIGNED(uid_t) + 1) * (2 + user_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_cred->uid, (unsigned int)user_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_cred->ngroups; i++) { + len = snprintf(cp, user_size - (cp - user_str), "%s%u", + i ? "," : "", (unsigned int)user_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(const struct command_details *command_details, + const struct sudo_cred *user_cred, struct tempfile *tf, + char *files[], int nfiles) +{ + const char **sesh_args, **sesh_ap; + char *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); + + 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++ = "--edit-create"; + if (!ISSET(command_details->flags, CD_SUDOEDIT_FOLLOW)) + *sesh_ap++ = "--no-dereference"; + if (ISSET(command_details->flags, CD_SUDOEDIT_CHECKDIR)) { + if ((user_str = selinux_fmt_sudo_user(user_cred)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + *sesh_ap++ = "--edit-checkdir"; + *sesh_ap++ = user_str; + } + *sesh_ap++ = "--"; + + 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 -c [-h] [-w userstr] <o1> <t1> ... <on> <tn> */ + error = selinux_run_helper(command_details->cred.uid, + command_details->cred.gid, command_details->cred.ngroups, + command_details->cred.groups, (char **)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_cred->uid, user_cred->gid) != 0) { + sudo_warn("unable to chown(%s) to %d:%d for editing", + tf[i].tfile, user_cred->uid, user_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(const struct command_details *command_details, + const struct sudo_cred *user_cred, struct tempfile *tf, + int nfiles, struct timespec *times) +{ + const char **sesh_args, **sesh_ap; + char *user_str = NULL; + bool run_helper = false; + 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); + + 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++ = "--edit-install"; + if (ISSET(command_details->flags, CD_SUDOEDIT_CHECKDIR)) { + if ((user_str = selinux_fmt_sudo_user(user_cred)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + *sesh_ap++ = "--edit-checkdir"; + *sesh_ap++ = user_str; + } + *sesh_ap++ = "--"; + + 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_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; + } + } + run_helper = true; + *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 (!run_helper) + goto done; + + /* Run sesh -i <t1> <o1> ... <tn> <on> */ + error = selinux_run_helper(command_details->cred.uid, + command_details->cred.gid, command_details->cred.ngroups, + command_details->cred.groups, (char **)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: + if (tfd != -1) + close(tfd); + /* 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, + const struct user_details *user_details) +{ + struct command_details saved_command_details; + const struct sudo_cred *user_cred = &user_details->cred; + char **nargv = NULL, **files = NULL; + int nfiles = command_details->nfiles; + int errors, i, ac, nargc, ret; + int editor_argc = 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_cred)) + goto cleanup; + + if (nfiles > 0) { + /* + * The plugin specified the number of files to edit, use it. + */ + editor_argc = command_details->argc - nfiles; + if (editor_argc < 2 || strcmp(command_details->argv[editor_argc - 1], "--") != 0) { + sudo_warnx("%s", U_("plugin error: invalid file list for sudoedit")); + goto cleanup; + } + + /* We don't include the "--" when running the user's editor. */ + files = &command_details->argv[editor_argc--]; + } else { + /* + * Compute the number of files to edit by looking for the "--" + * option which separate the editor from the files. + */ + for (i = 0; command_details->argv[i] != NULL; i++) { + if (strcmp(command_details->argv[i], "--") == 0) { + editor_argc = i; + files = &command_details->argv[i + 1]; + nfiles = command_details->argc - (i + 1); + break; + } + } + } + if (nfiles == 0) { + sudo_warnx("%s", U_("plugin error: missing file list for sudoedit")); + goto cleanup; + } + + /* Copy editor files to temporaries. */ + tf = calloc((size_t)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, user_cred, tf, files, nfiles); + else +#endif + nfiles = sudo_edit_create_tfiles(command_details, user_cred, 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, (size_t)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; + } +#ifdef HAVE_SELINUX + if (ISSET(command_details->flags, CD_RBAC_ENABLED)) + selinux_audit_role_change(); +#endif + memcpy(&saved_command_details, command_details, sizeof(struct command_details)); + command_details->cred = *user_cred; + command_details->cred.euid = user_cred->uid; + command_details->cred.egid = user_cred->gid; + command_details->argc = nargc; + command_details->argv = nargv; + ret = run_command(command_details, user_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->argc = saved_command_details.argc; + 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, user_cred, tf, nfiles, times); + else +#endif + errors = sudo_edit_copy_tfiles(command_details, user_cred, 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(const struct command_details *command_details, const struct sudo_cred *user_cred) +{ + 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..3209b9f --- /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, unsigned int sflags, const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred); +int dir_is_writable(int dfd, const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred); +bool sudo_edit_parent_valid(char *path, unsigned int sflags, const struct sudo_cred *user_cred, const 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..7dbda8a --- /dev/null +++ b/src/sudo_exec.h @@ -0,0 +1,236 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2010-2017, 2020-2023 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 + +/* + * Linux-specific wait flag used with ptrace(2). + */ +#ifndef __WALL +# define __WALL 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 + +struct user_details; +struct command_details; +struct command_status; +struct sudo_event_base; +struct stat; + +/* + * Closure passed to I/O event callbacks. + */ +struct exec_closure { + 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 *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; + struct sudo_event *sigwinch_event; + struct command_status *cstat; + char *ptyname; + void *intercept; + pid_t sudo_pid; + pid_t monitor_pid; + pid_t cmnd_pid; + pid_t ppgrp; + int rows; + int cols; + bool foreground; + bool term_raw; +}; + +/* + * 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 *ec; + struct sudo_event *revent; + struct sudo_event *wevent; + sudo_io_action_t action; + unsigned int len; /* buffer length (how much produced) */ + unsigned int off; /* write position (how much already consumed) */ + char buf[64 * 1024]; +}; +SLIST_HEAD(io_buffer_list, io_buffer); + +/* + * Indices into io_fds[] when logging I/O. + */ +#define SFD_STDIN 0 +#define SFD_STDOUT 1 +#define SFD_STDERR 2 +#define SFD_LEADER 3 +#define SFD_FOLLOWER 4 +#define SFD_USERTTY 5 + +/* 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]) + +/* + * 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 */ + +#define INTERCEPT_FD_MIN 64 /* minimum fd so shell won't close it */ +#define MESSAGE_SIZE_MAX 2097152 /* 2Mib max intercept message size */ + +union sudo_token_un { + unsigned char u8[16]; + unsigned int u32[4]; + unsigned long long u64[2]; +}; + +#define sudo_token_isset(_t) ((_t).u64[0] || (_t).u64[1]) + +/* + * Use ptrace-based intercept (using seccomp) on Linux if possible. + * On MIPS we can't change the syscall return and only support log_subcmds. + */ +#if defined(_PATH_SUDO_INTERCEPT) && defined(__linux__) +# if defined(HAVE_DECL_SECCOMP_MODE_FILTER) && HAVE_DECL_SECCOMP_MODE_FILTER +# if defined(__x86_64__) || defined(__i386__) || defined(__aarch64__) || defined(__arm__) || defined(__mips__) || defined(__powerpc__) || (defined(__riscv) && __riscv_xlen == 64) || defined(__s390__) +# ifndef HAVE_PTRACE_INTERCEPT +# define HAVE_PTRACE_INTERCEPT 1 +# endif /* HAVE_PTRACE_INTERCEPT */ +# endif /* __amd64__ || __i386__ || __aarch64__ || __riscv || __s390__ */ +# endif /* HAVE_DECL_SECCOMP_MODE_FILTER */ +#endif /* _PATH_SUDO_INTERCEPT && __linux__ */ + +/* exec.c */ +struct stat; +void exec_cmnd(struct command_details *details, sigset_t *mask, int intercept_fd, int errfd); +void terminate_command(pid_t pid, bool use_pgrp); +bool sudo_terminated(struct command_status *cstat); +void free_exec_closure(struct exec_closure *ec); +bool fd_matches_tty(int fd, struct stat *tty_sb, struct stat *fd_sb); + +/* exec_common.c */ +int sudo_execve(int fd, const char *path, char *const argv[], char *envp[], int intercept_fd, unsigned int flags); +char **disable_execute(char *envp[], const char *dso); +char **enable_monitor(char *envp[], const char *dso); + +/* exec_intercept.c */ +void *intercept_setup(int fd, struct sudo_event_base *evbase, const struct command_details *details); +void intercept_cleanup(struct exec_closure *ec); + +/* exec_iolog.c */ +bool log_ttyin(const char *buf, unsigned int n, struct io_buffer *iob); +bool log_stdin(const char *buf, unsigned int n, struct io_buffer *iob); +bool log_ttyout(const char *buf, unsigned int n, struct io_buffer *iob); +bool log_stdout(const char *buf, unsigned int n, struct io_buffer *iob); +bool log_stderr(const char *buf, unsigned int n, struct io_buffer *iob); +void log_suspend(void *v, int signo); +void log_winchange(struct exec_closure *ec, unsigned int rows, unsigned int cols); +void io_buf_new(int rfd, int wfd, bool (*action)(const char *, unsigned int, struct io_buffer *), void (*read_cb)(int fd, int what, void *v), void (*write_cb)(int fd, int what, void *v), struct exec_closure *ec); +int safe_close(int fd); +void ev_free_by_fd(struct sudo_event_base *evbase, int fd); +void free_io_bufs(void); +void add_io_events(struct exec_closure *ec); +void del_io_events(bool nonblocking); +void init_ttyblock(void); + +/* exec_nopty.c */ +void exec_nopty(struct command_details *details, const struct user_details *user_details, struct sudo_event_base *evbase, struct command_status *cstat); + +/* exec_pty.c */ +bool exec_pty(struct command_details *details, const struct user_details *user_details, struct sudo_event_base *evbase, struct command_status *cstat); +extern int io_fds[6]; + +/* exec_monitor.c */ +int exec_monitor(struct command_details *details, sigset_t *omask, bool foreground, int backchannel, int intercept_fd); + +/* 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); + +/* exec_preload.c */ +char **sudo_preload_dso(char *const envp[], const char *dso_file, int intercept_fd); +char **sudo_preload_dso_mmap(char *const envp[], const char *dso_file, int intercept_fd); + +/* exec_ptrace.c */ +bool exec_ptrace_stopped(pid_t pid, int status, void *intercept); +bool set_exec_filter(void); +int exec_ptrace_seize(pid_t child); + +/* suspend_parent.c */ +void sudo_suspend_parent(int signo, pid_t my_pid, pid_t my_pgrp, pid_t cmnd_pid, void *closure, void (*callback)(void *, int)); + +#endif /* SUDO_EXEC_H */ diff --git a/src/sudo_intercept.c b/src/sudo_intercept.c new file mode 100644 index 0000000..4eeb71c --- /dev/null +++ b/src/sudo_intercept.c @@ -0,0 +1,595 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021-2022 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 <errno.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <limits.h> +#include <string.h> +#include <signal.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include <compat/stdbool.h> +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_SHL_LOAD) +# include <dl.h> +#elif defined(HAVE_DLOPEN) +# include <dlfcn.h> +#endif +#ifdef HAVE_CRT_EXTERNS_H +# include <crt_externs.h> +#endif + +#include <sudo_compat.h> +#include <sudo_debug.h> +#include <sudo_util.h> +#include <pathnames.h> + +/* execl flavors */ +#define SUDO_EXECL 0x0 +#define SUDO_EXECLE 0x1 +#define SUDO_EXECLP 0x2 + +#ifdef HAVE__NSGETENVIRON +# define environ (*_NSGetEnviron()) +#else +extern char **environ; +#endif + +extern bool command_allowed(const char *cmnd, char * const argv[], char * const envp[], char **ncmnd, char ***nargv, char ***nenvp); + +typedef int (*sudo_fn_execve_t)(const char *, char *const *, char *const *); + +static void +free_vector(char **vec) +{ + char **cur; + debug_decl(free_vector, SUDO_DEBUG_EXEC); + + if (vec != NULL) { + for (cur = vec; *cur != NULL; cur++) { + sudo_mmap_free(*cur); + } + sudo_mmap_free(vec); + } + + debug_return; +} + +static char ** +copy_vector(char * const *src) +{ + char **copy; + size_t i, len = 0; + debug_decl(copy_vector, SUDO_DEBUG_EXEC); + + if (src != NULL) { + while (src[len] != NULL) + len++; + } + copy = sudo_mmap_allocarray(len + 1, sizeof(char *)); + if (copy == NULL) { + debug_return_ptr(NULL); + } + for (i = 0; i < len; i++) { + copy[i] = sudo_mmap_strdup(src[i]); + if (copy[i] == NULL) { + free_vector(copy); + debug_return_ptr(NULL); + } + } + copy[i] = NULL; + + debug_return_ptr(copy); +} + +/* + * We do PATH resolution here rather than in the policy because we + * want to use the PATH in the current environment. + */ +static bool +resolve_path(const char *cmnd, char *out_cmnd, size_t out_size) +{ + struct stat sb; + int errval = ENOENT; + char path[PATH_MAX]; + char **p, *cp, *endp; + int dirlen, len; + debug_decl(resolve_path, SUDO_DEBUG_EXEC); + + for (p = environ; (cp = *p) != NULL; p++) { + if (strncmp(cp, "PATH=", sizeof("PATH=") - 1) == 0) { + cp += sizeof("PATH=") - 1; + break; + } + } + if (cp == NULL) { + errno = ENOENT; + debug_return_bool(false); + } + + endp = cp + strlen(cp); + while (cp < endp) { + char *colon = strchr(cp, ':'); + dirlen = colon ? (int)(colon - cp) : (int)(endp - cp); + if (dirlen == 0) { + /* empty PATH component is the same as "." */ + len = snprintf(path, sizeof(path), "./%s", cmnd); + } else { + len = snprintf(path, sizeof(path), "%.*s/%s", dirlen, cp, cmnd); + } + cp = colon ? colon + 1 : endp; + if (len >= ssizeof(path)) { + /* skip too long path */ + errval = ENAMETOOLONG; + continue; + } + + if (stat(path, &sb) == 0) { + if (!S_ISREG(sb.st_mode)) + continue; + if (strlcpy(out_cmnd, path, out_size) >= out_size) { + errval = ENAMETOOLONG; + break; + } + debug_return_bool(true); + } + switch (errno) { + case EACCES: + errval = EACCES; + break; + case ELOOP: + case ENOTDIR: + case ENOENT: + break; + default: + debug_return_bool(false); + } + } + errno = errval; + debug_return_bool(false); +} + +static int +exec_wrapper(const char *cmnd, char * const argv[], char * const envp[], + bool is_execvp) +{ + char *cmnd_copy = NULL, **argv_copy = NULL, **envp_copy = NULL; + char *ncmnd = NULL, **nargv = NULL, **nenvp = NULL; + char cmnd_buf[PATH_MAX]; + void *fn = NULL; + debug_decl(exec_wrapper, SUDO_DEBUG_EXEC); + + if (cmnd == NULL) { + errno = EINVAL; + debug_return_int(-1); + } + + /* Only check PATH for the command for execlp/execvp/execvpe. */ + if (strchr(cmnd, '/') == NULL) { + if (!is_execvp) { + errno = ENOENT; + goto bad; + } + if (!resolve_path(cmnd, cmnd_buf, sizeof(cmnd_buf))) { + goto bad; + } + cmnd = cmnd_buf; + } else { + struct stat sb; + + /* Absolute or relative path name. */ + if (stat(cmnd, &sb) == -1) { + /* Leave errno unchanged. */ + goto bad; + } else if (!S_ISREG(sb.st_mode)) { + errno = EACCES; + goto bad; + } + } + + /* + * Make copies of cmnd, argv, and envp. + */ + cmnd_copy = sudo_mmap_strdup(cmnd); + if (cmnd_copy == NULL) { + debug_return_int(-1); + } + sudo_mmap_protect(cmnd_copy); + cmnd = cmnd_copy; + + argv_copy = copy_vector(argv); + if (argv_copy == NULL) { + goto bad; + } + sudo_mmap_protect(argv_copy); + argv = argv_copy; + + envp_copy = copy_vector(envp); + if (envp_copy == NULL) { + goto bad; + } + sudo_mmap_protect(envp_copy); + envp = envp_copy; + +# if defined(HAVE___INTERPOSE) + fn = execve; +# elif defined(HAVE_DLOPEN) + fn = dlsym(RTLD_NEXT, "execve"); +# elif defined(HAVE_SHL_LOAD) + fn = sudo_shl_get_next("execve", TYPE_PROCEDURE); +# endif + if (fn == NULL) { + errno = EACCES; + goto bad; + } + + if (command_allowed(cmnd, argv, envp, &ncmnd, &nargv, &nenvp)) { + /* Execute the command using the "real" execve() function. */ + ((sudo_fn_execve_t)fn)(ncmnd, nargv, nenvp); + + /* Fall back to exec via shell for execvp and friends. */ + if (errno == ENOEXEC && is_execvp) { + int argc; + const char **shargv; + + for (argc = 0; argv[argc] != NULL; argc++) + continue; + shargv = sudo_mmap_allocarray((size_t)argc + 2, sizeof(char *)); + if (shargv == NULL) + goto bad; + shargv[0] = "sh"; + shargv[1] = ncmnd; + memcpy(shargv + 2, nargv + 1, (size_t)argc * sizeof(char *)); + ((sudo_fn_execve_t)fn)(_PATH_SUDO_BSHELL, (char **)shargv, nenvp); + sudo_mmap_free(shargv); + } + } else { + errno = EACCES; + } + +bad: + sudo_mmap_free(cmnd_copy); + if (ncmnd != cmnd_copy) + sudo_mmap_free(ncmnd); + free_vector(argv_copy); + if (nargv != argv_copy) + free_vector(nargv); + free_vector(envp_copy); + /* Leaks allocated preload vars. */ + if (nenvp != envp_copy) + sudo_mmap_free(nenvp); + + debug_return_int(-1); +} + +static int +execl_wrapper(int type, const char *name, const char *arg, va_list ap) +{ + char * const *envp = environ; + char **argv; + int argc = 1; + va_list ap2; + debug_decl(execl_wrapper, SUDO_DEBUG_EXEC); + + if (name == NULL || arg == NULL) { + errno = EINVAL; + debug_return_int(-1); + } + + va_copy(ap2, ap); + while (va_arg(ap2, char *) != NULL) + argc++; + va_end(ap2); + argv = sudo_mmap_allocarray((size_t)argc + 1, sizeof(char *)); + if (argv == NULL) + debug_return_int(-1); + + argc = 0; + argv[argc++] = (char *)arg; + while ((argv[argc] = va_arg(ap, char *)) != NULL) + argc++; + if (type == SUDO_EXECLE) + envp = va_arg(ap, char **); + + exec_wrapper(name, argv, envp, type == SUDO_EXECLP); + sudo_mmap_free(argv); + + debug_return_int(-1); +} + +static int +system_wrapper(const char *cmnd) +{ + const char * const argv[] = { "sh", "-c", cmnd, NULL }; + const char shell[] = _PATH_SUDO_BSHELL; + struct sigaction saveint, savequit, sa; + sigset_t mask, omask; + pid_t child; + int status; + debug_decl(system_wrapper, SUDO_DEBUG_EXEC); + + /* Special case for NULL command, just check whether shell exists. */ + if (cmnd == NULL) + debug_return_int(access(shell, X_OK) == 0); + + /* First, block signals to avoid potential race conditions. */ + sigemptyset(&mask); + sigaddset(&mask, SIGCHLD); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGQUIT); + if (sigprocmask(SIG_BLOCK, &mask, &omask) == -1) + debug_return_int(-1); + + switch (child = fork()) { + case -1: + /* error */ + (void)sigprocmask(SIG_SETMASK, &omask, NULL); + debug_return_int(-1); + case 0: + /* child */ + if (sigprocmask(SIG_SETMASK, &omask, NULL) != -1) + exec_wrapper(shell, (char **)argv, environ, false); + _exit(127); + default: + /* parent */ + break; + } + + /* We must ignore SIGINT and SIGQUIT until the command finishes. */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_handler = SIG_IGN; + (void)sigaction(SIGINT, &sa, &saveint); + (void)sigaction(SIGQUIT, &sa, &savequit); + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGQUIT); + (void)sigprocmask(SIG_UNBLOCK, &mask, NULL); + + for (;;) { + if (waitpid(child, &status, 0) == -1) { + if (errno == EINTR) + continue; + status = -1; + } + break; + } + + /* Restore signal mask and handlers. */ + (void)sigprocmask(SIG_SETMASK, &omask, NULL); + (void)sigaction(SIGINT, &saveint, NULL); + (void)sigaction(SIGQUIT, &savequit, NULL); + + debug_return_int(status); +} + +#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; + +static int +my_system(const char *cmnd) +{ + return system_wrapper(cmnd); +} + +static int +my_execve(const char *cmnd, char * const argv[], char * const envp[]) +{ + return exec_wrapper(cmnd, argv, envp, false); +} + +static int +my_execv(const char *cmnd, char * const argv[]) +{ + return exec_wrapper(cmnd, argv, environ, false); +} + +#ifdef HAVE_EXECVPE +static int +my_execvpe(const char *cmnd, char * const argv[], char * const envp[]) +{ + return exec_wrapper(cmnd, argv, envp, true); +} +#endif + +static int +my_execvp(const char *cmnd, char * const argv[]) +{ + return exec_wrapper(cmnd, argv, environ, true); +} + +static int +my_execl(const char *name, const char *arg, ...) +{ + va_list ap; + + va_start(ap, arg); + execl_wrapper(SUDO_EXECL, name, arg, ap); + va_end(ap); + + return -1; +} + +static int +my_execle(const char *name, const char *arg, ...) +{ + va_list ap; + + va_start(ap, arg); + execl_wrapper(SUDO_EXECLE, name, arg, ap); + va_end(ap); + + return -1; +} + +static int +my_execlp(const char *name, const char *arg, ...) +{ + va_list ap; + + va_start(ap, arg); + execl_wrapper(SUDO_EXECLP, name, arg, ap); + va_end(ap); + + return -1; +} + +/* Magic to tell dyld to do symbol interposition. */ +__attribute__((__used__)) static const interpose_t interposers[] +__attribute__((__section__("__DATA,__interpose"))) = { + { (void *)my_system, (void *)system }, + { (void *)my_execl, (void *)execl }, + { (void *)my_execle, (void *)execle }, + { (void *)my_execlp, (void *)execlp }, + { (void *)my_execv, (void *)execv }, + { (void *)my_execve, (void *)execve }, + { (void *)my_execvp, (void *)execvp }, +#ifdef HAVE_EXECVPE + { (void *)my_execvpe, (void *)execvpe } +#endif +}; + +#else /* HAVE___INTERPOSE */ + +# if defined(HAVE_SHL_LOAD) +static void * +sudo_shl_get_next(const char *symbol, short type) +{ + const char *name, *myname; + struct shl_descriptor *desc; + void *fn = NULL; + int idx = 0; + debug_decl(sudo_shl_get_next, SUDO_DEBUG_EXEC); + + /* Search for symbol but skip this shared object. */ + /* XXX - could be set to a different path in sudo.conf */ + myname = sudo_basename(_PATH_SUDO_INTERCEPT); + while (shl_get(idx++, &desc) == 0) { + name = sudo_basename(desc->filename); + if (strcmp(name, myname) == 0) + continue; + if (shl_findsym(&desc->handle, symbol, type, &fn) == 0) + break; + } + + debug_return_ptr(fn); +} +# endif /* HAVE_SHL_LOAD */ + +sudo_dso_public int system(const char *cmnd); +sudo_dso_public int execve(const char *cmnd, char * const argv[], char * const envp[]); +sudo_dso_public int execv(const char *cmnd, char * const argv[]); +#ifdef HAVE_EXECVPE +sudo_dso_public int execvpe(const char *cmnd, char * const argv[], char * const envp[]); +#endif +sudo_dso_public int execvp(const char *cmnd, char * const argv[]); +sudo_dso_public int execl(const char *name, const char *arg, ...); +sudo_dso_public int execle(const char *name, const char *arg, ...); +sudo_dso_public int execlp(const char *name, const char *arg, ...); + +int +system(const char *cmnd) +{ + return system_wrapper(cmnd); +} + +int +execve(const char *cmnd, char * const argv[], char * const envp[]) +{ + return exec_wrapper(cmnd, argv, envp, false); +} + +int +execv(const char *cmnd, char * const argv[]) +{ + return execve(cmnd, argv, environ); +} + +#ifdef HAVE_EXECVPE +int +execvpe(const char *cmnd, char * const argv[], char * const envp[]) +{ + return exec_wrapper(cmnd, argv, envp, true); +} +#endif + +int +execvp(const char *cmnd, char * const argv[]) +{ + return exec_wrapper(cmnd, argv, environ, true); +} + +int +execl(const char *name, const char *arg, ...) +{ + va_list ap; + + va_start(ap, arg); + execl_wrapper(SUDO_EXECL, name, arg, ap); + va_end(ap); + + return -1; +} + +int +execle(const char *name, const char *arg, ...) +{ + va_list ap; + + va_start(ap, arg); + execl_wrapper(SUDO_EXECLE, name, arg, ap); + va_end(ap); + + return -1; +} + +int +execlp(const char *name, const char *arg, ...) +{ + va_list ap; + + va_start(ap, arg); + execl_wrapper(SUDO_EXECLP, name, arg, ap); + va_end(ap); + + return -1; +} +#endif /* HAVE___INTERPOSE) */ diff --git a/src/sudo_intercept_common.c b/src/sudo_intercept_common.c new file mode 100644 index 0000000..730614e --- /dev/null +++ b/src/sudo_intercept_common.c @@ -0,0 +1,509 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021-2022 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/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> + +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include <compat/stdbool.h> +#endif /* HAVE_STDBOOL_H */ +#ifdef HAVE_CRT_EXTERNS_H +# include <crt_externs.h> +#endif + +#include <sudo_compat.h> +#include <sudo_conf.h> +#include <sudo_debug.h> +#include <sudo_exec.h> +#include <sudo_fatal.h> +#include <sudo_gettext.h> +#include <sudo_util.h> +#include <intercept.pb-c.h> + +#ifdef HAVE__NSGETENVIRON +# define environ (*_NSGetEnviron()) +#else +extern char **environ; +#endif + +static union sudo_token_un intercept_token; +static in_port_t intercept_port; +static bool log_only; + +/* Send entire request to sudo (blocking). */ +static bool +send_req(int sock, const void *buf, size_t len) +{ + const uint8_t *cp = buf; + ssize_t nwritten; + debug_decl(send_req, SUDO_DEBUG_EXEC); + + do { + nwritten = send(sock, cp, len, 0); + if (nwritten == -1) { + if (errno == EINTR) + continue; + debug_return_bool(false); + } + len -= (size_t)nwritten; + cp += nwritten; + } while (len > 0); + + debug_return_bool(true); +} + +static bool +send_client_hello(int sock) +{ + InterceptRequest msg = INTERCEPT_REQUEST__INIT; + InterceptHello hello = INTERCEPT_HELLO__INIT; + uint8_t *buf = NULL; + uint32_t msg_len; + size_t len; + bool ret = false; + debug_decl(send_client_hello, SUDO_DEBUG_EXEC); + + /* Setup client hello. */ + hello.pid = getpid(); + msg.type_case = INTERCEPT_REQUEST__TYPE_HELLO; + msg.u.hello = &hello; + + len = intercept_request__get_packed_size(&msg); + if (len > MESSAGE_SIZE_MAX) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "InterceptRequest too large: %zu", len); + goto done; + } + /* Wire message size is used for length encoding, precedes message. */ + msg_len = len & 0xffffffff; + len += sizeof(msg_len); + + if ((buf = sudo_mmap_alloc(len)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + memcpy(buf, &msg_len, sizeof(msg_len)); + intercept_request__pack(&msg, buf + sizeof(msg_len)); + + ret = send_req(sock, buf, len); + +done: + sudo_mmap_free(buf); + debug_return_bool(ret); +} + +/* + * Receive InterceptResponse from sudo over fd. + */ +static InterceptResponse * +recv_intercept_response(int fd) +{ + InterceptResponse *res = NULL; + ssize_t nread; + uint32_t rem, res_len; + uint8_t *cp, *buf = NULL; + debug_decl(recv_intercept_response, SUDO_DEBUG_EXEC); + + /* Read message size (uint32_t in host byte order). */ + for (;;) { + nread = recv(fd, &res_len, sizeof(res_len), 0); + if (nread == ssizeof(res_len)) + break; + switch (nread) { + case 0: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected EOF reading response size"); + break; + case -1: + if (errno == EINTR) + continue; + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "error reading response size"); + break; + default: + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "error reading response size: short read"); + break; + } + goto done; + } + if (res_len > MESSAGE_SIZE_MAX) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "InterceptResponse too large: %u", res_len); + goto done; + } + + /* Read response from sudo (blocking). */ + if ((buf = sudo_mmap_alloc(res_len)) == NULL) { + goto done; + } + cp = buf; + rem = res_len; + do { + nread = recv(fd, cp, rem, 0); + switch (nread) { + case 0: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected EOF reading response"); + goto done; + case -1: + if (errno == EINTR) + continue; + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "error reading response"); + goto done; + default: + rem -= (uint32_t)nread; + cp += nread; + break; + } + } while (rem > 0); + res = intercept_response__unpack(NULL, res_len, buf); + if (res == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to unpack %s size %u", "InterceptResponse", res_len); + goto done; + } + +done: + sudo_mmap_free(buf); + debug_return_ptr(res); +} + +/* + * Look up SUDO_INTERCEPT_FD in the environment. + * This function is run when the shared library is loaded. + */ +__attribute__((constructor)) static void +sudo_interposer_init(void) +{ + InterceptResponse *res = NULL; + static bool initialized; + int flags, fd = -1; + char **p; + debug_decl(sudo_interposer_init, SUDO_DEBUG_EXEC); + + if (initialized) + debug_return; + initialized = true; + + /* Read debug and path section of sudo.conf and init debugging. */ + if (sudo_conf_read(NULL, SUDO_CONF_DEBUG|SUDO_CONF_PATHS) != -1) { + sudo_debug_register("sudo_intercept.so", NULL, NULL, + sudo_conf_debug_files("sudo_intercept.so"), INTERCEPT_FD_MIN); + } + sudo_debug_enter(__func__, __FILE__, __LINE__, sudo_debug_subsys); + + /* + * Missing SUDO_INTERCEPT_FD will result in execve() failure. + * Note that we cannot use getenv(3) here on Linux at least. + */ + for (p = environ; *p != NULL; p++) { + if (strncmp(*p, "SUDO_INTERCEPT_FD=", sizeof("SUDO_INTERCEPT_FD=") -1) == 0) { + const char *fdstr = *p + sizeof("SUDO_INTERCEPT_FD=") - 1; + const char *errstr; + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "%s", *p); + + fd = (int)sudo_strtonum(fdstr, 0, INT_MAX, &errstr); + if (errstr != NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "invalid SUDO_INTERCEPT_FD: %s: %s", fdstr, errstr); + goto done; + } + } + } + if (fd == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "SUDO_INTERCEPT_FD not found in environment"); + goto done; + } + + /* + * We don't want to use non-blocking I/O. + */ + flags = fcntl(fd, F_GETFL, 0); + if (flags != -1) + (void)fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); + + /* + * Send InterceptHello message to over the fd. + */ + if (!send_client_hello(fd)) + goto done; + + res = recv_intercept_response(fd); + if (res != NULL) { + if (res->type_case == INTERCEPT_RESPONSE__TYPE_HELLO_RESP) { + intercept_token.u64[0] = res->u.hello_resp->token_lo; + intercept_token.u64[1] = res->u.hello_resp->token_hi; + intercept_port = (in_port_t)res->u.hello_resp->portno; + log_only = res->u.hello_resp->log_only; + } else { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected type_case value %d in %s from %s", + res->type_case, "InterceptResponse", "sudo"); + } + intercept_response__free_unpacked(res, NULL); + } + +done: + if (fd != -1) + close(fd); + + debug_return; +} + +static bool +send_policy_check_req(int sock, const char *cmnd, char * const argv[], + char * const envp[]) +{ + InterceptRequest msg = INTERCEPT_REQUEST__INIT; + PolicyCheckRequest req = POLICY_CHECK_REQUEST__INIT; + char cwdbuf[PATH_MAX]; + char *empty[1] = { NULL }; + uint8_t *buf = NULL; + bool ret = false; + uint32_t msg_len; + size_t len; + debug_decl(fmt_policy_check_req, SUDO_DEBUG_EXEC); + + /* Send token first (out of band) to initiate connection. */ + if (!send_req(sock, &intercept_token, sizeof(intercept_token))) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to send token back to sudo"); + goto done; + } + + /* Setup policy check request. */ + req.intercept_fd = sock; + req.command = (char *)cmnd; + req.argv = argv ? (char **)argv : empty; + for (req.n_argv = 0; req.argv[req.n_argv] != NULL; req.n_argv++) + continue; + req.envp = envp ? (char **)envp : empty; + for (req.n_envp = 0; req.envp[req.n_envp] != NULL; req.n_envp++) + continue; + if (getcwd(cwdbuf, sizeof(cwdbuf)) != NULL) { + req.cwd = cwdbuf; + } + msg.type_case = INTERCEPT_REQUEST__TYPE_POLICY_CHECK_REQ; + msg.u.policy_check_req = &req; + + len = intercept_request__get_packed_size(&msg); + if (len > MESSAGE_SIZE_MAX) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "InterceptRequest too large: %zu", len); + goto done; + } + /* Wire message size is used for length encoding, precedes message. */ + msg_len = len & 0xffffffff; + len += sizeof(msg_len); + + if ((buf = sudo_mmap_alloc(len)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + memcpy(buf, &msg_len, sizeof(msg_len)); + intercept_request__pack(&msg, buf + sizeof(msg_len)); + + ret = send_req(sock, buf, len); + +done: + sudo_mmap_free(buf); + debug_return_bool(ret); +} + +/* + * Connect back to sudo process at localhost:intercept_port + */ +static int +intercept_connect(void) +{ + int sock = -1; + int on = 1; + struct sockaddr_in sin4; + debug_decl(intercept_connect, SUDO_DEBUG_EXEC); + + if (intercept_port == 0) { + sudo_warnx("%s", U_("intercept port not set")); + goto done; + } + + memset(&sin4, 0, sizeof(sin4)); + sin4.sin_family = AF_INET; + sin4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + sin4.sin_port = htons(intercept_port); + + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == -1) { + sudo_warn("socket"); + goto done; + } + + /* Send data immediately, we need low latency IPC. */ + (void)setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); + + if (connect(sock, (struct sockaddr *)&sin4, sizeof(sin4)) == -1) { + sudo_warn("connect"); + close(sock); + sock = -1; + goto done; + } + +done: + debug_return_int(sock); +} + +/* Called from sudo_intercept.c */ +bool command_allowed(const char *cmnd, char * const argv[], char * const envp[], char **ncmndp, char ***nargvp, char ***nenvpp); + +bool +command_allowed(const char *cmnd, char * const argv[], + char * const envp[], char **ncmndp, char ***nargvp, char ***nenvpp) +{ + char *ncmnd = NULL, **nargv = NULL, **nenvp = NULL; + InterceptResponse *res = NULL; + bool ret = false; + size_t idx, len = 0; + int sock; + debug_decl(command_allowed, SUDO_DEBUG_EXEC); + + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "req_command: %s", cmnd); + if (argv != NULL) { + for (idx = 0; argv[idx] != NULL; idx++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "req_argv[%zu]: %s", idx, argv[idx]); + } + } + } + + sock = intercept_connect(); + if (sock == -1) + goto done; + + if (!send_policy_check_req(sock, cmnd, argv, envp)) + goto done; + + if (log_only) { + /* Just logging, no policy check. */ + nenvp = sudo_preload_dso_mmap(envp, sudo_conf_intercept_path(), sock); + if (nenvp == NULL) + goto oom; + *ncmndp = (char *)cmnd; /* safe */ + *nargvp = (char **)argv; /* safe */ + *nenvpp = nenvp; + ret = true; + goto done; + } + + res = recv_intercept_response(sock); + if (res == NULL) + goto done; + + switch (res->type_case) { + case INTERCEPT_RESPONSE__TYPE_ACCEPT_MSG: + if (sudo_debug_needed(SUDO_DEBUG_INFO)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_command: %s", res->u.accept_msg->run_command); + for (idx = 0; idx < res->u.accept_msg->n_run_argv; idx++) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "run_argv[%zu]: %s", idx, res->u.accept_msg->run_argv[idx]); + } + } + ncmnd = sudo_mmap_strdup(res->u.accept_msg->run_command); + if (ncmnd == NULL) + goto oom; + nargv = sudo_mmap_allocarray(res->u.accept_msg->n_run_argv + 1, + sizeof(char *)); + if (nargv == NULL) + goto oom; + for (len = 0; len < res->u.accept_msg->n_run_argv; len++) { + nargv[len] = sudo_mmap_strdup(res->u.accept_msg->run_argv[len]); + if (nargv[len] == NULL) + goto oom; + } + nargv[len] = NULL; + nenvp = sudo_preload_dso_mmap(envp, sudo_conf_intercept_path(), sock); + if (nenvp == NULL) + goto oom; + *ncmndp = ncmnd; + *nargvp = nargv; + *nenvpp = nenvp; + ret = true; + goto done; + case INTERCEPT_RESPONSE__TYPE_REJECT_MSG: + /* Policy module displayed reject message but we are in raw mode. */ + fputc('\r', stderr); + goto done; + case INTERCEPT_RESPONSE__TYPE_ERROR_MSG: + /* Policy module may display error message but we are in raw mode. */ + fputc('\r', stderr); + sudo_warnx("%s", res->u.error_msg->error_message); + goto done; + default: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unexpected type_case value %d in %s from %s", + res->type_case, "InterceptResponse", "sudo"); + goto done; + } + +oom: + sudo_mmap_free(ncmnd); + while (len > 0) + sudo_mmap_free(nargv[--len]); + sudo_mmap_free(nargv); + +done: + /* Keep socket open for ctor when we execute the command. */ + if (!ret && sock != -1) + close(sock); + intercept_response__free_unpacked(res, NULL); + + debug_return_bool(ret); +} diff --git a/src/sudo_noexec.c b/src/sudo_noexec.c new file mode 100644 index 0000000..6ba74d5 --- /dev/null +++ b/src/sudo_noexec.c @@ -0,0 +1,251 @@ +/* + * 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_MODE_FILTER) && HAVE_DECL_SECCOMP_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); \ +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); \ +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); \ +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); \ +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, ...); \ +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); +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); +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; + + /* Search for wordexp() but skip this shared object. */ + myname = sudo_basename(myname); + while (shl_get(idx++, &desc) == 0) { + name = sudo_basename(desc->filename); + 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_MODE_FILTER) && HAVE_DECL_SECCOMP_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_MODE_FILTER */ diff --git a/src/sudo_plugin_int.h b/src/sudo_plugin_int.h new file mode 100644 index 0000000..0dd58c9 --- /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_plugin_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 *user); + int (*validate)(void); + void (*invalidate)(int rmcred); + 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_plugin_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_plugin_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 * restrict 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..6b186f4 --- /dev/null +++ b/src/sudo_usage.h.in @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2007-2010, 2013, 2015, 2017, 2020-2023 + * 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. + */ +static const char *sudo_usage1[] = { + "-h | -K | -k | -V", + NULL +}; +static const char *sudo_usage2[] = { + "-v [-ABkNnS] @BSDAUTH_USAGE@[-g group] [-h host] [-p prompt] [-u user]", + NULL +}; +static const char *sudo_usage3[] = { + "-l [-ABkNnS] @BSDAUTH_USAGE@[-g group] [-h host] [-p prompt] [-U user]", + "[-u user] [command [arg ...]]", + NULL +}; +static const char *sudo_usage4[] = { + "[-ABbEHkNnPS] @BSDAUTH_USAGE@@SELINUX_USAGE@[-C num] @LOGINCAP_USAGE@[-D directory]", + "[-g group] [-h host] [-p prompt] [-R directory] [-T timeout]", + "[-u user] [VAR=value] [-i | -s] [command [arg ...]]", + NULL +}; +static const char *sudo_usage5[] = { + "-e [-ABkNnS] @BSDAUTH_USAGE@@SELINUX_USAGE@[-C num] @LOGINCAP_USAGE@[-D directory]", + "[-g group] [-h host] [-p prompt] [-R directory] [-T timeout]", + "[-u user] file ...", + NULL +}; +static const char * const *sudo_usage[] = { + sudo_usage1, + sudo_usage2, + sudo_usage3, + sudo_usage4, + sudo_usage5, + NULL +}; + +static const char *sudoedit_usage1[] = { + "-h | -V", + NULL +}; +static const char *sudoedit_usage2[] = { + /* Same as sudo_usage5 but no -e flag. */ + "[-ABkNnS] @BSDAUTH_USAGE@@SELINUX_USAGE@[-C num] @LOGINCAP_USAGE@[-D directory]", + "[-g group] [-h host] [-p prompt] [-R directory] [-T timeout]", + "[-u user] file ...", + NULL +}; +static const char * const *sudoedit_usage[] = { + sudoedit_usage1, + sudoedit_usage2, + NULL +}; + +#endif /* SUDO_USAGE_H */ diff --git a/src/suspend_parent.c b/src/suspend_parent.c new file mode 100644 index 0000000..af6957f --- /dev/null +++ b/src/suspend_parent.c @@ -0,0 +1,170 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2023 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 <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> + +#include <pathnames.h> +#include <sudo_debug.h> +#include <sudo_fatal.h> +#include <sudo_gettext.h> +#include <sudo_exec.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. + */ +static int +tcsetpgrp_nobg(int fd, pid_t pgrp_id) +{ + struct sigaction sa, osa; + int rc; + debug_decl(tcsetpgrp_nobg, SUDO_DEBUG_UTIL); + + /* + * 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); + + debug_return_int(rc); +} + +/* + * Suspend the main process in response to an interactive child process + * being suspended. + */ +void +sudo_suspend_parent(int signo, pid_t my_pid, pid_t my_pgrp, pid_t cmnd_pid, + void *closure, void (*callback)(void *, int)) +{ + struct sigaction sa, osa; + pid_t saved_pgrp = -1; + int fd; + debug_decl(sudo_suspend_parent, SUDO_DEBUG_EXEC); + + /* + * 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. + */ + 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 == my_pgrp) { + pid_t cmnd_pgrp = getpgid(cmnd_pid); + if (cmnd_pgrp != my_pgrp) { + if (tcsetpgrp_nobg(fd, cmnd_pgrp) == 0) { + if (killpg(cmnd_pgrp, SIGCONT) != 0) + sudo_warn("kill(%d, SIGCONT)", (int)cmnd_pgrp); + close(fd); + debug_return; + } + } + } + } + } + + /* Run callback before we suspend. */ + if (callback != NULL) + callback(closure, signo); + + if (signo == SIGTSTP) { + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = SIG_DFL; + if (sigaction(SIGTSTP, &sa, &osa) != 0) + sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP); + } + if (kill(my_pid, signo) != 0) + sudo_warn("kill(%d, %d)", (int)my_pid, signo); + if (signo == SIGTSTP) { + if (sigaction(SIGTSTP, &osa, NULL) != 0) + sudo_warn(U_("unable to restore handler for signal %d"), SIGTSTP); + } + + /* Run callback on resume. */ + if (callback != NULL) + callback(closure, SIGCONT); + + 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, + * use tcsetpgrp_nobg() to prevent sudo from receiving SIGTTOU. + */ + if (saved_pgrp != my_pgrp) + tcsetpgrp_nobg(fd, saved_pgrp); + close(fd); + } + + debug_return; +} diff --git a/src/tgetpass.c b/src/tgetpass.c new file mode 100644 index 0000000..c9bc644 --- /dev/null +++ b/src/tgetpass.c @@ -0,0 +1,466 @@ +/* + * 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, unsigned 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((unsigned int)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. */ + if (!sudo_term_restore(input, true)) + sudo_warn("%s", U_("unable to restore terminal settings")); + } + 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; + const struct sudo_cred *cred = sudo_askpass_cred(NULL); + sigset_t chldmask; + enum tgetpass_errval errval; + int pfd[2], status; + pid_t child; + debug_decl(sudo_askpass, SUDO_DEBUG_CONV); + + /* Block SIGCHLD for the duration since we call waitpid() below. */ + sigemptyset(&chldmask); + sigaddset(&chldmask, SIGCHLD); + (void)sigprocmask(SIG_BLOCK, &chldmask, NULL); + + 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(cred->gid)) { + sudo_warn(U_("unable to set gid to %u"), (unsigned int)cred->gid); + _exit(255); + } + if (cred->ngroups != -1) { + if (sudo_setgroups(cred->ngroups, cred->groups) == -1) { + sudo_warn("%s", U_("unable to set supplementary group IDs")); + _exit(255); + } + } + if (setuid(cred->uid)) { + sudo_warn(U_("unable to set uid to %u"), (unsigned int)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 */ + + /* Unblock SIGCHLD. */ + (void)sigprocmask(SIG_UNBLOCK, &chldmask, 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; +} + +const struct sudo_cred * +sudo_askpass_cred(const struct sudo_cred *cred) +{ + static const struct sudo_cred *saved_cred; + + if (cred != NULL) + saved_cred = cred; + return saved_cred; +} diff --git a/src/ttyname.c b/src/ttyname.c new file mode 100644 index 0000000..45f8c21 --- /dev/null +++ b/src/ttyname.c @@ -0,0 +1,378 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2012-2022 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) || defined(HAVE_KINFO_PROC_DFLY) +# 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_DFLY) +# define SUDO_KERN_PROC KERN_PROC +# define sudo_kinfo_proc kinfo_proc +# define sudo_kp_tdev kp_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((dev_t)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 %lu to name", + (unsigned long)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; + } + } + } else { + struct stat sb; + int i; + + /* Missing /proc/pid/psinfo file. */ + for (i = STDIN_FILENO; i <= STDERR_FILENO; i++) { + if (sudo_isatty(i, &sb)) { + ret = sudo_ttyname_dev(sb.st_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; + pid_t ppid = 0; + 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, sizeof(buf) - (size_t)(cp - buf))) != 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', (size_t)(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'; + field++; + if (field == 7) { + int tty_nr = (int)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; + } + if (field == 3) { + ppid = + (int)sudo_strtonum(cp, INT_MIN, INT_MAX, NULL); + } + cp = ep + 1; + } + } + } + } + } + if (ppid == 0) { + struct stat sb; + int i; + + /* No parent pid found, /proc/self/stat is missing or corrupt. */ + for (i = STDIN_FILENO; i <= STDERR_FILENO; i++) { + if (sudo_isatty(i, &sb)) { + ret = sudo_ttyname_dev(sb.st_rdev, name, namelen); + goto done; + } + } + } + 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) +{ + struct stat sb; + char *tty; + int i; + debug_decl(get_process_ttyname, SUDO_DEBUG_UTIL); + + for (i = STDIN_FILENO; i <= STDERR_FILENO; i++) { + /* Only call ttyname() on a character special device. */ + if (fstat(i, &sb) == -1 || !S_ISCHR(sb.st_mode)) + continue; + if ((tty = ttyname(i)) == NULL) + continue; + + if (strlcpy(name, tty, namelen) >= namelen) { + errno = ENAMETOOLONG; + sudo_debug_printf( + SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to store tty from ttyname"); + debug_return_str(NULL); + } + debug_return_str(name); + } + + 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..7ce683d --- /dev/null +++ b/src/utmp.c @@ -0,0 +1,385 @@ +/* + * 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)); + } else if (ut_old != ut_new) { + memcpy(ut_new, ut_old, sizeof(*ut_new)); + } + 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); + sudo_setutent(); + } + 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 */ |