=== modified file '.bzrignore' --- .bzrignore 2012-05-17 01:55:58 +0000 +++ .bzrignore 2019-07-27 10:11:45 +0000 @@ -12,3 +12,6 @@ plugins.d/splashy plugins.d/usplash plugins.d/plymouth +plugin-helpers/mandos-client-iprouteadddel +dracut-module/password-agent +.tramp_history === modified file 'INSTALL' --- INSTALL 2019-02-09 23:23:26 +0000 +++ INSTALL 2019-07-27 10:11:45 +0000 @@ -40,7 +40,7 @@ *** Mandos Server + GnuTLS 3.3 https://www.gnutls.org/ (but not 3.6.0 or later, until 3.6.6, which works) - + Avahi 0.6.16 http://www.avahi.org/ + + Avahi 0.6.16 https://www.avahi.org/ + Python 2.7 https://www.python.org/ + dbus-python 0.82.4 https://dbus.freedesktop.org/doc/dbus-python/ + PyGObject 3.7.1 https://wiki.gnome.org/Projects/PyGObject @@ -57,22 +57,29 @@ fping ssh-client *** Mandos Client - + GNU C Library 2.16 https://gnu.org/software/libc/ - + initramfs-tools 0.85i - https://tracker.debian.org/pkg/initramfs-tools + + GNU C Library 2.17 https://gnu.org/software/libc/ + GnuTLS 3.3 https://www.gnutls.org/ (but not 3.6.0 or later, until 3.6.6 which works) - + Avahi 0.6.16 http://www.avahi.org/ + + Avahi 0.6.16 https://www.avahi.org/ + GnuPG 1.4.9 https://www.gnupg.org/ + GPGME 1.1.6 https://www.gnupg.org/related_software/gpgme/ + pkg-config https://www.freedesktop.org/wiki/Software/pkg-config/ + + libnl-route 3 https://www.infradead.org/~tgr/libnl/ + + GLib 2.40 http://www.gtk.org/ + + One of: + + initramfs-tools 0.85i + https://tracker.debian.org/pkg/initramfs-tools + + dracut 044+241 + http://www.kernel.org/pub/linux/utils/boot/dracut/dracut.html Strongly recommended: + OpenSSH http://www.openssh.com/ Package names: - initramfs-tools libgnutls-dev gnutls-bin libavahi-core-dev gnupg - libgpgme11-dev pkg-config ssh + initramfs-tools dracut libgnutls-dev gnutls-bin libavahi-core-dev + gnupg libgpgme11-dev pkg-config ssh libnl-route-3-dev + libglib2.0-dev * Installing the Mandos server === modified file 'Makefile' --- Makefile 2019-04-09 20:09:51 +0000 +++ Makefile 2019-07-27 10:11:45 +0000 @@ -10,10 +10,12 @@ -Wmissing-format-attribute -Wnormalized=nfc -Wpacked \ -Wredundant-decls -Wnested-externs -Winline -Wvla \ -Wvolatile-register-var -Woverlength-strings -#DEBUG:=-ggdb3 -fsanitize=address -# For info about _FORTIFY_SOURCE, see feature_test_macros(7) -# and . -FORTIFY:=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC + +#DEBUG:=-ggdb3 -fsanitize=address $(SANITIZE) +## Check which sanitizing options can be used +#SANITIZE:=$(foreach option,$(ALL_SANITIZE_OPTIONS),$(shell \ +# echo 'int main(){}' | $(CC) --language=c $(option) \ +# /dev/stdin -o /dev/null >/dev/null 2>&1 && echo $(option))) # ALL_SANITIZE_OPTIONS:=-fsanitize=leak -fsanitize=undefined \ -fsanitize=shift -fsanitize=integer-divide-by-zero \ @@ -23,11 +25,11 @@ -fsanitize=object-size -fsanitize=float-divide-by-zero \ -fsanitize=float-cast-overflow -fsanitize=nonnull-attribute \ -fsanitize=returns-nonnull-attribute -fsanitize=bool \ - -fsanitize=enum -# Check which sanitizing options can be used -SANITIZE:=$(foreach option,$(ALL_SANITIZE_OPTIONS),$(shell \ - echo 'int main(){}' | $(CC) --language=c $(option) /dev/stdin \ - -o /dev/null >/dev/null 2>&1 && echo $(option))) + -fsanitize=enum -fsanitize-address-use-after-scope + +# For info about _FORTIFY_SOURCE, see feature_test_macros(7) +# and . +FORTIFY:=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC LINK_FORTIFY_LD:=-z relro -z now LINK_FORTIFY:= @@ -43,8 +45,10 @@ version:=1.8.4 SED:=sed -USER:=$(firstword $(subst :, ,$(shell getent passwd _mandos || getent passwd nobody || echo 65534))) -GROUP:=$(firstword $(subst :, ,$(shell getent group _mandos || getent group nogroup || echo 65534))) +USER:=$(firstword $(subst :, ,$(shell getent passwd _mandos \ + || getent passwd nobody || echo 65534))) +GROUP:=$(firstword $(subst :, ,$(shell getent group _mandos \ + || getent group nogroup || echo 65534))) ## Use these settings for a traditional /usr/local install # PREFIX:=$(DESTDIR)/usr/local @@ -52,6 +56,7 @@ # KEYDIR:=$(DESTDIR)/etc/mandos/keys # MANDIR:=$(PREFIX)/man # INITRAMFSTOOLS:=$(DESTDIR)/etc/initramfs-tools +# DRACUTMODULE:=$(DESTDIR)/usr/lib/dracut/modules.d/90mandos # STATEDIR:=$(DESTDIR)/var/lib/mandos # LIBDIR:=$(PREFIX)/lib ## @@ -62,6 +67,7 @@ KEYDIR:=$(DESTDIR)/etc/keys/mandos MANDIR:=$(PREFIX)/share/man INITRAMFSTOOLS:=$(DESTDIR)/usr/share/initramfs-tools +DRACUTMODULE:=$(DESTDIR)/usr/lib/dracut/modules.d/90mandos STATEDIR:=$(DESTDIR)/var/lib/mandos LIBDIR:=$(shell \ for d in \ @@ -86,11 +92,14 @@ getconf LFS_LDFLAGS) LIBNL3_CFLAGS:=$(shell pkg-config --cflags-only-I libnl-route-3.0) LIBNL3_LIBS:=$(shell pkg-config --libs libnl-route-3.0) +GLIB_CFLAGS:=$(shell pkg-config --cflags glib-2.0) +GLIB_LIBS:=$(shell pkg-config --libs glib-2.0) # Do not change these two -CFLAGS+=$(WARN) $(DEBUG) $(FORTIFY) $(SANITIZE) $(COVERAGE) \ +CFLAGS+=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) \ $(OPTIMIZE) $(LANGUAGE) -DVERSION='"$(version)"' -LDFLAGS+=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) +LDFLAGS+=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(strip \ + ) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) # Commands to format a DocBook document into a manual page DOCBOOKTOMAN=$(strip cd $(dir $<); xsltproc --nonet --xinclude \ @@ -102,9 +111,9 @@ /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \ $(notdir $<); \ if locale --all 2>/dev/null | grep --regexp='^en_US\.utf8$$' \ - && type man 2>/dev/null; then LANG=en_US.UTF-8 MANWIDTH=80 \ - man --warnings --encoding=UTF-8 --local-file $(notdir $@); \ - fi >/dev/null) + && command -v man >/dev/null; then LANG=en_US.UTF-8 \ + MANWIDTH=80 man --warnings --encoding=UTF-8 --local-file \ + $(notdir $@); fi >/dev/null) DOCBOOKTOHTML=$(strip xsltproc --nonet --xinclude \ --param make.year.ranges 1 \ @@ -123,10 +132,12 @@ plugins.d/usplash plugins.d/splashy plugins.d/askpass-fifo \ plugins.d/plymouth PLUGIN_HELPERS:=plugin-helpers/mandos-client-iprouteadddel -CPROGS:=plugin-runner $(PLUGINS) $(PLUGIN_HELPERS) +CPROGS:=plugin-runner dracut-module/password-agent $(PLUGINS) \ + $(PLUGIN_HELPERS) PROGS:=mandos mandos-keygen mandos-ctl mandos-monitor $(CPROGS) DOCS:=mandos.8 mandos-keygen.8 mandos-monitor.8 mandos-ctl.8 \ mandos.conf.5 mandos-clients.conf.5 plugin-runner.8mandos \ + dracut-module/password-agent.8mandos \ plugins.d/mandos-client.8mandos \ plugins.d/password-prompt.8mandos plugins.d/usplash.8mandos \ plugins.d/splashy.8mandos plugins.d/askpass-fifo.8mandos \ @@ -204,6 +215,15 @@ overview.xml legalnotice.xml $(DOCBOOKTOHTML) +dracut-module/password-agent.8mandos: \ + dracut-module/password-agent.xml common.ent \ + overview.xml legalnotice.xml + $(DOCBOOKTOMAN) +dracut-module/password-agent.8mandos.xhtml: \ + dracut-module/password-agent.xml common.ent \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + plugins.d/mandos-client.8mandos: plugins.d/mandos-client.xml \ common.ent \ mandos-options.xml \ @@ -252,19 +272,23 @@ --expression='s/\(mandos_\)[0-9.]\+\(\.orig\.tar\.gz\)/\1$(version)\2/' \ $@) -# Need to add the GnuTLS, Avahi and GPGME libraries, and can't use -# -fsanitize=leak because GnuTLS and GPGME both leak memory. +# Need to add the GnuTLS, Avahi and GPGME libraries plugins.d/mandos-client: plugins.d/mandos-client.c - $(CC) $(filter-out -fsanitize=leak,$(CFLAGS)) $(strip\ - ) $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(GPGME_CFLAGS) $(strip\ - ) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH) $^ $(strip\ - ) -lrt $(GNUTLS_LIBS) $(AVAHI_LIBS) $(strip\ - ) $(GPGME_LIBS) $(LOADLIBES) $(LDLIBS) -o $@ + $(LINK.c) $^ $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(strip\ + ) $(GPGME_CFLAGS) $(GNUTLS_LIBS) $(strip\ + ) $(AVAHI_LIBS) $(GPGME_LIBS) $(LOADLIBES) $(strip\ + ) $(LDLIBS) -o $@ +# Need to add the libnl-route library plugin-helpers/mandos-client-iprouteadddel: plugin-helpers/mandos-client-iprouteadddel.c $(LINK.c) $(LIBNL3_CFLAGS) $^ $(LIBNL3_LIBS) $(strip\ ) $(LOADLIBES) $(LDLIBS) -o $@ +# Need to add the GLib and pthread libraries +dracut-module/password-agent: dracut-module/password-agent.c + $(LINK.c) $(GLIB_CFLAGS) $^ $(GLIB_LIBS) -lpthread $(strip\ + ) $(LOADLIBES) $(LDLIBS) -o $@ + .PHONY : all doc html clean distclean mostlyclean maintainer-clean \ check run-client run-server install install-html \ install-server install-client-nokey install-client uninstall \ @@ -279,9 +303,13 @@ maintainer-clean: clean -rm --force --recursive keydir confdir statedir -check: all +check: all ./mandos --check ./mandos-ctl --check + ./mandos-keygen --version + ./plugin-runner --version + ./plugin-helpers/mandos-client-iprouteadddel --version + ./dracut-module/password-agent --test # Run the client with a local config and key run-client: all keydir/seckey.txt keydir/pubkey.txt keydir/tls-privkey.pem keydir/tls-pubkey.pem @@ -431,6 +459,15 @@ $(INITRAMFSTOOLS)/scripts/init-premount/mandos install initramfs-tools-script-stop \ $(INITRAMFSTOOLS)/scripts/local-premount/mandos + install --directory $(DRACUTMODULE) + install --mode=u=rw,go=r --target-directory=$(DRACUTMODULE) \ + dracut-module/ask-password-mandos.path \ + dracut-module/ask-password-mandos.service + install --mode=u=rwxs,go=rx \ + --target-directory=$(DRACUTMODULE) \ + dracut-module/module-setup.sh \ + dracut-module/cmdline-mandos.sh \ + dracut-module/password-agent install --mode=u=rw,go=r plugin-runner.conf $(CONFDIR) gzip --best --to-stdout mandos-keygen.8 \ > $(MANDIR)/man8/mandos-keygen.8.gz @@ -448,11 +485,22 @@ > $(MANDIR)/man8/askpass-fifo.8mandos.gz gzip --best --to-stdout plugins.d/plymouth.8mandos \ > $(MANDIR)/man8/plymouth.8mandos.gz + gzip --best --to-stdout dracut-module/password-agent.8mandos \ + > $(MANDIR)/man8/password-agent.8mandos.gz install-client: install-client-nokey # Post-installation stuff -$(PREFIX)/sbin/mandos-keygen --dir "$(KEYDIR)" - update-initramfs -k all -u + if command -v update-initramfs >/dev/null; then \ + update-initramfs -k all -u; \ + elif command -v dracut >/dev/null; then \ + for initrd in $(DESTDIR)/boot/initr*-$(shell uname --kernel-release); do \ + if [ -w "$$initrd" ]; then \ + chmod go-r "$$initrd"; \ + dracut --force "$$initrd"; \ + fi; \ + done; \ + fi echo "Now run mandos-keygen --password --dir $(KEYDIR)" uninstall: uninstall-server uninstall-client @@ -485,6 +533,12 @@ $(INITRAMFSTOOLS)/hooks/mandos \ $(INITRAMFSTOOLS)/conf-hooks.d/mandos \ $(INITRAMFSTOOLS)/scripts/init-premount/mandos \ + $(INITRAMFSTOOLS)/scripts/local-premount/mandos \ + $(DRACUTMODULE)/ask-password-mandos.path \ + $(DRACUTMODULE)/ask-password-mandos.service \ + $(DRACUTMODULE)/module-setup.sh \ + $(DRACUTMODULE)/cmdline-mandos.sh \ + $(DRACUTMODULE)/password-agent \ $(MANDIR)/man8/mandos-keygen.8.gz \ $(MANDIR)/man8/plugin-runner.8mandos.gz \ $(MANDIR)/man8/mandos-client.8mandos.gz @@ -493,9 +547,16 @@ $(MANDIR)/man8/splashy.8mandos.gz \ $(MANDIR)/man8/askpass-fifo.8mandos.gz \ $(MANDIR)/man8/plymouth.8mandos.gz \ + $(MANDIR)/man8/password-agent.8mandos.gz \ -rmdir $(LIBDIR)/mandos/plugins.d $(CONFDIR)/plugins.d \ - $(LIBDIR)/mandos $(CONFDIR) $(KEYDIR) - update-initramfs -k all -u + $(LIBDIR)/mandos $(CONFDIR) $(KEYDIR) $(DRACUTMODULE) + if command -v update-initramfs >/dev/null; then \ + update-initramfs -k all -u; \ + elif command -v dracut >/dev/null; then \ + for initrd in $(DESTDIR)/boot/initr*-$(shell uname --kernel-release); do \ + test -w "$$initrd" && dracut --force "$$initrd"; \ + done; \ + fi purge: purge-server purge-client === modified file 'debian/control' --- debian/control 2019-02-11 06:14:29 +0000 +++ debian/control 2019-07-30 18:15:41 +0000 @@ -6,12 +6,12 @@ Björn Påhlsson Build-Depends: debhelper (>= 10), docbook-xml, docbook-xsl, libavahi-core-dev, libgpgme-dev | libgpgme11-dev, - libgnutls28-dev (>= 3.3.0), + libglib2.0-dev (>=2.40), libgnutls28-dev (>= 3.3.0), libgnutls28-dev (>= 3.6.6) | libgnutls28-dev (<< 3.6.0), xsltproc, pkg-config, libnl-route-3-dev Build-Depends-Indep: systemd, python (>= 2.7), python (<< 3), - python-dbus, python-gi -Standards-Version: 4.3.0 + python-dbus, python-gi, po-debconf +Standards-Version: 4.4.0 Vcs-Bzr: https://ftp.recompile.se/pub/mandos/trunk Vcs-Browser: https://bzr.recompile.se/loggerhead/mandos/trunk/files Homepage: https://www.recompile.se/mandos @@ -46,12 +46,14 @@ Architecture: linux-any Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, cryptsetup (<< 2:2.0.3-1) | cryptsetup-initramfs, - initramfs-tools (>= 0.99), dpkg-dev (>=1.16.0), + initramfs-tools (>= 0.99) | dracut (>= 044+241-3), + dpkg-dev (>=1.16.0), gnutls-bin (>= 3.6.6) | libgnutls30 (<< 3.6.0), debconf (>= 1.5.5) | debconf-2.0 Recommends: ssh Breaks: dropbear (<= 0.53.1-1) Enhances: cryptsetup +Conflicts: dracut-config-generic Description: do unattended reboots with an encrypted root file system This is the client part of the Mandos system, which allows computers to have encrypted root file systems and at the === modified file 'debian/mandos-client.README.Debian' --- debian/mandos-client.README.Debian 2019-02-09 23:23:26 +0000 +++ debian/mandos-client.README.Debian 2019-07-27 10:11:45 +0000 @@ -49,7 +49,11 @@ setting is changed, it will be necessary to update the initrd image by running this command: + (For initramfs-tools:) update-initramfs -k all -u + + (For dracut:) + dpkg-reconfigure dracut The device can also be overridden at boot time on the Linux kernel command line using the sixth colon-separated field of the "ip=" @@ -60,12 +64,12 @@ Note that since the network interfaces are used in the initial RAM disk environment, the network interfaces *must* exist at that stage. Thus, an interface can *not* be a pseudo-interface such as "br0" or - "tun0"; instead, only real interfaces (such as "eth0") can be used. - This can be overcome by writing a "network hook" program to create - an interface (see mandos-client(8mandos)) and placing it in - "/etc/mandos/network-hooks.d", from where it will be copied into the - initial RAM disk. Example network hook scripts can be found in - "/usr/share/doc/mandos-client/examples/network-hooks.d". + "tun0"; instead, only real interfaces (such as "enp1s0" or "eth0") + can be used. This can be overcome by writing a "network hook" + program to create an interface (see mandos-client(8mandos)) and + placing it in "/etc/mandos/network-hooks.d", from where it will be + copied into the initial RAM disk. Example network hook scripts can + be found in "/usr/share/doc/mandos-client/examples/network-hooks.d". * User-Supplied Plugins @@ -73,7 +77,11 @@ to the normal Mandos plugins. When adding or changing plugins, do not forget to update the initital RAM disk image: + (For initramfs-tools:) update-initramfs -k all -u + + (For dracut:) + dpkg-reconfigure dracut * Do *NOT* Edit "/etc/crypttab" @@ -108,4 +116,4 @@ policy or other reasons, simply replace the existing dhparams.pem file and update the initital RAM disk image. - -- Teddy Hogeborn , Sat, 9 Feb 2019 15:08:04 +0100 + -- Teddy Hogeborn , Mon, 15 Jul 2019 16:47:02 +0200 === modified file 'debian/mandos-client.lintian-overrides' --- debian/mandos-client.lintian-overrides 2019-02-10 03:50:20 +0000 +++ debian/mandos-client.lintian-overrides 2019-07-25 22:44:36 +0000 @@ -38,6 +38,3 @@ # The notice displayed from the postinst script really is critical mandos-client binary: postinst-uses-db-input - -# It is a really long line -mandos-client binary: manpage-has-errors-from-man usr/share/man/man8/plugin-runner.8mandos.gz *: warning *: can't break line === modified file 'debian/mandos-client.postinst' --- debian/mandos-client.postinst 2019-02-10 10:39:26 +0000 +++ debian/mandos-client.postinst 2019-07-27 10:11:45 +0000 @@ -22,7 +22,27 @@ # Update the initial RAM file system image update_initramfs() { - update-initramfs -u -k all + if command -v update-initramfs >/dev/null; then + update-initramfs -k all -u + elif command -v dracut >/dev/null; then + dracut_version="`dpkg-query --showformat='${Version}' --show dracut`" + if dpkg --compare-versions "$dracut_version" lt 043-1 \ + && bash -c '. /etc/dracut.conf; . /etc/dracut.conf.d/*; [ "$hostonly" != yes ]'; then + echo 'Dracut is not configured to use hostonly mode!' >&2 + return 1 + fi + # Logic taken from dracut.postinst + for kernel in /boot/vmlinu[xz]-*; do + kversion="${kernel#/boot/vmlinu[xz]-}" + # Dracut preserves old permissions of initramfs image + # files, so we adjust permissions before creating new + # initramfs image containing secret keys. + chmod go-r /boot/initrd.img-"$kversion" + if [ "$kversion" != "*" ]; then + /etc/kernel/postinst.d/dracut "$kversion" + fi + done + fi if dpkg --compare-versions "$2" lt-nl "1.0.10-1"; then # Make old initrd.img files unreadable too, in case they were === modified file 'debian/mandos-client.templates' --- debian/mandos-client.templates 2019-02-10 03:50:20 +0000 +++ debian/mandos-client.templates 2019-07-27 19:28:14 +0000 @@ -1,10 +1,19 @@ Template: mandos-client/key_id Type: note -Description: New client option "${key_id}" is REQUIRED on server - A new "key_id" client option is REQUIRED in the server's clients.conf file, otherwise this computer most likely will not reboot unattended. This option: +_description: New client option "${key_id}" is REQUIRED on server + A new "key_id" client option is REQUIRED in the server's clients.conf + file, otherwise this computer most likely will not reboot unattended. + This option: . ${key_id} . - must be added (all on one line!) on the Mandos server host, in the file /etc/mandos/clients.conf, right before the "fingerprint" option for this Mandos client. You must edit that file on that server and add this option. + must be added (all on one line!) on the Mandos server host, in the file + /etc/mandos/clients.conf, right before the "fingerprint" option for this + Mandos client. You must edit that file on that server and add this + option. . - With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS session keys. A new TLS key pair has been generated and will be used as identification, but the key ID of the public key needs to be added to the server, since this will now be used to identify the client to the server. + With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as + TLS session keys. A new TLS key pair has been generated and will be used + as identification, but the key ID of the public key needs to be added to + the server, since this will now be used to identify the client to the + server. === modified file 'debian/mandos.lintian-overrides' --- debian/mandos.lintian-overrides 2019-02-10 03:50:20 +0000 +++ debian/mandos.lintian-overrides 2019-07-26 00:44:35 +0000 @@ -2,7 +2,6 @@ # it, so it must be kept unreadable for non-root users. # mandos binary: non-standard-file-perm etc/mandos/clients.conf 0600 != 0644 -mandos: init.d-script-needs-depends-on-lsb-base etc/init.d/mandos (line 46) # The debconf templates is only used for displaying information # detected in the postinst, not for saving answers to questions, so we === modified file 'debian/mandos.templates' --- debian/mandos.templates 2019-02-10 08:41:14 +0000 +++ debian/mandos.templates 2019-07-27 19:28:14 +0000 @@ -1,19 +1,29 @@ Template: mandos/key_id Type: note -Description: New client option "key_id" is REQUIRED on server - A new "key_id" client option is REQUIRED in the clients.conf file, otherwise the client most likely will not reboot unattended. This option: +_Description: New client option "key_id" is REQUIRED on server + A new "key_id" client option is REQUIRED in the clients.conf file, + otherwise the client most likely will not reboot unattended. This option: . key_id = . - must be added in the file /etc/mandos/clients.conf, right before the "fingerprint" option, for each Mandos client. You must edit that file and add this option for all clients. To see the correct key ID for each client, run this command (on each client): + must be added in the file /etc/mandos/clients.conf, right before the + "fingerprint" option, for each Mandos client. You must edit that file and + add this option for all clients. To see the correct key ID for each + client, run this command (on each client): . mandos-keygen -F/dev/null|grep ^key_id . - Note: the client must all also be using GnuTLS 3.6.6 or later; the server cannot serve passwords for both old and new clients! + Note: the clients must all also be using GnuTLS 3.6.6 or later; the server + cannot serve passwords for both old and new clients! . - Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS session keys. A new TLS key pair will be generated on each client and will be used as identification, but the key ID of the public key needs to be added to this server, since this will now be used to identify the client to the server. + Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP + keys as TLS session keys. A new TLS key pair will be generated on each + client and will be used as identification, but the key ID of the public + key needs to be added to this server, since this will now be used to + identify the client to the server. Template: mandos/removed_bad_key_ids Type: note -Description: Bad key IDs have been removed from clients.conf - Bad key IDs, which were reported by a bug in Mandos client 1.8.0, have been removed from /etc/mandos/clients.conf +_Description: Bad key IDs have been removed from clients.conf + Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been + removed from /etc/mandos/clients.conf === added file 'debian/po/POTFILES.in' --- debian/po/POTFILES.in 1970-01-01 00:00:00 +0000 +++ debian/po/POTFILES.in 2019-07-27 19:28:14 +0000 @@ -0,0 +1,2 @@ +[type: gettext/rfc822deb] mandos.templates +[type: gettext/rfc822deb] mandos-client.templates === added file 'debian/po/templates.pot' --- debian/po/templates.pot 1970-01-01 00:00:00 +0000 +++ debian/po/templates.pot 2019-07-27 19:28:14 +0000 @@ -0,0 +1,127 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the mandos package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: mandos\n" +"Report-Msgid-Bugs-To: mandos@packages.debian.org\n" +"POT-Creation-Date: 2019-07-27 21:06+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "New client option \"key_id\" is REQUIRED on server" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the clients.conf file, " +"otherwise the client most likely will not reboot unattended. This option:" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "key_id = " +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"\"fingerprint\" option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "mandos-keygen -F/dev/null|grep ^key_id" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server." +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "Bad key IDs have been removed from clients.conf" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "New client option \"${key_id}\" is REQUIRED on server" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the server's clients.conf " +"file, otherwise this computer most likely will not reboot unattended. This " +"option:" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "${key_id}" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"must be added (all on one line!) on the Mandos server host, in the file /etc/" +"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos " +"client. You must edit that file on that server and add this option." +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." +msgstr "" === modified file 'debian/rules' --- debian/rules 2019-04-09 19:41:53 +0000 +++ debian/rules 2019-04-09 22:31:23 +0000 @@ -53,7 +53,7 @@ #bpo# -gnutls_version=$$(dpkg-query --showformat='$${Version}' \ #bpo# --show libgnutls30); \ #bpo# dpkg --compare-versions $$gnutls_version lt 3.6.0 \ -#bpo# && { cp --archive debian/control debian/control.orig; sed --expression='s/libgnutls28-dev (>= 3\.6\.6) |//' debian/control; } +#bpo# && { cp --archive debian/control debian/control.orig; sed --in-place --expression='s/libgnutls28-dev (>= 3\.6\.6) |//' debian/control; } #bpo# dh_shlibdeps #bpo# -gnutls_version=$$(dpkg-query --showformat='$${Version}' \ #bpo# --show libgnutls30); \ === added file 'debian/source/lintian-overrides' --- debian/source/lintian-overrides 1970-01-01 00:00:00 +0000 +++ debian/source/lintian-overrides 2019-07-27 19:48:18 +0000 @@ -0,0 +1,4 @@ +# We are both upstream and Debian maintainer for this package, so the +# .asc signature can not exist until after the orig.tar.gz has been +# built as part of the Debian package build. +mandos source: orig-tarball-missing-upstream-signature mandos_*.tar.gz === added directory 'debian/tests' === added file 'debian/tests/control' --- debian/tests/control 1970-01-01 00:00:00 +0000 +++ debian/tests/control 2019-07-27 10:11:45 +0000 @@ -0,0 +1,33 @@ +Test-Command: /usr/sbin/mandos --check +Restrictions: superficial +Features: test-name=mandos-check +Depends: mandos + +Test-Command: /usr/sbin/mandos-ctl --check --verbose +Restrictions: allow-stderr +Features: test-name=mandos-ctl +Depends: mandos + +Test-Command: /usr/sbin/mandos-keygen --version +Restrictions: superficial +Features: test-name=mandos-keygen-version +Depends: mandos-client + +Test-Command: /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-runner --version +Restrictions: needs-root, superficial +Features: test-name=plugin-runner-version +Depends: mandos-client + +Test-Command: /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-helpers/mandos-client-iprouteadddel --version +Restrictions: needs-root, superficial +Features: test-name=mandos-client-iprouteadddel-version +Depends: mandos-client + +Test-Command: /usr/lib/dracut/modules.d/90mandos/password-agent --test --verbose +Features: test-name=password-agent +Depends: mandos-client + +Test-Command: /usr/lib/dracut/modules.d/90mandos/password-agent --test --verbose -p /task-creators/start_mandos_client/suid +Restrictions: needs-root +Features: test-name=password-agent-suid +Depends: mandos-client === added directory 'dracut-module' === added file 'dracut-module/ask-password-mandos.path' --- dracut-module/ask-password-mandos.path 1970-01-01 00:00:00 +0000 +++ dracut-module/ask-password-mandos.path 2019-07-27 10:11:45 +0000 @@ -0,0 +1,47 @@ +# -*- systemd -*- +# +# Copyright © 2019 Teddy Hogeborn +# Copyright © 2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Mandos is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Mandos. If not, see . +# +# Contact the authors at . +# +# This systemd.path(5) unit will wait until there are any password +# questions present, represented by files named "ask.*" in the +# /run/systemd/ask-password directory, and then start the +# "ask-password-mandos.service" systemd.service(5) unit. + +# This file should be installed in the root file system as +# "/usr/lib/dracut/modules.d/90mandos/ask-password-mandos.path" and +# will be installed in the initramfs image file as +# "/lib/systemd/system/ask-password-mandos.path", and symlinked to +# "/lib/systemd/system//sysinit.target.wants/ask-password-mandos.path" +# by dracut when dracut creates the initramfs image file. + +[Unit] +Description=Forward Password Requests to remote Mandos server +Documentation=man:intro(8mandos) man:password-agent(8mandos) man:mandos-client(8mandos) +DefaultDependencies=no +Conflicts=shutdown.target +Before=basic.target shutdown.target +ConditionKernelCommandLine=!mandos=off +ConditionFileIsExecutable=/lib/mandos/password-agent +ConditionPathIsMountPoint=!/sysroot + +[Path] +PathExistsGlob=/run/systemd/ask-password/ask.* +MakeDirectory=yes === added file 'dracut-module/ask-password-mandos.service' --- dracut-module/ask-password-mandos.service 1970-01-01 00:00:00 +0000 +++ dracut-module/ask-password-mandos.service 2019-07-27 10:11:45 +0000 @@ -0,0 +1,51 @@ +# -*- systemd -*- +# +# Copyright © 2019 Teddy Hogeborn +# Copyright © 2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Mandos is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Mandos. If not, see . +# +# Contact the authors at . +# +# This systemd.service(5) unit file will start the Mandos +# password-agent(8mandos) program, which will in turn run +# mandos-client(8mandos) to get a password and send the password to +# any and all active password questions using the systemd “Password +# Agent” mechanism. + +# This file should be installed in the root file system as +# "/usr/lib/dracut/modules.d/90mandos/ask-password-mandos.service" and +# will be installed in the initramfs image file as +# "/lib/systemd/system/ask-password-mandos.service" by dracut when +# dracut creates the initramfs image file. + +[Unit] +Description=Forward Password Requests to remote Mandos server +Documentation=man:intro(8mandos) man:password-agent(8mandos) man:mandos-client(8mandos) +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target +ConditionKernelCommandLine=!mandos=off +ConditionFileIsExecutable=/lib/mandos/password-agent +ConditionFileIsExecutable=/lib/mandos/mandos-client +ConditionFileNotEmpty=/etc/mandos/keys/pubkey.txt +ConditionFileNotEmpty=/etc/mandos/keys/seckey.txt +ConditionFileNotEmpty=/etc/mandos/keys/tls-pubkey.pem +ConditionFileNotEmpty=/etc/mandos/keys/tls-privkey.pem +ConditionPathIsMountPoint=!/sysroot + +[Service] +ExecStart=/lib/mandos/password-agent -- /lib/mandos/mandos-client --pubkey=/etc/mandos/keys/pubkey.txt --seckey=/etc/mandos/keys/seckey.txt --tls-pubkey=/etc/mandos/keys/tls-pubkey.pem --tls-privkey=/etc/mandos/keys/tls-privkey.pem === added file 'dracut-module/cmdline-mandos.sh' --- dracut-module/cmdline-mandos.sh 1970-01-01 00:00:00 +0000 +++ dracut-module/cmdline-mandos.sh 2019-07-27 10:11:45 +0000 @@ -0,0 +1,74 @@ +#!/bin/sh +# +# This file should be present in the root file system directory +# /usr/lib/dracut/modules.d/90mandos. When dracut creates the +# initramfs image, dracut will run the "module-setup.sh" file in the +# same directory, which (when *not* using the "systemd" dracut module) +# will copy this file ("cmdline-mandos.sh") into the initramfs as +# "/lib/dracut/hooks/cmdline/20-cmdline-mandos.sh". +# +# Despite the above #!/bin/sh line and the executable flag, this file +# is not executed; this file is sourced by the /init script in the +# initramfs image created by dracut. + +if getargbool 1 mandos && [ -e /lib/dracut-crypt-lib.sh ]; then + cat >> /lib/dracut-crypt-lib.sh <<- "EOF" + ask_for_password(){ + local cmd; local prompt; local tries=3 + local ply_cmd; local ply_prompt; local ply_tries=3 + local tty_cmd; local tty_prompt; local tty_tries=3 + local ret + + while [ $# -gt 0 ]; do + case "$1" in + --cmd) ply_cmd="$2"; tty_cmd="$2"; shift;; + --ply-cmd) ply_cmd="$2"; shift;; + --tty-cmd) tty_cmd="$2"; shift;; + --prompt) ply_prompt="$2"; tty_prompt="$2"; shift;; + --ply-prompt) ply_prompt="$2"; shift;; + --tty-prompt) tty_prompt="$2"; shift;; + --tries) ply_tries="$2"; tty_tries="$2"; shift;; + --ply-tries) ply_tries="$2"; shift;; + --tty-tries) tty_tries="$2"; shift;; + --tty-echo-off) tty_echo_off=yes;; + -*) :;; + esac + shift + done + if [ -z "$ply_cmd" ]; then + ply_cmd="$tty_cmd" + fi + # Extract device and luksname from $ply_cmd + set -- $ply_cmd + shift + for arg in "$@"; do + case "$arg" in + -*) :;; + *) + if [ -z "$device" ]; then + device="$arg" + else + luksname="$arg" + break + fi + ;; + esac + done + { flock -s 9; + if [ -z "$ply_prompt" ]; then + if [ -z "$tty_prompt" ]; then + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + else + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=password-prompt:--prompt="${tty_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + fi + else + if [ -z "$tty_prompt" ]; then + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=plymouth:--prompt="${ply_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + else + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=password-prompt:--prompt="${tty_prompt}" --options-for=plymouth:--prompt="${ply_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + fi + fi + } 9>/.console_lock + } + EOF +fi === added file 'dracut-module/module-setup.sh' --- dracut-module/module-setup.sh 1970-01-01 00:00:00 +0000 +++ dracut-module/module-setup.sh 2019-07-27 10:11:45 +0000 @@ -0,0 +1,253 @@ +#!/bin/sh +# +# This file should be present in the root file system directory +# /usr/lib/dracut/modules.d/90mandos. When dracut creates the +# initramfs image, dracut will source this file and run the shell +# functions defined in this file: "install", "check", "depends", +# "cmdline", and "installkernel". +# +# Despite the above #!/bin/sh line and the executable flag, this file +# is not executed; this file is sourced by dracut when creating the +# initramfs image file. + +mandos_libdir(){ + for dir in /usr/lib \ + "/usr/lib/`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`" \ + "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/local/lib; do + if [ -d "$dir"/mandos ]; then + echo "$dir"/mandos + return + fi + done + # Mandos not found + return 1 +} + +mandos_keydir(){ + for dir in /etc/keys/mandos /etc/mandos/keys; do + if [ -d "$dir" ]; then + echo "$dir" + return + fi + done + # Mandos key directory not found + return 1 +} + +check(){ + if [ "${hostonly:-no}" = "no" ]; then + dwarning "Mandos: Dracut not in hostonly mode" + return 1 + fi + + local libdir=`mandos_libdir` + if [ -z "$libdir" ]; then + dwarning "Mandos lib directory not found" + return 1 + fi + + local keydir=`mandos_keydir` + if [ -z "$keydir" ]; then + dwarning "Mandos key directory not found" + return 1 + fi +} + +install(){ + chmod go+w,+t "$initdir"/tmp + local libdir=`mandos_libdir` + local keydir=`mandos_keydir` + set `{ getent passwd _mandos \ + || getent passwd nobody \ + || echo ::65534:65534:::; } \ + | cut --delimiter=: --fields=3,4 --only-delimited \ + --output-delimiter=" "` + local mandos_user="$1" + local mandos_group="$2" + inst "${libdir}" /lib/mandos + if dracut_module_included "systemd"; then + plugindir=/lib/mandos + inst "${libdir}/plugins.d/mandos-client" \ + "${plugindir}/mandos-client" + chmod u-s "${initdir}/${plugindir}/mandos-client" + inst "${moddir}/ask-password-mandos.service" \ + "${systemdsystemunitdir}/ask-password-mandos.service" + if [ ${mandos_user} != 65534 ]; then + sed --in-place \ + --expression="s,^ExecStart=/lib/mandos/password-agent ,&--user=${mandos_user} ," \ + "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service" + fi + if [ ${mandos_group} != 65534 ]; then + sed --in-place \ + --expression="s,^ExecStart=/lib/mandos/password-agent ,&--group=${mandos_group} ," \ + "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service" + fi + else + inst_hook cmdline 20 "$moddir"/cmdline-mandos.sh + plugindir=/lib/mandos/plugins.d + inst "${libdir}/plugin-runner" /lib/mandos/plugin-runner + inst /etc/mandos/plugin-runner.conf + sed --in-place \ + --expression='1i--options-for=mandos-client:--pubkey=/etc/mandos/keys/pubkey.txt,--seckey=/etc/mandos/keys/seckey.txt,--tls-pubkey=/etc/mandos/keys/tls-pubkey.pem,--tls-privkey=/etc/mandos/keys/tls-privkey.pem' \ + "${initdir}/etc/mandos/plugin-runner.conf" + if [ ${mandos_user} != 65534 ]; then + sed --in-place --expression="1i--userid=${mandos_user}" \ + "${initdir}/etc/mandos/plugin-runner.conf" + fi + if [ ${mandos_group} != 65534 ]; then + sed --in-place \ + --expression="1i--groupid=${mandos_group}" \ + "${initdir}/etc/mandos/plugin-runner.conf" + fi + inst "${libdir}/plugins.d" "$plugindir" + chown ${mandos_user}:${mandos_group} "${initdir}/${plugindir}" + # Copy the packaged plugins + for file in "$libdir"/plugins.d/*; do + base="`basename \"$file\"`" + # Is this plugin overridden? + if [ -e "/etc/mandos/plugins.d/$base" ]; then + continue + fi + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") dwarning "Mandos client plugin directory is empty." >&2 ;; + askpass-fifo) : ;; # Ignore packaged for dracut + *) inst "${file}" "${plugindir}/${base}" ;; + esac + done + # Copy any user-supplied plugins + for file in /etc/mandos/plugins.d/*; do + base="`basename \"$file\"`" + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) inst "$file" "${plugindir}/${base}" ;; + esac + done + # Copy any user-supplied plugin helpers + for file in /etc/mandos/plugin-helpers/*; do + base="`basename \"$file\"`" + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) inst "$file" "/lib/mandos/plugin-helpers/$base";; + esac + done + fi + # Copy network hooks + for hook in /etc/mandos/network-hooks.d/*; do + basename=`basename "$hook"` + case "$basename" in + "*") continue ;; + *[!A-Za-z0-9_.-]*) continue ;; + *) test -d "$hook" || inst "$hook" "/lib/mandos/network-hooks.d/$basename" ;; + esac + if [ -x "$hook" ]; then + # Copy any files needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \ + VERBOSITY=0 "$hook" files | while read file target; do + if [ ! -e "${file}" ]; then + dwarning "WARNING: file ${file} not found, requested by Mandos network hook '${basename}'" >&2 + fi + if [ -z "${target}" ]; then + inst "$file" + else + inst "$file" "$target" + fi + done + fi + done + # Copy the packaged plugin helpers + for file in "$libdir"/plugin-helpers/*; do + base="`basename \"$file\"`" + # Is this plugin overridden? + if [ -e "/etc/mandos/plugin-helpers/$base" ]; then + continue + fi + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) inst "$file" "/lib/mandos/plugin-helpers/$base";; + esac + done + local gpg=/usr/bin/gpg + if [ -e /usr/bin/gpgconf ]; then + inst /usr/bin/gpgconf + gpg="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg:[^:]*://p'`" + gpgagent="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg-agent:[^:]*://p'`" + # Newer versions of GnuPG 2 requires the gpg-agent binary + if [ -e "$gpgagent" ]; then + inst "$gpgagent" + fi + fi + inst "$gpg" + if dracut_module_included "systemd"; then + inst "${moddir}/password-agent" /lib/mandos/password-agent + inst "${moddir}/ask-password-mandos.path" \ + "${systemdsystemunitdir}/ask-password-mandos.path" + ln_r "${systemdsystemunitdir}/ask-password-mandos.path" \ + "${systemdsystemunitdir}/sysinit.target.wants/ask-password-mandos.path" + fi + # Key files + for file in "$keydir"/*; do + if [ -d "$file" ]; then + continue + fi + case "$file" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) + inst "$file" "/etc/mandos/keys/`basename \"$file\"`" + chown ${mandos_user}:${mandos_group} \ + "${initdir}/etc/mandos/keys/`basename \"$file\"`" + if [ `basename "$file"` = dhparams.pem ]; then + # Use Diffie-Hellman parameters file + if dracut_module_included "systemd"; then + sed --in-place \ + --expression='/^ExecStart/s/$/ --dh-params=\/etc\/mandos\/keys\/dhparams.pem/' \ + "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service" + else + sed --in-place \ + --expression="1i--options-for=mandos-client:--dh-params=/etc/mandos/keys/dhparams.pem" \ + "${initdir}/etc/mandos/plugin-runner.conf" + fi + fi + ;; + esac + done +} + +installkernel(){ + instmods =drivers/net + hostonly='' instmods ipv6 + # Copy any kernel modules needed by network hooks + for hook in /etc/mandos/network-hooks.d/*; do + basename=`basename "$hook"` + case "$basename" in + "*") continue ;; + *[!A-Za-z0-9_.-]*) continue ;; + esac + if [ -x "$hook" ]; then + # Copy and load any modules needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=modules \ + VERBOSITY=0 "$hook" modules | while read module; do + if [ -z "${target}" ]; then + instmods "$module" + fi + done + fi + done +} + +depends(){ + echo crypt +} + +cmdline(){ + : +} === added file 'dracut-module/password-agent.c' --- dracut-module/password-agent.c 1970-01-01 00:00:00 +0000 +++ dracut-module/password-agent.c 2019-07-27 10:11:45 +0000 @@ -0,0 +1,7721 @@ +/* -*- mode: c; coding: utf-8; after-save-hook: (lambda () (let* ((find-build-directory (lambda (try-directory &optional base-directory) (let ((base-directory (or base-directory try-directory))) (cond ((equal try-directory "/") base-directory) ((file-readable-p (concat (file-name-as-directory try-directory) "Makefile")) try-directory) ((funcall find-build-directory (directory-file-name (file-name-directory try-directory)) base-directory)))))) (build-directory (funcall find-build-directory (buffer-file-name))) (local-build-directory (if (fboundp 'file-local-name) (file-local-name build-directory) (or (file-remote-p build-directory 'localname) build-directory))) (command (file-relative-name (file-name-sans-extension (buffer-file-name)) build-directory))) (pcase (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (let ((qbdir (shell-quote-argument local-build-directory)) (qcmd (shell-quote-argument command))) (format "cd %s && CFLAGS=-Werror make --silent %s && %s --test --verbose" qbdir qcmd qcmd)) nil "*Test*")) (0 (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w)))) (_ (with-current-buffer "*Test*" (compilation-mode) (cd-absolute build-directory)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); -*- */ +/* + * Mandos password agent - Simple password agent to run Mandos client + * + * Copyright © 2019 Teddy Hogeborn + * Copyright © 2019 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Mandos is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Mandos. If not, see . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE +#include /* uintmax_t, PRIuMAX, PRIdMAX, + intmax_t, uint32_t, SCNx32, + SCNuMAX, SCNxMAX */ +#include /* size_t */ +#include /* pid_t, uid_t, gid_t, getuid(), + getpid() */ +#include /* bool, true, false */ +#include /* struct sigaction, sigset_t, + sigemptyset(), sigaddset(), + SIGCHLD, pthread_sigmask(), + SIG_BLOCK, SIG_SETMASK, SA_RESTART, + SA_NOCLDSTOP, sigfillset(), kill(), + SIGTERM, sigdelset(), SIGKILL, + NSIG, sigismember(), SA_ONSTACK, + SIG_DFL, SIG_IGN, SIGINT, SIGQUIT, + SIGHUP, SIGSTOP, SIG_UNBLOCK */ +#include /* EXIT_SUCCESS, EXIT_FAILURE, + malloc(), free(), strtoumax(), + realloc(), setenv(), calloc(), + mkdtemp(), mkostemp() */ +#include /* not, or, and, xor */ +#include /* error() */ +#include /* EX_USAGE, EX_OSERR, EX_OSFILE */ +#include /* errno, error_t, EACCES, + ENAMETOOLONG, ENOENT, EEXIST, + ECHILD, EPERM, ENOMEM, EAGAIN, + EINTR, ENOBUFS, EADDRINUSE, + ECONNREFUSED, ECONNRESET, + ETOOMANYREFS, EMSGSIZE, EBADF, + EINVAL */ +#include /* strdup(), memcpy(), + explicit_bzero(), memset(), + strcmp(), strlen(), strncpy(), + memcmp(), basename() */ +#include /* argz_create(), argz_count(), + argz_extract(), argz_next(), + argz_add() */ +#include /* epoll_create1(), EPOLL_CLOEXEC, + epoll_ctl(), EPOLL_CTL_ADD, + struct epoll_event, EPOLLIN, + EPOLLRDHUP, EPOLLOUT, + epoll_pwait() */ +#include /* struct timespec, clock_gettime(), + CLOCK_MONOTONIC */ +#include /* struct argp_option, OPTION_HIDDEN, + OPTION_ALIAS, struct argp_state, + ARGP_ERR_UNKNOWN, ARGP_KEY_ARGS, + struct argp, argp_parse(), + ARGP_NO_EXIT */ +#include /* uid_t, gid_t, close(), pipe2(), + fork(), _exit(), dup2(), + STDOUT_FILENO, setresgid(), + setresuid(), execv(), ssize_t, + read(), dup3(), getuid(), dup(), + STDERR_FILENO, pause(), write(), + rmdir(), unlink(), getpid() */ +#include /* munlock(), mlock() */ +#include /* O_CLOEXEC, O_NONBLOCK, fcntl(), + F_GETFD, F_GETFL, FD_CLOEXEC, + open(), O_WRONLY, O_RDONLY */ +#include /* waitpid(), WNOHANG, WIFEXITED(), + WEXITSTATUS() */ +#include /* PIPE_BUF, NAME_MAX, INT_MAX */ +#include /* inotify_init1(), IN_NONBLOCK, + IN_CLOEXEC, inotify_add_watch(), + IN_CLOSE_WRITE, IN_MOVED_TO, + IN_DELETE, struct inotify_event */ +#include /* fnmatch(), FNM_FILE_NAME */ +#include /* asprintf(), FILE, fopen(), + getline(), sscanf(), feof(), + ferror(), fclose(), stderr, + rename(), fdopen(), fprintf(), + fscanf() */ +#include /* GKeyFile, g_key_file_free(), g_key_file_new(), + GError, g_key_file_load_from_file(), + G_KEY_FILE_NONE, TRUE, G_FILE_ERROR_NOENT, + g_key_file_get_string(), guint64, + g_key_file_get_uint64(), + G_KEY_FILE_ERROR_KEY_NOT_FOUND, gconstpointer, + g_assert_true(), g_assert_nonnull(), + g_assert_null(), g_assert_false(), + g_assert_cmpint(), g_assert_cmpuint(), + g_test_skip(), g_assert_cmpstr(), + g_test_init(), g_test_add(), g_test_run(), + GOptionContext, g_option_context_new(), + g_option_context_set_help_enabled(), FALSE, + g_option_context_set_ignore_unknown_options(), + gboolean, GOptionEntry, G_OPTION_ARG_NONE, + g_option_context_add_main_entries(), + g_option_context_parse(), + g_option_context_free(), g_error() */ +#include /* struct sockaddr_un, SUN_LEN */ +#include /* AF_LOCAL, socket(), PF_LOCAL, + SOCK_DGRAM, SOCK_NONBLOCK, + SOCK_CLOEXEC, connect(), + struct sockaddr, socklen_t, + shutdown(), SHUT_RD, send(), + MSG_NOSIGNAL, bind(), recv(), + socketpair() */ +#include /* globfree(), glob_t, glob(), + GLOB_ERR, GLOB_NOSORT, GLOB_MARK, + GLOB_ABORTED, GLOB_NOMATCH, + GLOB_NOSPACE */ + +/* End of includes */ + +/* Start of declarations of private types and functions */ + +/* microseconds of CLOCK_MONOTONIC absolute time; 0 means unset */ +typedef uintmax_t mono_microsecs; + +/* "task_queue" - A queue of tasks to be run */ +typedef struct { + struct task_struct *tasks; /* Tasks in this queue */ + size_t length; /* Number of tasks */ + /* Memory allocated for "tasks", in bytes */ + size_t allocated; + /* Time when this queue should be run, at the latest */ + mono_microsecs next_run; +} __attribute__((designated_init)) task_queue; + +/* "func_type" - A function type for task functions + + I.e. functions for the code which runs when a task is run, all have + this type */ +typedef void (task_func) (const struct task_struct, + task_queue *const) + __attribute__((nonnull)); + +/* "buffer" - A data buffer for a growing array of bytes + + Used for the "password" variable */ +typedef struct { + char *data; + size_t length; + size_t allocated; +} __attribute__((designated_init)) buffer; + +/* "string_set" - A set type which can contain strings + + Used by the "cancelled_filenames" variable */ +typedef struct { + char *argz; /* Do not access these except in */ + size_t argz_len; /* the string_set_* functions */ +} __attribute__((designated_init)) string_set; + +/* "task_context" - local variables for tasks + + This data structure distinguishes between different tasks which are + using the same function. This data structure is passed to every + task function when each task is run. + + Note that not every task uses every struct member. */ +typedef struct task_struct { + task_func *const func; /* The function run by this task */ + char *const question_filename; /* The question file */ + const pid_t pid; /* Mandos client process ID */ + const int epoll_fd; /* The epoll set file descriptor */ + bool *const quit_now; /* Set to true on fatal errors */ + const int fd; /* General purpose file descriptor */ + bool *const mandos_client_exited; /* Set true when client exits */ + buffer *const password; /* As read from client process */ + bool *const password_is_read; /* "password" is done growing */ + char *filename; /* General purpose file name */ + /* A set of strings of all the file names of questions which have + been cancelled for any reason; tasks pertaining to these question + files should not be run */ + string_set *const cancelled_filenames; + const mono_microsecs notafter; /* "NotAfter" from question file */ + /* Updated before each queue run; is compared with queue.next_run */ + const mono_microsecs *const current_time; +} __attribute__((designated_init)) task_context; + +/* Declare all our functions here so we can define them in any order + below. Note: test functions are *not* declared here, they are + declared in the test section. */ +__attribute__((warn_unused_result)) +static bool should_only_run_tests(int *, char **[]); +__attribute__((warn_unused_result, cold)) +static bool run_tests(int, char *[]); +static void handle_sigchld(__attribute__((unused)) int sig){} +__attribute__((warn_unused_result, malloc)) +task_queue *create_queue(void); +__attribute__((nonnull, warn_unused_result)) +bool add_to_queue(task_queue *const, const task_context); +__attribute__((nonnull)) +void cleanup_task(const task_context *const); +__attribute__((nonnull)) +void cleanup_queue(task_queue *const *const); +__attribute__((pure, nonnull, warn_unused_result)) +bool queue_has_question(const task_queue *const); +__attribute__((nonnull)) +void cleanup_close(const int *const); +__attribute__((nonnull)) +void cleanup_string(char *const *const); +__attribute__((nonnull)) +void cleanup_buffer(buffer *const); +__attribute__((pure, nonnull, warn_unused_result)) +bool string_set_contains(const string_set, const char *const); +__attribute__((nonnull, warn_unused_result)) +bool string_set_add(string_set *const, const char *const); +__attribute__((nonnull)) +void string_set_clear(string_set *); +void string_set_swap(string_set *const, string_set *const); +__attribute__((nonnull, warn_unused_result)) +bool start_mandos_client(task_queue *const, const int, bool *const, + bool *const, buffer *const, bool *const, + const struct sigaction *const, + const sigset_t, const char *const, + const uid_t, const gid_t, + const char *const *const); +__attribute__((nonnull)) +task_func wait_for_mandos_client_exit; +__attribute__((nonnull)) +task_func read_mandos_client_output; +__attribute__((warn_unused_result)) +bool add_inotify_dir_watch(task_queue *const, const int, bool *const, + buffer *const, const char *const, + string_set *, const mono_microsecs *const, + bool *const, bool *const); +__attribute__((nonnull)) +task_func read_inotify_event; +__attribute__((nonnull)) +task_func open_and_parse_question; +__attribute__((nonnull)) +task_func cancel_old_question; +__attribute__((nonnull)) +task_func connect_question_socket; +__attribute__((nonnull)) +task_func send_password_to_socket; +__attribute__((warn_unused_result)) +bool add_existing_questions(task_queue *const, const int, + buffer *const, string_set *, + const mono_microsecs *const, + bool *const, bool *const, + const char *const); +__attribute__((nonnull, warn_unused_result)) +bool wait_for_event(const int, const mono_microsecs, + const mono_microsecs); +bool run_queue(task_queue **const, string_set *const, bool *const); +bool clear_all_fds_from_epoll_set(const int); +mono_microsecs get_current_time(void); +__attribute__((nonnull, warn_unused_result)) +bool setup_signal_handler(struct sigaction *const); +__attribute__((nonnull)) +bool restore_signal_handler(const struct sigaction *const); +__attribute__((nonnull, warn_unused_result)) +bool block_sigchld(sigset_t *const); +__attribute__((nonnull)) +bool restore_sigmask(const sigset_t *const); +__attribute__((nonnull)) +bool parse_arguments(int, char *[], const bool, char **, char **, + uid_t *const , gid_t *const, char **, size_t *); + +/* End of declarations of private types and functions */ + +/* Start of "main" section; this section LACKS TESTS! + + Code here should be as simple as possible. */ + +/* These are required to be global by Argp */ +const char *argp_program_version = "password-agent " VERSION; +const char *argp_program_bug_address = ""; + +int main(int argc, char *argv[]){ + + /* If the --test option is passed, skip all normal operations and + instead only run the run_tests() function, which also does all + its own option parsing, so we don't have to do anything here. */ + if(should_only_run_tests(&argc, &argv)){ + if(run_tests(argc, argv)){ + return EXIT_SUCCESS; /* All tests successful */ + } + return EXIT_FAILURE; /* Some test(s) failed */ + } + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + + uid_t user = 0; + gid_t group = 0; + + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + if(not parse_arguments(argc, argv, true, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)){ + /* This should never happen, since "true" is passed as the third + argument to parse_arguments() above, which should make + argp_parse() call exit() if any parsing error occurs. */ + error(EX_USAGE, errno, "Failed to parse arguments"); + } + + const char default_agent_directory[] = "/run/systemd/ask-password"; + const char default_helper_directory[] + = "/lib/mandos/plugin-helpers"; + const char *const default_argv[] + = {"/lib/mandos/plugins.d/mandos-client", NULL }; + + /* Set variables to default values if unset */ + if(agent_directory == NULL){ + agent_directory = strdup(default_agent_directory); + if(agent_directory == NULL){ + error(EX_OSERR, errno, "Failed strdup()"); + } + } + if(helper_directory == NULL){ + helper_directory = strdup(default_helper_directory); + if(helper_directory == NULL){ + error(EX_OSERR, errno, "Failed strdup()"); + } + } + if(user == 0){ + user = 65534; /* nobody */ + } + if(group == 0){ + group = 65534; /* nogroup */ + } + /* If parse_opt did not create an argz vector, create one with + default values */ + if(mandos_argz == NULL){ +#ifdef __GNUC__ +#pragma GCC diagnostic push + /* argz_create() takes a non-const argv for some unknown reason - + argz_create() isn't modifying the strings, just copying them. + Therefore, this cast to non-const should be safe. */ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + errno = argz_create((char *const *)default_argv, &mandos_argz, + &mandos_argz_length); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + if(errno != 0){ + error(EX_OSERR, errno, "Failed argz_create()"); + } + } + /* Use argz vector to create a normal argv, usable by execv() */ + + char **mandos_argv = malloc((argz_count(mandos_argz, + mandos_argz_length) + + 1) * sizeof(char *)); + if(mandos_argv == NULL){ + error_t saved_errno = errno; + free(mandos_argz); + error(EX_OSERR, saved_errno, "Failed malloc()"); + } + argz_extract(mandos_argz, mandos_argz_length, mandos_argv); + + sigset_t orig_sigmask; + if(not block_sigchld(&orig_sigmask)){ + return EX_OSERR; + } + + struct sigaction old_sigchld_action; + if(not setup_signal_handler(&old_sigchld_action)){ + return EX_OSERR; + } + + mono_microsecs current_time = 0; + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if(epoll_fd < 0){ + error(EX_OSERR, errno, "Failed to create epoll set fd"); + } + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + if(queue == NULL){ + error(EX_OSERR, errno, "Failed to create task queue"); + } + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + + /* Add tasks to queue */ + if(not start_mandos_client(queue, epoll_fd, &mandos_client_exited, + &quit_now, &password, &password_is_read, + &old_sigchld_action, orig_sigmask, + helper_directory, user, group, + (const char *const *)mandos_argv)){ + return EX_OSERR; /* Error has already been printed */ + } + /* These variables were only for start_mandos_client() and are not + needed anymore */ + free(mandos_argv); + free(mandos_argz); + mandos_argz = NULL; + if(not add_inotify_dir_watch(queue, epoll_fd, &quit_now, &password, + agent_directory, &cancelled_filenames, + ¤t_time, &mandos_client_exited, + &password_is_read)){ + switch(errno){ /* Error has already been printed */ + case EACCES: + case ENAMETOOLONG: + case ENOENT: + return EX_OSFILE; + default: + return EX_OSERR; + } + } + if(not add_existing_questions(queue, epoll_fd, &password, + &cancelled_filenames, ¤t_time, + &mandos_client_exited, + &password_is_read, agent_directory)){ + return EXIT_FAILURE; /* Error has already been printed */ + } + + /* Run queue */ + do { + current_time = get_current_time(); + if(not wait_for_event(epoll_fd, queue->next_run, current_time)){ + const error_t saved_errno = errno; + error(EXIT_FAILURE, saved_errno, "Failure while waiting for" + " events"); + } + + current_time = get_current_time(); + if(not run_queue(&queue, &cancelled_filenames, &quit_now)){ + const error_t saved_errno = errno; + error(EXIT_FAILURE, saved_errno, "Failure while running queue"); + } + + /* When no tasks about questions are left in the queue, break out + of the loop (and implicitly exit the program) */ + } while(queue_has_question(queue)); + + restore_signal_handler(&old_sigchld_action); + restore_sigmask(&orig_sigmask); + + return EXIT_SUCCESS; +} + +__attribute__((warn_unused_result)) +mono_microsecs get_current_time(void){ + struct timespec currtime; + if(clock_gettime(CLOCK_MONOTONIC, &currtime) != 0){ + error(0, errno, "Failed to get current time"); + return 0; + } + return ((mono_microsecs)currtime.tv_sec * 1000000) /* seconds */ + + ((mono_microsecs)currtime.tv_nsec / 1000); /* nanoseconds */ +} + +/* End of "main" section */ + +/* Start of regular code section; ALL this code has tests */ + +__attribute__((nonnull)) +bool parse_arguments(int argc, char *argv[], const bool exit_failure, + char **agent_directory, char **helper_directory, + uid_t *const user, gid_t *const group, + char **mandos_argz, size_t *mandos_argz_length){ + + const struct argp_option options[] = { + { .name="agent-directory",.key='d', .arg="DIRECTORY", + .doc="Systemd password agent directory" }, + { .name="helper-directory",.key=128, .arg="DIRECTORY", + .doc="Mandos Client password helper directory" }, + { .name="plugin-helper-dir", .key=129, /* From plugin-runner */ + .flags=OPTION_HIDDEN | OPTION_ALIAS }, + { .name="user", .key='u', .arg="USERID", + .doc="User ID the Mandos Client will use as its unprivileged" + " user" }, + { .name="userid", .key=130, /* From plugin--runner */ + .flags=OPTION_HIDDEN | OPTION_ALIAS }, + { .name="group", .key='g', .arg="GROUPID", + .doc="Group ID the Mandos Client will use as its unprivileged" + " group" }, + { .name="groupid", .key=131, /* From plugin--runner */ + .flags=OPTION_HIDDEN | OPTION_ALIAS }, + { .name="test", .key=255, /* See should_only_run_tests() */ + .doc="Skip normal operation, and only run self-tests. See" + " --test --help.", .group=10, }, + { NULL }, + }; + + __attribute__((nonnull(3))) + error_t parse_opt(int key, char *arg, struct argp_state *state){ + errno = 0; + switch(key){ + case 'd': /* --agent-directory */ + *agent_directory = strdup(arg); + break; + case 128: /* --helper-directory */ + case 129: /* --plugin-helper-dir */ + *helper_directory = strdup(arg); + break; + case 'u': /* --user */ + case 130: /* --userid */ + { + char *tmp; + uintmax_t tmp_id = 0; + errno = 0; + tmp_id = (uid_t)strtoumax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmp_id != (uid_t)tmp_id or (uid_t)tmp_id == 0){ + return ARGP_ERR_UNKNOWN; + } + *user = (uid_t)tmp_id; + errno = 0; + break; + } + case 'g': /* --group */ + case 131: /* --groupid */ + { + char *tmp; + uintmax_t tmp_id = 0; + errno = 0; + tmp_id = (uid_t)strtoumax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmp_id != (gid_t)tmp_id or (gid_t)tmp_id == 0){ + return ARGP_ERR_UNKNOWN; + } + *group = (gid_t)tmp_id; + errno = 0; + break; + } + case ARGP_KEY_ARGS: + /* Copy arguments into argz vector */ + return argz_create(state->argv + state->next, mandos_argz, + mandos_argz_length); + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + const struct argp argp = { + .options=options, + .parser=parse_opt, + .args_doc="[MANDOS_CLIENT [OPTION...]]\n--test", + .doc = "Mandos password agent -- runs Mandos client as a" + " systemd password agent", + }; + + errno = argp_parse(&argp, argc, argv, + exit_failure ? 0 : ARGP_NO_EXIT, NULL, NULL); + + return errno == 0; +} + +__attribute__((nonnull, warn_unused_result)) +bool block_sigchld(sigset_t *const orig_sigmask){ + sigset_t sigchld_sigmask; + if(sigemptyset(&sigchld_sigmask) < 0){ + error(0, errno, "Failed to empty signal set"); + return false; + } + if(sigaddset(&sigchld_sigmask, SIGCHLD) < 0){ + error(0, errno, "Failed to add SIGCHLD to signal set"); + return false; + } + if(pthread_sigmask(SIG_BLOCK, &sigchld_sigmask, orig_sigmask) != 0){ + error(0, errno, "Failed to block SIGCHLD signal"); + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result, const)) +bool restore_sigmask(const sigset_t *const orig_sigmask){ + if(pthread_sigmask(SIG_SETMASK, orig_sigmask, NULL) != 0){ + error(0, errno, "Failed to restore blocked signals"); + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result)) +bool setup_signal_handler(struct sigaction *const old_sigchld_action){ + struct sigaction sigchld_action = { + .sa_handler=handle_sigchld, + .sa_flags=SA_RESTART | SA_NOCLDSTOP, + }; + /* Set all signals in "sa_mask" struct member; this makes all + signals automatically blocked during signal handler */ + if(sigfillset(&sigchld_action.sa_mask) != 0){ + error(0, errno, "Failed to do sigfillset()"); + return false; + } + if(sigaction(SIGCHLD, &sigchld_action, old_sigchld_action) != 0){ + error(0, errno, "Failed to set SIGCHLD signal handler"); + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result)) +bool restore_signal_handler(const struct sigaction *const + old_sigchld_action){ + if(sigaction(SIGCHLD, old_sigchld_action, NULL) != 0){ + error(0, errno, "Failed to restore signal handler"); + return false; + } + return true; +} + +__attribute__((warn_unused_result, malloc)) +task_queue *create_queue(void){ + task_queue *queue = malloc(sizeof(task_queue)); + if(queue){ + queue->tasks = NULL; + queue->length = 0; + queue->allocated = 0; + queue->next_run = 0; + } + return queue; +} + +__attribute__((nonnull, warn_unused_result)) +bool add_to_queue(task_queue *const queue, const task_context task){ + const size_t needed_size = sizeof(task_context)*(queue->length + 1); + if(needed_size > (queue->allocated)){ + task_context *const new_tasks = realloc(queue->tasks, + needed_size); + if(new_tasks == NULL){ + error(0, errno, "Failed to allocate %" PRIuMAX + " bytes for queue->tasks", (uintmax_t)needed_size); + return false; + } + queue->tasks = new_tasks; + queue->allocated = needed_size; + } + /* Using memcpy here is necessary because doing */ + /* queue->tasks[queue->length++] = task; */ + /* would violate const-ness of task members */ + memcpy(&(queue->tasks[queue->length++]), &task, + sizeof(task_context)); + return true; +} + +__attribute__((nonnull)) +void cleanup_task(const task_context *const task){ + const error_t saved_errno = errno; + /* free and close all task data */ + free(task->question_filename); + if(task->filename != task->question_filename){ + free(task->filename); + } + if(task->pid > 0){ + kill(task->pid, SIGTERM); + } + if(task->fd > 0){ + close(task->fd); + } + errno = saved_errno; +} + +__attribute__((nonnull)) +void free_queue(task_queue *const queue){ + free(queue->tasks); + free(queue); +} + +__attribute__((nonnull)) +void cleanup_queue(task_queue *const *const queue){ + if(*queue == NULL){ + return; + } + for(size_t i = 0; i < (*queue)->length; i++){ + const task_context *const task = ((*queue)->tasks)+i; + cleanup_task(task); + } + free_queue(*queue); +} + +__attribute__((pure, nonnull, warn_unused_result)) +bool queue_has_question(const task_queue *const queue){ + for(size_t i=0; i < queue->length; i++){ + if(queue->tasks[i].question_filename != NULL){ + return true; + } + } + return false; +} + +__attribute__((nonnull)) +void cleanup_close(const int *const fd){ + const error_t saved_errno = errno; + close(*fd); + errno = saved_errno; +} + +__attribute__((nonnull)) +void cleanup_string(char *const *const ptr){ + free(*ptr); +} + +__attribute__((nonnull)) +void cleanup_buffer(buffer *buf){ + if(buf->allocated > 0){ +#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25) + explicit_bzero(buf->data, buf->allocated); +#else + memset(buf->data, '\0', buf->allocated); +#endif + } + if(buf->data != NULL){ + if(munlock(buf->data, buf->allocated) != 0){ + error(0, errno, "Failed to unlock memory of old buffer"); + } + free(buf->data); + buf->data = NULL; + } + buf->length = 0; + buf->allocated = 0; +} + +__attribute__((pure, nonnull, warn_unused_result)) +bool string_set_contains(const string_set set, const char *const str){ + for(const char *s = set.argz; s != NULL and set.argz_len > 0; + s = argz_next(set.argz, set.argz_len, s)){ + if(strcmp(s, str) == 0){ + return true; + } + } + return false; +} + +__attribute__((nonnull, warn_unused_result)) +bool string_set_add(string_set *const set, const char *const str){ + if(string_set_contains(*set, str)){ + return true; + } + error_t error = argz_add(&set->argz, &set->argz_len, str); + if(error == 0){ + return true; + } + errno = error; + return false; +} + +__attribute__((nonnull)) +void string_set_clear(string_set *set){ + free(set->argz); + set->argz = NULL; + set->argz_len = 0; +} + +__attribute__((nonnull)) +void string_set_swap(string_set *const set1, string_set *const set2){ + /* Swap contents of two string sets */ + { + char *const tmp_argz = set1->argz; + set1->argz = set2->argz; + set2->argz = tmp_argz; + } + { + const size_t tmp_argz_len = set1->argz_len; + set1->argz_len = set2->argz_len; + set2->argz_len = tmp_argz_len; + } +} + +__attribute__((nonnull, warn_unused_result)) +bool start_mandos_client(task_queue *const queue, + const int epoll_fd, + bool *const mandos_client_exited, + bool *const quit_now, buffer *const password, + bool *const password_is_read, + const struct sigaction *const + old_sigchld_action, const sigset_t sigmask, + const char *const helper_directory, + const uid_t user, const gid_t group, + const char *const *const argv){ + int pipefds[2]; + if(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK) != 0){ + error(0, errno, "Failed to pipe2(..., O_CLOEXEC | O_NONBLOCK)"); + return false; + } + + const pid_t pid = fork(); + if(pid == 0){ + if(not restore_signal_handler(old_sigchld_action)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&sigmask)){ + _exit(EXIT_FAILURE); + } + if(close(pipefds[0]) != 0){ + error(0, errno, "Failed to close() parent pipe fd"); + _exit(EXIT_FAILURE); + } + if(dup2(pipefds[1], STDOUT_FILENO) == -1){ + error(0, errno, "Failed to dup2() pipe fd to stdout"); + _exit(EXIT_FAILURE); + } + if(close(pipefds[1]) != 0){ + error(0, errno, "Failed to close() old child pipe fd"); + _exit(EXIT_FAILURE); + } + if(setenv("MANDOSPLUGINHELPERDIR", helper_directory, 1) != 0){ + error(0, errno, "Failed to setenv(\"MANDOSPLUGINHELPERDIR\"," + " \"%s\", 1)", helper_directory); + _exit(EXIT_FAILURE); + } + if(group != 0 and setresgid(group, 0, 0) == -1){ + error(0, errno, "Failed to setresgid(-1, %" PRIuMAX ", %" + PRIuMAX")", (uintmax_t)group, (uintmax_t)group); + _exit(EXIT_FAILURE); + } + if(user != 0 and setresuid(user, 0, 0) == -1){ + error(0, errno, "Failed to setresuid(-1, %" PRIuMAX ", %" + PRIuMAX")", (uintmax_t)user, (uintmax_t)user); + _exit(EXIT_FAILURE); + } +#ifdef __GNUC__ +#pragma GCC diagnostic push + /* For historical reasons, the "argv" argument to execv() is not + const, but it is safe to override this. */ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + execv(argv[0], (char **)argv); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + error(0, errno, "execv(\"%s\", ...) failed", argv[0]); + _exit(EXIT_FAILURE); + } + close(pipefds[1]); + + if(not add_to_queue(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=mandos_client_exited, + .quit_now=quit_now, + })){ + error(0, errno, "Failed to add wait_for_mandos_client to queue"); + close(pipefds[0]); + return false; + } + + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipefds[0], + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to add file descriptor to epoll set"); + close(pipefds[0]); + return false; + } + + return add_to_queue(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=quit_now, + .password=password, + .password_is_read=password_is_read, + }); +} + +__attribute__((nonnull)) +void wait_for_mandos_client_exit(const task_context task, + task_queue *const queue){ + const pid_t pid = task.pid; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const quit_now = task.quit_now; + + int status; + switch(waitpid(pid, &status, WNOHANG)){ + case 0: /* Not exited yet */ + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add myself to queue"); + *quit_now = true; + } + break; + case -1: /* Error */ + error(0, errno, "waitpid(%" PRIdMAX ") failed", (intmax_t)pid); + if(errno != ECHILD){ + kill(pid, SIGTERM); + } + *quit_now = true; + break; + default: /* Has exited */ + *mandos_client_exited = true; + if((not WIFEXITED(status)) + or (WEXITSTATUS(status) != EXIT_SUCCESS)){ + error(0, 0, "Mandos client failed or was killed"); + *quit_now = true; + } + } +} + +__attribute__((nonnull)) +void read_mandos_client_output(const task_context task, + task_queue *const queue){ + buffer *const password = task.password; + bool *const quit_now = task.quit_now; + bool *const password_is_read = task.password_is_read; + const int fd = task.fd; + const int epoll_fd = task.epoll_fd; + + const size_t new_potential_size = (password->length + PIPE_BUF); + if(password->allocated < new_potential_size){ + char *const new_buffer = calloc(new_potential_size, 1); + if(new_buffer == NULL){ + error(0, errno, "Failed to allocate %" PRIuMAX + " bytes for password", (uintmax_t)new_potential_size); + *quit_now = true; + close(fd); + return; + } + if(mlock(new_buffer, new_potential_size) != 0){ + /* Warn but do not treat as fatal error */ + if(errno != EPERM and errno != ENOMEM){ + error(0, errno, "Failed to lock memory for password"); + } + } + if(password->length > 0){ + memcpy(new_buffer, password->data, password->length); +#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25) + explicit_bzero(password->data, password->allocated); +#else + memset(password->data, '\0', password->allocated); +#endif + } + if(password->data != NULL){ + if(munlock(password->data, password->allocated) != 0){ + error(0, errno, "Failed to unlock memory of old buffer"); + } + free(password->data); + } + password->data = new_buffer; + password->allocated = new_potential_size; + } + + const ssize_t read_length = read(fd, password->data + + password->length, PIPE_BUF); + + if(read_length == 0){ /* EOF */ + *password_is_read = true; + close(fd); + return; + } + if(read_length < 0 and errno != EAGAIN){ /* Actual error */ + error(0, errno, "Failed to read password from Mandos client"); + *quit_now = true; + close(fd); + return; + } + if(read_length > 0){ /* Data has been read */ + password->length += (size_t)read_length; + } + + /* Either data was read, or EAGAIN was indicated, meaning no data + available yet */ + + /* Re-add the fd to the epoll set */ + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to re-add file descriptor to epoll set"); + *quit_now = true; + close(fd); + return; + } + + /* Re-add myself to the queue */ + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add myself to queue"); + *quit_now = true; + close(fd); + } +} + +__attribute__((nonnull, warn_unused_result)) +bool add_inotify_dir_watch(task_queue *const queue, + const int epoll_fd, bool *const quit_now, + buffer *const password, + const char *const dir, + string_set *cancelled_filenames, + const mono_microsecs *const current_time, + bool *const mandos_client_exited, + bool *const password_is_read){ + const int fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if(fd == -1){ + error(0, errno, "Failed to create inotify instance"); + return false; + } + + if(inotify_add_watch(fd, dir, IN_CLOSE_WRITE + | IN_MOVED_TO | IN_DELETE) + == -1){ + error(0, errno, "Failed to create inotify watch on %s", dir); + return false; + } + + /* Add the inotify fd to the epoll set */ + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to add file descriptor to epoll set"); + close(fd); + return false; + } + + const task_context read_inotify_event_task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .quit_now=quit_now, + .password=password, + .fd=fd, + .filename=strdup(dir), + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + }; + if(read_inotify_event_task.filename == NULL){ + error(0, errno, "Failed to strdup(\"%s\")", dir); + close(fd); + return false; + } + + return add_to_queue(queue, read_inotify_event_task); +} + +__attribute__((nonnull)) +void read_inotify_event(const task_context task, + task_queue *const queue){ + const int fd = task.fd; + const int epoll_fd = task.epoll_fd; + char *const filename = task.filename; + bool *quit_now = task.quit_now; + buffer *const password = task.password; + string_set *const cancelled_filenames = task.cancelled_filenames; + const mono_microsecs *const current_time = task.current_time; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + char ievent_buffer[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ievent = ((struct inotify_event *) + ievent_buffer); + + const ssize_t read_length = read(fd, ievent, ievent_size); + if(read_length == 0){ /* EOF */ + error(0, 0, "Got EOF from inotify fd for directory %s", filename); + *quit_now = true; + cleanup_task(&task); + return; + } + if(read_length < 0 and errno != EAGAIN){ /* Actual error */ + error(0, errno, "Failed to read from inotify fd for directory %s", + filename); + *quit_now = true; + cleanup_task(&task); + return; + } + if(read_length > 0 /* Data has been read */ + and fnmatch("ask.*", ievent->name, FNM_FILE_NAME) == 0){ + char *question_filename = NULL; + const ssize_t question_filename_length + = asprintf(&question_filename, "%s/%s", filename, ievent->name); + if(question_filename_length < 0){ + error(0, errno, "Failed to create file name from directory name" + " %s and file name %s", filename, ievent->name); + } else { + if(ievent->mask & (IN_CLOSE_WRITE | IN_MOVED_TO)){ + if(not add_to_queue(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .question_filename=question_filename, + .filename=question_filename, + .password=password, + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + })){ + error(0, errno, "Failed to add open_and_parse_question task" + " for file name %s to queue", filename); + } else { + /* Force the added task (open_and_parse_question) to run + immediately */ + queue->next_run = 1; + } + } else if(ievent->mask & IN_DELETE){ + if(not string_set_add(cancelled_filenames, + question_filename)){ + error(0, errno, "Could not add question %s to" + " cancelled_questions", question_filename); + *quit_now = true; + free(question_filename); + cleanup_task(&task); + return; + } + free(question_filename); + } + } + } + + /* Either data was read, or EAGAIN was indicated, meaning no data + available yet */ + + /* Re-add myself to the queue */ + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to re-add read_inotify_event(%s) to" + " queue", filename); + *quit_now = true; + cleanup_task(&task); + return; + } + + /* Re-add the fd to the epoll set */ + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to re-add inotify file descriptor %d for" + " directory %s to epoll set", fd, filename); + /* Force the added task (read_inotify_event) to run again, at most + one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } +} + +__attribute__((nonnull)) +void open_and_parse_question(const task_context task, + task_queue *const queue){ + __attribute__((cleanup(cleanup_string))) + char *question_filename = task.question_filename; + const int epoll_fd = task.epoll_fd; + buffer *const password = task.password; + string_set *const cancelled_filenames = task.cancelled_filenames; + const mono_microsecs *const current_time = task.current_time; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + + /* We use the GLib "Key-value file parser" functions to parse the + question file. See for specification of contents */ + __attribute__((nonnull)) + void cleanup_g_key_file(GKeyFile **key_file){ + if(*key_file != NULL){ + g_key_file_free(*key_file); + } + } + + __attribute__((cleanup(cleanup_g_key_file))) + GKeyFile *key_file = g_key_file_new(); + if(key_file == NULL){ + error(0, errno, "Failed g_key_file_new() for \"%s\"", + question_filename); + return; + } + GError *glib_error = NULL; + if(g_key_file_load_from_file(key_file, question_filename, + G_KEY_FILE_NONE, &glib_error) != TRUE){ + /* If a file was removed, we should ignore it, so */ + /* only show error message if file actually existed */ + if(glib_error->code != G_FILE_ERROR_NOENT){ + error(0, 0, "Failed to load question data from file \"%s\": %s", + question_filename, glib_error->message); + } + return; + } + + __attribute__((cleanup(cleanup_string))) + char *socket_name = g_key_file_get_string(key_file, "Ask", + "Socket", + &glib_error); + if(socket_name == NULL){ + error(0, 0, "Question file \"%s\" did not contain \"Socket\": %s", + question_filename, glib_error->message); + return; + } + + if(strlen(socket_name) == 0){ + error(0, 0, "Question file \"%s\" had empty \"Socket\" value", + question_filename); + return; + } + + const guint64 pid = g_key_file_get_uint64(key_file, "Ask", "PID", + &glib_error); + if(glib_error != NULL){ + error(0, 0, "Question file \"%s\" contained bad \"PID\": %s", + question_filename, glib_error->message); + return; + } + + if((pid != (guint64)((pid_t)pid)) + or (kill((pid_t)pid, 0) != 0)){ + error(0, 0, "PID %" PRIuMAX " in question file \"%s\" is bad or" + " does not exist", (uintmax_t)pid, question_filename); + return; + } + + guint64 notafter = g_key_file_get_uint64(key_file, "Ask", + "NotAfter", &glib_error); + if(glib_error != NULL){ + if(glib_error->code != G_KEY_FILE_ERROR_KEY_NOT_FOUND){ + error(0, 0, "Question file \"%s\" contained bad \"NotAfter\":" + " %s", question_filename, glib_error->message); + } + notafter = 0; + } + if(notafter != 0){ + if(queue->next_run == 0 or (queue->next_run > notafter)){ + queue->next_run = notafter; + } + if(*current_time >= notafter){ + return; + } + } + + const task_context connect_question_socket_task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=password, + .filename=strdup(socket_name), + .cancelled_filenames=task.cancelled_filenames, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + .current_time=current_time, + }; + if(connect_question_socket_task.question_filename == NULL + or connect_question_socket_task.filename == NULL + or not add_to_queue(queue, connect_question_socket_task)){ + error(0, errno, "Failed to add connect_question_socket for socket" + " %s (from \"%s\") to queue", socket_name, + question_filename); + cleanup_task(&connect_question_socket_task); + return; + } + /* Force the added task (connect_question_socket) to run + immediately */ + queue->next_run = 1; + + if(notafter > 0){ + char *const dup_filename = strdup(question_filename); + const task_context cancel_old_question_task = { + .func=cancel_old_question, + .question_filename=dup_filename, + .notafter=notafter, + .filename=dup_filename, + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + }; + if(cancel_old_question_task.question_filename == NULL + or not add_to_queue(queue, cancel_old_question_task)){ + error(0, errno, "Failed to add cancel_old_question for file " + "\"%s\" to queue", question_filename); + cleanup_task(&cancel_old_question_task); + return; + } + } +} + +__attribute__((nonnull)) +void cancel_old_question(const task_context task, + task_queue *const queue){ + char *const question_filename = task.question_filename; + string_set *const cancelled_filenames = task.cancelled_filenames; + const mono_microsecs notafter = task.notafter; + const mono_microsecs *const current_time = task.current_time; + + if(*current_time >= notafter){ + if(not string_set_add(cancelled_filenames, question_filename)){ + error(0, errno, "Failed to cancel question for file %s", + question_filename); + } + cleanup_task(&task); + return; + } + + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add cancel_old_question for file " + "%s to queue", question_filename); + cleanup_task(&task); + return; + } + + if((queue->next_run == 0) or (queue->next_run > notafter)){ + queue->next_run = notafter; + } +} + +__attribute__((nonnull)) +void connect_question_socket(const task_context task, + task_queue *const queue){ + char *const question_filename = task.question_filename; + char *const filename = task.filename; + const int epoll_fd = task.epoll_fd; + buffer *const password = task.password; + string_set *const cancelled_filenames = task.cancelled_filenames; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + const mono_microsecs *const current_time = task.current_time; + + struct sockaddr_un sock_name = { .sun_family=AF_LOCAL }; + + if(sizeof(sock_name.sun_path) <= strlen(filename)){ + error(0, 0, "Socket filename is larger than" + " sizeof(sockaddr_un.sun_path); %" PRIuMAX ": \"%s\"", + (uintmax_t)sizeof(sock_name.sun_path), filename); + if(not string_set_add(cancelled_filenames, question_filename)){ + error(0, errno, "Failed to cancel question for file %s", + question_filename); + } + cleanup_task(&task); + return; + } + + const int fd = socket(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if(fd < 0){ + error(0, errno, + "Failed to create socket(PF_LOCAL, SOCK_DGRAM, 0)"); + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add connect_question_socket for file" + " \"%s\" and socket \"%s\" to queue", question_filename, + filename); + cleanup_task(&task); + } else { + /* Force the added task (connect_question_socket) to run + immediately */ + queue->next_run = 1; + } + return; + } + + strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path)); + if(connect(fd, (struct sockaddr *)&sock_name, + (socklen_t)SUN_LEN(&sock_name)) != 0){ + error(0, errno, "Failed to connect socket to \"%s\"", filename); + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add connect_question_socket for file" + " \"%s\" and socket \"%s\" to queue", question_filename, + filename); + cleanup_task(&task); + } else { + /* Force the added task (connect_question_socket) to run again, + at most one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } + return; + } + + /* Not necessary, but we can try, and merely warn on failure */ + if(shutdown(fd, SHUT_RD) != 0){ + error(0, errno, "Failed to shutdown reading from socket \"%s\"", + filename); + } + + /* Add the fd to the epoll set */ + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event){ .events=EPOLLOUT }) + != 0){ + error(0, errno, "Failed to add inotify file descriptor %d for" + " socket %s to epoll set", fd, filename); + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add connect_question_socket for file" + " \"%s\" and socket \"%s\" to queue", question_filename, + filename); + cleanup_task(&task); + } else { + /* Force the added task (connect_question_socket) to run again, + at most one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } + return; + } + + /* add task send_password_to_socket to queue */ + const task_context send_password_to_socket_task = { + .func=send_password_to_socket, + .question_filename=question_filename, + .filename=filename, + .epoll_fd=epoll_fd, + .fd=fd, + .password=password, + .cancelled_filenames=cancelled_filenames, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + .current_time=current_time, + }; + + if(not add_to_queue(queue, send_password_to_socket_task)){ + error(0, errno, "Failed to add send_password_to_socket for" + " file \"%s\" and socket \"%s\" to queue", + question_filename, filename); + cleanup_task(&send_password_to_socket_task); + } +} + +__attribute__((nonnull)) +void send_password_to_socket(const task_context task, + task_queue *const queue){ + char *const question_filename=task.question_filename; + char *const filename=task.filename; + const int epoll_fd=task.epoll_fd; + const int fd=task.fd; + buffer *const password=task.password; + string_set *const cancelled_filenames=task.cancelled_filenames; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + const mono_microsecs *const current_time = task.current_time; + + if(*mandos_client_exited and *password_is_read){ + + const size_t send_buffer_length = password->length + 2; + char *send_buffer = malloc(send_buffer_length); + if(send_buffer == NULL){ + error(0, errno, "Failed to allocate send_buffer"); + } else { + if(mlock(send_buffer, send_buffer_length) != 0){ + /* Warn but do not treat as fatal error */ + if(errno != EPERM and errno != ENOMEM){ + error(0, errno, "Failed to lock memory for password" + " buffer"); + } + } + /* “[…] send a single datagram to the socket consisting of the + password string either prefixed with "+" or with "-" + depending on whether the password entry was successful or + not. You may but don't have to include a final NUL byte in + your message. + + — (Wed 08 Oct 2014 02:14:28 AM UTC) + */ + send_buffer[0] = '+'; /* Prefix with "+" */ + /* Always add an extra NUL */ + send_buffer[password->length + 1] = '\0'; + if(password->length > 0){ + memcpy(send_buffer + 1, password->data, password->length); + } + errno = 0; + ssize_t ssret = send(fd, send_buffer, send_buffer_length, + MSG_NOSIGNAL); + const error_t saved_errno = errno; +#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25) + explicit_bzero(send_buffer, send_buffer_length); +#else + memset(send_buffer, '\0', send_buffer_length); +#endif + if(munlock(send_buffer, send_buffer_length) != 0){ + error(0, errno, "Failed to unlock memory of send buffer"); + } + free(send_buffer); + if(ssret < 0 or ssret < (ssize_t)send_buffer_length){ + switch(saved_errno){ + case EINTR: + case ENOBUFS: + case ENOMEM: + case EADDRINUSE: + case ECONNREFUSED: + case ECONNRESET: + case ENOENT: + case ETOOMANYREFS: + case EAGAIN: + /* Retry, below */ + break; + case EMSGSIZE: + error(0, 0, "Password of size %" PRIuMAX " is too big", + (uintmax_t)password->length); +#if __GNUC__ < 7 + /* FALLTHROUGH */ +#else + __attribute__((fallthrough)); +#endif + case 0: + if(ssret >= 0 and ssret < (ssize_t)send_buffer_length){ + error(0, 0, "Password only partially sent to socket"); + } +#if __GNUC__ < 7 + /* FALLTHROUGH */ +#else + __attribute__((fallthrough)); +#endif + default: + error(0, saved_errno, "Failed to send() to socket %s", + filename); + if(not string_set_add(cancelled_filenames, + question_filename)){ + error(0, errno, "Failed to cancel question for file %s", + question_filename); + } + cleanup_task(&task); + return; + } + } else { + /* Success */ + cleanup_task(&task); + return; + } + } + } + + /* We failed or are not ready yet; retry later */ + + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add send_password_to_socket for" + " file %s and socket %s to queue", question_filename, + filename); + cleanup_task(&task); + } + + /* Add the fd to the epoll set */ + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event){ .events=EPOLLOUT }) + != 0){ + error(0, errno, "Failed to add socket file descriptor %d for" + " socket %s to epoll set", fd, filename); + /* Force the added task (send_password_to_socket) to run again, at + most one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } +} + +__attribute__((warn_unused_result)) +bool add_existing_questions(task_queue *const queue, + const int epoll_fd, + buffer *const password, + string_set *cancelled_filenames, + const mono_microsecs *const current_time, + bool *const mandos_client_exited, + bool *const password_is_read, + const char *const dirname){ + __attribute__((cleanup(cleanup_string))) + char *dir_pattern = NULL; + const int ret = asprintf(&dir_pattern, "%s/ask.*", dirname); + if(ret < 0 or dir_pattern == NULL){ + error(0, errno, "Could not create glob pattern for directory %s", + dirname); + return false; + } + __attribute__((cleanup(globfree))) + glob_t question_filenames = {}; + switch(glob(dir_pattern, GLOB_ERR | GLOB_NOSORT | GLOB_MARK, + NULL, &question_filenames)){ + case GLOB_ABORTED: + default: + error(0, errno, "Failed to open directory %s", dirname); + return false; + case GLOB_NOMATCH: + error(0, errno, "There are no question files in %s", dirname); + return false; + case GLOB_NOSPACE: + error(0, errno, "Could not allocate memory for question file" + " names in %s", dirname); +#if __GNUC__ < 7 + /* FALLTHROUGH */ +#else + __attribute__((fallthrough)); +#endif + case 0: + for(size_t i = 0; i < question_filenames.gl_pathc; i++){ + char *const question_filename = strdup(question_filenames + .gl_pathv[i]); + const task_context task = { + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .question_filename=question_filename, + .filename=question_filename, + .password=password, + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + }; + + if(question_filename == NULL + or not add_to_queue(queue, task)){ + error(0, errno, "Failed to add open_and_parse_question for" + " file %s to queue", + question_filenames.gl_pathv[i]); + free(question_filename); + } else { + queue->next_run = 1; + } + } + return true; + } +} + +__attribute__((nonnull, warn_unused_result)) +bool wait_for_event(const int epoll_fd, + const mono_microsecs queue_next_run, + const mono_microsecs current_time){ + __attribute__((const)) + int milliseconds_to_wait(const mono_microsecs currtime, + const mono_microsecs nextrun){ + if(currtime >= nextrun){ + return 0; + } + const uintmax_t wait_time_ms = (nextrun - currtime) / 1000; + if(wait_time_ms > (uintmax_t)INT_MAX){ + return INT_MAX; + } + return (int)wait_time_ms; + } + + const int wait_time_ms = milliseconds_to_wait(current_time, + queue_next_run); + + /* Prepare unblocking of SIGCHLD during epoll_pwait */ + sigset_t temporary_unblocked_sigmask; + /* Get current signal mask */ + if(pthread_sigmask(-1, NULL, &temporary_unblocked_sigmask) != 0){ + return false; + } + /* Remove SIGCHLD from the signal mask */ + if(sigdelset(&temporary_unblocked_sigmask, SIGCHLD) != 0){ + return false; + } + struct epoll_event events[8]; /* Ignored */ + int ret = epoll_pwait(epoll_fd, events, + sizeof(events) / sizeof(struct epoll_event), + queue_next_run == 0 ? -1 : (int)wait_time_ms, + &temporary_unblocked_sigmask); + if(ret < 0 and errno != EINTR){ + error(0, errno, "Failed epoll_pwait(epfd=%d, ..., timeout=%d," + " ...", epoll_fd, + queue_next_run == 0 ? -1 : (int)wait_time_ms); + return false; + } + return clear_all_fds_from_epoll_set(epoll_fd); +} + +bool clear_all_fds_from_epoll_set(const int epoll_fd){ + /* Create a new empty epoll set */ + __attribute__((cleanup(cleanup_close))) + const int new_epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if(new_epoll_fd < 0){ + return false; + } + /* dup3() the new epoll set fd over the old one, replacing it */ + if(dup3(new_epoll_fd, epoll_fd, O_CLOEXEC) < 0){ + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result)) +bool run_queue(task_queue **const queue, + string_set *const cancelled_filenames, + bool *const quit_now){ + + task_queue *new_queue = create_queue(); + if(new_queue == NULL){ + return false; + } + + __attribute__((cleanup(string_set_clear))) + string_set old_cancelled_filenames = {}; + string_set_swap(cancelled_filenames, &old_cancelled_filenames); + + /* Declare i outside the for loop, since we might need i after the + loop in case we aborted in the middle */ + size_t i; + for(i=0; i < (*queue)->length and not *quit_now; i++){ + task_context *const task = &((*queue)->tasks[i]); + const char *const question_filename = task->question_filename; + /* Skip any task referencing a cancelled question filename */ + if(question_filename != NULL + and string_set_contains(old_cancelled_filenames, + question_filename)){ + cleanup_task(task); + continue; + } + task->func(*task, new_queue); + } + + if(*quit_now){ + /* we might be in the middle of the queue, so clean up any + remaining tasks in the current queue */ + for(; i < (*queue)->length; i++){ + cleanup_task(&((*queue)->tasks[i])); + } + free_queue(*queue); + *queue = new_queue; + new_queue = NULL; + return false; + } + free_queue(*queue); + *queue = new_queue; + new_queue = NULL; + + return true; +} + +/* End of regular code section */ + +/* Start of tests section; here are the tests for the above code */ + +/* This "fixture" data structure is used by the test setup and + teardown functions */ +typedef struct { + struct sigaction orig_sigaction; + sigset_t orig_sigmask; +} test_fixture; + +static void test_setup(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + g_assert_true(setup_signal_handler(&fixture->orig_sigaction)); + g_assert_true(block_sigchld(&fixture->orig_sigmask)); +} + +static void test_teardown(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + g_assert_true(restore_signal_handler(&fixture->orig_sigaction)); + g_assert_true(restore_sigmask(&fixture->orig_sigmask)); +} + +/* Utility function used by tests to search queue for matching task */ +__attribute__((pure, nonnull, warn_unused_result)) +static task_context *find_matching_task(const task_queue *const queue, + const task_context task){ + /* The argument "task" structure is a pattern to match; 0 in any + member means any value matches, otherwise the value must match. + The filename strings are compared by strcmp(), not by pointer. */ + for(size_t i = 0; i < queue->length; i++){ + task_context *const current_task = queue->tasks+i; + /* Check all members of task_context, if set to a non-zero value. + If a member does not match, continue to next task in queue */ + + /* task_func *const func */ + if(task.func != NULL and current_task->func != task.func){ + continue; + } + /* char *const question_filename; */ + if(task.question_filename != NULL + and (current_task->question_filename == NULL + or strcmp(current_task->question_filename, + task.question_filename) != 0)){ + continue; + } + /* const pid_t pid; */ + if(task.pid != 0 and current_task->pid != task.pid){ + continue; + } + /* const int epoll_fd; */ + if(task.epoll_fd != 0 + and current_task->epoll_fd != task.epoll_fd){ + continue; + } + /* bool *const quit_now; */ + if(task.quit_now != NULL + and current_task->quit_now != task.quit_now){ + continue; + } + /* const int fd; */ + if(task.fd != 0 and current_task->fd != task.fd){ + continue; + } + /* bool *const mandos_client_exited; */ + if(task.mandos_client_exited != NULL + and current_task->mandos_client_exited + != task.mandos_client_exited){ + continue; + } + /* buffer *const password; */ + if(task.password != NULL + and current_task->password != task.password){ + continue; + } + /* bool *const password_is_read; */ + if(task.password_is_read != NULL + and current_task->password_is_read != task.password_is_read){ + continue; + } + /* char *filename; */ + if(task.filename != NULL + and (current_task->filename == NULL + or strcmp(current_task->filename, task.filename) != 0)){ + continue; + } + /* string_set *const cancelled_filenames; */ + if(task.cancelled_filenames != NULL + and current_task->cancelled_filenames + != task.cancelled_filenames){ + continue; + } + /* const mono_microsecs notafter; */ + if(task.notafter != 0 + and current_task->notafter != task.notafter){ + continue; + } + /* const mono_microsecs *const current_time; */ + if(task.current_time != NULL + and current_task->current_time != task.current_time){ + continue; + } + /* Current task matches all members; return it */ + return current_task; + } + /* No task in queue matches passed pattern task */ + return NULL; +} + +static void test_create_queue(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *const queue = create_queue(); + g_assert_nonnull(queue); + g_assert_null(queue->tasks); + g_assert_true(queue->length == 0); + g_assert_true(queue->next_run == 0); +} + +static task_func dummy_func; + +static void test_add_to_queue(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(add_to_queue(queue, + (task_context){ .func=dummy_func })); + g_assert_true(queue->length == 1); + g_assert_nonnull(queue->tasks); + g_assert_true(queue->tasks[0].func == dummy_func); +} + +static void dummy_func(__attribute__((unused)) + const task_context task, + __attribute__((unused)) + task_queue *const queue){ +} + +static void test_queue_has_question_empty(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_false(queue_has_question(queue)); +} + +static void test_queue_has_question_false(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_true(add_to_queue(queue, + (task_context){ .func=dummy_func })); + g_assert_false(queue_has_question(queue)); +} + +static void test_queue_has_question_true(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + char *const question_filename + = strdup("/nonexistent/question_filename"); + g_assert_nonnull(question_filename); + task_context task = { + .func=dummy_func, + .question_filename=question_filename, + }; + g_assert_true(add_to_queue(queue, task)); + g_assert_true(queue_has_question(queue)); +} + +static void test_queue_has_question_false2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + task_context task = { .func=dummy_func }; + g_assert_true(add_to_queue(queue, task)); + g_assert_true(add_to_queue(queue, task)); + g_assert_cmpint((int)queue->length, ==, 2); + g_assert_false(queue_has_question(queue)); +} + +static void test_queue_has_question_true2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + task_context task1 = { .func=dummy_func }; + g_assert_true(add_to_queue(queue, task1)); + char *const question_filename + = strdup("/nonexistent/question_filename"); + g_assert_nonnull(question_filename); + task_context task2 = { + .func=dummy_func, + .question_filename=question_filename, + }; + g_assert_true(add_to_queue(queue, task2)); + g_assert_cmpint((int)queue->length, ==, 2); + g_assert_true(queue_has_question(queue)); +} + +static void test_cleanup_buffer(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + buffer buf = {}; + + const size_t buffersize = 10; + + buf.data = malloc(buffersize); + g_assert_nonnull(buf.data); + if(mlock(buf.data, buffersize) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + + cleanup_buffer(&buf); + g_assert_null(buf.data); +} + +static +void test_string_set_new_set_contains_nothing(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set = {}; + g_assert_false(string_set_contains(set, "")); /* Empty string */ + g_assert_false(string_set_contains(set, "test_string")); +} + +static void +test_string_set_with_added_string_contains_it(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set = {}; + g_assert_true(string_set_add(&set, "test_string")); + g_assert_true(string_set_contains(set, "test_string")); +} + +static void +test_string_set_cleared_does_not_contain_str(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set = {}; + g_assert_true(string_set_add(&set, "test_string")); + string_set_clear(&set); + g_assert_false(string_set_contains(set, "test_string")); +} + +static +void test_string_set_swap_one_with_empty(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set1 = {}; + __attribute__((cleanup(string_set_clear))) + string_set set2 = {}; + g_assert_true(string_set_add(&set1, "test_string1")); + string_set_swap(&set1, &set2); + g_assert_false(string_set_contains(set1, "test_string1")); + g_assert_true(string_set_contains(set2, "test_string1")); +} + +static +void test_string_set_swap_empty_with_one(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set1 = {}; + __attribute__((cleanup(string_set_clear))) + string_set set2 = {}; + g_assert_true(string_set_add(&set2, "test_string2")); + string_set_swap(&set1, &set2); + g_assert_true(string_set_contains(set1, "test_string2")); + g_assert_false(string_set_contains(set2, "test_string2")); +} + +static void test_string_set_swap_one_with_one(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set1 = {}; + __attribute__((cleanup(string_set_clear))) + string_set set2 = {}; + g_assert_true(string_set_add(&set1, "test_string1")); + g_assert_true(string_set_add(&set2, "test_string2")); + string_set_swap(&set1, &set2); + g_assert_false(string_set_contains(set1, "test_string1")); + g_assert_true(string_set_contains(set1, "test_string2")); + g_assert_false(string_set_contains(set2, "test_string2")); + g_assert_true(string_set_contains(set2, "test_string1")); +} + +static bool fd_has_cloexec_and_nonblock(const int); + +static bool epoll_set_contains(int, int, uint32_t); + +static void test_start_mandos_client(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/bin/true", NULL }; + + g_assert_true(start_mandos_client(queue, epoll_fd, + &mandos_client_exited, &quit_now, + &password, &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, argv)); + + g_assert_cmpuint((unsigned int)queue->length, >=, 2); + + const task_context *const added_wait_task + = find_matching_task(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }); + g_assert_nonnull(added_wait_task); + g_assert_cmpint(added_wait_task->pid, >, 0); + g_assert_cmpint(kill(added_wait_task->pid, SIGKILL), ==, 0); + waitpid(added_wait_task->pid, NULL, 0); + + const task_context *const added_read_task + = find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }); + g_assert_nonnull(added_read_task); + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + g_assert_true(epoll_set_contains(epoll_fd, added_read_task->fd, + EPOLLIN | EPOLLRDHUP)); +} + +static bool fd_has_cloexec_and_nonblock(const int fd){ + const int socket_fd_flags = fcntl(fd, F_GETFD, 0); + const int socket_file_flags = fcntl(fd, F_GETFL, 0); + return ((socket_fd_flags >= 0) + and (socket_fd_flags & FD_CLOEXEC) + and (socket_file_flags >= 0) + and (socket_file_flags & O_NONBLOCK)); +} + +__attribute__((const)) +bool is_privileged(void){ + uid_t user = getuid() + 1; + if(user == 0){ /* Overflow check */ + user++; + } + gid_t group = getuid() + 1; + if(group == 0){ /* Overflow check */ + group++; + } + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(setresgid((uid_t)-1, group, group) == -1){ + if(errno != EPERM){ + error(EXIT_FAILURE, errno, "Failed to setresgid(-1, %" PRIuMAX + ", %" PRIuMAX")", (uintmax_t)group, (uintmax_t)group); + } + exit(EXIT_FAILURE); + } + if(setresuid((uid_t)-1, user, user) == -1){ + if(errno != EPERM){ + error(EXIT_FAILURE, errno, "Failed to setresuid(-1, %" PRIuMAX + ", %" PRIuMAX")", (uintmax_t)user, (uintmax_t)user); + } + exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); + } + int status; + waitpid(pid, &status, 0); + if(WIFEXITED(status) and (WEXITSTATUS(status) == EXIT_SUCCESS)){ + return true; + } + return false; +} + +static bool epoll_set_contains(int epoll_fd, int fd, uint32_t events){ + /* Only scan for events in this eventmask */ + const uint32_t eventmask = EPOLLIN | EPOLLOUT | EPOLLRDHUP; + __attribute__((cleanup(cleanup_string))) + char *fdinfo_name = NULL; + int ret = asprintf(&fdinfo_name, "/proc/self/fdinfo/%d", epoll_fd); + g_assert_cmpint(ret, >, 0); + g_assert_nonnull(fdinfo_name); + + FILE *fdinfo = fopen(fdinfo_name, "r"); + g_assert_nonnull(fdinfo); + uint32_t reported_events; + buffer line = {}; + int found_fd = -1; + + do { + if(getline(&line.data, &line.allocated, fdinfo) < 0){ + break; + } + /* See proc(5) for format of /proc/PID/fdinfo/FD for epoll fd's */ + if(sscanf(line.data, "tfd: %d events: %" SCNx32 " ", + &found_fd, &reported_events) == 2){ + if(found_fd == fd){ + break; + } + } + } while(not feof(fdinfo) and not ferror(fdinfo)); + g_assert_cmpint(fclose(fdinfo), ==, 0); + free(line.data); + if(found_fd != fd){ + return false; + } + + if(events == 0){ + /* Don't check events if none are given */ + return true; + } + return (reported_events & eventmask) == (events & eventmask); +} + +static void test_start_mandos_client_execv(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + const char helper_directory[] = "/nonexistent"; + /* Can't execv("/", ...), so this should fail */ + const char *const argv[] = { "/", NULL }; + + { + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", O_WRONLY | O_CLOEXEC); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + dup2(devnull_fd, STDERR_FILENO); + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, + &password, + (bool[]){false}, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, + argv); + dup2(real_stderr_fd, STDERR_FILENO); + g_assert_true(success); + } + g_assert_cmpuint((unsigned int)queue->length, ==, 2); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + + { + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + dup2(devnull_fd, STDERR_FILENO); + const bool success = run_queue(&queue, &cancelled_filenames, + &quit_now); + dup2(real_stderr_fd, STDERR_FILENO); + if(not success){ + break; + } + } + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); +} + +static void test_start_mandos_client_suid_euid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--user", NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((uid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, 0); +} + +static void test_start_mandos_client_suid_egid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--group", NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((gid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, 0); +} + +static void test_start_mandos_client_suid_ruid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--user", "--real", + NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((uid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, user); +} + +static void test_start_mandos_client_suid_rgid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--group", "--real", + NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((gid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, group); +} + +static void test_start_mandos_client_read(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char dummy_test_password[] = "dummy test password"; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/bin/echo", "-n", dummy_test_password, + NULL }; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, + argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_cmpint((int)password.length, ==, + sizeof(dummy_test_password)-1); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(dummy_test_password, password.data, + sizeof(dummy_test_password)-1), ==, 0); +} + +static +void test_start_mandos_client_helper_directory(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/bin/sh", "-c", + "echo -n ${MANDOSPLUGINHELPERDIR}", NULL }; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, + argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_cmpint((int)password.length, ==, + sizeof(helper_directory)-1); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(helper_directory, password.data, + sizeof(helper_directory)-1), ==, 0); +} + +__attribute__((nonnull, warn_unused_result)) +static bool proc_status_sigblk_to_sigset(const char *const, + sigset_t *const); + +static void test_start_mandos_client_sigmask(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + /* see proc(5) for format of /proc/self/status */ + const char *const argv[] = { "/usr/bin/awk", + "$1==\"SigBlk:\"{ print $2 }", "/proc/self/status", NULL }; + + g_assert_true(start_mandos_client(queue, epoll_fd, + &mandos_client_exited, &quit_now, + &password, &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, argv)); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not (mandos_client_exited and password_is_read)) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + g_assert_true(mandos_client_exited); + g_assert_true(password_is_read); + + sigset_t parsed_sigmask; + g_assert_true(proc_status_sigblk_to_sigset(password.data, + &parsed_sigmask)); + + for(int signum = 1; signum < NSIG; signum++){ + const bool has_signal = sigismember(&parsed_sigmask, signum); + if(sigismember(&fixture->orig_sigmask, signum)){ + g_assert_true(has_signal); + } else { + g_assert_false(has_signal); + } + } +} + +__attribute__((nonnull, warn_unused_result)) +static bool proc_status_sigblk_to_sigset(const char *const sigblk, + sigset_t *const sigmask){ + /* parse /proc/PID/status SigBlk value and convert to a sigset_t */ + uintmax_t scanned_sigmask; + if(sscanf(sigblk, "%" SCNxMAX " ", &scanned_sigmask) != 1){ + return false; + } + if(sigemptyset(sigmask) != 0){ + return false; + } + for(int signum = 1; signum < NSIG; signum++){ + if(scanned_sigmask & ((uintmax_t)1 << (signum-1))){ + if(sigaddset(sigmask, signum) != 0){ + return false; + } + } + } + return true; +} + +static void run_task_with_stderr_to_dev_null(const task_context task, + task_queue *const queue); + +static +void test_wait_for_mandos_client_exit_badpid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + + bool mandos_client_exited = false; + bool quit_now = false; + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const task_context task = { + .func=wait_for_mandos_client_exit, + .pid=1, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }; + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_false(mandos_client_exited); + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void run_task_with_stderr_to_dev_null(const task_context task, + task_queue *const queue){ + FILE *real_stderr = stderr; + FILE *devnull = fopen("/dev/null", "we"); + g_assert_nonnull(devnull); + + stderr = devnull; + task.func(task, queue); + stderr = real_stderr; + + g_assert_cmpint(fclose(devnull), ==, 0); +} + +static +void test_wait_for_mandos_client_exit_noexit(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_eternal_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + while(true){ + pause(); + } + } + return pid; + } + pid_t pid = create_eternal_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const task_context task = { + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(mandos_client_exited); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=task.pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + })); +} + +static +void test_wait_for_mandos_client_exit_success(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_successful_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); + } + return pid; + } + const pid_t pid = create_successful_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const task_context initial_task = { + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }; + g_assert_true(add_to_queue(queue, initial_task)); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + do { + queue->next_run = 0; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, (string_set[]){{}}, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not mandos_client_exited) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(mandos_client_exited); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static +void test_wait_for_mandos_client_exit_failure(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_failing_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + exit(EXIT_FAILURE); + } + return pid; + } + const pid_t pid = create_failing_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_true(add_to_queue(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + })); + + g_assert_true(sigismember(&fixture->orig_sigmask, SIGCHLD) == 0); + + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + dup2(devnull_fd, STDERR_FILENO); + const bool success = run_queue(&queue, &cancelled_filenames, + &quit_now); + dup2(real_stderr_fd, STDERR_FILENO); + if(not success){ + break; + } + + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not mandos_client_exited) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(quit_now); + g_assert_true(mandos_client_exited); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static +void test_wait_for_mandos_client_exit_killed(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_killed_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + while(true){ + pause(); + } + } + kill(pid, SIGKILL); + return pid; + } + const pid_t pid = create_killed_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_true(add_to_queue(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + })); + + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + dup2(devnull_fd, STDERR_FILENO); + const bool success = run_queue(&queue, &cancelled_filenames, + &quit_now); + dup2(real_stderr_fd, STDERR_FILENO); + if(not success){ + break; + } + + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not mandos_client_exited) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(mandos_client_exited); + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static bool epoll_set_does_not_contain(int, int); + +static +void test_read_mandos_client_output_readerror(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + /* Reading /proc/self/mem from offset 0 will always give EIO */ + const int fd = open("/proc/self/mem", O_RDONLY | O_CLOEXEC); + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=fd, + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, 0); + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, fd)); + + g_assert_cmpint(close(fd), ==, -1); +} + +static bool epoll_set_does_not_contain(int epoll_fd, int fd){ + return not epoll_set_contains(epoll_fd, fd, 0); +} + +static +void test_read_mandos_client_output_nodata(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, 0); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static void test_read_mandos_client_output_eof(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + g_assert_true(password_is_read); + g_assert_cmpint((int)password.length, ==, 0); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, pipefds[0])); + + g_assert_cmpint(close(pipefds[0]), ==, -1); +} + +static +void test_read_mandos_client_output_once(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + const char dummy_test_password[] = "dummy test password"; + /* Start with a pre-allocated buffer */ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(sizeof(dummy_test_password)), + .length=0, + .allocated=sizeof(dummy_test_password), + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF); + g_assert_cmpint((int)write(pipefds[1], dummy_test_password, + sizeof(dummy_test_password)), + ==, (int)sizeof(dummy_test_password)); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, + (int)sizeof(dummy_test_password)); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(password.data, dummy_test_password, + sizeof(dummy_test_password)), ==, 0); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static +void test_read_mandos_client_output_malloc(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + const char dummy_test_password[] = "dummy test password"; + /* Start with an empty buffer */ + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF); + g_assert_cmpint((int)write(pipefds[1], dummy_test_password, + sizeof(dummy_test_password)), + ==, (int)sizeof(dummy_test_password)); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, + (int)sizeof(dummy_test_password)); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(password.data, dummy_test_password, + sizeof(dummy_test_password)), ==, 0); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static +void test_read_mandos_client_output_append(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + const char dummy_test_password[] = "dummy test password"; + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(PIPE_BUF), + .length=PIPE_BUF, + .allocated=PIPE_BUF, + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + + memset(password.data, 'x', PIPE_BUF); + char password_expected[PIPE_BUF]; + memcpy(password_expected, password.data, PIPE_BUF); + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF); + g_assert_cmpint((int)write(pipefds[1], dummy_test_password, + sizeof(dummy_test_password)), + ==, (int)sizeof(dummy_test_password)); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, + PIPE_BUF + sizeof(dummy_test_password)); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(password_expected, password.data, PIPE_BUF), + ==, 0); + g_assert_cmpint(memcmp(password.data + PIPE_BUF, + dummy_test_password, + sizeof(dummy_test_password)), ==, 0); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); +} + +static char *make_temporary_directory(void); + +static void test_add_inotify_dir_watch(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .quit_now=&quit_now, + .password=&password, + .filename=tempdir, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + g_assert_true(epoll_set_contains(added_read_task->epoll_fd, + added_read_task->fd, + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_temporary_directory(void){ + char *name = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(name); + char *result = mkdtemp(name); + if(result == NULL){ + free(name); + } + return result; +} + +static void test_add_inotify_dir_watch_fail(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + const char nonexistent_dir[] = "/nonexistent"; + + FILE *real_stderr = stderr; + FILE *devnull = fopen("/dev/null", "we"); + g_assert_nonnull(devnull); + stderr = devnull; + g_assert_false(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, nonexistent_dir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + stderr = real_stderr; + g_assert_cmpint(fclose(devnull), ==, 0); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void test_add_inotify_dir_watch_EAGAIN(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + g_assert_cmpint(read(added_read_task->fd, ievent, ievent_size), ==, + -1); + g_assert_cmpint(errno, ==, EAGAIN); + + free(ievent); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_temporary_file_in_directory(const char + *const dir); + +static +void test_add_inotify_dir_watch_IN_CLOSE_WRITE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + __attribute__((cleanup(cleanup_string))) + char *filename = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(filename); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = 0; + read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_CLOSE_WRITE); + g_assert_cmpstr(ievent->name, ==, basename(filename)); + + free(ievent); + + g_assert_cmpint(unlink(filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_temporary_prefixed_file_in_directory(const char + *const prefix, + const char + *const dir){ + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%sXXXXXX", dir, prefix), + >, 0); + g_assert_nonnull(filename); + const int fd = mkostemp(filename, O_CLOEXEC); + g_assert_cmpint(fd, >=, 0); + g_assert_cmpint(close(fd), ==, 0); + return filename; +} + +static char *make_temporary_file_in_directory(const char + *const dir){ + return make_temporary_prefixed_file_in_directory("temp", dir); +} + +static +void test_add_inotify_dir_watch_IN_MOVED_TO(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *watchdir = make_temporary_directory(); + g_assert_nonnull(watchdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, watchdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + char *sourcedir = make_temporary_directory(); + g_assert_nonnull(sourcedir); + + __attribute__((cleanup(cleanup_string))) + char *filename = make_temporary_file_in_directory(sourcedir); + g_assert_nonnull(filename); + + __attribute__((cleanup(cleanup_string))) + char *targetfilename = NULL; + g_assert_cmpint(asprintf(&targetfilename, "%s/%s", watchdir, + basename(filename)), >, 0); + g_assert_nonnull(targetfilename); + + g_assert_cmpint(rename(filename, targetfilename), ==, 0); + g_assert_cmpint(rmdir(sourcedir), ==, 0); + free(sourcedir); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_MOVED_TO); + g_assert_cmpstr(ievent->name, ==, basename(targetfilename)); + + free(ievent); + + g_assert_cmpint(unlink(targetfilename), ==, 0); + g_assert_cmpint(rmdir(watchdir), ==, 0); +} + +static +void test_add_inotify_dir_watch_IN_DELETE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + __attribute__((cleanup(cleanup_string))) + char *tempfile = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfile); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + g_assert_cmpint(unlink(tempfile), ==, 0); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = 0; + read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_DELETE); + g_assert_cmpstr(ievent->name, ==, basename(tempfile)); + + free(ievent); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void test_read_inotify_event_readerror(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + /* Reading /proc/self/mem from offset 0 will always result in EIO */ + const int fd = open("/proc/self/mem", O_RDONLY | O_CLOEXEC); + + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=fd, + .quit_now=&quit_now, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + }; + g_assert_nonnull(task.filename); + run_task_with_stderr_to_dev_null(task, queue); + g_assert_true(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, fd)); + + g_assert_cmpint(close(fd), ==, -1); +} + +static void test_read_inotify_event_bad_epoll(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + const mono_microsecs current_time = 17; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + const int epoll_fd = pipefds[0]; /* This will obviously fail */ + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + g_assert_nonnull(task.filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_nonnull(find_matching_task(queue, task)); + g_assert_true(queue->next_run == 1000000 + current_time); + + g_assert_cmpint(close(pipefds[0]), ==, 0); + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static void test_read_inotify_event_nodata(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + g_assert_nonnull(task.filename); + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static void test_read_inotify_event_eof(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_true(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, pipefds[0])); + + g_assert_cmpint(close(pipefds[0]), ==, -1); +} + +static +void test_read_inotify_event_IN_CLOSE_WRITE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + char ievent_buffer[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ievent = ((struct inotify_event *) + ievent_buffer); + + const char dummy_file_name[] = "ask.dummy_file_name"; + ievent->mask = IN_CLOSE_WRITE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], ievent_buffer, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run != 0); + g_assert_cmpuint((unsigned int)queue->length, >=, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpuint((unsigned int)queue->length, >=, 2); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=filename, + .question_filename=filename, + .password=&password, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); +} + +static +void test_read_inotify_event_IN_MOVED_TO(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + char ievent_buffer[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ievent = ((struct inotify_event *) + ievent_buffer); + + const char dummy_file_name[] = "ask.dummy_file_name"; + ievent->mask = IN_MOVED_TO; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], ievent_buffer, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run != 0); + g_assert_cmpuint((unsigned int)queue->length, >=, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpuint((unsigned int)queue->length, >=, 2); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=filename, + .question_filename=filename, + .password=&password, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); +} + +static void test_read_inotify_event_IN_DELETE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + char ievent_buffer[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ievent = ((struct inotify_event *) + ievent_buffer); + + const char dummy_file_name[] = "ask.dummy_file_name"; + ievent->mask = IN_DELETE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], ievent_buffer, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_true(string_set_contains(*task.cancelled_filenames, + filename)); +} + +static void +test_read_inotify_event_IN_CLOSE_WRITE_badname(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + char ievent_buffer[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ievent = ((struct inotify_event *) + ievent_buffer); + + const char dummy_file_name[] = "ignored.dummy_file_name"; + ievent->mask = IN_CLOSE_WRITE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], ievent_buffer, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); +} + +static void +test_read_inotify_event_IN_MOVED_TO_badname(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + char ievent_buffer[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ievent = ((struct inotify_event *) + ievent_buffer); + + const char dummy_file_name[] = "ignored.dummy_file_name"; + ievent->mask = IN_MOVED_TO; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], ievent_buffer, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); +} + +static +void test_read_inotify_event_IN_DELETE_badname(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + char ievent_buffer[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ievent = ((struct inotify_event *) + ievent_buffer); + + const char dummy_file_name[] = "ignored.dummy_file_name"; + ievent->mask = IN_DELETE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], ievent_buffer, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_false(string_set_contains(cancelled_filenames, filename)); +} + +static +void test_open_and_parse_question_ENOENT(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + char *const filename = strdup("/nonexistent"); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void test_open_and_parse_question_EIO(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + char *filename = strdup("/proc/self/mem"); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void +test_open_and_parse_question_parse_error(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int tempfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(tempfile, >, 0); + const char bad_data[] = "this is bad syntax\n"; + g_assert_cmpint(write(tempfile, bad_data, sizeof(bad_data)), + ==, sizeof(bad_data)); + g_assert_cmpint(close(tempfile), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_nosocket(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nPID=1\n"), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_badsocket(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=\nPID=1\n"), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_nopid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\n"), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_badpid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=\n"), + >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_noexist_pid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + /* Find value of sysctl kernel.pid_max */ + uintmax_t pid_max = 0; + FILE *sysctl_pid_max = fopen("/proc/sys/kernel/pid_max", "r"); + g_assert_nonnull(sysctl_pid_max); + g_assert_cmpint(fscanf(sysctl_pid_max, "%" PRIuMAX, &pid_max), + ==, 1); + g_assert_cmpint(fclose(sysctl_pid_max), ==, 0); + + pid_t nonexisting_pid = ((pid_t)pid_max)+1; + g_assert_true(nonexisting_pid > 0); /* Overflow check */ + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX"\n", (uintmax_t)nonexisting_pid), + >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const question_filename = strdup(tempfilename); + g_assert_nonnull(question_filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=question_filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=question_filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_no_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX "\n", (uintmax_t)getpid()), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + __attribute__((cleanup(cleanup_string))) + char *socket_filename = strdup("/nonexistent"); + g_assert_nonnull(socket_filename); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=connect_question_socket, + .question_filename=tempfilename, + .filename=socket_filename, + .epoll_fd=epoll_fd, + .password=&password, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(queue->next_run != 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_bad_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX "\nNotAfter=\n", + (uintmax_t)getpid()), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + __attribute__((cleanup(cleanup_string))) + char *socket_filename = strdup("/nonexistent"); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=connect_question_socket, + .question_filename=tempfilename, + .filename=socket_filename, + .epoll_fd=epoll_fd, + .password=&password, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + g_assert_true(queue->next_run != 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void assert_open_and_parse_question_with_notafter(const mono_microsecs + current_time, + const mono_microsecs + notafter, + const mono_microsecs + next_queue_run){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = next_queue_run; + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX "\nNotAfter=%" PRIuMAX "\n", + (uintmax_t)getpid(), notafter), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + + if(queue->length >= 1){ + __attribute__((cleanup(cleanup_string))) + char *socket_filename = strdup("/nonexistent"); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=connect_question_socket, + .filename=socket_filename, + .epoll_fd=epoll_fd, + .password=&password, + .current_time=¤t_time, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + g_assert_true(queue->next_run != 0); + } + + if(notafter == 0){ + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + } else if(current_time >= notafter) { + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + } else { + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=cancel_old_question, + .question_filename=tempfilename, + .filename=tempfilename, + .notafter=notafter, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + })); + } + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_notafter_0(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 0, 0); +} + +static void +test_open_and_parse_question_notafter_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 1, 0); +} + +static void +test_open_and_parse_question_notafter_1_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 1, 1); +} + +static void +test_open_and_parse_question_notafter_1_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 1, 2); +} + +static void +test_open_and_parse_question_equal_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(1, 1, 0); +} + +static void +test_open_and_parse_question_late_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(2, 1, 0); +} + +static void assert_cancel_old_question_param(const mono_microsecs + next_queue_run, + const mono_microsecs + notafter, + const mono_microsecs + current_time, + const mono_microsecs + next_set_to){ + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = next_queue_run; + + char *const question_filename = strdup("/nonexistent"); + g_assert_nonnull(question_filename); + task_context task = { + .func=cancel_old_question, + .question_filename=question_filename, + .filename=question_filename, + .notafter=notafter, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + }; + task.func(task, queue); + + if(current_time >= notafter){ + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(string_set_contains(cancelled_filenames, + "/nonexistent")); + } else { + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=cancel_old_question, + .question_filename=question_filename, + .filename=question_filename, + .notafter=notafter, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + })); + + g_assert_false(string_set_contains(cancelled_filenames, + question_filename)); + } + g_assert_cmpuint((unsigned int)queue->next_run, ==, + (unsigned int)next_set_to); +} + +static void test_cancel_old_question_0_1_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run unset, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(0, 1, 2, 0); +} + +static void test_cancel_old_question_0_2_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is 0, meaning unset, and notafter is 2, + and current_time is not yet notafter or greater, + update value of next_queue_run to value of notafter */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(0, 2, 1, 2); +} + +static void test_cancel_old_question_1_2_3(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run 1, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(1, 2, 3, 1); +} + +static void test_cancel_old_question_1_3_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is set, + and current_time is not yet notafter or greater, + and notafter is larger than next_queue_run + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(1, 3, 2, 1); +} + +static void test_cancel_old_question_2_1_3(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run 2, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(2, 1, 3, 2); +} + +static void test_cancel_old_question_2_3_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is set, + and current_time is not yet notafter or greater, + and notafter is larger than next_queue_run + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(2, 3, 1, 2); +} + +static void test_cancel_old_question_3_1_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run 3, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(3, 1, 2, 3); +} + +static void test_cancel_old_question_3_2_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is set, + and current_time is not yet notafter or greater, + and notafter is smaller than next_queue_run + update value of next_queue_run to value of notafter */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(3, 2, 1, 2); +} + +static void +test_connect_question_socket_name_too_long(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const char question_filename[] = "/nonexistent/question"; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + struct sockaddr_un unix_socket = { .sun_family=AF_LOCAL }; + char socket_name[sizeof(unix_socket.sun_path)]; + memset(socket_name, 'x', sizeof(socket_name)); + socket_name[sizeof(socket_name)-1] = '\0'; + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_true(string_set_contains(cancelled_filenames, + question_filename)); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(queue->next_run == 0); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_connect_question_socket_connect_fail(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const char question_filename[] = "/nonexistent/question"; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 3; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + char socket_name[] = "nonexistent"; + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_nonnull(find_matching_task(queue, task)); + + g_assert_false(string_set_contains(cancelled_filenames, + question_filename)); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + g_assert_true(queue->next_run == 1000000 + current_time); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_connect_question_socket_bad_epoll(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = open("/dev/null", O_WRONLY | O_CLOEXEC); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 5; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_close))) + const int sock_fd = socket(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + g_assert_cmpint(sock_fd, >=, 0); + struct sockaddr_un sock_name = { .sun_family=AF_LOCAL }; + const char socket_name[] = "socket_name"; + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + g_assert_cmpint((int)strlen(filename), <, + (int)sizeof(sock_name.sun_path)); + strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path)); + sock_name.sun_path[sizeof(sock_name.sun_path)-1] = '\0'; + g_assert_cmpint((int)bind(sock_fd, (struct sockaddr *)&sock_name, + (socklen_t)SUN_LEN(&sock_name)), >=, 0); + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=strdup(filename), + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + const task_context *const added_task + = find_matching_task(queue, task); + g_assert_nonnull(added_task); + g_assert_true(queue->next_run == 1000000 + current_time); + + g_assert_cmpint(unlink(filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_connect_question_socket_usable(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + const mono_microsecs current_time = 0; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_close))) + const int sock_fd = socket(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + g_assert_cmpint(sock_fd, >=, 0); + struct sockaddr_un sock_name = { .sun_family=AF_LOCAL }; + const char socket_name[] = "socket_name"; + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + g_assert_cmpint((int)strlen(filename), <, + (int)sizeof(sock_name.sun_path)); + strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path)); + sock_name.sun_path[sizeof(sock_name.sun_path)-1] = '\0'; + g_assert_cmpint((int)bind(sock_fd, (struct sockaddr *)&sock_name, + (socklen_t)SUN_LEN(&sock_name)), >=, 0); + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=&password, + .filename=strdup(filename), + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + const task_context *const added_task + = find_matching_task(queue, (task_context){ + .func=send_password_to_socket, + .question_filename=question_filename, + .filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + .current_time=¤t_time, + }); + g_assert_nonnull(added_task); + g_assert_cmpint(added_task->fd, >, 0); + + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); + + const int fd = added_task->fd; + g_assert_cmpint(fd, >, 0); + g_assert_true(fd_has_cloexec_and_nonblock(fd)); + + /* write to fd */ + char write_data[PIPE_BUF]; + { + /* Construct test password buffer */ + /* Start with + since that is what the real procotol uses */ + write_data[0] = '+'; + /* Set a special character at string end just to mark the end */ + write_data[sizeof(write_data)-2] = 'y'; + /* Set NUL at buffer end, as suggested by the protocol */ + write_data[sizeof(write_data)-1] = '\0'; + /* Fill rest of password with 'x' */ + memset(write_data+1, 'x', sizeof(write_data)-3); + g_assert_cmpint((int)send(fd, write_data, sizeof(write_data), + MSG_NOSIGNAL), ==, sizeof(write_data)); + } + + /* read from sock_fd */ + char read_data[sizeof(write_data)]; + g_assert_cmpint((int)read(sock_fd, read_data, sizeof(read_data)), + ==, sizeof(read_data)); + + g_assert_true(memcmp(write_data, read_data, sizeof(write_data)) + == 0); + + /* writing to sock_fd should fail */ + g_assert_cmpint(send(sock_fd, write_data, sizeof(write_data), + MSG_NOSIGNAL), <, 0); + + /* reading from fd should fail */ + g_assert_cmpint((int)recv(fd, read_data, sizeof(read_data), + MSG_NOSIGNAL), <, 0); + + g_assert_cmpint(unlink(filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void +test_send_password_to_socket_client_not_exited(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool password_is_read = true; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=&password_is_read, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task + = find_matching_task(queue, task); + g_assert_nonnull(added_task); + g_assert_cmpuint((unsigned int)password.length, ==, 0); + g_assert_true(password_is_read); + + g_assert_cmpint(added_task->fd, >, 0); + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); +} + +static void +test_send_password_to_socket_password_not_read(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task = find_matching_task(queue, + task); + g_assert_nonnull(added_task); + g_assert_cmpuint((unsigned int)password.length, ==, 0); + g_assert_true(queue->next_run == 0); + + g_assert_cmpint(added_task->fd, >, 0); + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); +} + +static +void test_send_password_to_socket_EMSGSIZE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const char question_filename[] = "/nonexistent/question"; + char *const filename = strdup("/nonexistent/socket"); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const size_t oversized = 1024*1024; /* Limit seems to be 212960 */ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(oversized), + .length=oversized, + .allocated=oversized, + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + /* Construct test password buffer */ + /* Start with + since that is what the real procotol uses */ + password.data[0] = '+'; + /* Set a special character at string end just to mark the end */ + password.data[oversized-3] = 'y'; + /* Set NUL at buffer end, as suggested by the protocol */ + password.data[oversized-2] = '\0'; + /* Fill rest of password with 'x' */ + memset(password.data+1, 'x', oversized-3); + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + __attribute__((cleanup(cleanup_close))) + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=filename, + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(string_set_contains(cancelled_filenames, + question_filename)); +} + +static void test_send_password_to_socket_retry(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + /* Close the server side socket to force ECONNRESET on client */ + g_assert_cmpint(close(read_socket), ==, 0); + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task = find_matching_task(queue, + task); + g_assert_nonnull(added_task); + g_assert_cmpuint((unsigned int)password.length, ==, 0); + + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); +} + +static +void test_send_password_to_socket_bad_epoll(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = open("/dev/null", O_WRONLY | O_CLOEXEC); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + const mono_microsecs current_time = 11; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + /* Close the server side socket to force ECONNRESET on client */ + g_assert_cmpint(close(read_socket), ==, 0); + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task = find_matching_task(queue, + task); + g_assert_nonnull(added_task); + g_assert_true(queue->next_run == current_time + 1000000); + g_assert_cmpuint((unsigned int)password.length, ==, 0); +} + +static void assert_send_password_to_socket_password(buffer password){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=question_filename, + .filename=filename, + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=(mono_microsecs[]){0}, + }; + + char *expected_written_data = malloc(password.length + 2); + g_assert_nonnull(expected_written_data); + expected_written_data[0] = '+'; + expected_written_data[password.length + 1] = '\0'; + if(password.length > 0){ + g_assert_nonnull(password.data); + memcpy(expected_written_data + 1, password.data, password.length); + } + + task.func(task, queue); + + char buf[PIPE_BUF]; + g_assert_cmpint((int)read(read_socket, buf, PIPE_BUF), ==, + (int)(password.length + 2)); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(memcmp(expected_written_data, buf, + password.length + 2) == 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, write_socket)); + + free(expected_written_data); +} + +static void +test_send_password_to_socket_null_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_empty_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(1), /* because malloc(0) may return NULL */ + .length=0, + .allocated=0, /* deliberate lie */ + }; + g_assert_nonnull(password.data); + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_empty_str_pass(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=strdup(""), + .length=0, + .allocated=1, + }; + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_text_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + const char dummy_test_password[] = "dummy test password"; + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data = strdup(dummy_test_password), + .length = strlen(dummy_test_password), + .allocated = sizeof(dummy_test_password), + }; + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_binary_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(255), + .length=255, + .allocated=255, + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + char c = 1; /* Start at 1, avoiding NUL */ + for(int i=0; i < 255; i++){ + password.data[i] = c++; + } + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_nuls_in_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char test_password[] = {'\0', 'a', '\0', 'b', '\0', 'c', '\0'}; + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(sizeof(test_password)), + .length=sizeof(test_password), + .allocated=sizeof(test_password), + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) !=0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + memcpy(password.data, test_password, password.allocated); + assert_send_password_to_socket_password(password); +} + +static bool assert_add_existing_questions_to_devnull(task_queue + *const, + const int, + buffer *const, + string_set *, + const + mono_microsecs + *const, + bool *const, + bool *const, + const char + *const); + +static void test_add_existing_questions_ENOENT(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + + g_assert_false(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + (buffer[]){{}}, /* password */ + &cancelled_filenames, + (mono_microsecs[]){0}, /* current_time */ + (bool[]){false}, /* mandos_client_exited */ + (bool[]){false}, /* password_is_read */ + "/nonexistent")); /* dirname */ + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static +bool assert_add_existing_questions_to_devnull(task_queue + *const queue, + const int + epoll_fd, + buffer *const + password, + string_set + *cancelled_filenames, + const mono_microsecs + *const current_time, + bool *const + mandos_client_exited, + bool *const + password_is_read, + const char *const + dirname){ + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", O_WRONLY | O_CLOEXEC); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + dup2(devnull_fd, STDERR_FILENO); + const bool ret = add_existing_questions(queue, epoll_fd, password, + cancelled_filenames, + current_time, + mandos_client_exited, + password_is_read, dirname); + dup2(real_stderr_fd, STDERR_FILENO); + return ret; +} + +static +void test_add_existing_questions_no_questions(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_false(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + (buffer[]){{}}, /* password */ + &cancelled_filenames, + (mono_microsecs[]){0}, /* current_time */ + (bool[]){false}, /* mandos_client_exited */ + (bool[]){false}, /* password_is_read */ + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_question_file_in_directory(const char *const); + +static +void test_add_existing_questions_one_question(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *question_filename + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename); + + g_assert_true(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + &password, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read, + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=question_filename, + .question_filename=question_filename, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(question_filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_question_file_in_directory(const char + *const dir){ + return make_temporary_prefixed_file_in_directory("ask.", dir); +} + +static +void test_add_existing_questions_two_questions(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *question_filename1 + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename1); + __attribute__((cleanup(cleanup_string))) + char *question_filename2 + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename2); + + g_assert_true(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + &password, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read, + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 2); + + g_assert_true(queue->next_run == 1); + + __attribute__((cleanup(string_set_clear))) + string_set seen_questions = {}; + + bool queue_contains_question_opener(char *const question_filename){ + return(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .question_filename=question_filename, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }) != NULL); + } + + g_assert_true(queue_contains_question_opener(question_filename1)); + g_assert_true(queue_contains_question_opener(question_filename2)); + + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(question_filename1), ==, 0); + g_assert_cmpint(unlink(question_filename2), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void +test_add_existing_questions_non_questions(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *question_filename1 + = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(question_filename1); + __attribute__((cleanup(cleanup_string))) + char *question_filename2 + = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(question_filename2); + + g_assert_false(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + (buffer[]){{}}, /* password */ + &cancelled_filenames, + (mono_microsecs[]){0}, /* current_time */ + (bool[]){false}, /* mandos_client_exited */ + (bool[]){false}, /* password_is_read */ + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(question_filename1), ==, 0); + g_assert_cmpint(unlink(question_filename2), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void +test_add_existing_questions_both_types(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *tempfilename1 = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfilename1); + __attribute__((cleanup(cleanup_string))) + char *tempfilename2 = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfilename2); + __attribute__((cleanup(cleanup_string))) + char *question_filename + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename); + + g_assert_true(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + &password, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read, + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=question_filename, + .question_filename=question_filename, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(tempfilename1), ==, 0); + g_assert_cmpint(unlink(tempfilename2), ==, 0); + g_assert_cmpint(unlink(question_filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void test_wait_for_event_timeout(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + g_assert_true(wait_for_event(epoll_fd, 1, 0)); +} + +static void test_wait_for_event_event(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_pipe = pipefds[0]; + __attribute__((cleanup(cleanup_close))) + const int write_pipe = pipefds[1]; + g_assert_cmpint(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, read_pipe, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }), ==, 0); + g_assert_cmpint((int)write(write_pipe, "x", 1), ==, 1); + + g_assert_true(wait_for_event(epoll_fd, 0, 0)); +} + +static void test_wait_for_event_sigchld(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); + } + g_assert_true(pid != -1); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + g_assert_true(wait_for_event(epoll_fd, 0, 0)); + + int status; + g_assert_true(waitpid(pid, &status, 0) == pid); + g_assert_true(WIFEXITED(status)); + g_assert_cmpint(WEXITSTATUS(status), ==, EXIT_SUCCESS); +} + +static void test_run_queue_zeroes_next_run(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = 1; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->next_run, ==, 0); +} + +static +void test_run_queue_clears_cancelled_filenames(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + const char question_filename[] = "/nonexistent/question_filename"; + g_assert_true(string_set_add(&cancelled_filenames, + question_filename)); + + g_assert_true(add_to_queue(queue, + (task_context){ .func=dummy_func })); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); + g_assert_false(string_set_contains(cancelled_filenames, + question_filename)); +} + +static +void test_run_queue_skips_cancelled_filenames(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_pipe = pipefds[0]; + g_assert_cmpint(close(pipefds[1]), ==, 0); + const char question_filename[] = "/nonexistent/question_filename"; + g_assert_true(string_set_add(&cancelled_filenames, + question_filename)); + __attribute__((nonnull)) + void quit_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + g_assert_nonnull(task.quit_now); + *task.quit_now = true; + } + task_context task = { + .func=quit_func, + .question_filename=strdup(question_filename), + .quit_now=&quit_now, + .fd=read_pipe, + }; + g_assert_nonnull(task.question_filename); + + g_assert_true(add_to_queue(queue, task)); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + + /* read_pipe should be closed already */ + errno = 0; + bool read_pipe_closed = (close(read_pipe) == -1); + read_pipe_closed &= (errno == EBADF); + g_assert_true(read_pipe_closed); +} + +static void test_run_queue_one_task(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + + __attribute__((nonnull)) + void next_run_func(__attribute__((unused)) + const task_context task, + task_queue *const q){ + q->next_run = 1; + } + + task_context task = { + .func=next_run_func, + }; + g_assert_true(add_to_queue(queue, task)); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_cmpuint((unsigned int)(queue->next_run), ==, 1); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_run_queue_two_tasks(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = 1; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + bool mandos_client_exited = false; + + __attribute__((nonnull)) + void next_run_func(__attribute__((unused)) + const task_context task, + task_queue *const q){ + q->next_run = 1; + } + + __attribute__((nonnull)) + void exited_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.mandos_client_exited = true; + } + + task_context task1 = { + .func=next_run_func, + }; + g_assert_true(add_to_queue(queue, task1)); + + task_context task2 = { + .func=exited_func, + .mandos_client_exited=&mandos_client_exited, + }; + g_assert_true(add_to_queue(queue, task2)); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)(queue->next_run), ==, 1); + g_assert_true(mandos_client_exited); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_run_queue_two_tasks_quit(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((nonnull)) + void set_exited_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.mandos_client_exited = true; + *task.quit_now = true; + } + task_context task1 = { + .func=set_exited_func, + .quit_now=&quit_now, + .mandos_client_exited=&mandos_client_exited, + }; + g_assert_true(add_to_queue(queue, task1)); + + __attribute__((nonnull)) + void set_read_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.quit_now = true; + *task.password_is_read = true; + } + task_context task2 = { + .func=set_read_func, + .quit_now=&quit_now, + .password_is_read=&password_is_read, + }; + g_assert_true(add_to_queue(queue, task2)); + + g_assert_false(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(quit_now); + g_assert_true(mandos_client_exited xor password_is_read); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_run_queue_two_tasks_cleanup(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_pipe = pipefds[0]; + __attribute__((cleanup(cleanup_close))) + const int write_pipe = pipefds[1]; + bool quit_now = false; + + __attribute__((nonnull)) + void read_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.quit_now = true; + } + task_context task1 = { + .func=read_func, + .quit_now=&quit_now, + .fd=read_pipe, + }; + g_assert_true(add_to_queue(queue, task1)); + + __attribute__((nonnull)) + void write_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.quit_now = true; + } + task_context task2 = { + .func=write_func, + .quit_now=&quit_now, + .fd=write_pipe, + }; + g_assert_true(add_to_queue(queue, task2)); + + g_assert_false(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(quit_now); + + /* Either read_pipe or write_pipe should be closed already */ + errno = 0; + bool close_read_pipe = (close(read_pipe) == -1); + close_read_pipe &= (errno == EBADF); + errno = 0; + bool close_write_pipe = (close(write_pipe) == -1); + close_write_pipe &= (errno == EBADF); + g_assert_true(close_read_pipe xor close_write_pipe); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_setup_signal_handler(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save current SIGCHLD action, whatever it is */ + struct sigaction expected_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, &expected_sigchld_action), + ==, 0); + + /* Act; i.e. run the setup_signal_handler() function */ + struct sigaction actual_old_sigchld_action; + g_assert_true(setup_signal_handler(&actual_old_sigchld_action)); + + /* Check that the function correctly set "actual_old_sigchld_action" + to the same values as the previously saved + "expected_sigchld_action" */ + /* Check member sa_handler */ + g_assert_true(actual_old_sigchld_action.sa_handler + == expected_sigchld_action.sa_handler); + /* Check member sa_mask */ + for(int signum = 1; signum < NSIG; signum++){ + const int expected_old_block_state + = sigismember(&expected_sigchld_action.sa_mask, signum); + g_assert_cmpint(expected_old_block_state, >=, 0); + const int actual_old_block_state + = sigismember(&actual_old_sigchld_action.sa_mask, signum); + g_assert_cmpint(actual_old_block_state, >=, 0); + g_assert_cmpint(actual_old_block_state, + ==, expected_old_block_state); + } + /* Check member sa_flags */ + g_assert_true((actual_old_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)) + == (expected_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART))); + + /* Retrieve the current signal handler for SIGCHLD as set by + setup_signal_handler() */ + struct sigaction actual_new_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, + &actual_new_sigchld_action), ==, 0); + /* Check that the signal handler (member sa_handler) is correctly + set to the "handle_sigchld" function */ + g_assert_true(actual_new_sigchld_action.sa_handler != SIG_DFL); + g_assert_true(actual_new_sigchld_action.sa_handler != SIG_IGN); + g_assert_true(actual_new_sigchld_action.sa_handler + == handle_sigchld); + /* Check (in member sa_mask) that at least a handful of signals are + actually blocked during the signal handler */ + for(int signum = 1; signum < NSIG; signum++){ + int actual_new_block_state; + switch(signum){ + case SIGTERM: + case SIGINT: + case SIGQUIT: + case SIGHUP: + actual_new_block_state + = sigismember(&actual_new_sigchld_action.sa_mask, signum); + g_assert_cmpint(actual_new_block_state, ==, 1); + continue; + case SIGKILL: /* non-blockable */ + case SIGSTOP: /* non-blockable */ + case SIGCHLD: /* always blocked */ + default: + continue; + } + } + /* Check member sa_flags */ + g_assert_true((actual_new_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)) + == (SA_NOCLDSTOP | SA_RESTART)); + + /* Restore signal handler */ + g_assert_cmpint(sigaction(SIGCHLD, &expected_sigchld_action, NULL), + ==, 0); +} + +static void test_restore_signal_handler(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save current SIGCHLD action, whatever it is */ + struct sigaction expected_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, &expected_sigchld_action), + ==, 0); + /* Since we haven't established a signal handler yet, there should + not be one established. But another test may have relied on + restore_signal_handler() to restore the signal handler, and if + restore_signal_handler() is buggy (which we should be prepared + for in this test) the signal handler may not have been restored + properly; check for this: */ + g_assert_true(expected_sigchld_action.sa_handler != handle_sigchld); + + /* Establish a signal handler */ + struct sigaction sigchld_action = { + .sa_handler=handle_sigchld, + .sa_flags=SA_RESTART | SA_NOCLDSTOP, + }; + g_assert_cmpint(sigfillset(&sigchld_action.sa_mask), ==, 0); + g_assert_cmpint(sigaction(SIGCHLD, &sigchld_action, NULL), ==, 0); + + /* Act; i.e. run the restore_signal_handler() function */ + g_assert_true(restore_signal_handler(&expected_sigchld_action)); + + /* Retrieve the restored signal handler data */ + struct sigaction actual_restored_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, + &actual_restored_sigchld_action), ==, 0); + + /* Check that the function correctly restored the signal action, as + saved in "actual_restored_sigchld_action", to the same values as + the previously saved "expected_sigchld_action" */ + /* Check member sa_handler */ + g_assert_true(actual_restored_sigchld_action.sa_handler + == expected_sigchld_action.sa_handler); + /* Check member sa_mask */ + for(int signum = 1; signum < NSIG; signum++){ + const int expected_old_block_state + = sigismember(&expected_sigchld_action.sa_mask, signum); + g_assert_cmpint(expected_old_block_state, >=, 0); + const int actual_restored_block_state + = sigismember(&actual_restored_sigchld_action.sa_mask, signum); + g_assert_cmpint(actual_restored_block_state, >=, 0); + g_assert_cmpint(actual_restored_block_state, + ==, expected_old_block_state); + } + /* Check member sa_flags */ + g_assert_true((actual_restored_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)) + == (expected_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART))); +} + +static void test_block_sigchld(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save original signal mask */ + sigset_t expected_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &expected_sigmask), + ==, 0); + + /* Make sure SIGCHLD is unblocked for this test */ + sigset_t sigchld_sigmask; + g_assert_cmpint(sigemptyset(&sigchld_sigmask), ==, 0); + g_assert_cmpint(sigaddset(&sigchld_sigmask, SIGCHLD), ==, 0); + g_assert_cmpint(pthread_sigmask(SIG_UNBLOCK, &sigchld_sigmask, + NULL), ==, 0); + + /* Act; i.e. run the block_sigchld() function */ + sigset_t actual_old_sigmask; + g_assert_true(block_sigchld(&actual_old_sigmask)); + + /* Check the actual_old_sigmask; it should be the same as the + previously saved signal mask "expected_sigmask". */ + for(int signum = 1; signum < NSIG; signum++){ + const int expected_old_block_state + = sigismember(&expected_sigmask, signum); + g_assert_cmpint(expected_old_block_state, >=, 0); + const int actual_old_block_state + = sigismember(&actual_old_sigmask, signum); + g_assert_cmpint(actual_old_block_state, >=, 0); + g_assert_cmpint(actual_old_block_state, + ==, expected_old_block_state); + } + + /* Retrieve the newly set signal mask */ + sigset_t actual_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &actual_sigmask), ==, 0); + + /* SIGCHLD should be blocked */ + g_assert_cmpint(sigismember(&actual_sigmask, SIGCHLD), ==, 1); + + /* Restore signal mask */ + g_assert_cmpint(pthread_sigmask(SIG_SETMASK, &expected_sigmask, + NULL), ==, 0); +} + +static void test_restore_sigmask(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save original signal mask */ + sigset_t orig_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &orig_sigmask), ==, 0); + + /* Make sure SIGCHLD is blocked for this test */ + sigset_t sigchld_sigmask; + g_assert_cmpint(sigemptyset(&sigchld_sigmask), ==, 0); + g_assert_cmpint(sigaddset(&sigchld_sigmask, SIGCHLD), ==, 0); + g_assert_cmpint(pthread_sigmask(SIG_BLOCK, &sigchld_sigmask, + NULL), ==, 0); + + /* Act; i.e. run the restore_sigmask() function */ + g_assert_true(restore_sigmask(&orig_sigmask)); + + /* Retrieve the newly restored signal mask */ + sigset_t restored_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &restored_sigmask), + ==, 0); + + /* Check the restored_sigmask; it should be the same as the + previously saved signal mask "orig_sigmask". */ + for(int signum = 1; signum < NSIG; signum++){ + const int orig_block_state = sigismember(&orig_sigmask, signum); + g_assert_cmpint(orig_block_state, >=, 0); + const int restored_block_state = sigismember(&restored_sigmask, + signum); + g_assert_cmpint(restored_block_state, >=, 0); + g_assert_cmpint(restored_block_state, ==, orig_block_state); + } + + /* Restore signal mask */ + g_assert_cmpint(pthread_sigmask(SIG_SETMASK, &orig_sigmask, + NULL), ==, 0); +} + +static void test_parse_arguments_noargs(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +__attribute__((nonnull)) +static bool parse_arguments_devnull(int argc, char *argv[], + const bool exit_failure, + char **agent_directory, + char **helper_directory, + uid_t *const user, + gid_t *const group, + char **mandos_argz, + size_t *mandos_argz_length){ + + FILE *real_stderr = stderr; + FILE *devnull = fopen("/dev/null", "we"); + g_assert_nonnull(devnull); + stderr = devnull; + + const bool ret = parse_arguments(argc, argv, exit_failure, + agent_directory, + helper_directory, user, group, + mandos_argz, mandos_argz_length); + const error_t saved_errno = errno; + + stderr = real_stderr; + g_assert_cmpint(fclose(devnull), ==, 0); + + errno = saved_errno; + + return ret; +} + +static void test_parse_arguments_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--invalid"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_true(errno == EINVAL); + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_long_dir(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--agent-directory"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_short_dir(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("-d"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_helper_directory(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--helper-directory"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(helper_directory, ==, "/tmp"); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_plugin_helper_dir(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--plugin-helper-dir"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(helper_directory, ==, "/tmp"); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_user(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--user"), + strdup("1000"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)user, ==, 1000); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_user_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--user"), + strdup("invalid"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)user, ==, 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_user_zero_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--user"), + strdup("0"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)user, ==, 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_group(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--group"), + strdup("1000"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_cmpuint((unsigned int)group, ==, 1000); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_group_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--group"), + strdup("invalid"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_group_zero_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--group"), + strdup("0"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)group, ==, 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_mandos_noargs(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char *argv[] = { + strdup("prgname"), + strdup("mandos-client"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_cmpstr(mandos_argz, ==, "mandos-client"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 1); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_mandos_args(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("mandos-client"), + strdup("one"), + strdup("two"), + strdup("three"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + char *marg = mandos_argz; + g_assert_cmpstr(marg, ==, "mandos-client"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "one"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "two"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "three"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 4); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_all_args(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--agent-directory"), + strdup("/tmp"), + strdup("--helper-directory"), + strdup("/var/tmp"), + strdup("--user"), + strdup("1"), + strdup("--group"), + strdup("2"), + strdup("mandos-client"), + strdup("one"), + strdup("two"), + strdup("three"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_cmpstr(helper_directory, ==, "/var/tmp"); + g_assert_true(user == 1); + g_assert_true(group == 2); + char *marg = mandos_argz; + g_assert_cmpstr(marg, ==, "mandos-client"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "one"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "two"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "three"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 4); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_mixed(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("mandos-client"), + strdup("--user"), + strdup("1"), + strdup("one"), + strdup("--agent-directory"), + strdup("/tmp"), + strdup("two"), + strdup("three"), + strdup("--helper-directory=/var/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_cmpstr(helper_directory, ==, "/var/tmp"); + g_assert_true(user == 1); + g_assert_true(group == 0); + char *marg = mandos_argz; + g_assert_cmpstr(marg, ==, "mandos-client"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "one"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "two"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "three"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 4); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +/* End of tests section */ + +/* Test boilerplate section; New tests should be added to the test + suite definition here, in the "run_tests" function. + + Finally, this section also contains the should_only_run_tests() + function used by main() for deciding if tests should be run or to + start normally. */ + +__attribute__((cold)) +static bool run_tests(int argc, char *argv[]){ + g_test_init(&argc, &argv, NULL); + + /* A macro to add a test with no setup or teardown functions */ +#define test_add(testpath, testfunc) \ + do { \ + g_test_add((testpath), test_fixture, NULL, NULL, \ + (testfunc), NULL); \ + } while(false) + + /* Test the signal-related functions first, since some other tests + depend on these functions in their setups and teardowns */ + test_add("/signal-handling/setup", test_setup_signal_handler); + test_add("/signal-handling/restore", test_restore_signal_handler); + test_add("/signal-handling/block", test_block_sigchld); + test_add("/signal-handling/restore-sigmask", test_restore_sigmask); + + /* Regular non-signal-related tests; these use no setups or + teardowns */ + test_add("/parse_arguments/noargs", test_parse_arguments_noargs); + test_add("/parse_arguments/invalid", test_parse_arguments_invalid); + test_add("/parse_arguments/long-dir", + test_parse_arguments_long_dir); + test_add("/parse_arguments/short-dir", + test_parse_arguments_short_dir); + test_add("/parse_arguments/helper-directory", + test_parse_arguments_helper_directory); + test_add("/parse_arguments/plugin-helper-dir", + test_parse_arguments_plugin_helper_dir); + test_add("/parse_arguments/user", test_parse_arguments_user); + test_add("/parse_arguments/user-invalid", + test_parse_arguments_user_invalid); + test_add("/parse_arguments/user-zero-invalid", + test_parse_arguments_user_zero_invalid); + test_add("/parse_arguments/group", test_parse_arguments_group); + test_add("/parse_arguments/group-invalid", + test_parse_arguments_group_invalid); + test_add("/parse_arguments/group-zero-invalid", + test_parse_arguments_group_zero_invalid); + test_add("/parse_arguments/mandos-noargs", + test_parse_arguments_mandos_noargs); + test_add("/parse_arguments/mandos-args", + test_parse_arguments_mandos_args); + test_add("/parse_arguments/all-args", + test_parse_arguments_all_args); + test_add("/parse_arguments/mixed", test_parse_arguments_mixed); + test_add("/queue/create", test_create_queue); + test_add("/queue/add", test_add_to_queue); + test_add("/queue/has_question/empty", + test_queue_has_question_empty); + test_add("/queue/has_question/false", + test_queue_has_question_false); + test_add("/queue/has_question/true", test_queue_has_question_true); + test_add("/queue/has_question/false2", + test_queue_has_question_false2); + test_add("/queue/has_question/true2", + test_queue_has_question_true2); + test_add("/buffer/cleanup", test_cleanup_buffer); + test_add("/string_set/net-set-contains-nothing", + test_string_set_new_set_contains_nothing); + test_add("/string_set/with-added-string-contains-it", + test_string_set_with_added_string_contains_it); + test_add("/string_set/cleared-does-not-contain-string", + test_string_set_cleared_does_not_contain_str); + test_add("/string_set/swap/one-with-empty", + test_string_set_swap_one_with_empty); + test_add("/string_set/swap/empty-with-one", + test_string_set_swap_empty_with_one); + test_add("/string_set/swap/one-with-one", + test_string_set_swap_one_with_one); + + /* A macro to add a test using the setup and teardown functions */ +#define test_add_st(path, func) \ + do { \ + g_test_add((path), test_fixture, NULL, test_setup, (func), \ + test_teardown); \ + } while(false) + + /* Signal-related tests; these use setups and teardowns which + establish, during each test run, a signal handler for, and a + signal mask blocking, the SIGCHLD signal, just like main() */ + test_add_st("/wait_for_event/timeout", test_wait_for_event_timeout); + test_add_st("/wait_for_event/event", test_wait_for_event_event); + test_add_st("/wait_for_event/sigchld", test_wait_for_event_sigchld); + test_add_st("/run_queue/zeroes-next-run", + test_run_queue_zeroes_next_run); + test_add_st("/run_queue/clears-cancelled_filenames", + test_run_queue_clears_cancelled_filenames); + test_add_st("/run_queue/skips-cancelled-filenames", + test_run_queue_skips_cancelled_filenames); + test_add_st("/run_queue/one-task", test_run_queue_one_task); + test_add_st("/run_queue/two-tasks", test_run_queue_two_tasks); + test_add_st("/run_queue/two-tasks/quit", + test_run_queue_two_tasks_quit); + test_add_st("/run_queue/two-tasks-cleanup", + test_run_queue_two_tasks_cleanup); + test_add_st("/task-creators/start_mandos_client", + test_start_mandos_client); + test_add_st("/task-creators/start_mandos_client/execv", + test_start_mandos_client_execv); + test_add_st("/task-creators/start_mandos_client/suid/euid", + test_start_mandos_client_suid_euid); + test_add_st("/task-creators/start_mandos_client/suid/egid", + test_start_mandos_client_suid_egid); + test_add_st("/task-creators/start_mandos_client/suid/ruid", + test_start_mandos_client_suid_ruid); + test_add_st("/task-creators/start_mandos_client/suid/rgid", + test_start_mandos_client_suid_rgid); + test_add_st("/task-creators/start_mandos_client/read", + test_start_mandos_client_read); + test_add_st("/task-creators/start_mandos_client/helper-directory", + test_start_mandos_client_helper_directory); + test_add_st("/task-creators/start_mandos_client/sigmask", + test_start_mandos_client_sigmask); + test_add_st("/task/wait_for_mandos_client_exit/badpid", + test_wait_for_mandos_client_exit_badpid); + test_add_st("/task/wait_for_mandos_client_exit/noexit", + test_wait_for_mandos_client_exit_noexit); + test_add_st("/task/wait_for_mandos_client_exit/success", + test_wait_for_mandos_client_exit_success); + test_add_st("/task/wait_for_mandos_client_exit/failure", + test_wait_for_mandos_client_exit_failure); + test_add_st("/task/wait_for_mandos_client_exit/killed", + test_wait_for_mandos_client_exit_killed); + test_add_st("/task/read_mandos_client_output/readerror", + test_read_mandos_client_output_readerror); + test_add_st("/task/read_mandos_client_output/nodata", + test_read_mandos_client_output_nodata); + test_add_st("/task/read_mandos_client_output/eof", + test_read_mandos_client_output_eof); + test_add_st("/task/read_mandos_client_output/once", + test_read_mandos_client_output_once); + test_add_st("/task/read_mandos_client_output/malloc", + test_read_mandos_client_output_malloc); + test_add_st("/task/read_mandos_client_output/append", + test_read_mandos_client_output_append); + test_add_st("/task-creators/add_inotify_dir_watch", + test_add_inotify_dir_watch); + test_add_st("/task-creators/add_inotify_dir_watch/fail", + test_add_inotify_dir_watch_fail); + test_add_st("/task-creators/add_inotify_dir_watch/EAGAIN", + test_add_inotify_dir_watch_EAGAIN); + test_add_st("/task-creators/add_inotify_dir_watch/IN_CLOSE_WRITE", + test_add_inotify_dir_watch_IN_CLOSE_WRITE); + test_add_st("/task-creators/add_inotify_dir_watch/IN_MOVED_TO", + test_add_inotify_dir_watch_IN_MOVED_TO); + test_add_st("/task-creators/add_inotify_dir_watch/IN_DELETE", + test_add_inotify_dir_watch_IN_DELETE); + test_add_st("/task/read_inotify_event/readerror", + test_read_inotify_event_readerror); + test_add_st("/task/read_inotify_event/bad-epoll", + test_read_inotify_event_bad_epoll); + test_add_st("/task/read_inotify_event/nodata", + test_read_inotify_event_nodata); + test_add_st("/task/read_inotify_event/eof", + test_read_inotify_event_eof); + test_add_st("/task/read_inotify_event/IN_CLOSE_WRITE", + test_read_inotify_event_IN_CLOSE_WRITE); + test_add_st("/task/read_inotify_event/IN_MOVED_TO", + test_read_inotify_event_IN_MOVED_TO); + test_add_st("/task/read_inotify_event/IN_DELETE", + test_read_inotify_event_IN_DELETE); + test_add_st("/task/read_inotify_event/IN_CLOSE_WRITE/badname", + test_read_inotify_event_IN_CLOSE_WRITE_badname); + test_add_st("/task/read_inotify_event/IN_MOVED_TO/badname", + test_read_inotify_event_IN_MOVED_TO_badname); + test_add_st("/task/read_inotify_event/IN_DELETE/badname", + test_read_inotify_event_IN_DELETE_badname); + test_add_st("/task/open_and_parse_question/ENOENT", + test_open_and_parse_question_ENOENT); + test_add_st("/task/open_and_parse_question/EIO", + test_open_and_parse_question_EIO); + test_add_st("/task/open_and_parse_question/parse-error", + test_open_and_parse_question_parse_error); + test_add_st("/task/open_and_parse_question/nosocket", + test_open_and_parse_question_nosocket); + test_add_st("/task/open_and_parse_question/badsocket", + test_open_and_parse_question_badsocket); + test_add_st("/task/open_and_parse_question/nopid", + test_open_and_parse_question_nopid); + test_add_st("/task/open_and_parse_question/badpid", + test_open_and_parse_question_badpid); + test_add_st("/task/open_and_parse_question/noexist_pid", + test_open_and_parse_question_noexist_pid); + test_add_st("/task/open_and_parse_question/no-notafter", + test_open_and_parse_question_no_notafter); + test_add_st("/task/open_and_parse_question/bad-notafter", + test_open_and_parse_question_bad_notafter); + test_add_st("/task/open_and_parse_question/notafter-0", + test_open_and_parse_question_notafter_0); + test_add_st("/task/open_and_parse_question/notafter-1", + test_open_and_parse_question_notafter_1); + test_add_st("/task/open_and_parse_question/notafter-1-1", + test_open_and_parse_question_notafter_1_1); + test_add_st("/task/open_and_parse_question/notafter-1-2", + test_open_and_parse_question_notafter_1_2); + test_add_st("/task/open_and_parse_question/equal-notafter", + test_open_and_parse_question_equal_notafter); + test_add_st("/task/open_and_parse_question/late-notafter", + test_open_and_parse_question_late_notafter); + test_add_st("/task/cancel_old_question/0-1-2", + test_cancel_old_question_0_1_2); + test_add_st("/task/cancel_old_question/0-2-1", + test_cancel_old_question_0_2_1); + test_add_st("/task/cancel_old_question/1-2-3", + test_cancel_old_question_1_2_3); + test_add_st("/task/cancel_old_question/1-3-2", + test_cancel_old_question_1_3_2); + test_add_st("/task/cancel_old_question/2-1-3", + test_cancel_old_question_2_1_3); + test_add_st("/task/cancel_old_question/2-3-1", + test_cancel_old_question_2_3_1); + test_add_st("/task/cancel_old_question/3-1-2", + test_cancel_old_question_3_1_2); + test_add_st("/task/cancel_old_question/3-2-1", + test_cancel_old_question_3_2_1); + test_add_st("/task/connect_question_socket/name-too-long", + test_connect_question_socket_name_too_long); + test_add_st("/task/connect_question_socket/connect-fail", + test_connect_question_socket_connect_fail); + test_add_st("/task/connect_question_socket/bad-epoll", + test_connect_question_socket_bad_epoll); + test_add_st("/task/connect_question_socket/usable", + test_connect_question_socket_usable); + test_add_st("/task/send_password_to_socket/client-not-exited", + test_send_password_to_socket_client_not_exited); + test_add_st("/task/send_password_to_socket/password-not-read", + test_send_password_to_socket_password_not_read); + test_add_st("/task/send_password_to_socket/EMSGSIZE", + test_send_password_to_socket_EMSGSIZE); + test_add_st("/task/send_password_to_socket/retry", + test_send_password_to_socket_retry); + test_add_st("/task/send_password_to_socket/bad-epoll", + test_send_password_to_socket_bad_epoll); + test_add_st("/task/send_password_to_socket/null-password", + test_send_password_to_socket_null_password); + test_add_st("/task/send_password_to_socket/empty-password", + test_send_password_to_socket_empty_password); + test_add_st("/task/send_password_to_socket/empty-str-password", + test_send_password_to_socket_empty_str_pass); + test_add_st("/task/send_password_to_socket/text-password", + test_send_password_to_socket_text_password); + test_add_st("/task/send_password_to_socket/binary-password", + test_send_password_to_socket_binary_password); + test_add_st("/task/send_password_to_socket/nuls-in-password", + test_send_password_to_socket_nuls_in_password); + test_add_st("/task-creators/add_existing_questions/ENOENT", + test_add_existing_questions_ENOENT); + test_add_st("/task-creators/add_existing_questions/no-questions", + test_add_existing_questions_no_questions); + test_add_st("/task-creators/add_existing_questions/one-question", + test_add_existing_questions_one_question); + test_add_st("/task-creators/add_existing_questions/two-questions", + test_add_existing_questions_two_questions); + test_add_st("/task-creators/add_existing_questions/non-questions", + test_add_existing_questions_non_questions); + test_add_st("/task-creators/add_existing_questions/both-types", + test_add_existing_questions_both_types); + + return g_test_run() == 0; +} + +static bool should_only_run_tests(int *argc_p, char **argv_p[]){ + GOptionContext *context = g_option_context_new(""); + + g_option_context_set_help_enabled(context, FALSE); + g_option_context_set_ignore_unknown_options(context, TRUE); + + gboolean run_tests = FALSE; + GOptionEntry entries[] = { + { "test", 0, 0, G_OPTION_ARG_NONE, + &run_tests, "Run tests", NULL }, + { NULL } + }; + g_option_context_add_main_entries(context, entries, NULL); + + GError *error = NULL; + + if(g_option_context_parse(context, argc_p, argv_p, &error) != TRUE){ + g_option_context_free(context); + g_error("Failed to parse options: %s", error->message); + } + + g_option_context_free(context); + return run_tests != FALSE; +} === added file 'dracut-module/password-agent.xml' --- dracut-module/password-agent.xml 1970-01-01 00:00:00 +0000 +++ dracut-module/password-agent.xml 2019-07-27 10:11:45 +0000 @@ -0,0 +1,469 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + + Run Mandos client as a systemd password agent. + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + -- + + MANDOS_CLIENT + + OPTIONS + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + &COMMANDNAME; is a program which is meant to + be a systemd + 1 Password + Agent (See Password Agents). The aim of this program is therefore + to acquire and then send a password to some other program which + will use the password to unlock the encrypted root disk. + + + This program is not meant to be invoked directly, but can be in + order to test it. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + Specify a different agent directory. The default is + /run/systemd/ask-password as per the + Password Agents specification. + + + + + + + + + Specify a different helper directory. The default is + /lib/mandos/plugin-helpers, which + will exist in the initial RAM disk + environment. (This will simply be passed to the + MANDOS_CLIENT program via the + MANDOSPLUGINHELPERDIR environment variable. + See + mandos-client8mandos.) + + + + + + + + + Change real user ID to USERID + when running MANDOS_CLIENT. + The default is 65534. Note: This + must be a number, not a name. + + + + + + + + + Change real group ID to GROUPID + when running MANDOS_CLIENT. + The default is 65534. Note: This + must be a number, not a name. + + + + + + MANDOS_CLIENT + + + This specifies the file name for + mandos-client8mandos. If the + option is given, any + following options are passed to the MANDOS_CLIENT program. The default is + /lib/mandos/plugins.d/mandos-client + (which is the correct location for the initial + RAM disk environment) without any + options. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Ignore normal operation; instead only run self-tests. + Adding the option may show more + options possible in combination with + . + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + + + + + + OVERVIEW + + + This program, &COMMANDNAME;, will run on the client side in the + initial RAM disk environment, and is + responsible for getting a password from the Mandos client + program itself, and to send that password to whatever is + currently asking for a password using the systemd Password Agents mechanism. + + To accomplish this, &COMMANDNAME; runs the + mandos-client program (which is the actual + client program communicating with the Mandos server) or, + alternatively, any executable file specified as + MANDOS_CLIENT, and, as soon as a + password is acquired from the + MANDOS_CLIENT program, sends that + password (as per the Password Agents specification) to all currently + unanswered password questions. + + + This program should be started (normally as a systemd service, + which in turn is normally started by a systemd.path + 5 file) as a reaction to + files named ask.xxxx appearing in the agent directory + /run/systemd/ask-password + (or the directory specified by + ). + + + + + EXIT STATUS + + Exit status of this program is zero if no errors were + encountered, and otherwise not. + + + + + ENVIRONMENT + + This program does not use any environment variables itself, it + only passes on its environment to + MANDOS_CLIENT. Also, the + option will affect the + environment variable MANDOSPLUGINHELPERDIR for + MANDOS_CLIENT. + + + + + FILES + + + + /run/systemd/ask-password + + + The default directory to watch for password questions as + per the Password Agents specification; can be changed + by the option. + + + + + /lib/mandos/plugin-helpers + + + The helper directory as supplied to + MANDOS_CLIENT via the + MANDOSPLUGINHELPERDIR environment + variable; can be changed by the + option. + + + + + + + + + BUGS + + + + + EXAMPLE + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Run an alternative MANDOS_CLIENT + program:: + + + &COMMANDNAME; /usr/local/sbin/alternate + + + + + Use alternative locations for the helper directory and the + Mandos client, and add extra options suitable for running in + the normal file system: + + + + + &COMMANDNAME; --helper-directory=/usr/lib/x86_64-linux-gnu/mandos/plugin-helpers -- /usr/lib/x86_64-linux-gnu/mandos/plugins.d/mandos-client --pubkey=/etc/keys/mandos/pubkey.txt --seckey=/etc/keys/mandos/seckey.txt --tls-pubkey=/etc/keys/mandos/tls-pubkey.pem --tls-privkey=/etc/keys/mandos/tls-privkey.pem + + + + + + Use the default location for + mandos-client + 8mandos, but add many + options to it: + + + + +&COMMANDNAME; -- /lib/mandos/mandos-client --pubkey=/etc/mandos/keys/pubkey.txt --seckey=/etc/mandos/keys/seckey.txt --tls-pubkey=/etc/mandos/keys/tls-pubkey.pem --tls-privkey=/etc/mandos/keys/tls-privkey.pem + + + + + + Only run the self-tests: + + + &COMMANDNAME; --test + + + + + SECURITY + + This program will need to run as the root user in order to read + the agent directory and the ask.xxxx files + there, and will, when starting the Mandos client program, + require the ability to set the real user and + group ids to another user, by default user and group 65534, + which are assumed to be non-privileged. This is done in order + to match the expectations of mandos-client8mandos, which assumes that its executable file is + owned by the root user and also has the set-user-ID bit set (see + execve2). + + + + + SEE ALSO + + intro + 8mandos, + mandos-client + 8mandos, + systemd + 1, + + + + + Password Agents + + + + The specification for systemd Password + Agent programs, which + &COMMANDNAME; follows. + + + + + + +
+ + + + + === modified file 'initramfs-unpack' --- initramfs-unpack 2018-02-08 10:23:55 +0000 +++ initramfs-unpack 2019-07-27 10:11:45 +0000 @@ -2,8 +2,8 @@ # # Initramfs unpacker - unpacks initramfs images into /tmp # -# Copyright © 2013-2018 Teddy Hogeborn -# Copyright © 2013-2018 Björn Påhlsson +# Copyright © 2013-2019 Teddy Hogeborn +# Copyright © 2013-2019 Björn Påhlsson # # This file is part of Mandos. # @@ -43,25 +43,40 @@ if $cpio --quiet --list --file="$imgfile" >/dev/null 2>&1; then # Number of bytes to skip to get to the compressed archive skip=$(($(LANG=C $cpio --io-size=1 --list --file="$imgfile" 2>&1 \ - | sed --quiet --expression='s/^\([0-9]\+\) blocks$/\1/p')+8)) - catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer" + | sed --quiet \ + --expression='s/^\([0-9]\+\) blocks$/\1/p')+8)) + if [ -x /usr/lib/dracut/skipcpio ]; then + catimg="/usr/lib/dracut/skipcpio $imgfile" + else + catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer" + fi else + echo "No microcode detected" catimg="cat -- $imgfile" fi - # Determine the compression method - if { $catimg 2>/dev/null | zcat --test >/dev/null 2>&1; - [ ${PIPESTATUS[-1]} -eq 0 ]; }; then - decomp="zcat" - elif { $catimg 2>/dev/null | bzip2 --test >/dev/null 2>&1; - [ ${PIPESTATUS[-1]} -eq 0 ]; }; then - decomp="bzip2 --stdout --decompress" - elif { $catimg 2>/dev/null | lzop --test >/dev/null 2>&1; - [ ${PIPESTATUS[-1]} -eq 0 ]; }; then - decomp="lzop --stdout --decompress" - else - echo "Error: Could not determine type of $imgfile" >&2 - continue - fi + while :; do + # Determine the compression method + if { $catimg 2>/dev/null | zcat --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="zcat" + elif { $catimg 2>/dev/null | bzip2 --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="bzip2 --stdout --decompress" + elif { $catimg 2>/dev/null | lzop --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="lzop --stdout --decompress" + else + skip=$((${skip}+1)) + echo "Could not determine compression of ${imgfile}; trying to skip ${skip} bytes" >&2 + catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer" + continue + fi + break + done + case "$catimg" in + *skipcpio*) echo "Microcode detected, skipping";; + *) echo "Microcode detected, skipping ${skip} bytes";; + esac $catimg 2>/dev/null | $decomp | ( cd -- "$imgdir" && $cpio --quiet ) if [ ${PIPESTATUS[-1]} -eq 0 ]; then echo "$imgfile unpacked into $imgdir" === modified file 'intro.xml' --- intro.xml 2019-03-30 17:02:33 +0000 +++ intro.xml 2019-04-10 20:33:13 +0000 @@ -1,7 +1,7 @@ + %common; ]> @@ -131,8 +131,9 @@ So, at boot time, the Mandos client will ask for its encrypted - data over the network, decrypt it to get the password, use it to - decrypt the root file, and continue booting. + data over the network, decrypt the data to get the password, use + the password to decrypt the root file system, and the client can + then continue booting. Now, of course the initial RAM disk image is not on the === modified file 'mandos' --- mandos 2019-04-09 20:09:51 +0000 +++ mandos 2019-07-30 17:03:57 +0000 @@ -586,7 +586,7 @@ def __init__(self, socket, credentials=None): self._c_object = gnutls.session_t() gnutls_flags = gnutls.CLIENT - if gnutls.check_version("3.5.6"): + if gnutls.check_version(b"3.5.6"): gnutls_flags |= gnutls.NO_TICKETS if gnutls.has_rawpk: gnutls_flags |= gnutls.ENABLE_RAWPK @@ -794,7 +794,7 @@ ctypes.c_size_t)] openpgp_crt_get_fingerprint.restype = _error_code - if check_version("3.6.4"): + if check_version(b"3.6.4"): certificate_type_get2 = _library.gnutls_certificate_type_get2 certificate_type_get2.argtypes = [session_t, ctypes.c_int] certificate_type_get2.restype = _error_code @@ -821,9 +821,9 @@ approved: bool(); 'None' if not yet approved/disapproved approval_delay: datetime.timedelta(); Time to wait for approval approval_duration: datetime.timedelta(); Duration of one approval - checker: subprocess.Popen(); a running checker process used - to see if the client lives. - 'None' if no process is running. + checker: multiprocessing.Process(); a running checker process used + to see if the client lives. 'None' if no process is + running. checker_callback_tag: a GLib event source tag, or None checker_command: string; External command which is run to check if client lives. %() expansions are done at @@ -1036,11 +1036,12 @@ def checker_callback(self, source, condition, connection, command): """The checker has completed, so take appropriate actions.""" - self.checker_callback_tag = None - self.checker = None # Read return code from connection (see call_pipe) returncode = connection.recv() connection.close() + self.checker.join() + self.checker_callback_tag = None + self.checker = None if returncode >= 0: self.last_checker_status = returncode @@ -2287,7 +2288,7 @@ approval_required = False try: if gnutls.has_rawpk: - fpr = "" + fpr = b"" try: key_id = self.key_id( self.peer_certificate(session)) @@ -2297,7 +2298,7 @@ logger.debug("Key ID: %s", key_id) else: - key_id = "" + key_id = b"" try: fpr = self.fingerprint( self.peer_certificate(session)) @@ -2605,6 +2606,8 @@ raise # Only bind(2) the socket if we really need to. if self.server_address[0] or self.server_address[1]: + if self.server_address[1]: + self.allow_reuse_address = True if not self.server_address[0]: if self.address_family == socket.AF_INET6: any_address = "::" # in6addr_any @@ -3242,9 +3245,9 @@ for k in ("name", "host"): if isinstance(value[k], bytes): value[k] = value[k].decode("utf-8") - if not value.has_key("key_id"): + if "key_id" not in value: value["key_id"] = "" - elif not value.has_key("fingerprint"): + elif "fingerprint" not in value: value["fingerprint"] = "" # old_client_settings # .keys() === modified file 'mandos-ctl' --- mandos-ctl 2019-04-09 20:09:51 +0000 +++ mandos-ctl 2019-07-29 16:35:53 +0000 @@ -1,5 +1,5 @@ #!/usr/bin/python -# -*- mode: python; coding: utf-8; after-save-hook: (lambda () (let ((command (if (and (boundp 'tramp-file-name-structure) (string-match (car tramp-file-name-structure) (buffer-file-name))) (tramp-file-name-localname (tramp-dissect-file-name (buffer-file-name))) (buffer-file-name)))) (if (= (shell-command (format "%s --check" (shell-quote-argument command)) "*Test*") 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w)) (kill-buffer "*Test*")) (display-buffer "*Test*")))); -*- +# -*- after-save-hook: (lambda () (let ((command (if (fboundp 'file-local-name) (file-local-name (buffer-file-name)) (or (file-remote-p (buffer-file-name) 'localname) (buffer-file-name))))) (if (= (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (format "%s --check" (shell-quote-argument command)) nil "*Test*")) 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w))) (progn (with-current-buffer "*Test*" (compilation-mode)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); coding: utf-8 -*- # # Mandos Monitor - Control and monitor the Mandos server # @@ -250,22 +250,23 @@ def rfc3339_duration_to_delta(duration): """Parse an RFC 3339 "duration" and return a datetime.timedelta - >>> rfc3339_duration_to_delta("P7D") - datetime.timedelta(7) - >>> rfc3339_duration_to_delta("PT60S") - datetime.timedelta(0, 60) - >>> rfc3339_duration_to_delta("PT60M") - datetime.timedelta(0, 3600) - >>> rfc3339_duration_to_delta("P60M") - datetime.timedelta(1680) - >>> rfc3339_duration_to_delta("PT24H") - datetime.timedelta(1) - >>> rfc3339_duration_to_delta("P1W") - datetime.timedelta(7) - >>> rfc3339_duration_to_delta("PT5M30S") - datetime.timedelta(0, 330) - >>> rfc3339_duration_to_delta("P1DT3M20S") - datetime.timedelta(1, 200) + >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7) + True + >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60) + True + >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(hours=1) + True + >>> # 60 months + >>> rfc3339_duration_to_delta("P60M") == datetime.timedelta(1680) + True + >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1) + True + >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7) + True + >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330) + True + >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200) + True >>> # Can not be empty: >>> rfc3339_duration_to_delta("") Traceback (most recent call last): @@ -381,23 +382,23 @@ """Parse an interval string as documented by Mandos before 1.6.1, and return a datetime.timedelta - >>> parse_pre_1_6_1_interval('7d') - datetime.timedelta(7) - >>> parse_pre_1_6_1_interval('60s') - datetime.timedelta(0, 60) - >>> parse_pre_1_6_1_interval('60m') - datetime.timedelta(0, 3600) - >>> parse_pre_1_6_1_interval('24h') - datetime.timedelta(1) - >>> parse_pre_1_6_1_interval('1w') - datetime.timedelta(7) - >>> parse_pre_1_6_1_interval('5m 30s') - datetime.timedelta(0, 330) - >>> parse_pre_1_6_1_interval('') - datetime.timedelta(0) + >>> parse_pre_1_6_1_interval('7d') == datetime.timedelta(days=7) + True + >>> parse_pre_1_6_1_interval('60s') == datetime.timedelta(0, 60) + True + >>> parse_pre_1_6_1_interval('60m') == datetime.timedelta(hours=1) + True + >>> parse_pre_1_6_1_interval('24h') == datetime.timedelta(days=1) + True + >>> parse_pre_1_6_1_interval('1w') == datetime.timedelta(days=7) + True + >>> parse_pre_1_6_1_interval('5m 30s') == datetime.timedelta(0, 330) + True + >>> parse_pre_1_6_1_interval('') == datetime.timedelta(0) + True >>> # Ignore unknown characters, allow any order and repetitions - >>> parse_pre_1_6_1_interval('2dxy7zz11y3m5m') - datetime.timedelta(2, 480, 18000) + >>> parse_pre_1_6_1_interval('2dxy7zz11y3m5m') == datetime.timedelta(2, 480, 18000) + True """ @@ -1780,19 +1781,30 @@ self.assert_command_from_args(["--is-enabled", "client"], command.IsEnabled) - def assert_command_from_args(self, args, command_cls, - **cmd_attrs): + def assert_command_from_args(self, args, command_cls, length=1, + clients=None, **cmd_attrs): """Assert that parsing ARGS should result in an instance of COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS).""" options = self.parser.parse_args(args) check_option_syntax(self.parser, options) commands = commands_from_options(options) - self.assertEqual(1, len(commands)) - command = commands[0] - self.assertIsInstance(command, command_cls) + self.assertEqual(length, len(commands)) + for command in commands: + if isinstance(command, command_cls): + break + else: + self.assertIsInstance(command, command_cls) + if clients is not None: + self.assertEqual(clients, options.client) for key, value in cmd_attrs.items(): self.assertEqual(value, getattr(command, key)) + def assert_commands_from_args(self, args, commands, clients=None): + for cmd in commands: + self.assert_command_from_args(args, cmd, + length=len(commands), + clients=clients) + def test_is_enabled_short(self): self.assert_command_from_args(["-V", "client"], command.IsEnabled) @@ -1989,6 +2001,37 @@ verbose=True) + def test_manual_page_example_1(self): + self.assert_command_from_args("", + command.PrintTable, + clients=[], + verbose=False) + + def test_manual_page_example_2(self): + self.assert_command_from_args( + "--verbose foo1.example.org foo2.example.org".split(), + command.PrintTable, clients=["foo1.example.org", + "foo2.example.org"], + verbose=True) + + def test_manual_page_example_3(self): + self.assert_command_from_args("--enable --all".split(), + command.Enable, + clients=[]) + + def test_manual_page_example_4(self): + self.assert_commands_from_args( + ("--timeout=PT5M --interval=PT1M foo1.example.org" + " foo2.example.org").split(), + [command.SetTimeout, command.SetInterval], + clients=["foo1.example.org", "foo2.example.org"]) + + def test_manual_page_example_5(self): + self.assert_command_from_args("--approve --all".split(), + command.Approve, + clients=[]) + + class TestCommand(unittest.TestCase): """Abstract class for tests of command classes""" === modified file 'mandos-ctl.xml' --- mandos-ctl.xml 2019-03-08 23:55:34 +0000 +++ mandos-ctl.xml 2019-07-29 16:35:53 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -557,7 +557,11 @@ EXAMPLE + + To list all clients: @@ -567,6 +571,7 @@ + To list all settings for the clients named foo1.example.org and + To enable all clients: @@ -590,6 +596,7 @@ + To change timeout and interval value for the clients named foo1.example.org and -&COMMANDNAME; --timeout="PT5M" --interval="PT1M" foo1.example.org foo2.example.org +&COMMANDNAME; --timeout=PT5M --interval=PT1M foo1.example.org foo2.example.org + - To approve all clients currently waiting for it: + To approve all clients currently waiting for approval: &COMMANDNAME; --approve --all === modified file 'mandos-keygen' --- mandos-keygen 2019-04-09 20:09:51 +0000 +++ mandos-keygen 2019-07-17 23:37:13 +0000 @@ -381,7 +381,7 @@ while [ ! -s "$SECFILE" ]; do if [ -n "$PASSFILE" ]; then - cat "$PASSFILE" + cat -- "$PASSFILE" else tty --quiet && stty -echo echo -n "Enter passphrase: " >/dev/tty === modified file 'mandos-keygen.xml' --- mandos-keygen.xml 2019-02-10 04:20:26 +0000 +++ mandos-keygen.xml 2019-07-18 00:02:43 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -363,7 +363,9 @@ >8. The host name or the name specified with the option is used for the section header. All other options are ignored, - and no key is created. + and no key is created. Note: white space is stripped from + the beginning and from the end of the password; See . @@ -375,7 +377,8 @@ The same as , but read from - FILE, not the terminal. + FILE, not the terminal, and + white space is not stripped from the password in any way. @@ -489,6 +492,13 @@ BUGS + + The / option + strips white space from the start and from the end of the + password before using it. If this is a problem, use the + option instead, which does not do + this. + === modified file 'mandos-options.xml' --- mandos-options.xml 2019-02-09 23:23:26 +0000 +++ mandos-options.xml 2019-07-25 21:42:40 +0000 @@ -49,9 +49,10 @@ GnuTLS priority string for the TLS handshake. The default is - SECURE128​:!CTYPE-X.509​:+CTYPE-RAWPK​:!RSA​:!VERS-ALL​:+VERS-TLS1.3​:%PROFILE_ULTRA + + SECURE128​:!CTYPE-X.509​:+CTYPE-RAWPK​:!RSA​:!VERS-ALL​:+VERS-TLS1.3​:%PROFILE_ULTRA when using raw public keys in TLS, and - SECURE256​:!CTYPE-X.509​:+CTYPE-OPENPGP​:!RSA​:+SIGN-DSA-SHA256 + SECURE256​:!CTYPE-X.509​:+CTYPE-OPENPGP​:!RSA​:+SIGN-DSA-SHA256 when using OpenPGP keys in TLS,. See gnutls_priority_init 3 for the syntax. === modified file 'mandos-to-cryptroot-unlock' --- mandos-to-cryptroot-unlock 2018-08-19 14:58:40 +0000 +++ mandos-to-cryptroot-unlock 2019-07-24 11:02:24 +0000 @@ -61,7 +61,7 @@ # Keep running plugin-runner and trying any password, until either a # password is accepted by cryptroot-unlock, or plugin-runner fails, or # the file /run/mandos-keep-running has been removed. -while type cryptroot-unlock >/dev/null 2>&1; do +while command -v cryptroot-unlock >/dev/null 2>&1; do /lib/mandos/plugin-runner > "$passfile" & echo $! > /run/mandos-plugin-runner.pid wait %% || break === modified file 'mandos.conf.xml' --- mandos.conf.xml 2019-02-10 04:20:26 +0000 +++ mandos.conf.xml 2019-06-20 18:54:10 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/mandos.conf"> - + %common; ]> @@ -227,11 +227,11 @@ [DEFAULT] # A configuration example -interface = eth0 +interface = enp1s0 address = fe80::aede:48ff:fe71:f6f2 port = 1025 debug = True -priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA +priority = SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA:!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA servicename = Daena use_dbus = False use_ipv6 = True === modified file 'mandos.xml' --- mandos.xml 2019-02-10 04:20:26 +0000 +++ mandos.xml 2019-07-24 06:16:09 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -695,7 +695,7 @@ - Avahi + Avahi === modified file 'plugin-runner.c' --- plugin-runner.c 2019-04-08 21:53:22 +0000 +++ plugin-runner.c 2019-07-07 20:50:21 +0000 @@ -586,7 +586,11 @@ if(arg[0] == '\0'){ break; } - /* FALLTHROUGH */ +#if __GNUC__ >= 7 + __attribute__((fallthrough)); +#else + /* FALLTHROUGH */ +#endif default: return ARGP_ERR_UNKNOWN; } === modified file 'plugin-runner.xml' --- plugin-runner.xml 2019-02-10 04:20:26 +0000 +++ plugin-runner.xml 2019-07-25 22:44:36 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -628,14 +628,14 @@ Read a different configuration file, run plugins from a different directory, specify an alternate plugin helper - directory and add two options to the + directory and add four options to the mandos-client 8mandos plugin: -cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/x86_64-linux-gnu/mandos/plugins.d --plugin-helper-dir /usr/lib/x86_64-linux-gnu/mandos/plugin-helpers --options-for=mandos-client:--pubkey=pubkey.txt,--seckey=seckey.txt,--tls-pubkey=tls-pubkey.pem,--tls-privkey=tls-privkey.pem +cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/x86_64-linux-gnu/mandos/plugins.d --plugin-helper-dir /usr/lib/x86_64-linux-gnu/mandos/plugin-helpers --options-for=mandos-client:--pubkey=pubkey.txt,​--seckey=seckey.txt,​--tls-pubkey=tls-pubkey.pem,​--tls-privkey=tls-privkey.pem === modified file 'plugins.d/mandos-client.xml' --- plugins.d/mandos-client.xml 2019-02-10 04:20:26 +0000 +++ plugins.d/mandos-client.xml 2019-07-27 10:11:45 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -525,8 +525,8 @@ This environment variable will be assumed to contain the directory containing any helper executables. The use and - nature of these helper executables, if any, is - purposefully not documented. + nature of these helper executables, if any, is purposely + not documented. @@ -816,9 +816,9 @@ SECURITY - This program is set-uid to root, but will switch back to the - original (and presumably non-privileged) user and group after - bringing up the network interface. + This program assumes that it is set-uid to root, and will switch + back to the original (and presumably non-privileged) user and + group after bringing up the network interface. To use this program for its intended purpose (see - Avahi + Avahi === modified file 'plugins.d/password-prompt.c' --- plugins.d/password-prompt.c 2019-02-11 07:06:55 +0000 +++ plugins.d/password-prompt.c 2019-07-27 10:11:45 +0000 @@ -27,9 +27,10 @@ #include /* struct termios, tcsetattr(), TCSAFLUSH, tcgetattr(), ECHO */ -#include /* struct termios, tcsetattr(), - STDIN_FILENO, TCSAFLUSH, - tcgetattr(), ECHO, readlink() */ +#include /* access(), struct termios, + tcsetattr(), STDIN_FILENO, + TCSAFLUSH, tcgetattr(), ECHO, + readlink() */ #include /* sig_atomic_t, raise(), struct sigaction, sigemptyset(), sigaction(), sigaddset(), SIGINT, @@ -110,6 +111,10 @@ from the terminal. Password-prompt will exit if it detects plymouth since plymouth performs the same functionality. */ + if(access("/run/plymouth/pid", R_OK) == 0){ + return true; + } + __attribute__((nonnull)) int is_plymouth(const struct dirent *proc_entry){ int ret; @@ -234,6 +239,7 @@ struct termios t_new, t_old; char *buffer = NULL; char *prefix = NULL; + char *prompt = NULL; int status = EXIT_SUCCESS; struct sigaction old_action, new_action = { .sa_handler = termination_handler, @@ -243,6 +249,9 @@ { .name = "prefix", .key = 'p', .arg = "PREFIX", .flags = 0, .doc = "Prefix shown before the prompt", .group = 2 }, + { .name = "prompt", .key = 129, + .arg = "PROMPT", .flags = 0, + .doc = "The prompt to show", .group = 2 }, { .name = "debug", .key = 128, .doc = "Debug mode", .group = 3 }, /* @@ -261,12 +270,15 @@ error_t parse_opt (int key, char *arg, struct argp_state *state){ errno = 0; switch (key){ - case 'p': + case 'p': /* --prefix */ prefix = arg; break; - case 128: + case 128: /* --debug */ debug = true; break; + case 129: /* --prompt */ + prompt = arg; + break; /* * These reproduce what we would get without ARGP_NO_HELP */ @@ -427,7 +439,9 @@ if(prefix){ fprintf(stderr, "%s ", prefix); } - { + if(prompt != NULL){ + fprintf(stderr, "%s: ", prompt); + } else { const char *cryptsource = getenv("CRYPTTAB_SOURCE"); const char *crypttarget = getenv("CRYPTTAB_NAME"); /* Before cryptsetup 1.1.0~rc2 */ @@ -508,19 +522,23 @@ } if(sret < 0){ int e = errno; - if(errno != EINTR and not feof(stdin)){ - error_plus(0, errno, "getline"); - switch(e){ - case EBADF: - status = EX_UNAVAILABLE; - break; - case EIO: - case EINVAL: - default: - status = EX_IOERR; - break; + if(errno != EINTR){ + if(not feof(stdin)){ + error_plus(0, errno, "getline"); + switch(e){ + case EBADF: + status = EX_UNAVAILABLE; + break; + case EIO: + case EINVAL: + default: + status = EX_IOERR; + break; + } + break; + } else { + clearerr(stdin); } - break; } } /* if(sret == 0), then the only sensible thing to do is to retry === modified file 'plugins.d/password-prompt.xml' --- plugins.d/password-prompt.xml 2019-02-10 04:20:26 +0000 +++ plugins.d/password-prompt.xml 2019-07-27 10:11:45 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -69,6 +69,9 @@ >PREFIX + + + @@ -110,6 +113,15 @@ wrapper, although actual use of that function is not guaranteed or implied. + + This program tries to detect if a Plymouth daemon + (plymouthd8) + is running, by looking for a + /run/plymouth/pid file or a process named + plymouthd. If it is detected, + this process will immediately exit without doing anything. + @@ -138,6 +150,18 @@ + + + + The password prompt. Using this option will make this + program ignore the CRYPTTAB_SOURCE and + CRYPTTAB_NAME environment variables. + + + + + @@ -197,7 +221,8 @@ CRYPTTAB_NAME - If set, these environment variables will be assumed to + If set, and if the option is not + used, these environment variables will be assumed to contain the source device name and the target device mapper name, respectively, and will be shown as part of the prompt. @@ -205,22 +230,13 @@ These variables will normally be inherited from plugin-runner - 8mandos, which will - normally have inherited them from - /scripts/local-top/cryptroot in the - initial RAM disk environment, which will - have set them from parsing kernel arguments and - /conf/conf.d/cryptroot (also in the - initial RAM disk environment), which in turn will have been - created when the initial RAM disk image was created by - /usr/share/initramfs-tools/hooks/cryptroot, by - extracting the information of the root file system from - /etc/crypttab. + 8mandos, which might + have in turn inherited them from its calling process. This behavior is meant to exactly mirror the behavior of - askpass, the default password prompter. + askpass, the default password prompter + from initramfs-tools. @@ -301,13 +317,13 @@ SEE ALSO intro - 8mandos - crypttab - 5 + 8mandos, mandos-client - 8mandos + 8mandos, plugin-runner 8mandos, + plymouthd + 8
=== modified file 'plugins.d/plymouth.c' --- plugins.d/plymouth.c 2018-02-08 10:23:55 +0000 +++ plugins.d/plymouth.c 2019-07-27 10:11:45 +0000 @@ -2,8 +2,8 @@ /* * Plymouth - Read a password from Plymouth and output it * - * Copyright © 2010-2018 Teddy Hogeborn - * Copyright © 2010-2018 Björn Påhlsson + * Copyright © 2010-2019 Teddy Hogeborn + * Copyright © 2010-2019 Björn Påhlsson * * This file is part of Mandos. * @@ -53,8 +53,11 @@ #include /* TEMP_FAILURE_RETRY */ #include /* argz_count(), argz_extract() */ #include /* va_list, va_start(), ... */ +#include sig_atomic_t interrupted_by_signal = 0; +const char *argp_program_version = "plymouth " VERSION; +const char *argp_program_bug_address = ""; /* Used by Ubuntu 11.04 (Natty Narwahl) */ const char plymouth_old_old_pid[] = "/dev/.initramfs/plymouth.pid"; @@ -69,6 +72,7 @@ "--mode=boot", "--attach-to-session", NULL }; +bool debug = false; static void termination_handler(__attribute__((unused))int signum){ if(interrupted_by_signal){ @@ -77,6 +81,14 @@ interrupted_by_signal = 1; } +__attribute__((format (gnu_printf, 2, 3), nonnull)) +int fprintf_plus(FILE *stream, const char *format, ...){ + va_list ap; + va_start (ap, format); + fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name); + return vfprintf(stream, format, ap); +} + /* Function to use when printing errors */ __attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, @@ -159,11 +171,18 @@ __attribute__((nonnull (2, 3))) bool exec_and_wait(pid_t *pid_return, const char *path, - const char * const *argv, bool interruptable, + const char * const * const argv, bool interruptable, bool daemonize){ int status; int ret; pid_t pid; + if(debug){ + for(const char * const *arg = argv; *arg != NULL; arg++){ + fprintf_plus(stderr, "exec_and_wait arg: %s\n", *arg); + } + fprintf_plus(stderr, "exec_and_wait end of args\n"); + } + pid = fork(); if(pid == -1){ error_plus(0, errno, "fork"); @@ -209,12 +228,24 @@ and ((not interrupted_by_signal) or (not interruptable))); if(interrupted_by_signal and interruptable){ + if(debug){ + fprintf_plus(stderr, "Interrupted by signal\n"); + } return false; } if(ret == -1){ error_plus(0, errno, "waitpid"); return false; } + if(debug){ + if(WIFEXITED(status)){ + fprintf_plus(stderr, "exec_and_wait exited: %d\n", + WEXITSTATUS(status)); + } else if(WIFSIGNALED(status)) { + fprintf_plus(stderr, "exec_and_wait signaled: %d\n", + WTERMSIG(status)); + } + } if(WIFEXITED(status) and (WEXITSTATUS(status) == 0)){ return true; } @@ -407,17 +438,69 @@ int main(__attribute__((unused))int argc, __attribute__((unused))char **argv){ - char *prompt; + char *prompt = NULL; char *prompt_arg; pid_t plymouth_command_pid; int ret; bool bret; + { + struct argp_option options[] = { + { .name = "prompt", .key = 128, .arg = "PROMPT", + .doc = "The prompt to show" }, + { .name = "debug", .key = 129, + .doc = "Debug mode" }, + { .name = NULL } + }; + + __attribute__((nonnull(3))) + error_t parse_opt (int key, char *arg, __attribute__((unused)) + struct argp_state *state){ + errno = 0; + switch (key){ + case 128: /* --prompt */ + prompt = arg; + if(debug){ + fprintf_plus(stderr, "Custom prompt \"%s\"\n", prompt); + } + break; + case 129: /* --debug */ + debug = true; + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, .parser = parse_opt, + .args_doc = "", + .doc = "Mandos plymouth -- Read and" + " output a password" }; + ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error_plus(0, errno, "argp_parse"); + return EX_OSERR; + case EINVAL: + error_plus(0, errno, "argp_parse"); + return EX_USAGE; + } + } + /* test -x /bin/plymouth */ ret = access(plymouth_path, X_OK); if(ret == -1){ /* Plymouth is probably not installed. Don't print an error message, just exit. */ + if(debug){ + fprintf_plus(stderr, "Plymouth (%s) not found\n", + plymouth_path); + } exit(EX_UNAVAILABLE); } @@ -457,17 +540,27 @@ } /* Plymouth is probably not running. Don't print an error message, just exit. */ + if(debug){ + fprintf_plus(stderr, "Plymouth not running\n"); + } exit(EX_UNAVAILABLE); } - prompt = makeprompt(); - ret = asprintf(&prompt_arg, "--prompt=%s", prompt); - free(prompt); + if(prompt != NULL){ + ret = asprintf(&prompt_arg, "--prompt=%s", prompt); + } else { + char *made_prompt = makeprompt(); + ret = asprintf(&prompt_arg, "--prompt=%s", made_prompt); + free(made_prompt); + } if(ret == -1){ error_plus(EX_OSERR, errno, "asprintf"); } /* plymouth ask-for-password --prompt="$prompt" */ + if(debug){ + fprintf_plus(stderr, "Prompting for password via Plymouth\n"); + } bret = exec_and_wait(&plymouth_command_pid, plymouth_path, (const char *[]) { plymouth_path, "ask-for-password", === modified file 'plugins.d/plymouth.xml' --- plugins.d/plymouth.xml 2019-02-10 04:20:26 +0000 +++ plugins.d/plymouth.xml 2019-07-27 10:11:45 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -61,6 +61,28 @@ &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + @@ -102,8 +124,68 @@ OPTIONS - This program takes no options. + This program is commonly not invoked from the command line; it + is normally started by the Mandos + plugin runner, see plugin-runner8mandos + . Any command line options this program accepts + are therefore normally provided by the plugin runner, and not + directly. + + + + + + + The password prompt. Note that using this option will + make this program ignore the cryptsource + and crypttarget environment variables. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + + @@ -125,7 +207,8 @@ crypttarget - If set, these environment variables will be assumed to + If set, and if the option is not + used, these environment variables will be assumed to contain the source device name and the target device mapper name, respectively, and will be shown as part of the prompt. @@ -133,22 +216,13 @@ These variables will normally be inherited from plugin-runner - 8mandos, which will - normally have inherited them from - /scripts/local-top/cryptroot in the - initial RAM disk environment, which will - have set them from parsing kernel arguments and - /conf/conf.d/cryptroot (also in the - initial RAM disk environment), which in turn will have been - created when the initial RAM disk image was created by - /usr/share/initramfs-tools/hooks/cryptroot, by - extracting the information of the root file system from - /etc/crypttab. + 8mandos, which might + have in turn inherited them from its calling process. This behavior is meant to exactly mirror the behavior of - askpass, the default password prompter. + askpass, the default password prompter + from initramfs-tools. @@ -221,12 +295,20 @@ - This program takes no options. + Normal invocation needs no options: &COMMANDNAME; + + + Show a different prompt. + + + &COMMANDNAME; --prompt=Password + + @@ -272,8 +354,6 @@ intro 8mandos, - crypttab - 5, plugin-runner 8mandos, proc === modified file 'plugins.d/splashy.xml' --- plugins.d/splashy.xml 2019-02-10 04:20:26 +0000 +++ plugins.d/splashy.xml 2019-07-27 10:11:45 +0000 @@ -135,18 +135,8 @@ These variables will normally be inherited from plugin-runner - 8mandos, which will - normally have inherited them from - /scripts/local-top/cryptroot in the - initial RAM disk environment, which will - have set them from parsing kernel arguments and - /conf/conf.d/cryptroot (also in the - initial RAM disk environment), which in turn will have been - created when the initial RAM disk image was created by - /usr/share/initramfs-tools/hooks/cryptroot, by - extracting the information of the root file system from - /etc/crypttab. + 8mandos, which might + have in turn inherited them from its calling process. This behavior is meant to exactly mirror the behavior of @@ -276,8 +266,6 @@ intro 8mandos, - crypttab - 5, plugin-runner 8mandos, proc === modified file 'plugins.d/usplash.xml' --- plugins.d/usplash.xml 2019-02-10 04:20:26 +0000 +++ plugins.d/usplash.xml 2019-07-27 10:11:45 +0000 @@ -135,18 +135,8 @@ These variables will normally be inherited from plugin-runner - 8mandos, which will - normally have inherited them from - /scripts/local-top/cryptroot in the - initial RAM disk environment, which will - have set them from parsing kernel arguments and - /conf/conf.d/cryptroot (also in the - initial RAM disk environment), which in turn will have been - created when the initial RAM disk image was created by - /usr/share/initramfs-tools/hooks/cryptroot, by - extracting the information of the root file system from - /etc/crypttab. + 8mandos, which might + have in turn inherited them from its calling process. This behavior is meant to exactly mirror the behavior of @@ -290,8 +280,6 @@ intro 8mandos, - crypttab - 5, fifo 7, plugin-runner