summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/Makefile.in1099
-rw-r--r--src/apparmor.c118
-rw-r--r--src/conversation.c239
-rw-r--r--src/copy_file.c169
-rw-r--r--src/edit_open.c538
-rw-r--r--src/env_hooks.c315
-rw-r--r--src/exec.c586
-rw-r--r--src/exec_common.c136
-rw-r--r--src/exec_intercept.c1109
-rw-r--r--src/exec_intercept.h55
-rw-r--r--src/exec_iolog.c612
-rw-r--r--src/exec_monitor.c727
-rw-r--r--src/exec_nopty.c812
-rw-r--r--src/exec_preload.c419
-rw-r--r--src/exec_ptrace.c2135
-rw-r--r--src/exec_ptrace.h532
-rw-r--r--src/exec_pty.c1452
-rw-r--r--src/get_pty.c189
-rw-r--r--src/hooks.c245
-rw-r--r--src/intercept.exp.in7
-rw-r--r--src/intercept.pb-c.c842
-rw-r--r--src/intercept.proto71
-rw-r--r--src/limits.c714
-rw-r--r--src/load_plugins.c488
-rw-r--r--src/net_ifs.c876
-rw-r--r--src/openbsd.c36
-rw-r--r--src/parse_args.c881
-rw-r--r--src/preload.c77
-rw-r--r--src/preserve_fds.c216
-rw-r--r--src/regress/intercept/test_ptrace.c235
-rw-r--r--src/regress/net_ifs/check_net_ifs.c87
-rw-r--r--src/regress/noexec/check_noexec.c232
-rw-r--r--src/regress/ttyname/check_ttyname.c122
-rw-r--r--src/selinux.c515
-rw-r--r--src/sesh.c489
-rw-r--r--src/signal.c196
-rw-r--r--src/solaris.c117
-rw-r--r--src/sudo.c2255
-rw-r--r--src/sudo.h352
-rw-r--r--src/sudo_edit.c796
-rw-r--r--src/sudo_edit.h55
-rw-r--r--src/sudo_exec.h236
-rw-r--r--src/sudo_intercept.c595
-rw-r--r--src/sudo_intercept_common.c509
-rw-r--r--src/sudo_noexec.c251
-rw-r--r--src/sudo_plugin_int.h133
-rw-r--r--src/sudo_usage.h.in78
-rw-r--r--src/suspend_parent.c170
-rw-r--r--src/tgetpass.c466
-rw-r--r--src/ttyname.c378
-rw-r--r--src/utmp.c385
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 = &regs->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, &regs->u) == -1)
+ debug_return_bool(false);
+# else
+ struct iovec iov;
+ iov.iov_base = &regs->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(&regs, 0, sizeof(regs));
+ if (!ptrace_getregs(pid, &regs, 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(&regs);
+ 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(&regs);
+ 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, &regs, &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, &regs, errno);
+ goto done;
+ }
+
+ /* Must have a pathname. */
+ if (pathname == NULL) {
+ ptrace_fail_syscall(pid, &regs, 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(&regs) - 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(&regs, sp);
+
+ /* Skip over argv pointers (plus NULL) for string table. */
+ strtab += ((size_t)argc + 1 + regs.compat) * regs.wordsize;
+
+ nwritten = ptrace_write_vec(pid, &regs, 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(&regs, 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, &regs)) {
+ 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, &regs, 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, &regs, 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, &regs, 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(&times[0], &times[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(&times[0], &times[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(&times[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(&times[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 */