summaryrefslogtreecommitdiffstats
path: root/lib/iolog
diff options
context:
space:
mode:
Diffstat (limited to 'lib/iolog')
-rw-r--r--lib/iolog/Makefile.in368
-rw-r--r--lib/iolog/host_port.c102
-rw-r--r--lib/iolog/hostcheck.c389
-rw-r--r--lib/iolog/iolog_fileio.c1061
-rw-r--r--lib/iolog/iolog_json.c789
-rw-r--r--lib/iolog/iolog_json.h50
-rw-r--r--lib/iolog/iolog_path.c130
-rw-r--r--lib/iolog/iolog_util.c431
-rw-r--r--lib/iolog/regress/host_port/host_port_test.c145
-rw-r--r--lib/iolog/regress/iolog_json/check_iolog_json.c265
-rw-r--r--lib/iolog/regress/iolog_json/test1.in34
-rw-r--r--lib/iolog/regress/iolog_json/test2.in28
-rw-r--r--lib/iolog/regress/iolog_json/test2.out.ok34
-rw-r--r--lib/iolog/regress/iolog_json/test3.in22
-rw-r--r--lib/iolog/regress/iolog_mkpath/check_iolog_mkpath.c91
-rw-r--r--lib/iolog/regress/iolog_path/check_iolog_path.c273
-rw-r--r--lib/iolog/regress/iolog_path/data96
-rw-r--r--lib/iolog/regress/iolog_util/check_iolog_util.c148
18 files changed, 4456 insertions, 0 deletions
diff --git a/lib/iolog/Makefile.in b/lib/iolog/Makefile.in
new file mode 100644
index 0000000..2ae9c1b
--- /dev/null
+++ b/lib/iolog/Makefile.in
@@ -0,0 +1,368 @@
+#
+# SPDX-License-Identifier: ISC
+#
+# Copyright (c) 2011-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.
+#
+# @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@
+incdir = $(top_srcdir)/include
+scriptdir = $(top_srcdir)/scripts
+cross_compiling = @CROSS_COMPILING@
+
+# Compiler & tools to use
+CC = @CC@
+LIBTOOL = @LIBTOOL@
+
+# Libraries
+LT_LIBS = $(top_builddir)/lib/util/libsudo_util.la
+LIBS = @LIBS@ @ZLIB@ $(LT_LIBS)
+
+# C preprocessor flags
+CPPFLAGS = -I$(incdir) -I$(top_builddir) -I$(srcdir) @CPPFLAGS@
+
+# Usually -O and/or -g
+CFLAGS = @CFLAGS@
+
+# Flags to pass to the link stage
+LDFLAGS = @LDFLAGS@
+
+# Flags to pass to libtool
+LTFLAGS = @LT_STATIC@
+
+# Address sanitizer flags
+ASAN_CFLAGS = @ASAN_CFLAGS@
+ASAN_LDFLAGS = @ASAN_LDFLAGS@
+
+# PIE flags
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+
+# Stack smashing protection flags
+SSP_CFLAGS = @SSP_CFLAGS@
+SSP_LDFLAGS = @SSP_LDFLAGS@
+
+# cppcheck options, usually set in the top-level Makefile
+CPPCHECK_OPTS = -q --enable=warning,performance,portability --suppress=constStatement --suppress=compareBoolExpressionWithInt --error-exitcode=1 --inline-suppr -Dva_copy=va_copy -U__cplusplus -UQUAD_MAX -UQUAD_MIN -UUQUAD_MAX -U_POSIX_HOST_NAME_MAX -U_POSIX_PATH_MAX -U__NBBY -DNSIG=64
+
+# splint options, usually set in the top-level Makefile
+SPLINT_OPTS = -D__restrict= -checks
+
+# PVS-studio options
+PVS_CFG = $(top_srcdir)/PVS-Studio.cfg
+PVS_IGNORE = 'V707,V011,V002,V536'
+PVS_LOG_OPTS = -a 'GA:1,2' -e -t errorfile -d $(PVS_IGNORE)
+
+# Regression tests
+TEST_PROGS = check_iolog_json check_iolog_mkpath check_iolog_path check_iolog_util host_port_test
+TEST_LIBS = @LIBS@ $(top_builddir)/lib/eventlog/libsudo_eventlog.la
+TEST_LDFLAGS = @LDFLAGS@
+
+# Set to non-empty for development mode
+DEVEL = @DEVEL@
+
+#### End of system configuration section. ####
+
+SHELL = @SHELL@
+
+LIBIOLOG_OBJS = iolog_fileio.lo iolog_json.lo iolog_path.lo iolog_util.lo \
+ host_port.lo hostcheck.lo
+
+IOBJS = $(LIBIOLOG_OBJS:.lo=.i)
+
+POBJS = $(IOBJS:.i=.plog)
+
+CHECK_IOLOG_MKPATH_OBJS = check_iolog_mkpath.lo iolog_fileio.lo
+
+CHECK_IOLOG_PATH_OBJS = check_iolog_path.lo iolog_path.lo
+
+CHECK_IOLOG_UTIL_OBJS = check_iolog_util.lo iolog_json.lo iolog_util.lo
+
+CHECK_IOLOG_JSON_OBJS = check_iolog_json.lo iolog_json.lo
+
+HOST_PORT_TEST_OBJS = host_port_test.lo host_port.lo
+
+all: libsudo_iolog.la
+
+pvs-log-files: $(POBJS)
+
+pvs-studio: $(POBJS)
+ plog-converter $(PVS_LOG_OPTS) $(POBJS)
+
+depend:
+ $(scriptdir)/mkdep.pl --srcdir=$(abs_top_srcdir) \
+ --builddir=$(abs_top_builddir) lib/iolog/Makefile.in
+ cd $(top_builddir) && ./config.status --file lib/iolog/Makefile
+
+Makefile: $(srcdir)/Makefile.in
+ cd $(top_builddir) && ./config.status --file lib/iolog/Makefile
+
+.SUFFIXES: .c .h .i .lo .plog
+
+.c.lo:
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $<
+
+.c.i:
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+
+.i.plog:
+ ifile=$<; rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $${ifile%i}c --i-file $< --output-file $@
+
+libsudo_iolog.la: $(LIBIOLOG_OBJS)
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(LIBIOLOG_OBJS) $(LT_LIBS) @ZLIB@ @NET_LIBS@
+
+check_iolog_path: $(CHECK_IOLOG_PATH_OBJS) libsudo_iolog.la
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_IOLOG_PATH_OBJS) libsudo_iolog.la $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(TEST_LDFLAGS) $(TEST_LIBS)
+
+check_iolog_mkpath: $(CHECK_IOLOG_MKPATH_OBJS) libsudo_iolog.la
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_IOLOG_MKPATH_OBJS) libsudo_iolog.la $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(TEST_LDFLAGS) $(TEST_LIBS)
+
+check_iolog_util: $(CHECK_IOLOG_UTIL_OBJS) libsudo_iolog.la
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_IOLOG_UTIL_OBJS) libsudo_iolog.la $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(TEST_LDFLAGS) $(TEST_LIBS)
+
+check_iolog_json: $(CHECK_IOLOG_JSON_OBJS) libsudo_iolog.la
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_IOLOG_JSON_OBJS) libsudo_iolog.la $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(TEST_LDFLAGS) $(TEST_LIBS)
+
+host_port_test: $(HOST_PORT_TEST_OBJS) libsudo_iolog.la
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(HOST_PORT_TEST_OBJS) libsudo_iolog.la $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(TEST_LDFLAGS) $(TEST_LIBS)
+
+pre-install:
+
+install:
+
+install-binaries:
+
+install-includes:
+
+install-doc:
+
+install-plugin:
+
+uninstall:
+
+splint:
+ splint $(SPLINT_OPTS) -I$(incdir) -I$(top_builddir) $(srcdir)/*.c
+
+cppcheck:
+ cppcheck $(CPPCHECK_OPTS) -I$(incdir) -I$(top_builddir) $(srcdir)/*.c
+
+pvs-log-files: $(POBJS)
+
+check: $(TEST_PROGS)
+ @if test X"$(cross_compiling)" != X"yes"; then \
+ LC_ALL=C; export LC_ALL; \
+ unset LANG || LANG=; \
+ rval=0; \
+ ./check_iolog_json $(srcdir)/regress/iolog_json/*.in || rval=`expr $$rval + $$?`; \
+ ./check_iolog_path $(srcdir)/regress/iolog_path/data || rval=`expr $$rval + $$?`; \
+ ./check_iolog_mkpath || rval=`expr $$rval + $$?`; \
+ ./check_iolog_util || rval=`expr $$rval + $$?`; \
+ ./host_port_test || rval=`expr $$rval + $$?`; \
+ exit $$rval; \
+ fi
+
+clean:
+ -$(LIBTOOL) $(LTFLAGS) --mode=clean rm -f $(TEST_PROGS) *.lo *.o *.la
+ -rm -f *.i *.plog stamp-* core *.core core.* regress/*/*.out \
+ regress/*/*.err
+
+mostlyclean: clean
+
+distclean: clean
+ -rm -rf Makefile .libs
+
+clobber: distclean
+
+realclean: distclean
+ rm -f TAGS tags
+
+cleandir: realclean
+
+# Autogenerated dependencies, do not modify
+check_iolog_json.lo: $(srcdir)/regress/iolog_json/check_iolog_json.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_json.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(srcdir)/iolog_json.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/iolog_json/check_iolog_json.c
+check_iolog_json.i: $(srcdir)/regress/iolog_json/check_iolog_json.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_json.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(srcdir)/iolog_json.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+check_iolog_json.plog: check_iolog_json.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/iolog_json/check_iolog_json.c --i-file $< --output-file $@
+check_iolog_mkpath.lo: $(srcdir)/regress/iolog_mkpath/check_iolog_mkpath.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/iolog_mkpath/check_iolog_mkpath.c
+check_iolog_mkpath.i: $(srcdir)/regress/iolog_mkpath/check_iolog_mkpath.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+check_iolog_mkpath.plog: check_iolog_mkpath.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/iolog_mkpath/check_iolog_mkpath.c --i-file $< --output-file $@
+check_iolog_path.lo: $(srcdir)/regress/iolog_path/check_iolog_path.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/iolog_path/check_iolog_path.c
+check_iolog_path.i: $(srcdir)/regress/iolog_path/check_iolog_path.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+check_iolog_path.plog: check_iolog_path.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/iolog_path/check_iolog_path.c --i-file $< --output-file $@
+check_iolog_util.lo: $(srcdir)/regress/iolog_util/check_iolog_util.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/iolog_util/check_iolog_util.c
+check_iolog_util.i: $(srcdir)/regress/iolog_util/check_iolog_util.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+check_iolog_util.plog: check_iolog_util.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/iolog_util/check_iolog_util.c --i-file $< --output-file $@
+host_port.lo: $(srcdir)/host_port.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/host_port.c
+host_port.i: $(srcdir)/host_port.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+host_port.plog: host_port.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/host_port.c --i-file $< --output-file $@
+host_port_test.lo: $(srcdir)/regress/host_port/host_port_test.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/host_port/host_port_test.c
+host_port_test.i: $(srcdir)/regress/host_port/host_port_test.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+host_port_test.plog: host_port_test.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/host_port/host_port_test.c --i-file $< --output-file $@
+hostcheck.lo: $(srcdir)/hostcheck.c $(incdir)/compat/stdbool.h \
+ $(incdir)/hostcheck.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/hostcheck.c
+hostcheck.i: $(srcdir)/hostcheck.c $(incdir)/compat/stdbool.h \
+ $(incdir)/hostcheck.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+hostcheck.plog: hostcheck.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/hostcheck.c --i-file $< --output-file $@
+iolog_fileio.lo: $(srcdir)/iolog_fileio.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_json.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h \
+ $(top_builddir)/pathnames.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_fileio.c
+iolog_fileio.i: $(srcdir)/iolog_fileio.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_json.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h \
+ $(top_builddir)/pathnames.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+iolog_fileio.plog: iolog_fileio.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_fileio.c --i-file $< --output-file $@
+iolog_json.lo: $(srcdir)/iolog_json.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_json.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(srcdir)/iolog_json.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_json.c
+iolog_json.i: $(srcdir)/iolog_json.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_json.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(srcdir)/iolog_json.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+iolog_json.plog: iolog_json.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_json.c --i-file $< --output-file $@
+iolog_path.lo: $(srcdir)/iolog_path.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_path.c
+iolog_path.i: $(srcdir)/iolog_path.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+iolog_path.plog: iolog_path.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_path.c --i-file $< --output-file $@
+iolog_util.lo: $(srcdir)/iolog_util.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_util.c
+iolog_util.i: $(srcdir)/iolog_util.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+iolog_util.plog: iolog_util.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_util.c --i-file $< --output-file $@
diff --git a/lib/iolog/host_port.c b/lib/iolog/host_port.c
new file mode 100644
index 0000000..b16c658
--- /dev/null
+++ b/lib/iolog/host_port.c
@@ -0,0 +1,102 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "config.h"
+
+#ifdef HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# include "compat/stdbool.h"
+#endif /* HAVE_STDBOOL_H */
+#include <stdio.h>
+#include <string.h>
+
+#include "sudo_compat.h"
+#include "sudo_debug.h"
+#include "sudo_gettext.h"
+#include "sudo_util.h"
+
+/*
+ * Parse a string in the form host[:port] where host can also be
+ * an IPv4 address or an IPv6 address in square brackets.
+ * Fills in hostp and portp which may point within str, which is modified.
+ */
+bool
+iolog_parse_host_port(char *str, char **hostp, char **portp, bool *tlsp,
+ char *defport, char *defport_tls)
+{
+ char *flags, *port, *host = str;
+ bool ret = false;
+ bool tls = false;
+ debug_decl(iolog_parse_host_port, SUDO_DEBUG_UTIL);
+
+ /* Check for IPv6 address like [::0] followed by optional port */
+ if (*host == '[') {
+ host++;
+ port = strchr(host, ']');
+ if (port == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid IPv6 address %s", str);
+ goto done;
+ }
+ *port++ = '\0';
+ switch (*port) {
+ case ':':
+ port++;
+ break;
+ case '\0':
+ port = NULL; /* no port specified */
+ break;
+ case '(':
+ /* flag, handled below */
+ break;
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid IPv6 address %s", str);
+ goto done;
+ }
+ } else {
+ port = strrchr(host, ':');
+ if (port != NULL)
+ *port++ = '\0';
+ }
+
+ /* Check for optional tls flag at the end. */
+ flags = strchr(port ? port : host, '(');
+ if (flags != NULL) {
+ if (strcasecmp(flags, "(tls)") == 0)
+ tls = true;
+ *flags = '\0';
+ if (port == flags)
+ port = NULL;
+ }
+
+ if (port == NULL)
+ port = tls ? defport_tls : defport;
+ else if (*port == '\0')
+ goto done;
+
+ *hostp = host;
+ *portp = port;
+ *tlsp = tls;
+
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
diff --git a/lib/iolog/hostcheck.c b/lib/iolog/hostcheck.c
new file mode 100644
index 0000000..0dee14b
--- /dev/null
+++ b/lib/iolog/hostcheck.c
@@ -0,0 +1,389 @@
+/*
+ * Copyright (c) 2020 Laszlo Orban <laszlo.orban@oneidentity.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"
+
+#if defined(HAVE_OPENSSL)
+# include <sys/types.h>
+# include <sys/socket.h>
+# include <arpa/inet.h>
+# include <stdlib.h>
+# include <string.h>
+# include <netdb.h>
+
+# define NEED_INET_NTOP /* to expose sudo_inet_ntop in sudo_compat.h */
+
+# include "sudo_compat.h"
+# include "sudo_debug.h"
+# include "sudo_util.h"
+# include "hostcheck.h"
+
+/**
+ * @brief Checks if given hostname resolves to the given IP address.
+ *
+ * @param hostname hostname to be resolved
+ * @param ipaddr ip address to be checked
+ *
+ * @return 1 if hostname resolves to the given IP address
+ * 0 otherwise
+ */
+static int
+forward_lookup_match(const char *hostname, const char *ipaddr)
+{
+ int rc, ret = 0;
+ struct addrinfo *res = NULL, *p;
+ void *addr;
+ struct sockaddr_in *ipv4;
+#if defined(HAVE_STRUCT_IN6_ADDR)
+ struct sockaddr_in6 *ipv6;
+ char ipstr[INET6_ADDRSTRLEN];
+#else
+ char ipstr[INET_ADDRSTRLEN];
+#endif
+ debug_decl(forward_lookup_match, SUDO_DEBUG_UTIL);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "verify %s resolves to %s", hostname, ipaddr);
+
+ if ((rc = getaddrinfo(hostname, NULL, NULL, &res)) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to resolve %s: %s", hostname, gai_strerror(rc));
+ goto exit;
+ }
+
+ for (p = res; p != NULL; p = p->ai_next) {
+ if (p->ai_family == AF_INET) {
+ ipv4 = (struct sockaddr_in *)p->ai_addr;
+ addr = &(ipv4->sin_addr);
+#if defined(HAVE_STRUCT_IN6_ADDR)
+ } else if (p->ai_family == AF_INET6) {
+ ipv6 = (struct sockaddr_in6 *)p->ai_addr;
+ addr = &(ipv6->sin6_addr);
+#endif
+ } else {
+ goto exit;
+ }
+
+ if (inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr)) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "comparing %s to %s", ipstr, ipaddr);
+ if (strcmp(ipaddr, ipstr) == 0) {
+ ret = 1;
+ break;
+ }
+ }
+ }
+
+exit:
+ if (res != NULL) {
+ freeaddrinfo(res);
+ }
+ debug_return_int(ret);
+}
+
+/**
+ * @brief Compares the given hostname with a DNS entry in a certificate.
+ *
+ * The certificate DNS name can contain wildcards in the left-most label.
+ * A wildcard can match only one label.
+ * Accepted names:
+ * - foo.bar.example.com
+ * - *.example.com
+ * - *.bar.example.com
+ *
+ * @param hostname peer's name
+ * @param certname_asn1 hostname in the certificate
+ *
+ * @return MatchFound
+ * MatchNotFound
+ */
+static HostnameValidationResult
+validate_name(const char *hostname, ASN1_STRING *certname_asn1)
+{
+ char *certname_s = (char *) ASN1_STRING_get0_data(certname_asn1);
+ int certname_len = ASN1_STRING_length(certname_asn1);
+ int hostname_len = strlen(hostname);
+ debug_decl(validate_name, SUDO_DEBUG_UTIL);
+
+ /* remove last '.' from hostname if exists */
+ if (hostname_len != 0 && hostname[hostname_len - 1] == '.') {
+ --hostname_len;
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "comparing %.*s to %.*s in cert", hostname_len, hostname,
+ certname_len, certname_s);
+
+ /* skip the first label if wildcard */
+ if (certname_len > 2 && certname_s[0] == '*' && certname_s[1] == '.') {
+ if (hostname_len != 0) {
+ do {
+ --hostname_len;
+ if (*hostname++ == '.') {
+ break;
+ }
+ } while (hostname_len != 0);
+ }
+ certname_s += 2;
+ certname_len -= 2;
+ }
+ /* Compare expected hostname with the DNS name */
+ if (certname_len != hostname_len) {
+ debug_return_int(MatchNotFound);
+ }
+ if (strncasecmp(hostname, certname_s, hostname_len) != 0) {
+ debug_return_int(MatchNotFound);
+ }
+
+ debug_return_int(MatchFound);
+}
+
+/**
+ * @brief Matches a hostname with the cert's CN.
+ *
+ * @param hostname peer's name
+ * on client side: it is the name where the client is connected to
+ * on server side, it is in fact an IP address of the remote client
+ * @param ipaddr peer's IP address
+ * @param cert peer's X509 certificate
+ * @param resolve if the value is not 0, the function checks that the value of the CN
+ * resolves to the given ipaddr or not.
+ *
+ * @return MatchFound
+ * MatchNotFound
+ * MalformedCertificate
+ * Error
+ */
+static HostnameValidationResult
+matches_common_name(const char *hostname, const char *ipaddr, const X509 *cert, int resolve)
+{
+ X509_NAME_ENTRY *common_name_entry = NULL;
+ ASN1_STRING *common_name_asn1 = NULL;
+ int common_name_loc = -1;
+ debug_decl(matches_common_name, SUDO_DEBUG_UTIL);
+
+ /* Find the position of the CN field in the Subject field of the certificate */
+ common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name((X509 *) cert), NID_commonName, -1);
+ if (common_name_loc < 0) {
+ debug_return_int(Error);
+ }
+
+ /* Extract the CN field */
+ common_name_entry = X509_NAME_get_entry(X509_get_subject_name((X509 *) cert), common_name_loc);
+ if (common_name_entry == NULL) {
+ debug_return_int(Error);
+ }
+
+ /* Convert the CN field to a C string */
+ common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry);
+ if (common_name_asn1 == NULL) {
+ debug_return_int(Error);
+ }
+ const unsigned char *common_name_str = ASN1_STRING_get0_data(common_name_asn1);
+
+ /* Make sure there isn't an embedded NUL character in the CN */
+ if (memchr(common_name_str, '\0', ASN1_STRING_length(common_name_asn1)) != NULL) {
+ debug_return_int(MalformedCertificate);
+ }
+
+ /* Compare expected hostname with the CN */
+ if (validate_name(hostname, common_name_asn1) == MatchFound) {
+ debug_return_int(MatchFound);
+ }
+
+ int common_name_length = ASN1_STRING_length(common_name_asn1);
+ char *nullterm_common_name = malloc(common_name_length + 1);
+
+ if (nullterm_common_name == NULL) {
+ debug_return_int(Error);
+ }
+
+ memcpy(nullterm_common_name, common_name_str, common_name_length);
+ nullterm_common_name[common_name_length] = '\0';
+
+
+ /* check if hostname in the CN field resolves to the given ip address */
+ if (resolve && forward_lookup_match(nullterm_common_name, ipaddr)) {
+ free(nullterm_common_name);
+ debug_return_int(MatchFound);
+ }
+
+ free(nullterm_common_name);
+ debug_return_int(MatchNotFound);
+}
+
+/**
+ * @brief Matches a hostname or ipaddr with the cert's corresponding SAN field.
+ *
+ * SAN can have different fields. For hostname matching, the GEN_DNS field is used,
+ * for IP address matching, the GEN_IPADD field is used.
+ * Since SAN is an X503 v3 extension, it can happen that the cert does
+ * not contain SAN at all.
+ *
+ * @param hostname remote peer's name
+ * on client side: it is the name where the client is connected to
+ * on server side, it is in fact an IP address of the remote client
+ * @param ipaddr remote peer's IP address
+ * @param cert peer's X509 certificate
+ * @param resolve if the value is not 0, the function checks that the value of the
+ * SAN GEN_DNS resolves to the given ipaddr or not.
+ *
+ * @return MatchFound
+ * MatchNotFound
+ * NoSANPresent
+ * MalformedCertificate
+ * Error
+ */
+static HostnameValidationResult
+matches_subject_alternative_name(const char *hostname, const char *ipaddr, const X509 *cert, int resolve)
+{
+ HostnameValidationResult result = MatchNotFound;
+ int i;
+ int san_names_nb = -1;
+ STACK_OF(GENERAL_NAME) *san_names = NULL;
+ debug_decl(matches_subject_alternative_name, SUDO_DEBUG_UTIL);
+
+ /* Try to extract the names within the SAN extension from the certificate */
+ san_names = X509_get_ext_d2i((X509 *) cert, NID_subject_alt_name, NULL, NULL);
+ if (san_names == NULL) {
+ debug_return_int(NoSANPresent);
+ }
+ san_names_nb = sk_GENERAL_NAME_num(san_names);
+
+ /* Check each name within the extension */
+ for (i=0; i<san_names_nb; i++) {
+ const GENERAL_NAME *current_name = sk_GENERAL_NAME_value(san_names, i);
+
+ if (current_name->type == GEN_DNS) {
+ const unsigned char *dns_name = ASN1_STRING_get0_data(current_name->d.dNSName);
+
+ /* Make sure there isn't an embedded NUL character in the DNS name */
+ if (memchr(dns_name, '\0', ASN1_STRING_length(current_name->d.dNSName)) != NULL) {
+ result = MalformedCertificate;
+ break;
+ } else {
+ /* Compare expected hostname with the DNS name */
+ if (validate_name(hostname, current_name->d.dNSName) == MatchFound) {
+ result = MatchFound;
+ break;
+ }
+
+ int dns_name_length = ASN1_STRING_length(current_name->d.dNSName);
+ char *nullterm_dns_name = malloc(dns_name_length + 1);
+
+ if (nullterm_dns_name == NULL) {
+ debug_return_int(Error);
+ }
+
+ memcpy(nullterm_dns_name, dns_name, dns_name_length);
+ nullterm_dns_name[dns_name_length] = '\0';
+
+ if (resolve && forward_lookup_match(nullterm_dns_name, ipaddr)) {
+ free(nullterm_dns_name);
+ result = MatchFound;
+ break;
+ }
+ free(nullterm_dns_name);
+ }
+ } else if (current_name->type == GEN_IPADD) {
+ const unsigned char *san_ip = ASN1_STRING_get0_data(current_name->d.iPAddress);
+#if defined(HAVE_STRUCT_IN6_ADDR)
+ char san_ip_str[INET6_ADDRSTRLEN];
+#else
+ char san_ip_str[INET_ADDRSTRLEN];
+#endif
+
+ /* IPV4 address */
+ if(current_name->d.iPAddress->length == 4) {
+ if (inet_ntop(AF_INET, san_ip, san_ip_str, INET_ADDRSTRLEN) == NULL) {
+ result = MalformedCertificate;
+ break;
+ }
+#if defined(HAVE_STRUCT_IN6_ADDR)
+ /* IPV6 address */
+ } else if (current_name->d.iPAddress->length == 16) {
+ if (inet_ntop(AF_INET6, san_ip, san_ip_str, INET6_ADDRSTRLEN) == NULL) {
+ result = MalformedCertificate;
+ break;
+ }
+# endif
+ } else {
+ result = MalformedCertificate;
+ break;
+ }
+
+ if (strcasecmp(ipaddr, san_ip_str) == 0) {
+ result = MatchFound;
+ break;
+ }
+ }
+ }
+ sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free);
+
+ debug_return_int(result);
+}
+
+/**
+ * @brief Do hostname/IP validation on the given X509 certificate.
+ *
+ * According to RFC 6125 section 6.4.4, first the certificate's SAN field
+ * has to be checked. If there is no SAN field, the certificate's CN field
+ * has to be checked.
+ *
+ * @param cert X509 certificate
+ * @param hostname remote peer's name
+ * on client side: it is the name where the client is connected to
+ * on server side, it is in fact an IP address of the remote client
+ * @param ipaddr remote peer's IP address
+ * @param resolve if the value is not 0, the function checks that the value of the
+ * SAN GEN_DNS or the value of CN resolves to the given ipaddr or not.
+ *
+ * @return MatchFound
+ * MatchNotFound
+ * MalformedCertificate
+ * Error
+ */
+HostnameValidationResult
+validate_hostname(const X509 *cert, const char *hostname, const char *ipaddr, int resolve)
+{
+ HostnameValidationResult res = MatchFound;
+ debug_decl(validate_hostname, SUDO_DEBUG_UTIL);
+
+ /* hostname can be also an ip address, if client connects
+ * to ip instead of FQDN
+ */
+ if((ipaddr == NULL) || (cert == NULL)) {
+ debug_return_int(Error);
+ }
+
+ /* check SAN first if exists */
+ res = matches_subject_alternative_name(hostname, ipaddr, cert, resolve);
+
+ /* According to RFC 6125 section 6.4.4, check CN only,
+ * if no SAN name was provided
+ */
+ if (res == NoSANPresent) {
+ res = matches_common_name(hostname, ipaddr, cert, resolve);
+ }
+
+ debug_return_int(res);
+}
+#endif /* HAVE_OPENSSL */
diff --git a/lib/iolog/iolog_fileio.c b/lib/iolog/iolog_fileio.c
new file mode 100644
index 0000000..84aeaa8
--- /dev/null
+++ b/lib/iolog/iolog_fileio.c
@@ -0,0 +1,1061 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2009-2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include <config.h>
+
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdlib.h>
+#ifdef HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# include "compat/stdbool.h"
+#endif
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+
+#include "pathnames.h"
+#include "sudo_compat.h"
+#include "sudo_conf.h"
+#include "sudo_debug.h"
+#include "sudo_eventlog.h"
+#include "sudo_fatal.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_json.h"
+#include "sudo_queue.h"
+#include "sudo_util.h"
+
+static unsigned char const gzip_magic[2] = {0x1f, 0x8b};
+static unsigned int sessid_max = SESSID_MAX;
+static mode_t iolog_filemode = S_IRUSR|S_IWUSR;
+static mode_t iolog_dirmode = S_IRWXU;
+static uid_t iolog_uid = ROOT_UID;
+static gid_t iolog_gid = ROOT_GID;
+static bool iolog_gid_set;
+static bool iolog_compress;
+static bool iolog_flush;
+
+/*
+ * Set effective user and group-IDs to iolog_uid and iolog_gid.
+ * If restore flag is set, swap them back.
+ */
+static bool
+io_swapids(bool restore)
+{
+#ifdef HAVE_SETEUID
+ static uid_t user_euid = (uid_t)-1;
+ static gid_t user_egid = (gid_t)-1;
+ debug_decl(io_swapids, SUDO_DEBUG_UTIL);
+
+ if (user_euid == (uid_t)-1)
+ user_euid = geteuid();
+ if (user_egid == (gid_t)-1)
+ user_egid = getegid();
+
+ if (restore) {
+ if (seteuid(user_euid) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to restore effective uid to %d", __func__,
+ (int)user_euid);
+ sudo_warn("seteuid() %d -> %d", (int)iolog_uid, (int)user_euid);
+ debug_return_bool(false);
+ }
+ if (setegid(user_egid) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to restore effective gid to %d", __func__,
+ (int)user_egid);
+ sudo_warn("setegid() %d -> %d", (int)iolog_gid, (int)user_egid);
+ debug_return_bool(false);
+ }
+ } else {
+ /* Fail silently if the user has insufficient privileges. */
+ if (setegid(iolog_gid) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to set effective gid to %d", __func__,
+ (int)iolog_gid);
+ debug_return_bool(false);
+ }
+ if (seteuid(iolog_uid) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to set effective uid to %d", __func__,
+ (int)iolog_uid);
+ debug_return_bool(false);
+ }
+ }
+ debug_return_bool(true);
+#else
+ return false;
+#endif
+}
+
+/*
+ * Create directory and any parent directories as needed.
+ */
+static bool
+iolog_mkdirs(char *path)
+{
+ mode_t omask;
+ struct stat sb;
+ int dfd;
+ bool ok = true, uid_changed = false;
+ debug_decl(iolog_mkdirs, SUDO_DEBUG_UTIL);
+
+ dfd = open(path, O_RDONLY|O_NONBLOCK);
+ if (dfd == -1 && errno == EACCES) {
+ /* Try again as the I/O log owner (for NFS). */
+ if (io_swapids(false)) {
+ dfd = open(path, O_RDONLY|O_NONBLOCK);
+ if (!io_swapids(true)) {
+ ok = false;
+ goto done;
+ }
+ }
+ }
+ if (dfd != -1 && fstat(dfd, &sb) != -1) {
+ if (S_ISDIR(sb.st_mode)) {
+ if (sb.st_uid != iolog_uid || sb.st_gid != iolog_gid) {
+ if (fchown(dfd, iolog_uid, iolog_gid) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to chown %d:%d %s", __func__,
+ (int)iolog_uid, (int)iolog_gid, path);
+ }
+ }
+ if ((sb.st_mode & ALLPERMS) != iolog_dirmode) {
+ if (fchmod(dfd, iolog_dirmode) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to chmod 0%o %s", __func__,
+ (int)iolog_dirmode, path);
+ }
+ }
+ } else {
+ sudo_warnx(U_("%s exists but is not a directory (0%o)"),
+ path, (unsigned int) sb.st_mode);
+ ok = false;
+ }
+ goto done;
+ }
+
+ /* umask must not be more restrictive than the file modes. */
+ omask = umask(ACCESSPERMS & ~(iolog_filemode|iolog_dirmode));
+
+ ok = sudo_mkdir_parents(path, iolog_uid, iolog_gid, iolog_dirmode, true);
+ if (!ok && errno == EACCES) {
+ /* Try again as the I/O log owner (for NFS). */
+ uid_changed = io_swapids(false);
+ if (uid_changed)
+ ok = sudo_mkdir_parents(path, -1, -1, iolog_dirmode, false);
+ }
+ if (ok) {
+ /* Create final path component. */
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "mkdir %s, mode 0%o", path, (unsigned int) iolog_dirmode);
+ ok = mkdir(path, iolog_dirmode) == 0 || errno == EEXIST;
+ if (!ok) {
+ if (errno == EACCES && !uid_changed) {
+ /* Try again as the I/O log owner (for NFS). */
+ uid_changed = io_swapids(false);
+ if (uid_changed)
+ ok = mkdir(path, iolog_dirmode) == 0 || errno == EEXIST;
+ }
+ if (!ok)
+ sudo_warn(U_("unable to mkdir %s"), path);
+ } else {
+ if (chown(path, iolog_uid, iolog_gid) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to chown %d:%d %s", __func__,
+ (int)iolog_uid, (int)iolog_gid, path);
+ }
+ }
+ }
+ if (uid_changed) {
+ if (!io_swapids(true))
+ ok = false;
+ }
+
+ umask(omask);
+
+done:
+ if (dfd != -1)
+ close(dfd);
+ debug_return_bool(ok);
+}
+
+/*
+ * Create temporary directory and any parent directories as needed.
+ */
+bool
+iolog_mkdtemp(char *path)
+{
+ bool ok, uid_changed = false;
+ debug_decl(iolog_mkdtemp, SUDO_DEBUG_UTIL);
+
+ ok = sudo_mkdir_parents(path, iolog_uid, iolog_gid, iolog_dirmode, true);
+ if (!ok && errno == EACCES) {
+ /* Try again as the I/O log owner (for NFS). */
+ uid_changed = io_swapids(false);
+ if (uid_changed)
+ ok = sudo_mkdir_parents(path, -1, -1, iolog_dirmode, false);
+ }
+ if (ok) {
+ /* Create final path component. */
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "mkdtemp %s", path);
+ /* We cannot retry mkdtemp() so always open as iolog user */
+ if (!uid_changed)
+ uid_changed = io_swapids(false);
+ if (mkdtemp(path) == NULL) {
+ sudo_warn(U_("unable to mkdir %s"), path);
+ ok = false;
+ } else {
+ if (chmod(path, iolog_dirmode) != 0) {
+ sudo_warn(U_("unable to change mode of %s to 0%o"),
+ path, (unsigned int)iolog_dirmode);
+ }
+ }
+ }
+
+ if (uid_changed) {
+ if (!io_swapids(true))
+ ok = false;
+ }
+ debug_return_bool(ok);
+}
+
+/*
+ * Like rename(2) but changes UID as needed.
+ */
+bool
+iolog_rename(const char *from, const char *to)
+{
+ bool ok, uid_changed = false;
+ debug_decl(iolog_rename, SUDO_DEBUG_UTIL);
+
+ ok = rename(from, to) == 0;
+ if (!ok && errno == EACCES) {
+ uid_changed = io_swapids(false);
+ if (uid_changed)
+ ok = rename(from, to) == 0;
+ }
+
+ if (uid_changed) {
+ if (!io_swapids(true))
+ ok = false;
+ }
+ debug_return_bool(ok);
+}
+
+/*
+ * Reset I/O log settings to default values.
+ */
+void
+iolog_set_defaults(void)
+{
+ sessid_max = SESSID_MAX;
+ iolog_filemode = S_IRUSR|S_IWUSR;
+ iolog_dirmode = S_IRWXU;
+ iolog_uid = ROOT_UID;
+ iolog_gid = ROOT_GID;
+ iolog_gid_set = false;
+ iolog_compress = false;
+ iolog_flush = false;
+}
+
+/*
+ * Set max sequence number (aka session ID)
+ */
+void
+iolog_set_maxseq(unsigned int newval)
+{
+ debug_decl(iolog_set_maxseq, SUDO_DEBUG_UTIL);
+
+ /* Clamp to SESSID_MAX as documented. */
+ if (newval > SESSID_MAX)
+ newval = SESSID_MAX;
+ sessid_max = newval;
+
+ debug_return;
+}
+
+/*
+ * Set iolog_uid (and iolog_gid if gid not explicitly set).
+ */
+void
+iolog_set_owner(uid_t uid, gid_t gid)
+{
+ debug_decl(iolog_set_owner, SUDO_DEBUG_UTIL);
+
+ iolog_uid = uid;
+ if (!iolog_gid_set)
+ iolog_gid = gid;
+
+ debug_return;
+}
+
+/*
+ * Set iolog_gid.
+ */
+void
+iolog_set_gid(gid_t gid)
+{
+ debug_decl(iolog_set_gid, SUDO_DEBUG_UTIL);
+
+ iolog_gid = gid;
+ iolog_gid_set = true;
+
+ debug_return;
+}
+
+/*
+ * Set iolog_filemode and iolog_dirmode.
+ */
+void
+iolog_set_mode(mode_t mode)
+{
+ debug_decl(iolog_set_mode, SUDO_DEBUG_UTIL);
+
+ /* I/O log files must be readable and writable by owner. */
+ iolog_filemode = S_IRUSR|S_IWUSR;
+
+ /* Add in group and other read/write if specified. */
+ iolog_filemode |= mode & (S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
+
+ /* For directory mode, add execute bits as needed. */
+ iolog_dirmode = iolog_filemode | S_IXUSR;
+ if (iolog_dirmode & (S_IRGRP|S_IWGRP))
+ iolog_dirmode |= S_IXGRP;
+ if (iolog_dirmode & (S_IROTH|S_IWOTH))
+ iolog_dirmode |= S_IXOTH;
+
+ debug_return;
+}
+
+/*
+ * Set iolog_compress
+ */
+void
+iolog_set_compress(bool newval)
+{
+ debug_decl(iolog_set_compress, SUDO_DEBUG_UTIL);
+ iolog_compress = newval;
+ debug_return;
+}
+
+/*
+ * Set iolog_flush
+ */
+void
+iolog_set_flush(bool newval)
+{
+ debug_decl(iolog_set_flush, SUDO_DEBUG_UTIL);
+ iolog_flush = newval;
+ debug_return;
+}
+
+/*
+ * Wrapper for openat(2) that sets umask and retries as iolog_uid/iolog_gid
+ * if openat(2) returns EACCES.
+ */
+int
+iolog_openat(int dfd, const char *path, int flags)
+{
+ int fd;
+ mode_t omask = S_IRWXG|S_IRWXO;
+ debug_decl(iolog_openat, SUDO_DEBUG_UTIL);
+
+ if (ISSET(flags, O_CREAT)) {
+ /* umask must not be more restrictive than the file modes. */
+ omask = umask(ACCESSPERMS & ~(iolog_filemode|iolog_dirmode));
+ }
+ fd = openat(dfd, path, flags, iolog_filemode);
+ if (fd == -1 && errno == EACCES) {
+ /* Enable write bit if it is missing. */
+ struct stat sb;
+ if (fstatat(dfd, path, &sb, 0) == 0) {
+ mode_t write_bits = iolog_filemode & (S_IWUSR|S_IWGRP|S_IWOTH);
+ if ((sb.st_mode & write_bits) != write_bits) {
+ if (fchmodat(dfd, path, iolog_filemode, 0) == 0)
+ fd = openat(dfd, path, flags, iolog_filemode);
+ }
+ }
+ }
+ if (fd == -1 && errno == EACCES) {
+ /* Try again as the I/O log owner (for NFS). */
+ if (io_swapids(false)) {
+ fd = openat(dfd, path, flags, iolog_filemode);
+ if (!io_swapids(true)) {
+ /* io_swapids() warns on error. */
+ if (fd != -1) {
+ close(fd);
+ fd = -1;
+ }
+ }
+ }
+ }
+ if (ISSET(flags, O_CREAT))
+ umask(omask);
+ debug_return_int(fd);
+}
+
+/*
+ * Read the on-disk sequence number, set sessid to the next
+ * number, and update the on-disk copy.
+ * Uses file locking to avoid sequence number collisions.
+ */
+bool
+iolog_nextid(char *iolog_dir, char sessid[7])
+{
+ char buf[32], *ep;
+ int i, len, fd = -1;
+ unsigned long id = 0;
+ ssize_t nread;
+ bool ret = false;
+ char pathbuf[PATH_MAX];
+ static const char b36char[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ debug_decl(iolog_nextid, SUDO_DEBUG_UTIL);
+
+ /*
+ * Create I/O log directory if it doesn't already exist.
+ */
+ if (!iolog_mkdirs(iolog_dir))
+ goto done;
+
+ /*
+ * Open sequence file
+ */
+ len = snprintf(pathbuf, sizeof(pathbuf), "%s/seq", iolog_dir);
+ if (len < 0 || len >= ssizeof(pathbuf)) {
+ errno = ENAMETOOLONG;
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: %s/seq", __func__, iolog_dir);
+ goto done;
+ }
+ fd = iolog_openat(AT_FDCWD, pathbuf, O_RDWR|O_CREAT);
+ if (fd == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to open %s", __func__, pathbuf);
+ goto done;
+ }
+ if (!sudo_lock_file(fd, SUDO_LOCK)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to lock %s", pathbuf);
+ goto done;
+ }
+ if (fchown(fd, iolog_uid, iolog_gid) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to fchown %d:%d %s", __func__,
+ (int)iolog_uid, (int)iolog_gid, pathbuf);
+ }
+
+ /* Read current seq number (base 36). */
+ nread = read(fd, buf, sizeof(buf) - 1);
+ if (nread != 0) {
+ if (nread == -1) {
+ goto done;
+ }
+ if (buf[nread - 1] == '\n')
+ nread--;
+ buf[nread] = '\0';
+ id = strtoul(buf, &ep, 36);
+ if (ep == buf || *ep != '\0' || id >= sessid_max) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s: bad sequence number: %s", pathbuf, buf);
+ id = 0;
+ }
+ }
+ id++;
+
+ /*
+ * Convert id to a string and stash in sessid.
+ * Note that that least significant digits go at the end of the string.
+ */
+ for (i = 5; i >= 0; i--) {
+ buf[i] = b36char[id % 36];
+ id /= 36;
+ }
+ buf[6] = '\n';
+
+ /* Stash id for logging purposes. */
+ memcpy(sessid, buf, 6);
+ sessid[6] = '\0';
+
+ /* Rewind and overwrite old seq file, including the NUL byte. */
+#ifdef HAVE_PWRITE
+ if (pwrite(fd, buf, 7, 0) != 7) {
+#else
+ if (lseek(fd, 0, SEEK_SET) == -1 || write(fd, buf, 7) != 7) {
+#endif
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to write %s", __func__, pathbuf);
+ goto done;
+ }
+ ret = true;
+
+done:
+ if (fd != -1)
+ close(fd);
+ debug_return_bool(ret);
+}
+
+/*
+ * Create path and any intermediate directories.
+ * If path ends in 'XXXXXX', use mkdtemp().
+ */
+bool
+iolog_mkpath(char *path)
+{
+ size_t len;
+ bool ret;
+ debug_decl(iolog_mkpath, SUDO_DEBUG_UTIL);
+
+ /*
+ * Create path and intermediate subdirs as needed.
+ * If path ends in at least 6 Xs (ala POSIX mktemp), use mkdtemp().
+ * Sets iolog_gid (if it is not already set) as a side effect.
+ */
+ len = strlen(path);
+ if (len >= 6 && strcmp(&path[len - 6], "XXXXXX") == 0)
+ ret = iolog_mkdtemp(path);
+ else
+ ret = iolog_mkdirs(path);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "iolog path %s", path);
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Append suffix to pathbuf after len chars and open the resulting file.
+ * Note that the size of pathbuf is assumed to be PATH_MAX.
+ * Stores the open file handle which has the close-on-exec flag set.
+ * XXX - move enabled logic into caller?
+ */
+bool
+iolog_open(struct iolog_file *iol, int dfd, int iofd, const char *mode)
+{
+ int flags;
+ const char *file;
+ unsigned char magic[2];
+ debug_decl(iolog_open, SUDO_DEBUG_UTIL);
+
+ if (mode[0] == 'r') {
+ flags = mode[1] == '+' ? O_RDWR : O_RDONLY;
+ } else if (mode[0] == 'w') {
+ flags = O_CREAT|O_TRUNC;
+ flags |= mode[1] == '+' ? O_RDWR : O_WRONLY;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR,
+ "%s: invalid I/O mode %s", __func__, mode);
+ debug_return_bool(false);
+ }
+ if ((file = iolog_fd_to_name(iofd)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR,
+ "%s: invalid iofd %d", __func__, iofd);
+ debug_return_bool(false);
+ }
+
+ iol->writable = false;
+ iol->compressed = false;
+ if (iol->enabled) {
+ int fd = iolog_openat(dfd, file, flags);
+ if (fd != -1) {
+ if (*mode == 'w') {
+ if (fchown(fd, iolog_uid, iolog_gid) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to fchown %d:%d %s", __func__,
+ (int)iolog_uid, (int)iolog_gid, file);
+ }
+ iol->compressed = iolog_compress;
+ } else {
+ /* check for gzip magic number */
+ if (pread(fd, magic, sizeof(magic), 0) == ssizeof(magic)) {
+ if (magic[0] == gzip_magic[0] && magic[1] == gzip_magic[1])
+ iol->compressed = true;
+ }
+ }
+ if (fcntl(fd, F_SETFD, FD_CLOEXEC) != -1) {
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed)
+ iol->fd.g = gzdopen(fd, mode);
+ else
+#endif
+ iol->fd.f = fdopen(fd, mode);
+ }
+ if (iol->fd.v != NULL) {
+ switch ((flags & O_ACCMODE)) {
+ case O_WRONLY:
+ case O_RDWR:
+ iol->writable = true;
+ break;
+ }
+ } else {
+ int save_errno = errno;
+ close(fd);
+ errno = save_errno;
+ fd = -1;
+ }
+ }
+ if (fd == -1) {
+ iol->enabled = false;
+ debug_return_bool(false);
+ }
+ } else {
+ if (*mode == 'w') {
+ /* Remove old log file in case we recycled sequence numbers. */
+ (void)unlinkat(dfd, file, 0);
+ }
+ }
+ debug_return_bool(true);
+}
+
+#ifdef HAVE_ZLIB_H
+static const char *
+gzstrerror(gzFile file)
+{
+ const char *errstr;
+ int errnum;
+
+ errstr = gzerror(file, &errnum);
+ if (errnum == Z_ERRNO)
+ errstr = strerror(errno);
+
+ return errstr;
+}
+#endif /* HAVE_ZLIB_H */
+
+/*
+ * Close an I/O log.
+ */
+bool
+iolog_close(struct iolog_file *iol, const char **errstr)
+{
+ bool ret = true;
+ debug_decl(iolog_close, SUDO_DEBUG_UTIL);
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed) {
+ int errnum;
+
+ /* Must check error indicator before closing. */
+ if (iol->writable) {
+ if (gzflush(iol->fd.g, Z_SYNC_FLUSH) != Z_OK) {
+ ret = false;
+ if (errstr != NULL)
+ *errstr = gzstrerror(iol->fd.g);
+ }
+ }
+ errnum = gzclose(iol->fd.g);
+ if (ret && errnum != Z_OK) {
+ ret = false;
+ if (errstr != NULL)
+ *errstr = errnum == Z_ERRNO ? strerror(errno) : "unknown error";
+ }
+ } else
+#endif
+ if (fclose(iol->fd.f) != 0) {
+ ret = false;
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ }
+
+ debug_return_bool(ret);
+}
+
+/*
+ * I/O log wrapper for fseek/gzseek.
+ */
+off_t
+iolog_seek(struct iolog_file *iol, off_t offset, int whence)
+{
+ off_t ret;
+ //debug_decl(iolog_seek, SUDO_DEBUG_UTIL);
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed)
+ ret = gzseek(iol->fd.g, offset, whence);
+ else
+#endif
+ ret = fseeko(iol->fd.f, offset, whence);
+
+ //debug_return_off_t(ret);
+ return ret;
+}
+
+/*
+ * I/O log wrapper for rewind/gzrewind.
+ */
+void
+iolog_rewind(struct iolog_file *iol)
+{
+ debug_decl(iolog_rewind, SUDO_DEBUG_UTIL);
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed)
+ (void)gzrewind(iol->fd.g);
+ else
+#endif
+ rewind(iol->fd.f);
+
+ debug_return;
+}
+
+/*
+ * Read from a (possibly compressed) I/O log file.
+ */
+ssize_t
+iolog_read(struct iolog_file *iol, void *buf, size_t nbytes,
+ const char **errstr)
+{
+ ssize_t nread;
+ debug_decl(iolog_read, SUDO_DEBUG_UTIL);
+
+ if (nbytes > UINT_MAX) {
+ errno = EINVAL;
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ debug_return_ssize_t(-1);
+ }
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed) {
+ if ((nread = gzread(iol->fd.g, buf, nbytes)) == -1) {
+ if (errstr != NULL)
+ *errstr = gzstrerror(iol->fd.g);
+ }
+ } else
+#endif
+ {
+ nread = (ssize_t)fread(buf, 1, nbytes, iol->fd.f);
+ if (nread == 0 && ferror(iol->fd.f)) {
+ nread = -1;
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ }
+ }
+ debug_return_ssize_t(nread);
+}
+
+/*
+ * Write to an I/O log, optionally compressing.
+ */
+ssize_t
+iolog_write(struct iolog_file *iol, const void *buf, size_t len,
+ const char **errstr)
+{
+ ssize_t ret;
+ debug_decl(iolog_write, SUDO_DEBUG_UTIL);
+
+ if (len > UINT_MAX) {
+ errno = EINVAL;
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ debug_return_ssize_t(-1);
+ }
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed) {
+ ret = gzwrite(iol->fd.g, (const voidp)buf, len);
+ if (ret == 0) {
+ ret = -1;
+ if (errstr != NULL)
+ *errstr = gzstrerror(iol->fd.g);
+ goto done;
+ }
+ if (iolog_flush) {
+ if (gzflush(iol->fd.g, Z_SYNC_FLUSH) != Z_OK) {
+ ret = -1;
+ if (errstr != NULL)
+ *errstr = gzstrerror(iol->fd.g);
+ goto done;
+ }
+ }
+ } else
+#endif
+ {
+ ret = fwrite(buf, 1, len, iol->fd.f);
+ if (ret == 0) {
+ ret = -1;
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ goto done;
+ }
+ if (iolog_flush) {
+ if (fflush(iol->fd.f) != 0) {
+ ret = -1;
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ goto done;
+ }
+ }
+ }
+
+done:
+ debug_return_ssize_t(ret);
+}
+
+/*
+ * Returns true if at end of I/O log file, else false.
+ */
+bool
+iolog_eof(struct iolog_file *iol)
+{
+ bool ret;
+ debug_decl(iolog_eof, SUDO_DEBUG_UTIL);
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed)
+ ret = gzeof(iol->fd.g) == 1;
+ else
+#endif
+ ret = feof(iol->fd.f) == 1;
+ debug_return_int(ret);
+}
+
+void
+iolog_clearerr(struct iolog_file *iol)
+{
+ debug_decl(iolog_eof, SUDO_DEBUG_UTIL);
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed)
+ gzclearerr(iol->fd.g);
+ else
+#endif
+ clearerr(iol->fd.f);
+ debug_return;
+}
+
+/*
+ * Like gets() but for struct iolog_file.
+ */
+char *
+iolog_gets(struct iolog_file *iol, char *buf, size_t nbytes,
+ const char **errstr)
+{
+ char *str;
+ debug_decl(iolog_gets, SUDO_DEBUG_UTIL);
+
+ if (nbytes > UINT_MAX) {
+ errno = EINVAL;
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ debug_return_str(NULL);
+ }
+
+#ifdef HAVE_ZLIB_H
+ if (iol->compressed) {
+ if ((str = gzgets(iol->fd.g, buf, nbytes)) == NULL) {
+ if (errstr != NULL)
+ *errstr = gzstrerror(iol->fd.g);
+ }
+ } else
+#endif
+ {
+ if ((str = fgets(buf, nbytes, iol->fd.f)) == NULL) {
+ if (errstr != NULL)
+ *errstr = strerror(errno);
+ }
+ }
+ debug_return_str(str);
+}
+
+/*
+ * Write the legacy I/O log file that contains the user and command info.
+ * This file is not compressed.
+ */
+static bool
+iolog_write_info_file_legacy(int dfd, struct eventlog *evlog)
+{
+ char * const *av;
+ FILE *fp;
+ int error, fd;
+ debug_decl(iolog_info_write_log, SUDO_DEBUG_UTIL);
+
+ fd = iolog_openat(dfd, "log", O_CREAT|O_TRUNC|O_WRONLY);
+ if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to open %s/log", evlog->iolog_path);
+ if (fd != -1)
+ close(fd);
+ debug_return_bool(false);
+ }
+ if (fchown(fd, iolog_uid, iolog_gid) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to fchown %d:%d %s/log", __func__,
+ (int)iolog_uid, (int)iolog_gid, evlog->iolog_path);
+ }
+
+ fprintf(fp, "%lld:%s:%s:%s:%s:%d:%d\n%s\n",
+ (long long)evlog->submit_time.tv_sec,
+ evlog->submituser ? evlog->submituser : "unknown",
+ evlog->runuser ? evlog->runuser : RUNAS_DEFAULT,
+ evlog->rungroup ? evlog->rungroup : "",
+ evlog->ttyname ? evlog->ttyname : "unknown",
+ evlog->lines, evlog->columns,
+ evlog->cwd ? evlog->cwd : "unknown");
+ fputs(evlog->command ? evlog->command : "unknown", fp);
+ for (av = evlog->argv + 1; *av != NULL; av++) {
+ fputc(' ', fp);
+ fputs(*av, fp);
+ }
+ fputc('\n', fp);
+ fflush(fp);
+ if ((error = ferror(fp))) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to write to I/O log file %s/log", evlog->iolog_path);
+ }
+ fclose(fp);
+
+ debug_return_bool(!error);
+}
+
+/*
+ * Write the "log.json" file that contains the user and command info.
+ * This file is not compressed.
+ */
+static bool
+iolog_write_info_file_json(int dfd, struct eventlog *evlog)
+{
+ struct json_container json;
+ struct json_value json_value;
+ bool ret = false;
+ FILE *fp = NULL;
+ int fd = -1;
+ debug_decl(iolog_write_info_file_json, SUDO_DEBUG_UTIL);
+
+ if (!sudo_json_init(&json, 4, false, false))
+ debug_return_bool(false);
+
+ /* Timestamp */
+ if (!sudo_json_open_object(&json, "timestamp"))
+ goto oom;
+
+ json_value.type = JSON_NUMBER;
+ json_value.u.number = evlog->submit_time.tv_sec;
+ if (!sudo_json_add_value(&json, "seconds", &json_value))
+ goto oom;
+
+ json_value.type = JSON_NUMBER;
+ json_value.u.number = evlog->submit_time.tv_nsec;
+ if (!sudo_json_add_value(&json, "nanoseconds", &json_value))
+ goto oom;
+
+ if (!sudo_json_close_object(&json))
+ goto oom;
+
+ if (!eventlog_store_json(&json, evlog))
+ goto done;
+
+ fd = iolog_openat(dfd, "log.json", O_CREAT|O_TRUNC|O_WRONLY);
+ if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to open %s/log.json", evlog->iolog_path);
+ goto done;
+ }
+
+ if (fchown(fd, iolog_uid, iolog_gid) != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to fchown %d:%d %s/log", __func__,
+ (int)iolog_uid, (int)iolog_gid, evlog->iolog_path);
+ }
+ fd = -1;
+
+ fprintf(fp, "{%s\n}\n", sudo_json_get_buf(&json));
+ fflush(fp);
+ if (ferror(fp)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to write to I/O log file %s/log.json", evlog->iolog_path);
+ goto done;
+ }
+
+ ret = true;
+ goto done;
+
+oom:
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+done:
+ sudo_json_free(&json);
+ if (fp != NULL)
+ fclose(fp);
+ if (fd != -1)
+ close(fd);
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Write the I/O log and log.json files that contain user and command info.
+ * These files are not compressed.
+ */
+bool
+iolog_write_info_file(int dfd, struct eventlog *evlog)
+{
+ debug_decl(iolog_write_info_file, SUDO_DEBUG_UTIL);
+
+ if (!iolog_write_info_file_legacy(dfd, evlog))
+ debug_return_bool(false);
+ if (!iolog_write_info_file_json(dfd, evlog))
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+/*
+ * Map IOFD_* -> name.
+ */
+const char *
+iolog_fd_to_name(int iofd)
+{
+ const char *ret;
+ debug_decl(iolog_fd_to_name, SUDO_DEBUG_UTIL);
+
+ switch (iofd) {
+ case IOFD_STDIN:
+ ret = "stdin";
+ break;
+ case IOFD_STDOUT:
+ ret = "stdout";
+ break;
+ case IOFD_STDERR:
+ ret = "stderr";
+ break;
+ case IOFD_TTYIN:
+ ret = "ttyin";
+ break;
+ case IOFD_TTYOUT:
+ ret = "ttyout";
+ break;
+ case IOFD_TIMING:
+ ret = "timing";
+ break;
+ default:
+ ret = "unknown";
+ sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: unexpected iofd %d",
+ __func__, iofd);
+ break;
+ }
+ debug_return_const_str(ret);
+}
diff --git a/lib/iolog/iolog_json.c b/lib/iolog/iolog_json.c
new file mode 100644
index 0000000..684f938
--- /dev/null
+++ b/lib/iolog/iolog_json.c
@@ -0,0 +1,789 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include <config.h>
+
+#include <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 <ctype.h>
+#include <limits.h>
+#include <fcntl.h>
+#include <time.h>
+
+#include "sudo_compat.h"
+#include "sudo_debug.h"
+#include "sudo_eventlog.h"
+#include "sudo_fatal.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_util.h"
+
+#include "iolog_json.h"
+
+struct json_stack {
+ unsigned int depth;
+ unsigned int maxdepth;
+ struct json_object *frames[64];
+};
+#define JSON_STACK_INTIALIZER(s) { 0, nitems((s).frames) };
+
+static bool
+json_store_columns(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_columns, SUDO_DEBUG_UTIL);
+
+ if (item->u.number < 1 || item->u.number > INT_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "tty cols %lld: out of range", item->u.number);
+ evlog->columns = 0;
+ debug_return_bool(false);
+ }
+
+ evlog->columns = item->u.number;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_command(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_command, SUDO_DEBUG_UTIL);
+
+ /*
+ * Note: struct eventlog must store command + args.
+ * We don't have argv yet so we append the args later.
+ */
+ evlog->command = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_lines(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_lines, SUDO_DEBUG_UTIL);
+
+ if (item->u.number < 1 || item->u.number > INT_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "tty lines %lld: out of range", item->u.number);
+ evlog->lines = 0;
+ debug_return_bool(false);
+ }
+
+ evlog->lines = item->u.number;
+ debug_return_bool(true);
+}
+
+char **
+json_array_to_strvec(struct json_object *array)
+{
+ struct json_item *item;
+ int len = 0;
+ char **ret;
+ debug_decl(json_array_to_strvec, SUDO_DEBUG_UTIL);
+
+ TAILQ_FOREACH(item, &array->items, entries) {
+ /* Can only convert arrays of string. */
+ if (item->type != JSON_STRING) {
+ sudo_warnx(U_("expected JSON_STRING, got %d"), item->type);
+ debug_return_ptr(NULL);
+ }
+ len++;
+ }
+ if ((ret = reallocarray(NULL, len + 1, sizeof(char *))) == NULL) {
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ debug_return_ptr(NULL);
+ }
+ len = 0;
+ TAILQ_FOREACH(item, &array->items, entries) {
+ ret[len++] = item->u.string;
+ item->u.string = NULL;
+ }
+ ret[len] = NULL;
+
+ debug_return_ptr(ret);
+}
+
+static bool
+json_store_runargv(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_runargv, SUDO_DEBUG_UTIL);
+
+ evlog->argv = json_array_to_strvec(&item->u.child);
+
+ debug_return_bool(evlog->argv != NULL);
+}
+
+static bool
+json_store_runenv(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_runenv, SUDO_DEBUG_UTIL);
+
+ evlog->envp = json_array_to_strvec(&item->u.child);
+
+ debug_return_bool(evlog->envp != NULL);
+}
+
+static bool
+json_store_rungid(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_rungid, SUDO_DEBUG_UTIL);
+
+ evlog->rungid = (gid_t)item->u.number;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_rungroup(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_rungroup, SUDO_DEBUG_UTIL);
+
+ evlog->rungroup = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_runuid(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_runuid, SUDO_DEBUG_UTIL);
+
+ evlog->runuid = (uid_t)item->u.number;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_runuser(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_runuser, SUDO_DEBUG_UTIL);
+
+ evlog->runuser = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_runchroot(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_runchroot, SUDO_DEBUG_UTIL);
+
+ evlog->runchroot = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_runcwd(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_runcwd, SUDO_DEBUG_UTIL);
+
+ evlog->runcwd = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_submitcwd(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_submitcwd, SUDO_DEBUG_UTIL);
+
+ evlog->cwd = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_submithost(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_submithost, SUDO_DEBUG_UTIL);
+
+ evlog->submithost = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_submituser(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_submituser, SUDO_DEBUG_UTIL);
+
+ evlog->submituser = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static bool
+json_store_timestamp(struct json_item *item, struct eventlog *evlog)
+{
+ struct json_object *object;
+ debug_decl(json_store_timestamp, SUDO_DEBUG_UTIL);
+
+ object = &item->u.child;
+ TAILQ_FOREACH(item, &object->items, entries) {
+ if (item->type != JSON_NUMBER)
+ continue;
+ if (strcmp(item->name, "seconds") == 0) {
+ evlog->submit_time.tv_sec = item->u.number;
+ continue;
+ }
+ if (strcmp(item->name, "nanoseconds") == 0) {
+ evlog->submit_time.tv_nsec = item->u.number;
+ continue;
+ }
+ }
+ debug_return_bool(true);
+}
+
+static bool
+json_store_ttyname(struct json_item *item, struct eventlog *evlog)
+{
+ debug_decl(json_store_ttyname, SUDO_DEBUG_UTIL);
+
+ evlog->ttyname = item->u.string;
+ item->u.string = NULL;
+ debug_return_bool(true);
+}
+
+static struct iolog_json_key {
+ const char *name;
+ enum json_value_type type;
+ bool (*setter)(struct json_item *, struct eventlog *);
+} iolog_json_keys[] = {
+ { "columns", JSON_NUMBER, json_store_columns },
+ { "command", JSON_STRING, json_store_command },
+ { "lines", JSON_NUMBER, json_store_lines },
+ { "runargv", JSON_ARRAY, json_store_runargv },
+ { "runenv", JSON_ARRAY, json_store_runenv },
+ { "rungid", JSON_ID, json_store_rungid },
+ { "rungroup", JSON_STRING, json_store_rungroup },
+ { "runuid", JSON_ID, json_store_runuid },
+ { "runuser", JSON_STRING, json_store_runuser },
+ { "runchroot", JSON_STRING, json_store_runchroot },
+ { "runcwd", JSON_STRING, json_store_runcwd },
+ { "submitcwd", JSON_STRING, json_store_submitcwd },
+ { "submithost", JSON_STRING, json_store_submithost },
+ { "submituser", JSON_STRING, json_store_submituser },
+ { "timestamp", JSON_OBJECT, json_store_timestamp },
+ { "ttyname", JSON_STRING, json_store_ttyname },
+ { NULL }
+};
+
+static struct json_item *
+new_json_item(enum json_value_type type, char *name, unsigned int lineno)
+{
+ struct json_item *item;
+ debug_decl(new_json_item, SUDO_DEBUG_UTIL);
+
+ if ((item = malloc(sizeof(*item))) == NULL) {
+ sudo_warnx(U_("%s: %s"), __func__,
+ U_("unable to allocate memory"));
+ debug_return_ptr(NULL);
+ }
+ item->name = name;
+ item->type = type;
+ item->lineno = lineno;
+
+ debug_return_ptr(item);
+}
+
+static char *
+json_parse_string(char **strp)
+{
+ char *dst, *end, *ret, *src = *strp + 1;
+ size_t len;
+ debug_decl(json_parse_string, SUDO_DEBUG_UTIL);
+
+ for (end = src; *end != '"' && *end != '\0'; end++) {
+ if (end[0] == '\\' && end[1] == '"')
+ end++;
+ }
+ if (*end != '"') {
+ sudo_warnx("%s", U_("missing double quote in name"));
+ debug_return_str(NULL);
+ }
+ len = (size_t)(end - src);
+
+ /* Copy string, flattening escaped chars. */
+ dst = ret = malloc(len + 1);
+ if (dst == NULL)
+ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ while (src < end) {
+ char ch = *src++;
+ /* TODO: handle unicode escapes */
+ if (ch == '\\') {
+ switch (*src) {
+ case 'b':
+ ch = '\b';
+ break;
+ case 'f':
+ ch = '\f';
+ break;
+ case 'n':
+ ch = '\n';
+ break;
+ case 'r':
+ ch = '\r';
+ break;
+ case 't':
+ ch = '\t';
+ break;
+ case '"':
+ case '\\':
+ default:
+ /* Note: a bare \ at the end of a string will be removed. */
+ ch = *src;
+ break;
+ }
+ src++;
+ }
+ *dst++ = ch;
+ }
+ *dst = '\0';
+
+ /* Trim trailing whitespace. */
+ do {
+ end++;
+ } while (isspace((unsigned char)*end));
+ *strp = end;
+
+ debug_return_str(ret);
+}
+
+void
+free_json_items(struct json_item_list *items)
+{
+ struct json_item *item;
+ debug_decl(free_json_items, SUDO_DEBUG_UTIL);
+
+ while ((item = TAILQ_FIRST(items)) != NULL) {
+ TAILQ_REMOVE(items, item, entries);
+ switch (item->type) {
+ case JSON_STRING:
+ free(item->u.string);
+ break;
+ case JSON_ARRAY:
+ case JSON_OBJECT:
+ free_json_items(&item->u.child.items);
+ break;
+ default:
+ break;
+ }
+ free(item->name);
+ free(item);
+ }
+
+ debug_return;
+}
+
+static bool
+iolog_parse_json_object(struct json_object *object, struct eventlog *evlog)
+{
+ struct json_item *item;
+ bool ret = false;
+ debug_decl(iolog_parse_json_object, SUDO_DEBUG_UTIL);
+
+ /* First object holds all the actual data. */
+ item = TAILQ_FIRST(&object->items);
+ if (item->type != JSON_OBJECT) {
+ sudo_warnx(U_("expected JSON_OBJECT, got %d"), item->type);
+ goto done;
+ }
+ object = &item->u.child;
+
+ TAILQ_FOREACH(item, &object->items, entries) {
+ struct iolog_json_key *key;
+
+ /* lookup name */
+ for (key = iolog_json_keys; key->name != NULL; key++) {
+ if (strcmp(item->name, key->name) == 0)
+ break;
+ }
+ if (key->name == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "%s: unknown key %s", __func__, item->name);
+ } else if (key->type != item->type &&
+ (key->type != JSON_ID || item->type != JSON_NUMBER)) {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "%s: key mismatch %s type %d, expected %d", __func__,
+ item->name, item->type, key->type);
+ goto done;
+ } else {
+ /* Matched name and type. */
+ if (!key->setter(item, evlog)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to store %s", key->name);
+ goto done;
+ }
+ }
+ }
+
+ /* Merge cmd and argv as sudoreplay expects. */
+ if (evlog->command != NULL && evlog->argv != NULL) {
+ size_t len = strlen(evlog->command) + 1;
+ char *newcmd;
+ int ac;
+
+ /* Skip argv[0], we use evlog->command instead. */
+ for (ac = 1; evlog->argv[ac] != NULL; ac++)
+ len += strlen(evlog->argv[ac]) + 1;
+
+ if ((newcmd = malloc(len)) == NULL) {
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ goto done;
+ }
+
+ /* TODO: optimize this. */
+ if (strlcpy(newcmd, evlog->command, len) >= len)
+ sudo_fatalx(U_("internal error, %s overflow"), __func__);
+ for (ac = 1; evlog->argv[ac] != NULL; ac++) {
+ if (strlcat(newcmd, " ", len) >= len)
+ sudo_fatalx(U_("internal error, %s overflow"), __func__);
+ if (strlcat(newcmd, evlog->argv[ac], len) >= len)
+ sudo_fatalx(U_("internal error, %s overflow"), __func__);
+ }
+
+ free(evlog->command);
+ evlog->command = newcmd;
+ }
+
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
+
+static bool
+json_insert_bool(struct json_item_list *items, char *name, bool value,
+ unsigned int lineno)
+{
+ struct json_item *item;
+ debug_decl(json_insert_bool, SUDO_DEBUG_UTIL);
+
+ if ((item = new_json_item(JSON_BOOL, name, lineno)) == NULL)
+ debug_return_bool(false);
+ item->u.boolean = value;
+ TAILQ_INSERT_TAIL(items, item, entries);
+
+ debug_return_bool(true);
+}
+
+static bool
+json_insert_null(struct json_item_list *items, char *name, unsigned int lineno)
+{
+ struct json_item *item;
+ debug_decl(json_insert_null, SUDO_DEBUG_UTIL);
+
+ if ((item = new_json_item(JSON_NULL, name, lineno)) == NULL)
+ debug_return_bool(false);
+ TAILQ_INSERT_TAIL(items, item, entries);
+
+ debug_return_bool(true);
+}
+
+static bool
+json_insert_num(struct json_item_list *items, char *name, long long value,
+ unsigned int lineno)
+{
+ struct json_item *item;
+ debug_decl(json_insert_num, SUDO_DEBUG_UTIL);
+
+ if ((item = new_json_item(JSON_NUMBER, name, lineno)) == NULL)
+ debug_return_bool(false);
+ item->u.number = value;
+ TAILQ_INSERT_TAIL(items, item, entries);
+
+ debug_return_bool(true);
+}
+
+static bool
+json_insert_str(struct json_item_list *items, char *name, char **strp,
+ unsigned int lineno)
+{
+ struct json_item *item;
+ debug_decl(json_insert_str, SUDO_DEBUG_UTIL);
+
+ if ((item = new_json_item(JSON_STRING, name, lineno)) == NULL)
+ debug_return_bool(false);
+ item->u.string = json_parse_string(strp);
+ if (item->u.string == NULL) {
+ free(item);
+ debug_return_bool(false);
+ }
+ TAILQ_INSERT_TAIL(items, item, entries);
+
+ debug_return_bool(true);
+}
+
+static struct json_object *
+json_stack_push(struct json_stack *stack, struct json_item_list *items,
+ struct json_object *frame, enum json_value_type type, char *name,
+ unsigned int lineno)
+{
+ struct json_item *item;
+ debug_decl(iolog_parse_loginfo_json, SUDO_DEBUG_UTIL);
+
+ /* Allocate a new item and insert it into the list. */
+ if ((item = new_json_item(type, name, lineno)) == NULL)
+ debug_return_ptr(NULL);
+ TAILQ_INIT(&item->u.child.items);
+ item->u.child.parent = item;
+ TAILQ_INSERT_TAIL(items, item, entries);
+
+ /* Push the current frame onto the stack. */
+ if (stack->depth == stack->maxdepth)
+ sudo_fatalx(U_("internal error, %s overflow"), __func__);
+ stack->frames[stack->depth++] = frame;
+
+ /* Return the new frame */
+ debug_return_ptr(&item->u.child);
+}
+
+/* Only expect a value if a name is defined or we are in an array. */
+#define expect_value (name != NULL || (frame->parent != NULL && frame->parent->type == JSON_ARRAY))
+
+bool
+iolog_parse_json(FILE *fp, const char *filename, struct json_object *root)
+{
+ struct json_object *frame = root;
+ struct json_stack stack = JSON_STACK_INTIALIZER(stack);
+ unsigned int lineno = 0;
+ char *name = NULL;
+ char *buf = NULL;
+ size_t bufsize = 0;
+ ssize_t len;
+ bool ret = false;
+ long long num;
+ char ch;
+ debug_decl(iolog_parse_json, SUDO_DEBUG_UTIL);
+
+ root->parent = NULL;
+ TAILQ_INIT(&root->items);
+
+ while ((len = getdelim(&buf, &bufsize, '\n', fp)) != -1) {
+ char *cp = buf;
+ char *ep = buf + len - 1;
+
+ lineno++;
+
+ /* Trim trailing whitespace. */
+ while (ep > cp && isspace((unsigned char)*ep))
+ ep--;
+ ep[1] = '\0';
+
+ for (;;) {
+ const char *errstr;
+
+ /* Trim leading whitespace, skip blank lines. */
+ while (isspace((unsigned char)*cp))
+ cp++;
+
+ /* Strip out commas. TODO: require commas between values. */
+ if (*cp == ',') {
+ cp++;
+ while (isspace((unsigned char)*cp))
+ cp++;
+ }
+
+ if (*cp == '\0')
+ break;
+
+ switch (*cp) {
+ case '{':
+ cp++;
+ frame = json_stack_push(&stack, &frame->items, frame,
+ JSON_OBJECT, name, lineno);
+ if (frame == NULL)
+ goto parse_error;
+ name = NULL;
+ break;
+ case '}':
+ cp++;
+ if (stack.depth == 0 || frame->parent == NULL ||
+ frame->parent->type != JSON_OBJECT) {
+ sudo_warnx("%s", U_("unmatched close brace"));
+ goto parse_error;
+ }
+ frame = stack.frames[--stack.depth];
+ break;
+ case '[':
+ cp++;
+ if (frame->parent == NULL) {
+ /* Must have an enclosing object. */
+ sudo_warnx("%s", U_("unexpected array"));
+ goto parse_error;
+ }
+ frame = json_stack_push(&stack, &frame->items, frame,
+ JSON_ARRAY, name, lineno);
+ if (frame == NULL)
+ goto parse_error;
+ name = NULL;
+ break;
+ case ']':
+ cp++;
+ if (stack.depth == 0 || frame->parent == NULL ||
+ frame->parent->type != JSON_ARRAY) {
+ sudo_warnx("%s", U_("unmatched close bracket"));
+ goto parse_error;
+ }
+ frame = stack.frames[--stack.depth];
+ break;
+ case '"':
+ if (frame->parent == NULL) {
+ /* Must have an enclosing object. */
+ sudo_warnx("%s", U_("unexpected string"));
+ goto parse_error;
+ }
+
+ if (!expect_value) {
+ /* Parse "name": */
+ if ((name = json_parse_string(&cp)) == NULL)
+ goto parse_error;
+ /* TODO: allow colon on next line? */
+ if (*cp++ != ':') {
+ sudo_warnx("%s", U_("missing colon after name"));
+ goto parse_error;
+ }
+ } else {
+ if (!json_insert_str(&frame->items, name, &cp, lineno))
+ goto parse_error;
+ name = NULL;
+ }
+ break;
+ case 't':
+ if (!expect_value) {
+ sudo_warnx("%s", U_("unexpected boolean"));
+ goto parse_error;
+ }
+ if (strncmp(cp, "true", sizeof("true") - 1) != 0)
+ goto parse_error;
+ cp += sizeof("true") - 1;
+ if (*cp != ',' && !isspace((unsigned char)*cp) && *cp != '\0')
+ goto parse_error;
+
+ if (!json_insert_bool(&frame->items, name, true, lineno))
+ goto parse_error;
+ name = NULL;
+ break;
+ case 'f':
+ if (!expect_value) {
+ sudo_warnx("%s", U_("unexpected boolean"));
+ goto parse_error;
+ }
+ if (strncmp(cp, "false", sizeof("false") - 1) != 0)
+ goto parse_error;
+ cp += sizeof("false") - 1;
+ if (*cp != ',' && !isspace((unsigned char)*cp) && *cp != '\0')
+ goto parse_error;
+
+ if (!json_insert_bool(&frame->items, name, false, lineno))
+ goto parse_error;
+ name = NULL;
+ break;
+ case 'n':
+ if (!expect_value) {
+ sudo_warnx("%s", U_("unexpected boolean"));
+ goto parse_error;
+ }
+ if (strncmp(cp, "null", sizeof("null") - 1) != 0)
+ goto parse_error;
+ cp += sizeof("null") - 1;
+ if (*cp != ',' && !isspace((unsigned char)*cp) && *cp != '\0')
+ goto parse_error;
+
+ if (!json_insert_null(&frame->items, name, lineno))
+ goto parse_error;
+ name = NULL;
+ break;
+ case '+': case '-': case '0': case '1': case '2': case '3':
+ case '4': case '5': case '6': case '7': case '8': case '9':
+ if (!expect_value) {
+ sudo_warnx("%s", U_("unexpected number"));
+ goto parse_error;
+ }
+ /* XXX - strtonumx() would be simpler here. */
+ len = strcspn(cp, " \f\n\r\t\v,");
+ ch = cp[len];
+ cp[len] = '\0';
+ num = sudo_strtonum(cp, LLONG_MIN, LLONG_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_warnx(U_("%s: %s"), cp, U_(errstr));
+ goto parse_error;
+ }
+ cp += len;
+ *cp = ch;
+
+ if (!json_insert_num(&frame->items, name, num, lineno))
+ goto parse_error;
+ name = NULL;
+ break;
+ default:
+ goto parse_error;
+ }
+ }
+ }
+ if (stack.depth != 0) {
+ frame = stack.frames[stack.depth - 1];
+ if (frame->parent == NULL || frame->parent->type == JSON_OBJECT)
+ sudo_warnx("%s", U_("unmatched close brace"));
+ else
+ sudo_warnx("%s", U_("unmatched close bracket"));
+ goto parse_error;
+ }
+
+ ret = true;
+ goto done;
+
+parse_error:
+ sudo_warnx(U_("%s:%u unable to parse \"%s\""), filename, lineno, buf);
+done:
+ free(buf);
+ free(name);
+ if (!ret)
+ free_json_items(&root->items);
+
+ debug_return_bool(ret);
+}
+
+bool
+iolog_parse_loginfo_json(FILE *fp, const char *iolog_dir, struct eventlog *evlog)
+{
+ struct json_object root;
+ bool ret = false;
+ debug_decl(iolog_parse_loginfo_json, SUDO_DEBUG_UTIL);
+
+ if (iolog_parse_json(fp, iolog_dir, &root)) {
+ /* Walk the stack and parse entries. */
+ ret = iolog_parse_json_object(&root, evlog);
+
+ /* Cleanup. */
+ free_json_items(&root.items);
+ }
+
+ debug_return_bool(ret);
+}
diff --git a/lib/iolog/iolog_json.h b/lib/iolog/iolog_json.h
new file mode 100644
index 0000000..bc28dfa
--- /dev/null
+++ b/lib/iolog/iolog_json.h
@@ -0,0 +1,50 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 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 IOLOG_JSON_H
+#define IOLOG_JSON_H
+
+#include "sudo_json.h"
+#include "sudo_queue.h"
+
+TAILQ_HEAD(json_item_list, json_item);
+
+struct json_object {
+ struct json_item *parent;
+ struct json_item_list items;
+};
+
+struct json_item {
+ TAILQ_ENTRY(json_item) entries;
+ char *name; /* may be NULL for first brace */
+ unsigned int lineno;
+ enum json_value_type type;
+ union {
+ struct json_object child;
+ char *string;
+ long long number;
+ id_t id;
+ bool boolean;
+ } u;
+};
+
+void free_json_items(struct json_item_list *items);
+bool iolog_parse_json(FILE *fp, const char *filename, struct json_object *root);
+char **json_array_to_strvec(struct json_object *array);
+
+#endif /* IOLOG_JSON_H */
diff --git a/lib/iolog/iolog_path.c b/lib/iolog/iolog_path.c
new file mode 100644
index 0000000..1f34c0b
--- /dev/null
+++ b/lib/iolog/iolog_path.c
@@ -0,0 +1,130 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2011-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 <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 <limits.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "sudo_compat.h"
+#include "sudo_debug.h"
+#include "sudo_fatal.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_util.h"
+
+/*
+ * Expand any escape sequences in inpath, returning the expanded path.
+ */
+bool
+expand_iolog_path(const char *inpath, char *path, size_t pathlen,
+ const struct iolog_path_escape *escapes, void *closure)
+{
+ char *dst, *pathend, tmpbuf[PATH_MAX];
+ const char *endbrace, *src;
+ bool strfit = false;
+ size_t len;
+ debug_decl(expand_iolog_path, SUDO_DEBUG_UTIL);
+
+ /* Collapse multiple leading slashes. */
+ while (inpath[0] == '/' && inpath[1] == '/')
+ inpath++;
+
+ pathend = path + pathlen;
+ for (src = inpath, dst = path; *src != '\0'; src++) {
+ if (src[0] == '%') {
+ if (src[1] == '{') {
+ endbrace = strchr(src + 2, '}');
+ if (endbrace != NULL) {
+ const struct iolog_path_escape *esc;
+ len = (size_t)(endbrace - src - 2);
+ for (esc = escapes; esc->name != NULL; esc++) {
+ if (strncmp(src + 2, esc->name, len) == 0 &&
+ esc->name[len] == '\0')
+ break;
+ }
+ if (esc->name != NULL) {
+ len = esc->copy_fn(dst, (size_t)(pathend - dst),
+ closure);
+ if (len >= (size_t)(pathend - dst))
+ goto bad;
+ dst += len;
+ src = endbrace;
+ continue;
+ }
+ }
+ } else if (src[1] == '%') {
+ /* Collapse %% -> % */
+ src++;
+ } else {
+ /* May need strftime() */
+ strfit = true;
+ }
+ }
+ /* Need at least 2 chars, including the NUL terminator. */
+ if (dst + 1 >= pathend)
+ goto bad;
+ *dst++ = *src;
+ }
+
+ /* Trim trailing slashes and NUL terminate. */
+ while (dst > path && dst[-1] == '/')
+ dst--;
+ *dst = '\0';
+
+ /* Expand strftime escapes as needed. */
+ if (strfit) {
+ time_t now;
+ struct tm *timeptr;
+
+ time(&now);
+ if ((timeptr = localtime(&now)) == NULL)
+ goto bad;
+
+ /* We only call strftime() on the current part of the buffer. */
+ tmpbuf[sizeof(tmpbuf) - 1] = '\0';
+ len = strftime(tmpbuf, sizeof(tmpbuf), path, timeptr);
+
+ if (len == 0 || tmpbuf[sizeof(tmpbuf) - 1] != '\0')
+ goto bad; /* strftime() failed, buf too small? */
+
+ if (len >= (size_t)(pathend - path))
+ goto bad; /* expanded buffer too big to fit. */
+ memcpy(path, tmpbuf, len);
+ dst = path + len;
+ *dst = '\0';
+ }
+
+ debug_return_bool(true);
+bad:
+ debug_return_bool(false);
+}
diff --git a/lib/iolog/iolog_util.c b/lib/iolog/iolog_util.c
new file mode 100644
index 0000000..7af33d9
--- /dev/null
+++ b/lib/iolog/iolog_util.c
@@ -0,0 +1,431 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2009-2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include <config.h>
+
+#include <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 <signal.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <fcntl.h>
+#include <time.h>
+
+#include "sudo_compat.h"
+#include "sudo_debug.h"
+#include "sudo_eventlog.h"
+#include "sudo_fatal.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_util.h"
+
+static int timing_event_adj;
+
+static bool
+iolog_parse_loginfo_legacy(FILE *fp, const char *iolog_dir,
+ struct eventlog *evlog)
+{
+ char *buf = NULL, *cp, *ep;
+ const char *errstr;
+ size_t bufsize = 0, cwdsize = 0, cmdsize = 0;
+ bool ret = false;
+ debug_decl(iolog_parse_loginfo_legacy, SUDO_DEBUG_UTIL);
+
+ /*
+ * Info file has three lines:
+ * 1) a log info line
+ * 2) cwd
+ * 3) command with args
+ */
+ if (getdelim(&buf, &bufsize, '\n', fp) == -1 ||
+ getdelim(&evlog->cwd, &cwdsize, '\n', fp) == -1 ||
+ getdelim(&evlog->command, &cmdsize, '\n', fp) == -1) {
+ sudo_warn(U_("%s: invalid log file"), iolog_dir);
+ goto done;
+ }
+
+ /* Strip the newline from the cwd and command. */
+ evlog->cwd[strcspn(evlog->cwd, "\n")] = '\0';
+ evlog->command[strcspn(evlog->command, "\n")] = '\0';
+
+ /*
+ * Crack the log line (lines and cols not present in old versions).
+ * timestamp:user:runas_user:runas_group:tty:lines:cols
+ * XXX - probably better to use strtok and switch on the state.
+ */
+ buf[strcspn(buf, "\n")] = '\0';
+ cp = buf;
+
+ /* timestamp */
+ if ((ep = strchr(cp, ':')) == NULL) {
+ sudo_warn(U_("%s: time stamp field is missing"), iolog_dir);
+ goto done;
+ }
+ *ep = '\0';
+ evlog->submit_time.tv_sec = sudo_strtonum(cp, 0, TIME_T_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_warn(U_("%s: time stamp %s: %s"), iolog_dir, cp, errstr);
+ goto done;
+ }
+
+ /* submit user */
+ cp = ep + 1;
+ if ((ep = strchr(cp, ':')) == NULL) {
+ sudo_warn(U_("%s: user field is missing"), iolog_dir);
+ goto done;
+ }
+ if ((evlog->submituser = strndup(cp, (size_t)(ep - cp))) == NULL)
+ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+
+ /* runas user */
+ cp = ep + 1;
+ if ((ep = strchr(cp, ':')) == NULL) {
+ sudo_warn(U_("%s: runas user field is missing"), iolog_dir);
+ goto done;
+ }
+ if ((evlog->runuser = strndup(cp, (size_t)(ep - cp))) == NULL)
+ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+
+ /* runas group */
+ cp = ep + 1;
+ if ((ep = strchr(cp, ':')) == NULL) {
+ sudo_warn(U_("%s: runas group field is missing"), iolog_dir);
+ goto done;
+ }
+ if (cp != ep) {
+ if ((evlog->rungroup = strndup(cp, (size_t)(ep - cp))) == NULL)
+ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ }
+
+ /* tty, followed by optional lines + cols */
+ cp = ep + 1;
+ if ((ep = strchr(cp, ':')) == NULL) {
+ /* just the tty */
+ if ((evlog->ttyname = strdup(cp)) == NULL)
+ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ } else {
+ /* tty followed by lines + cols */
+ if ((evlog->ttyname = strndup(cp, (size_t)(ep - cp))) == NULL)
+ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ cp = ep + 1;
+ /* need to NULL out separator to use sudo_strtonum() */
+ /* XXX - use sudo_strtonumx */
+ if ((ep = strchr(cp, ':')) != NULL) {
+ *ep = '\0';
+ }
+ evlog->lines = sudo_strtonum(cp, 1, INT_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s: tty lines %s: %s", iolog_dir, cp, errstr);
+ }
+ if (ep != NULL) {
+ cp = ep + 1;
+ evlog->columns = sudo_strtonum(cp, 1, INT_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s: tty cols %s: %s", iolog_dir, cp, errstr);
+ }
+ }
+ }
+
+ ret = true;
+
+done:
+ free(buf);
+ debug_return_bool(ret);
+}
+
+struct eventlog *
+iolog_parse_loginfo(int dfd, const char *iolog_dir)
+{
+ struct eventlog *evlog = NULL;
+ FILE *fp = NULL;
+ int fd = -1;
+ int tmpfd = -1;
+ bool ok, legacy = false;
+ debug_decl(iolog_parse_loginfo, SUDO_DEBUG_UTIL);
+
+ if (dfd == -1) {
+ if ((tmpfd = open(iolog_dir, O_RDONLY)) == -1) {
+ sudo_warn("%s", iolog_dir);
+ goto bad;
+ }
+ dfd = tmpfd;
+ }
+ if ((fd = openat(dfd, "log.json", O_RDONLY, 0)) == -1) {
+ fd = openat(dfd, "log", O_RDONLY, 0);
+ legacy = true;
+ }
+ if (tmpfd != -1)
+ close(tmpfd);
+ if (fd == -1 || (fp = fdopen(fd, "r")) == NULL) {
+ sudo_warn("%s/log", iolog_dir);
+ goto bad;
+ }
+ fd = -1;
+
+ if ((evlog = calloc(1, sizeof(*evlog))) == NULL)
+ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ evlog->runuid = (uid_t)-1;
+ evlog->rungid = (gid_t)-1;
+
+ ok = legacy ? iolog_parse_loginfo_legacy(fp, iolog_dir, evlog) :
+ iolog_parse_loginfo_json(fp, iolog_dir, evlog);
+ if (ok) {
+ fclose(fp);
+ debug_return_ptr(evlog);
+ }
+
+bad:
+ if (fd != -1)
+ close(fd);
+ if (fp != NULL)
+ fclose(fp);
+ eventlog_free(evlog);
+ debug_return_ptr(NULL);
+}
+
+void
+iolog_adjust_delay(struct timespec *delay, struct timespec *max_delay,
+ double scale_factor)
+{
+ double seconds;
+ debug_decl(iolog_adjust_delay, SUDO_DEBUG_UTIL);
+
+ if (scale_factor != 1.0) {
+ /* Order is important: we don't want to double the remainder. */
+ seconds = (double)delay->tv_sec / scale_factor;
+ delay->tv_sec = (time_t)seconds;
+ delay->tv_nsec /= scale_factor;
+ delay->tv_nsec += (seconds - delay->tv_sec) * 1000000000;
+ while (delay->tv_nsec >= 1000000000) {
+ delay->tv_sec++;
+ delay->tv_nsec -= 1000000000;
+ }
+ }
+
+ /* Clamp to max delay. */
+ if (max_delay != NULL) {
+ if (sudo_timespeccmp(delay, max_delay, >)) {
+ delay->tv_sec = max_delay->tv_sec;
+ delay->tv_nsec = max_delay->tv_nsec;
+ }
+ }
+
+ debug_return;
+}
+
+/*
+ * Parse the delay as seconds and nanoseconds: %lld.%09ld
+ * Sudo used to write this as a double, but since timing data is logged
+ * in the C locale this may not match the current locale.
+ */
+char *
+iolog_parse_delay(const char *cp, struct timespec *delay,
+ const char *decimal_point)
+{
+ char numbuf[(((sizeof(long long) * 8) + 2) / 3) + 2];
+ const char *errstr, *ep;
+ long long llval;
+ size_t len;
+ debug_decl(iolog_parse_delay, SUDO_DEBUG_UTIL);
+
+ /* Parse seconds (whole number portion). */
+ for (ep = cp; isdigit((unsigned char)*ep); ep++)
+ continue;
+ len = (size_t)(ep - cp);
+ if (len >= sizeof(numbuf)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s: number of seconds is too large", cp);
+ debug_return_ptr(NULL);
+ }
+ memcpy(numbuf, cp, len);
+ numbuf[len] = '\0';
+ delay->tv_sec = sudo_strtonum(numbuf, 0, TIME_T_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s: number of seconds is %s", numbuf, errstr);
+ debug_return_ptr(NULL);
+ }
+
+ /* Radix may be in user's locale for sudo < 1.7.4 so accept that too. */
+ if (*ep != '.' && *ep != *decimal_point) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid characters after seconds: %s", ep);
+ debug_return_ptr(NULL);
+ }
+ cp = ep + 1;
+
+ /* Parse fractional part, we may read more precision than we can store. */
+ for (ep = cp; isdigit((unsigned char)*ep); ep++)
+ continue;
+ len = (size_t)(ep - cp);
+ if (len >= sizeof(numbuf)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s: number of nanoseconds is too large", cp);
+ debug_return_ptr(NULL);
+ }
+ memcpy(numbuf, cp, len);
+ numbuf[len] = '\0';
+ llval = sudo_strtonum(numbuf, 0, LLONG_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s: number of nanoseconds is %s", numbuf, errstr);
+ debug_return_ptr(NULL);
+ }
+
+ /* Adjust fractional part to nanosecond precision. */
+ if (len < 9) {
+ /* Convert to nanosecond precision. */
+ do {
+ llval *= 10;
+ } while (++len < 9);
+ } else if (len > 9) {
+ /* Clamp to nanoseconds. */
+ do {
+ llval /= 10;
+ } while (--len > 9);
+ }
+ delay->tv_nsec = (long)llval;
+
+ /* Advance to the next field. */
+ while (isspace((unsigned char)*ep))
+ ep++;
+
+ debug_return_str((char *)ep);
+}
+
+/*
+ * Parse a timing line, which is formatted as:
+ * IO_EVENT_TTYOUT sleep_time num_bytes
+ * IO_EVENT_WINSIZE sleep_time lines cols
+ * IO_EVENT_SUSPEND sleep_time signo
+ * Where type is IO_EVENT_*, sleep_time is the number of seconds to sleep
+ * before writing the data and num_bytes is the number of bytes to output.
+ * Returns true on success and false on failure.
+ */
+bool
+iolog_parse_timing(const char *line, struct timing_closure *timing)
+{
+ unsigned long ulval;
+ char *cp, *ep;
+ debug_decl(iolog_parse_timing, SUDO_DEBUG_UTIL);
+
+ /* Clear iolog descriptor. */
+ timing->iol = NULL;
+
+ /* Parse event type. */
+ ulval = strtoul(line, &ep, 10);
+ if (ep == line || !isspace((unsigned char) *ep))
+ goto bad;
+ if (ulval >= IO_EVENT_COUNT)
+ goto bad;
+ if (ulval == IO_EVENT_TTYOUT_1_8_7) {
+ /* work around a bug in timing files generated by sudo 1.8.7 */
+ timing_event_adj = 2;
+ }
+ timing->event = (int)ulval - timing_event_adj;
+ for (cp = ep + 1; isspace((unsigned char) *cp); cp++)
+ continue;
+
+ /* Parse delay, returns the next field or NULL on error. */
+ if ((cp = iolog_parse_delay(cp, &timing->delay, timing->decimal)) == NULL)
+ goto bad;
+
+ switch (timing->event) {
+ case IO_EVENT_SUSPEND:
+ /* Signal name (no leading SIG prefix) or number. */
+ if (str2sig(cp, &timing->u.signo) == -1)
+ goto bad;
+ break;
+ case IO_EVENT_WINSIZE:
+ ulval = strtoul(cp, &ep, 10);
+ if (ep == cp || !isspace((unsigned char) *ep))
+ goto bad;
+ if (ulval > INT_MAX)
+ goto bad;
+ timing->u.winsize.lines = (int)ulval;
+ for (cp = ep + 1; isspace((unsigned char) *cp); cp++)
+ continue;
+
+ ulval = strtoul(cp, &ep, 10);
+ if (ep == cp || *ep != '\0')
+ goto bad;
+ if (ulval > INT_MAX)
+ goto bad;
+ timing->u.winsize.cols = (int)ulval;
+ break;
+ default:
+ errno = 0;
+ ulval = strtoul(cp, &ep, 10);
+ if (ep == cp || *ep != '\0')
+ goto bad;
+ /* Note: assumes SIZE_MAX == ULONG_MAX */
+ if (errno == ERANGE && ulval == ULONG_MAX)
+ goto bad;
+ timing->u.nbytes = (size_t)ulval;
+ break;
+ }
+
+ debug_return_bool(true);
+bad:
+ debug_return_bool(false);
+}
+
+/*
+ * Read the next record from the timing file.
+ * Return 0 on success, 1 on EOF and -1 on error.
+ */
+int
+iolog_read_timing_record(struct iolog_file *iol, struct timing_closure *timing)
+{
+ char line[LINE_MAX];
+ const char *errstr;
+ debug_decl(iolog_read_timing_record, SUDO_DEBUG_UTIL);
+
+ /* Read next record from timing file. */
+ if (iolog_gets(iol, line, sizeof(line), &errstr) == NULL) {
+ /* EOF or error reading timing file, we are done. */
+ if (iolog_eof(iol))
+ debug_return_int(1);
+ sudo_warnx(U_("error reading timing file: %s"), errstr);
+ debug_return_int(-1);
+ }
+
+ /* Parse timing file record. */
+ line[strcspn(line, "\n")] = '\0';
+ if (!iolog_parse_timing(line, timing)) {
+ sudo_warnx(U_("invalid timing file line: %s"), line);
+ debug_return_int(-1);
+ }
+
+ debug_return_int(0);
+}
diff --git a/lib/iolog/regress/host_port/host_port_test.c b/lib/iolog/regress/host_port/host_port_test.c
new file mode 100644
index 0000000..109393a
--- /dev/null
+++ b/lib/iolog/regress/host_port/host_port_test.c
@@ -0,0 +1,145 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# include "compat/stdbool.h"
+#endif
+#include <time.h>
+#include <unistd.h>
+
+#include "sudo_compat.h"
+#include "sudo_fatal.h"
+#include "sudo_iolog.h"
+#include "sudo_util.h"
+
+sudo_dso_public int main(int argc, char *argv[]);
+
+/*
+ * Test that iolog_parse_host_port() works as expected.
+ */
+
+struct host_port_test {
+ const char *str; /* input string */
+ const char *host; /* parsed host */
+ const char *port; /* parsed port */
+ bool tls; /* parsed TLS flag */
+ char *defport; /* default port */
+ char *defport_tls; /* default port */
+ bool ret; /* return value */
+};
+
+static struct host_port_test test_data[] = {
+ /* No TLS */
+ { "xerxes", "xerxes", "12345", false, "12345", NULL, true },
+ { "xerxes:12345", "xerxes", "12345", false, "67890", NULL, true },
+ { "127.0.0.1", "127.0.0.1", "12345", false, "12345", NULL, true },
+ { "127.0.0.1:12345", "127.0.0.1", "12345", false, "67890", NULL, true },
+ { "[::1]", "::1", "12345", false, "12345", NULL, true },
+ { "[::1]:12345", "::1", "12345", false, "67890", NULL, true },
+
+ /* With TLS */
+ { "xerxes(tls)", "xerxes", "12345", true, "5678", "12345", true },
+ { "xerxes:12345(tls)", "xerxes", "12345", true, "5678", "67890", true },
+ { "127.0.0.1(tls)", "127.0.0.1", "12345", true, "5678", "12345", true },
+ { "127.0.0.1:12345(tls)", "127.0.0.1", "12345", true, "5678", "67890", true },
+ { "[::1](tls)", "::1", "12345", true, "5678", "12345", true },
+ { "[::1]:12345(tls)", "::1", "12345", true, "5678", "67890", true },
+
+ /* Errors */
+ { "xerxes:", NULL, NULL, false, "12345", NULL, false }, /* missing port */
+ { "127.0.0.1:", NULL, NULL, false, "12345", NULL, false }, /* missing port */
+ { "[::1:12345", NULL, NULL, false, "67890", NULL, false }, /* missing bracket */
+ { "[::1]:", NULL, NULL, false, "12345", NULL, false }, /* missing port */
+ { NULL }
+};
+
+int
+main(int argc, char *argv[])
+{
+ int i, errors = 0, ntests = 0;
+ char *host, *port, *copy = NULL;
+ bool ret, tls;
+
+ initprogname(argc > 0 ? argv[0] : "host_port_test");
+
+ for (i = 0; test_data[i].str != NULL; i++) {
+ host = port = NULL;
+ tls = false;
+ free(copy);
+ if ((copy = strdup(test_data[i].str)) == NULL)
+ sudo_fatal_nodebug(NULL);
+
+ ntests++;
+ ret = iolog_parse_host_port(copy, &host, &port, &tls,
+ test_data[i].defport, test_data[i].defport_tls);
+ if (ret != test_data[i].ret) {
+ sudo_warnx_nodebug("test #%d: %s: returned %s, expected %s",
+ ntests, test_data[i].str, ret ? "true" : "false",
+ test_data[i].ret ? "true" : "false");
+ errors++;
+ continue;
+ }
+ if (!ret)
+ continue;
+
+ if (host == NULL) {
+ sudo_warnx_nodebug("test #%d: %s: NULL host",
+ ntests, test_data[i].str);
+ errors++;
+ continue;
+ }
+ if (strcmp(host, test_data[i].host) != 0) {
+ sudo_warnx_nodebug("test #%d: %s: bad host, expected %s, got %s",
+ ntests, test_data[i].str, test_data[i].host, host);
+ errors++;
+ continue;
+ }
+ if (port == NULL) {
+ sudo_warnx_nodebug("test #%d: %s: NULL port",
+ ntests, test_data[i].str);
+ errors++;
+ continue;
+ }
+ if (strcmp(port, test_data[i].port) != 0) {
+ sudo_warnx_nodebug("test #%d: %s: bad port, expected %s, got %s",
+ ntests, test_data[i].str, test_data[i].port, port);
+ errors++;
+ continue;
+ }
+ if (tls != test_data[i].tls) {
+ sudo_warnx_nodebug("test #%d: %s: bad tls, expected %s, got %s",
+ ntests, test_data[i].str, test_data[i].tls ? "true" : "false",
+ tls ? "true" : "false");
+ errors++;
+ continue;
+ }
+ }
+ free(copy);
+ if (ntests != 0) {
+ printf("%s: %d tests run, %d errors, %d%% success rate\n",
+ getprogname(), ntests, errors, (ntests - errors) * 100 / ntests);
+ }
+ exit(errors);
+}
diff --git a/lib/iolog/regress/iolog_json/check_iolog_json.c b/lib/iolog/regress/iolog_json/check_iolog_json.c
new file mode 100644
index 0000000..a967ba6
--- /dev/null
+++ b/lib/iolog/regress/iolog_json/check_iolog_json.c
@@ -0,0 +1,265 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <unistd.h>
+
+#define SUDO_ERROR_WRAP 0
+
+#include "sudo_compat.h"
+#include "sudo_util.h"
+#include "sudo_fatal.h"
+
+#include "iolog_json.h"
+
+sudo_dso_public int main(int argc, char *argv[]);
+
+bool
+json_print_object(struct json_container *json, struct json_object *object)
+{
+ struct json_item *item;
+ struct json_value json_value;
+ bool ret = false;
+
+ TAILQ_FOREACH(item, &object->items, entries) {
+ switch (item->type) {
+ case JSON_STRING:
+ json_value.type = JSON_STRING;
+ json_value.u.string = item->u.string;
+ if (!sudo_json_add_value(json, item->name, &json_value))
+ goto oom;
+ break;
+ case JSON_NUMBER:
+ json_value.type = JSON_NUMBER;
+ json_value.u.number = item->u.number;
+ if (!sudo_json_add_value(json, item->name, &json_value))
+ goto oom;
+ break;
+ case JSON_OBJECT:
+ if (!sudo_json_open_object(json, item->name))
+ goto oom;
+ if (!json_print_object(json, &item->u.child))
+ goto done;
+ if (!sudo_json_close_object(json))
+ goto oom;
+ break;
+ case JSON_ARRAY:
+ if (!sudo_json_open_array(json, item->name))
+ goto oom;
+ if (!json_print_object(json, &item->u.child))
+ goto done;
+ if (!sudo_json_close_array(json))
+ goto oom;
+ break;
+ case JSON_BOOL:
+ json_value.type = JSON_BOOL;
+ json_value.u.boolean = item->u.boolean;
+ if (!sudo_json_add_value(json, item->name, &json_value))
+ goto oom;
+ break;
+ case JSON_NULL:
+ json_value.type = JSON_NULL;
+ if (!sudo_json_add_value(json, item->name, &json_value))
+ goto oom;
+ break;
+ default:
+ sudo_warnx("unsupported JSON type %d", item->type);
+ goto done;
+ }
+ }
+
+ ret = true;
+ goto done;
+
+oom:
+ sudo_warnx("%s: %s", __func__, "unable to allocate memory");
+done:
+ return ret;
+}
+
+static bool
+json_format(struct json_container *json, struct json_object *object)
+{
+ struct json_item *item;
+ bool ret = false;
+
+ /* First object holds all the actual data. */
+ item = TAILQ_FIRST(&object->items);
+ if (item->type != JSON_OBJECT) {
+ sudo_warnx("expected JSON_OBJECT, got %d", item->type);
+ goto done;
+ }
+ object = &item->u.child;
+
+ if (!json_print_object(json, object))
+ goto done;
+
+ ret = true;
+
+done:
+ return ret;
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-c] input_file ...\n",
+ getprogname());
+ exit(EXIT_FAILURE);
+}
+
+static bool
+compare(FILE *fp, const char *infile, struct json_container *json)
+{
+ const char *cp;
+ unsigned int lineno = 0;
+ size_t linesize = 0;
+ char *line = NULL;
+ ssize_t len;
+
+ cp = sudo_json_get_buf(json);
+
+ while ((len = getdelim(&line, &linesize, '\n', fp)) != -1) {
+ lineno++;
+
+ /* skip open/close brace, not present in formatted output */
+ if (lineno == 1 && strcmp(line, "{\n") == 0)
+ continue;
+ if (*cp == '\0' && strcmp(line, "}\n") == 0)
+ continue;
+
+ /* Ignore newlines in output to make comparison easier. */
+ if (*cp == '\n')
+ cp++;
+ if (line[len - 1] == '\n')
+ len--;
+
+ if (strncmp(line, cp, len) != 0) {
+ fprintf(stderr, "%s: mismatch on line %u\n", infile, lineno);
+ fprintf(stderr, "expected: %s", line);
+ fprintf(stderr, "got : %.*s\n", (int)len, cp);
+ return false;
+ }
+ cp += len;
+ }
+ free(line);
+
+ return true;
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct json_object root;
+ int ch, i, tests = 0, errors = 0;
+ bool cat = false;
+
+ initprogname(argc > 0 ? argv[0] : "check_iolog_json");
+
+ while ((ch = getopt(argc, argv, "c")) != -1) {
+ switch (ch) {
+ case 'c':
+ cat = true;
+ break;
+ default:
+ usage();
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 1)
+ usage();
+
+ for (i = 0; i < argc; i++) {
+ struct json_container json;
+ const char *infile = argv[i];
+ const char *outfile = argv[i];
+ const char *cp;
+ char pathbuf[PATH_MAX];
+ FILE *infp = NULL;
+ FILE *outfp = NULL;
+
+ tests++;
+
+ if (!sudo_json_init(&json, 4, false, true)) {
+ errors++;
+ continue;
+ }
+
+ /* Parse input file. */
+ if ((infp = fopen(infile, "r")) == NULL) {
+ sudo_warn("%s", argv[1]);
+ errors++;
+ goto next;
+ }
+ if (!iolog_parse_json(infp, infile, &root)) {
+ errors++;
+ goto next;
+ }
+
+ /* Format as pretty-printed JSON */
+ if (!json_format(&json, &root)) {
+ errors++;
+ goto next;
+ }
+
+ /* Check for a .out.ok file in the same location as the .in file. */
+ cp = strrchr(infile, '.');
+ if (cp != NULL && strcmp(cp, ".in") == 0) {
+ snprintf(pathbuf, sizeof(pathbuf), "%.*s.out.ok",
+ (int)(cp - infile), infile);
+ if ((outfp = fopen(pathbuf, "r")) != NULL)
+ outfile = pathbuf;
+ }
+ if (outfp == NULL)
+ outfp = infp;
+
+ /* Compare output to expected output. */
+ rewind(outfp);
+ if (!compare(outfp, outfile, &json))
+ errors++;
+
+ /* Write the formatted output to stdout for -c (cat) */
+ if (cat) {
+ fprintf(stdout, "{%s\n}\n", sudo_json_get_buf(&json));
+ fflush(stdout);
+ }
+
+next:
+ free_json_items(&root.items);
+ sudo_json_free(&json);
+ if (infp != NULL)
+ fclose(infp);
+ if (outfp != NULL && outfp != infp)
+ fclose(outfp);
+ }
+
+ if (tests != 0) {
+ printf("iolog_json: %d test%s run, %d errors, %d%% success rate\n",
+ tests, tests == 1 ? "" : "s", errors,
+ (tests - errors) * 100 / tests);
+ }
+
+ exit(errors);
+}
diff --git a/lib/iolog/regress/iolog_json/test1.in b/lib/iolog/regress/iolog_json/test1.in
new file mode 100644
index 0000000..8ad3689
--- /dev/null
+++ b/lib/iolog/regress/iolog_json/test1.in
@@ -0,0 +1,34 @@
+{
+ "timestamp": {
+ "seconds": 1584993067,
+ "nanoseconds": 880288287
+ },
+ "columns": 80,
+ "command": "/usr/bin/make",
+ "lines": 24,
+ "runargv": [
+ "make",
+ "test"
+ ],
+ "runenv": [
+ "LANG=en_US.UTF-8",
+ "PATH=/bin:/sbin:/usr/games:/usr/bin:/usr/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin",
+ "TERM=vt100",
+ "MAIL=/var/mail/root",
+ "LOGNAME=root",
+ "USER=root",
+ "HOME=/root",
+ "SHELL=/bin/ksh",
+ "SUDO_COMMAND=/usr/bin/make test",
+ "SUDO_USER=millert",
+ "SUDO_UID=8036",
+ "SUDO_GID=20",
+ "A__z=\"*SHLVL"
+ ],
+ "runuid": 0,
+ "runuser": "root",
+ "submitcwd": "/home/test",
+ "submithost": "sudo.ws",
+ "submituser": "millert",
+ "ttyname": "/dev/console"
+}
diff --git a/lib/iolog/regress/iolog_json/test2.in b/lib/iolog/regress/iolog_json/test2.in
new file mode 100644
index 0000000..df7170f
--- /dev/null
+++ b/lib/iolog/regress/iolog_json/test2.in
@@ -0,0 +1,28 @@
+{
+ "timestamp": { "seconds": 1584993067, "nanoseconds": 880288287 },
+ "columns": 80,
+ "command": "/usr/bin/make",
+ "lines": 24,
+ "runargv": [ "make", "test" ],
+ "runenv": [
+ "LANG=en_US.UTF-8",
+ "PATH=/bin:/sbin:/usr/games:/usr/bin:/usr/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin",
+ "TERM=vt100",
+ "MAIL=/var/mail/root",
+ "LOGNAME=root",
+ "USER=root",
+ "HOME=/root",
+ "SHELL=/bin/ksh",
+ "SUDO_COMMAND=/usr/bin/make test",
+ "SUDO_USER=millert",
+ "SUDO_UID=8036",
+ "SUDO_GID=20",
+ "A__z=\"*SHLVL"
+ ],
+ "runuid": 0,
+ "runuser": "root",
+ "submitcwd": "/home/test",
+ "submithost": "sudo.ws",
+ "submituser": "millert",
+ "ttyname": "/dev/console"
+}
diff --git a/lib/iolog/regress/iolog_json/test2.out.ok b/lib/iolog/regress/iolog_json/test2.out.ok
new file mode 100644
index 0000000..8ad3689
--- /dev/null
+++ b/lib/iolog/regress/iolog_json/test2.out.ok
@@ -0,0 +1,34 @@
+{
+ "timestamp": {
+ "seconds": 1584993067,
+ "nanoseconds": 880288287
+ },
+ "columns": 80,
+ "command": "/usr/bin/make",
+ "lines": 24,
+ "runargv": [
+ "make",
+ "test"
+ ],
+ "runenv": [
+ "LANG=en_US.UTF-8",
+ "PATH=/bin:/sbin:/usr/games:/usr/bin:/usr/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin",
+ "TERM=vt100",
+ "MAIL=/var/mail/root",
+ "LOGNAME=root",
+ "USER=root",
+ "HOME=/root",
+ "SHELL=/bin/ksh",
+ "SUDO_COMMAND=/usr/bin/make test",
+ "SUDO_USER=millert",
+ "SUDO_UID=8036",
+ "SUDO_GID=20",
+ "A__z=\"*SHLVL"
+ ],
+ "runuid": 0,
+ "runuser": "root",
+ "submitcwd": "/home/test",
+ "submithost": "sudo.ws",
+ "submituser": "millert",
+ "ttyname": "/dev/console"
+}
diff --git a/lib/iolog/regress/iolog_json/test3.in b/lib/iolog/regress/iolog_json/test3.in
new file mode 100644
index 0000000..ea2df89
--- /dev/null
+++ b/lib/iolog/regress/iolog_json/test3.in
@@ -0,0 +1,22 @@
+{
+ "true": false,
+ "false": true,
+ "number": 1234567890,
+ "null": null,
+ "string": "nonsense",
+ "scope": {
+ "a": "b",
+ "bah": null
+ },
+ "array1": [
+ "foo",
+ "bar",
+ [
+ 123,
+ null,
+ false,
+ "fizz",
+ "buzz"
+ ]
+ ]
+}
diff --git a/lib/iolog/regress/iolog_mkpath/check_iolog_mkpath.c b/lib/iolog/regress/iolog_mkpath/check_iolog_mkpath.c
new file mode 100644
index 0000000..bb13787
--- /dev/null
+++ b/lib/iolog/regress/iolog_mkpath/check_iolog_mkpath.c
@@ -0,0 +1,91 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+
+#include <sys/wait.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define SUDO_ERROR_WRAP 0
+
+#include "sudo_compat.h"
+#include "sudo_util.h"
+#include "sudo_fatal.h"
+#include "sudo_iolog.h"
+
+sudo_dso_public int main(int argc, char *argv[]);
+
+static const char *test_paths[] = {
+ "testdir/a/b/c/user", /* create new */
+ "testdir/a/b/c/user", /* open existing */
+ "testdir/a/b/c/user.XXXXXX", /* mkdtemp new */
+ NULL
+};
+
+static void
+test_iolog_mkpath(const char *testdir, int *ntests, int *nerrors)
+{
+ const char **tp;
+ char *path;
+
+ iolog_set_owner(geteuid(), getegid());
+
+ for (tp = test_paths; *tp != NULL; tp++) {
+ if (asprintf(&path, "%s/%s", testdir, *tp) == -1)
+ sudo_fatalx("unable to allocate memory");
+
+ (*ntests)++;
+ if (!iolog_mkpath(path)) {
+ sudo_warnx("unable to mkpath %s", path);
+ (*nerrors)++;
+ }
+ free(path);
+ }
+}
+
+int
+main(int argc, char *argv[])
+{
+ char testdir[] = "mkpath.XXXXXX";
+ char *rmargs[] = { "rm", "-rf", NULL, NULL };
+ int status, tests = 0, errors = 0;
+
+ initprogname(argc > 0 ? argv[0] : "check_iolog_mkpath");
+
+ if (mkdtemp(testdir) == NULL)
+ sudo_fatal("unable to create test dir");
+ rmargs[2] = testdir;
+
+ test_iolog_mkpath(testdir, &tests, &errors);
+
+ if (tests != 0) {
+ printf("iolog_mkpath: %d test%s run, %d errors, %d%% success rate\n",
+ tests, tests == 1 ? "" : "s", errors,
+ (tests - errors) * 100 / tests);
+ }
+
+ /* Clean up (avoid running via shell) */
+ execvp("rm", rmargs);
+ wait(&status);
+
+ exit(errors);
+}
diff --git a/lib/iolog/regress/iolog_path/check_iolog_path.c b/lib/iolog/regress/iolog_path/check_iolog_path.c
new file mode 100644
index 0000000..8598384
--- /dev/null
+++ b/lib/iolog/regress/iolog_path/check_iolog_path.c
@@ -0,0 +1,273 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 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.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <time.h>
+#include <unistd.h>
+
+#define SUDO_ERROR_WRAP 0
+
+#include "sudo_compat.h"
+#include "sudo_util.h"
+#include "sudo_fatal.h"
+#include "sudo_iolog.h"
+
+static struct iolog_escape_data {
+ char sessid[7];
+ char *user;
+ char *group;
+ char *runas_user;
+ char *runas_group;
+ char *host;
+ char *command;
+} escape_data;
+
+sudo_dso_public int main(int argc, char *argv[]);
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s datafile\n", getprogname());
+ exit(EXIT_FAILURE);
+}
+
+static void
+reset_escape_data(struct iolog_escape_data *data)
+{
+ free(data->user);
+ free(data->group);
+ free(data->runas_user);
+ free(data->runas_group);
+ free(data->host);
+ free(data->command);
+ memset(data, 0, sizeof(*data));
+}
+
+static size_t
+fill_seq(char *str, size_t strsize, void *unused)
+{
+ int len;
+
+ /* Path is of the form /var/log/sudo-io/00/00/01. */
+ len = snprintf(str, strsize, "%c%c/%c%c/%c%c", escape_data.sessid[0],
+ escape_data.sessid[1], escape_data.sessid[2], escape_data.sessid[3],
+ escape_data.sessid[4], escape_data.sessid[5]);
+ if (len < 0)
+ return strsize; /* handle non-standard snprintf() */
+ return len;
+}
+
+static size_t
+fill_user(char *str, size_t strsize, void *unused)
+{
+ return strlcpy(str, escape_data.user, strsize);
+}
+
+static size_t
+fill_group(char *str, size_t strsize, void *unused)
+{
+ return strlcpy(str, escape_data.group, strsize);
+}
+
+static size_t
+fill_runas_user(char *str, size_t strsize, void *unused)
+{
+ return strlcpy(str, escape_data.runas_user, strsize);
+}
+
+static size_t
+fill_runas_group(char *str, size_t strsize, void *unused)
+{
+ return strlcpy(str, escape_data.runas_group, strsize);
+}
+
+static size_t
+fill_hostname(char *str, size_t strsize, void *unused)
+{
+ return strlcpy(str, escape_data.host, strsize);
+}
+
+static size_t
+fill_command(char *str, size_t strsize, void *unused)
+{
+ return strlcpy(str, escape_data.command, strsize);
+}
+
+/* Note: "seq" must be first in the list. */
+static struct iolog_path_escape path_escapes[] = {
+ { "seq", fill_seq },
+ { "user", fill_user },
+ { "group", fill_group },
+ { "runas_user", fill_runas_user },
+ { "runas_group", fill_runas_group },
+ { "hostname", fill_hostname },
+ { "command", fill_command },
+ { NULL, NULL }
+};
+
+static int
+do_check(char *dir_in, char *file_in, char *tdir_out, char *tfile_out)
+{
+ char dir[PATH_MAX], dir_out[PATH_MAX];
+ char file[PATH_MAX], file_out[PATH_MAX];
+ struct tm *timeptr;
+ time_t now;
+ int error = 0;
+
+ /*
+ * Expand any strftime(3) escapes
+ * XXX - want to pass timeptr to expand_iolog_path
+ */
+ time(&now);
+ timeptr = localtime(&now);
+ if (timeptr == NULL)
+ sudo_fatalx("localtime returned NULL");
+ strftime(dir_out, sizeof(dir_out), tdir_out, timeptr);
+ strftime(file_out, sizeof(file_out), tfile_out, timeptr);
+
+ if (!expand_iolog_path(dir_in, dir, sizeof(dir), &path_escapes[1], NULL))
+ sudo_fatalx("unable to expand I/O log dir");
+ if (!expand_iolog_path(file_in, file, sizeof(file), &path_escapes[0], dir))
+ sudo_fatalx("unable to expand I/O log file");
+
+ if (strcmp(dir, dir_out) != 0) {
+ sudo_warnx("%s: expected %s, got %s", dir_in, dir_out, dir);
+ error = 1;
+ }
+ if (strcmp(file, file_out) != 0) {
+ sudo_warnx("%s: expected %s, got %s", file_in, file_out, file);
+ error = 1;
+ }
+
+ return error;
+}
+
+#define MAX_STATE 12
+
+int
+main(int argc, char *argv[])
+{
+ size_t len;
+ FILE *fp;
+ char line[2048];
+ char *file_in = NULL, *file_out = NULL;
+ char *dir_in = NULL, *dir_out = NULL;
+ int state = 0;
+ int errors = 0;
+ int tests = 0;
+
+ initprogname(argc > 0 ? argv[0] : "check_iolog_path");
+
+ if (argc != 2)
+ usage();
+
+ fp = fopen(argv[1], "r");
+ if (fp == NULL)
+ sudo_fatalx("unable to open %s", argv[1]);
+
+ /*
+ * Input consists of 12 lines:
+ * sequence number
+ * user name
+ * user gid
+ * runas user name
+ * runas gid
+ * hostname [short form]
+ * command
+ * dir [with escapes]
+ * file [with escapes]
+ * expanded dir
+ * expanded file
+ * empty line
+ */
+ while (fgets(line, sizeof(line), fp) != NULL) {
+ len = strcspn(line, "\n");
+ line[len] = '\0';
+
+ switch (state) {
+ case 0:
+ strlcpy(escape_data.sessid, line, sizeof(escape_data.sessid));
+ break;
+ case 1:
+ if ((escape_data.user = strdup(line)) == NULL)
+ sudo_fatal(NULL);
+ break;
+ case 2:
+ if ((escape_data.group = strdup(line)) == NULL)
+ sudo_fatal(NULL);
+ break;
+ case 3:
+ if ((escape_data.runas_user = strdup(line)) == NULL)
+ sudo_fatal(NULL);
+ break;
+ case 4:
+ if ((escape_data.runas_group = strdup(line)) == NULL)
+ sudo_fatal(NULL);
+ break;
+ case 5:
+ if ((escape_data.host = strdup(line)) == NULL)
+ sudo_fatal(NULL);
+ break;
+ case 6:
+ if ((escape_data.command = strdup(line)) == NULL)
+ sudo_fatal(NULL);
+ break;
+ case 7:
+ if (dir_in != NULL)
+ free(dir_in);
+ dir_in = strdup(line);
+ break;
+ case 8:
+ if (file_in != NULL)
+ free(file_in);
+ file_in = strdup(line);
+ break;
+ case 9:
+ if (dir_out != NULL)
+ free(dir_out);
+ dir_out = strdup(line);
+ break;
+ case 10:
+ if (file_out != NULL)
+ free(file_out);
+ file_out = strdup(line);
+ break;
+ case 11:
+ errors += do_check(dir_in, file_in, dir_out, file_out);
+ tests++;
+ reset_escape_data(&escape_data);
+ break;
+ default:
+ sudo_fatalx("internal error, invalid state %d", state);
+ }
+ state = (state + 1) % MAX_STATE;
+ }
+
+ if (tests != 0) {
+ printf("iolog_path: %d test%s run, %d errors, %d%% success rate\n",
+ tests, tests == 1 ? "" : "s", errors,
+ (tests - errors) * 100 / tests);
+ }
+
+ exit(errors);
+}
diff --git a/lib/iolog/regress/iolog_path/data b/lib/iolog/regress/iolog_path/data
new file mode 100644
index 0000000..48dc87e
--- /dev/null
+++ b/lib/iolog/regress/iolog_path/data
@@ -0,0 +1,96 @@
+000001
+nobody
+nogroup
+root
+root
+somehost
+id
+/var/log/sudo-io
+%%{bogus}
+/var/log/sudo-io
+%%{bogus}
+
+000001
+nobody
+nogroup
+root
+wheel
+somehost
+id
+/var/log/sudo-io
+%%{seq}
+/var/log/sudo-io
+%%{seq}
+
+000001
+nobody
+nogroup
+root
+wheel
+somehost
+id
+/var/log/sudo-io
+%{seq}
+/var/log/sudo-io
+00/00/01
+
+000001
+nobody
+nogroup
+root
+wheel
+somehost
+id
+/var/log/sudo-io/%{user}
+%{seq}
+/var/log/sudo-io/nobody
+00/00/01
+
+000001
+nobody
+nogroup
+root
+wheel
+somehost
+su
+/var/log/sudo-io/%{user}/%{runas_user}
+%{command}_%Y%m%s_%H%M
+/var/log/sudo-io/nobody/root
+su_%Y%m%s_%H%M
+
+000001
+nobody
+nogroup
+root
+wheel
+somehost
+su
+/var/log/sudo-io/
+//%{user}/%{runas_user}/%{command}_%Y%m%s_%H%M
+/var/log/sudo-io
+/nobody/root/su_%Y%m%s_%H%M
+
+000001
+nobody
+nogroup
+root
+wheel
+somehost
+su
+/var/log/sudo-io/%d%m%Y
+%{user}/%{runas_user}/%{command}
+/var/log/sudo-io/%d%m%Y
+nobody/root/su
+
+000001
+nobody
+nogroup
+root
+wheel
+somehost
+su
+////////
+%{user}/%{runas_user}/%{command}
+
+nobody/root/su
+
diff --git a/lib/iolog/regress/iolog_util/check_iolog_util.c b/lib/iolog/regress/iolog_util/check_iolog_util.c
new file mode 100644
index 0000000..de94ee1
--- /dev/null
+++ b/lib/iolog/regress/iolog_util/check_iolog_util.c
@@ -0,0 +1,148 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 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.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define SUDO_ERROR_WRAP 0
+
+#include "sudo_compat.h"
+#include "sudo_util.h"
+#include "sudo_fatal.h"
+#include "sudo_iolog.h"
+
+sudo_dso_public int main(int argc, char *argv[]);
+
+static struct parse_delay_test {
+ const char *input;
+ const char *next_field;
+ struct timespec expected_delay;
+} parse_delay_tests[] = {
+ { "10.99999999999 X", "X", { 10, 999999999 } }, /* clamp to nsec */
+ { "10.999999999 X", "X", { 10, 999999999 } }, /* nsec */
+ { "10.999999 X", "X", { 10, 999999000 } }, /* usec -> nsec */
+ { "10.000999999 X", "X", { 10, 999999 } },
+ { "10.9 X", "X", { 10, 900000000 } },
+ { "10.0 X", "X", { 10, 0 } }
+};
+
+/*
+ * Test iolog_parse_delay()
+ */
+void
+test_parse_delay(int *ntests, int *nerrors)
+{
+ unsigned int i;
+
+ for (i = 0; i < nitems(parse_delay_tests); i++) {
+ struct timespec delay;
+ struct parse_delay_test *test = &parse_delay_tests[i];
+ char *cp = iolog_parse_delay(test->input, &delay, ".");
+ if (cp == NULL) {
+ sudo_warnx("%s:%u failed to parse delay: %s", __func__,
+ i, test->input);
+ (*nerrors)++;
+ continue;
+ }
+ if (strcmp(cp, test->next_field) != 0) {
+ sudo_warnx("%s:%u next field (want \"%s\", got \"%s\"", __func__,
+ i, test->next_field, cp);
+ (*nerrors)++;
+ continue;
+ }
+ if (delay.tv_sec != test->expected_delay.tv_sec) {
+ sudo_warnx("%s:%u wrong seconds (want %lld, got %lld)", __func__,
+ i, (long long)test->expected_delay.tv_sec,
+ (long long)delay.tv_sec);
+ (*nerrors)++;
+ continue;
+ }
+ if (delay.tv_nsec != test->expected_delay.tv_nsec) {
+ sudo_warnx("%s:%u wrong nanoseconds (want %ld, got %ld)", __func__,
+ i, test->expected_delay.tv_nsec, delay.tv_nsec);
+ (*nerrors)++;
+ continue;
+ }
+ }
+ (*ntests) += i;
+}
+
+static struct adjust_delay_test {
+ struct timespec in_delay;
+ struct timespec out_delay;
+ struct timespec max_delay;
+ double scale_factor;
+} adjust_delay_tests[] = {
+ { { 10, 300 }, { 10, 300 }, { 0, 0 }, 1.0 },
+ { { 10, 300 }, { 5, 150 }, { 0, 0 }, 2.0 },
+ { { 5, 300 }, { 2, 500000150 }, { 0, 0 }, 2.0 },
+ { { 0, 1000000 }, { 0, 333333 }, { 0, 0 }, 3 },
+ { { 10, 1000000 }, { 3, 333666666 }, { 0, 0 }, 3 },
+ { { 5, 150 }, { 10, 300 }, { 0, 0 }, 0.5 },
+ { { 5, 500000000 }, { 11, 0 }, { 0, 0 }, 0.5 },
+ { { 5, 150 }, { 5, 0 }, { 5, 0 }, 0.5 }
+};
+
+/*
+ * Test iolog_adjust_delay()
+ */
+void
+test_adjust_delay(int *ntests, int *nerrors)
+{
+ unsigned int i;
+
+ for (i = 0; i < nitems(adjust_delay_tests); i++) {
+ struct adjust_delay_test *test = &adjust_delay_tests[i];
+
+ iolog_adjust_delay(&test->in_delay,
+ sudo_timespecisset(&test->max_delay) ? &test->max_delay : NULL,
+ test->scale_factor);
+ if (!sudo_timespeccmp(&test->in_delay, &test->out_delay, ==)) {
+ sudo_warnx("%s:%u want {%lld, %ld}, got {%lld, %ld}", __func__, i,
+ (long long)test->out_delay.tv_sec, test->out_delay.tv_nsec,
+ (long long)test->in_delay.tv_sec, test->in_delay.tv_nsec);
+ (*nerrors)++;
+ }
+ }
+ (*ntests) += i;
+}
+
+int
+main(int argc, char *argv[])
+{
+ int tests = 0, errors = 0;
+
+ initprogname(argc > 0 ? argv[0] : "check_iolog_util");
+
+ test_parse_delay(&tests, &errors);
+
+ test_adjust_delay(&tests, &errors);
+
+ if (tests != 0) {
+ printf("iolog_util: %d test%s run, %d errors, %d%% success rate\n",
+ tests, tests == 1 ? "" : "s", errors,
+ (tests - errors) * 100 / tests);
+ }
+
+ exit(errors);
+}